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 0000000..67e5fe1 Binary files /dev/null and b/docs/images/uidemo.png differ