From e4874ee32dd9b2084f87e73fb92d2b26bbd3b797 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Thu, 6 Aug 2026 20:24:13 +1000 Subject: [PATCH] [client] cursor: restore Wayland edge synchronization Keep guest-to-host pointer synchronization active while an attempted surface exit is blocked by the edge of a Wayland output. Continue forwarding relative motion and wait for wl_pointer.leave before marking the cursor outside the window. Distinguish whole-surface releases from exits into letterbox space so the expected asynchronous release callback cannot disable the feedback loop before the pointer is confined again. Treat Wayland pointer capability loss as a real leave so an unrelated inactive callback cannot be mistaken for the intentional release. --- client/displayservers/Wayland/input.c | 3 ++ client/src/core.c | 48 +++++++++++++++++ client/src/main.c | 1 + client/src/main.h | 3 ++ client/tests/CMakeLists.txt | 1 + client/tests/mouse_test.c | 78 +++++++++++++++++++++++++++ 6 files changed, 134 insertions(+) diff --git a/client/displayservers/Wayland/input.c b/client/displayservers/Wayland/input.c index 525b56b4..0e46ac4f 100644 --- a/client/displayservers/Wayland/input.c +++ b/client/displayservers/Wayland/input.c @@ -553,7 +553,10 @@ static void waylandCleanUpPointer(bool notify) MLOG(confSeq, "conf destroy id=%u why=pointer", confId); if (event) + { + app_handleEnterEvent(false); app_handleGrabEvent(false); + } } // Seat-handling listeners. diff --git a/client/src/core.c b/client/src/core.c index 9365cfc3..ac4494f2 100644 --- a/client/src/core.c +++ b/client/src/core.c @@ -70,6 +70,16 @@ static void cancelExit(const char * why) g_cursor.exit = false; } +static void cancelSurfaceExit(const char * why) +{ + if (!g_cursor.surfaceExit) + return; + + MTRACE("surface exit cancel=%s", why); + g_cursor.surfaceExit = false; + g_cursor.warpState = WARP_STATE_ON; +} + static void finishExit(void) { if (!g_cursor.exit || g_cursor.viewReq) @@ -193,6 +203,9 @@ void core_setCursorInView(bool enable) enable, g_cursor.viewReq, g_cursor.inView, g_state.focused, g_cursor.inWindow, g_cursor.grab); + if (!enable) + cancelSurfaceExit("view"); + if (g_cursor.exit && (enable || !g_state.focused || !g_cursor.inWindow || g_state.ignoreInput || !g_state.posInfoValid || app_isOverlayMode())) @@ -253,6 +266,17 @@ void core_handleGrabEvent(bool active) warpSupport == LG_DS_WARP_NONE) return; + if (g_cursor.surfaceExit) + { + if (!active && g_cursor.viewReq && g_cursor.inWindow) + { + MTRACE("surface exit release pending"); + return; + } + + cancelSurfaceExit(active ? "confined" : "state"); + } + if (!active) finishExit(); @@ -282,6 +306,7 @@ void core_setGrabQuiet(bool enable) if (g_cursor.grab == enable) return; + cancelSurfaceExit("capture"); g_cursor.grab = enable; g_cursor.acc.x = 0.0; g_cursor.acc.y = 0.0; @@ -541,6 +566,20 @@ void core_updatePositionInfo(void) } done: + if (g_cursor.surfaceExit) + { + if (g_cursor.viewReq && g_cursor.inWindow && g_state.focused && + !g_state.ignoreInput && g_state.posInfoValid && + !app_isOverlayMode()) + { + MTRACE("surface exit regrab=geometry"); + g_cursor.warpState = WARP_STATE_ON; + g_state.ds->grabPointer(); + } + else + core_setCursorInView(false); + } + MTRACE("geometry src=%dx%d dst=%d,%d,%d,%d win=%dx%d " "scale=%.4f,%.4f uiScale=%.4f rot=%d/%d border=%d,%d,%d,%d " "valid=%d", g_state.srcSize.x, g_state.srcSize.y, @@ -885,6 +924,15 @@ fallback: break; case LG_DS_WARP_SURFACE: + if (tx < 0 || tx >= g_state.windowW || + ty < 0 || ty >= g_state.windowH) + { + g_cursor.surfaceExit = true; + g_state.ds->ungrabPointer(); + core_warpPointer(tx, ty, true); + break; + } + startExit(local.x, local.y); return; diff --git a/client/src/main.c b/client/src/main.c index badfd979..7d84689c 100644 --- a/client/src/main.c +++ b/client/src/main.c @@ -147,6 +147,7 @@ static void lgInit(void) g_cursor.inView = false; g_cursor.viewReq = false; g_cursor.exit = false; + g_cursor.surfaceExit = false; g_cursor.guest.valid = false; // if spice is not in use, hide the local cursor diff --git a/client/src/main.h b/client/src/main.h index 8aef380b..32b95c7d 100644 --- a/client/src/main.h +++ b/client/src/main.h @@ -301,6 +301,9 @@ struct CursorState /* true if a pointer exit is waiting for confinement to release */ bool exit; + /* true if a Wayland surface exit is waiting for a real leave */ + bool surfaceExit; + /* the local pointer exit target */ struct DoublePoint exitPos; diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index 57e9e233..e1e4530d 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -66,6 +66,7 @@ target_link_libraries(mouse-tests ) set(MOUSE_CASES inset-exit + surface-exit exit-delay exit-guest exit-cancel diff --git a/client/tests/mouse_test.c b/client/tests/mouse_test.c index 645c058e..c7d3e088 100644 --- a/client/tests/mouse_test.c +++ b/client/tests/mouse_test.c @@ -329,6 +329,83 @@ static void setConf(bool active) core_handleGrabEvent(active); } +static void setSurfaceEdge(double x, double y) +{ + reset(); + g_state.windowW = 100; + g_state.windowH = 80; + g_state.dstRect = (LG_RendererRect) { + .valid = true, + .x = 0, + .y = 0, + .w = 100, + .h = 80, + }; + setLocal(x, y); +} + +static void testSurfaceExit(void) +{ + setSurfaceEdge(99, 40); + + core_handleMouseNormal(1, 0); + + const int drop = first(EV_UNGRAB); + const int warp = first(EV_WARP); + const int move = first(EV_MOTION); + CHECK(drop >= 0); + CHECK(warp > drop); + CHECK(move > warp); + CHECK(m.ev[warp].x == 100); + CHECK(m.ev[warp].y == 40); + CHECK(m.ev[move].x == 1); + CHECK(m.ev[move].y == 0); + CHECK(g_cursor.surfaceExit); + CHECK(g_cursor.inWindow); + CHECK(g_cursor.inView); + CHECK(g_cursor.viewReq); + + core_handleMouseNormal(1, 0); + CHECK(count(EV_MOTION) == 2); + CHECK(g_cursor.surfaceExit); + CHECK(g_cursor.inWindow); + CHECK(g_cursor.inView); + CHECK(g_cursor.viewReq); + + core_handleGuestMouseUpdate(); + CHECK(count(EV_GUEST) == 1); + + setLocal(99, 40); + core_handleMouseNormal(-1, 0); + CHECK(m.conf.req); + CHECK(g_cursor.surfaceExit); + + setConf(false); + CHECK(g_cursor.inView); + CHECK(g_cursor.surfaceExit); + + setConf(true); + CHECK(g_cursor.inView); + CHECK(!g_cursor.surfaceExit); + + setSurfaceEdge(0, 79); + core_handleMouseNormal(-1, 1); + CHECK(g_cursor.surfaceExit); + CHECK(g_cursor.inWindow); + CHECK(g_cursor.inView); + CHECK(g_cursor.viewReq); + CHECK(count(EV_MOTION) == 1); + + core_handleGuestMouseUpdate(); + CHECK(count(EV_GUEST) == 1); + + g_cursor.inWindow = false; + core_setCursorInView(false); + CHECK(!g_cursor.surfaceExit); + CHECK(!g_cursor.inView); + CHECK(!g_cursor.viewReq); +} + static void startExit(void) { reset(); @@ -742,6 +819,7 @@ struct Test static const struct Test tests[] = { { "inset-exit" , testInsetExit }, + { "surface-exit" , testSurfaceExit }, { "exit-delay" , testExitWait }, { "exit-guest" , testExitGuest }, { "exit-cancel" , testExitCancel },