fix: capture the mouse on macOS while the cursor is on a client

On the server, while the cursor is on a client, every physical mouse move
warped the local cursor back to screen center and the tap callback returned
the mouse-move event instead of consuming it. That leaked continuous motion
to whatever local app sat under the centered cursor (e.g. a fullscreen game
or a centered video reacting to the phantom movement).

Capture the mouse the way macOS games do instead: dissociate the hardware
mouse from the cursor in leave() so the cursor freezes and hides, read raw
deltas from the event in onMouseMove(), consume off-screen moves in the tap
callback, and re-associate in enter() (disable() already does via
showCursor()). This retires the center-warp and the "bogus motion" edge
heuristic that only existed to paper over the warp.

fixes: #5871
This commit is contained in:
tehreet 2026-07-11 20:42:28 -05:00 committed by Chris Rizzitello
parent 89a6c7f2ed
commit 96f544de6d
2 changed files with 39 additions and 64 deletions

View file

@ -112,7 +112,7 @@ private:
void sendClipboardEvent(EventTypes type, ClipboardID id) const; void sendClipboardEvent(EventTypes type, ClipboardID id) const;
// message handlers // message handlers
bool onMouseMove(); bool onMouseMove(CGEventRef event);
// mouse button handler. pressed is true if this is a mousedown // mouse button handler. pressed is true if this is a mousedown
// event, false if it is a mouseup event. macButton is the index // event, false if it is a mouseup event. macButton is the index
// of the button pressed using the mac button mapping. // of the button pressed using the mac button mapping.

View file

@ -769,6 +769,8 @@ void OSXScreen::enter()
showCursor(); showCursor();
if (m_isPrimary) { if (m_isPrimary) {
// re-couple the mouse to the cursor, undoing the capture from leave()
CGAssociateMouseAndMouseCursorPosition(true);
setZeroSuppressionInterval(); setZeroSuppressionInterval();
} else { } else {
// reset buttons // reset buttons
@ -798,8 +800,11 @@ void OSXScreen::leave()
if (m_isPrimary) { if (m_isPrimary) {
avoidHesitatingCursor(); avoidHesitatingCursor();
LOG_VERBOSE("centering cursor on leave: %+d, %+d", m_xCenter, m_yCenter);
warpCursor(m_xCenter, m_yCenter); // capture the mouse: freeze the cursor so no local app sees motion while on
// a client (onMouseMove reads raw deltas instead). must follow hideCursor(),
// which re-associates. re-coupled in enter()/disable().
CGAssociateMouseAndMouseCursorPosition(false);
} }
// now off screen // now off screen
@ -958,64 +963,40 @@ void OSXScreen::handleSystemEvent(const Event &event)
} }
} }
bool OSXScreen::onMouseMove() bool OSXScreen::onMouseMove(CGEventRef event)
{ {
// when we receive a mouse-move event, it is possible it was queued for a period
// and that the mouse has already moved again since then. to handle this, we need
// to query the current mouse position rather than using the position in the event.
CGEventRef event = CGEventCreate(NULL);
CGPoint pos = CGEventGetLocation(event);
CFRelease(event);
CGFloat mx = pos.x;
CGFloat my = pos.y;
LOG_VERBOSE("mouse move %+f,%+f", mx, my);
CGFloat x = mx - m_xCursor;
CGFloat y = my - m_yCursor;
if ((x == 0 && y == 0) || (mx == m_xCenter && mx == m_yCenter)) {
return true;
}
// save position to compute delta of next motion
m_xCursor = (int32_t)mx;
m_yCursor = (int32_t)my;
if (m_isOnScreen) { if (m_isOnScreen) {
// motion on primary screen // motion on primary screen. the event may have been queued a while, so
// query the live cursor position rather than the stale event position.
CGEventRef posEvent = CGEventCreate(NULL);
CGPoint pos = CGEventGetLocation(posEvent);
CFRelease(posEvent);
CGFloat mx = pos.x;
CGFloat my = pos.y;
LOG_VERBOSE("mouse move %+f,%+f", mx, my);
CGFloat x = mx - m_xCursor;
CGFloat y = my - m_yCursor;
if ((x == 0 && y == 0) || (mx == m_xCenter && mx == m_yCenter)) {
return true;
}
m_xCursor = (int32_t)mx;
m_yCursor = (int32_t)my;
sendEvent(EventTypes::PrimaryScreenMotionOnPrimary, MotionInfo::alloc(m_xCursor, m_yCursor)); sendEvent(EventTypes::PrimaryScreenMotionOnPrimary, MotionInfo::alloc(m_xCursor, m_yCursor));
} else { } else {
// motion on secondary screen. warp mouse back to // motion on secondary screen. the cursor is frozen (see leave()), so read
// center. // raw deltas from the event instead of diffing position.
warpCursor(m_xCenter, m_yCenter); int32_t dx = (int32_t)CGEventGetIntegerValueField(event, kCGMouseEventDeltaX);
int32_t dy = (int32_t)CGEventGetIntegerValueField(event, kCGMouseEventDeltaY);
// examine the motion. if it's about the distance LOG_VERBOSE("mouse delta %+d,%+d", dx, dy);
// from the center of the screen to an edge then
// it's probably a bogus motion that we want to
// ignore (see warpCursorNoFlush() for a further
// description).
static int32_t bogusZoneSize = 10;
if (-x + bogusZoneSize > m_xCenter - m_x || x + bogusZoneSize > m_x + m_w - m_xCenter ||
-y + bogusZoneSize > m_yCenter - m_y || y + bogusZoneSize > m_y + m_h - m_yCenter) {
LOG_DEBUG("dropped bogus motion %+d,%+d", x, y);
} else {
// send motion
// Accumulate together the move into the running total
static CGFloat m_xFractionalMove = 0;
static CGFloat m_yFractionalMove = 0;
m_xFractionalMove += x; if (dx != 0 || dy != 0) {
m_yFractionalMove += y; sendEvent(EventTypes::PrimaryScreenMotionOnSecondary, MotionInfo::alloc(dx, dy));
// Return the integer part
int32_t intX = (int32_t)m_xFractionalMove;
int32_t intY = (int32_t)m_yFractionalMove;
// And keep only the fractional part
m_xFractionalMove -= intX;
m_yFractionalMove -= intY;
sendEvent(EventTypes::PrimaryScreenMotionOnSecondary, MotionInfo::alloc(intX, intY));
} }
} }
@ -1690,15 +1671,9 @@ CGEventRef OSXScreen::handleCGInputEvent(CGEventTapProxy proxy, CGEventType type
case kCGEventRightMouseDragged: case kCGEventRightMouseDragged:
case kCGEventOtherMouseDragged: case kCGEventOtherMouseDragged:
case kCGEventMouseMoved: case kCGEventMouseMoved:
// we intentionally ignore the position in the event here as the events are // off-screen the cursor is frozen (see leave()), so fall through to consume
// queued and will no longer be accurate when we process them. // the move below instead of returning (leaking) it to local apps.
screen->onMouseMove(); screen->onMouseMove(event);
// The system ignores our cursor-centering calls if
// we don't return the event. This should be harmless,
// but might register as slight movement to other apps
// on the system. It hasn't been a problem before, though.
return event;
break; break;
case kCGEventScrollWheel: case kCGEventScrollWheel:
screen->onMouseWheel( screen->onMouseWheel(