diff --git a/README.md b/README.md index f91c4828..1d8ba38f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,7 @@ +# Documentation +- [Scripting](docs/SCRIPTING.md) — writing gameplay logic in JavaScript. +- [UI](docs/UI.md) — building buttons/menus/etc from engine/game C code. + # Building Each build target has different requirements. You can take a look at the git workflow to see how the builds are done for each target. In addition, for diff --git a/assets/engine.js b/assets/engine.js index 174718c7..d29e9f47 100644 --- a/assets/engine.js +++ b/assets/engine.js @@ -1,6 +1,7 @@ var Actions; var camera, cameraPosition; var cube, cubePosition, cubeRenderable, cubeMesh; +var ground, groundPosition, groundRenderable; // init() is called via scriptManagerCallGlobal(), which pumps the asset // system + job queue until any promise it returns settles - so it's safe @@ -20,6 +21,12 @@ async function init() { cubeMesh = Mesh.createCube(); cubeRenderable.mesh = cubeMesh; cubeRenderable.color = Color.red(); + + ground = new Entity(); + groundPosition = ground.add(POSITION); + groundRenderable = ground.add(RENDERABLE); + groundRenderable.mesh = Mesh.createCube(); + groundRenderable.color = Color.dark_gray(); } // Runs every frame (including dynamic/interpolation frames) - use for @@ -32,12 +39,6 @@ function update() { // Runs once per fixed timestep only - use for gameplay logic that should // be deterministic and independent of display refresh rate. function fixedUpdate() { - var move = 3.0 * TIME.delta; - if(Input.isDown(Actions.LEFT)) cubePosition.position.x -= move; - if(Input.isDown(Actions.RIGHT)) cubePosition.position.x += move; - if(Input.isDown(Actions.UP)) cubePosition.position.z += move; - if(Input.isDown(Actions.DOWN)) cubePosition.position.z -= move; - if(Input.pressed(Actions.ACCEPT)) cubePosition.position = new Vec3(0, 0, 0); } function deinit() { diff --git a/docs/SCRIPTING.md b/docs/SCRIPTING.md new file mode 100644 index 00000000..9626afe9 --- /dev/null +++ b/docs/SCRIPTING.md @@ -0,0 +1,379 @@ +# Scripting + +Dusk embeds [JerryScript](https://github.com/jerryscript-project/jerryscript) to +drive gameplay logic from JavaScript. The engine itself (rendering, physics, +asset loading, entity storage) is all C; scripts sit on top and manipulate that +state through a small set of bound objects. + +This document covers the JS-facing scripting API. **UI (buttons, sliders, +menus, etc.) is not exposed to scripts** — it's a separate, C-only API. See +[UI.md](UI.md) if you're building screens/menus from engine/game C code. + +## Lifecycle + +On startup, `engineInit()` loads and evaluates `assets/engine.js` as the main +script, then calls the global `init()` function if one is defined. From then +on, every engine tick calls (in order): + +1. `fixedUpdate()` — once per fixed timestep. Use this for gameplay logic that + must be deterministic and independent of display refresh rate (movement, + physics-adjacent input handling, etc). Skipped on interpolation/dynamic + frames when the build has variable-timestep rendering enabled. +2. `update()` — once per rendered frame, including interpolation frames. Use + this for smooth, purely presentational animation (nothing that needs to be + deterministic). + +On shutdown, `deinit()` is called once. + +All four hooks (`init`, `update`, `fixedUpdate`, `deinit`) are **optional** — +if a script doesn't define one, the engine simply skips it, no error. + +```js +function update() { + cubePosition.rotation.y += TIME.delta * 1.5; +} +``` + +### `async init()` and `include()` + +`init` (or any of the other hooks) can be declared `async` and use `await` +freely, including awaiting `include()` (see below), even though the engine +calls these functions synchronously from C with no external JS event loop. +When a hook returns a pending `Promise`, the engine keeps driving the asset +system and JerryScript's job queue until that promise settles before +continuing — so by the time e.g. `init()` "returns" from the engine's point of +view, everything it awaited has actually finished. + +```js +async function init() { + Actions = await include("input.js"); + // ... +} +``` + +If the awaited work throws/rejects, it surfaces as a C-level error. + +## Loading other scripts: `include(path)` + +```js +var Actions = await include("input.js"); +``` + +`include(path)` always returns a `Promise`. The named file is loaded and +evaluated once no matter how many times (or from how many different scripts) +you `include()` it — later calls for the same path are handed the same +in-flight/resolved promise rather than re-running the file. + +The included script communicates its result back by assigning to the bare +global `module`: + +```js +// input.js +Input.bind("w", INPUT_ACTION_UP); +// ... + +module = { + UP: INPUT_ACTION_UP, + DOWN: INPUT_ACTION_DOWN, + // ... +}; +``` + +Whatever `input.js` assigns to `module` becomes the resolved value of the +promise `include("input.js")` returned — that's what `Actions` ends up being +in the example above. If the included script throws, the promise rejects +instead. + +## Full example + +This is the actual shipped example content (`assets/engine.js` + +`assets/input.js`): + +```js +// input.js — binds physical buttons to abstract actions, then exports the +// action constants so other scripts don't need to know raw INPUT_ACTION_* names. +Input.bind("w", INPUT_ACTION_UP); +Input.bind("s", INPUT_ACTION_DOWN); +Input.bind("a", INPUT_ACTION_LEFT); +Input.bind("d", INPUT_ACTION_RIGHT); +Input.bind("space", INPUT_ACTION_ACCEPT); +Input.bind("escape", INPUT_ACTION_RAGEQUIT); + +if(typeof INPUT_GAMEPAD !== "undefined") { + Input.bind("gamepad_up", INPUT_ACTION_UP); + Input.bind("gamepad_down", INPUT_ACTION_DOWN); + Input.bind("gamepad_left", INPUT_ACTION_LEFT); + Input.bind("gamepad_right", INPUT_ACTION_RIGHT); + Input.bind("gamepad_a", INPUT_ACTION_ACCEPT); + Input.bind("gamepad_start", INPUT_ACTION_RAGEQUIT); +} + +module = { + UP: INPUT_ACTION_UP, + DOWN: INPUT_ACTION_DOWN, + LEFT: INPUT_ACTION_LEFT, + RIGHT: INPUT_ACTION_RIGHT, + ACCEPT: INPUT_ACTION_ACCEPT, + CANCEL: INPUT_ACTION_CANCEL, + RAGEQUIT: INPUT_ACTION_RAGEQUIT +}; +``` + +```js +// engine.js +var Actions; +var camera, cameraPosition; +var cube, cubePosition, cubeRenderable, cubeMesh; + +async function init() { + Actions = await include("input.js"); + + camera = new Entity(); + cameraPosition = camera.add(POSITION); + camera.add(CAMERA); + cameraPosition.position = new Vec3(3, 3, -6); + cameraPosition.lookAt(new Vec3(0, 0, 0)); + + cube = new Entity(); + cubePosition = cube.add(POSITION); + cubeRenderable = cube.add(RENDERABLE); + cubeMesh = Mesh.createCube(); + cubeRenderable.mesh = cubeMesh; + cubeRenderable.color = Color.red(); +} + +function update() { + cubePosition.rotation.y += TIME.delta * 1.5; + cubePosition.rotation.x += TIME.delta * 0.7; +} + +function fixedUpdate() { + var move = 3.0 * TIME.delta; + if(Input.isDown(Actions.LEFT)) cubePosition.position.x -= move; + if(Input.isDown(Actions.RIGHT)) cubePosition.position.x += move; + if(Input.isDown(Actions.UP)) cubePosition.position.z += move; + if(Input.isDown(Actions.DOWN)) cubePosition.position.z -= move; + if(Input.pressed(Actions.ACCEPT)) cubePosition.position = new Vec3(0, 0, 0); +} + +function deinit() { + cube.dispose(); + camera.dispose(); +} +``` + +## API reference + +### `TIME` + +Plain global object, live getters (read fresh engine state every access, not +snapshotted): + +| Property | Type | Description | +|---|---|---| +| `TIME.delta` | number | Seconds since the last frame. | +| `TIME.time` | number | Total elapsed engine time, in seconds. | + +### `PLATFORM` + +A single global string constant — the compile-time target name, e.g. +`"linux"`, `"psp"`, `"vita"`, `"dolphin"`. Individual platform builds may +inject additional platform-specific globals via their own +`modulePlatformPlatform()` hook; those aren't documented here since they vary +per target. + +### `Input` + +Static namespace (not constructible — there's no `new Input()`). + +| Method | Description | +|---|---| +| `Input.bind(buttonName, action)` | Binds a physical button/key (string, e.g. `"w"`, `"space"`, `"gamepad_up"`) to an abstract `INPUT_ACTION_*` constant. Many buttons can bind to the same action. Throws on an empty/unrecognized button name or invalid action. | +| `Input.isDown(action)` → boolean | Is the action currently held. | +| `Input.pressed(action)` → boolean | Action transitioned to down this frame. | +| `Input.released(action)` → boolean | Action transitioned to up this frame. | +| `Input.getValue(action)` → number | Current analog value for the action. | +| `Input.axis(negAction, posAction)` → number | Combined axis value from two opposing actions. | +| `Input.axis2D(negX, posX, negY, posY)` → `Vec2` | Combined 2D axis from four actions. | + +Global `INPUT_ACTION_*` constants (names are stable API; treat the numeric +values as opaque/build-specific): `INPUT_ACTION_UP`, `INPUT_ACTION_DOWN`, +`INPUT_ACTION_LEFT`, `INPUT_ACTION_RIGHT`, `INPUT_ACTION_ACCEPT`, +`INPUT_ACTION_CANCEL`, `INPUT_ACTION_PAUSE`, `INPUT_ACTION_RAGEQUIT`, +`INPUT_ACTION_CONSOLE`, `INPUT_ACTION_POINTERX`, `INPUT_ACTION_POINTERY`. + +Conditionally-defined boolean globals reflecting build capability — only +present at all if the corresponding input method is compiled in, so +feature-test with `typeof`, don't assume they exist: +`INPUT_KEYBOARD`, `INPUT_GAMEPAD`, `INPUT_POINTER`, `INPUT_TOUCH`. + +### `Vec2` / `Vec3` / `Vec4` + +`new Vec2(x?, y?)`, `new Vec3(x?, y?, z?)`, `new Vec4(x?, y?, z?, w?)` — all +components optional, default `0`. + +Common instance surface across all three: `.dot(other)`, `.length()`, +`.lengthSq()`, `.normalize()`, `.negate()`, `.add(other)`, `.sub(other)`, +`.scale(n)`, `.lerp(other, t)` — each of `add`/`sub`/`scale`/`negate`/ +`normalize`/`lerp` returns a **new** vector (non-mutating). `Vec3` additionally +has `.cross(other)` and `.distance(other)`; `Vec2` has `.distance(other)` too; +`Vec4` has neither `.cross()` nor `.distance()`. + +`Vec4` also has UV aliases over the same four floats: `.u0` (= `.x`), `.v0` +(= `.y`), `.u1` (= `.z`), `.v1` (= `.w`) — handy for texture-rect style code. + +All three have `.x`/`.y`(/`.z`/`.w`) get/set properties and a `.toString()` +like `"Vec3(1, 2, 3)"`. + +**"Vec3Ref" — live references.** Several engine properties (entity +`position`/`rotation`/`scale`, physics `velocity`, a mesh vertex's `position`) +return a vector-*like* object instead of a plain `Vec3`. It has the identical +`.x`/`.y`/`.z` surface, but reads/writes go straight into the underlying +native buffer — writing `.x` on `entity.position.position` immediately moves +the entity, no separate assignment needed. Anywhere the API expects a `Vec3` +argument, a Vec3Ref works too. You never construct one directly; you only +ever receive them from properties like the ones above. + +### `Mat4` + +`new Mat4()` — always constructs identity; no other constructor form. + +| Member | Description | +|---|---| +| `.mul(other)` → `Mat4` | `this * other`. | +| `.transpose()` → `Mat4` | | +| `.inverse()` → `Mat4` | | +| `.determinant()` → number | | +| `.mulVec3(vec3, w?)` → `Vec3` | `w` defaults to `1.0` (point); pass `0` for a direction. | +| `.mulVec4(vec4)` → `Vec4` | | +| `.translate(vec3)` → `Mat4` | Non-mutating — returns a translated copy. | +| `.scale(vec3)` → `Mat4` | Non-mutating — returns a scaled copy. | +| `Mat4.identity()` → `Mat4` | Static. | +| `Mat4.perspective(fov, aspect, near, far)` → `Mat4` | Static, all 4 args required. | +| `Mat4.lookAt(eye, center, up)` → `Mat4` | Static, all 3 args required `Vec3`s. | + +### `Color` + +`new Color(r?, g?, b?, a?)` — each an int `0..255`, default `255` (so +`new Color()` is opaque white). Properties `.r`/`.g`/`.b`/`.a` get/set. + +Named factories, each a zero-arg static returning a new opaque `Color` +(alpha `255` unless noted): `Color.black()`, `Color.white()`, `Color.red()`, +`Color.green()`, `Color.blue()`, `Color.yellow()`, `Color.cyan()`, +`Color.magenta()`, `Color.transparent()` (alpha 0), `Color.transparent_white()` +(alpha 0), `Color.transparent_black()` (alpha 0), `Color.gray()`, +`Color.light_gray()`, `Color.dark_gray()`, `Color.orange()`, `Color.purple()`, +`Color.brown()`, `Color.pink()`, `Color.lime()`, `Color.navy()`, +`Color.teal()`, `Color.cornflower_blue()`. + +`Color.rainbow(t?, speed?)` → `Color` — `t` defaults to `TIME.time * 4.0`; +produces a shifting rainbow color, useful for debug visuals. + +### `Mesh` + +`new Mesh(vertexCount)` — allocates an uninitialized CPU-side vertex buffer +(not yet uploaded to the GPU). + +| Member | Description | +|---|---| +| `.vertices` | Array of vertex wrappers, each with a `.position` (Vec3Ref, writes straight into that vertex). | +| `.vertexCount` | Read-only. | +| `.flush()` | Uploads to the GPU. First call initializes the GPU mesh; later calls re-upload the current vertex data — call this after editing `.vertices[i].position`. | +| `.dispose()` | Frees GPU + CPU resources. | + +Static engine-owned singletons (read-only, not something you dispose): +`Mesh.DEFAULT_CUBE`, `Mesh.DEFAULT_QUAD`, `Mesh.DEFAULT_SPHERE`, +`Mesh.DEFAULT_PLANE`, `Mesh.DEFAULT_CAPSULE`, `Mesh.DEFAULT_TRIPRISM`. + +Static factories (each builds and uploads a brand-new `Mesh`): + +| Factory | Notes | +|---|---| +| `Mesh.createCube(min?, max?)` | Both `Vec3`, default `(-0.5,-0.5,-0.5)`..`(0.5,0.5,0.5)`. | +| `Mesh.createQuad(minX?, minY?, maxX?, maxY?)` | Default `-0.5..0.5` both axes; UV fixed `0,0`–`1,1`. | +| `Mesh.createSphere(radius?, stacks?, sectors?)` | `radius` default `0.5`. | +| `Mesh.createPlane(width?, height?)` | Defaults `1.0`/`1.0`; XZ-aligned, centered at origin. | +| `Mesh.createCapsule(radius?, halfHeight?, capRings?, sectors?)` | Defaults `0.5`, `0.5`. | +| `Mesh.createTriPrism(x0, y0, x1, y1, x2, y2, minZ, maxZ)` | All 8 args required — a triangular cross-section extruded along Z. | + +### `Entity` and components + +```js +var e = new Entity(); +var pos = e.add(POSITION); +``` + +`new Entity()` allocates an entity. `.id` is the read-only numeric engine ID. +`.add(TYPE)` adds a component and returns its wrapper (`TYPE` is one of the +constants below). `.dispose()` removes the entity and all its components. + +Component-type constants: `POSITION`, `CAMERA`, `RENDERABLE`, `PHYSICS`, +`TRIGGER`. Each entity also exposes a lowercase getter that returns the +existing wrapper if the component is present, or `undefined` if not (it does +**not** add the component — use `.add()` for that): `entity.position`, +`entity.camera`, `entity.renderable`, `entity.physics`, `entity.trigger`. + +#### `entity.add(POSITION)` → position component + +| Member | Description | +|---|---| +| `.position` | Vec3Ref. Writing rebuilds the transform automatically. | +| `.rotation` | Vec3Ref, Euler angles. Same rebuild-on-write behavior. | +| `.scale` | Vec3Ref. Same rebuild-on-write behavior. | +| `.parent` | Get/set another position-component wrapper, or `null` to clear parenting. | +| `.lookAt(target, up?)` | `target` a `Vec3`; `up` defaults to `(0,1,0)`. | + +#### `entity.add(CAMERA)` → camera component + +| Member | Description | +|---|---| +| `.zNear` / `.zFar` | Numbers. | +| `.fov` | Only meaningful when `projectionType` is `CAMERA_TYPE_PERSPECTIVE`; otherwise get returns `undefined` and set is a no-op. | +| `.projectionType` | `CAMERA_TYPE_PERSPECTIVE` or `CAMERA_TYPE_ORTHOGRAPHIC`. | +| `.orthoTop` / `.orthoBottom` / `.orthoLeft` / `.orthoRight` | Only meaningful in orthographic mode, same undefined/no-op rule otherwise. | + +#### `entity.add(RENDERABLE)` → renderable component + +| Member | Description | +|---|---| +| `.type` | `ENTITY_RENDERABLE_TYPE_MATERIAL`, `_SPRITEBATCH`, or `_CALLBACK`. | +| `.mesh` | Get/set a `Mesh` instance or a `Mesh.DEFAULT_*` singleton. | +| `.color` | Get/set a `Color` instance (throws if given something else). | +| `.addSprite({ min?, max?, uvMin?, uvMax? })` | Adds a sprite to this renderable's sprite batch; all fields optional, default zero. | +| `.clearSprites()` | Clears the sprite batch. | +| `.setCallback(fn?)` | Switches to `ENTITY_RENDERABLE_TYPE_CALLBACK` and calls `fn()` on every render of this entity. Omit/pass non-function to clear. Exceptions inside `fn` surface as a C error. | + +#### `entity.add(PHYSICS)` → physics component + +| Member | Description | +|---|---| +| `.velocity` | Vec3Ref, plain (no rebuild-on-write). | +| `.onGround` | Read-only boolean. | +| `.bodyType` | `PHYSICS_BODY_STATIC`, `PHYSICS_BODY_DYNAMIC`, `PHYSICS_BODY_KINEMATIC`. | +| `.applyImpulse(vec3)` | Adds to velocity. No-op on static bodies. | +| `.setShapeCube(halfExtents)` | `halfExtents` a `Vec3`. | +| `.setShapeSphere(radius)` | Number. | +| `.setShapeCapsule(radius, halfHeight)` | Two numbers. | +| `.setShapePlane(normal, distance)` | `Vec3` + number. | + +Shape-type constants (for reading `.type` on the underlying shape, not for +`.bodyType`): `PHYSICS_SHAPE_CUBE`, `PHYSICS_SHAPE_SPHERE`, +`PHYSICS_SHAPE_CAPSULE`, `PHYSICS_SHAPE_PLANE`. + +#### `entity.add(TRIGGER)` → trigger component + +| Member | Description | +|---|---| +| `.min` / `.max` | Plain `Vec3` values (copies, not live refs). | +| `.setBounds(min, max)` | Sets both at once. | +| `.contains(point)` → boolean | `point` a `Vec3`. | + +## Not yet available to scripts + +The following C modules exist and are fully implemented, but aren't currently +wired into script registration (`moduleRegister()` in +`src/dusk/script/module/module.h`), so none of these globals exist in a +script today: `Screen`, `SpriteBatch`, `Text`, `Scene`, `Easing`, `Console`, +`Engine`. If you need one of these from a script, it needs to be registered +in `moduleRegister()` first — see the existing entries there and the modules +under `src/dusk/script/module/` for the pattern to follow. diff --git a/docs/UI.md b/docs/UI.md new file mode 100644 index 00000000..5e5c0ef8 --- /dev/null +++ b/docs/UI.md @@ -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.