Skip to content

fix(dock): resize a split without GPUI's drag machinery - #55

Merged
guyverino merged 1 commit into
masterfrom
fix/dock-resize-without-drag-machinery
Aug 27, 2026
Merged

fix(dock): resize a split without GPUI's drag machinery#55
guyverino merged 1 commit into
masterfrom
fix/dock-resize-without-drag-machinery

Conversation

@guyverino

Copy link
Copy Markdown
Collaborator

What & why

A dock resize handle was wired with on_drag + on_drag_move. That machinery exists to float a preview under the cursor, and GPUI pays for it by calling Window::refresh() on every mouse move while a drag is active — "redraw the window so that the active drag can follow the mouse cursor" (window.rs). refresh() sets refreshing, which makes every cached view in the window miss.

So dragging one handle rebuilt, re-laid-out and repainted every panel in the dock, including the ones nowhere near it. And DockResizeDrag::render returned Empty — the window was being refreshed per mouse move to move nothing.

How it works now

  • Mouse-down on the handle arms a gesture; window-level listeners follow the pointer.
  • Those listeners are installed from a canvas paint closure: Window::on_mouse_event is a paint-phase API, so registering from render would be the wrong phase.
  • Window-level rather than element-scoped, because the platform captures the pointer for the life of the press (SetCapture / ReleaseCapture on Windows) — a handle dragged past the window edge keeps receiving moves.
  • The listeners exist only while a gesture is in flight; an idle dock installs none.

Notable decisions

  • Repaints paced to 60 Hz, layout not paced. resize_* still runs on every move, so the handle never trails the pointer; only the repaint is thinned. The release notifies unconditionally, settling whatever the pacer dropped.
  • Cursor held for the gesture via set_window_cursor_style: the pointer leaves the thin handle immediately, and the axis is known only where the press happened. It is a per-frame request, so it lapses on its own when the gesture ends.
  • A move without a held button ends the gesture, so a lost release cannot leave it armed forever.
  • DockResizeDrag and its Render impl are removed along with the mechanism that needed them.

Known limitations

  • Tab and tile drags still use the drag machinery, correctly — they do float a preview.
  • DockEvent::LayoutChanged is still emitted per changed move, now at the raw mouse-move rate. Measured at 58 µs per consumer dump in the host terminal (≈5 ms/s), so it was left alone rather than paced on a guess.

Measured

Host terminal, 120 Hz display, one splitter drag, per second:

before after
window draws 100 80
Window::draw total 776 ms 515 ms
panels whose size does not change, rebuilt 54 and 53 4 and 1

The two panels beside the handle still rebuild per frame — that is the actual work of resizing them.

How to verify

powershell -ExecutionPolicy Bypass -File tools\run-component-guardrails.ps1

MoonUI guardrails PASS, exit 0 — fmt, gallery check, 431 component tests, 7 gallery tests, and the component-audit / component-api / donor-mirror baselines.

A dock resize handle was wired with `on_drag` + `on_drag_move`. That machinery
exists to float a preview under the cursor, and GPUI pays for it by calling
`Window::refresh()` on EVERY mouse move while a drag is active — "redraw the
window so that the active drag can follow the mouse cursor". `refresh()` sets
`refreshing`, which makes every cached view in the window miss.

So dragging one handle rebuilt, re-laid-out and repainted every panel in the
dock, including the ones nowhere near it. `DockResizeDrag::render` returned
`Empty`: the window was being refreshed per mouse move to move nothing.

The handle now arms a gesture on mouse-down and is followed by window-level
listeners installed from a `canvas` paint closure — `Window::on_mouse_event` is
a paint-phase API, so registering from `render` would be the wrong phase.
Window-level rather than element-scoped because the platform captures the
pointer for the life of the press (`SetCapture`/`ReleaseCapture` on Windows), so
a handle dragged past the window edge keeps receiving moves. The listeners exist
only while a gesture is in flight.

Repaints are paced to 60 Hz. The layout is not: `resize_*` still runs on every
move, so the handle never trails the pointer — only the repaint is thinned. The
release notifies unconditionally, settling whatever the pacer dropped.

The cursor is held for the gesture through `set_window_cursor_style`, since the
pointer leaves the thin handle immediately and the axis is known only where the
press happened. It is a per-frame request, so it lapses when the gesture ends.

`DockResizeDrag` and its `Render` impl are removed with the mechanism that
needed them.

Measured in a host terminal on a 120 Hz display, one splitter drag, per second:
window draws 100 -> 80, `Window::draw` total 776 -> 515 ms, and the two panels
whose size does NOT change went from being rebuilt 54 and 53 times a second to
4 and 1. The two beside the handle still rebuild per frame, which is the actual
work of resizing them.
@guyverino
guyverino merged commit e4fb30b into master Aug 27, 2026
2 checks passed
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.

1 participant