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
2 changed files with 23 additions and 2 deletions
Showing only changes of commit f2efbdf1b7 - Show all commits

View File

@@ -28,6 +28,15 @@ declarative blocks yourself between the same two frame calls, with the whole DSL
available. The demo this chapter quotes, [`examples/uidemo`](../examples/uidemo), does 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()`. both: its title menu and HUD are widgets, its options screen is raw `CLAY()`.
**What this chapter assumes.** A program that already brings libakgl up — the
`akgl_game_init` startup order from [Chapter 7](07-the-game-and-the-frame.md) (or
[Chapter 3](03-getting-started.md)'s smallest window), plus one loaded font from
[Chapter 17](17-text-and-fonts.md). None of that is restated here, and the UI adds only
three calls to it, shown below. When something in *your* bring-up fights you, the demo's
`startup()` in [`examples/uidemo/uidemo.c`](../examples/uidemo/uidemo.c) is the complete
working sequence to diff against — including the `main()` shape, which the runnable
snippets in this manual deliberately hide.
Like collision, the subsystem is optional at runtime rather than at build time: it is 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 always compiled in, costs static storage until `akgl_ui_init` runs, and a game that
never calls that pays nothing else. never calls that pays nothing else.
@@ -242,8 +251,13 @@ static akgl_UiMenu title_menu = {
Declaring it each frame is `akgl_ui_menu(&title_menu)`. Input reaches it two ways, and 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 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 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 South — exactly `SDLK_UP`, `SDLK_DOWN` and `SDLK_RETURN` against `SDL_Event.key.key`,
the declaration itself. A stationary pointer claims nothing — parking the mouse over the and `SDL_GAMEPAD_BUTTON_DPAD_UP`, `_DPAD_DOWN` and `_SOUTH` against
`SDL_Event.gbutton.button`. Keycodes, not scancodes — which only matters when you
*synthesize* events, as the demo's `--demo` script does: a hand-built key event must
fill in `key.key`, or it matches nothing and the menu silently ignores it. 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 menu must not pin the selection against the keyboard, which would otherwise fight it
sixty times a second and lose. sixty times a second and lose.

View File

@@ -391,6 +391,13 @@ akerr_ErrorContext AKERR_NOIGNORE *akgl_ui_frame_end(akgl_RenderBackend *self);
* -- clay keeps the pointer, not a copy. May be empty, which * -- clay keeps the pointer, not a copy. May be empty, which
* draws an empty panel. * draws an empty panel.
* @param style Look to draw with, or `NULL` for the textbox palette. * @param style Look to draw with, or `NULL` for the textbox palette.
*
* @note The panel's height is fixed at #AKGL_UI_DIALOG_HEIGHT and the text is
* not clipped to it: a message that wraps to more lines than fit runs
* past the bottom border, visibly. That is the same behaviour as the
* hand-rolled panel this widget mirrors, and it is deliberate --
* overflow you can see beats truncation you cannot. Size the height for
* your longest message, or raise the override.
* @return `NULL` on success, otherwise an error context owned by the caller. * @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER If @p id or @p text is `NULL`. * @throws AKERR_NULLPOINTER If @p id or @p text is `NULL`.
* @throws AKGL_ERR_UI If the subsystem is not initialized or no frame is * @throws AKGL_ERR_UI If the subsystem is not initialized or no frame is