feat(ribbon): preview drops on another display like on a single one
Merged
QE
2d ago+693+693+012345678901234567890123456789-66-66-01234567890123456789
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 #147).
MouseDropTarget.display(MonitorID, workspaceID:gap:)targets the gap under the pointer in another display's active workspace.applyMouseDroprevalidates 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.
mouseDropPreviewFramesmoves toDefiRuntimeand 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.hostputs 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.
RibbonProxyPointerTrackmoves 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
- Overlay path only (
animation.window_proxies = true, the default). - Not in this PR: edge scrolling to offscreen columns (#97).
Blast radius
Sources/DefiMacOS/RibbonProxyRenderer.swift: newhostingdrag 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 intowindowProxyCarryWindows.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 localon the final head: passed. NewMouseReorderingTestscase: the preview closes the gap on the source and opens the slot on the target.verify.py desktop, fullDesktopE2ETests, 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-daemonremained 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.