256 lines
11 KiB
Markdown
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.
|