/**
 * @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>

#include <SDL3/SDL.h>

#include <akerror.h>
#include <clay.h>

#include <akgl/renderer.h>
#include <akgl/types.h>

/**
 * @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

/**
 * @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

/**
 * @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;

/**
 * @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);

/**
 * @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);

/**
 * @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.
 *
 * 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);

/**
 * @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.
 *
 * @note The panel's height is fixed at #AKGL_UI_DIALOG_HEIGHT and the text is
 *       not clipped to it: a message that wraps to more lines than fit runs
 *       past the bottom border, visibly. That is the same behaviour as the
 *       hand-rolled panel this widget mirrors, and it is deliberate --
 *       overflow you can see beats truncation you cannot. Size the height for
 *       your longest message, or raise the override.
 * @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
 * 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);

/*
 * 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.
 */

/**
 * @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);

/**
 * @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_
