Skip to content

feat: add mouse_modifier for drag-to-move and drag-to-resize - #484

Open
rojnwa wants to merge 3 commits into
acsandmann:mainfrom
rojnwa:feat/mouse-modifier-drag
Open

feat: add mouse_modifier for drag-to-move and drag-to-resize#484
rojnwa wants to merge 3 commits into
acsandmann:mainfrom
rojnwa:feat/mouse-modifier-drag

Conversation

@rojnwa

@rojnwa rojnwa commented Sep 9, 2026

Copy link
Copy Markdown

Implements the yabai-style modifier drag from #371.

Hold the configured modifier and drag a window with the left button to move it,
or with the right button to resize it. Resizing grabs the edges nearest to where
the drag started, so pressing near the top-left corner drags that corner.

[settings]
mouse_modifier = "Cmd"

The value is a modifier-only spec parsed by the same code as keybindings, so
"Ctrl + Alt" works, and a spec that names an ordinary key is rejected when the
config loads instead of silently binding just the modifier. The setting is
optional and off when absent, so nothing changes for anyone who does not set it.

How it works

The event tap owns the gesture: it picks move or resize on mouse down, feeds
deltas to the reactor, and swallows the press and release so the application
underneath never sees a stray click. The reactor applies each delta to the
window's last known frame and pushes the result through the same handler a
native drag already uses, so drag-to-swap and split resizing keep working
without a second code path.

Two things decide whether it feels immediate, and both were painfully obvious
in the first draft:

  • Raw drag samples arrive faster than a window can be moved and each one costs
    a reactor pass, so they are coalesced to about one per display frame. Motion
    that gets withheld is rolled into the next update, so the update rate changes
    and the distance travelled does not.
  • Frames go out as AnimationFrame requests, since that path keeps only the
    newest frame per window and applies it once per drained batch with no
    read-back and no echo event. A SetWindowFrame per sample instead spends
    three Accessibility writes plus a frame read and emits an echo that costs
    another reactor pass, which a fast drag outruns within a second or two. A
    drag also holds one Enhanced UI lease for its whole duration rather than
    taking one per write.

Moves are written position-only, as they have no reason to pay for the size
writes a resize needs.

Commits

The two refactors are separated out so the feature commit stays readable. Both
are behaviour-preserving; the second one's diff is mostly rustfmt reacting to a
smaller indent.

Testing

Used as a daily driver on macOS 15.6 (Apple silicon) across a 3440x1440 display
and a retina laptop display, in the bsp layout, moving and resizing both tiled
and floating windows.

Added unit tests for the throttle behaviour, the config parsing rule, and the
move and resize geometry. cargo test gives 579 passing. One test,
topology_change_clears_stale_pending_hide_target_before_next_workspace_layout,
fails for me on an untouched main as well, so it looks unrelated to this
branch. cargo +nightly fmt --all --check is clean.

Open questions

  • Left is always move and right is always resize. I left out a configurable
    button mapping (yabai's mouse_action1 / mouse_action2) until someone
    actually wants the inverse; happy to add it if you would rather have it now.
  • If a window is destroyed mid-drag the write lease leaks for that window,
    leaving its notifications off. It seemed too narrow to guard, but say the word
    and I will handle it.

compute_disable_hotkey_active answered two questions at once: whether the
configured modifiers are held, and whether the base key is down. Only the
modifier half is reusable on its own, so give it a name.

No behaviour change.
The WindowFrameChanged arm had grown past ninety lines inside
dispatch_workflow, which makes the surrounding match hard to follow and
leaves the handler impossible to reach from anywhere else.

Move the body into handle_window_frame_changed_event, taking the values the
event carries. Most of the diff is rustfmt reacting to the smaller indent;
the logic is unchanged.
Hold the configured modifier and drag a window with the left button to move
it, or with the right button to resize it. Resizing grabs the edges nearest
to where the drag started, so pressing near the top-left corner drags that
corner. For anyone arriving from yabai, this covers mouse_modifier together
with mouse_action1 and mouse_action2.

    [settings]
    mouse_modifier = "Cmd"

The value is a modifier-only spec parsed by the same code as keybindings, so
"Ctrl + Alt" works and a spec that names an ordinary key is rejected when the
config loads rather than silently binding the modifier alone.

The event tap owns the gesture. It picks move or resize on mouse down, feeds
deltas to the reactor, and swallows the press and release so the application
underneath never sees a stray click. The reactor applies each delta to the
window's last known frame and pushes the result through the same handler a
native drag uses, which keeps drag-to-swap and split resizing working
without a second code path.

Two things decide whether this feels immediate. Raw drag samples arrive
faster than a window can actually be moved and each one costs a reactor
pass, so they are coalesced to roughly one per display frame with any
withheld motion rolled into the next update; the update rate changes, the
distance travelled does not. The frames themselves go out as animation
frame requests, because that path keeps only the newest frame per window and
applies it once per drained batch with no read-back and no echo event. A
SetWindowFrame per sample would instead spend three Accessibility writes
plus a frame read and emit an echo that costs another reactor pass, which a
fast drag outruns. A drag also holds a single Enhanced UI lease for its whole
duration instead of taking one per write.

Moves are written position-only, since they have no reason to pay for the
size writes a resize needs.

Refs acsandmann#371
@rojnwa
rojnwa force-pushed the feat/mouse-modifier-drag branch from 6ff8437 to 23db468 Compare September 12, 2026 09:50
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