Authoring API
The ui object from @typecad/ui is how you wire up a screen — mounting it, holding reactive values, connecting inputs, and drawing on a canvas. You call these functions in your TypeScript; Cuttlefish turns each one into the right device code when you build. (They only exist at build time — if you run them in a plain Node script they’ll throw an error, which is expected.)
import { ui } from '@typecad/ui';ui.mount(tree, opts?)
Mount a baked UI tree to the configured display. Validates the driver against the active framework at transpile time; lowers to display initialization plus the first-frame draw.
ui.mount(screen);
ui.mount(screen, { spiFrequency: 40_000_000 });MountOptions (all optional): display, bus, cs, dc, rst, rotation, backlight, spiFrequency, address (I2C), reset (I2C).
ui.signal(initial)
Declare a reactive signal. Lowers to a device variable plus a dirty flag. Three overloads narrow the type from the initial value:
const level = ui.signal(0); // Signal<number>
const label = ui.signal('Hi'); // Signal<string>
const on = ui.signal(false); // Signal<boolean>A Signal<T> is callable to read (level()) and has .set(value) to write (level.set(50)).
ui.bind(node, property, compute)
Bind a node property to a recomputed value, evaluated each tick. Marks the node dirty when the value changes. Supported properties include color, background, borderColor, value, visible, and text.
ui.bind(screen.label, 'text', () => `Level: ${level()}`);
ui.bind(screen.bar, 'value', () => level());ui.bindInput(node, onText)
Subscribe to an <input> element’s text. The callback fires after the on-screen keyboard commits.
ui.bindInput(screen.ssid, (text) => { console.log(`SSID: ${text}`); });ui.bindList(node, countFn, itemFn, onTap?)
Bind a <list> to dynamic data. Virtualized — only visible items render. countFn returns the item count (called each frame); itemFn returns the label for a given index; onTap (optional) fires when an item is tapped.
ui.bindList(
screen.networks,
() => scanResults.length,
(i) => scanResults[i].name,
(i) => connectTo(scanResults[i]),
);ui.watchPin(pin, onFalling)
Watch a GPIO pin for falling edges (a button press). It checks the pin roughly every 20ms in the background — no hardware interrupt needed. Use for buttons wired to GPIO when you don’t have a touch screen.
ui.watchPin(D2, () => { screen.count.value++; });ui.onTap(node?)
An awaitable tap signal for use inside async functions. No argument resumes on the next tap anywhere (including empty space); pass an element to resume only on that element. Use for “tap to continue” flows and display-sleep/screensaver wake.
await ui.onTap(screen.continueButton);
showNextPage();ui.drawCanvas(node, callback)
Register a per-frame draw callback for a <canvas> element. Coordinates are canvas-relative and auto-clipped. Colors are ordinary CSS strings, converted at build time to the compact RGB565 color format the display uses. The callback receives a CanvasCtx:
ui.drawCanvas(screen.scope, (ctx) => {
ctx.fillScreen('#000');
ctx.line(0, 0, ctx.width, ctx.height, '#0f0');
ctx.fillCircle(ctx.width / 2, ctx.height / 2, 8, '#f00');
ctx.text(4, 4, `${screen.load.value}`, '#fff');
});CanvasCtx
| Method | Signature |
|---|---|
width / height | readonly number |
drawPixel | (x, y, color) |
fillRect | (x, y, w, h, color) |
rect | (x, y, w, h, color) |
fillRoundRect | (x, y, w, h, r, color) |
roundRect | (x, y, w, h, r, color) |
line | (x0, y0, x1, y1, color) |
hline | (x, y, w, color) |
vline | (x, y, h, color) |
fillCircle | (x, y, r, color) |
circle | (x, y, r, color) |
rgbBitmap | (x, y, data: number[], w, h) |
text | (x, y, str, color?) |
fillScreen | (color) |
ui.window (native SDL only)
Native-desktop window controls. No-ops on hardware targets.
ui.window.setTitle('My App');
ui.window.setIcon('./assets/icon.png');Element types and .value
Every element exposes .value: number (its state) plus onClick, onHold, onRelease. Specific elements add more:
| Type | Notable members |
|---|---|
CheckElement | onToggle(pin, onChange?) — .value flips on a GPIO falling edge |
RangeElement | onChange(callback?) — fires while the thumb drags |
InputElement | text: string, onChange(callback?) |
SelectElement | .value cycles 0..N-1; text auto-reflects the option |
RadioElement | .value is 0/1; mutually exclusive within a name group |
ProgressElement | .value is 0-100 (read-only) |
The named screen.* references (e.g. screen.label, screen.ok) are typed against a generated per-file ScreenTree whose fields match the id attributes in your template.
The .ui single-file component
A .ui file has up to three sections, in any order: a <script> block, a <style> block, and the HTML template (typically a <screen> element). Only one <script> block is allowed; multiple <style> blocks are concatenated.
<script> section — TypeScript logic, signals, and the ui.mount call:
import { ui } from '@typecad/ui';
export const level = ui.signal(0);
ui.mount(screen);
setInterval(() => level.set((level() + 1) % 100), 250);<style> section — CSS rules and variables:
:root { --bg: #111; --fg: #eee; }
screen { background: var(--bg); color: var(--fg); padding: 12px; }
#bar { height: 16px; background: #2563eb; }Template section — the HTML element tree:
<screen>
<text id="label">Progress</text>
<progress id="bar"></progress>
</screen>The transpiler injects an implicit import { screen } from './app.ui.html' so the template is referenceable as screen.* inside the script.
Accepted entry extensions
Point your cuttlefish.config.ts entry at any of .ts, .tsx, or .ui.
Two project layouts
Pattern A — single-file .ui: script, style, and template in one file. The entry is the .ui file.
Pattern B — split main.ts + .ui.html: keep logic in main.ts and the template in a .ui.html file; import the template from the script. Useful for larger UIs.
State, input, and timing
.valuereads/writes element state (screen.check.value = 1).- Pin input —
onToggle(pin, onChange?)on<check>/<view>;ui.watchPin(pin, onFalling)for arbitrary GPIO. - Touch events —
onClick(up within 600ms),onHold(≥ 600ms),onRelease, with ~50ms debounce. - Two-way input binding —
ui.bindInput(node, onText)for<input>. - Slider changes —
onChange(callback?)on<range>. - Data-bound lists —
ui.bindList(node, countFn, itemFn, onTap?). - Signals — reactive values via
ui.signal; read withsig(), write withsig.set(...). - Timers — standard
setInterval/setTimeoutwork; they lower to the device’s task loop.
⚙️ Advanced details — the runtime architecture
- Retained-mode runtime.
ui_mountinitializes the display;ui_tick(called each loop) polls inputs, evaluates bindings and transitions, and repaints dirty regions;ui_initmarks all nodes dirty for the first frame. - Dirty-rectangle repaint. Only changed regions are redrawn and pushed.
- PSRAM-gated framebuffer. When
BOARD_HAS_PSRAMandpsramFound()are true, a full-screenGFXcanvas16framebuffer may be used to hide tearing on small updates.
Back to Cuttlefish UI overview.
On This Page