# 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.