w
This commit is contained in:
+255
@@ -0,0 +1,255 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user