Skip to content

feat(studio): a host can show the hover box and give the stage its starting size - #5116

Merged
miguel-heygen merged 3 commits into
mainfrom
dstackctx/host-hover-and-start-size
Oct 7, 2026
Merged

miguel-heygen merged 3 commits into
mainfrom
dstackctx/host-hover-and-start-size

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Oct 6, 2026 •

Copy link
Copy Markdown
Collaborator

What

Two small additions for a host app that embeds Studio's preview outside EditorShell:

  • The hover box in host mode. DomEditOverlay with canvasInput="host" now draws its hover box when it is given a hover selection. A host turns it on with showHoverSelection on ConnectedDomEditOverlay and feeds the hover through the edit session (handlePreviewCanvasPointerMove), so it gets Studio's own hover box instead of drawing a copy.
  • A starting stage size. NLEPreview takes an optional compositionSizeHint: { width, height }. Until the preview or its poster measures the composition, the stage takes that shape on first mount, so a host that already knows the size gets no 16:9 frame that then snaps to the real shape.

Studio's own app passes neither, so nothing changes there.

Why

A host that wanted a hover box had to draw and position its own, because host mode skipped Studio's. Its own box follows the pointer, not Studio's stage transform, so it goes stale when the preview zooms or pans with a still mouse. And a host that knows the composition's size (from the project) could not keep the stage at that shape on first load: the stage started 16:9 and jumped.

Related work

Refs #4640, which added host mode (canvasInput="host") and turned the hover box off in it along with the marquee and context menu. The overlay already gates hover on showHoverSelection, so host mode does not need a second gate.

Kept small on purpose: these are the only two changes a host needs from Studio for this, and no other open change in this repo carries them.

How

  • DomEditOverlay.tsx: the hover box renders on hoverSelection alone; the !hostInput condition is gone. In host mode the box is still pointer-events-none, so it takes no presses.
  • NLEPreview.tsx: the stage size uses the measured compositionSize, else the hint, else the default; the effect depends on the hint's width and height, so a host passing a new object each render re-subscribes nothing. onCompositionSizeChange still reports only measured sizes, and a measured or poster size always wins over the hint.

Before

A small host page outside EditorShell (a test harness, not part of this PR), on main. It mounts NLEPreview inside PreviewReadOnlyProvider and DomEditProvider from the package entry, with ConnectedDomEditOverlay canvasInput="host" showHoverSelection over it and a host pointer layer under it that calls the session's handlePreviewCanvasPointerMove. The fixture is a 1276 x 1078 composition built for this capture.

Before: the session holds #title, but no hover box is drawn
Before: the preview held back 3 s, the stage is 16:9
Before: the preview loaded, the stage has snapped to 1276 x 1078

After

The same page on this branch, passing compositionSizeHint={{ width: 1276, height: 1078 }}.

After: Studio's hover box hugs the title
After: the preview held back 3 s, the stage already has the composition's shape

Studio unchanged

Studio itself on main and on this branch, pointer resting on the title: the title, hover box and stage rects are identical.

Studio on main: hover
Studio on this branch: hover

Test plan

  • Unit tests added/updated

  • Manual testing performed

  • Documentation updated (if applicable)

  • Comments follow CONTRIBUTING.md "Comments": they say why, not what, and a bug fix says what the code must do and how to reproduce the bug

  • previewReadOnly.overlay.test.tsx: host mode now draws the hover box for the selection the host hovers, and none without one (the old test asserted no box in host mode). NLEPreview.test.ts: a 1276 x 1078 hint shapes the stage before the preview measures (584 px tall in an 800 x 600 box), while no hint keeps 784 x 441. Both fail with the two source files put back to main and pass on this branch. A third test: a measured 1920 x 1080 composition wins over a 1276 x 1078 hint (fails with the two sizes swapped).

  • Those two files plus ConnectedDomEditOverlay.test.tsx: 56 tests, 3 runs in a row. Studio typecheck, oxlint and oxfmt are clean on the changed files.

  • Host captures: headless Chrome 147 against Studio's dev server, 1440 x 900, the same scripted steps on main and on this branch. The first-load shots hold both the preview document and the poster for 3 s, since either can measure the composition. Measured: on main the stage is 1424 x 801 during the delay and 999.02 x 844 after; on this branch 999.02 x 844 both times (999.02 / 844 = 1276 / 1078). The hover box on the branch is within 0.02 px of the title's rect.

  • Studio captures: main and branch give identical rects; the pixels that differ (up to 3/255 per channel) are all in the player bar and timeline strip, and two runs of the branch differ by the same amount there.

What I did NOT exercise

  • A host that changes compositionSizeHint while the preview is loading.
  • Hover in host mode during playback or inline text editing: the host owns those gates (Studio's own preview hides hover while playing in PreviewOverlays, which a host outside EditorShell does not mount), for example by turning showHoverSelection off while playing and while onTextEditingChange reports editing.
  • A host that switches one NLEPreview to another composition: the hint shapes the stage on first mount; after a switch the stage keeps the previous measured size until the new preview loads, as it does in Studio today.

@github-actions

github-actions Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Edit accuracy: accurate 2055 (base branch 2055), smooth 1566 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (0)

Unstable (2)

  • crop-scale-px-r30-nested-z200-after: tracking 0.02, pressJump 0, drop 40.03, reload 40.02, render 0.23, renderKey 0.23, undo false, teleport true / tracking 0.02, pressJump 0, drop 0.03, reload 0.04, render 0.23, renderKey 0.23, undo true, teleport true / tracking 0.02, pressJump 0, drop 0.03, reload 0.04, render 0.23, renderKey 0.23, undo true, teleport true
  • crop-none-center-r30-root-z100: tracking 0.04, pressJump 0, drop 40.08, reload 40.08, render 34.74, renderKey -, undo true, teleport true / tracking 0.04, pressJump 0, drop 0.08, reload 0.08, render 0.26, renderKey -, undo true, teleport true / tracking 0.04, pressJump 0, drop 0.08, reload 0.08, render 0.26, renderKey -, undo true, teleport true

@miguel-heygen
miguel-heygen marked this pull request as ready for review October 6, 2026 16:02

@jrusso1020 jrusso1020 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at this head.

Hover box. Removing !hostInput leaves the hover box controlled by one existing switch, ConnectedDomEditOverlay's showHoverSelection ? domEditHoverSelection : null (ConnectedDomEditOverlay.tsx:59), and that is the right place for it. Studio's own PreviewOverlays never passes canvasInput="host", so Studio is unaffected. I also checked the existing host-mode caller I know of outside this repo, which passes showHoverSelection={false}. It keeps drawing no box, so no current host gets a second box on top of its own.

Size hint. The order measured size, then hint, then default is right, and splitting the hint into hintWidth/hintHeight for the effect's dependencies avoids re-subscribing the ResizeObserver when a host passes a new object every render. The truthiness check also drops a 0 or missing dimension instead of passing it to resolvePreviewStageSize.

The three changed test files pass locally (56/56).

Nit (optional): compositionSizeHint is the only prop in NLEPreviewProps without a doc comment. One line would help a host find it: "used until the preview measures; a measured size always wins".

Verdict: APPROVE
Reasoning: These are two small host-only additions with no change to Studio's own paths, and tests cover both the hint and the measured-wins order.

— Rames

@miguel-heygen
miguel-heygen added this pull request to the merge queue Oct 7, 2026
Merged via the queue into main with commit 1e711b0 Oct 7, 2026
169 of 170 checks passed
@miguel-heygen
miguel-heygen deleted the dstackctx/host-hover-and-start-size branch October 7, 2026 09:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants