From 8f848d80beac32cb00e6e4129eb0056d6d0cf068 Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Sun, 2 Aug 2026 11:32:20 -0400 Subject: [PATCH] Document the UI subsystem: chapter 22, with the appendix moving to 23 docs/22-ui.md covers the clay-backed UI: what clay owns against what libakgl owns, bring-up and the arena refusal contract, the frame bracket and where it sits in a real frame, the consumed-event rules (with the one-frame-stale hit test stated plainly), the three widgets, menus across keyboard, gamepad and mouse, writing raw CLAY() with an application-owned press edge, images and styles, and how layout errors surface from frame_end with a captured trace. Every listing is a checked block: one linked-and-run example with pinned output, eight excerpts quoting examples/uidemo and examples/jrpg/textbox.c, and the chapter figure is a frame out of uidemo via docs_game_figures. The chapter owes and pays the comparison the tutorials earn: the dialog widget's default style reproduces the JRPG textbox palette, the two listings sit side by side, and the trade is stated -- 125 lines you own completely against one call that costs you the subsystem. Neither is deprecated; chapter 21 keeps teaching the hand-rolled panel on purpose. The appendix moves to 23 (links in chapters 4, 5, 6, 7, 13 and the TOC updated) and gains the ui.h status cross-reference, the AKGL_UI_* limits table, and the new draw primitives' rows. Its status-band figures were stale at COUNT 5 / LIMIT 261 while the band already held six codes; now correct at 7 / 263. Chapter 4 gains AKGL_ERR_UI (262) in the excerpt, the table and the name list. Co-Authored-By: Claude Code (Claude Fable 5, claude-fable-5) Claude-Session: https://claude.ai/code/session_01KzBDV2fqgnUAcqCKqKvc71 --- docs/04-errors.md | 17 +- docs/05-the-heap.md | 2 +- docs/06-the-registry.md | 2 +- docs/07-the-game-and-the-frame.md | 2 +- docs/13-tilemaps.md | 2 +- docs/22-ui.md | 382 ++++++++++++++++++ ...pendix-limits.md => 23-appendix-limits.md} | 55 ++- docs/README.md | 3 +- docs/images/uidemo.png | Bin 0 -> 11380 bytes 9 files changed, 448 insertions(+), 17 deletions(-) create mode 100644 docs/22-ui.md rename docs/{22-appendix-limits.md => 23-appendix-limits.md} (87%) create mode 100644 docs/images/uidemo.png diff --git a/docs/04-errors.md b/docs/04-errors.md index 451a5f0..64d0277 100644 --- a/docs/04-errors.md +++ b/docs/04-errors.md @@ -25,7 +25,7 @@ The house rules for *writing* code against the protocol — never a `*_RETURN` i libakerror reserves statuses 0–255 for the host's `errno` values and its own `AKERR_*` codes; consumers allocate upward from `AKERR_FIRST_CONSUMER_STATUS`, which is **256**. -libakgl claims a band of six starting there, under the owner string `"libakgl"`: +libakgl claims a band of seven starting there, under the owner string `"libakgl"`: ```c excerpt=include/akgl/error.h #define AKGL_ERR_OWNER "libakgl" @@ -37,12 +37,13 @@ libakgl claims a band of six starting there, under the owner string `"libakgl"`: #define AKGL_ERR_BEHAVIOR (AKGL_ERR_BASE + 3) /**< A component did not behave the way its contract requires */ #define AKGL_ERR_LOGICINTERRUPT (AKGL_ERR_BASE + 4) /**< Actor logic is telling the physics simulator to skip it */ #define AKGL_ERR_COLLISION (AKGL_ERR_BASE + 5) /**< A collision query could not be answered; the message says why */ +#define AKGL_ERR_UI (AKGL_ERR_BASE + 6) /**< The UI subsystem refused, or clay reported a layout error; the message says which */ ``` -**`AKGL_ERR_LIMIT` is not a status code.** It is `AKGL_ERR_BASE + 6`, one past the last +**`AKGL_ERR_LIMIT` is not a status code.** It is `AKGL_ERR_BASE + 7`, one past the last one, and exists only so `AKGL_ERR_COUNT` can be computed from it. Nothing in `src/` raises it and `akgl_error_init` does not name it. Do not write a `HANDLE(e, AKGL_ERR_LIMIT)` -arm — it would catch nothing, and if a seventh real code is ever added it would silently +arm — it would catch nothing, and if an eighth real code is ever added it would silently start catching that instead. | Code | Value | Means | Raised by | What the caller does | @@ -53,10 +54,12 @@ start catching that instead. | `AKGL_ERR_BEHAVIOR` | 259 | A component did not behave the way its contract requires | **Nothing in the library.** Only `tests/` raises it, through `testutil.h` | Available to you for the same purpose: asserting a contract in your own code | | `AKGL_ERR_LOGICINTERRUPT` | 260 | **Not a failure — a control signal.** "Skip the rest of this step for this actor" | your own `movementlogicfunc` | Raise it deliberately. See the note below | | `AKGL_ERR_COLLISION` | 261 | A collision query could not be answered | **One cause only**: `akgl_collision_test`, when the ccd arena is exhausted. The message carries the high-water mark | Raise `AKGL_CCD_ARENA_BYTES` to the reported figure. See [Chapter 15](15-collision.md) | +| `AKGL_ERR_UI` | 262 | The UI subsystem refused, or clay reported a layout error | `akgl_ui_*` — lifecycle misuse (init twice, a widget outside the frame bracket), an arena or table at its ceiling, and layout errors surfacing from `akgl_ui_frame_end` | The message says which; the ceilings are in [Chapter 23](23-appendix-limits.md). See [Chapter 22](22-ui.md) | `akgl_error_init` reserves the band all-or-nothing and registers a name for each of the -six: `"SDL Error"`, `"Registry Error"`, `"Heap Error"`, `"Behavior Error"`, -`"Logic Interrupt"`, `"Collision Error"`. Those names are what a stack trace prints. +seven: `"SDL Error"`, `"Registry Error"`, `"Heap Error"`, `"Behavior Error"`, +`"Logic Interrupt"`, `"Collision Error"`, `"UI Error"`. Those names are what a stack trace +prints. Two rows deserve more than a cell. @@ -99,7 +102,7 @@ Statuses raised by the libraries underneath also reach you unchanged. `aksl_fope `akgl_error_init` can raise libakerror's `AKERR_STATUS_RANGE_OVERLAP`, `AKERR_STATUS_RANGE_FULL` or `AKERR_STATUS_NAME_FULL`. -[Chapter 22](22-appendix-limits.md) has the per-function cross-reference. +[Chapter 23](23-appendix-limits.md) has the per-function cross-reference. ## Table 3 — the exit status trap @@ -266,6 +269,6 @@ Four things in there are libakgl-specific and worth naming: release. - [Chapter 6](06-the-registry.md) — the name lookups behind `AKERR_KEY` and `AKERR_NULLPOINTER`. -- [Chapter 22](22-appendix-limits.md) — which function raises what. +- [Chapter 23](23-appendix-limits.md) — which function raises what. - `deps/libakerror/README.md` — the protocol itself, and the exit-status discussion. - The generated Doxygen reference — every function's own `@throws` list. diff --git a/docs/05-the-heap.md b/docs/05-the-heap.md index 713b5e2..5db47c9 100644 --- a/docs/05-the-heap.md +++ b/docs/05-the-heap.md @@ -344,4 +344,4 @@ all visible in `src/heap.c`. - [Chapter 4](04-errors.md) — `AKGL_ERR_HEAP` in the status tables. - [Chapter 6](06-the-registry.md) — the registries the releases clear entries from. -- [Chapter 22](22-appendix-limits.md) — every `AKGL_MAX_*` in one place. +- [Chapter 23](23-appendix-limits.md) — every `AKGL_MAX_*` in one place. diff --git a/docs/06-the-registry.md b/docs/06-the-registry.md index 41056d3..37758f8 100644 --- a/docs/06-the-registry.md +++ b/docs/06-the-registry.md @@ -224,5 +224,5 @@ registry's. - [Chapter 7](07-the-game-and-the-frame.md) — where in startup the registries are created, and where configuration has to be in place by. - [Chapter 14](14-physics.md) — choosing a backend, and what the `physics.*` properties do. -- [Chapter 22](22-appendix-limits.md) — the configuration table again, alongside every +- [Chapter 23](23-appendix-limits.md) — the configuration table again, alongside every `AKGL_MAX_*`. diff --git a/docs/07-the-game-and-the-frame.md b/docs/07-the-game-and-the-frame.md index 18268f0..3c6e13d 100644 --- a/docs/07-the-game-and-the-frame.md +++ b/docs/07-the-game-and-the-frame.md @@ -367,4 +367,4 @@ error-reporting path. See [Chapter 4](04-errors.md). what `draw_world` does with the layers. - [Chapter 14](14-physics.md) — what `simulate` does with the `dt` this chapter does not pace. -- [Chapter 22](22-appendix-limits.md) — `AKGL_GAME_*` and the rest of the constants. +- [Chapter 23](23-appendix-limits.md) — `AKGL_GAME_*` and the rest of the constants. diff --git a/docs/13-tilemaps.md b/docs/13-tilemaps.md index 0536676..33afb3d 100644 --- a/docs/13-tilemaps.md +++ b/docs/13-tilemaps.md @@ -37,7 +37,7 @@ why the library's own map, `akgl_default_gamemap`, is a file-scope object in The bounds are fixed at compile time, so the size is fixed too. Every field is present whether the map uses it or not: a 20x15 map with one tileset and two layers costs the same 25.2 MiB as a 511x511 one. Raising any of the constants below raises this number and -changes the ABI — see [Chapter 22](22-appendix-limits.md). +changes the ABI — see [Chapter 23](23-appendix-limits.md). ## Limits diff --git a/docs/22-ui.md b/docs/22-ui.md new file mode 100644 index 0000000..88ef180 --- /dev/null +++ b/docs/22-ui.md @@ -0,0 +1,382 @@ +# 22. User interfaces + +![The UI demo's play screen: a LIVES label top-left, a SCORE label top-right, and a dialog panel across the bottom, over a checkerboard standing in for a game world](images/uidemo.png) + +Every game eventually wants a dialog box, a score counter, or a menu — and hand-rolling +them out of rectangles and `akgl_text_rendertextat` is tolerable exactly once. +[Chapter 21](21-tutorial-jrpg.md) does it once, on purpose, and its 125-line +`textbox.c` is still the right call for one panel with no ambitions. This chapter is for +everything past that point: HUDs, menus, options screens, and the layout arithmetic that +makes them miserable to maintain by hand. + +The layout engine is [clay](https://github.com/nicbarker/clay), vendored under +`deps/clay` and compiled into `libakgl.so`. Per this manual's rule, clay's own API is +documented by clay — its README is thorough — and this chapter covers what libakgl adds +or constrains. The split: + +| clay owns | libakgl owns | +|---|---| +| The layout algorithm and the `CLAY()` declaration DSL | The arena clay allocates from — static storage, no `malloc`, sized by `AKGL_UI_ARENA_BYTES` | +| Sizing, padding, floating elements, scroll containers | Text measurement, through SDL_ttf and the font registry | +| The render command list each frame produces | Drawing those commands, through the render backend | +| Hover and pointer-over queries | Feeding it the mouse, and telling *you* which events the UI consumed | + +**There are two ways in, and they compose.** The widget helpers — `akgl_ui_dialog`, +`akgl_ui_label`, `akgl_ui_menu` — cover the common cases in one call each, and a simple +game never touches a `CLAY()` macro. When a screen outgrows them, you write clay's +declarative blocks yourself between the same two frame calls, with the whole DSL +available. The demo this chapter quotes, [`examples/uidemo`](../examples/uidemo), does +both: its title menu and HUD are widgets, its options screen is raw `CLAY()`. + +Like collision, the subsystem is optional at runtime rather than at build time: it is +always compiled in, costs static storage until `akgl_ui_init` runs, and a game that +never calls that pays nothing else. + +**One warning before any code.** libakgl ships clay inside `libakgl.so` and exports its +symbols, because the `CLAY()` macros in *your* translation units expand to calls into +them. Do not define `CLAY_IMPLEMENTATION` anywhere and do not link a second copy of clay +— two definitions of the same symbols, and the loader picks one silently. + +## Bring it up, lay something out + +`akgl_ui_init(width, height)` takes the layout size as parameters — deliberately not +read from the camera or the window, so a headless program can bring the UI up with no +renderer at all. It bounds clay to the `AKGL_UI_*` ceilings, checks the arena fits them, +and refuses **with both byte counts in the message** when it does not: a raised ceiling +without a raised arena is a loud startup failure, never a corruption at frame forty +thousand. + +After that, a frame of UI is a bracket with declarations inside: + +```c run=akglapp +PASS(errctx, akgl_ui_init(320, 240)); + +PASS(errctx, akgl_ui_frame_begin()); +CLAY({ + .id = CLAY_ID("panel"), + .layout = { + .sizing = { + .width = CLAY_SIZING_FIXED(120), + .height = CLAY_SIZING_FIXED(40) + } + }, + .backgroundColor = { 24, 20, 37, 255 } +}) {} +PASS(errctx, akgl_ui_frame_end(akgl_renderer)); + +PASS(errctx, akgl_ui_shutdown()); +printf("one panel, laid out and drawn\n"); +``` + +```output +one panel, laid out and drawn +``` + +`frame_begin` starts the clay layout and feeds it the pointer state the event handler +has been accumulating; `frame_end` computes the layout and draws every render command it +produces through the backend — fills and rounded fills, borders, clip rectangles, text, +and sprites. Everything lands in screen coordinates on top of whatever is already on the +target. + +Text needs one more step at startup: fonts. clay names a font by a `uint16_t` fontId; +libakgl names one by a registry key ([Chapter 17](17-text-and-fonts.md)). The bridge is +`akgl_ui_font_register`, and the demo's whole font story is three lines: + +```c excerpt=examples/uidemo/uidemo.c + PASS(errctx, akgl_text_loadfont(UIDEMO_FONT_NAME, UIDEMO_FONT_FILE, UIDEMO_FONT_SIZE)); + PASS(errctx, akgl_ui_init(UIDEMO_WIDTH, UIDEMO_HEIGHT)); + PASS(errctx, akgl_ui_font_register(UIDEMO_FONT_NAME, &fontid)); +``` + +The first font registered gets id 0, which is what the widgets' default style uses — so +a one-font game never mentions a fontId again. Two things worth knowing before they +surprise you: + +- **`Clay_TextElementConfig.fontSize` is ignored.** A libakgl font bakes its size in at + load ([Chapter 17](17-text-and-fonts.md) explains why); the size text renders at is + the size the font behind its id was loaded at. One face at two sizes is two loads, two + registrations, two ids. +- The table stores the *name* and resolves it per use, so `akgl_text_unloadfont` on a + registered font makes the next frame fail loudly with the name in the message — not + dangle. + +## The frame contract + +Where the bracket goes in a real frame, from the demo — compare the JRPG's `frame()`, +which calls `akgl_game_update` where this program paints a checkerboard: + +```c excerpt=examples/uidemo/uidemo.c + PASS(errctx, akgl_renderer->frame_start(akgl_renderer)); + if ( state == UIDEMO_STATE_PLAY ) { + PASS(errctx, akgl_draw_background(akgl_renderer, UIDEMO_WIDTH, UIDEMO_HEIGHT)); + score += 1; + } + + PASS(errctx, akgl_ui_frame_begin()); + switch ( state ) { + case UIDEMO_STATE_TITLE: + PASS(errctx, declare_title()); + break; + case UIDEMO_STATE_OPTIONS: + PASS(errctx, declare_options()); + break; + case UIDEMO_STATE_PLAY: + PASS(errctx, declare_play()); + break; + default: + break; + } + PASS(errctx, akgl_ui_frame_end(akgl_renderer)); +``` + +The UI draws after the world because it is declared after the world — the overlay slot +between `akgl_game_update` and the backend's `frame_end` is exactly where the JRPG drew +its hand-rolled text box, and nothing about that slot changed. + +Events go through the UI *first*. `akgl_ui_handle_event` takes every event +unconditionally — the same pass-everything contract as `akgl_controller_handle_event` — +and reports back whether the UI claimed it, so a click on a menu never leaks through and +also fires a game control: + +```c excerpt=examples/uidemo/uidemo.c + PASS(errctx, akgl_ui_handle_event((void *)&akgl_game.state, event, &consumed)); + if ( consumed ) { + SUCCEED_RETURN(errctx); + } + + switch ( state ) { + case UIDEMO_STATE_TITLE: + PASS(errctx, akgl_ui_menu_handle_event(&title_menu, event, &consumed)); + break; +``` + +Three rules govern what gets consumed, and each is a decision worth stating: + +- **Presses, releases and the wheel are 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; one frame of staleness is the standard model's accepted cost, + and before any frame has been laid out, nothing is over anything. +- **Keyboard events are never consumed by the UI itself.** Which menu hears the arrow + keys is something the application declares — the `switch` above *is* the focus model — + not something a pointer position implies. + +## A dialog in one call — and what it replaces + +The play screen's declarations, whole: + +```c excerpt=examples/uidemo/uidemo.c + PASS(errctx, aksl_snprintf(&count, scoretext, sizeof(scoretext), "SCORE %05d", score)); + PASS(errctx, aksl_snprintf(&count, livestext, sizeof(livestext), "LIVES %d", lives)); + PASS(errctx, akgl_ui_label("score", scoretext, AKGL_UI_ANCHOR_TOP_RIGHT, NULL)); + PASS(errctx, akgl_ui_label("lives", livestext, AKGL_UI_ANCHOR_TOP_LEFT, NULL)); + if ( dialog_open ) { + PASS(errctx, akgl_ui_dialog("dialog", + "This panel is one call. Space dismisses it; " + "compare examples/jrpg/textbox.c.", + NULL)); + } +``` + +Note what is absent: no `visible` flag, no draw call, no geometry. The dialog is open +because this frame declares it — a declarative frame *is* the flag. The text buffers are +`static` because clay borrows the pointer until `frame_end` rather than copying; format +your score into storage that outlives the bracket. + +Now the same panel the way [Chapter 21](21-tutorial-jrpg.md) builds it, which is the +comparison this chapter owes you. The hand-rolled version keeps a flag and a copy of the +string, and its draw function does the geometry itself: + +```c excerpt=examples/jrpg/textbox.c + panel.x = TEXTBOX_MARGIN; + panel.w = akgl_camera->w - (2.0f * TEXTBOX_MARGIN); + panel.h = TEXTBOX_HEIGHT; + panel.y = akgl_camera->h - TEXTBOX_MARGIN - panel.h; + + PASS(errctx, akgl_draw_filled_rect(akgl_renderer, &panel, TEXTBOX_FILL)); + PASS(errctx, akgl_draw_rect(akgl_renderer, &panel, TEXTBOX_EDGE)); + PASS(errctx, + akgl_text_rendertextat( + font, + textbox_text, + TEXTBOX_INK, + (int)(panel.w - (2.0f * TEXTBOX_PADDING)), + (int)(panel.x + TEXTBOX_PADDING), + (int)(panel.y + TEXTBOX_PADDING) + )); +``` + +The widget's default style is **deliberately that panel's palette** — near-black fill, +parchment edge and ink, 8 pixels of padding — so the two produce the same picture, and +the trade is visible with nothing hidden in a theme: + +- `textbox.c` is 125 lines you own completely. It costs no new concepts, no arena, no + frame bracket; its geometry is four assignments you can read. For one panel, that is a + perfectly good deal — which is why chapter 21 still teaches it and its game still + ships it. +- `akgl_ui_dialog` is one line that costs you the subsystem: `akgl_ui_init`, a + registered font, and the bracket in your frame. The payment buys every *next* piece of + interface — the second panel is also one line, the score label is one line, the menu + is a struct — and the layout arithmetic, wrapping, and stacking are clay's problem + from then on. + +Neither is deprecated. The library's own position: build one panel by hand; build an +interface on the subsystem. + +## Menus: one selection, three devices + +A menu is caller-owned state — a struct you keep, the way `textbox.c` keeps its statics. +There is no heap pool behind it because it owns no texture, no font, and no registry +entry: + +```c excerpt=examples/uidemo/uidemo.c +static akgl_UiMenu title_menu = { + .id = "title", + .items = { "Start", "Options", "Quit" }, + .count = 3, +}; +``` + +Declaring it each frame is `akgl_ui_menu(&title_menu)`. Input reaches it two ways, and +they meet in the same struct: `akgl_ui_menu_handle_event` moves `selected` from the +arrow keys and D-pad (wrapping at both ends) and sets `activated` on Return or gamepad +South; the mouse selects by *moving onto* a row and activates by clicking one, during +the declaration itself. A stationary pointer claims nothing — parking the mouse over the +menu must not pin the selection against the keyboard, which would otherwise fight it +sixty times a second and lose. + +`activated` latches until you clear it, so the check lives wherever reading it is +convenient — the demo reads it after the frame closes, which catches both input routes: + +```c excerpt=examples/uidemo/uidemo.c + if ( title_menu.activated ) { + title_menu.activated = false; + switch ( title_menu.selected ) { + case 0: + state = UIDEMO_STATE_PLAY; + score = 0; + dialog_open = false; + break; + case 1: + state = UIDEMO_STATE_OPTIONS; + break; + case 2: + default: + running = false; + break; + } + } +``` + +## Writing layout directly with CLAY() + +The options screen uses no widgets, and exists to show that the widgets are a +convenience rather than a boundary. One row of it carries every idea — hover styling +computed *inside* the declaration, and a click paired with a press edge the application +tracks itself: + +```c excerpt=examples/uidemo/uidemo.c + CLAY({ + .id = CLAY_ID("options-music"), + .layout = { .padding = { 8, 8, 4, 4 } }, + .backgroundColor = Clay_Hovered() + ? (Clay_Color){ 64, 58, 88, 255 } + : (Clay_Color){ 0, 0, 0, 0 } + }) { + if ( Clay_Hovered() && clicked ) { + opt_music = !opt_music; + } + CLAY_TEXT(((Clay_String){ .length = (int32_t)strlen(musicrow), .chars = musicrow }), + CLAY_TEXT_CONFIG({ .textColor = { 240, 236, 214, 255 }, .fontId = 0 })); + } +``` + +`clicked` is one application-owned `bool`: set when a left press arrives in the event +loop — *before* `akgl_ui_handle_event` can consume it, because a click on a UI row is +always consumed — and cleared at the end of the frame. The widgets keep an equivalent +edge internally; a raw screen keeps its own. Everything else — the sizing model, scroll +containers, floating attach points, aspect ratios — is clay's DSL, and clay's README +documents it far better than a restatement here would. + +## Images and styles + +An image on a UI element is a sprite: set `.image.imageData` in a `CLAY()` declaration +to an `akgl_Sprite *` ([Chapter 10](10-spritesheets-and-sprites.md)) and its first frame +is drawn stretched to the element's box. Two current limits, both deliberate scope +rather than accident: clay's image tint colour and `CUSTOM` render commands are skipped +by the executor, and per-corner radii collapse to the top-left value — the widgets only +produce uniform corners. The mouse cursor stays the operating system's; a game that +wants a themed cursor draws a floating image element at the pointer, which makes a good +exercise. + +A style is one struct shared by all three widgets — colours, padding, corner radius, +fontId — and `NULL` means the textbox palette described above. There is no per-field +defaulting: a transparent fill and a zero radius are things a style legitimately says, +so copy the default and change what you mean to change: + +```c wrap=akglbody +akgl_UiStyle alert = { + .fill = { 120, 24, 24, 235 }, + .edge = { 240, 236, 214, 255 }, + .ink = { 240, 236, 214, 255 }, + .padding = 8.0f, + .corner_radius = 6.0f, + .fontid = 0 +}; + +PASS(errctx, akgl_ui_dialog("alert", "The reactor is on fire.", &alert)); +``` + +## When things go wrong + +Everything here reports through the ordinary error protocol +([Chapter 4](04-errors.md)) under one status, `AKGL_ERR_UI`, and the message says which +refusal it was. Lifecycle misuse fails at the call that misused it: + +```text +/home/andrew/source/libakgl/src/ui.c:ui_widget_ready:850: 262 (UI Error) : Widgets are declared between akgl_ui_frame_begin and akgl_ui_frame_end +/home/andrew/source/libakgl/src/ui.c:akgl_ui_dialog:866 +/home/andrew/source/libakgl/main.c:main:13 +/home/andrew/source/libakgl/main.c:main:16: Unhandled Error 262 (UI Error): Widgets are declared between akgl_ui_frame_begin and akgl_ui_frame_end +``` + +Errors *inside the layout* cannot fail at the call that caused them, because clay +reports through a void callback with libakgl nowhere on the stack. So they are logged as +they happen, stashed, and raised from `akgl_ui_frame_end` — the earliest point the +protocol can carry them — with the first message and a count of the rest. A frame that +fails is one bad frame: the next `akgl_ui_frame_begin` is legal, and the executor clears +any clip rectangle on its way out so a failed UI cannot leave the next frame's *world* +clipped. + +The ceilings — elements, measured words, arena bytes, fonts, text-run length, menu +entries — are all in [Chapter 23](23-appendix-limits.md), every one of them overridable, +and every refusal names the number to raise. + +## Build it and run it + +The demo builds with the library: + +```sh norun +cmake -S . -B build +cmake --build build -j$(nproc) +./build/examples/uidemo/uidemo +``` + +Arrow keys, Return, the D-pad and the mouse all drive the title menu; Space opens the +dialog on the play screen; Escape backs out. `ctest -R example_uidemo` runs the same +program headless through a scripted tour of every screen. + +## Where to look next + +- [clay's README](https://github.com/nicbarker/clay) — the layout DSL itself: sizing, + floating elements, scroll containers, and the debug tools. +- [Chapter 16](16-input.md) — the control maps that own the keyboard and gamepad once + the UI has declined an event. +- [Chapter 17](17-text-and-fonts.md) — fonts, the registry, and what text costs per + frame; a UI panel of text pays the same immediate-mode price. +- [Chapter 21](21-tutorial-jrpg.md) — the hand-rolled text box this chapter keeps + comparing against, in the game that earns it. +- [Chapter 23](23-appendix-limits.md) — every `AKGL_UI_*` limit and the `ui.h` status + cross-reference. diff --git a/docs/22-appendix-limits.md b/docs/23-appendix-limits.md similarity index 87% rename from docs/22-appendix-limits.md rename to docs/23-appendix-limits.md index a996d61..d0fbb03 100644 --- a/docs/22-appendix-limits.md +++ b/docs/23-appendix-limits.md @@ -1,4 +1,4 @@ -# 22. Appendix: limits, statuses and properties +# 23. Appendix: limits, statuses and properties Three reference tables that no single chapter owns: which function raises which status, every compile-time bound, and every configuration property the library reads. @@ -148,6 +148,9 @@ What the statuses mean is [Chapter 4](04-errors.md). | `akgl_draw_circle` | `AKERR_NULLPOINTER`, `AKERR_OUTOFBOUNDS`, `AKGL_ERR_SDL` | | `akgl_draw_flood_fill` | `AKERR_NULLPOINTER`, `AKGL_ERR_SDL`, `AKERR_OUTOFBOUNDS` past `AKGL_DRAW_MAX_FLOOD_SPANS` | | `akgl_draw_copy_region`, `_paste_region` | `AKERR_NULLPOINTER`, `AKGL_ERR_SDL` | +| `akgl_draw_filled_rounded_rect` | `AKERR_NULLPOINTER`, `AKGL_ERR_SDL` | +| `akgl_draw_arc` | `AKERR_NULLPOINTER`, `AKERR_OUTOFBOUNDS`, `AKGL_ERR_SDL` | +| `akgl_draw_set_clip` | `AKERR_NULLPOINTER`, `AKGL_ERR_SDL` — a `NULL` rectangle is legal here and clears the clip | ### `tilemap.h` @@ -176,6 +179,22 @@ What the statuses mean is [Chapter 4](04-errors.md). | `akgl_controller_default` | `AKERR_OUTOFBOUNDS`, **`AKGL_ERR_REGISTRY`** — the library's only site | | `akgl_controller_poll_key`, `_poll_keystroke` | `AKERR_NULLPOINTER` | +### `ui.h` + +| Function | Raises | +|---|---| +| `akgl_ui_init` | `AKERR_OUTOFBOUNDS`, `AKGL_ERR_UI` — already initialized, or an arena clay's structures do not fit; the message carries both byte counts | +| `akgl_ui_shutdown` | *nothing — idempotent by design* | +| `akgl_ui_resize` | `AKGL_ERR_UI`, `AKERR_OUTOFBOUNDS` | +| `akgl_ui_font_register` | `AKERR_NULLPOINTER`, `AKGL_ERR_UI`, `AKERR_OUTOFBOUNDS` past `AKGL_UI_FONT_NAME_LENGTH` | +| `akgl_ui_handle_event` | `AKERR_NULLPOINTER`, `AKERR_OUTOFBOUNDS` from a resize event | +| `akgl_ui_frame_begin` | `AKGL_ERR_UI` — uninitialized, or a frame already open | +| `akgl_ui_frame_end` | `AKERR_NULLPOINTER`, `AKGL_ERR_UI` — no frame open, clay layout errors, an over-long text run, an unresolvable fontId — plus whatever the draw primitives raise | +| `akgl_ui_dialog`, `_label`, `_menu` | `AKERR_NULLPOINTER`, `AKGL_ERR_UI` outside the frame bracket; `_label` and `_menu` add `AKERR_OUTOFBOUNDS` | +| `akgl_ui_menu_handle_event` | `AKERR_NULLPOINTER`, `AKERR_OUTOFBOUNDS` | +| `akgl_ui_execute_commands` | `AKERR_NULLPOINTER`, `AKGL_ERR_UI`, plus whatever the draw primitives raise | +| `akgl_ui_measure_text` | *cannot report — clay's callback signature; failures stash and surface from `akgl_ui_frame_end`* | + ### `text.h`, `audio.h`, `assets.h`, `util.h`, `staticstring.h` | Function | Raises | @@ -196,8 +215,9 @@ What the statuses mean is [Chapter 4](04-errors.md). ## B. Compile-time limits -Everything below is fixed when the library is compiled. **Only the `AKGL_MAX_HEAP_*` eight -and `AKGL_CCD_ARENA_BYTES` are overridable**, and even those have to be overridden for the whole build — see +Everything below is fixed when the library is compiled. **Only the `AKGL_MAX_HEAP_*` eight, +`AKGL_CCD_ARENA_BYTES` and the `AKGL_UI_*` family are overridable**, and even those have to +be overridden for the whole build — see [Chapter 5](05-the-heap.md), "The ceilings are a compile-time ABI constraint". The rest are plain `#define`s with no `#ifndef` guard: changing one means editing the header and rebuilding libakgl and everything linking it. @@ -347,6 +367,31 @@ keeps instead of recursing per pixel. A region needing more pending runs at once `AKERR_OUTOFBOUNDS` rather than overflowing; an ordinary convex or moderately concave shape needs a few dozen. +`AKGL_DRAW_ROUNDED_RECT_SEGMENTS` is 16 — triangles per corner of +`akgl_draw_filled_rounded_rect`. `AKGL_DRAW_ARC_MAX_POINTS` is 64 — the vertex ceiling one +`akgl_draw_arc` spends, two per step along the curve; the step count scales with the swept +angle under it. Both size fixed arrays, so neither is a per-call cost and neither is +overridable. + +### The UI + +Every `AKGL_UI_*` limit is guarded by `#ifndef`, so a consumer may override any of them the +way the pool ceilings are overridden — for the whole build, before `` is seen. +`akgl_ui_init` checks that the arena still fits the element and word ceilings and refuses +with both byte counts in the message when it does not, so a raised ceiling without a raised +arena is a loud startup failure rather than a corruption. + +| Constant | Value | Bounds | +|---|---|---| +| `AKGL_UI_MAX_ELEMENTS` | 1024 | Elements one layout may declare | +| `AKGL_UI_MAX_MEASURE_WORDS` | 4096 | Words clay's text-measurement cache holds | +| `AKGL_UI_ARENA_BYTES` | 1048576 | Static storage clay lays its structures out in (812544 needed at the defaults) | +| `AKGL_UI_MAX_FONTS` | 8 | fontId table slots | +| `AKGL_UI_FONT_NAME_LENGTH` | 64 | Bytes per fontId slot's registry name, terminator included | +| `AKGL_UI_MAX_TEXT_BYTES` | 1024 | Bytes one text render command's line may occupy | +| `AKGL_UI_MENU_MAX_ITEMS` | 16 | Entries one `akgl_UiMenu` may carry | +| `AKGL_UI_DIALOG_HEIGHT` | 56 | Height of the `akgl_ui_dialog` panel, in pixels | + ### Audio ```c excerpt=include/akgl/audio.h @@ -383,8 +428,8 @@ budget and was wrong by a factor of a thousand, blocking for roughly sixteen min | Constant | Value | |---|---| | `AKGL_ERR_BASE` | 256 (`AKERR_FIRST_CONSUMER_STATUS`) | -| `AKGL_ERR_COUNT` | 5 | -| `AKGL_ERR_LIMIT` | 261 — **the one-past-the-end sentinel, not a status** | +| `AKGL_ERR_COUNT` | 7 | +| `AKGL_ERR_LIMIT` | 263 — **the one-past-the-end sentinel, not a status** | ## C. Configuration properties diff --git a/docs/README.md b/docs/README.md index c47e41f..a116205 100644 --- a/docs/README.md +++ b/docs/README.md @@ -34,7 +34,8 @@ per-function reference, build the Doxygen output with `doxygen Doxyfile`. | **[19. Utilities](19-utilities.md)** | Rectangle overlap, path resolution, the JSON accessors, static strings | | **[20. Tutorial: a 2D sidescroller](20-tutorial-sidescroller.md)** | Thirteen steps from an empty directory to a game with gravity, a jump, coins and hazards. **Start here** | | **[21. Tutorial: a top-down JRPG](21-tutorial-jrpg.md)** | Thirteen more, for four-way movement, NPCs, a text box, a follower, and freezing the world | -| **[22. Appendix](22-appendix-limits.md)** | Every limit, every status, every configuration property | +| **[22. User interfaces](22-ui.md)** | Menus, HUDs and dialogs on the clay layout engine: the widgets, the frame bracket, and writing `CLAY()` yourself | +| **[23. Appendix](23-appendix-limits.md)** | Every limit, every status, every configuration property | **If you are new, read chapter 20 first and read it in order.** It builds a working game from an empty directory and teaches the library as it needs each piece; chapter 21 assumes it. diff --git a/docs/images/uidemo.png b/docs/images/uidemo.png new file mode 100644 index 0000000000000000000000000000000000000000..67e5fe16aedb12a6ff4c0233e4fb37566a77f6d7 GIT binary patch literal 11380 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 literal 0 HcmV?d00001