11 KiB
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 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 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 insidesceneRender()— i.e. after the active scene's own 3D/game-world rendering, using an orthographic screen-space projection. Your own widgetxxxDraw()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, anduitab_tcache their label's measured width/height atInittime (an optimization — label text doesn't change after that point). CallingDrawbeforeInit, or mutating->labeldirectly instead of re-initializing, leaves stale layout.uibutton_t/uicheckbox_tdon't have this restriction.
Button
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
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
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
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
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.
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:
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.
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 outsideSCREEN.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.