[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.
This commit is contained in:
Geoffrey McRae
2026-08-06 20:24:13 +10:00
parent 5cc7f06185
commit e4874ee32d
6 changed files with 134 additions and 0 deletions

View File

@@ -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.

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -66,6 +66,7 @@ target_link_libraries(mouse-tests
)
set(MOUSE_CASES
inset-exit
surface-exit
exit-delay
exit-guest
exit-cancel

View File

@@ -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 },