Add the akgl_ui subsystem: clay-backed menus, HUDs and dialogs #3
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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_*`.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
382
docs/22-ui.md
Normal file
@@ -0,0 +1,382 @@
|
||||
# 22. User interfaces
|
||||
|
||||

|
||||
|
||||
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.
|
||||
@@ -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
|
||||
|
||||
@@ -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
BIN
docs/images/uidemo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Reference in New Issue
Block a user