/** * @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 `` 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 #include #include #include #include /** * @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 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 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); /* * 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_