Fix middle-drag smoothness and add compatibility for some apps#104
Fix middle-drag smoothness and add compatibility for some apps#104NullPointerDepressiveDisorder merged 9 commits intomainfrom
Conversation
…et position; optimize touch data copying in MultitouchManager to reduce GC pressure.
…enerator - Introduced `lastDragPosition` to track the accumulated drag position for improved cursor handling in applications requiring absolute positioning. - Added `reassociateCursor` method to restore normal cursor behavior after drag operations. - Updated drag handling to use accumulated position for mouse events, enhancing compatibility with various applications. - Ensured cursor is properly reassociated after drag ends or is canceled to prevent freezing issues.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
Pull request overview
This PR addresses middle-drag jitter introduced after v1.4.1 by reducing per-frame allocations in the multitouch callback, and improves compatibility with CAD/3D apps by posting drag events with consistent absolute positions while explicitly managing cursor association/warping during drags.
Changes:
- Replace per-frame Swift
Array+maptouch copying with a raw buffermemcpyand deferred deallocation on the gesture queue. - Add accumulated drag position tracking and cursor disassociate/warp logic so drag events provide both correct absolute positions and deltas.
- Populate both integer and double delta fields on synthetic drag events for broader app compatibility.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
| MiddleDrag/Managers/MultitouchManager.swift | Optimizes touch frame copying to reduce high-frequency allocation overhead before async gesture processing. |
| MiddleDrag/Core/MouseEventGenerator.swift | Implements cursor disassociate/warp drag strategy with accumulated positions and expanded delta field setting. |
Comments suppressed due to low confidence (3)
MiddleDrag/Core/MouseEventGenerator.swift:248
endDrag()re-associates the cursor before settingisMiddleMouseDown = false. SinceupdateDrag()can run concurrently (it guards onisMiddleMouseDown), there’s a small race where a drag update can still post/warp after the cursor has been re-associated, potentially causing visible cursor jumps. SetisMiddleMouseDown = false(and/or gateCGWarpMouseCursorPositionbehind a dedicated “disassociated” flag) before callingreassociateCursor().
// Stop watchdog since drag is ending normally
stopWatchdog()
// Re-associate cursor so it's no longer frozen
reassociateCursor()
// CRITICAL: Set isMiddleMouseDown = false SYNCHRONOUSLY to match startDrag
// This prevents race conditions with rapid start/end cycles and ensures
// updateDrag() stops processing immediately
isMiddleMouseDown = false
MiddleDrag/Core/MouseEventGenerator.swift:345
cancelDrag()re-associates the cursor before clearingisMiddleMouseDown. BecauseupdateDrag()can be running on another queue, this ordering can allow one last drag update to post/warp after reassociation, leading to cursor jitter/jumps. ClearisMiddleMouseDownfirst (and/or maintain an explicit disassociation state flag) before callingreassociateCursor().
func cancelDrag() {
guard isMiddleMouseDown else { return }
// Stop watchdog since drag is being cancelled
stopWatchdog()
// Re-associate cursor so it's no longer frozen
reassociateCursor()
// CRITICAL: Set isMiddleMouseDown = false SYNCHRONOUSLY to match startDrag
// This prevents race conditions with rapid cancel/start cycles and ensures
// updateDrag() stops processing immediately
isMiddleMouseDown = false
MiddleDrag/Core/MouseEventGenerator.swift:375
forceMiddleMouseUp()re-associates the cursor before atomically clearing the drag state. IfupdateDrag()is still in-flight, it can still post/warp after reassociation. Consider clearingisMiddleMouseDown/ bumping generation first, then re-associate (or gate warp/post on a dedicated “cursorDisassociated” flag tied to the generation).
Log.warning("Force sending MIDDLE_UP (unconditional)", category: .gesture)
// Stop watchdog if running
stopWatchdog()
// Re-associate cursor so it's no longer frozen
reassociateCursor()
// Atomically reset state and capture generation
let currentGeneration: UInt64 = stateLock.withLock {
_isMiddleMouseDown = false
return _dragGeneration
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…optimize touch data copying in MultitouchManager to reduce GC pressure
…d clamping to display bounds; enhance test coverage for drag position accumulation and clamping behavior
…ventGenerator; ensure atomic cursor reassociation during drag release to prevent race conditions
…order of double and integer values for improved compatibility with various applications
…d state updates during drag cancellation, preventing race conditions and improving drag state management.
…d drag state updates during normal drag termination, preventing race conditions and enhancing overall drag state management.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
…ring atomic cancellation of existing drags, preventing race conditions during new drag initiation.
* Refactor mouse event handling to use current position instead of target position; optimize touch data copying in MultitouchManager to reduce GC pressure. * Add accumulated drag position and cursor reassociation in MouseEventGenerator - Introduced `lastDragPosition` to track the accumulated drag position for improved cursor handling in applications requiring absolute positioning. - Added `reassociateCursor` method to restore normal cursor behavior after drag operations. - Updated drag handling to use accumulated position for mouse events, enhancing compatibility with various applications. - Ensured cursor is properly reassociated after drag ends or is canceled to prevent freezing issues. * Enhance cursor re-association error handling in MouseEventGenerator; optimize touch data copying in MultitouchManager to reduce GC pressure * Refactor MouseEventGenerator to improve drag position handling and add clamping to display bounds; enhance test coverage for drag position accumulation and clamping behavior * Enhance display bounds caching and reconfiguration handling in MouseEventGenerator; ensure atomic cursor reassociation during drag release to prevent race conditions * Refactor delta field setting in MouseEventGenerator to ensure proper order of double and integer values for improved compatibility with various applications * Refactor MouseEventGenerator to ensure atomic cursor reassociation and state updates during drag cancellation, preventing race conditions and improving drag state management. * Refactor MouseEventGenerator to ensure atomic cursor reassociation and drag state updates during normal drag termination, preventing race conditions and enhancing overall drag state management. * Refactor MouseEventGenerator to enhance drag state management by ensuring atomic cancellation of existing drags, preventing race conditions during new drag initiation.
Resolves a smoothness regression introduced after v1.4.1 and adds proper support for apps like Fusion 360 that read absolute position from drag events.
Root cause of smoothness regression: The touch callback was allocating a Swift Array via
mapon every frame at 100Hz+, creating per-frame GC pressure that caused visible jitter. Replaced with a singlememcpyinto a raw buffer, same safety (data copied before async dispatch, freed after), near-zero overhead.CAD app support via disassociate-and-warp: macOS CGEvent drag events carry both an absolute position field and delta fields independently, standard apps read deltas, but 3D/CAD apps (Fusion 360, Unity) read position. Setting both on the same event causes the window server to fight with delta-driven cursor movement. The fix uses
CGAssociateMouseAndMouseCursorPosition(false)to freeze the cursor during drags, posts events with both correct position and deltas, then warps the cursor explicitly each frame viaCGWarpMouseCursorPosition. All drag exit paths (end, cancel, force-release, watchdog) callreassociateCursor()to restore normal behavior.This pull request introduces several improvements to the mouse drag and multitouch event handling logic, focusing on more robust cursor management during drag operations and optimizing touch data copying to reduce performance overhead. The most important changes are grouped below:
Cursor Association and Drag Handling Enhancements
lastDragPositionproperty to accumulate drag position, ensuring consistent event position reporting and cursor warping during disassociated drags.reassociateCursor()method, which is now called at the end of drag operations and during drag cancellation to restore normal cursor behavior. [1] [2] [3] [4] [5] [6]updateDrag()to use the accumulatedlastDragPositioninstead of the current mouse location, providing smoother and more predictable cursor movement.Multitouch Data Copy Optimization
Note
Medium Risk
Touches core input event injection and cursor association/warping paths; mistakes could cause stuck cursor behavior or incorrect drag/click events across apps and multi-display setups.
Overview
Improves middle-drag behavior by disassociating the cursor during drags, tracking an accumulated
lastDragPosition, and warping the visible cursor each frame so apps that rely on absolute event positions (e.g. CAD/3D tools) get consistent movement.Adds clamping to global display bounds, rounds Quartz drag deltas to integer fields explicitly, and hardens drag teardown/recovery paths (
endDrag/cancelDrag/watchdog/force-up) to re-associate the cursor atomically and avoid races.Reduces high-frequency multitouch overhead by replacing per-frame Swift array creation with a raw
memcpybuffer that’s freed after async processing, and adds targeted tests for drag position accumulation, bounds clamping, and delta rounding.Written by Cursor Bugbot for commit 19452b6. This will update automatically on new commits. Configure here.