Feed the mouse to the UI and tell the host which events it consumed

akgl_ui_handle_event is the mouse half of the input story -- keyboard and
gamepad stay with the control maps, because keyboard focus is something an
application declares rather than something a pointer position implies. It
handles motion, left-button presses and releases, the wheel and window
resizes; everything else, and everything while the subsystem is inert,
reports consumed=false and succeeds, the same pass-everything contract
akgl_controller_handle_event already has. The documented call order is UI
first, control maps second, skipping on consumed.

A press, release or wheel is consumed when the pointer is over any UI
element. The hit test asks clay against the layout the previous frame
retained -- this frame's does not exist while events are polled, and one
frame of staleness is the standard model's accepted cost -- with clay's
internal full-screen Clay__RootContainer excluded, because being inside the
window is not being over the interface (unexcluded, every click anywhere was
consumed; the test that pins this caught it).

Pointer state is fed to clay per event, as clay documents; the press edge is
latched once per frame_begin for the widgets to come, rather than trusting
Clay_PointerData's per-SetPointerState edge, which advances per mouse event.
frame_begin also drives Clay_UpdateScrollContainers from the accumulated
wheel (32 pixels per notch) and a wall-clock dt.

Tests pin the whole contract: pass-through before init, no consumption
before any layout exists, press/release inside vs beside a panel, right
button ignored, motion never consumed, wheel consumed only over the UI,
resize reaching the layout engine, and key events left alone.

Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
This commit is contained in:
2026-08-02 11:10:35 -04:00
parent f11b9c5603
commit 606fc3364a
3 changed files with 354 additions and 0 deletions

View File

@@ -38,6 +38,8 @@
#include <stddef.h>
#include <stdint.h>
#include <SDL3/SDL.h>
#include <akerror.h>
#include <clay.h>
@@ -205,6 +207,50 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_resize(int width, int height);
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_font_register(char *name, uint16_t *fontid);
/**
* @brief Feed one SDL event to the UI, and learn whether the UI claimed it.
*
* The mouse half of the input story -- keyboard and gamepad stay with the
* control maps and the widget helpers, because keyboard focus is something
* the application declares, not something a pointer position implies. This
* handles mouse motion, left-button presses and releases, the wheel, and
* window resizes; every other event, and every event while the subsystem is
* not initialized, reports @p consumed false and succeeds, so a host passes
* every event through unconditionally -- the same contract
* akgl_controller_handle_event() has.
*
* Call it *before* akgl_controller_handle_event() in the event loop, and skip
* the rest of the chain when it consumed:
*
* while ( SDL_PollEvent(&event) ) {
* PASS(errctx, akgl_ui_handle_event(&app, &event, &consumed));
* if ( consumed ) { continue; }
* PASS(errctx, akgl_controller_handle_event(&app, &event));
* }
*
* A press or release is consumed when the pointer is over any UI element;
* motion and resizes never are (the game may care where the mouse is, and
* certainly cares about its window). The hit test runs against the layout
* the *previous* frame declared, because this frame's does not exist while
* events are being polled -- clay retains the last tree for exactly this
* purpose, and one frame of staleness is the accepted cost of the standard
* model. Before any frame has been laid out, nothing is over anything and
* nothing is consumed.
*
* @param appstate The application state pointer the event loop owns.
* Required, though this implementation does not read it --
* the parameter exists so the signature matches
* akgl_controller_handle_event() and can grow the same way.
* @param event The event SDL handed the loop. Required.
* @param consumed Receives whether the UI claimed the event. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p appstate, @p event, or @p consumed is
* `NULL`.
* @throws AKERR_OUTOFBOUNDS If a resize event carries a non-positive size,
* from akgl_ui_resize().
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_handle_event(void *appstate, SDL_Event *event, bool *consumed);
/**
* @brief Open the UI frame: clear the error stash and begin the clay layout.
*