From 89dcf560732ce52b825ce4b8539a66a8f469ed3f Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Mon, 10 Aug 2026 18:47:27 +1000 Subject: [PATCH] [client] X11: release pointer after capture Normal input now uses absolute positioning, so retaining the XI pointer grab after capture leaves no later path to release it. Always release the grab so leaving the window works again and the uncapture path can align the host cursor to the guest position. --- client/displayservers/X11/x11.c | 12 +------- client/tests/CMakeLists.txt | 1 + client/tests/mouse_test.c | 49 +++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 11 deletions(-) diff --git a/client/displayservers/X11/x11.c b/client/displayservers/X11/x11.c index fc0c6ec8..f26a0969 100644 --- a/client/displayservers/X11/x11.c +++ b/client/displayservers/X11/x11.c @@ -2103,19 +2103,9 @@ static void x11CapturePointer(void) static void x11UncapturePointer(void) { - const bool ungrab = !app_isFormatValid() || app_isCaptureOnlyMode(); - LG_LOCK(x11.pointerLock); atomic_store_explicit(&x11.captureActive, false, memory_order_release); - - /* we need to ungrab the pointer on the following conditions when exiting capture mode: - * - if the format is invalid as we do not know where the guest cursor is, - * which breaks edge detection as the cursor can not be warped out of the - * window when we release it. - * - if the user has opted to use captureInputOnly mode. - */ - if (ungrab) - x11UngrabPointerLocked(); + x11UngrabPointerLocked(); LG_UNLOCK(x11.pointerLock); } diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index dd163587..4dc5e96b 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -126,6 +126,7 @@ set(MOUSE_CASES capture-revoked capture-release capture-align + x11-capture-exit capture-fallback rotate-scale geometry diff --git a/client/tests/mouse_test.c b/client/tests/mouse_test.c index ef25d1bc..6bd6774a 100644 --- a/client/tests/mouse_test.c +++ b/client/tests/mouse_test.c @@ -67,6 +67,7 @@ static struct struct Trap conf; struct Trap lock; bool valid; + bool captureGrab; struct Ev ev[256]; unsigned int count; } @@ -152,6 +153,18 @@ static void capture(void) return; } + if (m.captureGrab) + { + m.conf.req = true; + if (!m.conf.hold) + m.conf.on = true; + m.lock.req = true; + if (!m.lock.hold) + m.lock.on = m.conf.on; + push(EV_CAPTURE, 0, 0, false); + return; + } + m.conf.req = false; if (!m.conf.hold) m.conf.on = false; @@ -171,6 +184,9 @@ static void uncapture(void) m.lock.req = false; if (!m.lock.hold) m.lock.on = false; + + if (m.captureGrab) + ungrab(); } static bool captured(void) @@ -592,6 +608,38 @@ static void testCapAlign(void) CHECK(!m.ev[warp].exit); } +static void testX11CapExit(void) +{ + reset(); + setLocal(50, 50); + m.support = LG_DS_WARP_SCREEN; + m.captureGrab = true; + m.conf.req = false; + m.conf.on = false; + + core_setGrabQuiet(true); + CHECK(m.conf.on); + CHECK(m.lock.on); + + g_cursor.guest.x += 10; + m.count = 0; + core_setGrabQuiet(false); + + const int uncapture = first(EV_UNCAPTURE); + const int ungrab = first(EV_UNGRAB); + const int warp = first(EV_WARP); + CHECK(uncapture >= 0); + CHECK(ungrab > uncapture); + CHECK(warp > ungrab); + CHECK(m.ev[warp].x == 60); + CHECK(m.ev[warp].y == 50); + CHECK(!m.ev[warp].exit); + CHECK(!m.conf.req); + CHECK(!m.conf.on); + CHECK(!m.lock.req); + CHECK(!m.lock.on); +} + static void testCapFallback(void) { reset(); @@ -853,6 +901,7 @@ static const struct Test tests[] = { { "capture-revoked" , testCapRevoke }, { "capture-release" , testCapRelease }, { "capture-align" , testCapAlign }, + { "x11-capture-exit", testX11CapExit }, { "capture-fallback", testCapFallback }, { "rotate-scale" , testRotateScale }, { "geometry" , testGeometry },