diff --git a/packages/studio/src/components/editor/DomEditOverlay.tsx b/packages/studio/src/components/editor/DomEditOverlay.tsx
index 4edd7a3b0f..6408546adf 100644
--- a/packages/studio/src/components/editor/DomEditOverlay.tsx
+++ b/packages/studio/src/components/editor/DomEditOverlay.tsx
@@ -61,7 +61,7 @@ export interface DomEditOverlayProps {
hoverSelection: DomEditSelection | null;
allowCanvasMovement?: boolean;
allowBodyDrag?: boolean;
- /** "host": no hover, marquee, box re-select, or body drag if allowBodyDrag is false; Enter still opens text. */
+ /** "host": no marquee, box re-select, or body drag if allowBodyDrag is false; Enter still opens text. */
canvasInput?: "overlay" | "host";
onTextEditingChange?: (editing: boolean) => void;
/** 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({
onLostPointerCapture={gestures.onLostPointerCapture}
onContextMenu={hostInput ? undefined : handleContextMenu}
>
- {!hostInput && hoverSelection && hoverRect && compRect.width > 0 && (
+ {hoverSelection && hoverRect && compRect.width > 0 && (
{
expect(fixture(HOST).overlay.className).toContain("pointer-events-none");
});
- it("draws no hover box", () => {
+ it("draws the hover box for the selection the host hovers, and none without one", () => {
layout.hover = RECT;
const hovered = (props = {}) => {
const { overlay } = fixture(props);
@@ -321,7 +321,8 @@ describe("DomEditOverlay with canvasInput host", () => {
};
const selection = makeSelection("Hover", textElement("hover"));
expect(hovered({ hoverSelection: selection })).not.toBeNull();
- expect(hovered({ ...HOST, hoverSelection: selection })).toBeNull();
+ expect(hovered({ ...HOST, hoverSelection: selection })).not.toBeNull();
+ expect(hovered({ ...HOST, hoverSelection: null })).toBeNull();
});
it("starts no marquee and makes no selection from a press on empty canvas", () => {
diff --git a/packages/studio/src/components/nle/NLEPreview.test.ts b/packages/studio/src/components/nle/NLEPreview.test.ts
index 25814a8ff1..6f61f8e671 100644
--- a/packages/studio/src/components/nle/NLEPreview.test.ts
+++ b/packages/studio/src/components/nle/NLEPreview.test.ts
@@ -12,6 +12,12 @@ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
const playerMounts: string[] = [];
let livePlayerProps: { onReadyToShowChange?: (ready: boolean) => void } = {};
+const measured = vi.hoisted(() => ({ size: null as { width: number; height: number } | null }));
+vi.mock("../../utils/previewCompositionSize", async (original) => ({
+ ...(await original
()),
+ readPreviewCompositionSize: () => measured.size,
+}));
+
vi.mock("../../player", async () => {
const React = await import("react");
@@ -81,7 +87,12 @@ function renderPreview(
{
box = { width: 800, height: 600 },
fillBox,
- }: { box?: { width: number; height: number }; fillBox?: boolean } = {},
+ compositionSizeHint,
+ }: {
+ box?: { width: number; height: number };
+ fillBox?: boolean;
+ compositionSizeHint?: { width: number; height: number };
+ } = {},
) {
resizeCallbacks = [];
const host = document.createElement("div");
@@ -103,6 +114,7 @@ function renderPreview(
setShadowIframeNode: () => {},
resetPreviewSlots: () => {},
fillBox,
+ compositionSizeHint,
}),
);
});
@@ -378,6 +390,37 @@ describe("NLEPreview", () => {
filled.cleanup();
});
+ it("takes the host's composition size as the stage's shape until the preview measures its own", () => {
+ const box = { width: 800, height: 600 };
+ const plain = renderPreview(undefined, { box });
+ expect([plain.stage.style.width, plain.stage.style.height]).toEqual(["784px", "441px"]);
+ plain.cleanup();
+
+ const hinted = renderPreview(undefined, {
+ box,
+ compositionSizeHint: { width: 1276, height: 1078 },
+ });
+ const shape = parseFloat(hinted.stage.style.width) / parseFloat(hinted.stage.style.height);
+ expect(shape).toBeCloseTo(1276 / 1078, 3);
+ expect(hinted.stage.style.height).toBe("584px");
+ hinted.cleanup();
+ });
+
+ it("lets the size the preview measures win over the host's hint", () => {
+ measured.size = { width: 1920, height: 1080 };
+ try {
+ const box = { width: 800, height: 600 };
+ const hinted = renderPreview(undefined, {
+ box,
+ compositionSizeHint: { width: 1276, height: 1078 },
+ });
+ expect([hinted.stage.style.width, hinted.stage.style.height]).toEqual(["784px", "441px"]);
+ hinted.cleanup();
+ } finally {
+ measured.size = null;
+ }
+ });
+
it("clips a shadow reload so its own loading overlay cannot paint over the live frame", () => {
const view = renderPreview([
{ gen: 0, role: "live" },
diff --git a/packages/studio/src/components/nle/NLEPreview.tsx b/packages/studio/src/components/nle/NLEPreview.tsx
index 1b4ad5ce11..e41c77e7c2 100644
--- a/packages/studio/src/components/nle/NLEPreview.tsx
+++ b/packages/studio/src/components/nle/NLEPreview.tsx
@@ -50,6 +50,7 @@ interface NLEPreviewProps extends Pick<
onCompositionSizeChange?: (size: PreviewCompositionSize | null) => void;
/** Draws the picture edge to edge in this box, without Studio's inset band. */
fillBox?: boolean;
+ compositionSizeHint?: PreviewCompositionSize;
}
export function getPreviewPlayerKey({
@@ -127,6 +128,7 @@ export const NLEPreview = memo(function NLEPreview({
onStageRef,
onCompositionSizeChange,
fillBox,
+ compositionSizeHint,
}: NLEPreviewProps) {
const activeKey = getPreviewPlayerKey({ projectId, directUrl });
const viewportRef = useRef(null);
@@ -151,6 +153,8 @@ export const NLEPreview = memo(function NLEPreview({
const gutterPx = usePreviewGuidesStore((s) => (s.rulerVisible ? RULER_GUTTER_PX : 0));
const insetPx = fillBox ? 0 : PREVIEW_STAGE_INSET_PX;
const [stageSize, setStageSize] = useState(() => resolvePreviewStageSize(0, 0, null, portrait));
+ const hintWidth = compositionSizeHint?.width;
+ const hintHeight = compositionSizeHint?.height;
const zoomRef = useRef(DEFAULT_PREVIEW_ZOOM);
const [settledZoom, setSettledZoom] = useState(DEFAULT_PREVIEW_ZOOM);
@@ -183,7 +187,8 @@ export const NLEPreview = memo(function NLEPreview({
resolvePreviewStageSize(
rect.width,
rect.height,
- compositionSize,
+ compositionSize ??
+ (hintWidth && hintHeight ? { width: hintWidth, height: hintHeight } : null),
portrait,
gutterPx,
insetPx,
@@ -195,7 +200,7 @@ export const NLEPreview = memo(function NLEPreview({
const observer = new ResizeObserver(updateStageSize);
observer.observe(viewport);
return () => observer.disconnect();
- }, [compositionSize, portrait, gutterPx, insetPx]);
+ }, [compositionSize, hintWidth, hintHeight, portrait, gutterPx, insetPx]);
const onCompositionSizeChangeRef = useRef(onCompositionSizeChange);
onCompositionSizeChangeRef.current = onCompositionSizeChange;