From cbd1fc7ee6dced7ad6beecd5b9ad56a7f977f0c4 Mon Sep 17 00:00:00 2001 From: Berke Güzel Date: Tue, 13 Oct 2026 16:57:59 +0300 Subject: Report held pointer buttons on every pointer event Pointer events set button only on press and release, so wsap_term tracked held buttons itself to tell a drag from a hover. yasl_pointer_event.buttons now holds a YASL_BUTTON_BIT() mask of the buttons held after each event. The common input queue tracks it from the DOWN and UP events it queues, and clears it on INPUT_RESET and on allocation failure. Wayland also clears it on leave, since the compositor sends leave during a press only when it takes the pointer and no release follows. X11 and Cocoa keep it, because they deliver motion and the release to the pressed window after leave. Co-Authored-By: Claude Opus 5.5 --- docs/guide.md | 9 +++++++-- include/yasl/yasl.h | 9 +++++++++ src/common/input.c | 17 ++++++++++++++++- src/common/input.h | 7 ++++++- src/platform/wayland/popup.c | 1 + src/platform/wayland/surface.c | 2 ++ tests/common/test_contracts.c | 40 ++++++++++++++++++++++++++++++++++++++++ tests/common/test_input_alloc.c | 10 ++++++++++ 8 files changed, 91 insertions(+), 4 deletions(-) diff --git a/docs/guide.md b/docs/guide.md index 888e4ab..61583b1 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -219,8 +219,13 @@ remain available for commands; input methods may consume keys while composing. Pointer events use the `YASL_EVENT_POINTER_*` types and share `event.pointer`, with `device` distinguishing mouse, touch, pen, and eraser. `POINTER_DOWN` and `POINTER_UP` carry a `YASL_BUTTON_*` value: `PRIMARY` is the left button, pen tip, -or touch contact. Track those transitions; other types leave `button` -meaningless. `POINTER_MOTION` reports `delta`, the device-pixel movement since +or touch contact. Other types leave `button` meaningless. Every pointer event +also carries `buttons`, a `YASL_BUTTON_BIT()` mask of the buttons held after +that event, tracked from the queue's down and up events. Use it to tell a drag +from a hover. `YASL_EVENT_INPUT_RESET` clears the mask. On Wayland, +`POINTER_LEAVE` also clears it, because the compositor sends leave during a +press only when it takes the pointer away. X11 and Cocoa keep the press across +leave and still deliver the release. `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 diff --git a/include/yasl/yasl.h b/include/yasl/yasl.h index de9a62c..973c39d 100644 --- a/include/yasl/yasl.h +++ b/include/yasl/yasl.h @@ -299,6 +299,9 @@ enum yasl_pointer_button { YASL_BUTTON_FORWARD = 4, /**< Side "forward" button. */ }; +/** Bit for a yasl_pointer_button in yasl_pointer_event.buttons. */ +#define YASL_BUTTON_BIT(button) (1u << (button)) + /** Pointer capture mode for a surface. */ enum yasl_pointer_mode { YASL_POINTER_NORMAL = 0, /**< Absolute positions; cursor per yasl_surface_set_cursor(). */ @@ -376,6 +379,12 @@ struct yasl_pointer_event { struct yasl_vector2d scroll; /** DOWN/UP: which button changed. */ enum yasl_pointer_button button; + /** YASL_BUTTON_BIT() mask of buttons held after this event, on every + * POINTER_* type. Tracked from this queue's DOWN and UP events for all + * devices; INPUT_RESET clears it. On Wayland LEAVE clears it too, since the + * compositor sends leave during a press only when it takes the pointer. + * X11 and Cocoa keep sending motion and the release after LEAVE. */ + uint32_t buttons; /** Mouse, touch, pen, or eraser. */ enum yasl_pointer_device device; /** yasl_mod bitmask at this event. */ diff --git a/src/common/input.c b/src/common/input.c index 0b55f1f..ce3219c 100644 --- a/src/common/input.c +++ b/src/common/input.c @@ -33,6 +33,7 @@ bool input_queue_push(struct input_queue *q, const struct yasl_event *ev) owned[len] = '\0'; copy.text.data = owned; } + uint32_t buttons = q->buttons; switch (ev->type) { case YASL_EVENT_POINTER_MOTION: if (!q->relative) { @@ -41,19 +42,31 @@ bool input_queue_push(struct input_queue *q, const struct yasl_event *ev) ev->pointer.pos.y - q->last_pos.y} : (struct yasl_vector2d){0, 0}; } - [[fallthrough]]; + q->last_pos = ev->pointer.pos; + q->has_last_pos = true; + break; case YASL_EVENT_POINTER_DOWN: case YASL_EVENT_POINTER_UP: + if ((unsigned)ev->pointer.button < 32) { + uint32_t bit = YASL_BUTTON_BIT(ev->pointer.button); + buttons = ev->type == YASL_EVENT_POINTER_DOWN ? buttons | bit : buttons & ~bit; + } q->last_pos = ev->pointer.pos; q->has_last_pos = true; break; case YASL_EVENT_POINTER_LEAVE: + if (q->leave_ends_press) buttons = 0; + q->has_last_pos = false; + break; case YASL_EVENT_INPUT_RESET: + buttons = 0; q->has_last_pos = false; break; default: break; } + if (ev->type >= YASL_EVENT_POINTER_DOWN && ev->type <= YASL_EVENT_POINTER_LEAVE) + copy.pointer.buttons = buttons; if (q->count == q->capacity) { size_t capacity = q->capacity ? q->capacity * 2 : 32; if (capacity < q->capacity || capacity > SIZE_MAX / sizeof(*q->events)) @@ -74,6 +87,7 @@ bool input_queue_push(struct input_queue *q, const struct yasl_event *ev) } q->events[(q->head + q->count) & (q->capacity - 1)] = copy; q->count++; + q->buttons = buttons; return true; failed: @@ -84,6 +98,7 @@ failed: q->reset_surface = ev->surface; q->reset_pending = true; q->has_last_pos = false; + q->buttons = 0; return false; } diff --git a/src/common/input.h b/src/common/input.h index ffd1663..aad5c9a 100644 --- a/src/common/input.h +++ b/src/common/input.h @@ -15,11 +15,16 @@ struct input_queue { bool relative; struct yasl_surface *reset_surface; bool reset_pending; + /* YASL_BUTTON_BIT() mask from pushed DOWN/UP events. */ + uint32_t buttons; + /* Set by backends whose pointer leave ends any press: no release follows. */ + bool leave_ends_press; }; /* Copies text payloads and stamps time_ns when zero. Unless q->relative is set, * POINTER_MOTION delta is computed from the previous position; otherwise the - * backend-supplied delta is kept. Leave and reset clear the position history. */ + * backend-supplied delta is kept. Leave and reset clear the position history. + * Pointer events get the held-button mask; reset clears it. */ bool input_queue_push(struct input_queue *q, const struct yasl_event *ev); bool input_queue_pop(struct input_queue *q, struct yasl_event *ev); void input_queue_clear(struct input_queue *q); diff --git a/src/platform/wayland/popup.c b/src/platform/wayland/popup.c index cb578d5..44c7206 100644 --- a/src/platform/wayland/popup.c +++ b/src/platform/wayland/popup.c @@ -138,6 +138,7 @@ struct wayland_surface *wayland_surface_create_popup(struct wayland_surface *par return NULL; w->base.display = &c->base; w->base.state = YASL_STATE_CSD; + w->input.leave_ends_press = true; w->conn = c; w->conn_next = c->surfaces; c->surfaces = w; diff --git a/src/platform/wayland/surface.c b/src/platform/wayland/surface.c index b17d203..0f6d7d7 100644 --- a/src/platform/wayland/surface.c +++ b/src/platform/wayland/surface.c @@ -298,6 +298,8 @@ uint32_t wayland_display_caps(const struct wayland_display *c) static void conn_link(struct wayland_display *c, struct wayland_surface *w) { w->base.display = &c->base; + /* The compositor sends leave during a press only when it takes the pointer. */ + w->input.leave_ends_press = true; w->conn = c; w->conn_next = c->surfaces; c->surfaces = w; diff --git a/tests/common/test_contracts.c b/tests/common/test_contracts.c index 7c1ab06..654eb1e 100644 --- a/tests/common/test_contracts.c +++ b/tests/common/test_contracts.c @@ -143,6 +143,45 @@ static bool native_frame(struct yasl_surface *s, const struct yasl_framebuffer * return true; } +static uint32_t push_buttons(struct input_queue *q, enum yasl_event_type type, + enum yasl_pointer_button button) +{ + struct yasl_event ev = {.type = type, .pointer.button = button}; + assert(input_queue_push(q, &ev)); + assert(input_queue_pop(q, &ev)); + return ev.pointer.buttons; +} + +static void test_held_buttons(void) +{ + const uint32_t primary = YASL_BUTTON_BIT(YASL_BUTTON_PRIMARY); + const uint32_t secondary = YASL_BUTTON_BIT(YASL_BUTTON_SECONDARY); + struct input_queue q = {0}; + assert(push_buttons(&q, YASL_EVENT_POINTER_MOTION, 0) == 0); + assert(push_buttons(&q, YASL_EVENT_POINTER_DOWN, YASL_BUTTON_PRIMARY) == primary); + assert(push_buttons(&q, YASL_EVENT_POINTER_MOTION, 0) == primary); + assert(push_buttons(&q, YASL_EVENT_POINTER_DOWN, YASL_BUTTON_SECONDARY) == (primary | secondary)); + assert(push_buttons(&q, YASL_EVENT_POINTER_UP, YASL_BUTTON_PRIMARY) == secondary); + assert(push_buttons(&q, YASL_EVENT_POINTER_SCROLL, 0) == secondary); + /* X11 and Cocoa keep the press across leave; the release still follows. */ + assert(push_buttons(&q, YASL_EVENT_POINTER_LEAVE, 0) == secondary); + assert(push_buttons(&q, YASL_EVENT_POINTER_UP, YASL_BUTTON_SECONDARY) == 0); + /* A release without a press leaves nothing held. */ + assert(push_buttons(&q, YASL_EVENT_POINTER_UP, YASL_BUTTON_MIDDLE) == 0); + + assert(push_buttons(&q, YASL_EVENT_POINTER_DOWN, YASL_BUTTON_PRIMARY) == primary); + struct yasl_event reset = {.type = YASL_EVENT_INPUT_RESET}; + assert(input_queue_push(&q, &reset)); + assert(input_queue_pop(&q, &reset)); + assert(push_buttons(&q, YASL_EVENT_POINTER_MOTION, 0) == 0); + + q.leave_ends_press = true; + assert(push_buttons(&q, YASL_EVENT_POINTER_DOWN, YASL_BUTTON_PRIMARY) == primary); + assert(push_buttons(&q, YASL_EVENT_POINTER_LEAVE, 0) == 0); + assert(push_buttons(&q, YASL_EVENT_POINTER_MOTION, 0) == 0); + input_queue_clear(&q); +} + static void test_native(void) { const struct yasl_backend_ops ops = {.display_caps = native_caps, @@ -332,6 +371,7 @@ int main(void) test_timing_clock(); test_surface_state(); test_queue(); + test_held_buttons(); test_validation(); test_native(); test_scale(); diff --git a/tests/common/test_input_alloc.c b/tests/common/test_input_alloc.c index b999a62..7aba4b2 100644 --- a/tests/common/test_input_alloc.c +++ b/tests/common/test_input_alloc.c @@ -46,6 +46,16 @@ int main(void) assert(input_queue_pop(&q, &out) && out.type == YASL_EVENT_INPUT_RESET); assert(input_queue_pop(&q, &out) && out.key.action == YASL_KEY_RELEASE); input_queue_clear(&q); + + /* A lost event clears held buttons along with the stream. */ + in = (struct yasl_event){.surface = surface, .type = YASL_EVENT_POINTER_DOWN}; + assert(input_queue_push(&q, &in) && q.buttons); + for (size_t i = 1; i < q.capacity; i++) assert(input_queue_push(&q, &in)); + allocations = 0; + assert(!input_queue_push(&q, &in)); + allocations = -1; + assert(!q.buttons); + input_queue_clear(&q); } return 0; } -- cgit v1.2.3