162 lines
6.9 KiB
C
162 lines
6.9 KiB
C
|
|
/**
|
||
|
|
* @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 <akerror.h>
|
||
|
|
#include <clay.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 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_
|