feat: add mouse_modifier for drag-to-move and drag-to-resize - #484
Open
rojnwa wants to merge 3 commits into
Open
feat: add mouse_modifier for drag-to-move and drag-to-resize#484rojnwa wants to merge 3 commits into
rojnwa wants to merge 3 commits into
Conversation
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
force-pushed
the
feat/mouse-modifier-drag
branch
from
September 12, 2026 09:50
6ff8437 to
23db468
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
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 theconfig 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:
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.
AnimationFramerequests, since that path keeps only thenewest frame per window and applies it once per drained batch with no
read-back and no echo event. A
SetWindowFrameper sample instead spendsthree 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
bsplayout, moving and resizing both tiledand floating windows.
Added unit tests for the throttle behaviour, the config parsing rule, and the
move and resize geometry.
cargo testgives 579 passing. One test,topology_change_clears_stale_pending_hide_target_before_next_workspace_layout,fails for me on an untouched
mainas well, so it looks unrelated to thisbranch.
cargo +nightly fmt --all --checkis clean.Open questions
button mapping (yabai's
mouse_action1/mouse_action2) until someoneactually wants the inverse; happy to add it if you would rather have it now.
leaving its notifications off. It seemed too narrow to guard, but say the word
and I will handle it.