Repository navigation
🐞 Avoid cursor stutter when suppressing Mission Control - #1133
jvanderen1 wants to merge 8 commits into
Conversation
Rewrite top-edge drag events in an active tap instead of warping the cursor each frame, which caused the stutter reported in mrkai77#609. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
|
@mrkai77 I was wondering if you had q chance to see this PR? I have noticed the "Supress Misson Control" option in Loop was quite laggy on the top edge. This resolves that issue. |
Rewrite top-edge drags before window lookup finishes, and resolve the display with CoreGraphics so the tap thread does not touch AppKit. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Thanks for digging into this!
The thing stopping me from merging is the always-on active event tap (and I apologize for taking this long to respond). Right now every leftMouseDragged event on the system goes through our callback before macOS delivers it. That's true for all drags, for as long as Loop is running, even for users who have snapping or suppressMissionControlOnTopDrag turned off. A listen-only tap failing just means Loop misses events. If an active tap stalls or gets disabled by a timeout, drag input breaks for the whole system, and it's a failure point I'd rather avoid adding for this feature.
I think Rectangle handles this ideally, and it's worth a look. It rewrites the event location the same way this PR does, but it only keeps the active tap while the setting is on (startEventMonitor / reloadFromDefaults). Rectangle only blocks fast drags into Mission Control, and it may make sense to adopt this feature in Loop too.
Could we narrow the tap's lifetime along those lines? Some ideas from me:
- Only while a window is being dragged. Keep the passive monitor as the default, create the active tap once we've resolved a window and seen it move, and remove it on mouse-up. Non-window drags would then never reach the active tap, so they'd stop getting rewritten.
- At minimum, only while the setting is on. Use the passive monitor when
suppressMissionControlOnTopDragor snapping is off, and swap monitors when the setting changes, like Rectangle does.
If the lifetime is tied to a window drag, the generation/pause state probably goes away too. That would also remove a race I found while reviewing: the lookup Task reads generation only when it starts running, which can be after mouse-up has already bumped it, so a leftover pause can carry into the next drag.
One smaller thing: topEdgeY also matches the border between stacked displays, where Mission Control can't trigger. Rectangle's dragPrevY check avoids that by only rewriting when the cursor was already at the edge on the previous event.
An always-on active tap can stall every drag on the system. Snap detection stays listen-only, and the active tap is installed only after a resolved window moves. Co-authored-by: Cursor <cursoragent@cursor.com>
|
The active tap is no longer always on. Snap detection stays on the listen-only monitor. The active tap is created only after a window has been resolved and has moved, and it is removed on mouse-up, so a non-window drag never reaches it. The generation/pause state is gone with that. Rewriting also waits until the previous event was already on the same top edge, so crossing the seam between stacked displays once is left alone. |
Co-authored-by: Cursor <cursoragent@cursor.com>
|
Hey, thanks so much for all the work on this, and sorry for how this turned out! While digging, I found that macOS 15 added its own setting for this (Desktop & Dock > "Drag windows to top of screen to enter Mission Control"), and it catches every case. So I've opened #1175, which syncs Loop's toggle to that setting on macOS 15+ and keeps the existing cursor warp for 13-14. That makes all the event-tap code here unnecessary, so I'm going to close this PR in favor of that one. Your PR is helped me get there! Without the experiments on this branch I'd never had found the system setting. I've added you as a co-author on the new commit. Thanks again, and sorry for the back and forth! |
|
@mrkai77 No worries! Glad there is a robust solution. I don't know if you think it's worth adding for legacy OS versions though? Also, when is the next version going to be released? A lot of changes are on the beta version at this point. Thanks in advance! |
I did consider it, but we’re planning to drop support for macOS 13 sometime in the near future, so I’m not sure it would be worth adding at this point.
As for the next release, I’m aiming to get trackpad gestures a little more stable first! I’d say we’re about 90% there, with just a few more stability fixes remaining. |
|
@mrkai77 That makes sense! Do you need any assistance with closing the remaining 10% gap? |
|
@jvanderen1 any help would be appreciated! The most useful thing right now would probably be stress-testing trackpad gestures and reporting any issues you run into, even small ones. |
|
@mrkai77 I'll take a look at the Discord, your fix, and see if there is anything else I can look at to assist 👍🏻 |
Description
With Suppress Mission Control enabled, dragging a window to the top of the screen stutters. #609 reported this and was closed by turning the toggle off. The stutter is still in current
developwhen the toggle stays on. Tracked again in #1159.Why: Loop keeps Mission Control closed by warping the cursor 1px down on every drag event (
CGWarpMouseCursorPosition). That fights the upward drag, so the cursor thrashes.Fix: Rewrite the drag event’s location instead of warping the cursor. Snap detection stays on a listen-only tap. The active tap is created only after a window has been resolved and has moved, and it is removed on mouse-up. Drags that are not a window never hit it. A held top-edge drag is rewritten only once the previous event was already on that edge, so a single crossing between stacked displays is left alone.
Fixes #1159
How has this been tested?
Local Debug build on macOS 27.0 (also checked earlier on macOS Tahoe):
The checked drag tests are from the earlier build. This update changes when the tap exists: it starts only after the window moves, and it is gone on mouse-up.
Screencast
output.mp4
Checklist:
Please describe to which degree, if any, an LLM was used in creating this pull request.
Cursor (Grok) helped investigate the stutter and draft the change in
WindowDragManager, including the later event-tap hardening. I reviewed the code, confirmed it builds, and manually tested top-edge dragging before opening this PR.