summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBerke Güzel <wenekar1@gmail.com>2026-10-13 16:23:07 +0300
committerBerke Güzel <wenekar1@gmail.com>2026-10-13 16:23:07 +0300
commit276a70a84fed5be229c12546df1ac393158a4ec9 (patch)
treef3d0e00d6339efacc466b32048644c388315e90d
parent138b9c40aa211edff1d0201083aa25a8b467ee15 (diff)
Implement relative pointer mode on Wayland
yasl_surface_set_pointer_mode(YASL_POINTER_RELATIVE) now takes a persistent zwp_locked_pointer_v1 on the surface and hides its cursor. While the compositor keeps the lock engaged, zwp_relative_pointer_v1 motion is pushed as POINTER_MOTION with pos frozen and delta in device pixels. When the lock disengages, as on focus loss, motion is absolute again with a delta derived by the input queue. The lock follows the pointer's seat: it moves to another seat when its seat loses the pointer, and is released on NORMAL and on surface destruction. Compositors without both protocols return false. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
-rw-r--r--docs/guide.md8
-rw-r--r--include/yasl/yasl.h6
-rw-r--r--src/platform/wayland/api.h1
-rw-r--r--src/platform/wayland/backend.c6
-rw-r--r--src/platform/wayland/internal.h11
-rw-r--r--src/platform/wayland/pointer.c131
-rw-r--r--src/platform/wayland/shell.c6
-rw-r--r--src/platform/wayland/surface.c3
-rw-r--r--tests/meson.build5
-rw-r--r--tests/wayland/test_relative_pointer.c170
10 files changed, 340 insertions, 7 deletions
diff --git a/docs/guide.md b/docs/guide.md
index a65972f..62c2e1a 100644
--- a/docs/guide.md
+++ b/docs/guide.md
@@ -224,8 +224,12 @@ meaningless. `POINTER_MOTION` reports `delta`, the device-pixel movement since
the previous sample, which restarts from zero after enter, leave, or reset.
`yasl_surface_set_pointer_mode()` with `YASL_POINTER_RELATIVE` hides and locks the
cursor while the surface is focused; motion then carries unbounded `delta` and
-`pos` stays at the lock position. No backend implements it yet, so it
-currently returns false and leaves the surface in `YASL_POINTER_NORMAL`. Pen
+`pos` stays at the lock position. Wayland implements it with the
+relative-pointer and pointer-constraints protocols, reporting the compositor's
+accelerated motion in device pixels. The compositor decides when the lock
+engages; until then, and while it is released on focus loss, motion stays
+absolute with a computed `delta`. X11, Cocoa, and compositors without those
+protocols return false and keep the current mode. Pen
pressure is normalized to 0..65535, signed tilt is in degrees, and missing axes
are zero. Pressure-only changes can produce motion. Zero pressure alone does
not mean release. This interface does not expose simultaneous touch contacts.
diff --git a/include/yasl/yasl.h b/include/yasl/yasl.h
index 22ed308..de9a62c 100644
--- a/include/yasl/yasl.h
+++ b/include/yasl/yasl.h
@@ -757,10 +757,12 @@ YASL_API bool yasl_surface_poll_event(struct yasl_surface *s, struct yasl_event
* Select absolute or relative (locked) pointer input for a surface.
* @param s Surface.
* @param mode YASL_POINTER_NORMAL or YASL_POINTER_RELATIVE.
- * @return true if the mode is supported by the backend; false leaves NORMAL.
+ * @return true if the mode is supported; false leaves the current mode.
* RELATIVE hides the cursor and locks it while the surface is focused;
* POINTER_MOTION then carries unbounded delta. Returning to NORMAL restores
- * the cursor selected with yasl_surface_set_cursor().
+ * the cursor selected with yasl_surface_set_cursor(). RELATIVE is supported on
+ * Wayland compositors with the relative-pointer and pointer-constraints
+ * protocols; X11 and Cocoa return false.
*/
YASL_API bool yasl_surface_set_pointer_mode(struct yasl_surface *s, enum yasl_pointer_mode mode);
diff --git a/src/platform/wayland/api.h b/src/platform/wayland/api.h
index aabc182..e5e933e 100644
--- a/src/platform/wayland/api.h
+++ b/src/platform/wayland/api.h
@@ -29,6 +29,7 @@ bool wayland_surface_poll_event(struct wayland_surface *s, struct yasl_event *ev
struct yasl_geometry wayland_surface_geometry(const struct wayland_surface *s);
struct yasl_timing wayland_surface_timing(const struct wayland_surface *s);
void wayland_surface_set_cursor(struct wayland_surface *s, enum yasl_cursor cursor);
+bool wayland_surface_set_pointer_mode(struct wayland_surface *s, enum yasl_pointer_mode mode);
void wayland_surface_set_background_color(struct wayland_surface *s, uint32_t bg);
bool wayland_surface_ime_preedit_cursor(const struct wayland_surface *s, int *begin, int *end);
void wayland_surface_set_ime_caret(struct wayland_surface *s, struct yasl_rect2i caret);
diff --git a/src/platform/wayland/backend.c b/src/platform/wayland/backend.c
index 1d83ec6..c3c7bed 100644
--- a/src/platform/wayland/backend.c
+++ b/src/platform/wayland/backend.c
@@ -126,6 +126,11 @@ static void impl_surface_set_cursor(struct yasl_surface *s, enum yasl_cursor cur
wayland_surface_set_cursor((struct wayland_surface *)s, cursor);
}
+static bool impl_surface_set_pointer_mode(struct yasl_surface *s, enum yasl_pointer_mode mode)
+{
+ return wayland_surface_set_pointer_mode((struct wayland_surface *)s, mode);
+}
+
static void impl_surface_set_background_color(struct yasl_surface *s, uint32_t bg)
{
wayland_surface_set_background_color((struct wayland_surface *)s, bg);
@@ -206,6 +211,7 @@ const struct yasl_backend_ops yasl_wayland_backend = {
.surface_schedule_redraw = impl_surface_schedule_redraw,
.surface_present = impl_surface_present,
.surface_poll_event = impl_surface_poll_event,
+ .surface_set_pointer_mode = impl_surface_set_pointer_mode,
.surface_geometry = impl_surface_geometry,
.surface_timing = impl_surface_timing,
.surface_frame_time_ns = impl_surface_frame_time_ns,
diff --git a/src/platform/wayland/internal.h b/src/platform/wayland/internal.h
index d619dba..c35db7f 100644
--- a/src/platform/wayland/internal.h
+++ b/src/platform/wayland/internal.h
@@ -23,6 +23,8 @@
#include "protocols/commit-timing-v1-client-protocol.h"
#include "protocols/xdg-toplevel-drag-v1-client-protocol.h"
#include "protocols/primary-selection-unstable-v1-client-protocol.h"
+#include "protocols/relative-pointer-unstable-v1-client-protocol.h"
+#include "protocols/pointer-constraints-unstable-v1-client-protocol.h"
#include <fcntl.h>
#include <limits.h>
@@ -131,6 +133,7 @@ struct seat {
struct wl_pointer *pointer;
struct wl_touch *touch;
struct wp_cursor_shape_device_v1 *cursor_shape_dev;
+ struct zwp_relative_pointer_v1 *relative_pointer;
struct wl_surface *cursor_surface;
struct wp_viewport *cursor_viewport;
struct zwp_tablet_seat_v2 *tablet_seat;
@@ -212,6 +215,8 @@ struct wayland_display {
struct wl_data_device_manager *data_manager;
struct zwp_primary_selection_device_manager_v1 *primary_selection_mgr;
struct wp_cursor_shape_manager_v1 *cursor_shape_mgr;
+ struct zwp_relative_pointer_manager_v1 *relative_pointer_mgr;
+ struct zwp_pointer_constraints_v1 *pointer_constraints;
struct wl_cursor_theme *cursor_theme;
const char *cursor_theme_name;
struct zwp_tablet_manager_v2 *tablet_mgr;
@@ -275,6 +280,9 @@ struct wayland_surface {
struct wp_fifo_v1 *fifo;
struct wp_commit_timer_v1 *commit_timer;
struct wl_callback *frame_cb;
+ /* Persistent lock held while relative; lock_seat owns the locked wl_pointer. */
+ struct zwp_locked_pointer_v1 *pointer_lock;
+ struct seat *lock_seat;
const struct yasl_window_cbs *cbs;
void *ud;
uint64_t frames_skipped;
@@ -316,6 +324,8 @@ struct wayland_surface {
bool destroyed;
bool opaque;
bool want_draw;
+ bool relative;
+ bool pointer_locked;
};
@@ -388,6 +398,7 @@ int seat_modifiers(const struct seat *s);
void surface_init_cursor(struct wayland_display *c);
void seat_destroy_tablet(struct seat *s);
void seat_pointer_reset(struct seat *s);
+void surface_unlock_pointer(struct wayland_surface *w);
extern const struct zwp_text_input_v3_listener text_input_listener;
void ime_reset(struct wayland_surface *w);
diff --git a/src/platform/wayland/pointer.c b/src/platform/wayland/pointer.c
index 83f6e2f..cdc5780 100644
--- a/src/platform/wayland/pointer.c
+++ b/src/platform/wayland/pointer.c
@@ -152,8 +152,10 @@ static uint32_t cursor_shape_of(enum yasl_cursor shape)
}
}
-static void seat_set_cursor(struct seat *s, struct wayland_surface *w, enum yasl_cursor shape)
+/* Relative mode hides the pointer but keeps w->cursor for the return to NORMAL. */
+static void seat_set_cursor(struct seat *s, struct wayland_surface *w)
{
+ enum yasl_cursor shape = w->relative ? YASL_CURSOR_HIDDEN : w->cursor;
if (!s->pointer || !s->pointer_serial || s->pointer_focus != w ||
s->cursor_shape == (int)shape)
return;
@@ -183,7 +185,7 @@ void wayland_surface_set_cursor(struct wayland_surface *w, enum yasl_cursor shap
if (!w || !w->conn || w->destroyed || (unsigned)shape > YASL_CURSOR_HIDDEN) return;
w->cursor = shape;
for (struct seat *s = w->conn->seats; s; s = s->next) {
- seat_set_cursor(s, w, shape);
+ seat_set_cursor(s, w);
if (s->tool && s->tablet_focus == w)
tool_set_cursor(s->tool, shape);
}
@@ -279,7 +281,7 @@ static void pointer_enter(void *data, struct wl_pointer *pointer,
s->pointer_pos = device_from_fixed(w, surface_x, surface_y);
s->pointer_serial = serial;
s->cursor_shape = -1;
- seat_set_cursor(s, w, w->cursor);
+ seat_set_cursor(s, w);
s->motion_pending = true;
pointer_event_done(s);
}
@@ -448,6 +450,105 @@ static void pointer_axis_relative_direction(void *data, struct wl_pointer *point
static void pointer_warp(void *data, struct wl_pointer *pointer, wl_fixed_t x, wl_fixed_t y)
{ (void)data; (void)pointer; (void)x; (void)y; }
+static void relative_motion(void *data, struct zwp_relative_pointer_v1 *pointer,
+ uint32_t utime_hi, uint32_t utime_lo, wl_fixed_t dx, wl_fixed_t dy,
+ wl_fixed_t dx_unaccel, wl_fixed_t dy_unaccel)
+{
+ (void)pointer; (void)utime_hi; (void)utime_lo; (void)dx_unaccel; (void)dy_unaccel;
+ struct seat *s = data;
+ struct wayland_surface *w = s->pointer_focus;
+ /* Unlocked movement arrives as wl_pointer.motion; the queue derives its delta. */
+ if (!w || !w->pointer_locked || w->lock_seat != s) return;
+ flush_pending_motion(s, w);
+ push_pointer(s, w, YASL_EVENT_POINTER_MOTION, (struct yasl_pointer_event){
+ .pos = s->pointer_pos,
+ .delta = {wl_fixed_to_double(dx) * w->scale_factor,
+ wl_fixed_to_double(dy) * w->scale_factor},
+ });
+}
+
+static const struct zwp_relative_pointer_v1_listener relative_pointer_listener = {
+ .relative_motion = relative_motion,
+};
+
+static void pointer_locked(void *data, struct zwp_locked_pointer_v1 *lock)
+{
+ (void)lock;
+ struct wayland_surface *w = data;
+ w->pointer_locked = true;
+ surface_root(w)->input.relative = true;
+}
+
+static void pointer_unlocked(void *data, struct zwp_locked_pointer_v1 *lock)
+{
+ (void)lock;
+ struct wayland_surface *w = data;
+ w->pointer_locked = false;
+ surface_root(w)->input.relative = false;
+}
+
+static const struct zwp_locked_pointer_v1_listener locked_pointer_listener = {
+ .locked = pointer_locked,
+ .unlocked = pointer_unlocked,
+};
+
+/* A persistent lock re-engages each time the compositor focuses the surface. */
+static void surface_lock_pointer(struct wayland_surface *w, struct seat *s)
+{
+ struct wayland_display *c = w->conn;
+ if (!w->relative || w->pointer_lock || w->destroyed || !s->pointer)
+ return;
+ w->pointer_lock = zwp_pointer_constraints_v1_lock_pointer(c->pointer_constraints,
+ w->surface, s->pointer, NULL, ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT);
+ if (!w->pointer_lock)
+ return;
+ w->lock_seat = s;
+ zwp_locked_pointer_v1_add_listener(w->pointer_lock, &locked_pointer_listener, w);
+}
+
+void surface_unlock_pointer(struct wayland_surface *w)
+{
+ if (!w->pointer_lock)
+ return;
+ zwp_locked_pointer_v1_destroy(w->pointer_lock);
+ w->pointer_lock = NULL;
+ w->lock_seat = NULL;
+ if (w->pointer_locked) {
+ w->pointer_locked = false;
+ surface_root(w)->input.relative = false;
+ }
+}
+
+static void seat_lock_surfaces(struct seat *s)
+{
+ for (struct wayland_surface *w = s->conn->surfaces; w; w = w->conn_next)
+ surface_lock_pointer(w, s);
+}
+
+bool wayland_surface_set_pointer_mode(struct wayland_surface *w, enum yasl_pointer_mode mode)
+{
+ if (!w || !w->conn || w->destroyed)
+ return false;
+ struct wayland_display *c = w->conn;
+ bool relative = mode == YASL_POINTER_RELATIVE;
+ if (relative && (!c->relative_pointer_mgr || !c->pointer_constraints))
+ return false;
+ w->relative = relative;
+ if (relative) {
+ /* Prefer the seat that last delivered input; others lock once it goes away. */
+ struct seat *seat = c->active_seat && c->active_seat->pointer ? c->active_seat : NULL;
+ for (struct seat *s = c->seats; s && !seat; s = s->next)
+ if (s->pointer) seat = s;
+ if (seat)
+ surface_lock_pointer(w, seat);
+ } else {
+ surface_unlock_pointer(w);
+ }
+ for (struct seat *s = c->seats; s; s = s->next)
+ seat_set_cursor(s, w);
+ return true;
+}
+
static const struct wl_pointer_listener pointer_listener = {
.axis_relative_direction = pointer_axis_relative_direction,
.warp = pointer_warp,
@@ -984,6 +1085,18 @@ void seat_clipboard_ensure(struct seat *s)
static void seat_drop_pointer(struct seat *s)
{
+ struct wayland_display *c = s->conn;
+ bool unlocked = false;
+ for (struct wayland_surface *w = c->surfaces; w; w = w->conn_next) {
+ if (w->lock_seat == s) {
+ surface_unlock_pointer(w);
+ unlocked = true;
+ }
+ }
+ if (s->relative_pointer) {
+ zwp_relative_pointer_v1_destroy(s->relative_pointer);
+ s->relative_pointer = NULL;
+ }
if (s->cursor_shape_dev) {
wp_cursor_shape_device_v1_destroy(s->cursor_shape_dev);
s->cursor_shape_dev = NULL;
@@ -998,6 +1111,9 @@ static void seat_drop_pointer(struct seat *s)
seat_pointer_reset(s);
s->pointer_focus = s->ptr_left = NULL;
s->ptr_presses.n = 0;
+ for (struct seat *o = c->seats; unlocked && o; o = o->next)
+ if (o != s && o->pointer)
+ seat_lock_surfaces(o);
}
static void seat_drop_keyboard(struct seat *s)
@@ -1057,6 +1173,15 @@ static void seat_capabilities(void *data, struct wl_seat *proxy, uint32_t caps)
if (s->pointer && c->cursor_shape_mgr)
s->cursor_shape_dev = wp_cursor_shape_manager_v1_get_pointer(
c->cursor_shape_mgr, s->pointer);
+ if (s->pointer && c->relative_pointer_mgr) {
+ s->relative_pointer = zwp_relative_pointer_manager_v1_get_relative_pointer(
+ c->relative_pointer_mgr, s->pointer);
+ if (s->relative_pointer)
+ zwp_relative_pointer_v1_add_listener(s->relative_pointer,
+ &relative_pointer_listener, s);
+ }
+ if (s->pointer)
+ seat_lock_surfaces(s);
}
} else if (s->pointer) {
seat_drop_pointer(s);
diff --git a/src/platform/wayland/shell.c b/src/platform/wayland/shell.c
index 92c89d1..a12bd4d 100644
--- a/src/platform/wayland/shell.c
+++ b/src/platform/wayland/shell.c
@@ -208,6 +208,12 @@ static void registry_global(void *data, struct wl_registry *registry,
} else if (strcmp(interface, wp_cursor_shape_manager_v1_interface.name) == 0) {
c->cursor_shape_mgr = wl_registry_bind(registry, name,
&wp_cursor_shape_manager_v1_interface, 1);
+ } else if (strcmp(interface, zwp_relative_pointer_manager_v1_interface.name) == 0) {
+ c->relative_pointer_mgr = wl_registry_bind(registry, name,
+ &zwp_relative_pointer_manager_v1_interface, 1);
+ } else if (strcmp(interface, zwp_pointer_constraints_v1_interface.name) == 0) {
+ c->pointer_constraints = wl_registry_bind(registry, name,
+ &zwp_pointer_constraints_v1_interface, 1);
} else if (strcmp(interface, wp_fractional_scale_manager_v1_interface.name) == 0) {
c->scale_mgr = wl_registry_bind(registry, name,
&wp_fractional_scale_manager_v1_interface, 1);
diff --git a/src/platform/wayland/surface.c b/src/platform/wayland/surface.c
index ad3a24c..b17d203 100644
--- a/src/platform/wayland/surface.c
+++ b/src/platform/wayland/surface.c
@@ -200,6 +200,8 @@ static void display_free(struct wayland_display *c)
if (c->primary_selection_mgr) zwp_primary_selection_device_manager_v1_destroy(c->primary_selection_mgr);
if (c->data_manager) wl_data_device_manager_destroy(c->data_manager);
if (c->cursor_shape_mgr) wp_cursor_shape_manager_v1_destroy(c->cursor_shape_mgr);
+ if (c->relative_pointer_mgr) zwp_relative_pointer_manager_v1_destroy(c->relative_pointer_mgr);
+ if (c->pointer_constraints) zwp_pointer_constraints_v1_destroy(c->pointer_constraints);
if (c->cursor_theme) wl_cursor_theme_destroy(c->cursor_theme);
if (c->tablet_mgr) zwp_tablet_manager_v2_destroy(c->tablet_mgr);
if (c->text_input_mgr) zwp_text_input_manager_v3_destroy(c->text_input_mgr);
@@ -399,6 +401,7 @@ void wayland_surface_destroy(struct wayland_surface *w)
if (!w || w->destroyed) return;
for (struct wayland_surface *o = w->conn ? w->conn->surfaces : NULL; o; o = o->conn_next)
assert(o->parent != w && "destroy a popup before its parent");
+ surface_unlock_pointer(w);
input_queue_clear(&w->input);
free(w->ime_preedit);
free(w->ime_commit);
diff --git a/tests/meson.build b/tests/meson.build
index 951b30e..980f5db 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -189,6 +189,11 @@ if has_wayland
include_directories: inc, dependencies: yasl_dep,
)
test('cursor-wayland', cursor_wayland_test)
+ relative_pointer_test = executable('yasl-relative-pointer-wayland-test',
+ 'wayland/test_relative_pointer.c',
+ include_directories: inc, dependencies: yasl_dep,
+ )
+ test('relative-pointer-wayland', relative_pointer_test)
endif
benchmark_exe = executable(
diff --git a/tests/wayland/test_relative_pointer.c b/tests/wayland/test_relative_pointer.c
new file mode 100644
index 0000000..bcc41e3
--- /dev/null
+++ b/tests/wayland/test_relative_pointer.c
@@ -0,0 +1,170 @@
+#include "platform/wayland/internal.h"
+#include <assert.h>
+
+/* Record lock, relative-pointer and cursor requests without a compositor. */
+static char lock_objects[4];
+static int locks, unlocks, pointer_hides, pointer_shapes;
+static struct wl_surface *locked_surface;
+static struct wl_pointer *locked_wl_pointer;
+static uint32_t lock_lifetime;
+
+static struct zwp_locked_pointer_v1 *lock_pointer(struct zwp_pointer_constraints_v1 *mgr,
+ struct wl_surface *surface, struct wl_pointer *pointer,
+ struct wl_region *region, uint32_t lifetime)
+{
+ assert(mgr && surface && pointer && !region);
+ locked_surface = surface;
+ locked_wl_pointer = pointer;
+ lock_lifetime = lifetime;
+ return (struct zwp_locked_pointer_v1 *)&lock_objects[locks++ % 4];
+}
+
+static int lock_add_listener(struct zwp_locked_pointer_v1 *lock,
+ const struct zwp_locked_pointer_v1_listener *listener, void *data)
+{
+ assert(lock && listener && data);
+ return 0;
+}
+
+static void lock_destroy(struct zwp_locked_pointer_v1 *lock)
+{
+ assert(lock);
+ unlocks++;
+}
+
+static void pointer_cursor(struct wl_pointer *pointer, uint32_t serial,
+ struct wl_surface *surface, int32_t x, int32_t y)
+{
+ assert(pointer && serial && !surface && !x && !y);
+ pointer_hides++;
+}
+
+static void cursor_shape(struct wp_cursor_shape_device_v1 *device,
+ uint32_t serial, uint32_t shape)
+{
+ assert(device && serial && shape == WP_CURSOR_SHAPE_DEVICE_V1_SHAPE_TEXT);
+ pointer_shapes++;
+}
+
+static uint32_t pointer_version(struct wl_pointer *pointer)
+{
+ assert(pointer);
+ return WL_POINTER_FRAME_SINCE_VERSION;
+}
+
+static void pointer_release(struct wl_pointer *pointer) { assert(pointer); }
+static void shape_device_destroy(struct wp_cursor_shape_device_v1 *device) { assert(device); }
+
+#define zwp_pointer_constraints_v1_lock_pointer lock_pointer
+#define zwp_locked_pointer_v1_add_listener lock_add_listener
+#define zwp_locked_pointer_v1_destroy lock_destroy
+#define wl_pointer_set_cursor pointer_cursor
+#define wp_cursor_shape_device_v1_set_shape cursor_shape
+#define wp_cursor_shape_device_v1_destroy shape_device_destroy
+#define wl_pointer_get_version pointer_version
+#define wl_pointer_release pointer_release
+#include "../../src/platform/wayland/pointer.c"
+
+static struct yasl_event next_event(struct wayland_surface *w)
+{
+ struct yasl_event event;
+ assert(yasl_surface_poll_event(&w->base, &event));
+ return event;
+}
+
+static void move_to(struct seat *s, int x, int y)
+{
+ pointer_motion(s, s->pointer, 0, wl_fixed_from_int(x), wl_fixed_from_int(y));
+ pointer_frame(s, s->pointer);
+}
+
+static void relative_move(struct seat *s, double dx, double dy)
+{
+ relative_motion(s, NULL, 0, 0, wl_fixed_from_double(dx), wl_fixed_from_double(dy), 0, 0);
+}
+
+int main(void)
+{
+ struct wayland_display display = {.base.ops = &yasl_wayland_backend};
+ struct wayland_surface surface = {.base.display = &display.base, .conn = &display,
+ .scale_factor = 2, .cursor = YASL_CURSOR_TEXT};
+ surface.surface = (struct wl_surface *)&surface;
+ wl_surface_set_user_data(surface.surface, &surface);
+ display.surfaces = &surface;
+ struct seat seat = {.conn = &display, .cursor_shape = -1};
+ seat.pointer = (struct wl_pointer *)&seat;
+ seat.cursor_shape_dev = (struct wp_cursor_shape_device_v1 *)&seat;
+ display.seats = &seat;
+ struct yasl_event event;
+
+ /* Without both protocols the surface stays in absolute mode. */
+ assert(!yasl_surface_set_pointer_mode(&surface.base, YASL_POINTER_RELATIVE));
+ assert(!surface.relative && !locks);
+ display.relative_pointer_mgr = (struct zwp_relative_pointer_manager_v1 *)&display;
+ display.pointer_constraints = (struct zwp_pointer_constraints_v1 *)&display;
+
+ pointer_enter(&seat, seat.pointer, 10, surface.surface, wl_fixed_from_int(5), wl_fixed_from_int(5));
+ pointer_frame(&seat, seat.pointer);
+ assert(pointer_shapes == 1);
+ event = next_event(&surface);
+ assert(event.type == YASL_EVENT_POINTER_MOTION && event.pointer.pos.x == 10);
+
+ /* Relative mode locks persistently on the pointer's seat and hides the cursor. */
+ assert(yasl_surface_set_pointer_mode(&surface.base, YASL_POINTER_RELATIVE));
+ assert(locks == 1 && locked_surface == surface.surface && locked_wl_pointer == seat.pointer);
+ assert(lock_lifetime == ZWP_POINTER_CONSTRAINTS_V1_LIFETIME_PERSISTENT);
+ assert(pointer_hides == 1 && surface.lock_seat == &seat);
+
+ /* Until the compositor activates the lock, motion is absolute with a queue delta. */
+ relative_move(&seat, 3, 3);
+ move_to(&seat, 7, 5);
+ event = next_event(&surface);
+ assert(event.pointer.pos.x == 14 && event.pointer.pos.y == 10);
+ assert(event.pointer.delta.x == 4 && event.pointer.delta.y == 0);
+ assert(!yasl_surface_poll_event(&surface.base, &event));
+
+ /* Locked: relative samples in device pixels, position frozen at the lock. */
+ pointer_locked(&surface, surface.pointer_lock);
+ relative_move(&seat, 3.5, -2);
+ relative_move(&seat, 1000, 0);
+ event = next_event(&surface);
+ assert(event.type == YASL_EVENT_POINTER_MOTION);
+ assert(event.pointer.pos.x == 14 && event.pointer.pos.y == 10);
+ assert(event.pointer.delta.x == 7 && event.pointer.delta.y == -4);
+ event = next_event(&surface);
+ assert(event.pointer.delta.x == 2000 && event.pointer.pos.x == 14);
+
+ /* Deactivation (e.g. focus loss) returns to absolute motion without unlocking. */
+ pointer_unlocked(&surface, surface.pointer_lock);
+ relative_move(&seat, 5, 5);
+ assert(!yasl_surface_poll_event(&surface.base, &event));
+ move_to(&seat, 8, 5);
+ event = next_event(&surface);
+ assert(event.pointer.delta.x == 2 && event.pointer.delta.y == 0);
+ assert(unlocks == 0 && surface.pointer_lock);
+ pointer_locked(&surface, surface.pointer_lock);
+
+ /* Returning to normal destroys the lock and restores the chosen cursor. */
+ assert(yasl_surface_set_pointer_mode(&surface.base, YASL_POINTER_NORMAL));
+ assert(unlocks == 1 && !surface.pointer_lock && !surface.input.relative);
+ assert(pointer_shapes == 2);
+ assert(yasl_surface_set_pointer_mode(&surface.base, YASL_POINTER_NORMAL));
+ assert(unlocks == 1);
+ assert(yasl_surface_set_pointer_mode(&surface.base, YASL_POINTER_RELATIVE));
+ assert(locks == 2 && pointer_hides == 2);
+ pointer_locked(&surface, surface.pointer_lock);
+
+ /* Losing the seat's pointer hands the lock to another seat with a pointer. */
+ struct seat other = {.conn = &display, .cursor_shape = -1};
+ other.pointer = (struct wl_pointer *)&other;
+ seat.next = &other;
+ seat_drop_pointer(&seat);
+ assert(unlocks == 2 && locks == 3 && locked_wl_pointer == other.pointer);
+ assert(surface.lock_seat == &other && !surface.input.relative);
+
+ /* Surface destruction releases the lock through surface_unlock_pointer(). */
+ surface_unlock_pointer(&surface);
+ assert(unlocks == 3 && !surface.lock_seat && !surface.pointer_lock);
+ input_queue_clear(&surface.input);
+ return 0;
+}