From b8955fd012966a854e0bf0e3d577ff69a6ba4a91 Mon Sep 17 00:00:00 2001 From: Geoffrey McRae Date: Mon, 10 Aug 2026 17:15:11 +1000 Subject: [PATCH] [client] input: add display server behaviour tests Extract input policy from the Wayland and X11 protocol handlers. Exercise both implementations against literal Wayland event traces. --- client/displayservers/Wayland/CMakeLists.txt | 1 + client/displayservers/Wayland/cursor.c | 2 +- client/displayservers/Wayland/input.c | 268 +++--- client/displayservers/Wayland/input_event.c | 179 ++++ client/displayservers/Wayland/input_event.h | 63 ++ client/displayservers/Wayland/wayland.h | 5 +- client/displayservers/X11/CMakeLists.txt | 1 + client/displayservers/X11/input_event.c | 183 ++++ client/displayservers/X11/input_event.h | 69 ++ client/displayservers/X11/x11.c | 251 +++--- client/displayservers/X11/x11.h | 4 +- client/displayservers/input.h | 44 + client/tests/CMakeLists.txt | 40 + client/tests/displayserver_input_test.c | 877 +++++++++++++++++++ 14 files changed, 1731 insertions(+), 256 deletions(-) create mode 100644 client/displayservers/Wayland/input_event.c create mode 100644 client/displayservers/Wayland/input_event.h create mode 100644 client/displayservers/X11/input_event.c create mode 100644 client/displayservers/X11/input_event.h create mode 100644 client/displayservers/input.h create mode 100644 client/tests/displayserver_input_test.c diff --git a/client/displayservers/Wayland/CMakeLists.txt b/client/displayservers/Wayland/CMakeLists.txt index 5ffb97b3..12775999 100644 --- a/client/displayservers/Wayland/CMakeLists.txt +++ b/client/displayservers/Wayland/CMakeLists.txt @@ -19,6 +19,7 @@ add_library(displayserver_Wayland STATIC icon.c idle.c input.c + input_event.c output.c poll.c presentation.c diff --git a/client/displayservers/Wayland/cursor.c b/client/displayservers/Wayland/cursor.c index e07cd8e8..173e38dd 100644 --- a/client/displayservers/Wayland/cursor.c +++ b/client/displayservers/Wayland/cursor.c @@ -240,7 +240,7 @@ void waylandSetPointer(LG_DSPointer pointer) wlWm.cursorHotY = wlWm.cursorHot[pointer].y; MTRACE("set id=%d surface=%p hot=%d,%d scale=%d entered=%d", pointer, (void *)wlWm.cursor, wlWm.cursorHotX, wlWm.cursorHotY, - wlWm.cursorScale, wlWm.pointerInSurface); + wlWm.cursorScale, wlWm.input.pointerInSurface); if (wlWm.pointer) wl_pointer_set_cursor(wlWm.pointer, wlWm.pointerEnterSerial, wlWm.cursor, wlWm.cursorHotX, wlWm.cursorHotY); } diff --git a/client/displayservers/Wayland/input.c b/client/displayservers/Wayland/input.c index d3bbd916..96c1e900 100644 --- a/client/displayservers/Wayland/input.c +++ b/client/displayservers/Wayland/input.c @@ -32,9 +32,6 @@ #include "app.h" #include "common/debug.h" -const double WL_SCROLL_STEP = 15.0; -const double WL_HALF_SCROLL_STEP = WL_SCROLL_STEP / 2.0; - #define MTRACE(fmt, ...) \ do \ { \ @@ -59,6 +56,93 @@ static uint32_t proxyId(void * proxy) return proxy ? wl_proxy_get_id(proxy) : 0; } +static void inputPosition(void * opaque, double x, double y) +{ + (void)opaque; + app_updateCursorPos(x, y); +} + +static void inputRelative(void * opaque, double x, double y, + double rawX, double rawY) +{ + (void)opaque; + app_handleMouseRelative(x, y, rawX, rawY); +} + +static void inputButton(void * opaque, unsigned int button, bool pressed) +{ + (void)opaque; + if (pressed) + app_handleButtonPress(button); + else + app_handleButtonRelease(button); +} + +static void inputWheel(void * opaque, double motion) +{ + (void)opaque; + app_handleWheelMotion(motion); +} + +static void inputKey(void * opaque, int scancode, bool pressed) +{ + (void)opaque; + if (pressed) + app_handleKeyPress(scancode); + else + app_handleKeyRelease(scancode); +} + +static void inputText(void * opaque, const char * text) +{ + (void)opaque; + /* Test overlay state after the key callback, matching the native event + * handler's ordering. */ + if (app_isOverlayMode()) + app_handleKeyboardTyped(text); +} + +static void inputModifiers(void * opaque, bool ctrl, bool shift, bool alt, + bool super) +{ + (void)opaque; + app_handleKeyboardModifiers(ctrl, shift, alt, super); +} + +static void inputLEDs(void * opaque, bool numLock, bool capsLock, + bool scrollLock) +{ + (void)opaque; + app_handleKeyboardLEDs(numLock, capsLock, scrollLock); +} + +static void inputEnter(void * opaque, bool entered) +{ + (void)opaque; + app_handleEnterEvent(entered); +} + +static void inputFocus(void * opaque, bool focused) +{ + (void)opaque; + if (!focused) + waylandCBInvalidate(); + app_handleFocusEvent(focused); +} + +static const LG_DSInputSink inputSink = { + .position = inputPosition, + .relative = inputRelative, + .button = inputButton, + .wheel = inputWheel, + .key = inputKey, + .text = inputText, + .modifiers = inputModifiers, + .leds = inputLEDs, + .enter = inputEnter, + .focus = inputFocus, +}; + // Mouse-handling listeners. static void pointerMotionHandler(void * data, struct wl_pointer * pointer, @@ -68,7 +152,7 @@ static void pointerMotionHandler(void * data, struct wl_pointer * pointer, wl_fixed_to_double(syW)); MTRACE("abs time=%u pos=%.3f,%.3f", time, wlWm.motion.x, wlWm.motion.y); - app_updateCursorPos(wlWm.motion.x, wlWm.motion.y); + wlInputPointerMotion(&wlWm.input, wlWm.motion.x, wlWm.motion.y); } static void pointerEnterHandler(void * data, struct wl_pointer * pointer, @@ -79,18 +163,15 @@ static void pointerEnterHandler(void * data, struct wl_pointer * pointer, surface == wlWm.surface, (void *)surface, serial, wl_fixed_to_double(sxW), wl_fixed_to_double(syW)); - if (surface != wlWm.surface) + if (!wlInputPointerEnter(&wlWm.input, surface == wlWm.surface)) return; - wlWm.pointerInSurface = true; - app_handleEnterEvent(true); - wl_pointer_set_cursor(pointer, serial, wlWm.cursor, wlWm.cursorHotX, wlWm.cursorHotY); wlWm.pointerEnterSerial = serial; wlMotionAbs(&wlWm.motion, wl_fixed_to_double(sxW), wl_fixed_to_double(syW)); - app_updateCursorPos(wlWm.motion.x, wlWm.motion.y); + wlInputPointerMotion(&wlWm.input, wlWm.motion.x, wlWm.motion.y); } static void pointerLeaveHandler(void * data, struct wl_pointer * pointer, @@ -99,76 +180,21 @@ static void pointerLeaveHandler(void * data, struct wl_pointer * pointer, MTRACE("leave main=%d surface=%p serial=%u", surface == wlWm.surface, (void *)surface, serial); - if (surface != wlWm.surface) - return; - - wlWm.pointerInSurface = false; - app_handleEnterEvent(false); + wlInputPointerLeave(&wlWm.input, surface == wlWm.surface); } static void pointerAxisHandler(void * data, struct wl_pointer * pointer, uint32_t serial, uint32_t axis, wl_fixed_t value) { - if (axis != WL_POINTER_AXIS_VERTICAL_SCROLL) - return; - - double delta = wl_fixed_to_double(value); - - wlWm.scrollState += delta; - - while (wlWm.scrollState > WL_HALF_SCROLL_STEP) - { - app_handleButtonPress(5 /* SPICE_MOUSE_BUTTON_DOWN */); - app_handleButtonRelease(5 /* SPICE_MOUSE_BUTTON_DOWN */); - wlWm.scrollState -= WL_SCROLL_STEP; - } - - while (wlWm.scrollState < -WL_HALF_SCROLL_STEP) - { - app_handleButtonPress(4 /* SPICE_MOUSE_BUTTON_UP */); - app_handleButtonRelease(4 /* SPICE_MOUSE_BUTTON_UP */); - wlWm.scrollState += WL_SCROLL_STEP; - } - - app_handleWheelMotion(delta / WL_SCROLL_STEP); -} - -static int mapWaylandButton(uint32_t button) -{ - switch (button) - { - case BTN_LEFT: - return 1; - case BTN_MIDDLE: - return 2; - case BTN_RIGHT: - return 3; - case BTN_SIDE: - return 6; - case BTN_EXTRA: - return 7; - case BTN_FORWARD: - return 8; - case BTN_BACK: - return 9; - case BTN_TASK: - return 10; - } - - return 0; + wlInputPointerAxis(&wlWm.input, + axis == WL_POINTER_AXIS_VERTICAL_SCROLL, wl_fixed_to_double(value)); } static void pointerButtonHandler(void *data, struct wl_pointer *pointer, uint32_t serial, uint32_t time, uint32_t button, uint32_t stateW) { - button = mapWaylandButton(button); - if (!button) - return; - - if (stateW == WL_POINTER_BUTTON_STATE_PRESSED) - app_handleButtonPress(button); - else - app_handleButtonRelease(button); + wlInputPointerButton(&wlWm.input, button, + stateW == WL_POINTER_BUTTON_STATE_PRESSED); } static const struct wl_pointer_listener pointerListener = { @@ -223,21 +249,18 @@ static void relativePointerMotionHandler(void * data, wl_fixed_t dxW, wl_fixed_t dyW, wl_fixed_t dxUnaccelW, wl_fixed_t dyUnaccelW) { - if (!atomic_load_explicit(&wlWm.lockActive, memory_order_acquire)) - return; - + const bool active = + atomic_load_explicit(&wlWm.lockActive, memory_order_acquire); const double dx = wl_fixed_to_double(dxW); const double dy = wl_fixed_to_double(dyW); - MTRACE("rel time=%u:%u delta=%.3f,%.3f raw=%.3f,%.3f " - "pos=%.3f,%.3f", timeHi, timeLo, dx, dy, - wl_fixed_to_double(dxUnaccelW), wl_fixed_to_double(dyUnaccelW), - wlWm.motion.x, wlWm.motion.y); + const double rawX = wl_fixed_to_double(dxUnaccelW); + const double rawY = wl_fixed_to_double(dyUnaccelW); + if (active) + MTRACE("rel time=%u:%u delta=%.3f,%.3f raw=%.3f,%.3f " + "pos=%.3f,%.3f", timeHi, timeLo, dx, dy, rawX, rawY, + wlWm.motion.x, wlWm.motion.y); - app_handleMouseRelative( - dx, - dy, - wl_fixed_to_double(dxUnaccelW), - wl_fixed_to_double(dyUnaccelW)); + wlInputRelativeMotion(&wlWm.input, active, dx, dy, rawX, rawY); } static const struct zwp_relative_pointer_v1_listener relativePointerListener = { @@ -314,51 +337,46 @@ static void keyboardEnterHandler(void * data, struct wl_keyboard * keyboard, uint32_t serial, struct wl_surface * surface, struct wl_array * keys) { if (surface != wlWm.surface) + { + wlInputKeyboardEnter(&wlWm.input, false, NULL, 0); return; + } - wlWm.focusedOnSurface = true; - app_handleFocusEvent(true); wlWm.keyboardEnterSerial = serial; - - uint32_t * key; - wl_array_for_each(key, keys) - app_handleKeyPress(*key); + wlInputKeyboardEnter(&wlWm.input, true, keys->data, + keys->size / sizeof(uint32_t)); } static void keyboardLeaveHandler(void * data, struct wl_keyboard * keyboard, uint32_t serial, struct wl_surface * surface) { - if (surface != wlWm.surface) - return; - - wlWm.focusedOnSurface = false; - waylandCBInvalidate(); - app_handleFocusEvent(false); + wlInputKeyboardLeave(&wlWm.input, surface == wlWm.surface); } static void keyboardKeyHandler(void * data, struct wl_keyboard * keyboard, uint32_t serial, uint32_t time, uint32_t key, uint32_t state) { - if (!wlWm.focusedOnSurface) + if (!wlWm.input.focused) return; - if (state == WL_KEYBOARD_KEY_STATE_PRESSED) - app_handleKeyPress(key); - else - app_handleKeyRelease(key); - - if (!wlWm.xkbState || !app_isOverlayMode() || state != WL_KEYBOARD_KEY_STATE_PRESSED) + const bool pressed = state == WL_KEYBOARD_KEY_STATE_PRESSED; + if (!wlWm.xkbState || !pressed) + { + wlInputKeyboardKey(&wlWm.input, key, pressed, NULL); return; + } - key += 8; // xkb scancode is evdev scancode + 8 - int size = xkb_state_key_get_utf8(wlWm.xkbState, key, NULL, 0); - + const xkb_keycode_t xkbKey = key + 8; + const int size = xkb_state_key_get_utf8(wlWm.xkbState, xkbKey, NULL, 0); if (size <= 0) + { + wlInputKeyboardKey(&wlWm.input, key, true, NULL); return; + } char buffer[size + 1]; - xkb_state_key_get_utf8(wlWm.xkbState, key, buffer, size + 1); - app_handleKeyboardTyped(buffer); + xkb_state_key_get_utf8(wlWm.xkbState, xkbKey, buffer, size + 1); + wlInputKeyboardKey(&wlWm.input, key, true, buffer); } static void keyboardModifiersHandler(void * data, @@ -369,18 +387,24 @@ static void keyboardModifiersHandler(void * data, return; xkb_state_update_mask(wlWm.xkbState, modsDepressed, modsLatched, modsLocked, 0, 0, group); - app_handleKeyboardModifiers( - xkb_state_mod_name_is_active(wlWm.xkbState, XKB_MOD_NAME_CTRL, XKB_STATE_MODS_EFFECTIVE) > 0, - xkb_state_mod_name_is_active(wlWm.xkbState, XKB_MOD_NAME_SHIFT, XKB_STATE_MODS_EFFECTIVE) > 0, - xkb_state_mod_name_is_active(wlWm.xkbState, XKB_MOD_NAME_ALT, XKB_STATE_MODS_EFFECTIVE) > 0, - xkb_state_mod_name_is_active(wlWm.xkbState, XKB_MOD_NAME_LOGO, XKB_STATE_MODS_EFFECTIVE) > 0 - ); + const bool ctrl = xkb_state_mod_name_is_active(wlWm.xkbState, + XKB_MOD_NAME_CTRL, XKB_STATE_MODS_EFFECTIVE) > 0; + const bool shift = xkb_state_mod_name_is_active(wlWm.xkbState, + XKB_MOD_NAME_SHIFT, XKB_STATE_MODS_EFFECTIVE) > 0; + const bool alt = xkb_state_mod_name_is_active(wlWm.xkbState, + XKB_MOD_NAME_ALT, XKB_STATE_MODS_EFFECTIVE) > 0; + const bool super = xkb_state_mod_name_is_active(wlWm.xkbState, + XKB_MOD_NAME_LOGO, XKB_STATE_MODS_EFFECTIVE) > 0; - app_handleKeyboardLEDs( - xkb_state_led_name_is_active(wlWm.xkbState, XKB_LED_NAME_NUM) > 0, - xkb_state_led_name_is_active(wlWm.xkbState, XKB_LED_NAME_CAPS) > 0, - xkb_state_led_name_is_active(wlWm.xkbState, XKB_LED_NAME_SCROLL) > 0 - ); + const bool numLock = xkb_state_led_name_is_active(wlWm.xkbState, + XKB_LED_NAME_NUM) > 0; + const bool capsLock = xkb_state_led_name_is_active(wlWm.xkbState, + XKB_LED_NAME_CAPS) > 0; + const bool scrollLock = xkb_state_led_name_is_active(wlWm.xkbState, + XKB_LED_NAME_SCROLL) > 0; + + wlInputKeyboardState(&wlWm.input, ctrl, shift, alt, super, + numLock, capsLock, scrollLock); } static const struct wl_keyboard_listener keyboardListener = { @@ -468,13 +492,10 @@ static void waylandCleanUpPointer(bool notify) wl_pointer_destroy(wlWm.pointer); wlWm.pointer = NULL; - wlWm.pointerInSurface = false; }); MLOG(lockSeq, "lock destroy id=%u why=pointer", lockId); - - if (notify) - app_handleEnterEvent(false); + wlInputPointerDisconnect(&wlWm.input, notify); } // Seat-handling listeners. @@ -531,6 +552,8 @@ static const struct wl_seat_listener seatListener = { bool waylandInputInit(bool allowNoInput) { + wlInputInit(&wlWm.input, &inputSink, NULL); + if (!wlWm.seat) { MTRACE("input seat=0 allowNone=%d", allowNoInput); @@ -672,7 +695,7 @@ void waylandWarpPointer(int x, int y, bool exiting) { const int reqX = x; const int reqY = y; - if (!wlWm.pointerInSurface) + if (!wlWm.input.pointerInSurface) { MTRACE("warp drop=surface target=%d,%d exit=%d", x, y, exiting); return; @@ -737,8 +760,7 @@ void waylandWarpPointer(int x, int y, bool exiting) void waylandRealignPointer(void) { - if (wlWm.pointerInSurface) - app_updateCursorPos(wlWm.motion.x, wlWm.motion.y); + wlInputRealignPointer(&wlWm.input, wlWm.motion.x, wlWm.motion.y); } void waylandGuestPointerUpdated(double x, double y, double localX, diff --git a/client/displayservers/Wayland/input_event.c b/client/displayservers/Wayland/input_event.c new file mode 100644 index 00000000..b4fed665 --- /dev/null +++ b/client/displayservers/Wayland/input_event.c @@ -0,0 +1,179 @@ +/** + * 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 "input_event.h" + +#include +#include + +static const double WL_SCROLL_STEP = 15.0; +static const double WL_HALF_SCROLL_STEP = 7.5; + +void wlInputInit(WaylandInput * input, const LG_DSInputSink * sink, + void * opaque) +{ + memset(input, 0, sizeof(*input)); + input->sink = sink; + input->opaque = opaque; +} + +bool wlInputPointerEnter(WaylandInput * input, bool mainSurface) +{ + if (!mainSurface) + return false; + + input->pointerInSurface = true; + input->sink->enter(input->opaque, true); + return true; +} + +bool wlInputPointerLeave(WaylandInput * input, bool mainSurface) +{ + if (!mainSurface) + return false; + + input->pointerInSurface = false; + input->sink->enter(input->opaque, false); + return true; +} + +void wlInputPointerDisconnect(WaylandInput * input, bool notify) +{ + input->pointerInSurface = false; + if (notify) + input->sink->enter(input->opaque, false); +} + +void wlInputPointerMotion(WaylandInput * input, double x, double y) +{ + input->sink->position(input->opaque, x, y); +} + +static unsigned int mapButton(uint32_t button) +{ + switch (button) + { + case BTN_LEFT: + return 1; + case BTN_MIDDLE: + return 2; + case BTN_RIGHT: + return 3; + case BTN_SIDE: + return 6; + case BTN_EXTRA: + return 7; + case BTN_FORWARD: + return 8; + case BTN_BACK: + return 9; + case BTN_TASK: + return 10; + } + + return 0; +} + +void wlInputPointerButton(WaylandInput * input, uint32_t button, + bool pressed) +{ + const unsigned int mapped = mapButton(button); + if (mapped) + input->sink->button(input->opaque, mapped, pressed); +} + +void wlInputPointerAxis(WaylandInput * input, bool vertical, double delta) +{ + if (!vertical) + return; + + input->scrollState += delta; + + while (input->scrollState > WL_HALF_SCROLL_STEP) + { + input->sink->button(input->opaque, 5, true); + input->sink->button(input->opaque, 5, false); + input->scrollState -= WL_SCROLL_STEP; + } + + while (input->scrollState < -WL_HALF_SCROLL_STEP) + { + input->sink->button(input->opaque, 4, true); + input->sink->button(input->opaque, 4, false); + input->scrollState += WL_SCROLL_STEP; + } + + input->sink->wheel(input->opaque, delta / WL_SCROLL_STEP); +} + +void wlInputRelativeMotion(WaylandInput * input, bool active, + double x, double y, double rawX, double rawY) +{ + if (active) + input->sink->relative(input->opaque, x, y, rawX, rawY); +} + +void wlInputRealignPointer(WaylandInput * input, double x, double y) +{ + if (input->pointerInSurface) + input->sink->position(input->opaque, x, y); +} + +bool wlInputKeyboardEnter(WaylandInput * input, bool mainSurface, + const uint32_t * keys, size_t count) +{ + if (!mainSurface) + return false; + + input->focused = true; + input->sink->focus(input->opaque, true); + for (size_t i = 0; i < count; ++i) + input->sink->key(input->opaque, keys[i], true); + return true; +} + +bool wlInputKeyboardLeave(WaylandInput * input, bool mainSurface) +{ + if (!mainSurface) + return false; + + input->focused = false; + input->sink->focus(input->opaque, false); + return true; +} + +void wlInputKeyboardKey(WaylandInput * input, uint32_t key, bool pressed, + const char * text) +{ + if (!input->focused) + return; + + input->sink->key(input->opaque, key, pressed); + if (pressed && text && *text) + input->sink->text(input->opaque, text); +} + +void wlInputKeyboardState(WaylandInput * input, + bool ctrl, bool shift, bool alt, bool super, + bool numLock, bool capsLock, bool scrollLock) +{ + input->sink->modifiers(input->opaque, ctrl, shift, alt, super); + input->sink->leds(input->opaque, numLock, capsLock, scrollLock); +} diff --git a/client/displayservers/Wayland/input_event.h b/client/displayservers/Wayland/input_event.h new file mode 100644 index 00000000..0264d9ef --- /dev/null +++ b/client/displayservers/Wayland/input_event.h @@ -0,0 +1,63 @@ +/** + * 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 + */ + +#ifndef _H_LG_WAYLAND_INPUT_EVENT_ +#define _H_LG_WAYLAND_INPUT_EVENT_ + +#include +#include +#include + +#include "../input.h" + +typedef struct WaylandInput +{ + const LG_DSInputSink * sink; + void * opaque; + double scrollState; + bool pointerInSurface; + bool focused; +} +WaylandInput; + +void wlInputInit(WaylandInput * input, const LG_DSInputSink * sink, + void * opaque); + +bool wlInputPointerEnter(WaylandInput * input, bool mainSurface); +bool wlInputPointerLeave(WaylandInput * input, bool mainSurface); +void wlInputPointerDisconnect(WaylandInput * input, bool notify); +void wlInputPointerMotion(WaylandInput * input, double x, double y); +void wlInputPointerButton(WaylandInput * input, uint32_t button, + bool pressed); +void wlInputPointerAxis(WaylandInput * input, bool vertical, double delta); +void wlInputRelativeMotion(WaylandInput * input, bool active, + double x, double y, double rawX, double rawY); +void wlInputRealignPointer(WaylandInput * input, double x, double y); + +bool wlInputKeyboardEnter(WaylandInput * input, bool mainSurface, + const uint32_t * keys, size_t count); +bool wlInputKeyboardLeave(WaylandInput * input, bool mainSurface); +void wlInputKeyboardKey(WaylandInput * input, uint32_t key, bool pressed, + const char * text); +void wlInputKeyboardState(WaylandInput * input, + bool ctrl, bool shift, bool alt, bool super, + bool numLock, bool capsLock, bool scrollLock); + +#endif diff --git a/client/displayservers/Wayland/wayland.h b/client/displayservers/Wayland/wayland.h index ce448851..6c872631 100644 --- a/client/displayservers/Wayland/wayland.h +++ b/client/displayservers/Wayland/wayland.h @@ -57,6 +57,7 @@ #include "scale.h" #include "motion.h" +#include "input_event.h" typedef void (*WaylandPollCallback)(uint32_t events, void * opaque); typedef void (*WaylandPollCleanup)(void * opaque); @@ -128,8 +129,7 @@ struct WaylandDSState { _Atomic(bool) lockActive; bool keyboardGrabbed; - bool pointerInSurface; - bool focusedOnSurface; + WaylandInput input; WL_DesktopOps * desktop; @@ -147,7 +147,6 @@ struct WaylandDSState bool configured; bool warpSupport; struct WlMotion motion; - double scrollState; #if defined(ENABLE_EGL) || defined(ENABLE_OPENGL) struct wl_egl_window * eglWindow; diff --git a/client/displayservers/X11/CMakeLists.txt b/client/displayservers/X11/CMakeLists.txt index e366e6cb..fd34d42b 100644 --- a/client/displayservers/X11/CMakeLists.txt +++ b/client/displayservers/X11/CMakeLists.txt @@ -16,6 +16,7 @@ pkg_check_modules(DISPLAYSERVER_X11 REQUIRED IMPORTED_TARGET add_library(displayserver_X11 STATIC x11.c + input_event.c atoms.c clipboard.c cursor.c diff --git a/client/displayservers/X11/input_event.c b/client/displayservers/X11/input_event.c new file mode 100644 index 00000000..e7c4bcfc --- /dev/null +++ b/client/displayservers/X11/input_event.c @@ -0,0 +1,183 @@ +/** + * 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 "input_event.h" + +#include + +static unsigned int mapButton(unsigned int button) +{ + if (button >= 1 && button <= 5) + return button; + + /* X11 reserves buttons 6 and 7 for horizontal scrolling. */ + if (button >= 8 && button < 34) + return button - 2; + + return 0; +} + +void x11InputInit(X11Input * input, const LG_DSInputSink * sink, + void * opaque) +{ + memset(input, 0, sizeof(*input)); + input->sink = sink; + input->opaque = opaque; +} + +bool x11InputFocus(X11Input * input, bool focused, double x, double y, + const uint32_t * keys, size_t count) +{ + (void)keys; + (void)count; + + if (input->focused == focused) + return false; + + input->focused = focused; + input->sink->position(input->opaque, x, y); + input->sink->focus(input->opaque, focused); + return true; +} + +bool x11InputPointerEnter(X11Input * input, bool mainWindow, + bool normal, double x, double y) +{ + if (input->entered || !mainWindow || !normal) + return false; + + input->sink->position(input->opaque, x, y); + input->sink->enter(input->opaque, true); + input->entered = true; + return true; +} + +bool x11InputPointerLeave(X11Input * input, bool mainWindow, + bool normal, bool captureMode, double x, double y) +{ + if (!input->entered || !mainWindow || input->buttons || captureMode || + !normal) + return false; + + input->sink->position(input->opaque, x, y); + input->sink->enter(input->opaque, false); + input->entered = false; + return true; +} + +void x11InputPointerMotion(X11Input * input, double x, double y, + bool pointerGrabbed) +{ + input->sink->position(input->opaque, x, y); + + if (!pointerGrabbed) + input->sink->relative(input->opaque, 0.0, 0.0, 0.0, 0.0); +} + +void x11InputPointerButton(X11Input * input, unsigned int detail, + bool pressed, bool raw, uint32_t time, bool inputActive) +{ + if (raw) + { + if (!inputActive) + return; + } + else if (!input->focused || !input->entered) + return; + + const unsigned int button = mapButton(detail); + if (!button) + return; + + if (!raw) + { + if (pressed && button == 4) + input->sink->wheel(input->opaque, -0.5); + else if (pressed && button == 5) + input->sink->wheel(input->opaque, 0.5); + else if (button != 4 && button != 5) + input->sink->button(input->opaque, button, pressed); + return; + } + + uint32_t * previousTime = pressed ? + &input->previousPressTime : &input->previousReleaseTime; + uint32_t * previousButton = pressed ? + &input->previousPressButton : &input->previousReleaseButton; + + if (time == *previousTime && detail == *previousButton) + return; + + *previousTime = time; + *previousButton = detail; + + const uint32_t mask = UINT32_C(1) << button; + if (pressed) + input->buttons |= mask; + else + input->buttons &= ~mask; + + input->sink->button(input->opaque, button, pressed); +} + +void x11InputRelativeMotion(X11Input * input, uint32_t time, + double x, double y, double rawX, double rawY, bool inputActive) +{ + if (!inputActive) + return; + + if (time == input->previousMotionTime && + x == input->previousMotionX && y == input->previousMotionY) + return; + + input->previousMotionTime = time; + input->previousMotionX = x; + input->previousMotionY = y; + + input->sink->relative(input->opaque, x, y, rawX, rawY); +} + +void x11InputKeyboardKey(X11Input * input, unsigned int keycode, + int minKeycode, bool pressed, bool raw, bool inputActive, + const char * text) +{ + if (raw) + { + if (!inputActive) + return; + } + else if (!input->focused) + return; + + input->sink->key(input->opaque, (int)keycode - minKeycode, pressed); + + if (!raw && pressed && text) + input->sink->text(input->opaque, text); +} + +void x11InputKeyboardState(X11Input * input, + bool ctrl, bool shift, bool alt, bool super, + bool numLock, bool capsLock, bool scrollLock) +{ + (void)numLock; + (void)capsLock; + (void)scrollLock; + input->sink->modifiers(input->opaque, ctrl, shift, alt, super); +} diff --git a/client/displayservers/X11/input_event.h b/client/displayservers/X11/input_event.h new file mode 100644 index 00000000..b46f3dc8 --- /dev/null +++ b/client/displayservers/X11/input_event.h @@ -0,0 +1,69 @@ +/** + * 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 + */ + +#ifndef _H_LG_X11_INPUT_EVENT_ +#define _H_LG_X11_INPUT_EVENT_ + +#include +#include +#include + +#include "../input.h" + +typedef struct X11Input +{ + const LG_DSInputSink * sink; + void * opaque; + uint32_t buttons; + uint32_t previousPressTime; + uint32_t previousReleaseTime; + uint32_t previousPressButton; + uint32_t previousReleaseButton; + uint32_t previousMotionTime; + double previousMotionX; + double previousMotionY; + bool entered; + bool focused; +} +X11Input; + +void x11InputInit(X11Input * input, const LG_DSInputSink * sink, + void * opaque); + +bool x11InputFocus(X11Input * input, bool focused, double x, double y, + const uint32_t * keys, size_t count); +bool x11InputPointerEnter(X11Input * input, bool mainWindow, + bool normal, double x, double y); +bool x11InputPointerLeave(X11Input * input, bool mainWindow, + bool normal, bool captureMode, double x, double y); +void x11InputPointerMotion(X11Input * input, double x, double y, + bool pointerGrabbed); +void x11InputPointerButton(X11Input * input, unsigned int button, + bool pressed, bool raw, uint32_t time, bool inputActive); +void x11InputRelativeMotion(X11Input * input, uint32_t time, + double x, double y, double rawX, double rawY, bool inputActive); +void x11InputKeyboardKey(X11Input * input, unsigned int keycode, + int minKeycode, bool pressed, bool raw, bool inputActive, + const char * text); +void x11InputKeyboardState(X11Input * input, + bool ctrl, bool shift, bool alt, bool super, + bool numLock, bool capsLock, bool scrollLock); + +#endif diff --git a/client/displayservers/X11/x11.c b/client/displayservers/X11/x11.c index d5c544fb..431e9af8 100644 --- a/client/displayservers/X11/x11.c +++ b/client/displayservers/X11/x11.c @@ -95,6 +95,88 @@ static void x11XPresentEvent(XGenericEventCookie *cookie); static void x11UpdateKeyboardGroup(void); static void x11GrabPointer(void); +static void inputPosition(void * opaque, double x, double y) +{ + (void)opaque; + app_updateCursorPos(x, y); +} + +static void inputRelative(void * opaque, double x, double y, + double rawX, double rawY) +{ + (void)opaque; + app_handleMouseRelative(x, y, rawX, rawY); +} + +static void inputButton(void * opaque, unsigned int button, bool pressed) +{ + (void)opaque; + if (pressed) + app_handleButtonPress(button); + else + app_handleButtonRelease(button); +} + +static void inputWheel(void * opaque, double motion) +{ + (void)opaque; + app_handleWheelMotion(motion); +} + +static void inputKey(void * opaque, int scancode, bool pressed) +{ + (void)opaque; + if (pressed) + app_handleKeyPress(scancode); + else + app_handleKeyRelease(scancode); +} + +static void inputText(void * opaque, const char * text) +{ + (void)opaque; + app_handleKeyboardTyped(text); +} + +static void inputModifiers(void * opaque, bool ctrl, bool shift, + bool alt, bool super) +{ + (void)opaque; + app_handleKeyboardModifiers(ctrl, shift, alt, super); +} + +static void inputLEDs(void * opaque, bool numLock, bool capsLock, + bool scrollLock) +{ + (void)opaque; + app_handleKeyboardLEDs(numLock, capsLock, scrollLock); +} + +static void inputEnter(void * opaque, bool entered) +{ + (void)opaque; + app_handleEnterEvent(entered); +} + +static void inputFocus(void * opaque, bool focused) +{ + (void)opaque; + app_handleFocusEvent(focused); +} + +static const LG_DSInputSink inputSink = { + .position = inputPosition, + .relative = inputRelative, + .button = inputButton, + .wheel = inputWheel, + .key = inputKey, + .text = inputText, + .modifiers = inputModifiers, + .leds = inputLEDs, + .enter = inputEnter, + .focus = inputFocus, +}; + static void x11SignalFrame(LG_DSWaitFrameResult result) { atomic_fetch_or_explicit( @@ -354,6 +436,7 @@ static bool x11Init(const LG_DSInitParams params) XSetIOErrorHandler(x11IOErrorHandler); memset(&x11, 0, sizeof(x11)); + x11InputInit(&x11.input, &inputSink, NULL); LG_LOCK_INIT(x11.pointerLock); atomic_init(&x11.captureActive, false); atomic_init(&x11.pointerGrabbed, false); @@ -1117,10 +1200,10 @@ static int x11EventThread(void * unused) focused = true; } - if (x11.ewmhHasFocusEvent && x11.focused != focused) + if (x11.ewmhHasFocusEvent && x11.input.focused != focused) { - x11.focused = focused; - app_handleFocusEvent(focused); + x11.input.focused = focused; + inputFocus(x11.input.opaque, focused); } x11.fullscreen = fullscreen; @@ -1181,12 +1264,12 @@ static enum Modifiers keySymToModifier(KeySym sym) static void updateModifiers(void) { - app_handleKeyboardModifiers( + x11InputKeyboardState(&x11.input, x11.modifiers[MOD_CTRL_LEFT] || x11.modifiers[MOD_CTRL_RIGHT], x11.modifiers[MOD_SHIFT_LEFT] || x11.modifiers[MOD_SHIFT_RIGHT], x11.modifiers[MOD_ALT_LEFT] || x11.modifiers[MOD_ALT_RIGHT], - x11.modifiers[MOD_SUPER_LEFT] || x11.modifiers[MOD_SUPER_RIGHT] - ); + x11.modifiers[MOD_SUPER_LEFT] || x11.modifiers[MOD_SUPER_RIGHT], + false, false, false); } static void x11UpdateKeyboardGroup(void) @@ -1196,29 +1279,15 @@ static void x11UpdateKeyboardGroup(void) atomic_store(&x11.keyboardGroup, state.group); } -static int x11MapButton(unsigned int button) -{ - if (button >= 1 && button <= 5) - return button; - - /* X11 reserves buttons 6 and 7 for horizontal scrolling. */ - if (button >= 8 && button < 34) - return button - 2; - - return 0; -} - static void setFocus(bool focused, double x, double y) { - if (x11.focused == focused) + if (x11.input.focused == focused) return; if (focused) x11UpdateKeyboardGroup(); - x11.focused = focused; - app_updateCursorPos(x, y); - app_handleFocusEvent(focused); + x11InputFocus(&x11.input, focused, x, y, NULL, 0); } static bool x11GetKeyLabel(int sc, char * label, size_t size) @@ -1238,8 +1307,6 @@ static bool x11GetKeyLabel(int sc, char * label, size_t size) static void x11XInputEvent(XGenericEventCookie *cookie) { - static uint32_t button_state = 0; - switch(cookie->evtype) { case XI_FocusIn: @@ -1254,7 +1321,7 @@ static void x11XInputEvent(XGenericEventCookie *cookie) } atomic_store(&x11.lastWMEvent, microtime()); - if (x11.focused) + if (x11.input.focused) return; if (xie->mode != XINotifyNormal && @@ -1279,7 +1346,7 @@ static void x11XInputEvent(XGenericEventCookie *cookie) } atomic_store(&x11.lastWMEvent, microtime()); - if (!x11.focused) + if (!x11.input.focused) return; if (xie->mode != XINotifyNormal && @@ -1295,13 +1362,8 @@ static void x11XInputEvent(XGenericEventCookie *cookie) { atomic_store(&x11.lastWMEvent, microtime()); XIEnterEvent *xie = cookie->data; - if (x11.entered || xie->event != x11.window || - xie->mode != XINotifyNormal) - return; - - app_updateCursorPos(xie->event_x, xie->event_y); - app_handleEnterEvent(true); - x11.entered = true; + x11InputPointerEnter(&x11.input, xie->event == x11.window, + xie->mode == XINotifyNormal, xie->event_x, xie->event_y); return; } @@ -1310,15 +1372,11 @@ static void x11XInputEvent(XGenericEventCookie *cookie) atomic_store(&x11.lastWMEvent, microtime()); XILeaveEvent *xie = cookie->data; - if (!x11.entered || xie->event != x11.window || - button_state != 0 || app_isCaptureMode() || - xie->mode == NotifyGrab) + if (!x11InputPointerLeave(&x11.input, xie->event == x11.window, + xie->mode != NotifyGrab, app_isCaptureMode(), + xie->event_x, xie->event_y)) return; - app_updateCursorPos(xie->event_x, xie->event_y); - app_handleEnterEvent(false); - x11.entered = false; - /** * Because there is a race with the pointer ungrab the enter event for the * next window is sometimes sent with the mode NotifyUngrab, unfortunatly @@ -1363,12 +1421,13 @@ static void x11XInputEvent(XGenericEventCookie *cookie) case XI_KeyPress: { - if (!x11.focused || x11.keyboardGrabbed) + if (!x11.input.focused || x11.keyboardGrabbed) return; XIDeviceEvent *device = cookie->data; atomic_store(&x11.keyboardGroup, device->group.effective); - app_handleKeyPress(device->detail - x11.minKeycode); + x11InputKeyboardKey(&x11.input, device->detail, x11.minKeycode, + true, false, true, NULL); if (!x11.xic || !app_isOverlayMode()) return; @@ -1397,7 +1456,7 @@ static void x11XInputEvent(XGenericEventCookie *cookie) if (status == XLookupChars || status == XLookupBoth) { buffer[count] = '\0'; - app_handleKeyboardTyped(buffer); + inputText(x11.input.opaque, buffer); } if (status == XLookupKeySym || status == XLookupBoth) @@ -1414,11 +1473,12 @@ static void x11XInputEvent(XGenericEventCookie *cookie) case XI_KeyRelease: { - if (!x11.focused || x11.keyboardGrabbed) + if (!x11.input.focused || x11.keyboardGrabbed) return; XIDeviceEvent *device = cookie->data; - app_handleKeyRelease(device->detail - x11.minKeycode); + x11InputKeyboardKey(&x11.input, device->detail, x11.minKeycode, + false, false, true, NULL); if (!x11.xic || !app_isOverlayMode()) return; @@ -1443,116 +1503,63 @@ static void x11XInputEvent(XGenericEventCookie *cookie) case XI_RawKeyPress: { - if (!x11.focused) - return; - XIRawEvent *raw = cookie->data; - app_handleKeyPress(raw->detail - x11.minKeycode); + x11InputKeyboardKey(&x11.input, raw->detail, x11.minKeycode, + true, true, x11.input.focused, NULL); return; } case XI_RawKeyRelease: { - if (!x11.focused) - return; - XIRawEvent *raw = cookie->data; - app_handleKeyRelease(raw->detail - x11.minKeycode); + x11InputKeyboardKey(&x11.input, raw->detail, x11.minKeycode, + false, true, x11.input.focused, NULL); return; } case XI_ButtonPress: { - if (!x11.focused || !x11.entered) - return; - XIDeviceEvent *device = cookie->data; - const int button = x11MapButton(device->detail); - if (button == 4) - app_handleWheelMotion(-0.5); - else if (button == 5) - app_handleWheelMotion(0.5); - else if (button) - app_handleButtonPress(button); - + x11InputPointerButton(&x11.input, device->detail, true, false, + device->time, true); return; } case XI_ButtonRelease: { - if (!x11.focused || !x11.entered) - return; - XIDeviceEvent *device = cookie->data; - const int button = x11MapButton(device->detail); - if (button && button != 4 && button != 5) - app_handleButtonRelease(button); + x11InputPointerButton(&x11.input, device->detail, false, false, + device->time, true); return; } case XI_RawButtonPress: { - if (!x11.focused || !x11.entered) - return; - XIRawEvent *raw = cookie->data; - const int button = x11MapButton(raw->detail); - if (!button) - return; - - /* filter out duplicate events */ - static Time prev_time = 0; - static unsigned int prev_detail = 0; - if (raw->time == prev_time && raw->detail == prev_detail) - return; - - prev_time = raw->time; - prev_detail = raw->detail; - button_state |= UINT32_C(1) << button; - - app_handleButtonPress(button); + x11InputPointerButton(&x11.input, raw->detail, true, true, + raw->time, x11.input.focused && x11.input.entered); return; } case XI_RawButtonRelease: { - if (!x11.focused || !x11.entered) - return; - XIRawEvent *raw = cookie->data; - const int button = x11MapButton(raw->detail); - if (!button) - return; - - /* filter out duplicate events */ - static Time prev_time = 0; - static unsigned int prev_detail = 0; - if (raw->time == prev_time && raw->detail == prev_detail) - return; - - prev_time = raw->time; - prev_detail = raw->detail; - button_state &= ~(UINT32_C(1) << button); - - app_handleButtonRelease(button); + x11InputPointerButton(&x11.input, raw->detail, false, true, + raw->time, x11.input.focused && x11.input.entered); return; } case XI_Motion: { XIDeviceEvent *device = cookie->data; - app_updateCursorPos(device->event_x, device->event_y); - - if (!atomic_load_explicit(&x11.pointerGrabbed, memory_order_acquire)) - app_handleMouseRelative(0.0, 0.0, 0.0, 0.0); + x11InputPointerMotion(&x11.input, device->event_x, device->event_y, + atomic_load_explicit(&x11.pointerGrabbed, + memory_order_acquire)); return; } case XI_RawMotion: { - if (!x11.focused || !x11.entered) - return; - XIRawEvent *raw = cookie->data; double raw_axis[2] = { 0 }; double axis[2] = { 0 }; @@ -1587,19 +1594,9 @@ static void x11XInputEvent(XGenericEventCookie *cookie) if (!has_axes) return; - /* filter out duplicate events */ - static Time prev_time = 0; - static double prev_axis[2] = {0}; - if (raw->time == prev_time && - axis[0] == prev_axis[0] && - axis[1] == prev_axis[1]) - return; - - prev_time = raw->time; - prev_axis[0] = axis[0]; - prev_axis[1] = axis[1]; - - app_handleMouseRelative(axis[0], axis[1], raw_axis[0], raw_axis[1]); + x11InputRelativeMotion(&x11.input, raw->time, axis[0], axis[1], + raw_axis[0], raw_axis[1], + x11.input.focused && x11.input.entered); return; } } @@ -1903,7 +1900,7 @@ static void x11StopWaitFrame(void) static void x11GuestPointerUpdated(double x, double y, double localX, double localY) { - if (app_isCaptureMode() || !x11.entered) + if (app_isCaptureMode() || !x11.input.entered) return; // avoid running too often diff --git a/client/displayservers/X11/x11.h b/client/displayservers/X11/x11.h index bf6d5169..da8dd236 100644 --- a/client/displayservers/X11/x11.h +++ b/client/displayservers/X11/x11.h @@ -34,6 +34,7 @@ #include "common/locking.h" #include "common/thread.h" #include "common/types.h" +#include "input_event.h" #include "wm.h" enum Modifiers @@ -96,12 +97,11 @@ struct X11DSState int xValuator; int yValuator; + X11Input input; LG_Lock pointerLock; _Atomic(bool) captureActive; _Atomic(bool) pointerGrabbed; bool keyboardGrabbed; - bool entered; - bool focused; bool fullscreen; struct Rect rect; diff --git a/client/displayservers/input.h b/client/displayservers/input.h new file mode 100644 index 00000000..0b7cfff2 --- /dev/null +++ b/client/displayservers/input.h @@ -0,0 +1,44 @@ +/** + * 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 + */ + +#ifndef _H_LG_DS_INPUT_ +#define _H_LG_DS_INPUT_ + +#include + +typedef struct LG_DSInputSink +{ + void (*position)(void * opaque, double x, double y); + void (*relative)(void * opaque, double x, double y, + double rawX, double rawY); + void (*button)(void * opaque, unsigned int button, bool pressed); + void (*wheel)(void * opaque, double motion); + void (*key)(void * opaque, int scancode, bool pressed); + void (*text)(void * opaque, const char * text); + void (*modifiers)(void * opaque, bool ctrl, bool shift, bool alt, + bool super); + void (*leds)(void * opaque, bool numLock, bool capsLock, + bool scrollLock); + void (*enter)(void * opaque, bool entered); + void (*focus)(void * opaque, bool focused); +} +LG_DSInputSink; + +#endif diff --git a/client/tests/CMakeLists.txt b/client/tests/CMakeLists.txt index a4868c92..f5bf1283 100644 --- a/client/tests/CMakeLists.txt +++ b/client/tests/CMakeLists.txt @@ -47,6 +47,46 @@ set_tests_properties(wayland-motion-tests PROPERTIES TIMEOUT 10 ) +add_executable(displayserver-input-tests + displayserver_input_test.c + ../displayservers/Wayland/input_event.c + ../displayservers/X11/input_event.c +) +target_include_directories(displayserver-input-tests PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}/../displayservers" +) + +set(DISPLAYSERVER_INPUT_CASES + pointer-events + pointer-buttons + pointer-release + wheel-steps + relative-motion + keyboard-focus + keyboard-keys + keyboard-state +) + +foreach(backend IN ITEMS wayland x11) + foreach(name IN LISTS DISPLAYSERVER_INPUT_CASES) + add_test(NAME display-input-${backend}-${name} + COMMAND displayserver-input-tests ${backend} ${name} + ) + set_tests_properties(display-input-${backend}-${name} PROPERTIES + TIMEOUT 10 + ) + endforeach() +endforeach() + +foreach(name IN ITEMS wheel-residual wheel-multiple) + add_test(NAME display-input-wayland-${name} + COMMAND displayserver-input-tests wayland ${name} + ) + set_tests_properties(display-input-wayland-${name} PROPERTIES + TIMEOUT 10 + ) +endforeach() + add_executable(mouse-tests mouse_test.c ../src/core.c diff --git a/client/tests/displayserver_input_test.c b/client/tests/displayserver_input_test.c new file mode 100644 index 00000000..13d226a1 --- /dev/null +++ b/client/tests/displayserver_input_test.c @@ -0,0 +1,877 @@ +/** + * 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 "Wayland/input_event.h" +#include "X11/input_event.h" +#include "test.h" + +#include + +#include +#include +#include + +enum Backend +{ + BACKEND_WAYLAND, + BACKEND_X11, +}; + +enum TraceType +{ + TRACE_POSITION, + TRACE_RELATIVE, + TRACE_BUTTON, + TRACE_WHEEL, + TRACE_KEY, + TRACE_TEXT, + TRACE_MODIFIERS, + TRACE_LEDS, + TRACE_ENTER, + TRACE_FOCUS, +}; + +struct Trace +{ + enum TraceType type; + union + { + struct + { + double x; + double y; + double rawX; + double rawY; + } + motion; + + struct + { + unsigned int value; + bool pressed; + } + input; + + struct + { + bool ctrl; + bool shift; + bool alt; + bool super; + } + modifiers; + + struct + { + bool num; + bool caps; + bool scroll; + } + leds; + + double wheel; + bool state; + char text[32]; + } data; +}; + +#define POSITION(_x, _y) \ + { \ + .type = TRACE_POSITION, \ + .data.motion = { .x = (_x), .y = (_y) }, \ + } + +#define RELATIVE(_x, _y, _rawX, _rawY) \ + { \ + .type = TRACE_RELATIVE, \ + .data.motion = \ + { .x = (_x), .y = (_y), .rawX = (_rawX), .rawY = (_rawY) }, \ + } + +#define BUTTON(_button, _pressed) \ + { \ + .type = TRACE_BUTTON, \ + .data.input = { .value = (_button), .pressed = (_pressed) }, \ + } + +#define WHEEL(_motion) \ + { \ + .type = TRACE_WHEEL, \ + .data.wheel = (_motion), \ + } + +#define KEY(_key, _pressed) \ + { \ + .type = TRACE_KEY, \ + .data.input = { .value = (_key), .pressed = (_pressed) }, \ + } + +#define TEXT(_text) \ + { \ + .type = TRACE_TEXT, \ + .data.text = _text, \ + } + +#define MODIFIERS(_ctrl, _shift, _alt, _super) \ + { \ + .type = TRACE_MODIFIERS, \ + .data.modifiers = \ + { .ctrl = (_ctrl), .shift = (_shift), .alt = (_alt), \ + .super = (_super) }, \ + } + +#define LEDS(_num, _caps, _scroll) \ + { \ + .type = TRACE_LEDS, \ + .data.leds = { .num = (_num), .caps = (_caps), .scroll = (_scroll) }, \ + } + +#define ENTER(_entered) \ + { \ + .type = TRACE_ENTER, \ + .data.state = (_entered), \ + } + +#define FOCUS(_focused) \ + { \ + .type = TRACE_FOCUS, \ + .data.state = (_focused), \ + } + +#define ARRAY_LENGTH(_array) (sizeof(_array) / sizeof((_array)[0])) +#define MAX_TRACE 128 + +struct TraceLog +{ + struct Trace values[MAX_TRACE]; + size_t count; +}; + +struct Fixture +{ + enum Backend backend; + struct TraceLog trace; + uint32_t time; + + union + { + WaylandInput wayland; + X11Input x11; + } input; +}; + +static struct Trace * pushTrace(struct TraceLog * log, enum TraceType type) +{ + CHECK(log->count < ARRAY_LENGTH(log->values)); + struct Trace * trace = &log->values[log->count++]; + memset(trace, 0, sizeof(*trace)); + trace->type = type; + return trace; +} + +static void tracePosition(void * opaque, double x, double y) +{ + struct Trace * trace = pushTrace(opaque, TRACE_POSITION); + trace->data.motion.x = x; + trace->data.motion.y = y; +} + +static void traceRelative(void * opaque, double x, double y, + double rawX, double rawY) +{ + struct Trace * trace = pushTrace(opaque, TRACE_RELATIVE); + trace->data.motion.x = x; + trace->data.motion.y = y; + trace->data.motion.rawX = rawX; + trace->data.motion.rawY = rawY; +} + +static void traceButton(void * opaque, unsigned int button, bool pressed) +{ + struct Trace * trace = pushTrace(opaque, TRACE_BUTTON); + trace->data.input.value = button; + trace->data.input.pressed = pressed; +} + +static void traceWheel(void * opaque, double motion) +{ + struct Trace * trace = pushTrace(opaque, TRACE_WHEEL); + trace->data.wheel = motion; +} + +static void traceKey(void * opaque, int scancode, bool pressed) +{ + struct Trace * trace = pushTrace(opaque, TRACE_KEY); + trace->data.input.value = scancode; + trace->data.input.pressed = pressed; +} + +static void traceText(void * opaque, const char * text) +{ + struct Trace * trace = pushTrace(opaque, TRACE_TEXT); + snprintf(trace->data.text, sizeof(trace->data.text), "%s", text); +} + +static void traceModifiers(void * opaque, bool ctrl, bool shift, + bool alt, bool super) +{ + struct Trace * trace = pushTrace(opaque, TRACE_MODIFIERS); + trace->data.modifiers.ctrl = ctrl; + trace->data.modifiers.shift = shift; + trace->data.modifiers.alt = alt; + trace->data.modifiers.super = super; +} + +static void traceLEDs(void * opaque, bool numLock, bool capsLock, + bool scrollLock) +{ + struct Trace * trace = pushTrace(opaque, TRACE_LEDS); + trace->data.leds.num = numLock; + trace->data.leds.caps = capsLock; + trace->data.leds.scroll = scrollLock; +} + +static void traceEnter(void * opaque, bool entered) +{ + struct Trace * trace = pushTrace(opaque, TRACE_ENTER); + trace->data.state = entered; +} + +static void traceFocus(void * opaque, bool focused) +{ + struct Trace * trace = pushTrace(opaque, TRACE_FOCUS); + trace->data.state = focused; +} + +static const LG_DSInputSink sink = +{ + .position = tracePosition, + .relative = traceRelative, + .button = traceButton, + .wheel = traceWheel, + .key = traceKey, + .text = traceText, + .modifiers = traceModifiers, + .leds = traceLEDs, + .enter = traceEnter, + .focus = traceFocus, +}; + +static void initFixture(struct Fixture * fixture, enum Backend backend) +{ + memset(fixture, 0, sizeof(*fixture)); + fixture->backend = backend; + fixture->time = 1; + + if (backend == BACKEND_WAYLAND) + wlInputInit(&fixture->input.wayland, &sink, &fixture->trace); + else + x11InputInit(&fixture->input.x11, &sink, &fixture->trace); +} + +static void clearTrace(struct Fixture * fixture) +{ + fixture->trace.count = 0; +} + +static bool sameDouble(double a, double b) +{ + const double delta = a > b ? a - b : b - a; + return delta < 0.000000001; +} + +static bool sameTrace(const struct Trace * actual, + const struct Trace * expected) +{ + if (actual->type != expected->type) + return false; + + switch (actual->type) + { + case TRACE_POSITION: + return + sameDouble(actual->data.motion.x, expected->data.motion.x) && + sameDouble(actual->data.motion.y, expected->data.motion.y); + + case TRACE_RELATIVE: + return + sameDouble(actual->data.motion.x, expected->data.motion.x) && + sameDouble(actual->data.motion.y, expected->data.motion.y) && + sameDouble(actual->data.motion.rawX, expected->data.motion.rawX) && + sameDouble(actual->data.motion.rawY, expected->data.motion.rawY); + + case TRACE_BUTTON: + case TRACE_KEY: + return + actual->data.input.value == expected->data.input.value && + actual->data.input.pressed == expected->data.input.pressed; + + case TRACE_WHEEL: + return sameDouble(actual->data.wheel, expected->data.wheel); + + case TRACE_TEXT: + return strcmp(actual->data.text, expected->data.text) == 0; + + case TRACE_MODIFIERS: + return + actual->data.modifiers.ctrl == expected->data.modifiers.ctrl && + actual->data.modifiers.shift == expected->data.modifiers.shift && + actual->data.modifiers.alt == expected->data.modifiers.alt && + actual->data.modifiers.super == expected->data.modifiers.super; + + case TRACE_LEDS: + return + actual->data.leds.num == expected->data.leds.num && + actual->data.leds.caps == expected->data.leds.caps && + actual->data.leds.scroll == expected->data.leds.scroll; + + case TRACE_ENTER: + case TRACE_FOCUS: + return actual->data.state == expected->data.state; + } + + return false; +} + +static const char * traceName(enum TraceType type) +{ + static const char * names[] = + { + [TRACE_POSITION ] = "position", + [TRACE_RELATIVE ] = "relative", + [TRACE_BUTTON ] = "button", + [TRACE_WHEEL ] = "wheel", + [TRACE_KEY ] = "key", + [TRACE_TEXT ] = "text", + [TRACE_MODIFIERS] = "modifiers", + [TRACE_LEDS ] = "leds", + [TRACE_ENTER ] = "enter", + [TRACE_FOCUS ] = "focus", + }; + return names[type]; +} + +static void printTrace(const struct Trace * trace) +{ + fprintf(stderr, "%s", traceName(trace->type)); + switch (trace->type) + { + case TRACE_POSITION: + fprintf(stderr, " %.6f %.6f", trace->data.motion.x, + trace->data.motion.y); + break; + + case TRACE_RELATIVE: + fprintf(stderr, " %.6f %.6f %.6f %.6f", trace->data.motion.x, + trace->data.motion.y, trace->data.motion.rawX, + trace->data.motion.rawY); + break; + + case TRACE_BUTTON: + case TRACE_KEY: + fprintf(stderr, " %u %s", trace->data.input.value, + trace->data.input.pressed ? "down" : "up"); + break; + + case TRACE_WHEEL: + fprintf(stderr, " %.6f", trace->data.wheel); + break; + + case TRACE_TEXT: + fprintf(stderr, " %s", trace->data.text); + break; + + case TRACE_MODIFIERS: + fprintf(stderr, " %d %d %d %d", trace->data.modifiers.ctrl, + trace->data.modifiers.shift, trace->data.modifiers.alt, + trace->data.modifiers.super); + break; + + case TRACE_LEDS: + fprintf(stderr, " %d %d %d", trace->data.leds.num, + trace->data.leds.caps, trace->data.leds.scroll); + break; + + case TRACE_ENTER: + case TRACE_FOCUS: + fprintf(stderr, " %d", trace->data.state); + break; + } + fputc('\n', stderr); +} + +static void expectTrace(const struct Fixture * fixture, + const struct Trace * expected, size_t expectedCount) +{ + bool match = fixture->trace.count == expectedCount; + if (match) + for (size_t i = 0; i < expectedCount; ++i) + if (!sameTrace(&fixture->trace.values[i], &expected[i])) + { + match = false; + break; + } + + if (match) + return; + + fprintf(stderr, "expected trace (%zu events):\n", expectedCount); + for (size_t i = 0; i < expectedCount; ++i) + printTrace(&expected[i]); + + fprintf(stderr, "actual trace (%zu events):\n", fixture->trace.count); + for (size_t i = 0; i < fixture->trace.count; ++i) + printTrace(&fixture->trace.values[i]); + + exit(EXIT_FAILURE); +} + +static void pointerEnter(struct Fixture * fixture, bool mainSurface, + double x, double y) +{ + if (fixture->backend == BACKEND_WAYLAND) + { + if (wlInputPointerEnter(&fixture->input.wayland, mainSurface)) + wlInputPointerMotion(&fixture->input.wayland, x, y); + } + else + x11InputPointerEnter(&fixture->input.x11, mainSurface, true, x, y); +} + +static void pointerLeave(struct Fixture * fixture, bool mainSurface, + double x, double y) +{ + if (fixture->backend == BACKEND_WAYLAND) + wlInputPointerLeave(&fixture->input.wayland, mainSurface); + else + x11InputPointerLeave(&fixture->input.x11, mainSurface, true, false, + x, y); +} + +static void pointerMotion(struct Fixture * fixture, double x, double y) +{ + if (fixture->backend == BACKEND_WAYLAND) + wlInputPointerMotion(&fixture->input.wayland, x, y); + else + x11InputPointerMotion(&fixture->input.x11, x, y, false); +} + +static unsigned int nativeButton(enum Backend backend, + unsigned int button) +{ + if (backend == BACKEND_X11) + return button > 5 ? button + 2 : button; + + static const unsigned int wayland[] = + { + [1 ] = BTN_LEFT, + [2 ] = BTN_MIDDLE, + [3 ] = BTN_RIGHT, + [6 ] = BTN_SIDE, + [7 ] = BTN_EXTRA, + [8 ] = BTN_FORWARD, + [9 ] = BTN_BACK, + [10] = BTN_TASK, + }; + CHECK(button < ARRAY_LENGTH(wayland)); + return wayland[button]; +} + +static void pointerButton(struct Fixture * fixture, unsigned int button, + bool pressed) +{ + const unsigned int native = nativeButton(fixture->backend, button); + if (fixture->backend == BACKEND_WAYLAND) + wlInputPointerButton(&fixture->input.wayland, native, pressed); + else + x11InputPointerButton(&fixture->input.x11, native, pressed, false, + fixture->time++, true); +} + +static void unmappedButton(struct Fixture * fixture) +{ + if (fixture->backend == BACKEND_WAYLAND) + wlInputPointerButton(&fixture->input.wayland, BTN_STYLUS, true); + else + x11InputPointerButton(&fixture->input.x11, 6, true, false, + fixture->time++, true); +} + +static void wheelStep(struct Fixture * fixture, bool down) +{ + if (fixture->backend == BACKEND_WAYLAND) + wlInputPointerAxis(&fixture->input.wayland, true, down ? 15.0 : -15.0); + else + x11InputPointerButton(&fixture->input.x11, down ? 5 : 4, true, + false, fixture->time++, true); +} + +static void relativeMotion(struct Fixture * fixture, bool active, + uint32_t time, double x, double y, double rawX, double rawY) +{ + if (fixture->backend == BACKEND_WAYLAND) + wlInputRelativeMotion(&fixture->input.wayland, active, + x, y, rawX, rawY); + else + x11InputRelativeMotion(&fixture->input.x11, time, + x, y, rawX, rawY, active); +} + +static void keyboardEnter(struct Fixture * fixture, bool mainSurface, + const uint32_t * keys, size_t count) +{ + if (fixture->backend == BACKEND_WAYLAND) + wlInputKeyboardEnter(&fixture->input.wayland, mainSurface, keys, count); + else if (mainSurface) + x11InputFocus(&fixture->input.x11, true, 40.0, 30.0, keys, count); +} + +static void keyboardLeave(struct Fixture * fixture, bool mainSurface) +{ + if (fixture->backend == BACKEND_WAYLAND) + wlInputKeyboardLeave(&fixture->input.wayland, mainSurface); + else if (mainSurface) + x11InputFocus(&fixture->input.x11, false, 40.0, 30.0, NULL, 0); +} + +static void keyboardKey(struct Fixture * fixture, unsigned int key, + bool pressed, const char * text, bool inputActive) +{ + if (fixture->backend == BACKEND_WAYLAND) + wlInputKeyboardKey(&fixture->input.wayland, key, pressed, text); + else + x11InputKeyboardKey(&fixture->input.x11, key + 8, 8, pressed, + false, inputActive, text); +} + +static void keyboardState(struct Fixture * fixture, + bool ctrl, bool shift, bool alt, bool super, + bool numLock, bool capsLock, bool scrollLock) +{ + if (fixture->backend == BACKEND_WAYLAND) + wlInputKeyboardState(&fixture->input.wayland, + ctrl, shift, alt, super, numLock, capsLock, scrollLock); + else + x11InputKeyboardState(&fixture->input.x11, + ctrl, shift, alt, super, numLock, capsLock, scrollLock); +} + +static void activateInput(struct Fixture * fixture) +{ + pointerEnter(fixture, true, 40.0, 30.0); + keyboardEnter(fixture, true, NULL, 0); + clearTrace(fixture); +} + +static void activatePointer(struct Fixture * fixture) +{ + pointerEnter(fixture, true, 40.0, 30.0); + clearTrace(fixture); +} + +static void testPointerEvents(enum Backend backend) +{ + struct Fixture fixture; + initFixture(&fixture, backend); + + pointerEnter(&fixture, false, 1.0, 2.0); + pointerEnter(&fixture, true, 10.0, 20.0); + pointerMotion(&fixture, 11.5, 21.25); + pointerLeave(&fixture, false, 4.0, 5.0); + pointerLeave(&fixture, true, 12.0, 22.0); + + static const struct Trace expected[] = + { + ENTER(true), + POSITION(10.0, 20.0), + POSITION(11.5, 21.25), + ENTER(false), + }; + expectTrace(&fixture, expected, ARRAY_LENGTH(expected)); +} + +static void testPointerButtons(enum Backend backend) +{ + struct Fixture fixture; + initFixture(&fixture, backend); + activatePointer(&fixture); + + static const unsigned int buttons[] = { 1, 2, 3, 6, 7, 8, 9, 10 }; + for (size_t i = 0; i < ARRAY_LENGTH(buttons); ++i) + { + pointerButton(&fixture, buttons[i], true); + pointerButton(&fixture, buttons[i], false); + } + unmappedButton(&fixture); + + static const struct Trace expected[] = + { + BUTTON(1 , true), BUTTON(1 , false), + BUTTON(2 , true), BUTTON(2 , false), + BUTTON(3 , true), BUTTON(3 , false), + BUTTON(6 , true), BUTTON(6 , false), + BUTTON(7 , true), BUTTON(7 , false), + BUTTON(8 , true), BUTTON(8 , false), + BUTTON(9 , true), BUTTON(9 , false), + BUTTON(10, true), BUTTON(10, false), + }; + expectTrace(&fixture, expected, ARRAY_LENGTH(expected)); +} + +static void testPointerRelease(enum Backend backend) +{ + struct Fixture fixture; + initFixture(&fixture, backend); + activateInput(&fixture); + + pointerButton(&fixture, 1, true); + + /* Wayland's implicit pointer grab suppresses this native leave. */ + if (backend == BACKEND_X11) + pointerLeave(&fixture, true, 100.0, 100.0); + + pointerButton(&fixture, 1, false); + + static const struct Trace expected[] = + { + BUTTON(1, true), + BUTTON(1, false), + }; + expectTrace(&fixture, expected, ARRAY_LENGTH(expected)); +} + +static void testWheelSteps(enum Backend backend) +{ + struct Fixture fixture; + initFixture(&fixture, backend); + activateInput(&fixture); + + wheelStep(&fixture, true); + wheelStep(&fixture, false); + + static const struct Trace expected[] = + { + BUTTON(5, true), + BUTTON(5, false), + WHEEL(1.0), + BUTTON(4, true), + BUTTON(4, false), + WHEEL(-1.0), + }; + expectTrace(&fixture, expected, ARRAY_LENGTH(expected)); +} + +static void testWheelResidual(enum Backend backend) +{ + CHECK(backend == BACKEND_WAYLAND); + struct Fixture fixture; + initFixture(&fixture, backend); + + wlInputPointerAxis(&fixture.input.wayland, false, 100.0); + wlInputPointerAxis(&fixture.input.wayland, true, 5.0); + wlInputPointerAxis(&fixture.input.wayland, true, 5.0); + wlInputPointerAxis(&fixture.input.wayland, true, -2.0); + wlInputPointerAxis(&fixture.input.wayland, true, -1.0); + + static const struct Trace expected[] = + { + WHEEL(1.0 / 3.0), + BUTTON(5, true), + BUTTON(5, false), + WHEEL(1.0 / 3.0), + WHEEL(-2.0 / 15.0), + BUTTON(4, true), + BUTTON(4, false), + WHEEL(-1.0 / 15.0), + }; + expectTrace(&fixture, expected, ARRAY_LENGTH(expected)); +} + +static void testWheelMultiple(enum Backend backend) +{ + CHECK(backend == BACKEND_WAYLAND); + struct Fixture fixture; + initFixture(&fixture, backend); + + wlInputPointerAxis(&fixture.input.wayland, true, 45.0); + + static const struct Trace expected[] = + { + BUTTON(5, true), BUTTON(5, false), + BUTTON(5, true), BUTTON(5, false), + BUTTON(5, true), BUTTON(5, false), + WHEEL(3.0), + }; + expectTrace(&fixture, expected, ARRAY_LENGTH(expected)); +} + +static void testRelativeMotion(enum Backend backend) +{ + struct Fixture fixture; + initFixture(&fixture, backend); + activateInput(&fixture); + + relativeMotion(&fixture, false, 50, 1.0, 2.0, 3.0, 4.0); + relativeMotion(&fixture, true, 51, 1.0, 2.0, 3.0, 4.0); + relativeMotion(&fixture, true, 51, 1.0, 2.0, 3.0, 4.0); + + static const struct Trace expected[] = + { + RELATIVE(1.0, 2.0, 3.0, 4.0), + RELATIVE(1.0, 2.0, 3.0, 4.0), + }; + expectTrace(&fixture, expected, ARRAY_LENGTH(expected)); +} + +static void testKeyboardFocus(enum Backend backend) +{ + struct Fixture fixture; + initFixture(&fixture, backend); + static const uint32_t held[] = { 30, 42 }; + + keyboardEnter(&fixture, false, held, ARRAY_LENGTH(held)); + keyboardEnter(&fixture, true, held, ARRAY_LENGTH(held)); + keyboardLeave(&fixture, false); + keyboardLeave(&fixture, true); + + static const struct Trace expected[] = + { + FOCUS(true), + KEY(30, true), + KEY(42, true), + FOCUS(false), + }; + expectTrace(&fixture, expected, ARRAY_LENGTH(expected)); +} + +static void testKeyboardKeys(enum Backend backend) +{ + struct Fixture fixture; + initFixture(&fixture, backend); + + keyboardKey(&fixture, 30, true, "ignored", true); + keyboardKey(&fixture, 30, false, "ignored", true); + CHECK(fixture.trace.count == 0); + + keyboardEnter(&fixture, true, NULL, 0); + clearTrace(&fixture); + + keyboardKey(&fixture, 30, true, "a", true); + keyboardKey(&fixture, 30, false, "ignored", true); + + static const struct Trace expected[] = + { + KEY(30, true), + TEXT("a"), + KEY(30, false), + }; + expectTrace(&fixture, expected, ARRAY_LENGTH(expected)); + + keyboardLeave(&fixture, true); + clearTrace(&fixture); + keyboardKey(&fixture, 31, true, "s", true); + keyboardKey(&fixture, 31, false, NULL, true); + CHECK(fixture.trace.count == 0); +} + +static void testKeyboardState(enum Backend backend) +{ + struct Fixture fixture; + initFixture(&fixture, backend); + keyboardEnter(&fixture, true, NULL, 0); + clearTrace(&fixture); + + keyboardState(&fixture, true, false, true, false, + true, false, true); + keyboardState(&fixture, false, true, false, true, + false, true, false); + + static const struct Trace expected[] = + { + MODIFIERS(true, false, true, false), + LEDS(true, false, true), + MODIFIERS(false, true, false, true), + LEDS(false, true, false), + }; + expectTrace(&fixture, expected, ARRAY_LENGTH(expected)); +} + +struct Test +{ + const char * name; + void (*run)(enum Backend backend); +}; + +static const struct Test tests[] = +{ + { "pointer-events" , testPointerEvents }, + { "pointer-buttons", testPointerButtons }, + { "pointer-release", testPointerRelease }, + { "wheel-steps" , testWheelSteps }, + { "wheel-residual" , testWheelResidual }, + { "wheel-multiple" , testWheelMultiple }, + { "relative-motion", testRelativeMotion }, + { "keyboard-focus" , testKeyboardFocus }, + { "keyboard-keys" , testKeyboardKeys }, + { "keyboard-state" , testKeyboardState }, +}; + +static bool parseBackend(const char * name, enum Backend * backend) +{ + if (strcmp(name, "wayland") == 0) + { + *backend = BACKEND_WAYLAND; + return true; + } + + if (strcmp(name, "x11") == 0) + { + *backend = BACKEND_X11; + return true; + } + + return false; +} + +int main(int argc, char ** argv) +{ + if (argc != 3) + { + fprintf(stderr, "usage: %s \n", argv[0]); + return EXIT_FAILURE; + } + + enum Backend backend; + if (!parseBackend(argv[1], &backend)) + { + fprintf(stderr, "unknown backend: %s\n", argv[1]); + return EXIT_FAILURE; + } + + for (size_t i = 0; i < ARRAY_LENGTH(tests); ++i) + if (strcmp(argv[2], tests[i].name) == 0) + { + tests[i].run(backend); + return 0; + } + + fprintf(stderr, "unknown test: %s\n", argv[2]); + return EXIT_FAILURE; +}