diff --git a/.claude/build.md b/.claude/build.md index 8510b39e..f1f5133e 100644 --- a/.claude/build.md +++ b/.claude/build.md @@ -75,6 +75,37 @@ if(DUSK_TARGET_SYSTEM STREQUAL "psp") endif() ``` +## Embedding JS files (`dusk_embed_js`) + +Source: `cmake/modules/duskjs2c.cmake` + +The `dusk_embed_js()` CMake function embeds a `.js` source file as a +C string constant in a generated header. It is used to ship script +module code alongside the engine binary without a separate file load. + +```cmake +dusk_embed_js( + TARGET ${DUSK_LIBRARY_TARGET_NAME} + JS_FILE path/to/mymodule.js + # NAME is optional; defaults to uppercase stem + "_JS" + # e.g. "mymodule.js" -> "MYMODULE_JS" + NAME MY_CUSTOM_NAME +) +``` + +The generated header is placed in +`${DUSK_GENERATED_HEADERS_DIR}/_js.h` and defines: + +```c +static const char MY_CUSTOM_NAME[] = "... js source ..."; +static const size_t MY_CUSTOM_NAME_SIZE = sizeof(MY_CUSTOM_NAME) - 1; +``` + +Under the hood it calls `python -m tools.js2c` from the repo root. +The header is generated at build time; include it in the `.c` file +that registers the JS module, then pass `NAME` and `NAME_SIZE` to +`jerry_eval()` (or the equivalent module load helper). + ## Tests - Test files live in `test/` mirroring the `src/dusk/` structure. diff --git a/.claude/console.md b/.claude/console.md new file mode 100644 index 00000000..2739df81 --- /dev/null +++ b/.claude/console.md @@ -0,0 +1,92 @@ +# Console + +Source: `src/dusk/console/` + +--- + +## Overview + +The console is a lightweight in-engine debug overlay. It maintains a +fixed-size ring buffer of text lines and can render them to the screen +as an overlay. On Linux (where `DUSK_CONSOLE_POSIX` is defined), it +also reads commands from stdin on a background thread, allowing +interactive input during development without pausing the game loop. + +--- + +## Limits + +| Constant | Value | +|----------|-------| +| `CONSOLE_LINE_MAX` | 512 chars per line | +| `CONSOLE_HISTORY_MAX` | 16 lines in the ring buffer | +| `CONSOLE_EXEC_BUFFER_MAX` | 32 pending execution slots | + +--- + +## Global state + +```c +typedef struct { + char_t line[CONSOLE_HISTORY_MAX][CONSOLE_LINE_MAX]; // ring buffer + bool_t visible; + + #ifdef DUSK_CONSOLE_POSIX + threadmutex_t printMutex; // guards ring buffer on POSIX targets + #endif +} console_t; + +extern console_t CONSOLE; +``` + +--- + +## API + +```c +void consoleInit(void); + +// printf-style print into the ring buffer. +// Thread-safe on POSIX (uses printMutex). +void consolePrint(const char_t *message, ...); + +// Process any queued script input lines. Must be called from the +// main thread once per frame. +void consoleUpdate(void); + +// Draw the ring buffer as an overlay in UI space. +errorret_t consoleDraw(void); + +void consoleDispose(void); +``` + +--- + +## POSIX stdin mode (`DUSK_CONSOLE_POSIX`) + +On Linux only (`DUSK_CONSOLE_POSIX`), the console launches a background +thread that polls stdin using `poll()` at 75 ms intervals +(`CONSOLE_POSIX_POLL_RATE`). Lines typed at the terminal are queued +and dispatched on the main thread by `consoleUpdate()`. + +This allows typing commands or JS expressions into the running game +without blocking the render loop. The ring buffer is protected by +`CONSOLE.printMutex` so `consolePrint` is safe to call from either +thread. + +POSIX mode is not available on PSP, Vita, or Dolphin targets. + +--- + +## Notes + +- `CONSOLE.visible` controls whether `consoleDraw` renders anything. + Toggle it from a debug keybind or always set it to `true` in + development builds. +- `consolePrint` wraps lines at `CONSOLE_LINE_MAX` -- long messages are + truncated. Use multiple calls for long output. +- The console is distinct from the error system (`errorThrow` / + `errorPrint`). Use `consolePrint` for diagnostic output; use + `errorThrow` for recoverable failures. +- Log calls (`logDebug`, `logError`) go to the platform's debug output + (stdout/stderr), not to the console ring buffer. diff --git a/.claude/display-color.md b/.claude/display-color.md new file mode 100644 index 00000000..08a568f4 --- /dev/null +++ b/.claude/display-color.md @@ -0,0 +1,88 @@ +# Display -- Color + +Source: `build/generated/display/color.h` (generated at build time) + +--- + +## Overview + +`color.h` is a generated header. It is not hand-edited -- the source +lives in the CMake generator that produces it from platform configuration. +Include it via `"display/color.h"`. + +--- + +## Types + +```c +typedef float_t colorchannelf_t; // float channel [0.0, 1.0] +typedef uint8_t colorchannel8_t; // 8-bit channel [0, 255] + +typedef struct { colorchannelf_t r, g, b; } color3f_t; +typedef struct { colorchannelf_t r, g, b, a; } color4f_t; +typedef struct { colorchannel8_t r, g, b; } color3b_t; +typedef struct { colorchannel8_t r, g, b, a; } color4b_t; + +typedef color4b_t color_t; // default: RGBA uint8 +``` + +`color_t` is always `color4b_t` -- four `uint8_t` channels. +Most engine APIs (text rendering, UI, mesh vertices) take `color_t`. + +--- + +## Constructors + +```c +color3f(r, g, b) // float RGB +color4f(r, g, b, a) // float RGBA +color3b(r, g, b) // uint8 RGB +color4b(r, g, b, a) // uint8 RGBA +color(r, g, b, a) // alias for color4b +colorHex(0xRRGGBBAA) // unpack 32-bit hex into color4b +``` + +--- + +## Predefined constants + +Each named colour comes in four variants: `_4B` (uint8 RGBA, default), +`_3B` (uint8 RGB), `_4F` (float RGBA), `_3F` (float RGB). The bare +name (e.g. `COLOR_BLACK`) always resolves to the `_4B` variant. + +| Constant | RGBA (uint8) | +|----------|-------------| +| `COLOR_BLACK` | 0, 0, 0, 255 | +| `COLOR_WHITE` | 255, 255, 255, 255 | +| `COLOR_RED` | 255, 0, 0, 255 | +| `COLOR_GREEN` | 0, 255, 0, 255 | +| `COLOR_BLUE` | 0, 0, 255, 255 | +| `COLOR_YELLOW` | 255, 255, 0, 255 | +| `COLOR_CYAN` | 0, 255, 255, 255 | +| `COLOR_MAGENTA` | 255, 0, 255, 255 | +| `COLOR_ORANGE` | 255, 165, 0, 255 | +| `COLOR_PURPLE` | 127, 0, 127, 255 | +| `COLOR_GRAY` | 127, 127, 127, 255 | +| `COLOR_LIGHT_GRAY` | 191, 191, 191, 255 | +| `COLOR_DARK_GRAY` | 63, 63, 63, 255 | +| `COLOR_BROWN` | 153, 102, 51, 255 | +| `COLOR_PINK` | 255, 191, 204, 255 | +| `COLOR_LIME` | 191, 255, 0, 255 | +| `COLOR_NAVY` | 0, 0, 127, 255 | +| `COLOR_TEAL` | 0, 127, 127, 255 | +| `COLOR_CORNFLOWER_BLUE` | 99, 147, 237, 255 | +| `COLOR_TRANSPARENT` | 0, 0, 0, 0 | +| `COLOR_TRANSPARENT_WHITE` | 255, 255, 255, 0 | +| `COLOR_TRANSPARENT_BLACK` | 0, 0, 0, 0 | + +--- + +## Notes + +- `color_t` uses premultiplied-friendly `uint8_t` channels for + compatibility with both OpenGL texture uploads and GX on Dolphin. +- For shader uniforms that expect float colours, convert manually: + `(float_t)col.r / 255.0f` etc. +- The `COLOR_SCRIPT` macro is a C string containing the JS Color class + static methods. It is concatenated into the embedded JS runtime + during module init (see `.claude/script.md`). diff --git a/.claude/display-mesh.md b/.claude/display-mesh.md new file mode 100644 index 00000000..ad69c494 --- /dev/null +++ b/.claude/display-mesh.md @@ -0,0 +1,240 @@ +# Display -- Mesh + +Source: `src/dusk/display/mesh/` + +See also: `.claude/display-spritebatch.md`, `.claude/display-shader.md` + +--- + +## Overview + +The mesh system wraps platform-specific GPU geometry buffers behind a +common `mesh_t` type. Geometry is described as an array of +`meshvertex_t` values and a primitive type. The platform layer +(`meshplatform.h`) provides the concrete buffer and draw implementation +(VBO on OpenGL, display list or immediate-mode on Dolphin GX). + +--- + +## Vertex format (`meshvertex.h`) + +```c +typedef struct { + #if MESH_ENABLE_COLOR + color_t color; // optional per-vertex colour (disabled by default) + #endif + float_t uv[2]; // texture coordinates (U, V) + float_t pos[3]; // position (X, Y, Z) +} meshvertex_t; +``` + +`MESH_ENABLE_COLOR` is a compile-time flag (default 0). Enable it with +`-DMESH_ENABLE_COLOR=1` at CMake configure time if per-vertex colouring +is needed; be aware this changes the struct size and breaks binary +compatibility with pre-built mesh data. + +--- + +## Core API (`mesh.h`) + +```c +// Platform alias -- do not use meshplatform_t directly. +typedef meshplatform_t mesh_t; +typedef meshprimitivetypeplatform_t meshprimitivetype_t; + +// Upload vertices to the GPU. Must be called from the main thread. +errorret_t meshInit( + mesh_t *mesh, + const meshprimitivetype_t primitiveType, + const int32_t vertexCount, + const meshvertex_t *vertices +); + +// Flush a range of updated vertices to the GPU (modern targets only). +// vertexCount == -1 flushes all vertices. +errorret_t meshFlush( + mesh_t *mesh, + const int32_t vertexOffset, + const int32_t vertexCount +); + +// Draw the mesh. vertexCount == -1 draws all vertices. +errorret_t meshDraw( + const mesh_t *mesh, + const int32_t vertexOffset, + const int32_t vertexCount +); + +// Compute the axis-aligned bounding box. +void meshGetBounds(const mesh_t *mesh, vec3 outMin, vec3 outMax); + +int32_t meshGetVertexCount(const mesh_t *mesh); + +errorret_t meshDispose(mesh_t *mesh); +``` + +On constrained targets (GameCube/Wii) `meshFlush` is a no-op -- the +hardware reads vertices from main memory directly. Always call it on +desktop/mobile targets after modifying vertex data. + +--- + +## Primitive generators + +Each generator provides: +- A `*Buffer(vertices, ...)` function that writes into a caller-supplied + `meshvertex_t` array (no allocation). +- A global pre-built `*_MESH_SIMPLE` singleton + `*_MESH_SIMPLE_VERTICES` + array initialised at engine startup (for common one-off uses). + +All generators use CCW winding and `MESH_PRIMITIVE_TYPE_TRIANGLES`. + +### Quad (`quad.h`) + +```c +#define QUAD_VERTEX_COUNT 6 // two triangles + +// 2D quad in XY plane: +void quadBuffer( + meshvertex_t *vertices, + const float_t minX, const float_t minY, + const float_t maxX, const float_t maxY, + const float_t u0, const float_t v0, + const float_t u1, const float_t v1 +); + +// 3D quad using full vec3 min/max: +void quadBuffer3D( + meshvertex_t *vertices, + const vec3 min, const vec3 max, + const vec2 uvMin, const vec2 uvMax +); + +extern mesh_t QUAD_MESH_SIMPLE; +``` + +The SpriteBatch is built on `quadBuffer3D` internally. + +### Cube (`cube.h`) + +```c +#define CUBE_VERTEX_COUNT 36 // 6 faces x 6 vertices + +// Axis-aligned box from min to max: +void cubeBuffer( + meshvertex_t *vertices, + const vec3 min, const vec3 max +); + +extern mesh_t CUBE_MESH_SIMPLE; // unit cube (0,0,0) to (1,1,1) +``` + +### Plane (`plane.h`) + +```c +#define PLANE_VERTEX_COUNT 6 + +typedef enum { + PLANE_AXIS_XY, // flat in XY, normal along +Z (billboard / wall face) + PLANE_AXIS_XZ, // flat in XZ, normal along +Y (ground / floor) + PLANE_AXIS_YZ, // flat in YZ, normal along +X (side wall) +} planeaxis_t; + +void planeBuffer( + meshvertex_t *vertices, + const planeaxis_t axis, + const vec3 min, const vec3 max, + const vec2 uvMin, const vec2 uvMax +); + +extern mesh_t PLANE_MESH_SIMPLE; // unit XZ plane (0,0,0) to (1,0,1) +``` + +### Sphere (`sphere.h`) + +```c +#define SPHERE_STACKS 8 +#define SPHERE_SECTORS 16 +#define SPHERE_VERTEX_COUNT (SPHERE_STACKS * SPHERE_SECTORS * 6) + +void sphereBuffer( + meshvertex_t *vertices, + const vec3 center, + const float_t radius, + const int32_t stacks, + const int32_t sectors +); + +extern mesh_t SPHERE_MESH_SIMPLE; // unit sphere centered at (0,0,0), r=0.5 +``` + +### Capsule (`capsule.h`) + +```c +#define CAPSULE_CAP_RINGS 4 +#define CAPSULE_SECTORS 16 +// Total vertex count = (2 * capRings + 1) * sectors * 6 + +void capsuleBuffer( + meshvertex_t *vertices, + const vec3 center, + const float_t radius, + const float_t halfHeight, // half-height of the cylindrical section only + const int32_t capRings, + const int32_t sectors +); + +extern mesh_t CAPSULE_MESH_SIMPLE; // r=0.5, halfHeight=0.5 (total h=2.0) +``` + +The long axis is always Y. This mirrors the physics capsule body (see +`.claude/physics.md`). + +### Triangular prism (`triprism.h`) + +```c +#define TRIPRISM_VERTEX_COUNT 24 + +// Cross-section triangle defined by three 2D points in XY; +// extruded along Z from minZ to maxZ. +void triPrismBuffer( + meshvertex_t *vertices, + const float_t x0, const float_t y0, + const float_t x1, const float_t y1, + const float_t x2, const float_t y2, + const float_t minZ, const float_t maxZ +); + +extern mesh_t TRIPRISM_MESH_SIMPLE; +// Unit prism: triangle (0,0),(1,0),(0.5,1) extruded z=0 to z=1. +``` + +--- + +## Custom dynamic mesh + +If you need to update geometry each frame (e.g. a procedural mesh): + +```c +static meshvertex_t myVerts[MY_VERT_COUNT]; +static mesh_t myMesh; + +// On init: +// Fill myVerts, then: +errorChain(meshInit(&myMesh, MESH_PRIMITIVE_TYPE_TRIANGLES, + MY_VERT_COUNT, myVerts)); + +// Each frame (after modifying myVerts): +errorChain(meshFlush(&myMesh, 0, -1)); +errorChain(meshDraw(&myMesh, 0, -1)); +``` + +--- + +## Notes + +- `meshInit` must be called on the **main thread** (GPU upload). +- `meshFlush` is required on OpenGL targets when vertices change + after init. It is a no-op on Dolphin. +- All `_MESH_SIMPLE` globals are initialised during engine startup -- + do not call `meshInit` on them manually. diff --git a/.claude/display-spritebatch.md b/.claude/display-spritebatch.md new file mode 100644 index 00000000..ec9008be --- /dev/null +++ b/.claude/display-spritebatch.md @@ -0,0 +1,133 @@ +# Display -- SpriteBatch + +Source: `src/dusk/display/spritebatch/` + +See also: `.claude/display-mesh.md`, `.claude/display-texture.md` + +--- + +## Overview + +The SpriteBatch is the primary 2D rendering primitive. It accumulates +axis-aligned quads (sprites) into a shared vertex buffer and draws them +in batches. All 2D rendering in the engine -- UI frames, text, tilemaps, +HUD -- goes through the global `SPRITEBATCH`. + +The batch flushes automatically when the per-flush limit is reached, or +explicitly via `spriteBatchFlush()`. + +--- + +## Limits + +| Constant | Value | Meaning | +|----------|-------|---------| +| `SPRITEBATCH_SPRITES_MAX` | 512 | Total sprites in the vertex buffer | +| `SPRITEBATCH_FLUSH_COUNT` | 16 | Number of auto-flush segments | +| `SPRITEBATCH_SPRITES_MAX_PER_FLUSH` | 32 | Sprites per auto-flush segment | +| `SPRITEBATCH_VERTEX_COUNT` | 3072 | Total vertices (512 * QUAD_VERTEX_COUNT) | + +--- + +## Sprite structure + +```c +typedef struct { + vec3 min; // minimum XYZ corner of the quad in world/screen space + vec3 max; // maximum XYZ corner of the quad in world/screen space + vec2 uvMin; // minimum UV (top-left in [0,1] texture space) + vec2 uvMax; // maximum UV (bottom-right in [0,1] texture space) +} spritebatchsprite_t; +``` + +Z in `min` and `max` controls draw depth (further from camera = higher Z +in a typical orthographic setup). For flat 2D, set `min.z = max.z = 0`. + +--- + +## SpriteBatch struct + +```c +typedef struct { + mesh_t mesh; + int32_t spriteCount; + int32_t spriteFlush; + shader_t *shader; + shadermaterial_t material; +} spritebatch_t; + +extern spritebatch_t SPRITEBATCH; +extern meshvertex_t SPRITEBATCH_VERTICES[SPRITEBATCH_VERTEX_COUNT]; +``` + +`SPRITEBATCH_VERTICES` is a separate global (not embedded in the struct) +for platform alignment requirements. + +--- + +## API + +```c +errorret_t spriteBatchInit(); +errorret_t spriteBatchDispose(); + +// Clear the buffer and reset state. Call before starting a new batch. +void spriteBatchClear(); + +// Append sprites to the buffer. Flushes automatically when the per-flush +// segment fills. shader + material are used on the next flush. +errorret_t spriteBatchBuffer( + const spritebatchsprite_t *sprites, + const uint32_t count, + shader_t *shader, + const shadermaterial_t material +); + +// Upload and draw all buffered sprites. Binds shader and applies +// material if set. No-op if the buffer is empty. +errorret_t spriteBatchFlush(); +``` + +--- + +## Typical usage + +```c +// Beginning of a 2D render pass: +spriteBatchClear(); + +// Build sprites (e.g. via tilesetTileGetUV, then fill spritebatchsprite_t): +spritebatchsprite_t s; +glm_vec3_copy((vec3){ x, y, 0 }, s.min); +glm_vec3_copy((vec3){ x + w, y + h, 0 }, s.max); +glm_vec2_copy(uvMin, s.uvMin); +glm_vec2_copy(uvMax, s.uvMax); + +shadermaterial_t mat = { .unlit = { .texture = myTexture } }; +spriteBatchBuffer(&s, 1, myShader, mat); + +// End of pass -- flush remaining sprites: +spriteBatchFlush(); +``` + +--- + +## Relationship to other systems + +- **Text rendering** (`textDraw`) internally calls `spriteBatchBuffer` + for each glyph and requires a final `spriteBatchFlush()` after drawing. +- **UI frames** (`uiFrameDraw`) push 9 quads to the batch without + flushing -- the caller or `uitextboxDraw` is responsible for the flush. +- **ECS renderables** of type `ENTITY_RENDERABLE_TYPE_SPRITEBATCH` are + drawn via the spritebatch in the scene render pipeline. + +--- + +## Notes + +- `spriteBatchBuffer` changes the batch's `shader` and `material` fields. + If you mix different shaders or textures in one batch, add an explicit + `spriteBatchFlush()` call between groups to avoid draws with the wrong + material. +- The vertex buffer is a static global -- `SPRITEBATCH_VERTICES` must + not be written from multiple threads. diff --git a/.claude/display-text.md b/.claude/display-text.md new file mode 100644 index 00000000..d83dee82 --- /dev/null +++ b/.claude/display-text.md @@ -0,0 +1,115 @@ +# Display -- Text Rendering + +Source: `src/dusk/display/text/` + +See also: `.claude/display-spritebatch.md`, `.claude/display-texture.md` + +--- + +## Overview + +Text rendering is layered on top of the SpriteBatch. Each character +maps to a glyph tile in a bitmap font atlas; `textDraw` builds the +corresponding `spritebatchsprite_t` values and pushes them to the +global `SPRITEBATCH`. The caller is responsible for flushing the batch. + +--- + +## Font type (`font.h`) + +```c +typedef struct { + texture_t *texture; // glyph atlas texture + tileset_t *tileset; // grid describing glyph size + UV layout +} font_t; +``` + +Both pointers are caller-owned. The text system does not allocate or +free them. + +```c +extern font_t FONT_DEFAULT; +``` + +`FONT_DEFAULT` is the engine's built-in bitmap font. It is initialised +during `textInit()` and available for the engine lifetime. + +--- + +## Character range + +```c +#define TEXT_CHAR_START '!' // ASCII 33 +``` + +The glyph atlas begins at `'!'` (ASCII 33). Characters below this value +-- space, control characters -- are handled specially: + +- `' '` (space) advances the cursor by one tile width without drawing. +- Characters below `TEXT_CHAR_START` other than space are skipped. + +--- + +## API (`text.h`) + +```c +// Initialises the text system and FONT_DEFAULT. +errorret_t textInit(void); + +// Disposes of the text system. +errorret_t textDispose(void); + +// Draw a null-terminated string at (x, y) in screen/world space. +// Pushes sprites to SPRITEBATCH. Caller must call spriteBatchFlush() +// after all text has been drawn. +errorret_t textDraw( + const float_t x, + const float_t y, + const char_t *text, + const color_t color, + font_t *font +); + +// Measure the bounding box of a string without drawing it. +void textMeasure( + const char_t *text, + const font_t *font, + int32_t *outWidth, + int32_t *outHeight +); + +// Low-level: build a single glyph sprite at position pos. +// Returns a spritebatchsprite_t ready for spriteBatchBuffer. +spritebatchsprite_t textGetSprite( + const vec2 pos, + const char_t c, + const font_t *font +); +``` + +--- + +## Typical usage + +```c +// Inside a render callback: +errorChain(textDraw(10.0f, 10.0f, "Hello", COLOR_WHITE, &FONT_DEFAULT)); +errorChain(spriteBatchFlush()); +``` + +If you are also drawing UI frames or other sprites in the same pass, +batch all the `textDraw` and `spriteBatchBuffer` calls first, then call +`spriteBatchFlush()` once at the end. + +--- + +## Notes + +- Text coordinates are in the same space as the scene render (screen + space for UI, or world space if placed in the scene). +- `textMeasure` returns pixel dimensions based on the font's + `tileset.tileWidth` and `tileset.tileHeight`. Use it to centre or + right-align text before drawing. +- For UI-attached text (dialogue, labels), prefer the `uitextbox_t` + system which handles word-wrap and paging automatically + (see `.claude/ui.md`). diff --git a/.claude/display.md b/.claude/display.md index 3b52263d..0899b864 100644 --- a/.claude/display.md +++ b/.claude/display.md @@ -17,3 +17,7 @@ Dolphin (GX) implementation in `src/duskdolphin/`. | Screen size modes, framebuffer, screen | `.claude/display-core.md` | | Texture, tileset, font | `.claude/display-texture.md` | | Shader, shader material, display state | `.claude/display-shader.md` | +| Mesh, vertex format, primitive generators | `.claude/display-mesh.md` | +| SpriteBatch (2D quad renderer) | `.claude/display-spritebatch.md` | +| Text rendering, font, FONT_DEFAULT | `.claude/display-text.md` | +| Color types, macros, named constants | `.claude/display-color.md` | diff --git a/.claude/ui.md b/.claude/ui.md new file mode 100644 index 00000000..45a7d12d --- /dev/null +++ b/.claude/ui.md @@ -0,0 +1,226 @@ +# UI System + +Source: `src/dusk/ui/` + +See also: `.claude/display-spritebatch.md`, `.claude/display-text.md`, +`.claude/display-color.md` + +--- + +## Overview + +The UI system renders overlaid interface elements on top of the scene +each frame. It is called by the engine after the scene render pipeline +and before the screen unbind -- game code does not drive it directly. + +All UI elements render through the global SpriteBatch. + +--- + +## Lifecycle + +```c +extern ui_t UI; + +errorret_t uiInit(void); +void uiUpdate(void); +errorret_t uiRender(void); +void uiDispose(void); +``` + +`uiUpdate` is called each game tick; `uiRender` is called each render +tick. The engine calls both automatically -- do not call them from +game scripts. + +--- + +## Element registration (`uielement.h`) + +```c +typedef struct { + uielementtype_t type; // UI_ELEMENT_TYPE_NULL or UI_ELEMENT_TYPE_NATIVE + errorret_t (*draw)(); // draw callback +} uielement_t; + +extern uielement_t UI_ELEMENTS[]; // registered element array +``` + +`uiRender` iterates `UI_ELEMENTS` and calls each element's `draw` +callback. Elements register themselves during `uiInit`. + +--- + +## Elements + +### uiframe_t -- 9-slice border (`uiframe.h`) + +A resizable bordered panel rendered using 9-slice (9-patch) technique +from a 3x3 tileset. + +```c +typedef struct { + tileset_t tileset; + texture_t *texture; +} uiframe_t; + +errorret_t uiFrameInit(uiframe_t *frame); + +errorret_t uiFrameDraw( + const uiframe_t *frame, + const float_t x, + const float_t y, + const float_t width, + const float_t height +); + +void uiFrameDispose(uiframe_t *frame); // does not dispose texture +``` + +`uiFrameDraw` pushes 9 quads (corners + edges + centre) to the +SpriteBatch without flushing. The caller must call `spriteBatchFlush()` +after all sprites for a draw pass are buffered. + +--- + +### uitextbox_t -- typewriter dialogue box (`uitextbox.h`) + +A global single-instance dialogue box that displays text one character +at a time with word-wrap and multi-page support. + +```c +extern uitextbox_t UI_TEXTBOX; + +errorret_t uiTextboxInit(void); +void uiTextboxDispose(void); +``` + +**Setting text:** + +```c +uiTextboxSetText("Hello, world!"); +// Automatically word-wraps and paginates. +// Resets currentPage and scroll to 0. +``` + +**Tick update (called from script `update` or uiUpdate):** + +```c +uiTextboxUpdate(); +// Advances typewriter by UI_TEXTBOX_SCROLL_CHARS_PER_TICK (1) each +// fixed tick. Skipped on dynamic sub-ticks. +``` + +**Page control:** + +```c +bool_t uiTextboxPageIsComplete(void); // scroll revealed full page +bool_t uiTextboxHasNextPage(void); // more pages remain +void uiTextboxNextPage(void); // advance; no-op on last page +int32_t uiTextboxGetPageCharCount(void); +``` + +**Events:** + +```c +// Fires when the current page is fully scrolled into view: +UI_TEXTBOX.onPageComplete + +// Fires when the last page has been fully scrolled: +UI_TEXTBOX.onLastPage +``` + +Subscribe via the event system (see `.claude/events.md`). + +**Limits:** + +| Constant | Value | +|----------|-------| +| `UI_TEXTBOX_TEXT_MAX` | 1024 chars | +| `UI_TEXTBOX_LINES_MAX` | 64 lines | +| `UI_TEXTBOX_LINES_PER_PAGE_MAX` | 3 lines per page | +| `UI_TEXTBOX_SCROLL_CHARS_PER_TICK` | 1 char per tick | + +--- + +### uifullbox_t -- full-screen colour overlay (`uifullbox.h`) + +An animated solid-colour overlay that covers the entire screen. Used +for fade-to-black, scene transitions, and flash effects. + +Two global instances are provided: + +```c +extern uifullbox_t UI_FULLBOX_UNDER; // draws below scene content +extern uifullbox_t UI_FULLBOX_OVER; // draws above all content +``` + +```c +void uiFullboxInit(uifullbox_t *fullbox); + +// Start a colour-to-colour transition: +void uiFullboxTransition( + uifullbox_t *fullbox, + color_t from, + color_t to, + float_t duration, + easingtype_t easing +); + +void uiFullboxUpdate(uifullbox_t *fullbox, float_t delta); +errorret_t uiFullboxDraw(uifullbox_t *fullbox); +// Draw is skipped entirely when alpha == 0. +``` + +`fullbox.onTransitionEnd` event fires once when the transition +completes. Subscribe to it to chain scene transitions: + +```c +// Typical fade-out before a scene change: +uiFullboxTransition( + &UI_FULLBOX_OVER, + COLOR_TRANSPARENT, COLOR_BLACK, + 0.5f, EASING_OUT_QUAD +); +eventSubscribe(&UI_FULLBOX_OVER.onTransitionEnd, onFadeComplete, NULL); +``` + +--- + +### uiloading_t -- loading indicator (`uiloading.h`) + +An animated loading indicator with fade-in / fade-out transitions. +Shown while asset batches are loading. + +```c +extern uiloading_t UI_LOADING; + +void uiLoadingInit(void); +void uiLoadingUpdate(float_t delta); +errorret_t uiLoadingDraw(void); + +// Fade in; callback fires when fully visible: +void uiLoadingShow(eventcallback_t callback, void *user); + +// Fade out; callback fires when fully hidden: +void uiLoadingHide(eventcallback_t callback, void *user); +``` + +`UI_LOADING_FADE_DURATION` is 0.5 seconds. `UI_LOADING_MARGIN` is 8px. +`uiLoadingDraw` is a no-op when the indicator is fully transparent. + +--- + +### uifps_t -- FPS counter (`uifps.h`) + +Debug FPS display. Measures real elapsed time between ticks and draws +the average frame rate as text in the corner of the screen. + +```c +extern uifps_t UIFPS; + +errorret_t uiFPSDraw(); // also performs the measurement update +``` + +Intended for debug builds only. Draw it explicitly from a JS `render` +hook or from a UI element -- it is not registered in `UI_ELEMENTS` by +default. diff --git a/CLAUDE.md b/CLAUDE.md index 57bb5f6b..659ae4b7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -65,6 +65,12 @@ Detailed documentation on specific topics lives in `.claude/`: | Display -- screen, framebuffer, size modes | `.claude/display-core.md` | | Display -- texture, tileset, font | `.claude/display-texture.md` | | Display -- shader, material, display state | `.claude/display-shader.md` | +| Display -- mesh, vertex format, primitives | `.claude/display-mesh.md` | +| Display -- SpriteBatch (2D quad renderer) | `.claude/display-spritebatch.md` | +| Display -- text rendering, FONT_DEFAULT | `.claude/display-text.md` | +| Display -- color types, macros, constants | `.claude/display-color.md` | +| UI system (frame, textbox, fullbox, loading, FPS) | `.claude/ui.md` | +| Console (debug overlay, POSIX stdin mode) | `.claude/console.md` | | Scene system (lifecycle, render pipeline, JS hooks) | `.claude/scene.md` | ---