Add the akgl_ui subsystem: clay-backed menus, HUDs and dialogs #3

Merged
andrew merged 15 commits from clay-ui into main 2026-08-02 16:14:50 -04:00
9 changed files with 448 additions and 17 deletions
Showing only changes of commit 8f848d80be - Show all commits

View File

@@ -25,7 +25,7 @@ The house rules for *writing* code against the protocol — never a `*_RETURN` i
libakerror reserves statuses 0255 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.

View File

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

View File

@@ -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_*`.

View File

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

View File

@@ -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

382
docs/22-ui.md Normal file
View File

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

View File

@@ -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 `<akgl/ui.h>` 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

View File

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

BIN
docs/images/uidemo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB