Files
akbasic/include/akbasic/ui.h
Tachikoma 3b32a682a1 Give BASIC menus, dialogs and HUD labels over libakgl's UI helpers
Group K, and the first verbs to reach the akgl_ui subsystem 0.9.0 brought
in: MENU and GETMENU and RMENU, DIALOG, HUD and UISTYLE. A program that
wanted a title screen had to draw one out of CHAR and GETKEY, which is
what both breakout tutorials make a reader do.

The interesting part is the impedance mismatch. libakgl's UI is immediate
mode -- widgets are re-declared inside a frame bracket every frame and
clay borrows their text until the bracket closes -- and a BASIC program
says MENU 1, "START" on line 100 and expects it up on line 900, several
hundred frames later. So src/ui_akgl.c is retained on this side and
immediate on that one: the record's entry points are setters that copy
into akbasic_AkglUi, and akbasic_ui_akgl_render() replays the whole set
once a frame from the host's pump. No BASIC string, which lives in the
per-line value pool, is ever what clay is handed.

The shapes are borrowed rather than invented. MENU retires the way SOLID
does -- no entries retires one, no arguments retire them all. GETMENU
holds the step loop the way GETKEY does, so parking is not blocking: the
step still returns, the host keeps its frame rate, and the sprite, audio
and collision services keep running underneath because they run before
the blocking checks. RMENU(n,1) reads and clears the way BUMP() does.
Withdrawing the device or retiring the menu releases a holding GETMENU
with 0 rather than wedging the script, which is akbasic_input_service()'s
rule for a withdrawn keyboard.

One thing a program has to know, and docs/19-user-interface.md says it
twice: a menu that is up owns the cursor keys and Return. It has to, and
retiring it gives them back -- forget the MENU n before an INPUT and the
INPUT never sees the Return that ends it.

akbasic_runtime_set_ui() is its own function rather than a fifth argument
to akbasic_runtime_set_devices(), whose signature has twenty-eight call
sites in tests and documentation that are about something else.

deps/libakgl is not touched. akgl_UiAnchor has the four corners and dead
centre, so HUD offers exactly those five; TODO.md records what a
top-centre and bottom-centre would cost upstream, along with the three
other things this deliberately leaves out. No new error code either --
DEVICE, BOUNDS, SYNTAX and TYPE cover the group, and 520 stays free.

tools/screenshot.c had to learn that "needs a font" and "draws the text
grid" are two questions. They were one, and a UI figure came out black:
the text layer owns every pixel of the rows it covers and painted over
the widgets. The new ui=1 fence attribute asks for the first without the
second; MAINTENANCE.md documents it.

112/112 in both configurations, 112/112 under ASan and UBSan, coverage
94.1% against the 90% gate with src/runtime_ui.c at 99% of lines and
100% of functions, doxygen clean, and the four new figures byte-identical
on a re-render. TODO.md section 8's gate table was stale on several
counts besides these and is refreshed with measured numbers.

Co-Authored-By: Tachikoma (Claude Code Opus 5 1M) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EwxGB6TdoVvZ11KQQME9cL
Co-Authored-By: Andrew Kesterson <andrew@aklabs.net>
2026-08-02 18:37:10 -04:00

180 lines
7.6 KiB
C

