Skip to content

Commit 1e711b0

Browse files
feat(studio): a host can show the hover box and give the stage its starting size (#5116)
* test(studio): pin a host's hover box and a starting stage size * feat(studio): a host can show the hover box and give the stage its starting size * fix(studio): keep the comment share flat and let a measured size win over the hint
1 parent 17c699d commit 1e711b0

4 files changed

Lines changed: 56 additions & 7 deletions

File tree

‎packages/studio/src/components/editor/DomEditOverlay.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export interface DomEditOverlayProps {
6161
hoverSelection: DomEditSelection | null;
6262
allowCanvasMovement?: boolean;
6363
allowBodyDrag?: boolean;
64-
/** "host": no hover, marquee, box re-select, or body drag if allowBodyDrag is false; Enter still opens text. */
64+
/** "host": no marquee, box re-select, or body drag if allowBodyDrag is false; Enter still opens text. */
6565
canvasInput?: "overlay" | "host";
6666
onTextEditingChange?: (editing: boolean) => void;
6767
/** A click on a single selection's box, in either mode; the event may be the pointerup. */
@@ -500,7 +500,7 @@ export const DomEditOverlay = memo(function DomEditOverlay({
500500
onLostPointerCapture={gestures.onLostPointerCapture}
501501
onContextMenu={hostInput ? undefined : handleContextMenu}
502502
>
503-
{!hostInput && hoverSelection && hoverRect && compRect.width > 0 && (
503+
{hoverSelection && hoverRect && compRect.width > 0 && (
504504
<div
505505
aria-hidden="true"
506506
data-dom-edit-hover-box="true"

‎packages/studio/src/components/editor/previewReadOnly.overlay.test.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ describe("DomEditOverlay with canvasInput host", () => {
311311
expect(fixture(HOST).overlay.className).toContain("pointer-events-none");
312312
});
313313

314-
it("draws no hover box", () => {
314+
it("draws the hover box for the selection the host hovers, and none without one", () => {
315315
layout.hover = RECT;
316316
const hovered = (props = {}) => {
317317
const { overlay } = fixture(props);
@@ -321,7 +321,8 @@ describe("DomEditOverlay with canvasInput host", () => {
321321
};
322322
const selection = makeSelection("Hover", textElement("hover"));
323323
expect(hovered({ hoverSelection: selection })).not.toBeNull();
324-
expect(hovered({ ...HOST, hoverSelection: selection })).toBeNull();
324+
expect(hovered({ ...HOST, hoverSelection: selection })).not.toBeNull();
325+
expect(hovered({ ...HOST, hoverSelection: null })).toBeNull();
325326
});
326327

327328
it("starts no marquee and makes no selection from a press on empty canvas", () => {

‎packages/studio/src/components/nle/NLEPreview.test.ts‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
1212
const playerMounts: string[] = [];
1313
let livePlayerProps: { onReadyToShowChange?: (ready: boolean) => void } = {};
1414

15+
const measured = vi.hoisted(() => ({ size: null as { width: number; height: number } | null }));
16+
vi.mock("../../utils/previewCompositionSize", async (original) => ({
17+
...(await original<typeof import("../../utils/previewCompositionSize")>()),
18+
readPreviewCompositionSize: () => measured.size,
19+
}));
20+
1521
vi.mock("../../player", async () => {
1622
const React = await import("react");
1723

@@ -81,7 +87,12 @@ function renderPreview(
8187
{
8288
box = { width: 800, height: 600 },
8389
fillBox,
84-
}: { box?: { width: number; height: number }; fillBox?: boolean } = {},
90+
compositionSizeHint,
91+
}: {
92+
box?: { width: number; height: number };
93+
fillBox?: boolean;
94+
compositionSizeHint?: { width: number; height: number };
95+
} = {},
8596
) {
8697
resizeCallbacks = [];
8798
const host = document.createElement("div");
@@ -103,6 +114,7 @@ function renderPreview(
103114
setShadowIframeNode: () => {},
104115
resetPreviewSlots: () => {},
105116
fillBox,
117+
compositionSizeHint,
106118
}),
107119
);
108120
});
@@ -378,6 +390,37 @@ describe("NLEPreview", () => {
378390
filled.cleanup();
379391
});
380392

393+
it("takes the host's composition size as the stage's shape until the preview measures its own", () => {
394+
const box = { width: 800, height: 600 };
395+
const plain = renderPreview(undefined, { box });
396+
expect([plain.stage.style.width, plain.stage.style.height]).toEqual(["784px", "441px"]);
397+
plain.cleanup();
398+
399+
const hinted = renderPreview(undefined, {
400+
box,
401+
compositionSizeHint: { width: 1276, height: 1078 },
402+
});
403+
const shape = parseFloat(hinted.stage.style.width) / parseFloat(hinted.stage.style.height);
404+
expect(shape).toBeCloseTo(1276 / 1078, 3);
405+
expect(hinted.stage.style.height).toBe("584px");
406+
hinted.cleanup();
407+
});
408+
409+
it("lets the size the preview measures win over the host's hint", () => {
410+
measured.size = { width: 1920, height: 1080 };
411+
try {
412+
const box = { width: 800, height: 600 };
413+
const hinted = renderPreview(undefined, {
414+
box,
415+
compositionSizeHint: { width: 1276, height: 1078 },
416+
});
417+
expect([hinted.stage.style.width, hinted.stage.style.height]).toEqual(["784px", "441px"]);
418+
hinted.cleanup();
419+
} finally {
420+
measured.size = null;
421+
}
422+
});
423+
381424
it("clips a shadow reload so its own loading overlay cannot paint over the live frame", () => {
382425
const view = renderPreview([
383426
{ gen: 0, role: "live" },

‎packages/studio/src/components/nle/NLEPreview.tsx‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ interface NLEPreviewProps extends Pick<
5050
onCompositionSizeChange?: (size: PreviewCompositionSize | null) => void;
5151
/** Draws the picture edge to edge in this box, without Studio's inset band. */
5252
fillBox?: boolean;
53+
compositionSizeHint?: PreviewCompositionSize;
5354
}
5455

5556
export function getPreviewPlayerKey({
@@ -127,6 +128,7 @@ export const NLEPreview = memo(function NLEPreview({
127128
onStageRef,
128129
onCompositionSizeChange,
129130
fillBox,
131+
compositionSizeHint,
130132
}: NLEPreviewProps) {
131133
const activeKey = getPreviewPlayerKey({ projectId, directUrl });
132134
const viewportRef = useRef<HTMLDivElement>(null);
@@ -151,6 +153,8 @@ export const NLEPreview = memo(function NLEPreview({
151153
const gutterPx = usePreviewGuidesStore((s) => (s.rulerVisible ? RULER_GUTTER_PX : 0));
152154
const insetPx = fillBox ? 0 : PREVIEW_STAGE_INSET_PX;
153155
const [stageSize, setStageSize] = useState(() => resolvePreviewStageSize(0, 0, null, portrait));
156+
const hintWidth = compositionSizeHint?.width;
157+
const hintHeight = compositionSizeHint?.height;
154158

155159
const zoomRef = useRef<PreviewZoomState>(DEFAULT_PREVIEW_ZOOM);
156160
const [settledZoom, setSettledZoom] = useState<PreviewZoomState>(DEFAULT_PREVIEW_ZOOM);
@@ -183,7 +187,8 @@ export const NLEPreview = memo(function NLEPreview({
183187
resolvePreviewStageSize(
184188
rect.width,
185189
rect.height,
186-
compositionSize,
190+
compositionSize ??
191+
(hintWidth && hintHeight ? { width: hintWidth, height: hintHeight } : null),
187192
portrait,
188193
gutterPx,
189194
insetPx,
@@ -195,7 +200,7 @@ export const NLEPreview = memo(function NLEPreview({
195200
const observer = new ResizeObserver(updateStageSize);
196201
observer.observe(viewport);
197202
return () => observer.disconnect();
198-
}, [compositionSize, portrait, gutterPx, insetPx]);
203+
}, [compositionSize, hintWidth, hintHeight, portrait, gutterPx, insetPx]);
199204

200205
const onCompositionSizeChangeRef = useRef(onCompositionSizeChange);
201206
onCompositionSizeChangeRef.current = onCompositionSizeChange;

0 commit comments

Comments
 (0)