/** * 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; core_handleGrabEvent(false); } } static bool grabbed(void) { return m.conf.on; } static void capture(void) { if (m.support == LG_DS_WARP_NONE) { grab(); return; } 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) { if (m.support == LG_DS_WARP_NONE) { ungrab(); return; } push(EV_UNCAPTURE, 0, 0, false); m.lock.req = false; if (!m.lock.hold) m.lock.on = false; } static bool captured(void) { return m.support == LG_DS_WARP_NONE ? m.conf.on : m.lock.on; } 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, .isPointerGrabbed = grabbed, .capturePointer = capture, .uncapturePointer = uncapture, .isPointerCaptured = captured, .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; m.conf.req = true; m.conf.on = 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.viewReq = 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 bool near(double a, double b) { return fabs(a - b) < 0.000001; } 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.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); m.conf.req = false; m.conf.on = false; g_cursor.inView = false; g_cursor.viewReq = false; core_handleMouseNormal(0, 0); } static void setConf(bool active) { m.conf.on = active; core_handleGrabEvent(active); } static void testConfWait(void) { startConf(); CHECK(m.conf.req); CHECK(!m.conf.on); CHECK(!g_cursor.inView); setConf(true); CHECK(g_cursor.inView); } static void testConfGuest(void) { startConf(); core_handleGuestMouseUpdate(); CHECK(m.conf.req); CHECK(count(EV_GUEST) == 0); setConf(true); core_handleGuestMouseUpdate(); CHECK(count(EV_GUEST) == 1); } 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); } static void testCapFallback(void) { reset(); setLocal(50, 50); m.support = LG_DS_WARP_NONE; g_params.captureInputOnly = true; core_setGrabQuiet(true); m.conf.on = true; core_handleMouseGrabbed(1, 0); CHECK(count(EV_MOTION) == 1); m.conf.on = false; core_handleMouseGrabbed(1, 0); CHECK(count(EV_MOTION) == 1); } static void testRotateScale(void) { reset(); g_state.dstRect = (LG_RendererRect) { .valid = true, .x = 10, .y = 20, .w = 200, .h = 100, }; g_cursor.scale = (struct DoublePoint) { 2, 4 }; g_cursor.guest.x = 40; g_cursor.guest.y = 20; for (int rot = 0; rot < LG_ROTATE_MAX; ++rot) { g_state.rotate = rot; struct DoublePoint local; CHECK(util_guestCurToLocal(&local)); g_cursor.pos = local; struct DoublePoint guest; util_localCurToGuest(&guest); CHECK(near(guest.x, 40)); CHECK(near(guest.y, 20)); } } static void testGeometry(void) { static const struct DoublePoint expected[] = { { 30, 25 }, { 205, 40 }, { 190, 115 }, { 15, 100 }, }; static const struct DoublePoint rotated[] = { { 1, 2 }, { 2, -1 }, { -1, -2 }, { -2, 1 }, }; reset(); g_state.dstRect = (LG_RendererRect) { .valid = true, .x = 10, .y = 20, .w = 200, .h = 100, }; g_cursor.scale = (struct DoublePoint) { 2, 4 }; g_cursor.guest.x = 40; g_cursor.guest.y = 20; for (int rot = 0; rot < LG_ROTATE_MAX; ++rot) { g_state.rotate = rot; struct DoublePoint local; CHECK(util_guestCurToLocal(&local)); CHECK(near(local.x, expected[rot].x)); CHECK(near(local.y, expected[rot].y)); struct DoublePoint delta = { 1, 2 }; util_rotatePoint(&delta); CHECK(near(delta.x, rotated[rot].x)); CHECK(near(delta.y, rotated[rot].y)); } static const double scales[] = { 0.5, 1.0, 2.0 }; for (unsigned int i = 0; i < sizeof(scales) / sizeof(scales[0]); ++i) for (int rot = 0; rot < LG_ROTATE_MAX; ++rot) { g_state.rotate = rot; g_cursor.scale.x = scales[i]; g_cursor.scale.y = scales[i]; g_cursor.guest.x = 40; g_cursor.guest.y = 20; struct DoublePoint local; CHECK(util_guestCurToLocal(&local)); g_cursor.pos = local; struct DoublePoint round; util_localCurToGuest(&round); CHECK(near(round.x, 40)); CHECK(near(round.y, 20)); } g_cursor.guest.valid = false; struct DoublePoint local = { -1, -2 }; CHECK(!util_guestCurToLocal(&local)); CHECK(near(local.x, -1)); CHECK(near(local.y, -2)); } static struct DoublePoint inputFor(int rot, double x, double y) { switch (rot) { case LG_ROTATE_0: return (struct DoublePoint) { x, y }; case LG_ROTATE_90: return (struct DoublePoint) { -y, x }; case LG_ROTATE_180: return (struct DoublePoint) { -x, -y }; case LG_ROTATE_270: return (struct DoublePoint) { y, -x }; } CHECK(false); return (struct DoublePoint) { 0, 0 }; } static void testEdges(void) { static const struct { struct Point pos; struct Point delta; struct Point target; } cases[] = { { { 10, 60 }, { -1, 0 }, { 9, 60 } }, { { 109, 60 }, { 1, 0 }, { 110, 60 } }, { { 60, 20 }, { 0, -1 }, { 60, 19 } }, { { 60, 99 }, { 0, 1 }, { 60, 100 } }, { { 10, 20 }, { -1, -1 }, { 9, 19 } }, { { 109, 20 }, { 1, -1 }, { 110, 19 } }, { { 10, 99 }, { -1, 1 }, { 9, 100 } }, { { 109, 99 }, { 1, 1 }, { 110, 100 } }, }; for (int rot = 0; rot < LG_ROTATE_MAX; ++rot) for (unsigned int i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) { reset(); g_state.rotate = rot; setLocal(cases[i].pos.x, cases[i].pos.y); const struct DoublePoint input = inputFor(rot, cases[i].delta.x, cases[i].delta.y); core_handleMouseNormal(input.x, input.y); const int index = first(EV_WARP); CHECK(index >= 0); CHECK(m.ev[index].x == cases[i].target.x); CHECK(m.ev[index].y == cases[i].target.y); CHECK(m.ev[index].exit); } } static void testScales(void) { static const struct { double scale; double pos; double delta; int target; } cases[] = { { 0.5, 108, 4, 110 }, { 1.0, 109, 1, 110 }, { 2.0, 109, 1, 111 }, }; for (unsigned int i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) { reset(); g_cursor.useScale = true; g_cursor.scale.x = cases[i].scale; g_cursor.scale.y = cases[i].scale; setLocal(cases[i].pos, 50); core_handleMouseNormal(cases[i].delta, 0); const int index = first(EV_WARP); CHECK(index >= 0); CHECK(m.ev[index].x == cases[i].target); CHECK(m.ev[index].y == 50); } } static void testBorderExit(void) { static const struct { struct Border border; struct Point valid; } cases[] = { { { 8, 24, 8, 8 }, { 219, 274 } }, { { 0, 0, 0, 0 }, { 211, 250 } }, }; for (unsigned int i = 0; i < sizeof(cases) / sizeof(cases[0]); ++i) { reset(); m.support = LG_DS_WARP_SCREEN; g_state.windowPos = (struct Point) { 100, 200 }; g_state.border = cases[i].border; setLocal(109, 50); core_handleMouseNormal(2, 0); const int check = first(EV_VALID); const int move = first(EV_WARP); CHECK(check >= 0); CHECK(move > check); CHECK(m.ev[check].x == cases[i].valid.x); CHECK(m.ev[check].y == cases[i].valid.y); CHECK(m.ev[move].x == 111); CHECK(m.ev[move].y == 50); CHECK(m.ev[move].exit); } } 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 }, { "capture-fallback", testCapFallback }, { "rotate-scale" , testRotateScale }, { "geometry" , testGeometry }, { "edges" , testEdges }, { "scales" , testScales }, { "border-exit" , testBorderExit }, }; 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; }