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
This commit is contained in:
@@ -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.
|
||||
*
|
||||
|
||||
159
src/ui.c
159
src/ui.c
@@ -15,6 +15,7 @@
|
||||
|
||||
#include <akgl/draw.h>
|
||||
#include <akgl/error.h>
|
||||
#include <akgl/game.h>
|
||||
#include <akgl/registry.h>
|
||||
#include <akgl/renderer.h>
|
||||
#include <akgl/sprite.h>
|
||||
@@ -53,6 +54,39 @@ static int ui_font_count;
|
||||
/** @brief Scratch one TEXT render command's line is copied through, so SDL_ttf gets a C string. */
|
||||
static char ui_text_scratch[AKGL_UI_MAX_TEXT_BYTES];
|
||||
|
||||
/**
|
||||
* @brief Pixels one wheel notch scrolls a clay scroll container.
|
||||
*
|
||||
* SDL reports the wheel in notches; clay wants pixels. 32 is three lines of
|
||||
* 16pt text per notch, which is what desktop toolkits converge on.
|
||||
*/
|
||||
#define UI_WHEEL_NOTCH_PIXELS 32.0f
|
||||
|
||||
/** @brief Where the pointer last was, in layout pixels. Fed to clay per event. */
|
||||
static float ui_pointer_x;
|
||||
/** @brief Vertical half of the pointer position. */
|
||||
static float ui_pointer_y;
|
||||
/** @brief Whether the left button is currently held. */
|
||||
static bool ui_pointer_down;
|
||||
/**
|
||||
* @brief A left press arrived since the last frame_begin latched.
|
||||
*
|
||||
* Widgets read the *latched* copy (#ui_pointer_pressed) rather than clay's
|
||||
* `Clay_PointerData.state`, because clay advances its press-edge per
|
||||
* Clay_SetPointerState call and several of those can land between two
|
||||
* layouts -- one per motion event -- which turns "pressed this frame" into
|
||||
* "pressed since the last mouse event", an edge no widget can trust.
|
||||
*/
|
||||
static bool ui_pointer_pending_pressed;
|
||||
/** @brief The frame-stable press edge: one left press happened before this frame. */
|
||||
static bool ui_pointer_pressed;
|
||||
/** @brief Wheel notches accumulated since the last frame_begin, horizontal. */
|
||||
static float ui_wheel_x;
|
||||
/** @brief Wheel notches accumulated since the last frame_begin, vertical. */
|
||||
static float ui_wheel_y;
|
||||
/** @brief When the last frame_begin ran, for the scroll containers' dt. 0 before the first. */
|
||||
static uint64_t ui_last_frame_ns;
|
||||
|
||||
/**
|
||||
* @brief First error clay reported since the stash was last cleared.
|
||||
*
|
||||
@@ -199,6 +233,14 @@ akerr_ErrorContext *akgl_ui_shutdown(void)
|
||||
ui_font_count = 0;
|
||||
ui_clay_error_text[0] = '\0';
|
||||
ui_clay_error_count = 0;
|
||||
ui_pointer_x = 0.0f;
|
||||
ui_pointer_y = 0.0f;
|
||||
ui_pointer_down = false;
|
||||
ui_pointer_pending_pressed = false;
|
||||
ui_pointer_pressed = false;
|
||||
ui_wheel_x = 0.0f;
|
||||
ui_wheel_y = 0.0f;
|
||||
ui_last_frame_ns = 0;
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
@@ -561,8 +603,108 @@ akerr_ErrorContext *akgl_ui_execute_commands(akgl_RenderBackend *self, Clay_Rend
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Report whether the pointer is over any element of the retained layout.
|
||||
*
|
||||
* The one question the consumed contract turns on. Asked of clay rather than
|
||||
* answered from our own geometry, so floating elements, z-order and culling
|
||||
* all mean whatever clay says they mean -- with one exception: clay wraps
|
||||
* every layout in an internal `Clay__RootContainer` sized to the whole
|
||||
* screen, so "over anything" naively answers yes for every pixel of the
|
||||
* window. The root is excluded here; being inside the window is not being
|
||||
* over the interface.
|
||||
*/
|
||||
static bool ui_pointer_over_anything(void)
|
||||
{
|
||||
static uint32_t rootid = 0;
|
||||
Clay_ElementIdArray over = Clay_GetPointerOverIds();
|
||||
int32_t i = 0;
|
||||
|
||||
if ( rootid == 0 ) {
|
||||
rootid = Clay_GetElementId(CLAY_STRING("Clay__RootContainer")).id;
|
||||
}
|
||||
for ( i = 0; i < over.length; i++ ) {
|
||||
if ( over.internalArray[i].id != rootid ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_ui_handle_event(void *appstate, SDL_Event *event, bool *consumed)
|
||||
{
|
||||
Clay_Vector2 position;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, appstate, AKERR_NULLPOINTER, "NULL appstate");
|
||||
FAIL_ZERO_RETURN(errctx, event, AKERR_NULLPOINTER, "NULL event");
|
||||
FAIL_ZERO_RETURN(errctx, consumed, AKERR_NULLPOINTER, "NULL consumed destination");
|
||||
*consumed = false;
|
||||
// Inert until initialized, like the rest of the subsystem: the host is
|
||||
// told "not mine" rather than "not ready", so its event loop needs no
|
||||
// knowledge of whether the UI is up.
|
||||
if ( ui_context == NULL ) {
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
switch ( event->type ) {
|
||||
case SDL_EVENT_MOUSE_MOTION:
|
||||
ui_pointer_x = event->motion.x;
|
||||
ui_pointer_y = event->motion.y;
|
||||
position.x = ui_pointer_x;
|
||||
position.y = ui_pointer_y;
|
||||
// Fed per event, not per frame, which is the usage clay documents:
|
||||
// hover tracks the pointer through fast motion instead of sampling
|
||||
// wherever it happened to be when the frame began.
|
||||
Clay_SetPointerState(position, ui_pointer_down);
|
||||
break;
|
||||
case SDL_EVENT_MOUSE_BUTTON_DOWN:
|
||||
if ( event->button.button != SDL_BUTTON_LEFT ) {
|
||||
break;
|
||||
}
|
||||
ui_pointer_x = event->button.x;
|
||||
ui_pointer_y = event->button.y;
|
||||
ui_pointer_down = true;
|
||||
ui_pointer_pending_pressed = true;
|
||||
position.x = ui_pointer_x;
|
||||
position.y = ui_pointer_y;
|
||||
Clay_SetPointerState(position, true);
|
||||
*consumed = ui_pointer_over_anything();
|
||||
break;
|
||||
case SDL_EVENT_MOUSE_BUTTON_UP:
|
||||
if ( event->button.button != SDL_BUTTON_LEFT ) {
|
||||
break;
|
||||
}
|
||||
ui_pointer_x = event->button.x;
|
||||
ui_pointer_y = event->button.y;
|
||||
ui_pointer_down = false;
|
||||
position.x = ui_pointer_x;
|
||||
position.y = ui_pointer_y;
|
||||
Clay_SetPointerState(position, false);
|
||||
// The release is consumed on the same terms as the press: a click
|
||||
// that began on a panel must not leak its other half to the game.
|
||||
*consumed = ui_pointer_over_anything();
|
||||
break;
|
||||
case SDL_EVENT_MOUSE_WHEEL:
|
||||
ui_wheel_x += event->wheel.x;
|
||||
ui_wheel_y += event->wheel.y;
|
||||
*consumed = ui_pointer_over_anything();
|
||||
break;
|
||||
case SDL_EVENT_WINDOW_RESIZED:
|
||||
PASS(errctx, akgl_ui_resize(event->window.data1, event->window.data2));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
akerr_ErrorContext *akgl_ui_frame_begin(void)
|
||||
{
|
||||
Clay_Vector2 scroll;
|
||||
uint64_t now = 0;
|
||||
float dt = 0.0f;
|
||||
|
||||
PREPARE_ERROR(errctx);
|
||||
FAIL_ZERO_RETURN(errctx, ui_context, AKGL_ERR_UI, "The UI subsystem is not initialized");
|
||||
FAIL_NONZERO_RETURN(
|
||||
@@ -572,6 +714,23 @@ akerr_ErrorContext *akgl_ui_frame_begin(void)
|
||||
"A UI frame is already open; a frame_begin/frame_begin sequence means a frame_end went missing");
|
||||
ui_clay_error_text[0] = '\0';
|
||||
ui_clay_error_count = 0;
|
||||
|
||||
// Latch the press edge for the widgets this frame declares, then let the
|
||||
// event loop start accumulating the next one.
|
||||
ui_pointer_pressed = ui_pointer_pending_pressed;
|
||||
ui_pointer_pending_pressed = false;
|
||||
|
||||
now = SDL_GetTicksNS();
|
||||
if ( ui_last_frame_ns != 0 ) {
|
||||
dt = (float)(now - ui_last_frame_ns) / (float)AKGL_TIME_ONESEC_NS;
|
||||
}
|
||||
ui_last_frame_ns = now;
|
||||
scroll.x = ui_wheel_x * UI_WHEEL_NOTCH_PIXELS;
|
||||
scroll.y = ui_wheel_y * UI_WHEEL_NOTCH_PIXELS;
|
||||
ui_wheel_x = 0.0f;
|
||||
ui_wheel_y = 0.0f;
|
||||
Clay_UpdateScrollContainers(false, scroll, dt);
|
||||
|
||||
Clay_BeginLayout();
|
||||
ui_in_frame = true;
|
||||
SUCCEED_RETURN(errctx);
|
||||
|
||||
149
tests/ui.c
149
tests/ui.c
@@ -482,6 +482,154 @@ akerr_ErrorContext *test_ui_layout_text(void)
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/** @brief Build a left-button mouse event at a position, down or up. */
|
||||
static void make_button_event(SDL_Event *event, uint32_t type, float x, float y)
|
||||
{
|
||||
SDL_memset(event, 0x00, sizeof(SDL_Event));
|
||||
event->type = type;
|
||||
event->button.button = SDL_BUTTON_LEFT;
|
||||
event->button.x = x;
|
||||
event->button.y = y;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief The consumed contract: clicks on the UI are the UI's, everything else passes.
|
||||
*
|
||||
* The hit test runs against the layout the previous frame retained, so the
|
||||
* test lays one panel out, then throws events at it: a click inside is
|
||||
* consumed, a click outside is not, motion never is, and -- the case most
|
||||
* likely to surprise -- a click before any layout exists is not consumed,
|
||||
* because there is nothing to be over.
|
||||
*/
|
||||
akerr_ErrorContext *test_ui_handle_event(void)
|
||||
{
|
||||
PREPARE_ERROR(e);
|
||||
SDL_Event event;
|
||||
bool consumed = true;
|
||||
int token = 1;
|
||||
|
||||
ATTEMPT {
|
||||
// Uninitialized: everything passes through untouched.
|
||||
make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_DOWN, 10.0f, 10.0f);
|
||||
TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
|
||||
"an event before init was refused");
|
||||
TEST_ASSERT(e, consumed == false, "an event before init was consumed");
|
||||
|
||||
TEST_EXPECT_OK(e, akgl_ui_init(TEST_TARGET_SIZE, TEST_TARGET_SIZE),
|
||||
"init for the event test failed");
|
||||
|
||||
// Initialized but nothing laid out yet: a click lands on no tree.
|
||||
make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_DOWN, 10.0f, 10.0f);
|
||||
TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
|
||||
"a click before any layout was refused");
|
||||
TEST_ASSERT(e, consumed == false, "a click was consumed before any layout existed");
|
||||
make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_UP, 10.0f, 10.0f);
|
||||
TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
|
||||
"the release before any layout was refused");
|
||||
|
||||
// Lay out one 20x20 panel at (8,8) for the hit tests that follow.
|
||||
CATCH(e, clear_target());
|
||||
TEST_EXPECT_OK(e, akgl_ui_frame_begin(), "opening the hit-test frame failed");
|
||||
CLAY({ .layout = { .padding = { 8, 0, 8, 0 } } }) {
|
||||
CLAY({
|
||||
.layout = {
|
||||
.sizing = {
|
||||
.width = CLAY_SIZING_FIXED(20),
|
||||
.height = CLAY_SIZING_FIXED(20)
|
||||
}
|
||||
},
|
||||
.backgroundColor = { 255, 0, 0, 255 }
|
||||
}) {}
|
||||
}
|
||||
TEST_EXPECT_OK(e, akgl_ui_frame_end(akgl_renderer), "closing the hit-test frame failed");
|
||||
|
||||
// A press on the panel is the UI's; its release is too.
|
||||
make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_DOWN, 10.0f, 10.0f);
|
||||
TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
|
||||
"the press on the panel was refused");
|
||||
TEST_ASSERT(e, consumed == true, "a press on a UI panel was not consumed");
|
||||
make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_UP, 10.0f, 10.0f);
|
||||
TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
|
||||
"the release on the panel was refused");
|
||||
TEST_ASSERT(e, consumed == true, "a release on a UI panel was not consumed");
|
||||
|
||||
// A press beside the panel belongs to the game.
|
||||
make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_DOWN, 50.0f, 50.0f);
|
||||
TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
|
||||
"the press beside the panel was refused");
|
||||
TEST_ASSERT(e, consumed == false, "a press beside the panel was consumed");
|
||||
make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_UP, 50.0f, 50.0f);
|
||||
TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
|
||||
"the release beside the panel was refused");
|
||||
|
||||
// A right-button press is not the UI's business at all.
|
||||
make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_DOWN, 10.0f, 10.0f);
|
||||
event.button.button = SDL_BUTTON_RIGHT;
|
||||
TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
|
||||
"a right-button press was refused");
|
||||
TEST_ASSERT(e, consumed == false, "a right-button press was consumed");
|
||||
|
||||
// Motion is never consumed, wherever it lands.
|
||||
SDL_memset(&event, 0x00, sizeof(SDL_Event));
|
||||
event.type = SDL_EVENT_MOUSE_MOTION;
|
||||
event.motion.x = 10.0f;
|
||||
event.motion.y = 10.0f;
|
||||
TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
|
||||
"motion over the panel was refused");
|
||||
TEST_ASSERT(e, consumed == false, "motion over a panel was consumed");
|
||||
|
||||
// The wheel goes to the UI only while the pointer is over it. The
|
||||
// motion event above left the pointer on the panel.
|
||||
SDL_memset(&event, 0x00, sizeof(SDL_Event));
|
||||
event.type = SDL_EVENT_MOUSE_WHEEL;
|
||||
event.wheel.y = 1.0f;
|
||||
TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
|
||||
"the wheel over the panel was refused");
|
||||
TEST_ASSERT(e, consumed == true, "the wheel over a panel was not consumed");
|
||||
SDL_memset(&event, 0x00, sizeof(SDL_Event));
|
||||
event.type = SDL_EVENT_MOUSE_MOTION;
|
||||
event.motion.x = 50.0f;
|
||||
event.motion.y = 50.0f;
|
||||
TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
|
||||
"motion off the panel was refused");
|
||||
SDL_memset(&event, 0x00, sizeof(SDL_Event));
|
||||
event.type = SDL_EVENT_MOUSE_WHEEL;
|
||||
event.wheel.y = 1.0f;
|
||||
TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
|
||||
"the wheel beside the panel was refused");
|
||||
TEST_ASSERT(e, consumed == false, "the wheel beside the panel was consumed");
|
||||
|
||||
// A resize reaches the layout engine and still passes through.
|
||||
SDL_memset(&event, 0x00, sizeof(SDL_Event));
|
||||
event.type = SDL_EVENT_WINDOW_RESIZED;
|
||||
event.window.data1 = TEST_TARGET_SIZE;
|
||||
event.window.data2 = TEST_TARGET_SIZE;
|
||||
TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
|
||||
"a resize event was refused");
|
||||
TEST_ASSERT(e, consumed == false, "a resize event was consumed");
|
||||
|
||||
// A key event is not the UI's; keyboard focus is the application's.
|
||||
SDL_memset(&event, 0x00, sizeof(SDL_Event));
|
||||
event.type = SDL_EVENT_KEY_DOWN;
|
||||
event.key.key = SDLK_RETURN;
|
||||
TEST_EXPECT_OK(e, akgl_ui_handle_event(&token, &event, &consumed),
|
||||
"a key event was refused");
|
||||
TEST_ASSERT(e, consumed == false, "a key event was consumed");
|
||||
|
||||
make_button_event(&event, SDL_EVENT_MOUSE_BUTTON_DOWN, 10.0f, 10.0f);
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_ui_handle_event(NULL, &event, &consumed),
|
||||
"a NULL appstate was accepted");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_ui_handle_event(&token, NULL, &consumed),
|
||||
"a NULL event was accepted");
|
||||
TEST_EXPECT_STATUS(e, AKERR_NULLPOINTER, akgl_ui_handle_event(&token, &event, NULL),
|
||||
"a NULL consumed destination was accepted");
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_ui_shutdown());
|
||||
} PROCESS(e) {
|
||||
} FINISH(e, true);
|
||||
SUCCEED_RETURN(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief A layout error inside clay must surface from frame_end, not vanish.
|
||||
*
|
||||
@@ -565,6 +713,7 @@ int main(void)
|
||||
CATCH(errctx, test_ui_frame_bracket());
|
||||
CATCH(errctx, test_ui_layout_pixels());
|
||||
CATCH(errctx, test_ui_layout_text());
|
||||
CATCH(errctx, test_ui_handle_event());
|
||||
CATCH(errctx, test_ui_layout_error_surfaces());
|
||||
} CLEANUP {
|
||||
IGNORE(akgl_text_unloadallfonts());
|
||||
|
||||
Reference in New Issue
Block a user