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