/**
* @file ui.h
* @brief Declares the UI backend: where MENU, DIALOG, HUD and UISTYLE land.
*
* Same reasoning as graphics.h, audio.h and input.h -- the core library is free
* of SDL and builds with no libakgl present, so the UI verbs call through a
* record of function pointers rather than reaching akgl_ui_* directly.
*
* One thing here is unlike the other four groups, and it shapes the whole
* record. **libakgl's UI is immediate mode and this interpreter is not.** A game
* written against akgl/ui.h re-declares its widgets inside a frame bracket sixty
* times a second and clay borrows the text pointers until the bracket closes; a
* BASIC program says `MENU 1, "START", "QUIT"` once on line 100 and expects it to
* still be there on line 900, several hundred frames later. So the entry points
* below are all *setters*: a verb pushes what it wants into storage the backend
* owns, and the backend re-declares the whole set once a frame from that. A
* BASIC string is transient by construction and is never what clay is handed.
*
* A runtime with no UI backend is the normal case -- the standalone driver has
* none -- so every UI verb refuses with AKBASIC_ERR_DEVICE rather than
* dereferencing a NULL vtable.
*/
#ifndef _AKBASIC_UI_H_
#define _AKBASIC_UI_H_
#include <akerror.h>
#include <akbasic/graphics.h>
#include <akbasic/types.h>
/**
* @brief How many menus a program may have up at once.
*
* Four rather than one because a pause menu over a title menu is an ordinary
* thing to want, and rather than eight because nothing needs eight and each one
* costs its item storage in the backend.
*/
#define AKBASIC_UI_MAX_MENUS 4
/** @brief How many HUD label slots there are. Eight, the way there are eight sprites. */
#define AKBASIC_UI_MAX_LABELS 8
/**
* @brief Most entries one MENU may carry.
*
* This is libakgl's AKGL_UI_MENU_MAX_ITEMS, restated rather than included: the
* core library must not include an akgl header, and a menu longer than a screen
* is a different widget anyway. src/ui_akgl.c asserts the two agree at compile
* time, so raising one without the other is a build error and not a surprise.
*/
#define AKBASIC_UI_MAX_MENU_ITEMS 16
/**
* @brief Where on the screen a HUD label pins itself.
*
* These are akgl_UiAnchor's five values and its numbering, so the adaptor maps
* them across without a table. **There is deliberately no top-centre or
* bottom-centre**, because libakgl has neither and this repository does not edit
* its dependencies to add one; see TODO.md.
*/
typedef enum
{
AKBASIC_UI_ANCHOR_TOP_LEFT = 0,
AKBASIC_UI_ANCHOR_TOP_RIGHT,
AKBASIC_UI_ANCHOR_BOTTOM_LEFT,
AKBASIC_UI_ANCHOR_BOTTOM_RIGHT,
AKBASIC_UI_ANCHOR_CENTER,
AKBASIC_UI_ANCHOR_LIMIT /* one past the last; not an anchor */
} akbasic_UiAnchor;
/**
* @brief Where the UI verbs draw.
*
* Every entry point is required. There is no optional one -- akbasic_GraphicsBackend
* has one only because `size` was added after hosts already existed, and this
* record has no such history.
*
* Text arguments are **copied** by the implementation, not borrowed. A BASIC
* string lives in a value from the per-line pool and is gone by the next
* statement, let alone the next frame.
*/
typedef struct akbasic_UiBackend
{
void *self;
/** Set the dialog panel's text. NULL or empty retires the panel. */
akerr_ErrorContext AKERR_NOIGNORE *(*dialog)(struct akbasic_UiBackend *self, const char *text);
/**
* Set one HUD label slot. NULL text retires the slot.
*
* `slot` is zero-based here; BASIC numbers them from one, and the verb does
* that subtraction. `anchor` is an akbasic_UiAnchor, already range-checked.
*/
akerr_ErrorContext AKERR_NOIGNORE *(*label)(struct akbasic_UiBackend *self, int slot, int anchor, const char *text);
/**
* Define one menu, replacing whatever it held. `count` 0 retires it.
*
* Defining a menu resets its highlight to the first entry and clears its
* activation latch: the program has just changed what the entries mean, so
* a selection index into the old list is not worth carrying over.
*/
akerr_ErrorContext AKERR_NOIGNORE *(*menu)(struct akbasic_UiBackend *self, int slot, const char *const *items, int count);
/**
* Read a menu's highlight and its activation latch.
*
* `selected` comes back zero-based. `activated` is true when the entry has
* been confirmed since the latch was last cleared, and `clear` clears it --
* which is what makes RMENU(n,1) a read-and-clear the way BUMP() is. A menu
* that was never defined reports 0 and false rather than failing: a program
* polling a menu it has retired has asked a reasonable question.
*/
akerr_ErrorContext AKERR_NOIGNORE *(*menu_state)(struct akbasic_UiBackend *self, int slot, int *selected, bool *activated, bool clear);
/**
* Set the one style every widget draws with.
*
* `padding` and `radius` are pixels. Restoring the library default is
* `style` with a NULL fill -- see akbasic_ui_style_default().
*/
akerr_ErrorContext AKERR_NOIGNORE *(*style)(struct akbasic_UiBackend *self, akbasic_Color *fill, akbasic_Color *edge, akbasic_Color *ink, double padding, double radius);
/** Retire every widget and restore the default style. NEW and CLR call this. */
akerr_ErrorContext AKERR_NOIGNORE *(*clear)(struct akbasic_UiBackend *self);
} akbasic_UiBackend;
/**
* @brief The UI verbs' own state, which lives on the runtime.
*
* Deliberately small. The widget text is in the backend, because the backend is
* what has to hand clay a pointer that outlives a frame; what is left here is
* what the *interpreter* needs to answer questions about, which is which menus
* exist and whether a GETMENU is holding.
*/
typedef struct
{
bool menudefined[AKBASIC_UI_MAX_MENUS]; /* MENU n has entries, so it is on screen */
bool waiting; /* a GETMENU is holding the program */
int waitmenu; /* ...on this menu, zero-based */
char variable[64]; /* ...to assign the chosen entry to */
} akbasic_UiState;
/* --- Internal API: exposed for the step loop and the tests. --- */
struct akbasic_Runtime;
/**
* @brief Reset the UI state to its power-on values: no menus, nothing holding.
* @param obj Object to initialize, inspect, or modify.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When `obj` is NULL.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_ui_state_init(akbasic_UiState *obj);
/**
* @brief Answer whether a GETMENU is still holding, assigning the entry if one was chosen.
*
* Called from akbasic_runtime_step() beside akbasic_input_service() and for the
* same reason: waiting must not mean blocking. While `*blocked` comes back true
* the step executes no source line, so the program stays where the GETMENU left
* it, and the sprite, audio and collision services -- which run *before* this --
* keep running underneath it.
*
* Two things release the hold rather than wedging the script, both of them the
* rules akbasic_input_service() already keeps: taking the UI device away, and
* retiring the menu that was being waited on. Either assigns 0.
*
* @param obj Object to initialize, inspect, or modify.
* @param blocked Output destination populated by the function; true while a GETMENU is unsatisfied.
* @return `NULL` on success, otherwise an error context owned by the caller.
* @throws AKERR_NULLPOINTER When either argument is NULL.
* @throws AKBASIC_ERR_UNDEFINED When the variable the GETMENU named has gone out of scope.
*/
akerr_ErrorContext AKERR_NOIGNORE *akbasic_ui_service(struct akbasic_Runtime *obj, bool *blocked);
#endif // _AKBASIC_UI_H_