Files
LookingGlass/client/tests/mouse_test.c
Geoffrey McRae a0b7b12ffc [client] tree: restore unrelated working changes
Restore client and test files that were already staged when the OBS
cadence commit was created. Keep that work out of the scheduler series
while leaving its working-tree contents untouched.
2026-08-06 18:02:08 +10:00

783 lines
16 KiB
C

/**
* 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 <math.h>
#include <stdarg.h>
#include <stdint.h>
#include <string.h>
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 setConf(bool active)
{
m.conf.on = active;
core_handleGrabEvent(active);
}
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);
CHECK(!g_cursor.viewReq);
CHECK(g_cursor.exit);
g_cursor.pos = (struct DoublePoint) { 111, 50 };
g_cursor.warpState = WARP_STATE_OFF;
setConf(false);
const int move = first(EV_WARP);
CHECK(move > drop);
CHECK(count(EV_WARP) == 1);
CHECK(m.ev[move].x == 111);
CHECK(m.ev[move].y == 50);
CHECK(m.ev[move].exit);
CHECK(!m.ev[move].conf);
CHECK(!g_cursor.inView);
CHECK(!g_cursor.exit);
setConf(false);
CHECK(count(EV_WARP) == 1);
}
static void testExitGuest(void)
{
startExit();
g_cursor.guest.x = 99;
core_handleGuestMouseUpdate();
CHECK(count(EV_GUEST) == 0);
}
static void testExitCancel(void)
{
startExit();
CHECK(g_cursor.exit);
core_handleMouseNormal(1, 0);
CHECK(g_cursor.exit);
CHECK(!g_cursor.viewReq);
core_handleMouseNormal(-3, 0);
CHECK(!g_cursor.exit);
CHECK(g_cursor.viewReq);
CHECK(m.conf.req);
CHECK(count(EV_WARP) == 0);
setConf(false);
CHECK(count(EV_WARP) == 0);
setConf(true);
CHECK(g_cursor.inView);
startExit();
g_state.focused = false;
core_setCursorInView(false);
CHECK(!g_cursor.exit);
setConf(false);
CHECK(count(EV_WARP) == 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 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 },
{ "exit-cancel" , testExitCancel },
{ "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;
}