[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.
This commit is contained in:
Geoffrey McRae
2026-08-10 17:15:11 +10:00
parent 802c8b5803
commit b8955fd012
14 changed files with 1731 additions and 256 deletions

View File

@@ -19,6 +19,7 @@ add_library(displayserver_Wayland STATIC
icon.c
idle.c
input.c
input_event.c
output.c
poll.c
presentation.c

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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 <linux/input.h>
#include <string.h>
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);
}

View File

@@ -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 <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#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

View File

@@ -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;