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

MethodSignature
width / heightreadonly 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:

TypeNotable members
CheckElementonToggle(pin, onChange?).value flips on a GPIO falling edge
RangeElementonChange(callback?) — fires while the thumb drags
InputElementtext: 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

  • .value reads/writes element state (screen.check.value = 1).
  • Pin inputonToggle(pin, onChange?) on <check>/<view>; ui.watchPin(pin, onFalling) for arbitrary GPIO.
  • Touch eventsonClick (up within 600ms), onHold (≥ 600ms), onRelease, with ~50ms debounce.
  • Two-way input bindingui.bindInput(node, onText) for <input>.
  • Slider changesonChange(callback?) on <range>.
  • Data-bound listsui.bindList(node, countFn, itemFn, onTap?).
  • Signals — reactive values via ui.signal; read with sig(), write with sig.set(...).
  • Timers — standard setInterval/setTimeout work; they lower to the device’s task loop.
⚙️ Advanced details — the runtime architecture
  • Retained-mode runtime. ui_mount initializes the display; ui_tick (called each loop) polls inputs, evaluates bindings and transitions, and repaints dirty regions; ui_init marks all nodes dirty for the first frame.
  • Dirty-rectangle repaint. Only changed regions are redrawn and pushed.
  • PSRAM-gated framebuffer. When BOARD_HAS_PSRAM and psramFound() are true, a full-screen GFXcanvas16 framebuffer may be used to hide tearing on small updates.

Back to Cuttlefish UI overview.