diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index 0978037f..53523601 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -46,6 +46,43 @@ set_tests_properties(wayland-motion-tests PROPERTIES TIMEOUT 10 ) +add_executable(mouse-tests + mouse_test.c + ../src/core.c + ../src/util.c +) +target_compile_definitions(mouse-tests PRIVATE + CIMGUI_DEFINE_ENUMS_AND_STRUCTS=1 +) +target_include_directories(mouse-tests PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../src" + "${PROJECT_TOP}/repos/PureSpice/include" + "${PROJECT_TOP}/repos/gui/cimgui" + "${PROJECT_TOP}/repos/gui/cimgui/imgui" +) +target_link_libraries(mouse-tests + ${EXE_FLAGS} + lg_common +) +set(MOUSE_CASES + inset-exit + exit-delay + exit-guest + confine-pending + confine-guest + capture-pending + capture-revoked + capture-release +) +foreach(name IN LISTS MOUSE_CASES) + add_test(NAME mouse-${name} + COMMAND mouse-tests ${name} + ) + set_tests_properties(mouse-${name} PROPERTIES + TIMEOUT 10 + ) +endforeach() + find_package(GTest REQUIRED) find_program(WESTON_EXECUTABLE NAMES weston) if(NOT WESTON_EXECUTABLE) diff --git a/client/tests/mouse_test.c b/client/tests/mouse_test.c new file mode 100644 index 00000000..8bb51da8 --- /dev/null +++ b/client/tests/mouse_test.c @@ -0,0 +1,443 @@ +/** + * Looking Glass + * Copyright © 2017-2026 The Looking Glass Authors + * https://looking-glass.io + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation; either version 2 of the License, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., 59 + * Temple Place, Suite 330, Boston, MA 02111-1307 USA + */ + +#include "app.h" +#include "core.h" +#include "main.h" +#include "test.h" +#include "util.h" + +#include +#include +#include +#include + +enum EvType +{ + EV_GRAB, + EV_UNGRAB, + EV_CAPTURE, + EV_UNCAPTURE, + EV_WARP, + EV_GUEST, + EV_MOTION, + EV_VALID, +}; + +struct Ev +{ + enum EvType type; + double x; + double y; + bool exit; + bool conf; + bool lock; +}; + +struct Trap +{ + bool req; + bool on; + bool hold; +}; + +static struct +{ + enum LG_DSWarpSupport support; + struct Trap conf; + struct Trap lock; + bool valid; + struct Ev ev[256]; + unsigned int count; +} +m; + +struct AppState g_state; +struct CursorState g_cursor; +struct AppParams g_params; + +static void push(enum EvType type, double x, double y, bool leaving) +{ + CHECK(m.count < sizeof(m.ev) / sizeof(m.ev[0])); + m.ev[m.count++] = (struct Ev) { + .type = type, + .x = x, + .y = y, + .exit = leaving, + .conf = m.conf.on, + .lock = m.lock.on, + }; +} + +static unsigned int count(enum EvType type) +{ + unsigned int result = 0; + for (unsigned int i = 0; i < m.count; ++i) + if (m.ev[i].type == type) + ++result; + return result; +} + +static int first(enum EvType type) +{ + for (unsigned int i = 0; i < m.count; ++i) + if (m.ev[i].type == type) + return i; + return -1; +} + +static void guest(double x, double y, double localX, double localY) +{ + (void)x; + (void)y; + push(EV_GUEST, localX, localY, false); +} + +static void setPointer(LG_DSPointer pointer) +{ + (void)pointer; +} + +static void keyNoop(void) +{ +} + +static void grab(void) +{ + m.conf.req = true; + push(EV_GRAB, 0, 0, false); +} + +static void ungrab(void) +{ + push(EV_UNGRAB, 0, 0, false); + m.conf.req = false; + if (!m.conf.hold) + m.conf.on = false; +} + +static void capture(void) +{ + m.conf.req = false; + if (!m.conf.hold) + m.conf.on = false; + m.lock.req = true; + push(EV_CAPTURE, 0, 0, false); +} + +static void uncapture(void) +{ + push(EV_UNCAPTURE, 0, 0, false); + m.lock.req = false; + if (!m.lock.hold) + m.lock.on = false; +} + +static void warp(int x, int y, bool exiting) +{ + push(EV_WARP, x, y, exiting); +} + +static bool valid(int x, int y) +{ + push(EV_VALID, x, y, false); + return m.valid; +} + +static struct LG_DisplayServerOps ds = { + .name = "test", + .guestPointerUpdated = guest, + .setPointer = setPointer, + .grabKeyboard = keyNoop, + .ungrabKeyboard = keyNoop, + .grabPointer = grab, + .ungrabPointer = ungrab, + .capturePointer = capture, + .uncapturePointer = uncapture, + .warpPointer = warp, + .isValidPointerPos = valid, +}; + +bool app_getProp(LG_DSProperty prop, void * ret) +{ + if (prop != LG_DS_WARP_SUPPORT) + return false; + + *(enum LG_DSWarpSupport *)ret = m.support; + return true; +} + +bool app_guestIsWindows(void) +{ + return false; +} + +bool app_isOverlayMode(void) +{ + return false; +} + +bool app_isRunning(void) +{ + return false; +} + +uint64_t app_mouseSeq(void) +{ + return 0; +} + +void app_mouseTrace(const char * file, unsigned int line, + const char * function, uint64_t seq, const char * format, ...) +{ + (void)file; + (void)line; + (void)function; + (void)seq; + (void)format; +} + +bool purespice_mouseMotion(int32_t x, int32_t y) +{ + push(EV_MOTION, x, y, false); + return true; +} + +static void reset(void) +{ + memset(&m, 0, sizeof(m)); + memset(&g_state, 0, sizeof(g_state)); + memset(&g_cursor, 0, sizeof(g_cursor)); + memset(&g_params, 0, sizeof(g_params)); + + m.support = LG_DS_WARP_SURFACE; + m.valid = true; + + g_state.ds = &ds; + g_state.focused = true; + g_state.haveSrcSize = true; + g_state.posInfoValid = true; + g_state.formatValid = true; + g_state.windowW = 300; + g_state.windowH = 200; + g_state.srcSize = (struct Point) { 100, 80 }; + g_state.dstRect = (LG_RendererRect) { + .valid = true, + .x = 10, + .y = 20, + .w = 100, + .h = 80, + }; + + g_params.useSpiceInput = true; + g_params.scaleMouseInput = true; + + g_cursor.inWindow = true; + g_cursor.inView = true; + g_cursor.warpState = WARP_STATE_ON; + g_cursor.guest.valid = true; + g_cursor.scale = (struct DoublePoint) { 1.0, 1.0 }; +} + +static void setLocal(double x, double y) +{ + g_cursor.pos = (struct DoublePoint) { x, y }; + + struct DoublePoint guest; + util_localCurToGuest(&guest); + g_cursor.guest.x = lround(guest.x); + g_cursor.guest.y = lround(guest.y); +} + +static void testInsetExit(void) +{ + reset(); + setLocal(10, 20); + g_cursor.pos = (struct DoublePoint) { 9.75, 19.75 }; + + core_handleMouseNormal(-0.25, -0.25); + + const int drop = first(EV_UNGRAB); + const int move = first(EV_WARP); + CHECK(drop >= 0); + CHECK(count(EV_WARP) == 1); + CHECK(move > drop); + CHECK(m.ev[move].x == 9); + CHECK(m.ev[move].y == 19); + CHECK(m.ev[move].exit); + CHECK(!g_cursor.inView); + + core_handleGuestMouseUpdate(); + CHECK(count(EV_GUEST) == 0); +} + +static void startExit(void) +{ + reset(); + setLocal(109, 50); + m.conf.req = true; + m.conf.on = true; + m.conf.hold = true; + + core_handleMouseNormal(2, 0); +} + +static void testExitWait(void) +{ + startExit(); + const int drop = first(EV_UNGRAB); + CHECK(drop >= 0); + CHECK(count(EV_WARP) == 0); + CHECK(g_cursor.inView); +} + +static void testExitGuest(void) +{ + startExit(); + g_cursor.guest.x = 99; + core_handleGuestMouseUpdate(); + CHECK(count(EV_GUEST) == 0); +} + +static void startConf(void) +{ + reset(); + setLocal(50, 50); + g_cursor.inView = false; + + core_handleMouseNormal(0, 0); +} + +static void testConfWait(void) +{ + startConf(); + CHECK(m.conf.req); + CHECK(!m.conf.on); + CHECK(!g_cursor.inView); +} + +static void testConfGuest(void) +{ + startConf(); + core_handleGuestMouseUpdate(); + CHECK(m.conf.req); + CHECK(count(EV_GUEST) == 0); +} + +static void testCapWait(void) +{ + reset(); + setLocal(50, 50); + g_params.captureInputOnly = true; + + core_handleMouseGrabbed(1, 0); + CHECK(count(EV_MOTION) == 0); + + core_setGrabQuiet(true); + CHECK(g_cursor.grab); + CHECK(m.lock.req); + CHECK(!m.lock.on); + + core_handleMouseGrabbed(1, 0); + CHECK(count(EV_MOTION) == 0); +} + +static void testCapRevoke(void) +{ + reset(); + setLocal(50, 50); + g_params.captureInputOnly = true; + core_setGrabQuiet(true); + + m.lock.on = true; + core_handleMouseGrabbed(1, 0); + CHECK(count(EV_MOTION) == 1); + CHECK(m.ev[first(EV_MOTION)].lock); + + m.lock.on = false; + core_handleMouseGrabbed(1, 0); + CHECK(g_cursor.grab); + CHECK(count(EV_MOTION) == 1); +} + +static void testCapRelease(void) +{ + reset(); + setLocal(50, 50); + g_params.captureInputOnly = true; + core_setGrabQuiet(true); + + m.lock.on = true; + core_handleMouseGrabbed(1, 0); + CHECK(count(EV_MOTION) == 1); + + m.lock.hold = true; + core_setGrabQuiet(false); + CHECK(!g_cursor.grab); + CHECK(!m.lock.req); + CHECK(m.lock.on); + + const int drop = first(EV_UNCAPTURE); + CHECK(drop >= 0); + CHECK(m.ev[drop].lock); + + core_handleMouseGrabbed(1, 0); + CHECK(count(EV_MOTION) == 1); +} + +struct Test +{ + const char * name; + void (*run)(void); +}; + +static const struct Test tests[] = { + { "inset-exit" , testInsetExit }, + { "exit-delay" , testExitWait }, + { "exit-guest" , testExitGuest }, + { "confine-pending", testConfWait }, + { "confine-guest" , testConfGuest }, + { "capture-pending", testCapWait }, + { "capture-revoked", testCapRevoke }, + { "capture-release", testCapRelease }, +}; + +int main(int argc, char ** argv) +{ + if (argc == 2) + { + for (unsigned int i = 0; i < sizeof(tests) / sizeof(tests[0]); ++i) + if (strcmp(argv[1], tests[i].name) == 0) + { + tests[i].run(); + return 0; + } + + fprintf(stderr, "unknown test: %s\n", argv[1]); + return EXIT_FAILURE; + } + + if (argc != 1) + return EXIT_FAILURE; + + for (unsigned int i = 0; i < sizeof(tests) / sizeof(tests[0]); ++i) + tests[i].run(); + return 0; +}