Files
2026-07-12 10:10:05 -05:00

256 lines
11 KiB
Markdown

# UI
Dusk's UI system (buttons, checkboxes, sliders, dropdowns, tabs, menus, focus
navigation) is a **C-only API**. It is not exposed to JerryScript — see
[SCRIPTING.md](SCRIPTING.md) for what scripts *can* touch. If you need a
script to open/react to a menu, wire it through a C callback or a game-side
flag scripts can poll; there's no bridge for this today.
## Mental model
There is no retained-mode UI tree, no automatic dispatch, no scissor/clip-rect
API. Every widget is a plain struct you own (usually as a global or
scene-owned variable). You call its `xxxInit(...)` once, then call its
`xxxDraw(widget, x, y)` yourself, every frame you want it visible, at whatever
screen position you choose. Nothing draws itself automatically except three
fixed system overlays (overscan bars, debug console, FPS counter) — see
[System overlays](#system-overlays-automatic) below.
### Where UI rendering happens in the frame
- `uiInit()` / `uiDispose()` run once, at engine startup/shutdown.
- `uiUpdate()` runs once per tick (drives focus-navigation input handling).
- `uiRender()` runs once per frame, called from inside `sceneRender()` — i.e.
**after** the active scene's own 3D/game-world rendering, using an
orthographic screen-space projection. Your own widget `xxxDraw()` calls
should happen around the same point — typically from your scene's render
callback, after world content, so UI draws on top.
## Widgets
Every widget follows the same shape: `xxxInit(widget, ...)` zeroes the struct
and sets its fields; `xxxDraw(const widget*, x, y) -> errorret_t` draws it at
that screen position.
> **Init before Draw.** `uislider_t`, `uidropdown_t`, and `uitab_t` cache
> their label's measured width/height at `Init` time (an optimization —
> label text doesn't change after that point). Calling `Draw` before `Init`,
> or mutating `->label` directly instead of re-initializing, leaves stale
> layout. `uibutton_t`/`uicheckbox_t` don't have this restriction.
### Button
```c
void uiButtonInit(uibutton_t *button, const char_t *label);
bool_t uiButtonIsHighlighted(const uibutton_t *button);
void uiButtonSetHighlighted(uibutton_t *button, bool_t highlighted);
errorret_t uiButtonDraw(const uibutton_t *button, float_t x, float_t y);
```
Draws `label` in red when highlighted, white otherwise.
### Checkbox
```c
void uiCheckboxInit(uicheckbox_t *checkbox, const char_t *label);
bool_t uiCheckboxIsChecked(const uicheckbox_t *checkbox);
void uiCheckboxSetChecked(uicheckbox_t *checkbox, bool_t checked);
void uiCheckboxToggle(uicheckbox_t *checkbox);
bool_t uiCheckboxIsHighlighted(const uicheckbox_t *checkbox);
void uiCheckboxSetHighlighted(uicheckbox_t *checkbox, bool_t highlighted);
errorret_t uiCheckboxDraw(const uicheckbox_t *checkbox, float_t x, float_t y);
```
Draws `"Y "`/`"N "` then the label.
### Slider
```c
typedef union { float_t f; int32_t i; } uislidervalue_t;
void uiSliderInitFloat(uislider_t*, const char_t *label,
float_t value, float_t min, float_t max, float_t step);
void uiSliderInitInt(uislider_t*, const char_t *label,
int32_t value, int32_t min, int32_t max, int32_t step);
float_t uiSliderGetFloat(const uislider_t*); // works for either type
int32_t uiSliderGetInt(const uislider_t*); // asserts type == INT
void uiSliderSetFloat(uislider_t*, float_t value); // asserts type == FLOAT, clamps
void uiSliderSetInt(uislider_t*, int32_t value); // asserts type == INT, clamps
void uiSliderStepUp(uislider_t*); // wraps to min past max
void uiSliderStepDown(uislider_t*); // wraps to max past min
float_t uiSliderGetRatio(const uislider_t*); // normalized 0..1
int32_t uiSliderGetStepCount(const uislider_t*); // 0 for float sliders
bool_t uiSliderIsHighlighted(const uislider_t*);
void uiSliderSetHighlighted(uislider_t*, bool_t highlighted);
errorret_t uiSliderDraw(const uislider_t*, float_t x, float_t y);
```
Draws label, a track, a fill proportional to the current ratio, discrete step
markers if it's an int slider with fewer than 10 steps, then the value as
text.
### Dropdown
```c
void uiDropdownInit(uidropdown_t *dropdown, const char_t *label,
const char_t *const *options, uint8_t optionCount,
uint8_t selectedIndex);
uint8_t uiDropdownGetSelectedIndex(const uidropdown_t *dropdown);
const char_t *uiDropdownGetSelectedOption(const uidropdown_t *dropdown);
void uiDropdownSetSelectedIndex(uidropdown_t *dropdown, uint8_t index);
void uiDropdownStepNext(uidropdown_t *dropdown); // wraps
void uiDropdownStepPrev(uidropdown_t *dropdown); // wraps
bool_t uiDropdownIsHighlighted(const uidropdown_t *dropdown);
void uiDropdownSetHighlighted(uidropdown_t *dropdown, bool_t highlighted);
errorret_t uiDropdownDraw(const uidropdown_t *dropdown, float_t x, float_t y);
```
`options` is a caller-owned array of strings that must outlive the dropdown
(it isn't copied). Draws `label` then `"< Option >"`.
### Tab
```c
void uiTabInit(uitab_t *tab, const char_t *label);
bool_t uiTabIsActive(const uitab_t *tab);
void uiTabSetActive(uitab_t *tab, bool_t active);
errorret_t uiTabDraw(const uitab_t *tab, float_t x, float_t y);
```
Draws a background box sized to the label (green if active, red if inactive)
with the label on top.
## Menus: assembling widgets into a navigable list
`uimenu_t` is the one aggregate widget — it owns an array of items (labels,
spacers, and any of the widgets above), lays them out in a grid, and wires
keyboard/gamepad navigation via the focus system for you.
```c
typedef enum {
UI_MENU_WIDGET_TYPE_NONE, UI_MENU_WIDGET_TYPE_LABEL,
UI_MENU_WIDGET_TYPE_SPACER, UI_MENU_WIDGET_TYPE_CHECKBOX,
UI_MENU_WIDGET_TYPE_BUTTON, UI_MENU_WIDGET_TYPE_TAB,
UI_MENU_WIDGET_TYPE_SLIDER, UI_MENU_WIDGET_TYPE_DROPDOWN,
} uimenuwidgettype_t;
void uiMenuInit(uimenu_t *menu, uimenuselectedcallback_t selected,
uimenuclosedcallback_t closed, uimenuchangedcallback_t changed);
void uiMenuSetItems(uimenu_t *menu, const uimenuitem_t *items,
uint8_t itemCount, uint8_t columns);
void uiMenuSetPosition(uimenu_t *menu, uint8_t x, uint8_t y); // focus cursor cell, not pixels
void uiMenuOpen(uimenu_t *menu); // pushes onto the focus stack
void uiMenuClose(uimenu_t *menu); // pops it
bool_t uiMenuIsActive(const uimenu_t *menu);
errorret_t uiMenuDraw(const uimenu_t *menu, float_t x, float_t y,
float_t width, float_t height);
```
- `selected(menu, index, item)` fires when the player presses accept on an item.
- `changed(menu, index, item)` fires when the highlighted item changes.
- `closed(menu)` fires when the menu is popped off the focus stack.
- LEFT/RIGHT on a highlighted slider/checkbox/dropdown adjusts its value in
place instead of moving focus off it (handled internally).
### Building a menu with the `MENU_*` macros
`uimenu.h` provides macros that cut the boilerplate of filling in a
`uimenuitem_t` array. They expand into statements using local variables named
`menu`, `menuIndex`, and `menuCapacity`, so use them together, inside one
function, starting with `MENU_BEGIN` and ending with `MENU_END`:
```c
static uimenuitem_t optionsItems[8];
static uimenu_t optionsMenu;
static const char_t *qualityOptions[] = { "Low", "Medium", "High" };
static void onOptionsSelected(
const uimenu_t *menu, const uint8_t index, const uimenuitem_t *item
) {
if(index == 4) uiMenuClose(&optionsMenu); // "Back" button
}
static void onOptionsClosed(const uimenu_t *menu) {
// e.g. return to the previous screen
}
void optionsMenuBuild(void) {
MENU_BEGIN(&optionsMenu, optionsItems, onOptionsSelected, onOptionsClosed, NULL);
MENU_LABEL("Options");
MENU_CHECKBOX("Fullscreen");
MENU_SLIDER_FLOAT("Volume", 0.8f, 0.0f, 1.0f, 0.05f);
MENU_DROPDOWN("Quality", qualityOptions, 3, 1);
MENU_BUTTON("Back");
MENU_END(optionsItems, 1);
}
// Once, when the menu screen becomes active:
uiMenuOpen(&optionsMenu);
// Every frame the menu should be visible:
uiMenuDraw(&optionsMenu, 20.0f, 20.0f, 200.0f, 100.0f);
// When leaving the menu screen:
uiMenuClose(&optionsMenu);
```
`MENU_LABEL`/`MENU_SPACER` force a row break and aren't focusable/selectable.
Every other `MENU_*` macro calls the matching widget's own `Init` for you.
> This example is constructed directly from the widget/menu API surface (all
> function and macro signatures above are verified against the source), but
> there's currently no real menu-building call site anywhere else in the
> engine to cross-check the *pattern* against — treat it as a starting point,
> not a copy of shipped code.
## Focus system: navigation underneath `uimenu`
If you're building a custom widget that needs keyboard/gamepad navigation
without going through `uimenu`, use `ui/focus/uifocus.h` directly. `uimenu`
is implemented entirely in terms of this API, so it's a reasonable reference.
```c
uifocusitem_t * uiFocusPush(
uint8_t cols, uint8_t rows,
uifocusitemcallback_t selected, // fires on accept
uifocusitemcallback_t changed, // fires on cursor move (and once immediately)
uifocusitemcallback_t closed, // fires on pop
uifocusitemdirectioncallback_t direction, // optional pre-empt of a direction press; NULL for default grid movement
void *user
);
void uiFocusPop(void);
void uiFocusPopItem(uifocusitem_t *item);
void uiFocusSetPosition(uifocusitem_t *item, uint8_t x, uint8_t y); // wraps
void uiFocusMoveDirection(uifocusitem_t *item, uifocusdirection_t dir);
```
`uiFocusUpdate()` runs automatically from `uiUpdate()` every tick — you don't
call it yourself. It reads `INPUT_ACTION_ACCEPT` (fires `selected`),
`INPUT_ACTION_CANCEL` (pops the stack), and the four directional actions
(with hold-to-repeat timing) to move the cursor within the topmost pushed
item. Only the topmost stack entry (max depth 8) receives input at a time —
opening a submenu means pushing a new focus item on top; closing it pops back
to the parent.
There's no separate "is this widget focused" query — "focused" is expressed
as the pushed item's current `(x, y)` cursor cell matching a given slot, which
is exactly how `uimenu`'s `changed` callback decides which item to highlight.
## System overlays (automatic)
Three small overlays are wired into a fixed internal list and draw themselves
every frame with no call needed from game code:
- **Overscan bars** (`ui/overlay/uicrop.h`) — draws opaque bars over the
screen area outside `SCREEN.scanX/scanY/scanWidth/scanHeight` (the
overscan-safe viewport). A no-op on platforms/configs where the scan area
already equals the full viewport. `UI_CROP.color` (default black) is the
only thing you'd normally touch here.
- **Debug console** (`ui/debug/uiconsole.h`) — draws console history when
visible.
- **FPS counter** (`ui/debug/uifps.h`) — draws a live FPS/frame-time readout.
None of these have a scissor/clip-rect equivalent for your own widgets —
there is no clipping API in this UI system; everything draws unclipped at
whatever position you give it.