Skip to content

feat(ribbon): preview drops on another display like on a single one - #153

Merged
qeude merged 5 commits into
mainfrom
feat/cross-display-drag-preview
Oct 24, 2026
Merged

qeude merged 5 commits into
mainfrom
feat/cross-display-drag-preview

Conversation

@qeude

@qeude qeude commented Oct 24, 2026 •

Copy link
Copy Markdown
Owner

Closes #95. Supersedes #147.

Builds on #152 (merged).

Why

On a single display, a drag shows the drop before release: the ribbon makes room and a landing zone marks the slot. Across displays, there was no preview. The overlay of the target display stayed down, the landing zone stayed on the source display, and the carried picture was cut at the source display's edge. On release, the window landed at the end of the other ribbon.

What changed

  • Drop target on another display (from feat: drop dragged windows at the gap under the pointer on any display #147). MouseDropTarget.display(MonitorID, workspaceID:gap:) targets the gap under the pointer in another display's active workspace. applyMouseDrop revalidates it and inserts the window there. A 24 pt hysteresis keeps a pointer resting on a boundary from flipping the target.
  • Preview frames for every display. mouseDropPreviewFrames moves to DefiRuntime and lays out every display's active workspace in the previewed state. The source display closes the gap, and the target display opens the slot.
  • Hosting display. RibbonProxyPresenter.host puts the target display's ribbon on its overlay (RibbonProxyDrag.hosting). Its windows make room, the landing zone shows there, and a copy of the carried picture follows the pointer above the native window hidden below. The landing zone shows only on the display the drop lands on.
  • Pointer track guests. RibbonProxyPointerTrack moves the carried copy on each hosting display with the same grab offset, in the same display-refresh tick.
  • Retry. A hosting display whose overlay is not ready yet retries on the next drag event, at most every 100 ms.
  • Release. The hosting copy glides into its slot. The existing cross-display landing takes over from there: the source drops its picture, and the target lands. Coming back to the source display or releasing elsewhere lands both overlays.

Scope

Blast radius

  • Sources/DefiMacOS/RibbonProxyRenderer.swift: new hosting drag state, host, showSlot, and pointer-track guests. Carry, stretch and single-display preview behave as before.
  • Sources/DefiMacOS/MacOSPlatform.swift: hostWindowProxyPreview. The carry window list is factored into windowProxyCarryWindows.
  • Sources/DefiRuntime/MouseReordering.swift, Sources/DefiDaemon/DaemonDesktopSynchronization.swift: target and preview frames.
  • No new Accessibility writes. Preview frames are still computed only when the target changes. The hosting check on drag events does a monitor lookup and nothing else once a display hosts.

Verification

  • python3 script/verify.py local on the final head: passed. New MouseReorderingTests case: the preview closes the gap on the source and opens the slot on the target.
  • verify.py desktop, full DesktopE2ETests, two displays, Accessibility available: one failure, testSettingsSearchRevealsOffscreenOptionAgain (native focus of a fixture). It passed when rerun alone. Session restoration passed.
  • Real two-display drags on the installed build (5K plus built-in, staircase active), fixture windows, synthetic CGEvent drags with human pacing:
    • Into the gap between two columns on the other display: the target display hosted, the slot moved from gap 0 to gap 1, and the window landed at gap 1. Membership was unchanged while held. Held-state screenshots show the ribbon making room and the landing zone.
    • Over to the other display and back, then release: no change, and both overlays landed.
    • One defi-daemon remained after each run.
  • Not exercised: three or more displays.

Note

Show ribbon overlay and window-proxy previews on every display

Reworks the ribbon overlay and window-proxy system to run per display instead of once globally, so dragging and previewing a window onto another display behaves like on a single display.

  • RibbonProxyRenderer and RibbonProxyPresenter now keep one overlay panel and presenter per display, with per-display motion state, background captures, and pointer tracking that moves hosted window copies on the destination display (RibbonProxyRenderer.swift).
  • MacOSPlatform window-proxy carry resolves the display containing the dragged window, hosts a preview copy on the destination display, and retries when the display or presenter is unavailable (MacOSPlatform.swift).
  • Adds cross-display mouse drops: MouseDropTarget gains a display case, and dropTiledWindow moves a tiled window into another display's active workspace gap with 24-point boundary hysteresis (MouseReordering.swift).
  • WindowBorderManager hides border sets per display rather than globally, so one display's overlay can hide its windows without suppressing borders on other displays (WindowBorderManager.swift).
  • Removes the duration-driven native animation path: frame writes now go directly to final geometry, animation config keeps only the enabled flag, and quarantine uses fixed 0.18/0.8-second durations (FrameCommit.swift, ConfigModels.swift).
  • Risk: AnimationConfig no longer decodes duration_ms or window_proxies; existing configs carrying those keys are silently ignored. positionsCanAnimateTogether, the pending-animation status properties on MacOSPlatform, and the public spring/animation utilities in Animation.swift are removed, so out-of-tree callers of those APIs break.

Macroscope summarized c3c4727.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 8 files

You’re at about 94% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread .agents/skills/verify-defi/features/window-drag.md Outdated
Comment thread Sources/DefiMacOS/MacOSPlatform.swift Outdated
Comment thread Sources/DefiRuntime/MouseReordering.swift Outdated
Comment thread .agents/skills/verify-defi/drag_check.py Outdated
Comment thread Sources/DefiMacOS/MacOSPlatform.swift
Comment thread Sources/DefiRuntime/MouseReordering.swift Outdated
Comment thread .agents/skills/verify-defi/drag_check.py
@qeude
qeude force-pushed the feat/cross-display-drag-preview branch from f62a151 to e7df7ee Compare October 24, 2026 11:10
@macroscopeapp

macroscopeapp Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Approvability

Verdict: Needs Human Approval

Macroscope's review found this PR requires human approval — This PR introduces substantial cross-display tiled-window drop behavior, including new runtime state transfer and coordinated previews across multiple macOS overlays. The cross-component interaction and new user-facing workflow merit human review despite the accompanying tests.

No code changes detected at 3bc8159. Prior analysis still applies.

You can add or adjust custom eligibility rules. Learn more.

@cubic-dev-ai cubic-dev-ai Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

All reported issues were addressed across 6 files (changes from recent commits).

You’re at about 96% of the monthly reviewed-line limit. You may want to disable incremental reviews to conserve quota. Reviews will continue until that limit is exceeded. If you need help avoiding interruptions, please contact contact@cubic.dev.

Reply with feedback, questions, or to request a fix.

View guided diff | Turn on auto-fix | Re-trigger cubic

Comment thread Sources/DefiRuntime/MouseReordering.swift
Base automatically changed from feat/multi-display-proxies to main October 24, 2026 11:24
qeude added 5 commits October 24, 2026 13:25
A drag whose pointer leaves its display now targets the gap under the
pointer in the other display's active workspace. Release inserts the
window there instead of where the native cross-display move puts it.
The drop revalidates the destination and changes nothing when that
workspace is no longer active or the gap is out of range.

A 24 pt hysteresis keeps a pointer resting on a target boundary from
flipping the target, so the overlay preview does not restart its
springs. The daemon traces each preview change for the drag harness.
A drag over another display now puts that display's ribbon on its
overlay too: its windows make room for the drop, the landing zone shows
there, and a copy of the carried window follows the pointer above the
native window. The source display closes the gap at the same time.
Drop preview frames now cover every display's active workspace, and the
landing zone shows only on the display the drop lands on. Coming back to
the source display or releasing elsewhere lands both overlays as before.
- Never hold a target with the hysteresis when the pointer has none, so
  the window's own stack and off-display pointers keep the native path.
- Host a target display only while the source carries, apply a preview
  computed before the carry once it starts, and clear it on release.
- Retry a host that lacks the carried window's metadata at most every
  100 ms instead of on every drag event.
- drag_check reads only the current drag's preview events; the doc names
  the real switches for the overlay.
The hysteresis probed horizontal offsets only, so a pointer resting on the
edge between stacked displays could still flip the target.
@qeude
qeude force-pushed the feat/cross-display-drag-preview branch from c3c4727 to 3bc8159 Compare October 24, 2026 11:25
@qeude
qeude merged commit 24bb1dc into main Oct 24, 2026
4 of 5 checks passed
@qeude
qeude deleted the feat/cross-display-drag-preview branch October 24, 2026 11:33
qeude added a commit that referenced this pull request Oct 24, 2026
Adapt the PR to #152 and #153: keep the release-time translation guard on
the #153 drop preview, keep both verify-defi journeys, and drop the
removed animatesSize and isReentering arguments from the motion write
test helper.
qeude added a commit that referenced this pull request Oct 25, 2026
The overlay preview comes from mouseDropPreviewFrames since #153, so
MouseStackTarget no longer carries a preview rectangle. A workspace moved
to a fallback display after display loss keeps its stacks, so it returns
unchanged when its display comes back.
qeude added a commit that referenced this pull request Oct 26, 2026
* feat(model): keep verified minimum window height from native metadata

* feat(core): allocate stack heights within verified size constraints

Balanced water-filling over per-window minimum and maximum heights, a pure
stack fit decision, and a deterministic separation of impossible stacks.

* feat(runtime): reject impossible joins and separate stacks that stop fitting

* feat(status): report oversized single-window stacks and document stack constraints

* docs(verify-defi): add stacks journey and check script

* fix(core): target oversized stack windows at their verified minimum height

A target below a window's minimum is refused by the app and was re-sent on every
desktop sync. Raising the target to the minimum, top-aligned, matches what the app
accepts, mirroring how widths clamp to their minimum.

* docs(verify-defi): keep stacks journey on its workspace and check oversized retries

* test(fixture): open clamp fixture windows at their minimum size

The window server reports a minimum no larger than the current frame, so a
fixture window opened below its minimum hid it from the first constraint read.
Also make the stacks journey wait for focus and adoption, judge oversized retries
by applied writes, and fail if another install replaces the daemon mid-run.

* fix: address PR review feedback (#148)

Count each unconstrained window's gaps and 1pt drawable height in a stack's lower
bound, so stackFit no longer accepts a stack that squeezes a sibling below the
frame applyGaps emits.

* fix: address PR review feedback (#148)

- format stack rejection heights as Double so an extreme minimum cannot trap
- make WindowSizeConstraints.minimumHeight a let so callers cannot drop it
- exclude native fullscreen windows from oversized-stack-windows
- stacks journey: exact window ID matching, every completion kind, require the
  stack-constraint error, and fail unless exactly one daemon remains

* fix: address PR review feedback (#148)

- compute accepted-size slot offsets in one pass
- describe viewport-based limits and capped-maximum centring in CONFIGURATION.md
- stacks journey: require one monitor, validate --restore-bottom, and close a
  fixture whose setup fails

* fix: address PR review feedback (#148)

Import Foundation explicitly where StackRejection formats heights.

* fix: address PR review feedback (#148)

Enforce a window's verified maximum height on its final gapped frame, since
applyGaps shrinks gaps wider than half a frame after the slot-level cap.

* fix(runtime): keep drag stacking within verified stack heights

A drag over a column only offers a stack when the stack fits the windows'
verified minimum heights, so a drop never stacks and then splits. The
overlay preview uses the same slot heights the layout will apply.

* fix(runtime): keep migrated stacks and drop the unused stack preview

The overlay preview comes from mouseDropPreviewFrames since #153, so
MouseStackTarget no longer carries a preview rectangle. A workspace moved
to a fallback display after display loss keeps its stacks, so it returns
unchanged when its display comes back.
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.

[P1] Reorder individual windows with native drag previews

1 participant