w
This commit is contained in:
@@ -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.
|
||||
+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