Files
libakgl/include/akgl/ui.h

546 lines
26 KiB
C
Raw Normal View History

Vendor clay and bring the UI subsystem up: arena, lifecycle, status band clay v0.14 (deps/clay, zlib licence) supplies the layout engine for the new akgl_ui subsystem. Sources-listed rather than add_subdirectory()'d, on the semver/libccd precedent: clay's CMakeLists declares no library target, builds its examples by default, and requires CMake 3.27 against this project's 3.10. src/ui_clay.c is the one CLAY_IMPLEMENTATION translation unit, compiled -w on the vendored-code terms but with clay's symbols deliberately exported -- consumers' CLAY() macros resolve against libakgl.so, and akgl/ui.h warns them never to link a second clay. The subsystem is runtime-optional the way collision is: always compiled in, inert until akgl_ui_init(), which bounds clay to the overridable AKGL_UI_* ceilings, checks Clay_MinMemorySize() against a static BSS arena (no malloc), and refuses with both numbers in the message when it does not fit. Measured: 812544 bytes at the default 1024 elements / 4096 measured words, against a 1 MiB arena. clay's void-callback layout errors are logged as they happen and the first is stashed for the error protocol to raise later. AKGL_ERR_UI joins the status band before AKGL_ERR_LIMIT, named in akgl_error_init. New `ui` test suite covers validation, the init/shutdown lifecycle, and arena exhaustion via the akgl_ui_arena_limit test hook (same contract as akgl_ccd_arena_set_limit). clay.h is installed beside semver.h, its licence beside libccd's, and the headers suite proves akgl/ui.h self-contained -- clay.h compiles clean under -Wall. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 10:53:11 -04:00
/**
* @file ui.h
* @brief Screen-space user interface layouts, built on the clay layout engine.
*
* This subsystem exists because every game eventually wants a dialog box, a
* HUD counter, or a menu, and hand-rolling them out of rectangles and
* akgl_text_rendertextat() is tedious the first time and unmaintainable by the
* fourth (`examples/jrpg/textbox.c` is the honest record of the first time).
* clay computes the layout; libakgl owns everything that has to touch SDL or
* the rest of the library -- the arena clay allocates from, the text
* measurement it calls back into, the render commands it emits, and the
* pointer state it is fed.
*
* There are two ways in, and they compose:
*
* - Write clay's declarative `CLAY({...}) { ... }` blocks yourself between
* akgl_ui_frame_begin() and akgl_ui_frame_end(). The whole of clay's API
* is available -- this header includes `<clay.h>` on purpose, the same way
* SDL types appear undisguised elsewhere in this library.
* - Call the widget helpers, which emit those blocks for you.
*
* The subsystem is optional the way collision is optional: always compiled in,
* inert until akgl_ui_init() runs, and costing nothing but static storage
* before that.
*
* @warning libakgl compiles clay into `libakgl.so` (the `CLAY_IMPLEMENTATION`
* translation unit is `src/ui_clay.c`) and exports its symbols,
* because the `CLAY()` macros in *your* translation units expand to
* calls into them. Do not define `CLAY_IMPLEMENTATION` yourself and
* do not link another copy of clay: two definitions of the same
* symbols and the loader picks one silently, which is the exact
* defect AGENTS.md records against an exported `renderer`.
*/
#ifndef _AKGL_UI_H_
#define _AKGL_UI_H_
#include <stddef.h>
#include <stdint.h>
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
2026-08-02 11:10:35 -04:00
#include <SDL3/SDL.h>
Vendor clay and bring the UI subsystem up: arena, lifecycle, status band clay v0.14 (deps/clay, zlib licence) supplies the layout engine for the new akgl_ui subsystem. Sources-listed rather than add_subdirectory()'d, on the semver/libccd precedent: clay's CMakeLists declares no library target, builds its examples by default, and requires CMake 3.27 against this project's 3.10. src/ui_clay.c is the one CLAY_IMPLEMENTATION translation unit, compiled -w on the vendored-code terms but with clay's symbols deliberately exported -- consumers' CLAY() macros resolve against libakgl.so, and akgl/ui.h warns them never to link a second clay. The subsystem is runtime-optional the way collision is: always compiled in, inert until akgl_ui_init(), which bounds clay to the overridable AKGL_UI_* ceilings, checks Clay_MinMemorySize() against a static BSS arena (no malloc), and refuses with both numbers in the message when it does not fit. Measured: 812544 bytes at the default 1024 elements / 4096 measured words, against a 1 MiB arena. clay's void-callback layout errors are logged as they happen and the first is stashed for the error protocol to raise later. AKGL_ERR_UI joins the status band before AKGL_ERR_LIMIT, named in akgl_error_init. New `ui` test suite covers validation, the init/shutdown lifecycle, and arena exhaustion via the akgl_ui_arena_limit test hook (same contract as akgl_ccd_arena_set_limit). clay.h is installed beside semver.h, its licence beside libccd's, and the headers suite proves akgl/ui.h self-contained -- clay.h compiles clean under -Wall. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 10:53:11 -04:00
#include <akerror.h>
#include <clay.h>
Render clay layouts: fonts, text measurement, executor and the frame bracket The UI subsystem now draws. akgl_ui_frame_begin/frame_end bracket one frame's CLAY() declarations: begin clears the error stash and starts the clay layout, end computes it and walks the render commands through a backend -- RECTANGLE as (rounded) fills, BORDER as radius-shortened edge fills plus corner arcs, TEXT through akgl_text_rendertextat one wrapped line per command, IMAGE as an akgl_Sprite's first frame stretched to the bounding box, and the SCISSOR pair through akgl_draw_set_clip, cleared again on any exit so a failed frame cannot leave the world clipped. akgl_ui_font_register maps registry font names onto clay's uint16_t fontIds. The table keeps the *name* and resolves it per use -- fonts are not reference counted, and a cached TTF_Font* would dangle where a name reports "gone" honestly. Clay_TextElementConfig.fontSize is deliberately ignored: libakgl bakes the size into the handle at load, so one face at two sizes is two ids. The measure bridge passes clay's non-NUL-terminated slices straight to SDL_ttf's explicit-length TTF_GetStringSize -- no copy, no scratch. Its signature has no error channel, so failures report zero-by-zero and stash a message that frame_end raises, the same route clay's own void error handler uses; layout errors take precedence over drawing and one bad frame leaves the next one clean. Tests drive real CLAY() layouts through the software renderer and read pixels back: fill placement, border edges with an empty middle, a child clipped by its container, text landing in its colour, slice-vs-whole measurement agreement, fontId table dedupe/refusal/exhaustion, and the bracket's begin/begin, end-without-begin and failure-recovery contracts. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 11:06:27 -04:00
#include <akgl/renderer.h>
Add the widget helpers: dialog, HUD label, and menu The other half of the hybrid decision: an application that wants a dialog box, a score counter or a main menu gets each in one call, without touching a CLAY() macro -- and an application that outgrows them still has clay's whole DSL, because the widgets are nothing but functions that declare the same elements between the same frame bracket. akgl_ui_dialog is the JRPG textbox in one line: bottom panel spanning the layout, AKGL_UI_DIALOG_HEIGHT tall, one-pixel border, wrapped text. The NULL style *is* the textbox palette -- near-black fill, parchment edge and ink, 8px padding -- deliberately, so the widget and the 125-line hand-rolled panel it stands beside in the comparison chapter produce the same picture. Showing and hiding is the frame's business: call it while somebody is talking, don't while nobody is; a declarative frame is the visible flag. akgl_ui_label pins a fit-sized text chip to a corner or the centre, inset by the style's padding. akgl_ui_menu declares a centred vertical menu from a caller-owned struct (no heap layer: it owns no texture, no font, no registry entry), with the selected row drawn inverted, hover moving the selection and the frame-latched press edge activating it. akgl_ui_menu_handle_event drives the same struct from Up/Down/Return and D-pad/South with wrapping, slotting into the event chain after akgl_ui_handle_event; routing events to a menu is the application's focus model, stated rather than invented. Tests cover the dialog's geometry and palette by pixel, label anchoring by quadrant, menu validation, the inverted highlight, keyboard and gamepad wrap/activate/pass-through, and the full two-frame mouse protocol -- click aimed at the second row's real box via Clay_GetElementData, seen by the next frame's declaration as selection plus activation. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 11:17:32 -04:00
#include <akgl/types.h>
Render clay layouts: fonts, text measurement, executor and the frame bracket The UI subsystem now draws. akgl_ui_frame_begin/frame_end bracket one frame's CLAY() declarations: begin clears the error stash and starts the clay layout, end computes it and walks the render commands through a backend -- RECTANGLE as (rounded) fills, BORDER as radius-shortened edge fills plus corner arcs, TEXT through akgl_text_rendertextat one wrapped line per command, IMAGE as an akgl_Sprite's first frame stretched to the bounding box, and the SCISSOR pair through akgl_draw_set_clip, cleared again on any exit so a failed frame cannot leave the world clipped. akgl_ui_font_register maps registry font names onto clay's uint16_t fontIds. The table keeps the *name* and resolves it per use -- fonts are not reference counted, and a cached TTF_Font* would dangle where a name reports "gone" honestly. Clay_TextElementConfig.fontSize is deliberately ignored: libakgl bakes the size into the handle at load, so one face at two sizes is two ids. The measure bridge passes clay's non-NUL-terminated slices straight to SDL_ttf's explicit-length TTF_GetStringSize -- no copy, no scratch. Its signature has no error channel, so failures report zero-by-zero and stash a message that frame_end raises, the same route clay's own void error handler uses; layout errors take precedence over drawing and one bad frame leaves the next one clean. Tests drive real CLAY() layouts through the software renderer and read pixels back: fill placement, border edges with an empty middle, a child clipped by its container, text landing in its colour, slice-vs-whole measurement agreement, fontId table dedupe/refusal/exhaustion, and the bracket's begin/begin, end-without-begin and failure-recovery contracts. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 11:06:27 -04:00
Vendor clay and bring the UI subsystem up: arena, lifecycle, status band clay v0.14 (deps/clay, zlib licence) supplies the layout engine for the new akgl_ui subsystem. Sources-listed rather than add_subdirectory()'d, on the semver/libccd precedent: clay's CMakeLists declares no library target, builds its examples by default, and requires CMake 3.27 against this project's 3.10. src/ui_clay.c is the one CLAY_IMPLEMENTATION translation unit, compiled -w on the vendored-code terms but with clay's symbols deliberately exported -- consumers' CLAY() macros resolve against libakgl.so, and akgl/ui.h warns them never to link a second clay. The subsystem is runtime-optional the way collision is: always compiled in, inert until akgl_ui_init(), which bounds clay to the overridable AKGL_UI_* ceilings, checks Clay_MinMemorySize() against a static BSS arena (no malloc), and refuses with both numbers in the message when it does not fit. Measured: 812544 bytes at the default 1024 elements / 4096 measured words, against a 1 MiB arena. clay's void-callback layout errors are logged as they happen and the first is stashed for the error protocol to raise later. AKGL_ERR_UI joins the status band before AKGL_ERR_LIMIT, named in akgl_error_init. New `ui` test suite covers validation, the init/shutdown lifecycle, and arena exhaustion via the akgl_ui_arena_limit test hook (same contract as akgl_ccd_arena_set_limit). clay.h is installed beside semver.h, its licence beside libccd's, and the headers suite proves akgl/ui.h self-contained -- clay.h compiles clean under -Wall. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 10:53:11 -04:00
/**
* @brief Most elements one layout may declare.
*
* Passed to `Clay_SetMaxElementCount` before the arena is sized, so it is a
* compile-time ceiling in the same sense as the `AKGL_MAX_HEAP_*` family:
* define it before including this header to raise it, and see
* #AKGL_UI_ARENA_BYTES, which must grow with it.
*/
#ifndef AKGL_UI_MAX_ELEMENTS
#define AKGL_UI_MAX_ELEMENTS 1024
#endif
/**
* @brief Most whitespace-separated words clay's text measurement cache holds.
*
* Clay measures text one word at a time and caches the result; a cache miss
* costs one call back into SDL_ttf. This bounds the cache, not the text -- a
* layout with more distinct words than this still renders, it just measures
* some of them every frame and clay reports the overflow through the error
* handler.
*/
#ifndef AKGL_UI_MAX_MEASURE_WORDS
#define AKGL_UI_MAX_MEASURE_WORDS 4096
#endif
/**
* @brief Bytes of static storage clay lays its internal structures out in.
*
* The arena is a fixed array in `src/ui.c` -- libakgl does not call `malloc`
* at runtime, and clay never allocates behind the arena's back. What clay
* needs is a function of #AKGL_UI_MAX_ELEMENTS and #AKGL_UI_MAX_MEASURE_WORDS
* and is only computable at runtime, so akgl_ui_init() checks
* `Clay_MinMemorySize()` against this and refuses with both numbers in the
* message when it does not fit. A too-small arena is a loud failure at
* startup, never a corruption at frame forty thousand.
*/
#ifndef AKGL_UI_ARENA_BYTES
#define AKGL_UI_ARENA_BYTES (1024 * 1024)
#endif
Render clay layouts: fonts, text measurement, executor and the frame bracket The UI subsystem now draws. akgl_ui_frame_begin/frame_end bracket one frame's CLAY() declarations: begin clears the error stash and starts the clay layout, end computes it and walks the render commands through a backend -- RECTANGLE as (rounded) fills, BORDER as radius-shortened edge fills plus corner arcs, TEXT through akgl_text_rendertextat one wrapped line per command, IMAGE as an akgl_Sprite's first frame stretched to the bounding box, and the SCISSOR pair through akgl_draw_set_clip, cleared again on any exit so a failed frame cannot leave the world clipped. akgl_ui_font_register maps registry font names onto clay's uint16_t fontIds. The table keeps the *name* and resolves it per use -- fonts are not reference counted, and a cached TTF_Font* would dangle where a name reports "gone" honestly. Clay_TextElementConfig.fontSize is deliberately ignored: libakgl bakes the size into the handle at load, so one face at two sizes is two ids. The measure bridge passes clay's non-NUL-terminated slices straight to SDL_ttf's explicit-length TTF_GetStringSize -- no copy, no scratch. Its signature has no error channel, so failures report zero-by-zero and stash a message that frame_end raises, the same route clay's own void error handler uses; layout errors take precedence over drawing and one bad frame leaves the next one clean. Tests drive real CLAY() layouts through the software renderer and read pixels back: fill placement, border edges with an empty middle, a child clipped by its container, text landing in its colour, slice-vs-whole measurement agreement, fontId table dedupe/refusal/exhaustion, and the bracket's begin/begin, end-without-begin and failure-recovery contracts. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 11:06:27 -04:00
/**
* @brief Most fonts akgl_ui_font_register() will map to clay fontIds.
*
* clay identifies a font as a `uint16_t`; libakgl identifies one as a name in
* #AKGL_REGISTRY_FONT. The table joining them is this many fixed slots. A
* size is baked into each registered font handle, so a game using one face at
* three sizes is using three slots.
*/
#ifndef AKGL_UI_MAX_FONTS
#define AKGL_UI_MAX_FONTS 8
#endif
/**
* @brief Bytes each fontId table slot holds for its registry name, terminator included.
*/
#ifndef AKGL_UI_FONT_NAME_LENGTH
#define AKGL_UI_FONT_NAME_LENGTH 64
#endif
/**
* @brief Bytes of scratch one text render command may occupy, terminator included.
*
* clay hands text to a renderer as a length-and-pointer slice, one command
* per wrapped line; akgl_text_rendertextat() takes a C string, so each line
* is copied through a bounded buffer on the way. A line longer than this
* fails the frame loudly rather than drawing a truncation that reads as the
* author's text.
*/
#ifndef AKGL_UI_MAX_TEXT_BYTES
#define AKGL_UI_MAX_TEXT_BYTES 1024
#endif
Add the widget helpers: dialog, HUD label, and menu The other half of the hybrid decision: an application that wants a dialog box, a score counter or a main menu gets each in one call, without touching a CLAY() macro -- and an application that outgrows them still has clay's whole DSL, because the widgets are nothing but functions that declare the same elements between the same frame bracket. akgl_ui_dialog is the JRPG textbox in one line: bottom panel spanning the layout, AKGL_UI_DIALOG_HEIGHT tall, one-pixel border, wrapped text. The NULL style *is* the textbox palette -- near-black fill, parchment edge and ink, 8px padding -- deliberately, so the widget and the 125-line hand-rolled panel it stands beside in the comparison chapter produce the same picture. Showing and hiding is the frame's business: call it while somebody is talking, don't while nobody is; a declarative frame is the visible flag. akgl_ui_label pins a fit-sized text chip to a corner or the centre, inset by the style's padding. akgl_ui_menu declares a centred vertical menu from a caller-owned struct (no heap layer: it owns no texture, no font, no registry entry), with the selected row drawn inverted, hover moving the selection and the frame-latched press edge activating it. akgl_ui_menu_handle_event drives the same struct from Up/Down/Return and D-pad/South with wrapping, slotting into the event chain after akgl_ui_handle_event; routing events to a menu is the application's focus model, stated rather than invented. Tests cover the dialog's geometry and palette by pixel, label anchoring by quadrant, menu validation, the inverted highlight, keyboard and gamepad wrap/activate/pass-through, and the full two-frame mouse protocol -- click aimed at the second row's real box via Clay_GetElementData, seen by the next frame's declaration as selection plus activation. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 11:17:32 -04:00
/**
* @brief Most entries one akgl_UiMenu may carry.
*
* A menu deeper than this wants scrolling and sub-screens, which is `CLAY()`
* territory rather than a widget's.
*/
#ifndef AKGL_UI_MENU_MAX_ITEMS
#define AKGL_UI_MENU_MAX_ITEMS 16
#endif
/**
* @brief Height of the akgl_ui_dialog() panel, in pixels.
*
* Two lines of 12pt monospace plus padding -- the JRPG textbox's height, kept
* so the widget's output is comparable with the hand-rolled panel it
* replaces.
*/
#ifndef AKGL_UI_DIALOG_HEIGHT
#define AKGL_UI_DIALOG_HEIGHT 56
#endif
/**
* @brief One look, shared by every widget: colours, spacing, corners, font.
*
* Passing `NULL` wherever a style is taken means the library default, which
* is deliberately the JRPG textbox's palette -- near-black fill (24,20,37,235)
* with parchment edge and ink (240,236,214,255), 8 pixels of padding, square
* corners, fontId 0. A caller wanting one change copies those values and
* changes one; there is no per-field "zero means default" magic, because a
* transparent fill and a zero radius are things a style legitimately says.
*/
typedef struct {
SDL_Color fill; /**< Panel background. */
SDL_Color edge; /**< Border colour. Drawn one pixel wide where a widget has a border. */
SDL_Color ink; /**< Text colour. */
float32_t padding; /**< Pixels between a panel's edge and its content, and between a widget and the screen edge it anchors to. */
float32_t corner_radius; /**< Corner rounding in pixels. 0 is square. */
uint16_t fontid; /**< The clay fontId text renders with, from akgl_ui_font_register(). */
} akgl_UiStyle;
/**
* @brief Where on the screen akgl_ui_label() pins itself.
*/
typedef enum {
AKGL_UI_ANCHOR_TOP_LEFT, /**< Inset by the style's padding from the top-left corner. */
AKGL_UI_ANCHOR_TOP_RIGHT, /**< Inset from the top-right corner. */
AKGL_UI_ANCHOR_BOTTOM_LEFT, /**< Inset from the bottom-left corner. */
AKGL_UI_ANCHOR_BOTTOM_RIGHT, /**< Inset from the bottom-right corner. */
AKGL_UI_ANCHOR_CENTER /**< Dead centre, no inset. */
} akgl_UiAnchor;
/**
* @brief One vertical menu: its entries, its selection, and whether it fired.
*
* Caller-owned, the way the JRPG's textbox state is a static in the game --
* there is no heap layer behind this because it owns no texture, no font and
* no registry entry; it is a struct of borrowed pointers and two integers.
* Declare it static, fill in `id`, `items` and `count`, and leave the rest
* zeroed.
*
* `selected` is yours to read and the menu's to move: keyboard and gamepad
* move it through akgl_ui_menu_handle_event(), and the mouse moves it by
* hovering while akgl_ui_menu() declares the rows. `activated` latches true
* on Return, gamepad South, or a click on a row; the caller acts on it and
* sets it back to false -- the menu never clears it, so an unread activation
* is not lost between frames.
*/
typedef struct {
char *id; /**< Element id prefix, unique per menu. Required. Borrowed. */
char *items[AKGL_UI_MENU_MAX_ITEMS]; /**< The entries, top to bottom. Borrowed; they must outlive the frame. */
int32_t count; /**< How many of @c items are set. 1 to #AKGL_UI_MENU_MAX_ITEMS. */
int32_t selected; /**< Index of the highlighted entry. In and out. */
bool activated; /**< Out: the selected entry was confirmed. Caller clears. */
akgl_UiStyle *style; /**< Look to draw with, or `NULL` for the library default. */
} akgl_UiMenu;
Vendor clay and bring the UI subsystem up: arena, lifecycle, status band clay v0.14 (deps/clay, zlib licence) supplies the layout engine for the new akgl_ui subsystem. Sources-listed rather than add_subdirectory()'d, on the semver/libccd precedent: clay's CMakeLists declares no library target, builds its examples by default, and requires CMake 3.27 against this project's 3.10. src/ui_clay.c is the one CLAY_IMPLEMENTATION translation unit, compiled -w on the vendored-code terms but with clay's symbols deliberately exported -- consumers' CLAY() macros resolve against libakgl.so, and akgl/ui.h warns them never to link a second clay. The subsystem is runtime-optional the way collision is: always compiled in, inert until akgl_ui_init(), which bounds clay to the overridable AKGL_UI_* ceilings, checks Clay_MinMemorySize() against a static BSS arena (no malloc), and refuses with both numbers in the message when it does not fit. Measured: 812544 bytes at the default 1024 elements / 4096 measured words, against a 1 MiB arena. clay's void-callback layout errors are logged as they happen and the first is stashed for the error protocol to raise later. AKGL_ERR_UI joins the status band before AKGL_ERR_LIMIT, named in akgl_error_init. New `ui` test suite covers validation, the init/shutdown lifecycle, and arena exhaustion via the akgl_ui_arena_limit test hook (same contract as akgl_ccd_arena_set_limit). clay.h is installed beside semver.h, its licence beside libccd's, and the headers suite proves akgl/ui.h self-contained -- clay.h compiles clean under -Wall. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 10:53:11 -04:00
/**
* @brief Bring the UI subsystem up.
*
* Bounds clay to the `AKGL_UI_*` ceilings, checks that the arena is big
* enough for them, and hands it to `Clay_Initialize` with an error handler
* that routes clay's layout-time complaints into the libakgl error protocol
* (they surface from akgl_ui_frame_end(), which is the first call after they
* can happen).
*
* The layout dimensions are taken as parameters rather than read from the
* camera or the window, so a headless program -- a test, a layout tool -- can
* bring the UI up without a renderer existing at all. A windowed game passes
* its screen size and lets akgl_ui_handle_event() track resizes from there.
*
* @param width Layout width in pixels. Must be positive.
* @param height Layout height in pixels. Must be positive.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_OUTOFBOUNDS If @p width or @p height is not positive.
* @throws AKGL_ERR_UI If the subsystem is already initialized -- call
* akgl_ui_shutdown() first; if the arena cannot hold clay's
* structures at the current ceilings -- the message carries the bytes
* required and the bytes available, so the operator knows what to
* raise #AKGL_UI_ARENA_BYTES to; or if `Clay_Initialize` refuses,
* which the preceding checks should make unreachable.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_init(int width, int height);
/**
* @brief Return the UI subsystem to the inert state.
*
* clay has no teardown of its own -- everything it built lives in the arena,
* and the next akgl_ui_init() lays a new context out over it. This forgets
* the context and clears the stashed error state, and is idempotent, because
* shutdown paths run after partial startups.
*
* @warning Anything that captured clay state -- and any `CLAY()` block that
* runs after this -- is left talking to a context this subsystem has
* disowned. Stop declaring layouts before shutting down, the same
* way fonts stop being drawn before akgl_text_unloadallfonts().
*
* @return `NULL`. Shutting down an uninitialized subsystem is success.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_shutdown(void);
/**
* @brief Tell the layout engine the render target changed size.
*
* akgl_ui_handle_event() calls this for `SDL_EVENT_WINDOW_RESIZED`, so a game
* that routes its events through there never calls it directly. It is public
* for the host that owns its own event loop.
*
* @param width New layout width in pixels. Must be positive.
* @param height New layout height in pixels. Must be positive.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_UI If the subsystem is not initialized.
* @throws AKERR_OUTOFBOUNDS If @p width or @p height is not positive.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_resize(int width, int height);
Render clay layouts: fonts, text measurement, executor and the frame bracket The UI subsystem now draws. akgl_ui_frame_begin/frame_end bracket one frame's CLAY() declarations: begin clears the error stash and starts the clay layout, end computes it and walks the render commands through a backend -- RECTANGLE as (rounded) fills, BORDER as radius-shortened edge fills plus corner arcs, TEXT through akgl_text_rendertextat one wrapped line per command, IMAGE as an akgl_Sprite's first frame stretched to the bounding box, and the SCISSOR pair through akgl_draw_set_clip, cleared again on any exit so a failed frame cannot leave the world clipped. akgl_ui_font_register maps registry font names onto clay's uint16_t fontIds. The table keeps the *name* and resolves it per use -- fonts are not reference counted, and a cached TTF_Font* would dangle where a name reports "gone" honestly. Clay_TextElementConfig.fontSize is deliberately ignored: libakgl bakes the size into the handle at load, so one face at two sizes is two ids. The measure bridge passes clay's non-NUL-terminated slices straight to SDL_ttf's explicit-length TTF_GetStringSize -- no copy, no scratch. Its signature has no error channel, so failures report zero-by-zero and stash a message that frame_end raises, the same route clay's own void error handler uses; layout errors take precedence over drawing and one bad frame leaves the next one clean. Tests drive real CLAY() layouts through the software renderer and read pixels back: fill placement, border edges with an empty middle, a child clipped by its container, text landing in its colour, slice-vs-whole measurement agreement, fontId table dedupe/refusal/exhaustion, and the bracket's begin/begin, end-without-begin and failure-recovery contracts. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 11:06:27 -04:00
/**
* @brief Map a font-registry name to the clay fontId that text elements name it by.
*
* The table stores the *name* and resolves it through #AKGL_REGISTRY_FONT on
* every use, rather than caching the `TTF_Font *` -- fonts are not reference
* counted, so a cached handle would dangle the moment akgl_text_unloadfont()
* took the font away, while a name honestly reports "no longer registered".
* Registering a name that is already in the table returns its existing id
* rather than spending a second slot.
*
* clay's `Clay_TextElementConfig.fontSize` is **ignored** by this subsystem:
* a libakgl font bakes its size in at akgl_text_loadfont() time, so the size
* a text element renders at is the size the font behind its fontId was loaded
* at. One face at two sizes is two loads, two registrations, two ids.
*
* @param name Registry key the font was published under. Required, and it
* must already be in the registry -- registering first and
* loading later would leave a window where a fontId resolves to
* nothing.
* @param fontid Receives the id to put in `Clay_TextElementConfig.fontId`.
* Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p name or @p fontid is `NULL`.
* @throws AKGL_ERR_UI If the subsystem is not initialized; if no font is
* registered under @p name; or if all #AKGL_UI_MAX_FONTS slots are
* taken. Each message says which.
* @throws AKERR_OUTOFBOUNDS If @p name does not fit in a table slot --
* #AKGL_UI_FONT_NAME_LENGTH bytes including the terminator.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_font_register(char *name, uint16_t *fontid);
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
2026-08-02 11:10:35 -04:00
/**
* @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);
Render clay layouts: fonts, text measurement, executor and the frame bracket The UI subsystem now draws. akgl_ui_frame_begin/frame_end bracket one frame's CLAY() declarations: begin clears the error stash and starts the clay layout, end computes it and walks the render commands through a backend -- RECTANGLE as (rounded) fills, BORDER as radius-shortened edge fills plus corner arcs, TEXT through akgl_text_rendertextat one wrapped line per command, IMAGE as an akgl_Sprite's first frame stretched to the bounding box, and the SCISSOR pair through akgl_draw_set_clip, cleared again on any exit so a failed frame cannot leave the world clipped. akgl_ui_font_register maps registry font names onto clay's uint16_t fontIds. The table keeps the *name* and resolves it per use -- fonts are not reference counted, and a cached TTF_Font* would dangle where a name reports "gone" honestly. Clay_TextElementConfig.fontSize is deliberately ignored: libakgl bakes the size into the handle at load, so one face at two sizes is two ids. The measure bridge passes clay's non-NUL-terminated slices straight to SDL_ttf's explicit-length TTF_GetStringSize -- no copy, no scratch. Its signature has no error channel, so failures report zero-by-zero and stash a message that frame_end raises, the same route clay's own void error handler uses; layout errors take precedence over drawing and one bad frame leaves the next one clean. Tests drive real CLAY() layouts through the software renderer and read pixels back: fill placement, border edges with an empty middle, a child clipped by its container, text landing in its colour, slice-vs-whole measurement agreement, fontId table dedupe/refusal/exhaustion, and the bracket's begin/begin, end-without-begin and failure-recovery contracts. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 11:06:27 -04:00
/**
* @brief Open the UI frame: clear the error stash and begin the clay layout.
*
* Call once per rendered frame, after akgl_game_update() and before any
* `CLAY()` block or widget helper. Everything declared between this and
* akgl_ui_frame_end() is this frame's interface; a dialog is "open" because
* the frame declares it, not because a mode was toggled somewhere.
*
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKGL_ERR_UI If the subsystem is not initialized, or if a frame is
* already open -- a begin/begin sequence means a frame_end went
* missing, and absorbing it would hide the layout of one frame inside
* another.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_frame_begin(void);
/**
* @brief Close the UI frame: compute the layout and draw it.
*
* Runs `Clay_EndLayout` and walks the render commands it produces through
* @p self -- fills and rounded fills, borders, clip rectangles, text through
* akgl_text_rendertextat(), images through the backend's `draw_texture`.
* Draws in screen coordinates on whatever is already on the target, so call
* it between akgl_game_update() and the backend's `frame_end`: the UI lands
* on top of the world.
*
* The frame is considered closed even when this fails: the next
* akgl_ui_frame_begin() is legal, so one bad frame is one bad frame rather
* than a wedged subsystem.
*
* If clay reported errors during layout -- through its handler, or the
* measure callback failing -- this raises the first of them here and does not
* draw, because a layout that failed is not a layout, and the raise names how
* many more followed it.
*
* @param self The backend to draw through. Required, along with its
* `sdl_renderer` and `draw_texture`.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self, its `sdl_renderer`, or its
* `draw_texture` is `NULL`.
* @throws AKGL_ERR_UI If the subsystem is not initialized; if no frame is
* open; if clay reported layout errors; if a text command's line
* exceeds #AKGL_UI_MAX_TEXT_BYTES; or if a fontId cannot be resolved.
* @throws AKERR_* Whatever the drawing primitives underneath raise.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_frame_end(akgl_RenderBackend *self);
Add the widget helpers: dialog, HUD label, and menu The other half of the hybrid decision: an application that wants a dialog box, a score counter or a main menu gets each in one call, without touching a CLAY() macro -- and an application that outgrows them still has clay's whole DSL, because the widgets are nothing but functions that declare the same elements between the same frame bracket. akgl_ui_dialog is the JRPG textbox in one line: bottom panel spanning the layout, AKGL_UI_DIALOG_HEIGHT tall, one-pixel border, wrapped text. The NULL style *is* the textbox palette -- near-black fill, parchment edge and ink, 8px padding -- deliberately, so the widget and the 125-line hand-rolled panel it stands beside in the comparison chapter produce the same picture. Showing and hiding is the frame's business: call it while somebody is talking, don't while nobody is; a declarative frame is the visible flag. akgl_ui_label pins a fit-sized text chip to a corner or the centre, inset by the style's padding. akgl_ui_menu declares a centred vertical menu from a caller-owned struct (no heap layer: it owns no texture, no font, no registry entry), with the selected row drawn inverted, hover moving the selection and the frame-latched press edge activating it. akgl_ui_menu_handle_event drives the same struct from Up/Down/Return and D-pad/South with wrapping, slotting into the event chain after akgl_ui_handle_event; routing events to a menu is the application's focus model, stated rather than invented. Tests cover the dialog's geometry and palette by pixel, label anchoring by quadrant, menu validation, the inverted highlight, keyboard and gamepad wrap/activate/pass-through, and the full two-frame mouse protocol -- click aimed at the second row's real box via Clay_GetElementData, seen by the next frame's declaration as selection plus activation. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 11:17:32 -04:00
/**
* @brief Declare a dialog panel: the JRPG textbox in one call.
*
* A panel across the bottom of the screen, inset by the style's padding,
* #AKGL_UI_DIALOG_HEIGHT pixels tall, with a one-pixel border and the text
* wrapped inside it. Legal only between akgl_ui_frame_begin() and
* akgl_ui_frame_end(). Showing and hiding is the frame's business: call this
* while somebody is talking and don't while nobody is -- there is no
* `visible` flag, because a declarative frame *is* the flag.
*
* @param id Element id for the panel, unique within the frame. Required.
* Borrowed until frame_end.
* @param text What the dialog says. Required, and borrowed until frame_end
* -- clay keeps the pointer, not a copy. May be empty, which
* draws an empty panel.
* @param style Look to draw with, or `NULL` for the textbox palette.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p id or @p text is `NULL`.
* @throws AKGL_ERR_UI If the subsystem is not initialized or no frame is
* open.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_dialog(char *id, char *text, akgl_UiStyle *style);
/**
* @brief Declare a HUD label: one line of text on a padded panel, pinned to a corner.
*
* Fit-sized around its text, anchored per @p anchor and inset by the style's
* padding. The score counter and the lives counter are two calls to this with
* two ids. Legal only between akgl_ui_frame_begin() and akgl_ui_frame_end().
*
* The text is borrowed until frame_end, which matters for the obvious use:
* format the score into a buffer that outlives the frame bracket, not into a
* scope that closes before the draw.
*
* @param id Element id for the label, unique within the frame. Required.
* @param text What the label says. Required; empty draws an empty chip.
* @param anchor Which corner (or the centre) to pin to.
* @param style Look to draw with, or `NULL` for the library default.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p id or @p text is `NULL`.
* @throws AKGL_ERR_UI If the subsystem is not initialized or no frame is
* open.
* @throws AKERR_OUTOFBOUNDS If @p anchor is not one of the enum's values.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_label(char *id, char *text, akgl_UiAnchor anchor, akgl_UiStyle *style);
/**
* @brief Declare a vertical menu from caller-owned state, centred on screen.
*
* One row per entry, the selected row drawn inverted (ink on fill swaps to
* fill on ink). Moving the mouse onto a row selects it; a left click on a
* row selects and sets `activated`. A *stationary* pointer claims nothing --
* parking the mouse over the menu must not pin the selection against the
* keyboard, which is fighting sixty times a second and losing. Keyboard and
* gamepad go through
Add the widget helpers: dialog, HUD label, and menu The other half of the hybrid decision: an application that wants a dialog box, a score counter or a main menu gets each in one call, without touching a CLAY() macro -- and an application that outgrows them still has clay's whole DSL, because the widgets are nothing but functions that declare the same elements between the same frame bracket. akgl_ui_dialog is the JRPG textbox in one line: bottom panel spanning the layout, AKGL_UI_DIALOG_HEIGHT tall, one-pixel border, wrapped text. The NULL style *is* the textbox palette -- near-black fill, parchment edge and ink, 8px padding -- deliberately, so the widget and the 125-line hand-rolled panel it stands beside in the comparison chapter produce the same picture. Showing and hiding is the frame's business: call it while somebody is talking, don't while nobody is; a declarative frame is the visible flag. akgl_ui_label pins a fit-sized text chip to a corner or the centre, inset by the style's padding. akgl_ui_menu declares a centred vertical menu from a caller-owned struct (no heap layer: it owns no texture, no font, no registry entry), with the selected row drawn inverted, hover moving the selection and the frame-latched press edge activating it. akgl_ui_menu_handle_event drives the same struct from Up/Down/Return and D-pad/South with wrapping, slotting into the event chain after akgl_ui_handle_event; routing events to a menu is the application's focus model, stated rather than invented. Tests cover the dialog's geometry and palette by pixel, label anchoring by quadrant, menu validation, the inverted highlight, keyboard and gamepad wrap/activate/pass-through, and the full two-frame mouse protocol -- click aimed at the second row's real box via Clay_GetElementData, seen by the next frame's declaration as selection plus activation. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 11:17:32 -04:00
* akgl_ui_menu_handle_event(), which the application calls for whichever
* menu currently has its attention -- that routing *is* the focus model.
* Legal only between akgl_ui_frame_begin() and akgl_ui_frame_end().
*
* @param menu The menu state. Required, with a non-`NULL` `id`, a `count` in
* range, every used entry non-`NULL`, and `selected` pointing at
* an entry.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p menu, its `id`, or any of its first
* `count` items is `NULL`. The message names the index.
* @throws AKGL_ERR_UI If the subsystem is not initialized or no frame is
* open.
* @throws AKERR_OUTOFBOUNDS If `count` or `selected` is out of range.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_menu(akgl_UiMenu *menu);
/**
* @brief Drive a menu from a keyboard or gamepad event.
*
* Up and Down arrows and the D-pad move the selection, wrapping at both
* ends; Return and the gamepad South button set `activated`. Anything else
* -- and anything while the subsystem is inert -- reports @p consumed false
* and succeeds, so it slots into the same pass-everything event chain as
* akgl_ui_handle_event(), after it and before the control maps:
*
* PASS(errctx, akgl_ui_handle_event(&app, &event, &consumed));
* if ( consumed ) { continue; }
* if ( menu_active ) {
* PASS(errctx, akgl_ui_menu_handle_event(&menu, &event, &consumed));
* if ( consumed ) { continue; }
* }
* PASS(errctx, akgl_controller_handle_event(&app, &event));
*
* @param menu The menu the application is routing input to. Required,
* with `count` in range.
* @param event The event SDL handed the loop. Required.
* @param consumed Receives whether the menu claimed the event. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p menu, @p event, or @p consumed is `NULL`.
* @throws AKERR_OUTOFBOUNDS If `count` or `selected` is out of range.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_menu_handle_event(akgl_UiMenu *menu, SDL_Event *event, bool *consumed);
Vendor clay and bring the UI subsystem up: arena, lifecycle, status band clay v0.14 (deps/clay, zlib licence) supplies the layout engine for the new akgl_ui subsystem. Sources-listed rather than add_subdirectory()'d, on the semver/libccd precedent: clay's CMakeLists declares no library target, builds its examples by default, and requires CMake 3.27 against this project's 3.10. src/ui_clay.c is the one CLAY_IMPLEMENTATION translation unit, compiled -w on the vendored-code terms but with clay's symbols deliberately exported -- consumers' CLAY() macros resolve against libakgl.so, and akgl/ui.h warns them never to link a second clay. The subsystem is runtime-optional the way collision is: always compiled in, inert until akgl_ui_init(), which bounds clay to the overridable AKGL_UI_* ceilings, checks Clay_MinMemorySize() against a static BSS arena (no malloc), and refuses with both numbers in the message when it does not fit. Measured: 812544 bytes at the default 1024 elements / 4096 measured words, against a 1 MiB arena. clay's void-callback layout errors are logged as they happen and the first is stashed for the error protocol to raise later. AKGL_ERR_UI joins the status band before AKGL_ERR_LIMIT, named in akgl_error_init. New `ui` test suite covers validation, the init/shutdown lifecycle, and arena exhaustion via the akgl_ui_arena_limit test hook (same contract as akgl_ccd_arena_set_limit). clay.h is installed beside semver.h, its licence beside libccd's, and the headers suite proves akgl/ui.h self-contained -- clay.h compiles clean under -Wall. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 10:53:11 -04:00
/*
* The following is part of the internal API. It is exposed so the test suite
* can reach it and is not meant to be called by a game.
*/
Render clay layouts: fonts, text measurement, executor and the frame bracket The UI subsystem now draws. akgl_ui_frame_begin/frame_end bracket one frame's CLAY() declarations: begin clears the error stash and starts the clay layout, end computes it and walks the render commands through a backend -- RECTANGLE as (rounded) fills, BORDER as radius-shortened edge fills plus corner arcs, TEXT through akgl_text_rendertextat one wrapped line per command, IMAGE as an akgl_Sprite's first frame stretched to the bounding box, and the SCISSOR pair through akgl_draw_set_clip, cleared again on any exit so a failed frame cannot leave the world clipped. akgl_ui_font_register maps registry font names onto clay's uint16_t fontIds. The table keeps the *name* and resolves it per use -- fonts are not reference counted, and a cached TTF_Font* would dangle where a name reports "gone" honestly. Clay_TextElementConfig.fontSize is deliberately ignored: libakgl bakes the size into the handle at load, so one face at two sizes is two ids. The measure bridge passes clay's non-NUL-terminated slices straight to SDL_ttf's explicit-length TTF_GetStringSize -- no copy, no scratch. Its signature has no error channel, so failures report zero-by-zero and stash a message that frame_end raises, the same route clay's own void error handler uses; layout errors take precedence over drawing and one bad frame leaves the next one clean. Tests drive real CLAY() layouts through the software renderer and read pixels back: fill placement, border edges with an empty middle, a child clipped by its container, text landing in its colour, slice-vs-whole measurement agreement, fontId table dedupe/refusal/exhaustion, and the bracket's begin/begin, end-without-begin and failure-recovery contracts. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 11:06:27 -04:00
/**
* @brief The measure callback akgl_ui_init() hands to clay.
*
* clay's callback signature returns dimensions by value and has no error
* channel, so this cannot raise: on any failure -- an unregistered fontId, a
* font gone from the registry, SDL_ttf refusing the string -- it reports zero
* by zero and stashes the failure where akgl_ui_frame_end() will raise it.
* The slice is measured at its stated length through SDL_ttf directly, so it
* is never copied and need not be NUL-terminated.
*
* @param text The slice of text clay wants measured.
* @param config The text element's configuration; only `fontId` is
* consulted (see akgl_ui_font_register() on `fontSize`).
* @param userData Unused. clay passes back whatever init registered.
* @return The slice's size in pixels, or zero by zero on failure.
*/
Clay_Dimensions akgl_ui_measure_text(Clay_StringSlice text, Clay_TextElementConfig *config, void *userData);
/**
* @brief Draw one frame's render commands through a backend.
*
* The renderer half of akgl_ui_frame_end(), separated so a test can hand it
* a command array and read pixels back without a full frame bracket. Handles
* RECTANGLE, BORDER, TEXT, IMAGE and the SCISSOR pair; CUSTOM commands and
* image tint colours are skipped in this version, and per-corner radii are
* collapsed to the top-left value -- the widget helpers only produce uniform
* corners.
*
* An IMAGE command's `imageData` is an `akgl_Sprite *`; its first frame is
* drawn stretched to the command's bounding box.
*
* Any clip rectangle a command set is cleared before this returns, success or
* failure -- the UI must not leave the next frame's world clipped.
*
* @note TEXT commands draw through akgl_text_rendertextat(), whose contract
* is the *global* `akgl_renderer` -- so a host drawing its UI through
* some other backend must keep the two pointed at the same renderer,
* or its text lands somewhere else.
*
* @param self The backend to draw through. Required, along with its
* `sdl_renderer` and `draw_texture`.
* @param commands The commands `Clay_EndLayout` returned. Required.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p self, its `sdl_renderer`, its
* `draw_texture`, or @p commands is `NULL`.
* @throws AKGL_ERR_UI If a text line exceeds #AKGL_UI_MAX_TEXT_BYTES, a
* fontId does not resolve, or an IMAGE command carries no sprite.
* @throws AKERR_* Whatever the drawing primitives underneath raise.
*/
akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_execute_commands(akgl_RenderBackend *self, Clay_RenderCommandArray *commands);
Vendor clay and bring the UI subsystem up: arena, lifecycle, status band clay v0.14 (deps/clay, zlib licence) supplies the layout engine for the new akgl_ui subsystem. Sources-listed rather than add_subdirectory()'d, on the semver/libccd precedent: clay's CMakeLists declares no library target, builds its examples by default, and requires CMake 3.27 against this project's 3.10. src/ui_clay.c is the one CLAY_IMPLEMENTATION translation unit, compiled -w on the vendored-code terms but with clay's symbols deliberately exported -- consumers' CLAY() macros resolve against libakgl.so, and akgl/ui.h warns them never to link a second clay. The subsystem is runtime-optional the way collision is: always compiled in, inert until akgl_ui_init(), which bounds clay to the overridable AKGL_UI_* ceilings, checks Clay_MinMemorySize() against a static BSS arena (no malloc), and refuses with both numbers in the message when it does not fit. Measured: 812544 bytes at the default 1024 elements / 4096 measured words, against a 1 MiB arena. clay's void-callback layout errors are logged as they happen and the first is stashed for the error protocol to raise later. AKGL_ERR_UI joins the status band before AKGL_ERR_LIMIT, named in akgl_error_init. New `ui` test suite covers validation, the init/shutdown lifecycle, and arena exhaustion via the akgl_ui_arena_limit test hook (same contract as akgl_ccd_arena_set_limit). clay.h is installed beside semver.h, its licence beside libccd's, and the headers suite proves akgl/ui.h self-contained -- clay.h compiles clean under -Wall. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71
2026-08-02 10:53:11 -04:00
/**
* @brief Narrow the arena, so a test can drive akgl_ui_init() into refusal.
*
* The interesting behaviour is the refusal message naming both numbers, and
* the only other way to reach it is rebuilding the library with a smaller
* #AKGL_UI_ARENA_BYTES, which CI cannot do and a reader cannot repeat. Same
* contract as akgl_ccd_arena_set_limit().
*
* @param limit Usable bytes. 0, or anything above #AKGL_UI_ARENA_BYTES,
* restores the full arena.
*/
void akgl_ui_arena_limit(size_t limit);
#endif // _AKGL_UI_H_