diff --git a/client/src/app.c b/client/src/app.c index 8dc25784..ebaf9355 100644 --- a/client/src/app.c +++ b/client/src/app.c @@ -130,17 +130,22 @@ bool app_isOverlayMode(void) void app_updateCursorPos(double x, double y) { - g_cursor.pos.x = x; - g_cursor.pos.y = y; - g_cursor.valid = true; + const bool overlay = app_isOverlayMode(); + if (overlay) + { + g_cursor.pos.x = x; + g_cursor.pos.y = y; + g_cursor.valid = true; + g_cursor.motionValid = false; + } + else + core_handleMousePosition(x, y); MTRACE("pos pos=%.3f,%.3f inWin=%d inView=%d grab=%d", x, y, g_cursor.inWindow, g_cursor.inView, g_cursor.grab); - if (app_isOverlayMode()) + if (overlay) g_state.io->MousePos = (ImVec2) { x, y }; - else - core_handleMouseAbsolute(); } void app_updateMouseState(void) @@ -220,13 +225,21 @@ void app_handleFocusEvent(bool focused) if (g_state.focused == focused) return; - g_state.focused = focused; + g_state.focused = focused; + g_cursor.motionValid = false; + const bool inputEnabled = core_inputEnabled(); // release any imgui buttons/keys if we lost focus if (!focused && app_isOverlayMode()) core_resetOverlayInputState(); - if (!core_inputEnabled()) + if (!focused) + { + core_setGrabQuiet(false); + core_setCursorInView(false); + } + + if (!inputEnabled) { if (!focused && g_params.minimizeOnFocusLoss && app_getFullscreen()) g_state.ds->minimize(); @@ -235,9 +248,6 @@ void app_handleFocusEvent(bool focused) if (!focused) { - core_setGrabQuiet(false); - core_setCursorInView(false); - if (g_params.releaseKeysOnFocusLoss) lgInput_releaseKeys(); @@ -262,6 +272,8 @@ void app_handleEnterEvent(bool entered) MTRACE("enter set=%d inWin=%d inView=%d grab=%d", entered, g_cursor.inWindow, g_cursor.inView, g_cursor.grab); + g_cursor.motionValid = false; + if (entered) { g_cursor.inWindow = true; diff --git a/client/src/config.c b/client/src/config.c index 141fb5f9..0c29da49 100644 --- a/client/src/config.c +++ b/client/src/config.c @@ -412,7 +412,7 @@ static struct Option options[] = { .module = "input", .name = "autoCapture", - .description = "Try to keep the mouse captured when needed", + .description = "Grab the keyboard in the guest view and release it before exit", .type = OPTION_TYPE_BOOL, .value.x_bool = false }, diff --git a/client/src/core.c b/client/src/core.c index 9363b371..39dbe199 100644 --- a/client/src/core.c +++ b/client/src/core.c @@ -137,6 +137,34 @@ bool core_inputEnabled(void) ((g_cursor.grab && g_params.captureInputOnly) || !g_params.captureInputOnly); } +static void updateKeyboardGrab(void) +{ + const bool capture = g_cursor.grab && g_params.grabKeyboard; + const bool view = g_cursor.inView && g_params.grabKeyboardOnFocus; + const bool automatic = !g_cursor.grab && + g_cursor.autoCaptureActive && !g_params.captureInputOnly; + const bool active = g_state.focused && core_inputEnabled() && + !app_isOverlayMode() && (capture || view || automatic); + + if (active) + g_state.ds->grabKeyboard(); + else + g_state.ds->ungrabKeyboard(); +} + +static void setAutoCapture(bool active) +{ + active = active && g_params.autoCapture && g_cursor.inView && + !g_cursor.grab && !g_params.captureInputOnly; + if (g_cursor.autoCaptureActive == active) + return; + + MTRACE("auto capture active=%d old=%d", active, + g_cursor.autoCaptureActive); + g_cursor.autoCaptureActive = active; + updateKeyboardGrab(); +} + static void applyView(bool active, bool force) { if (active && !g_cursor.viewReq) @@ -146,33 +174,36 @@ static void applyView(bool active, bool force) } if (!force && g_cursor.inView == active) + { + updateKeyboardGrab(); return; + } MTRACE("view active=%d old=%d req=%d force=%d", active, g_cursor.inView, g_cursor.viewReq, force); - g_cursor.inView = active; - g_cursor.draw = (g_params.alwaysShowCursor || g_params.captureInputOnly) + g_cursor.inView = active; + g_cursor.draw = + (g_params.alwaysShowCursor || g_params.captureInputOnly) ? true : g_cursor.inView; - g_cursor.redraw = true; + g_cursor.redraw = true; + g_cursor.motionValid = false; + g_cursor.autoCaptureActive = active && g_params.autoCapture && + !g_cursor.grab && !g_params.captureInputOnly; g_cursor.warpState = g_cursor.inView ? WARP_STATE_ON : WARP_STATE_OFF; if (g_cursor.inView) { if (g_params.hideMouse) g_state.ds->setPointer(LG_POINTER_NONE); - - if (g_params.grabKeyboardOnFocus) - g_state.ds->grabKeyboard(); } else { if (g_params.hideMouse) g_state.ds->setPointer(LG_POINTER_SQUARE); - - g_state.ds->ungrabKeyboard(); } + updateKeyboardGrab(); g_cursor.warpState = WARP_STATE_ON; } @@ -277,9 +308,9 @@ void core_setGrabQuiet(bool enable) return; cancelSurfaceExit("capture"); - g_cursor.grab = enable; - g_cursor.acc.x = 0.0; - g_cursor.acc.y = 0.0; + g_cursor.acc.x = 0.0; + g_cursor.acc.y = 0.0; + g_cursor.motionValid = false; /* if the display server does not support warp we need to ungrab the pointer * here instead of in the move handler */ @@ -288,8 +319,11 @@ void core_setGrabQuiet(bool enable) if (enable) { + g_state.ignoreInput = false; + g_cursor.grab = true; + g_cursor.autoCaptureActive = false; + updateKeyboardGrab(); core_setCursorInView(true); - g_state.ignoreInput = false; /* ensure the local mouse is inside the window before we capture, this fixes * odd UI behaviour if the user is using focus follows mouse and the window @@ -302,29 +336,21 @@ void core_setGrabQuiet(bool enable) core_warpPointer(local.x, local.y, true); } - if (g_params.grabKeyboard) - g_state.ds->grabKeyboard(); - g_state.ds->capturePointer(); } else { - if (g_params.grabKeyboard) - { - if (!g_params.grabKeyboardOnFocus || - !g_state.focused || g_params.captureInputOnly) - g_state.ds->ungrabKeyboard(); - } - g_state.ds->uncapturePointer(); + g_cursor.grab = false; + g_cursor.autoCaptureActive = g_params.autoCapture && + g_cursor.inView && !g_params.captureInputOnly; + updateKeyboardGrab(); if (warpSupport == LG_DS_WARP_NONE) core_handleMouseAbsolute(); else { - if (!g_params.captureInputOnly) - applyView(g_state.ds->isPointerGrabbed(), false); - + g_cursor.warpState = WARP_STATE_ON; core_alignToGuest(); } } @@ -369,6 +395,7 @@ bool core_warpPointer(int x, int y, bool exiting) MTRACE("warp send target=%d,%d exit=%d pos=%.3f,%.3f", x, y, exiting, g_cursor.pos.x, g_cursor.pos.y); + g_cursor.motionValid = false; g_state.ds->warpPointer(x, y, exiting); return true; } @@ -397,6 +424,7 @@ void core_onWindowSizeChanged(unsigned width, unsigned height) void core_updatePositionInfo(void) { + g_cursor.motionValid = false; cancelExit("geometry"); if (g_params.setGuestRes && @@ -679,13 +707,12 @@ void core_handleGuestMouseUpdate(void) ); } -void core_handleMouseAbsolute(void) +static void handleMouseAbsolute(bool inputEnabled, bool absolute) { if (g_cursor.grab || !g_cursor.inWindow || !g_cursor.valid || !g_state.haveSrcSize || !g_state.posInfoValid || !g_state.focused || g_cursor.realigning || - app_isOverlayMode() || !core_inputEnabled() || - !lgInput_supports(LG_INPUT_SUPPORT_MOUSE_ABSOLUTE)) + app_isOverlayMode() || !inputEnabled || !absolute) return; const bool inView = isInView(); @@ -711,6 +738,79 @@ void core_handleMouseAbsolute(void) g_cursor.realign = false; } +void core_handleMouseAbsolute(void) +{ + const bool inputEnabled = core_inputEnabled(); + const bool absolute = inputEnabled && + lgInput_supports(LG_INPUT_SUPPORT_MOUSE_ABSOLUTE); + handleMouseAbsolute(inputEnabled, absolute); +} + +void core_handleMousePosition(double x, double y) +{ + bool allowPrediction = g_cursor.motionValid && !g_cursor.realign; + const double ex = allowPrediction ? x - g_cursor.pos.x : 0.0; + const double ey = allowPrediction ? y - g_cursor.pos.y : 0.0; + + g_cursor.pos.x = x; + g_cursor.pos.y = y; + g_cursor.valid = true; + g_cursor.motionValid = true; + + const bool inputEnabled = core_inputEnabled(); + const bool absolute = inputEnabled && + lgInput_supports(LG_INPUT_SUPPORT_MOUSE_ABSOLUTE); + const bool canTrack = !g_cursor.grab && g_cursor.inWindow && + g_state.haveSrcSize && g_state.posInfoValid && + g_state.dstRect.valid && g_state.focused && + !app_isOverlayMode() && inputEnabled; + if (canTrack && !g_cursor.realigning) + { + const bool inView = isInView(); + if (g_cursor.viewReq != inView || g_cursor.inView != inView) + { + core_setCursorInView(inView); + allowPrediction = false; + g_cursor.motionValid = true; + } + } + + handleMouseAbsolute(inputEnabled, absolute); + + if (!canTrack) + { + g_cursor.motionValid = false; + setAutoCapture(false); + return; + } + + if (g_cursor.realigning) + { + g_cursor.motionValid = false; + return; + } + + bool active = g_cursor.inView && g_cursor.viewReq; + if (!allowPrediction || !g_params.autoCapture || + !active || g_cursor.buttons) + { + setAutoCapture(active); + return; + } + + const double nextX = x + ex; + const double nextY = y + ey; + active = + nextX >= g_state.dstRect.x && + nextX < g_state.dstRect.x + g_state.dstRect.w && + nextY >= g_state.dstRect.y && + nextY < g_state.dstRect.y + g_state.dstRect.h; + + MTRACE("auto capture move=%.3f,%.3f next=%.3f,%.3f inView=%d", + ex, ey, nextX, nextY, active); + setAutoCapture(active); +} + void core_handleMouseGrabbed(double ex, double ey) { const bool active = g_cursor.grab && g_state.ds->isPointerCaptured(); @@ -911,11 +1011,6 @@ fallback: testExit = false; } - /* if we are in "autoCapture" and the delta was large don't test for exit */ - if (g_params.autoCapture && - (fabs(ex) > 20.0 / g_cursor.scale.x || fabs(ey) > 20.0 / g_cursor.scale.y)) - testExit = false; - /* if any buttons are held we should not allow exit to happen */ if (g_cursor.buttons) testExit = false; @@ -989,6 +1084,9 @@ fallback: } } + if (g_params.autoCapture && testExit) + setAutoCapture(!didExit); + if (absolute) return; @@ -998,25 +1096,11 @@ fallback: if (x == 0 && y == 0) return; - if (g_params.autoCapture) - { - g_cursor.delta.x += x; - g_cursor.delta.y += y; - - if (fabs(g_cursor.delta.x) > 50.0 || fabs(g_cursor.delta.y) > 50.0) - { - g_cursor.delta.x = 0; - g_cursor.delta.y = 0; - } - } - else - { - /* assume the mouse will move to the location we attempt to move it to so we - * avoid warp out of window issues. The cursorThread will correct this if - * wrong after the movement has ocurred on the guest */ - g_cursor.guest.x += x; - g_cursor.guest.y += y; - } + /* assume the mouse will move to the location we attempt to move it to so we + * avoid warp out of window issues. The cursorThread will correct this if + * wrong after the movement has ocurred on the guest */ + g_cursor.guest.x += x; + g_cursor.guest.y += y; MTRACE("motion delta=%d,%d guest=%d,%d exit=%d test=%d warp=%d", x, y, g_cursor.guest.x, g_cursor.guest.y, didExit, testExit, diff --git a/client/src/core.h b/client/src/core.h index 1cc92a24..82be6879 100644 --- a/client/src/core.h +++ b/client/src/core.h @@ -40,6 +40,7 @@ bool core_startFrameThread(void); void core_stopFrameThread(void); void core_handleGuestMouseUpdate(void); void core_handleMouseAbsolute(void); +void core_handleMousePosition(double x, double y); void core_handleMouseGrabbed(double ex, double ey); void core_handleMouseNormal(double ex, double ey); void core_resetOverlayInputState(void); diff --git a/client/src/evdev.c b/client/src/evdev.c index 9909962a..cf977ea7 100644 --- a/client/src/evdev.c +++ b/client/src/evdev.c @@ -381,6 +381,7 @@ void evdev_stop(void) void evdev_grabKeyboard(void) { + state.pending = PENDING_NONE; if (state.grabbed) return; @@ -405,6 +406,7 @@ void evdev_grabKeyboard(void) void evdev_ungrabKeyboard(void) { + state.pending = PENDING_NONE; if (!state.grabbed) return; diff --git a/client/src/main.h b/client/src/main.h index 85e8cd80..4c04f98c 100644 --- a/client/src/main.h +++ b/client/src/main.h @@ -316,12 +316,15 @@ struct CursorState /* true if the position is valid */ bool valid; + /* true if the last local position can be used to predict motion */ + bool motionValid; + + /* true if auto capture currently requests the keyboard */ + bool autoCaptureActive; + /* the button state */ unsigned int buttons; - /* the delta since last warp when in auto capture mode */ - struct DoublePoint delta; - /* the scale factor for the mouse sensitiviy */ int sens; diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index bd6bcdf1..6a6f7e7f 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -121,6 +121,7 @@ set(MOUSE_CASES exit-immediate exit-guest absolute-sync + auto-capture exit-reentry view-immediate capture-pending diff --git a/client/tests/mouse_test.c b/client/tests/mouse_test.c index bf517699..a9b802df 100644 --- a/client/tests/mouse_test.c +++ b/client/tests/mouse_test.c @@ -42,6 +42,8 @@ enum EvType EV_GUEST, EV_MOTION, EV_VALID, + EV_KEY_GRAB, + EV_KEY_UNGRAB, }; struct Ev @@ -68,6 +70,7 @@ static struct struct Trap lock; bool valid; bool captureGrab; + bool keyboardGrabbed; struct Ev ev[256]; unsigned int count; } @@ -119,8 +122,22 @@ static void setPointer(LG_DSPointer pointer) (void)pointer; } -static void keyNoop(void) +static void keyGrab(void) { + if (m.keyboardGrabbed) + return; + + m.keyboardGrabbed = true; + push(EV_KEY_GRAB, 0, 0, false); +} + +static void keyUngrab(void) +{ + if (!m.keyboardGrabbed) + return; + + m.keyboardGrabbed = false; + push(EV_KEY_UNGRAB, 0, 0, false); } static void grab(void) @@ -209,8 +226,8 @@ static struct LG_DisplayServerOps ds = { .name = "test", .guestPointerUpdated = guest, .setPointer = setPointer, - .grabKeyboard = keyNoop, - .ungrabKeyboard = keyNoop, + .grabKeyboard = keyGrab, + .ungrabKeyboard = keyUngrab, .grabPointer = grab, .ungrabPointer = ungrab, .isPointerGrabbed = grabbed, @@ -511,6 +528,141 @@ static void testAbsoluteSync(void) lgInput_setFallback(&inputOps, NULL); } +static void testAutoCapture(void) +{ + static const enum LG_DSWarpSupport supports[] = { + LG_DS_WARP_SURFACE, + LG_DS_WARP_SCREEN, + }; + + for (unsigned int i = 0; i < sizeof(supports) / sizeof(supports[0]); ++i) + { + reset(); + m.support = supports[i]; + g_params.autoCapture = true; + g_cursor.inView = false; + g_cursor.viewReq = false; + setLocal(50, 50); + + core_invalidatePointer(true); + CHECK(m.keyboardGrabbed); + CHECK(count(EV_KEY_GRAB) == 1); + + setLocal(109, 50); + m.count = 0; + core_handleMouseNormal(1, 0); + CHECK(!m.keyboardGrabbed); + CHECK(count(EV_KEY_UNGRAB) == 1); + CHECK(g_cursor.exit); + CHECK(!g_cursor.inView); + CHECK(!g_cursor.viewReq); + + setLocal(108, 50); + m.count = 0; + core_handleMouseNormal(-1, 0); + CHECK(m.keyboardGrabbed); + CHECK(count(EV_KEY_GRAB) == 1); + CHECK(!g_cursor.exit); + CHECK(g_cursor.inView); + CHECK(g_cursor.viewReq); + } + + bool absolute = true; + lgInput_setFallback(&inputOps, &absolute); + reset(); + g_params.autoCapture = true; + g_cursor.inView = false; + g_cursor.viewReq = false; + g_cursor.valid = true; + setLocal(50, 50); + + core_handleMousePosition(50, 50); + CHECK(m.keyboardGrabbed); + CHECK(count(EV_KEY_GRAB) == 1); + + g_cursor.motionValid = false; + core_handleMousePosition(108, 50); + m.count = 0; + core_handleMousePosition(109, 50); + CHECK(!m.keyboardGrabbed); + CHECK(count(EV_KEY_UNGRAB) == 1); + CHECK(g_cursor.inView); + CHECK(g_cursor.viewReq); + + m.count = 0; + core_handleMousePosition(108, 50); + CHECK(m.keyboardGrabbed); + CHECK(count(EV_KEY_GRAB) == 1); + CHECK(g_cursor.inView); + CHECK(g_cursor.viewReq); + + reset(); + g_params.autoCapture = true; + g_cursor.inView = false; + g_cursor.viewReq = false; + g_cursor.valid = true; + g_cursor.motionValid = true; + setLocal(9, 50); + + core_handleMousePosition(109, 50); + CHECK(m.keyboardGrabbed); + CHECK(count(EV_KEY_GRAB) == 1); + CHECK(g_cursor.motionValid); + + m.count = 0; + core_handleMousePosition(109.5, 50); + CHECK(!m.keyboardGrabbed); + CHECK(count(EV_KEY_UNGRAB) == 1); + + reset(); + g_params.autoCapture = true; + g_cursor.inView = false; + g_cursor.viewReq = false; + g_cursor.valid = true; + g_state.posInfoValid = false; + setLocal(50, 50); + + core_handleMousePosition(50, 50); + CHECK(!m.keyboardGrabbed); + CHECK(!g_cursor.inView); + CHECK(!g_cursor.viewReq); + + g_state.posInfoValid = true; + g_cursor.realigning = true; + core_handleMousePosition(50, 50); + CHECK(!m.keyboardGrabbed); + CHECK(!g_cursor.inView); + CHECK(!g_cursor.viewReq); + + lgInput_setFallback(&inputOps, NULL); + + reset(); + g_params.grabKeyboard = true; + g_state.ignoreInput = true; + + core_setGrabQuiet(true); + CHECK(!g_state.ignoreInput); + CHECK(m.keyboardGrabbed); + CHECK(count(EV_KEY_GRAB) == 1); + + reset(); + m.support = LG_DS_WARP_SCREEN; + m.captureGrab = true; + m.conf.req = false; + m.conf.on = false; + g_params.autoCapture = true; + g_params.grabKeyboard = true; + g_cursor.autoCaptureActive = true; + keyGrab(); + + core_setGrabQuiet(true); + m.count = 0; + core_setGrabQuiet(false); + CHECK(m.keyboardGrabbed); + CHECK(count(EV_KEY_GRAB) == 0); + CHECK(count(EV_KEY_UNGRAB) == 0); +} + static void testExitReentry(void) { startExit(); @@ -624,7 +776,6 @@ static void testCapAlign(void) m.lock.on = true; g_cursor.guest.x += 10; - g_cursor.warpState = WARP_STATE_ON; m.count = 0; core_setGrabQuiet(false); @@ -925,6 +1076,7 @@ static const struct Test tests[] = { { "exit-immediate" , testExitImmediate}, { "exit-guest" , testExitGuest }, { "absolute-sync" , testAbsoluteSync }, + { "auto-capture" , testAutoCapture }, { "exit-reentry" , testExitReentry }, { "view-immediate" , testViewImmediate}, { "capture-pending" , testCapWait }, diff --git a/doc/usage.rst b/doc/usage.rst index 9c923ddd..364333e7 100644 --- a/doc/usage.rst +++ b/doc/usage.rst @@ -328,7 +328,7 @@ All command line options +------------------------------+-------+---------------------+----------------------------------------------------------------------------------------------------------+ | input:mouseTrace | | no | Enable mouse input diagnostics | +------------------------------+-------+---------------------+----------------------------------------------------------------------------------------------------------+ - | input:autoCapture | | no | Try to keep the mouse captured when needed | + | input:autoCapture | | no | Grab the keyboard in the guest view and release it before exit | +------------------------------+-------+---------------------+----------------------------------------------------------------------------------------------------------+ | input:captureOnly | | no | Only enable input via SPICE if in capture mode | +------------------------------+-------+---------------------+----------------------------------------------------------------------------------------------------------+