Files
dusk/docs/UI.md
T
2026-07-12 10:10:05 -05:00

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 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

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 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.