@typecad/ui 0.1.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1443 -0
- package/dist/engine-index.d.ts +7 -0
- package/dist/engine-index.js +41 -0
- package/dist/index.d.ts +165 -0
- package/dist/index.js +42 -0
- package/dist/preview/build-program.d.ts +7 -0
- package/dist/preview/build-program.js +665 -0
- package/dist/preview/host-gfx.d.ts +90 -0
- package/dist/preview/host-gfx.js +552 -0
- package/dist/preview/host-ui-runtime.d.ts +241 -0
- package/dist/preview/host-ui-runtime.js +3236 -0
- package/dist/preview/types.d.ts +83 -0
- package/dist/preview/types.js +1 -0
- package/dist/types.d.ts +108 -0
- package/dist/types.js +10 -0
- package/dist/ui-engine/block-layout.d.ts +8 -0
- package/dist/ui-engine/block-layout.js +95 -0
- package/dist/ui-engine/color.d.ts +49 -0
- package/dist/ui-engine/color.js +273 -0
- package/dist/ui-engine/css-parser.d.ts +155 -0
- package/dist/ui-engine/css-parser.js +1058 -0
- package/dist/ui-engine/default-keyboards.d.ts +3 -0
- package/dist/ui-engine/default-keyboards.js +53 -0
- package/dist/ui-engine/easing.d.ts +12 -0
- package/dist/ui-engine/easing.js +76 -0
- package/dist/ui-engine/font-assets.d.ts +50 -0
- package/dist/ui-engine/font-assets.js +431 -0
- package/dist/ui-engine/html-parser.d.ts +116 -0
- package/dist/ui-engine/html-parser.js +284 -0
- package/dist/ui-engine/image-assets.d.ts +21 -0
- package/dist/ui-engine/image-assets.js +125 -0
- package/dist/ui-engine/inline-parser.d.ts +30 -0
- package/dist/ui-engine/inline-parser.js +66 -0
- package/dist/ui-engine/keyframes.d.ts +3 -0
- package/dist/ui-engine/keyframes.js +39 -0
- package/dist/ui-engine/layout-engine.d.ts +35 -0
- package/dist/ui-engine/layout-engine.js +203 -0
- package/dist/ui-engine/model.d.ts +239 -0
- package/dist/ui-engine/model.js +987 -0
- package/dist/ui-engine/rich-layout.d.ts +51 -0
- package/dist/ui-engine/rich-layout.js +220 -0
- package/dist/ui-engine/run-types.d.ts +13 -0
- package/dist/ui-engine/run-types.js +1 -0
- package/dist/ui-engine/runtime-header/antialiasing.d.ts +1 -0
- package/dist/ui-engine/runtime-header/antialiasing.js +141 -0
- package/dist/ui-engine/runtime-header/blend-bodies.d.ts +1 -0
- package/dist/ui-engine/runtime-header/blend-bodies.js +39 -0
- package/dist/ui-engine/runtime-header/canvas-helpers.d.ts +1 -0
- package/dist/ui-engine/runtime-header/canvas-helpers.js +155 -0
- package/dist/ui-engine/runtime-header/canvas-scrollbar.d.ts +1 -0
- package/dist/ui-engine/runtime-header/canvas-scrollbar.js +105 -0
- package/dist/ui-engine/runtime-header/color-mono-refresh.d.ts +1 -0
- package/dist/ui-engine/runtime-header/color-mono-refresh.js +154 -0
- package/dist/ui-engine/runtime-header/dirty-scroll-mutators.d.ts +1 -0
- package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +169 -0
- package/dist/ui-engine/runtime-header/display-shim.d.ts +1 -0
- package/dist/ui-engine/runtime-header/display-shim.js +104 -0
- package/dist/ui-engine/runtime-header/forward-decls.d.ts +1 -0
- package/dist/ui-engine/runtime-header/forward-decls.js +137 -0
- package/dist/ui-engine/runtime-header/guard-close.d.ts +1 -0
- package/dist/ui-engine/runtime-header/guard-close.js +8 -0
- package/dist/ui-engine/runtime-header/image-drawing.d.ts +1 -0
- package/dist/ui-engine/runtime-header/image-drawing.js +300 -0
- package/dist/ui-engine/runtime-header/init-press-input.d.ts +1 -0
- package/dist/ui-engine/runtime-header/init-press-input.js +126 -0
- package/dist/ui-engine/runtime-header/keyboard.d.ts +1 -0
- package/dist/ui-engine/runtime-header/keyboard.js +342 -0
- package/dist/ui-engine/runtime-header/node-decoration.d.ts +1 -0
- package/dist/ui-engine/runtime-header/node-decoration.js +212 -0
- package/dist/ui-engine/runtime-header/paint-order-coords.d.ts +1 -0
- package/dist/ui-engine/runtime-header/paint-order-coords.js +190 -0
- package/dist/ui-engine/runtime-header/paint-rects-repair.d.ts +1 -0
- package/dist/ui-engine/runtime-header/paint-rects-repair.js +553 -0
- package/dist/ui-engine/runtime-header/scroll-physics.d.ts +1 -0
- package/dist/ui-engine/runtime-header/scroll-physics.js +161 -0
- package/dist/ui-engine/runtime-header/state-bindings-nav.d.ts +1 -0
- package/dist/ui-engine/runtime-header/state-bindings-nav.js +176 -0
- package/dist/ui-engine/runtime-header/structs.d.ts +1 -0
- package/dist/ui-engine/runtime-header/structs.js +220 -0
- package/dist/ui-engine/runtime-header/text-rendering.d.ts +1 -0
- package/dist/ui-engine/runtime-header/text-rendering.js +715 -0
- package/dist/ui-engine/runtime-header/tick/bindings-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/bindings-phase.js +103 -0
- package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +1051 -0
- package/dist/ui-engine/runtime-header/tick/flush-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/flush-phase.js +17 -0
- package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +15 -0
- package/dist/ui-engine/runtime-header/tick/transitions-phase.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick/transitions-phase.js +213 -0
- package/dist/ui-engine/runtime-header/tick-motion-helpers.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick-motion-helpers.js +26 -0
- package/dist/ui-engine/runtime-header/tick.d.ts +1 -0
- package/dist/ui-engine/runtime-header/tick.js +17 -0
- package/dist/ui-engine/runtime-header/touch-keyboard-fwd.d.ts +1 -0
- package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +406 -0
- package/dist/ui-engine/runtime-header/types-defines.d.ts +1 -0
- package/dist/ui-engine/runtime-header/types-defines.js +79 -0
- package/dist/ui-engine/runtime-header.d.ts +1 -0
- package/dist/ui-engine/runtime-header.js +69 -0
- package/dist/ui-engine/scroll-memory-diagnostics.d.ts +16 -0
- package/dist/ui-engine/scroll-memory-diagnostics.js +58 -0
- package/dist/ui-engine/select-engine.d.ts +4 -0
- package/dist/ui-engine/select-engine.js +19 -0
- package/dist/ui-engine/style-resolver.d.ts +77 -0
- package/dist/ui-engine/style-resolver.js +341 -0
- package/dist/ui-engine/text-layout.d.ts +19 -0
- package/dist/ui-engine/text-layout.js +92 -0
- package/dist/ui-engine/transpile-ui.d.ts +11 -0
- package/dist/ui-engine/transpile-ui.js +33 -0
- package/dist/ui-engine/ua-stylesheet.d.ts +3 -0
- package/dist/ui-engine/ua-stylesheet.js +56 -0
- package/dist/ui-engine/ui-file-splitter.d.ts +14 -0
- package/dist/ui-engine/ui-file-splitter.js +47 -0
- package/dist/ui-engine/ui-lowering.d.ts +33 -0
- package/dist/ui-engine/ui-lowering.js +390 -0
- package/dist/ui-engine/ui-registry.d.ts +81 -0
- package/dist/ui-engine/ui-registry.js +429 -0
- package/dist/ui-engine/yoga-layout.d.ts +12 -0
- package/dist/ui-engine/yoga-layout.js +314 -0
- package/package.json +86 -0
- package/src/engine-index.ts +51 -0
- package/src/index.ts +232 -0
- package/src/preview/build-program.ts +734 -0
- package/src/preview/host-gfx.ts +580 -0
- package/src/preview/host-ui-runtime.ts +3245 -0
- package/src/preview/types.ts +96 -0
- package/src/types.ts +127 -0
- package/src/ui-engine/block-layout.ts +104 -0
- package/src/ui-engine/color.ts +263 -0
- package/src/ui-engine/css-parser.ts +1003 -0
- package/src/ui-engine/default-keyboards.ts +58 -0
- package/src/ui-engine/easing.ts +53 -0
- package/src/ui-engine/font-assets.ts +512 -0
- package/src/ui-engine/html-parser.ts +397 -0
- package/src/ui-engine/image-assets.ts +170 -0
- package/src/ui-engine/inline-parser.ts +78 -0
- package/src/ui-engine/keyframes.ts +47 -0
- package/src/ui-engine/layout-engine.ts +228 -0
- package/src/ui-engine/model.ts +1225 -0
- package/src/ui-engine/rich-layout.ts +257 -0
- package/src/ui-engine/run-types.ts +14 -0
- package/src/ui-engine/runtime-header/antialiasing.ts +141 -0
- package/src/ui-engine/runtime-header/blend-bodies.ts +39 -0
- package/src/ui-engine/runtime-header/canvas-helpers.ts +155 -0
- package/src/ui-engine/runtime-header/canvas-scrollbar.ts +105 -0
- package/src/ui-engine/runtime-header/color-mono-refresh.ts +154 -0
- package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +169 -0
- package/src/ui-engine/runtime-header/display-shim.ts +104 -0
- package/src/ui-engine/runtime-header/forward-decls.ts +137 -0
- package/src/ui-engine/runtime-header/guard-close.ts +8 -0
- package/src/ui-engine/runtime-header/image-drawing.ts +300 -0
- package/src/ui-engine/runtime-header/init-press-input.ts +126 -0
- package/src/ui-engine/runtime-header/keyboard.ts +342 -0
- package/src/ui-engine/runtime-header/node-decoration.ts +212 -0
- package/src/ui-engine/runtime-header/paint-order-coords.ts +190 -0
- package/src/ui-engine/runtime-header/paint-rects-repair.ts +553 -0
- package/src/ui-engine/runtime-header/scroll-physics.ts +161 -0
- package/src/ui-engine/runtime-header/state-bindings-nav.ts +176 -0
- package/src/ui-engine/runtime-header/structs.ts +220 -0
- package/src/ui-engine/runtime-header/text-rendering.ts +715 -0
- package/src/ui-engine/runtime-header/tick/bindings-phase.ts +103 -0
- package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +1051 -0
- package/src/ui-engine/runtime-header/tick/flush-phase.ts +17 -0
- package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +15 -0
- package/src/ui-engine/runtime-header/tick/transitions-phase.ts +213 -0
- package/src/ui-engine/runtime-header/tick-motion-helpers.ts +26 -0
- package/src/ui-engine/runtime-header/tick.ts +18 -0
- package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +406 -0
- package/src/ui-engine/runtime-header/types-defines.ts +79 -0
- package/src/ui-engine/runtime-header.ts +71 -0
- package/src/ui-engine/scroll-memory-diagnostics.ts +79 -0
- package/src/ui-engine/select-engine.ts +21 -0
- package/src/ui-engine/style-resolver.ts +416 -0
- package/src/ui-engine/text-layout.ts +114 -0
- package/src/ui-engine/transpile-ui.ts +45 -0
- package/src/ui-engine/ua-stylesheet.ts +60 -0
- package/src/ui-engine/ui-file-splitter.ts +59 -0
- package/src/ui-engine/ui-lowering.ts +459 -0
- package/src/ui-engine/ui-registry.ts +530 -0
- package/src/ui-engine/yoga-layout.ts +309 -0
package/README.md
ADDED
|
@@ -0,0 +1,1443 @@
|
|
|
1
|
+
# @typecad/ui — HTML/CSS-Driven Graphics for Microcontrollers
|
|
2
|
+
|
|
3
|
+
Write UIs in HTML and CSS. TypeCAD's `cuttlefish` transpiler lowers them to a retained-mode C++ runtime that draws on ILI9341 (and future) displays over hardware SPI. No browser, no DOM, no CSS engine on the device — everything is resolved at transpile time.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
`@typecad/ui` is the authoring API you import in source; `@typecad/cuttlefish` is the transpiler that lowers those imports to firmware at build time. You need both, plus a board package:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @typecad/ui @typecad/board-esp32-devkit
|
|
11
|
+
npm install --save-dev @typecad/cuttlefish
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`@typecad/ui` is compile-time only — none of its code is shipped to the device. The transpiler intercepts `ui.mount` / `ui.signal` / `ui.bind` / ... calls and lowers them to device variables and binding-table entries, so the package can be safely kept in `dependencies`.
|
|
15
|
+
|
|
16
|
+
## Project layout
|
|
17
|
+
|
|
18
|
+
A TypeCAD UI project has one **entry** — the file you point `cuttlefish.config.ts` at. The entry can be either a `.ui` single-file component or a plain `.ts` module. Both intermix freely with regular cuttlefish TypeScript (HAL pin reads, `setInterval`, `console.log`, your own `.ts` modules) — the `<script>` block of a `.ui` file and a standalone `.ts` file are lowered by the same pipeline.
|
|
19
|
+
|
|
20
|
+
The transpiler accepts three entry extensions: `.ts`, `.tsx`, and `.ui`.
|
|
21
|
+
|
|
22
|
+
### Pattern A — `.ui` single-file component (display + logic together)
|
|
23
|
+
|
|
24
|
+
A `.ui` file is a Svelte-style single-file component with three sections — `<script>`, `<style>`, and the HTML template — in one file. The blocks may appear in any order; only one `<script>` is supported, and multiple `<style>` blocks are concatenated.
|
|
25
|
+
|
|
26
|
+
The transpiler injects an implicit `import { screen } from './app.ui.html'` into the script, so the in-file template is referenceable as `screen.*` without an explicit import.
|
|
27
|
+
|
|
28
|
+
**`src/app.ui`** — markup, styling, and behavior for one screen in one file:
|
|
29
|
+
|
|
30
|
+
```html
|
|
31
|
+
<script>
|
|
32
|
+
import { ui } from '@typecad/ui';
|
|
33
|
+
import { A0 } from '@typecad/board-esp32-devkit';
|
|
34
|
+
|
|
35
|
+
ui.mount(screen);
|
|
36
|
+
|
|
37
|
+
// Plain TypeScript — same lowering as a standalone .ts. HAL pin reads,
|
|
38
|
+
// timers, and UI writes coexist as regular statements.
|
|
39
|
+
const sensor = A0.asInput();
|
|
40
|
+
|
|
41
|
+
setInterval(() => {
|
|
42
|
+
screen.reading.value = sensor.readAnalog();
|
|
43
|
+
}, 500);
|
|
44
|
+
|
|
45
|
+
ui.bind(screen.lamp, 'background', () =>
|
|
46
|
+
screen.reading.value > 512 ? 'limegreen' : '#333'
|
|
47
|
+
);
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<style>
|
|
51
|
+
screen { background: #1a1a2e; display: flex; flex-direction: column; padding: 10px; gap: 8px; }
|
|
52
|
+
#reading { color: dodgerblue; font-size: 24px; }
|
|
53
|
+
#lamp { width: 16px; height: 16px; border-radius: 4px; }
|
|
54
|
+
</style>
|
|
55
|
+
|
|
56
|
+
<screen>
|
|
57
|
+
<text id="reading">0</text>
|
|
58
|
+
<view id="lamp"></view>
|
|
59
|
+
</screen>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**`cuttlefish.config.ts`** — point the entry at the `.ui` file:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import type { CuttlefishConfig } from '@typecad/cuttlefish/api';
|
|
66
|
+
|
|
67
|
+
const config: CuttlefishConfig = {
|
|
68
|
+
entry: './src/app.ui',
|
|
69
|
+
target: 'esp32',
|
|
70
|
+
mcu: '@typecad/mcu-esp32',
|
|
71
|
+
board: '@typecad/board-esp32-devkit',
|
|
72
|
+
framework: '@typecad/framework-arduino',
|
|
73
|
+
frameworkData: { buildTarget: 'esp32:esp32:esp32' },
|
|
74
|
+
output: { framework: 'arduino', optimize: 'size', outDir: './out' },
|
|
75
|
+
toolchain: { type: 'arduino-cli' },
|
|
76
|
+
console: { baudRate: 115200 },
|
|
77
|
+
display: { profile: 'ili9341-spi', cs: 5, dc: 21, rst: 22, backlight: 33 },
|
|
78
|
+
};
|
|
79
|
+
export default config;
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Use this layout when a screen's markup and behavior are tightly coupled and you want them in one place.
|
|
83
|
+
|
|
84
|
+
### Pattern B — `main.ts` entry + `.ui.html` for display (logic factored out)
|
|
85
|
+
|
|
86
|
+
When the sensor/IO logic is substantial, factor it into plain `.ts` modules and keep the UI file purely declarative. `main.ts` is the entry; it imports `screen` from a `.ui.html` file **and** the sensor functions from a sibling `.ts`, then marries them.
|
|
87
|
+
|
|
88
|
+
The import graph resolves both directions: `main.ts → app.ui.html` (for `screen`) and `main.ts → sensors.ts` (for the reading). Cross-module function calls survive lowering into the emitted C++.
|
|
89
|
+
|
|
90
|
+
**`src/main.ts`** — the entry, wires display and sensor logic:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { ui } from '@typecad/ui';
|
|
94
|
+
import { screen } from './app.ui.html';
|
|
95
|
+
import { readVolts } from './sensors.js'; // .js extension required (Node16 resolution)
|
|
96
|
+
|
|
97
|
+
ui.mount(screen, { display: 'ili9341', bus: 'SPI', cs: 10, dc: 9, rst: 8 });
|
|
98
|
+
|
|
99
|
+
setInterval(() => {
|
|
100
|
+
screen.meter.value = Math.round(readVolts() * 100);
|
|
101
|
+
}, 500);
|
|
102
|
+
|
|
103
|
+
export function main(): void { while (true) {} }
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**`src/sensors.ts`** — plain cuttlefish TS, owns device I/O, no UI imports:
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { A0 } from '@typecad/board-arduino-uno';
|
|
110
|
+
const adc = A0.asInput();
|
|
111
|
+
export function readVolts(): number {
|
|
112
|
+
return adc.readVoltage();
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**`src/app.ui.html`** — display only:
|
|
117
|
+
|
|
118
|
+
```html
|
|
119
|
+
<screen>
|
|
120
|
+
<range id="meter" min="0" max="330"></range>
|
|
121
|
+
</screen>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
**`cuttlefish.config.ts`** — point the entry at `main.ts`:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
const config: CuttlefishConfig = {
|
|
128
|
+
entry: './src/main.ts', // ← .ts entry instead of .ui
|
|
129
|
+
// ...rest identical
|
|
130
|
+
};
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
The transpiler lowers the `setInterval` callback to a timer that writes the node table and marks it dirty, preserving the cross-module `readVolts()` call verbatim:
|
|
134
|
+
|
|
135
|
+
```c
|
|
136
|
+
static void __tc_timer_cb_0() {
|
|
137
|
+
__ui_nodes[1].value = round(readVolts() * 100); // sensor call preserved
|
|
138
|
+
ui_mark_dirty(1); // screen.meter.value = ... lowered
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Use this layout when sensor or IO logic is large enough to deserve its own module, or when you want the UI file to stay purely declarative.
|
|
143
|
+
|
|
144
|
+
### Choosing between the two
|
|
145
|
+
|
|
146
|
+
| Layout | Entry | Use when |
|
|
147
|
+
|---|---|---|
|
|
148
|
+
| `.ui` single-file | `entry: './src/app.ui'` | Display and behavior are tightly coupled; one screen's markup + logic belong together |
|
|
149
|
+
| `main.ts` + `.ui.html` split | `entry: './src/main.ts'` | Sensor/IO logic is substantial; UI file should stay declarative |
|
|
150
|
+
|
|
151
|
+
Both patterns can import the same things (`@typecad/ui`, the board package, the HAL, your own `.ts` modules), and both lower through the same pipeline. You can also mix them within a project — a `.ui` entry can import helper functions from a sibling `.ts`, and a `main.ts` entry can import `screen` from multiple `.ui.html` files.
|
|
152
|
+
|
|
153
|
+
### A note on dynamic text bindings
|
|
154
|
+
|
|
155
|
+
Most bindings lower general expressions and work in either layout — color, background, border, value, visibility. The one exception is `ui.bind(node, 'text', ...)`, whose body in v1 only lowers three shapes: `String(<numeric>)`, a bare string literal, or a template literal with numeric interpolations. Anything else (including a cross-module function call) lowers to an empty body and emits a `ui-bind-text-unlowered` warning.
|
|
156
|
+
|
|
157
|
+
For dynamic text that doesn't fit those shapes, write to the element from `setInterval` instead:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
// Avoid (body won't lower in v1):
|
|
161
|
+
ui.bind(screen.reading, 'text', () => readVolts().toFixed(2) + 'V');
|
|
162
|
+
|
|
163
|
+
// Prefer — drive from a timer:
|
|
164
|
+
setInterval(() => {
|
|
165
|
+
// write to .value on numeric elements, or use String(<expr>) in the bind
|
|
166
|
+
screen.meter.value = Math.round(readVolts() * 100);
|
|
167
|
+
}, 500);
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Quick start
|
|
171
|
+
|
|
172
|
+
The fastest path is a single `.ui` file with markup, styling, and behavior together (see [Project layout](#project-layout) for the alternative `main.ts` + `.ui.html` split).
|
|
173
|
+
|
|
174
|
+
### 1. Create `src/app.ui`
|
|
175
|
+
|
|
176
|
+
```html
|
|
177
|
+
<script>
|
|
178
|
+
import { ui } from '@typecad/ui';
|
|
179
|
+
|
|
180
|
+
ui.mount(screen);
|
|
181
|
+
|
|
182
|
+
// A signal carries the count; the binding recomputes the label each frame.
|
|
183
|
+
// Text updates via ui.bind(..., 'text', ...) — <text> elements don't expose
|
|
184
|
+
// a writable .text. String(<number>) is one of the supported bind shapes.
|
|
185
|
+
const taps = ui.signal(0);
|
|
186
|
+
ui.bind(screen.count, 'text', () => String(taps()));
|
|
187
|
+
|
|
188
|
+
screen.action.onClick(() => {
|
|
189
|
+
taps.set(taps() + 1);
|
|
190
|
+
});
|
|
191
|
+
</script>
|
|
192
|
+
|
|
193
|
+
<style>
|
|
194
|
+
screen {
|
|
195
|
+
background: #1a1a2e;
|
|
196
|
+
display: flex;
|
|
197
|
+
flex-direction: column;
|
|
198
|
+
gap: 8px;
|
|
199
|
+
padding: 10px;
|
|
200
|
+
}
|
|
201
|
+
#count { color: dodgerblue; font-size: 16px; }
|
|
202
|
+
#action {
|
|
203
|
+
background: darkgreen;
|
|
204
|
+
color: white;
|
|
205
|
+
border: 2px solid limegreen;
|
|
206
|
+
border-radius: 4px;
|
|
207
|
+
padding: 8px 20px;
|
|
208
|
+
text-align: center;
|
|
209
|
+
transition: background 300ms;
|
|
210
|
+
}
|
|
211
|
+
#action:pressed { background: limegreen; }
|
|
212
|
+
</style>
|
|
213
|
+
|
|
214
|
+
<screen>
|
|
215
|
+
<text id="count">0</text>
|
|
216
|
+
<button id="action">Start</button>
|
|
217
|
+
</screen>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Display hardware and wiring live in `cuttlefish.config.ts` under `display`, so the UI source stays focused on UI behavior.
|
|
221
|
+
|
|
222
|
+
### 2. Point the entry at the `.ui` file
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
// cuttlefish.config.ts
|
|
226
|
+
import type { CuttlefishConfig } from '@typecad/cuttlefish/api';
|
|
227
|
+
|
|
228
|
+
const config: CuttlefishConfig = {
|
|
229
|
+
entry: './src/app.ui',
|
|
230
|
+
target: 'esp32',
|
|
231
|
+
mcu: '@typecad/mcu-esp32',
|
|
232
|
+
board: '@typecad/board-esp32-devkit',
|
|
233
|
+
framework: '@typecad/framework-arduino',
|
|
234
|
+
frameworkData: { buildTarget: 'esp32:esp32:esp32' },
|
|
235
|
+
output: { framework: 'arduino', optimize: 'size', outDir: './out' },
|
|
236
|
+
toolchain: { type: 'arduino-cli' },
|
|
237
|
+
console: { baudRate: 115200 },
|
|
238
|
+
display: { profile: 'ili9341-spi', cs: 5, dc: 21, rst: 22, backlight: 33 },
|
|
239
|
+
};
|
|
240
|
+
export default config;
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### 3. Build
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
npx cuttlefish build --compile
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
## Elements
|
|
250
|
+
|
|
251
|
+
### `<screen>`
|
|
252
|
+
|
|
253
|
+
The root container. Required (exactly one). Its box fills the display viewport (320×240 landscape for ILI9341).
|
|
254
|
+
|
|
255
|
+
### `<view>`
|
|
256
|
+
|
|
257
|
+
A generic container. Supports `display: flex` for layout. Has a `.value` property and `onToggle`/`onChange` for interaction.
|
|
258
|
+
|
|
259
|
+
```html
|
|
260
|
+
<view id="row">
|
|
261
|
+
<text id="label">Status</text>
|
|
262
|
+
<text id="value">OK</text>
|
|
263
|
+
</view>
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### `<text>`
|
|
267
|
+
|
|
268
|
+
Static or dynamic text. Has a `.value` property for numeric state.
|
|
269
|
+
|
|
270
|
+
```html
|
|
271
|
+
<text id="counter">0</text>
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### `<button>`
|
|
275
|
+
|
|
276
|
+
A clickable button with `:pressed` pseudo-state support and transition animations.
|
|
277
|
+
|
|
278
|
+
```html
|
|
279
|
+
<button id="start">Start</button>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Interactive elements
|
|
283
|
+
|
|
284
|
+
| Element | Purpose | `.value` |
|
|
285
|
+
|---|---|---|
|
|
286
|
+
| `<check>` | Checkbox (tap to toggle) | 0 / 1 |
|
|
287
|
+
| `<radio name="g">` | Radio (mutually exclusive within a `name` group) | 0 / 1 |
|
|
288
|
+
| `<select>` | Tap to cycle options; text auto-shows the current option | 0..N-1 |
|
|
289
|
+
| `<progress>` | Progress bar (0-100) | fill percentage |
|
|
290
|
+
| `<range>` | Draggable slider | between `min` and `max` |
|
|
291
|
+
| `<input>` | Text input (tap opens the on-screen keyboard) | — (use `.text`) |
|
|
292
|
+
| `<list>` | Virtualized, data-bound list (renders only visible items) | — |
|
|
293
|
+
| `<canvas>` | User-drawn graphics (sparklines, gauges, custom shapes) via `ui.drawCanvas` | — |
|
|
294
|
+
|
|
295
|
+
```html
|
|
296
|
+
<range id="brightness" min="0" max="100"></range>
|
|
297
|
+
<progress id="load" value="40"></progress>
|
|
298
|
+
<check id="enable">Enable feature</check>
|
|
299
|
+
<input id="ssid" type="text" placeholder="Network name" maxlength="32"></input>
|
|
300
|
+
<select id="mode"><option>Auto</option><option>Manual</option></select>
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Slider and progress values can be driven live from `loop()` via bindings (see
|
|
304
|
+
[Reactive bindings](#reactive-bindings) below).
|
|
305
|
+
|
|
306
|
+
### Images
|
|
307
|
+
|
|
308
|
+
`<img>` embeds a raw RGB565 `.img` file as a `static const uint16_t[]` array:
|
|
309
|
+
|
|
310
|
+
```html
|
|
311
|
+
<img id="logo" src="assets/logo.img" width="64" height="64"></img>
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
The `.img` file is a flat row-major RGB565 dump (width × height × 2 bytes). Use
|
|
315
|
+
`object-fit` (`contain`, `cover`, `fill`) to control scaling.
|
|
316
|
+
|
|
317
|
+
### HTML tag aliases
|
|
318
|
+
|
|
319
|
+
Common HTML tags are accepted and remapped to the internal primitives, so you
|
|
320
|
+
can write familiar HTML:
|
|
321
|
+
|
|
322
|
+
| HTML tag | Maps to | Notes |
|
|
323
|
+
|---|---|---|
|
|
324
|
+
| `body`, `div`, `header`, `footer`, `nav`, `main`, `section`, `article`, `aside` | `<view>` | Block container |
|
|
325
|
+
| `span`, `p`, `h1`–`h6` | `<text>` | Inline/heading text |
|
|
326
|
+
|
|
327
|
+
```html
|
|
328
|
+
<header><h1 id="title">Settings</h1></header>
|
|
329
|
+
<main><p id="desc">Adjust preferences.</p></main>
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### Global attributes
|
|
333
|
+
|
|
334
|
+
All UI elements support the `hidden` attribute. Hidden elements and their
|
|
335
|
+
descendants stay in the generated node table, but they do not take space in
|
|
336
|
+
layout and are skipped for drawing and hit testing.
|
|
337
|
+
|
|
338
|
+
```html
|
|
339
|
+
<view id="advancedPanel" hidden>
|
|
340
|
+
<text>Advanced settings</text>
|
|
341
|
+
</view>
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
## CSS reference
|
|
345
|
+
|
|
346
|
+
### Supported properties
|
|
347
|
+
|
|
348
|
+
#### Box model
|
|
349
|
+
| Property | Values | Notes |
|
|
350
|
+
|---|---|---|
|
|
351
|
+
| `padding` | `8px`, `8px 16px` | Shorthand supported |
|
|
352
|
+
| `margin` | `8px`, `8px 16px` | Shorthand supported |
|
|
353
|
+
| `width` | `100px` | Explicit size |
|
|
354
|
+
| `height` | `50px` | Explicit size |
|
|
355
|
+
| `min-width` / `max-width` | `100px` | Yoga constraints |
|
|
356
|
+
| `min-height` / `max-height` | `50px` | Yoga constraints |
|
|
357
|
+
| `aspect-ratio` | `16 / 9`, `1 / 1`, `1.5` | Infers the missing width or height |
|
|
358
|
+
| `box-sizing` | `border-box` | Yoga border-box |
|
|
359
|
+
| `overflow` | `hidden`, `scroll` | Clips children; `scroll` enables touch-drag scrolling |
|
|
360
|
+
|
|
361
|
+
#### Length units
|
|
362
|
+
|
|
363
|
+
All length values accept `px`, bare numbers, and `rem`/`em` (× 16 root font
|
|
364
|
+
size). `0.625rem` resolves to `10px`. Percentages are used as-is in the contexts
|
|
365
|
+
that honor them (flex/position).
|
|
366
|
+
|
|
367
|
+
`calc()` evaluates simple arithmetic (`+ - * /`) on lengths after `var()`
|
|
368
|
+
substitution, with proper operator precedence:
|
|
369
|
+
|
|
370
|
+
```css
|
|
371
|
+
:root { --radius: 10px; }
|
|
372
|
+
.card { border-radius: calc(var(--radius) - 4px); } /* 6px */
|
|
373
|
+
.a { padding: calc(0.625rem * 2); } /* 20px */
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
#### Flexbox (via Yoga)
|
|
377
|
+
| Property | Values |
|
|
378
|
+
|---|---|
|
|
379
|
+
| `display` | `flex`, `none` |
|
|
380
|
+
| `flex-direction` | `row`, `row-reverse`, `column`, `column-reverse` |
|
|
381
|
+
| `gap` | `8px` (sets both row and column gap) |
|
|
382
|
+
| `row-gap` / `column-gap` | `8px` (per-axis; overrides uniform `gap`) |
|
|
383
|
+
| `flex-grow` | `1` |
|
|
384
|
+
| `flex-shrink` | `0` |
|
|
385
|
+
| `flex` (shorthand) | `1`, `1 0 auto`, `none` |
|
|
386
|
+
| `align-items` | `flex-start`, `center`, `flex-end`, `stretch` |
|
|
387
|
+
| `align-self` | `flex-start`, `center`, `flex-end`, `stretch`, `baseline` |
|
|
388
|
+
| `align-content` | `flex-start`, `center`, `flex-end`, `stretch`, `space-between`, `space-around`, `space-evenly` |
|
|
389
|
+
| `justify-content` | `flex-start`, `center`, `flex-end`, `space-between`, `space-around`, `space-evenly` |
|
|
390
|
+
| `flex-wrap` | `wrap`, `nowrap`, `wrap-reverse` |
|
|
391
|
+
| `order` | `1`, `2`, ... |
|
|
392
|
+
| `position` | `relative`, `absolute`, `static` |
|
|
393
|
+
| `top` / `right` / `bottom` / `left` | `10px` |
|
|
394
|
+
| `z-index` | numeric layers; inherited by descendants |
|
|
395
|
+
|
|
396
|
+
`display: none` removes the element subtree from layout, drawing, and hit
|
|
397
|
+
testing while preserving generated node indices.
|
|
398
|
+
|
|
399
|
+
#### Colors
|
|
400
|
+
All standard CSS color formats are supported:
|
|
401
|
+
- `#rrggbb` — `#ff0000`
|
|
402
|
+
- `#rgb` — `#f00`
|
|
403
|
+
- `#rrggbbaa` — `#ff0000ff` (alpha ignored)
|
|
404
|
+
- `rgb(r,g,b)` — `rgb(255, 0, 0)`
|
|
405
|
+
- `rgba(r,g,b,a)` — `rgba(255, 0, 0, 0.5)` (alpha ignored)
|
|
406
|
+
- `hsl(h, s%, l%)` / `hsla(...)` — `hsl(240, 100%, 50%)`, `hsla(0 0% 0% / 0.05)`
|
|
407
|
+
- Named colors — `red`, `dodgerblue`, `limegreen`, `transparent`, ... (147 CSS named colors)
|
|
408
|
+
|
|
409
|
+
Both comma (`hsl(0, 0%, 0%)`) and CSS4 space (`hsl(0 0% 0%)`) syntaxes work.
|
|
410
|
+
The slash-alpha form (`hsl(0 0% 0% / 0.05)`) is honored in `box-shadow` alpha;
|
|
411
|
+
elsewhere alpha is ignored (no runtime blending on bare metal).
|
|
412
|
+
|
|
413
|
+
#### Typography
|
|
414
|
+
| Property | Values | Notes |
|
|
415
|
+
|---|---|---|
|
|
416
|
+
| `color` | any color | Text foreground color |
|
|
417
|
+
| `font-family` | `"MyFont"` | Uses a generated font when matched by `@font-face`; otherwise the built-in bitmap font |
|
|
418
|
+
| `font-size` | `16px` | Generated fonts are rasterized at this pixel size; bitmap text maps to GFX text size |
|
|
419
|
+
| `font` | `italic bold 18px DeviceSans` | Shorthand support for style, weight, size, and family |
|
|
420
|
+
| `text-align` | `left`, `center`, `right` | Horizontal alignment within the box |
|
|
421
|
+
| `text-decoration` | `underline`, `line-through`, `none` | Both may combine: `underline line-through` |
|
|
422
|
+
| `text-overflow` | `ellipsis`, `clip` | Truncates overflowing single-line text with `...` |
|
|
423
|
+
| `text-transform` | `uppercase`, `lowercase`, `capitalize`, `none` | Applied at transpile time |
|
|
424
|
+
| `line-height` | `1.5`, `150%`, `24px` | Line advance for wrapped text; `normal` = font default |
|
|
425
|
+
| `letter-spacing` | `2px`, `-1px` | Per-character advance adjustment |
|
|
426
|
+
| `white-space` | `normal`, `nowrap`, `pre`, `pre-line` | Controls word-wrap behavior |
|
|
427
|
+
| `font-weight` | `normal`, `bold`, `400`, `700` | Selects the matching `@font-face` variant when available |
|
|
428
|
+
| `font-style` | `normal`, `italic`, `oblique` | Selects the matching `@font-face` variant when available |
|
|
429
|
+
| `font-smoothing` | `antialiased`, `none` | Overrides display-level text antialiasing |
|
|
430
|
+
| `font-subset` | `exact`, `fallback` | Controls generated-font glyph selection |
|
|
431
|
+
|
|
432
|
+
### Fonts
|
|
433
|
+
|
|
434
|
+
Local TTF/OTF fonts can be referenced with `@font-face`. The transpiler does
|
|
435
|
+
not copy the whole font to the board. It reads the font at build time, rasterizes
|
|
436
|
+
only the glyphs needed by the UI, packs them as 4-bit alpha bitmap data, and
|
|
437
|
+
emits those tables into the firmware. On ESP32-class targets those generated
|
|
438
|
+
tables are `static const` data in flash/rodata; the original TTF/OTF file is not
|
|
439
|
+
held in RAM on the hardware.
|
|
440
|
+
|
|
441
|
+
#### Install a font in a project
|
|
442
|
+
|
|
443
|
+
Put font files somewhere inside the project, usually next to the `.ui.css` file
|
|
444
|
+
or under a local `fonts/` folder:
|
|
445
|
+
|
|
446
|
+
```text
|
|
447
|
+
src/
|
|
448
|
+
app.ui.html
|
|
449
|
+
app.ui.css
|
|
450
|
+
fonts/
|
|
451
|
+
DeviceSans-Regular.ttf
|
|
452
|
+
DeviceSans-Bold.ttf
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Reference them from CSS with paths relative to the `.ui.css` file:
|
|
456
|
+
|
|
457
|
+
```css
|
|
458
|
+
@font-face {
|
|
459
|
+
font-family: "DeviceSans";
|
|
460
|
+
src: url("fonts/DeviceSans.ttf");
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
#title {
|
|
464
|
+
font-family: "DeviceSans";
|
|
465
|
+
font-size: 24px;
|
|
466
|
+
font-smoothing: antialiased;
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Remote font URLs are not supported for embedded builds. Use local files so the
|
|
471
|
+
build is reproducible and does not depend on network access.
|
|
472
|
+
|
|
473
|
+
#### Declare variants
|
|
474
|
+
|
|
475
|
+
Declare each weight/style variant as its own `@font-face`. The UI compiler
|
|
476
|
+
chooses the closest matching variant for each node based on `font-family`,
|
|
477
|
+
`font-weight`, `font-style`, and `font-size`.
|
|
478
|
+
|
|
479
|
+
```css
|
|
480
|
+
@font-face {
|
|
481
|
+
font-family: "DeviceSans";
|
|
482
|
+
src: url("fonts/DeviceSans-Regular.ttf");
|
|
483
|
+
font-weight: 400;
|
|
484
|
+
font-style: normal;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
@font-face {
|
|
488
|
+
font-family: "DeviceSans";
|
|
489
|
+
src: url("fonts/DeviceSans-Bold.ttf");
|
|
490
|
+
font-weight: 700;
|
|
491
|
+
font-style: normal;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
@font-face {
|
|
495
|
+
font-family: "DeviceSans";
|
|
496
|
+
src: url("fonts/DeviceSans-Italic.ttf");
|
|
497
|
+
font-weight: 400;
|
|
498
|
+
font-style: italic;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
#title {
|
|
502
|
+
font-family: "DeviceSans";
|
|
503
|
+
font-size: 24px;
|
|
504
|
+
font-weight: bold;
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Every distinct `font-family` + resolved font file + `font-size` + variant becomes
|
|
509
|
+
one generated font asset. Reusing the same face and size across many nodes
|
|
510
|
+
shares one asset. Using the same face at `16px` and `24px` creates two assets
|
|
511
|
+
because each size is rasterized separately.
|
|
512
|
+
|
|
513
|
+
#### Exact subsetting and icon fonts
|
|
514
|
+
|
|
515
|
+
By default, generated fonts use `font-subset: exact`. Only the literal
|
|
516
|
+
characters found in static UI text, placeholders, and option labels are encoded.
|
|
517
|
+
This is useful for icon fonts and symbol fonts:
|
|
518
|
+
|
|
519
|
+
```html
|
|
520
|
+
<text id="wifiIcon">✓</text>
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
```css
|
|
524
|
+
@font-face {
|
|
525
|
+
font-family: "DeviceIcons";
|
|
526
|
+
src: url("fonts/device-icons.ttf");
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
#wifiIcon {
|
|
530
|
+
font-family: "DeviceIcons";
|
|
531
|
+
font-size: 20px;
|
|
532
|
+
font-subset: exact;
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
In this case, only the checkmark glyph is emitted for that font/size, not the
|
|
537
|
+
whole icon font and not the common ASCII set.
|
|
538
|
+
|
|
539
|
+
Generated font glyph lookup supports UTF-8 text for codepoints in the Basic
|
|
540
|
+
Multilingual Plane (`U+0000` to `U+FFFF`). Many icon fonts use Private Use Area
|
|
541
|
+
codepoints such as `U+E000`; those are supported. Emoji and other characters
|
|
542
|
+
above `U+FFFF` are not currently supported by the generated-font runtime.
|
|
543
|
+
|
|
544
|
+
Wingdings-style fonts can work, but be careful: some older symbol fonts use
|
|
545
|
+
legacy character mappings rather than standard Unicode symbols. Copy the exact
|
|
546
|
+
character/codepoint that the font maps to the glyph you want, or prefer a
|
|
547
|
+
Unicode icon font when possible.
|
|
548
|
+
|
|
549
|
+
#### Dynamic text and fallback glyphs
|
|
550
|
+
|
|
551
|
+
Exact subsetting can only see text known at build time. If a generated font is
|
|
552
|
+
used on a node whose text changes at runtime, include a fallback character set:
|
|
553
|
+
|
|
554
|
+
```css
|
|
555
|
+
#counter {
|
|
556
|
+
font-family: "DeviceSans";
|
|
557
|
+
font-size: 18px;
|
|
558
|
+
font-subset: fallback;
|
|
559
|
+
}
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
`font-subset: fallback` includes the static text plus a small common ASCII set
|
|
563
|
+
containing digits, letters, spaces, and punctuation. Use it for counters,
|
|
564
|
+
formatted numeric values, input fields, or any generated-font text binding that
|
|
565
|
+
can produce characters not present in the initial HTML.
|
|
566
|
+
|
|
567
|
+
If the node uses the built-in bitmap font, `font-subset` has no effect.
|
|
568
|
+
|
|
569
|
+
#### Smoothing
|
|
570
|
+
|
|
571
|
+
Generated TTF/OTF glyphs are rasterized as alpha masks. Use
|
|
572
|
+
`font-smoothing: antialiased` to blend edge pixels for smoother text on RGB
|
|
573
|
+
displays. Use `font-smoothing: none` to threshold the same glyph masks for a
|
|
574
|
+
sharper, more pixel-like look. On monochrome displays smoothing is disabled.
|
|
575
|
+
|
|
576
|
+
```css
|
|
577
|
+
.smooth {
|
|
578
|
+
font-family: "DeviceSans";
|
|
579
|
+
font-size: 18px;
|
|
580
|
+
font-smoothing: antialiased;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.sharp {
|
|
584
|
+
font-family: "DeviceSans";
|
|
585
|
+
font-size: 18px;
|
|
586
|
+
font-smoothing: none;
|
|
587
|
+
}
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
#### Converting fonts
|
|
591
|
+
|
|
592
|
+
Use TTF or OTF files when possible. WOFF/WOFF2 web fonts should be converted to
|
|
593
|
+
TTF/OTF before use.
|
|
594
|
+
|
|
595
|
+
Common conversion options:
|
|
596
|
+
|
|
597
|
+
- FontForge GUI: open the source font, then use `File -> Generate Fonts...` and
|
|
598
|
+
choose TrueType (`.ttf`) or OpenType (`.otf`).
|
|
599
|
+
- FontForge CLI:
|
|
600
|
+
|
|
601
|
+
```bash
|
|
602
|
+
fontforge -lang=ff -c 'Open($1); Generate($2)' input.otf output.ttf
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
- WOFF2 tools: use `woff2_decompress input.woff2` to produce a TTF-flavored
|
|
606
|
+
font when the source is a WOFF2 web font.
|
|
607
|
+
- fonttools can inspect and subset fonts:
|
|
608
|
+
|
|
609
|
+
```bash
|
|
610
|
+
python -m pip install fonttools brotli
|
|
611
|
+
pyftsubset DeviceSans.ttf --text="ABC123" --unicodes=U+2713 --output-file=DeviceSans-subset.ttf
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
Manual external subsetting is optional. TypeCAD already subsets the emitted
|
|
615
|
+
hardware glyphs. External subsetting is mainly useful when you need to distribute
|
|
616
|
+
a smaller source font file, remove unused font tables for licensing reasons, or
|
|
617
|
+
speed up build-time parsing of a very large font.
|
|
618
|
+
|
|
619
|
+
#### Licensing
|
|
620
|
+
|
|
621
|
+
Do not assume system fonts are redistributable. Fonts such as commercial OS
|
|
622
|
+
fonts may be licensed for local use but not for checking into a repository or
|
|
623
|
+
shipping in a firmware project. Prefer open-licensed fonts, or keep proprietary
|
|
624
|
+
fonts outside shared source control if your license requires it.
|
|
625
|
+
|
|
626
|
+
#### Visual
|
|
627
|
+
| Property | Values | Notes |
|
|
628
|
+
|---|---|---|
|
|
629
|
+
| `background` / `background-color` | any color | Fill color |
|
|
630
|
+
| `border` (shorthand) | `2px solid #808080` | Splits into width/style/color |
|
|
631
|
+
| `border-width` | `2px` | |
|
|
632
|
+
| `border-color` | any color | |
|
|
633
|
+
| `border-style` | `solid`, `dashed`, `none` | Dashed approximated with segments |
|
|
634
|
+
| `border-radius` | `4px` | Rounded fill/border on hardware; preview approximates |
|
|
635
|
+
| `outline` | `1px solid #fff`, `2px dashed red` | Drawn outside the element box |
|
|
636
|
+
| `visibility` | `visible`, `hidden` | Hidden elements are not drawn |
|
|
637
|
+
| `box-shadow` | `inset 0 1px 0 #fff`, `0 10px 0 #333` | Up to 4 rect shadows; approximated for TFT drawing |
|
|
638
|
+
| `transform` | `translateY(10px)`, `translate(0, 10px)` | Draw-time translate offset; no flex relayout |
|
|
639
|
+
| `opacity` | parsed | (Blending not supported without PSRAM framebuffer) |
|
|
640
|
+
|
|
641
|
+
#### Transitions
|
|
642
|
+
| Property | Values | Notes |
|
|
643
|
+
|---|---|---|
|
|
644
|
+
| `transition` | `background 300ms`, `color 120ms` | Lerps the property over the duration |
|
|
645
|
+
| `:pressed` | pseudo-class | Applied when `.value` is 1 (button press) |
|
|
646
|
+
|
|
647
|
+
Pressed rules may also include `top` / `left` / `right` / `bottom` or
|
|
648
|
+
`transform: translate(...)`. These are applied as draw-time offsets so the
|
|
649
|
+
element face/content can move visually without recomputing the flex layout;
|
|
650
|
+
outset shadows stay anchored, which is useful for raised button effects.
|
|
651
|
+
|
|
652
|
+
Small dirty paint regions for text, backgrounds, borders, outlines, shadows,
|
|
653
|
+
and draw-time translate offsets are composed in an offscreen RGB565 canvas and
|
|
654
|
+
pushed as one rectangle when memory allows. Larger regions fall back to direct
|
|
655
|
+
drawing.
|
|
656
|
+
|
|
657
|
+
### Selectors
|
|
658
|
+
- Element: `screen { ... }`
|
|
659
|
+
- ID: `#title { ... }`
|
|
660
|
+
- Class: `.card { ... }`
|
|
661
|
+
- Compound: `.card.active { ... }`, `button.primary { ... }`
|
|
662
|
+
- Descendant: `view text { ... }`
|
|
663
|
+
- Child: `view > text { ... }` (direct children only)
|
|
664
|
+
- Adjacent sibling: `.first + .second { ... }` (immediate next sibling)
|
|
665
|
+
- General sibling: `.first ~ .later { ... }` (any following sibling)
|
|
666
|
+
- Attribute: `[disabled]`, `[type="number"]` (presence and exact-value match)
|
|
667
|
+
- Negation: `button:not(.disabled)`, `.a:not(.b.c)` (compound `:not()` supported)
|
|
668
|
+
- Pseudo-state: `#btn:pressed`, `input:disabled`, `check:checked`, `*:focus`
|
|
669
|
+
- Inline style: `<text style="color: red">hi</text>`
|
|
670
|
+
- `<style>` blocks embedded in the `.ui.html`
|
|
671
|
+
|
|
672
|
+
Pseudo-states match runtime element state: `:pressed` (button held), `:disabled`
|
|
673
|
+
(`disabled` attribute), `:checked` (`<check>`/`<radio>` with `.value` 1), and
|
|
674
|
+
`:focus` (the node currently receiving input).
|
|
675
|
+
|
|
676
|
+
### CSS variables
|
|
677
|
+
|
|
678
|
+
Define variables in `:root` and reference them with `var()`:
|
|
679
|
+
|
|
680
|
+
```css
|
|
681
|
+
:root {
|
|
682
|
+
--bg: #0a0a0a;
|
|
683
|
+
--fg: #fafafa;
|
|
684
|
+
--primary: #7c3aed;
|
|
685
|
+
}
|
|
686
|
+
screen { background: var(--bg); }
|
|
687
|
+
#title { color: var(--fg); }
|
|
688
|
+
```
|
|
689
|
+
|
|
690
|
+
Variables resolve at transpile time — no runtime cost.
|
|
691
|
+
|
|
692
|
+
#### Class-scoped variables (themes)
|
|
693
|
+
|
|
694
|
+
Variables can also be defined under a class selector (e.g. `.dark`) and selected
|
|
695
|
+
at build time via the `themeClass` config option. This is how shadcn-style
|
|
696
|
+
light/dark themes work:
|
|
697
|
+
|
|
698
|
+
```css
|
|
699
|
+
:root { --bg: #ffffff; --fg: #0a0a0a; }
|
|
700
|
+
.dark { --bg: #0a0a0a; --fg: #fafafa; }
|
|
701
|
+
screen { background: var(--bg); color: var(--fg); }
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
```typescript
|
|
705
|
+
// cuttlefish.config.ts — or the display config in ui.mount
|
|
706
|
+
display: {
|
|
707
|
+
themeClass: 'dark', // resolves var(--x) using the .dark overrides
|
|
708
|
+
}
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
When `themeClass` is set, `var()` substitution prefers that class's variables
|
|
712
|
+
over `:root`. This is **transpile-time selection** — one theme per firmware
|
|
713
|
+
build (there is no runtime theme switch on a fixed-screen device).
|
|
714
|
+
|
|
715
|
+
#### `@media` (compile-time variant selection)
|
|
716
|
+
|
|
717
|
+
`@media` rules are evaluated against the resolved display profile at transpile
|
|
718
|
+
time. Since each build targets one fixed screen size, this acts as a
|
|
719
|
+
compile-time variant selector, not responsive design:
|
|
720
|
+
|
|
721
|
+
```css
|
|
722
|
+
/* Applied only when the display is ≤ 240px wide */
|
|
723
|
+
@media (max-width: 240px) {
|
|
724
|
+
#title { font-size: 12px; }
|
|
725
|
+
}
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
Supported conditions: `min-width`, `max-width`, `min-height`, `max-height`
|
|
729
|
+
(in `px`). Unsupported conditions (e.g. `orientation`) emit a warning and the
|
|
730
|
+
rule is skipped. `@import` and `@supports` are not supported (warned + skipped).
|
|
731
|
+
|
|
732
|
+
### Theming
|
|
733
|
+
|
|
734
|
+
Themes are **compile-time**. There are two complementary mechanisms:
|
|
735
|
+
|
|
736
|
+
**1. Theme file (`themeCss`)** — swap the entire CSS file:
|
|
737
|
+
|
|
738
|
+
```typescript
|
|
739
|
+
// cuttlefish.config.ts
|
|
740
|
+
display: {
|
|
741
|
+
themeCss: './src/hello.dark.css', // relative to .ui.html dir
|
|
742
|
+
// or: themeCss: '/absolute/path/to/theme.css',
|
|
743
|
+
}
|
|
744
|
+
```
|
|
745
|
+
|
|
746
|
+
When `themeCss` is set, that file replaces the default sibling `.ui.css`. Use CSS variables to define a palette once, then swap the variable file for different themes:
|
|
747
|
+
|
|
748
|
+
```
|
|
749
|
+
src/
|
|
750
|
+
hello.ui.html ← layout (shared)
|
|
751
|
+
hello.ui.css ← default theme (no themeCss set)
|
|
752
|
+
hello.dark.css ← dark theme
|
|
753
|
+
hello.shadcn.css ← shadcn palette
|
|
754
|
+
```
|
|
755
|
+
|
|
756
|
+
**2. Theme class (`themeClass`)** — select a class-scoped variable block
|
|
757
|
+
within a single CSS file (see [Class-scoped variables](#class-scoped-variables-themes) above):
|
|
758
|
+
|
|
759
|
+
```typescript
|
|
760
|
+
display: {
|
|
761
|
+
themeClass: 'dark', // resolves var(--x) from .dark { ... } overrides
|
|
762
|
+
}
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
The two can be combined: `themeCss` picks the file, `themeClass` picks the
|
|
766
|
+
variable scope within it.
|
|
767
|
+
|
|
768
|
+
The `.ui.html` file defines the structure (elements, IDs, layout); the CSS file defines the appearance (colors, fonts, borders, shadows). Swap either in config without touching the HTML.
|
|
769
|
+
|
|
770
|
+
### Unsupported (and why)
|
|
771
|
+
- `display: grid` — needs a GridLayoutEngine
|
|
772
|
+
- Full inline rich text — basic wrapping, `line-height`, `white-space`, and `<br>` are supported; mixed inline spans are not
|
|
773
|
+
- `background-image` / sprites — use `<img>` for embedded images; CSS `background: url(...)` is unsupported (only solid colors and `linear-gradient`)
|
|
774
|
+
- `position: fixed` — viewport-fixed positioning is not implemented
|
|
775
|
+
- `:after` / `:before` pseudo-elements — no generated content
|
|
776
|
+
- `text-shadow` on built-in font — needs sub-pixel font data (works with custom fonts)
|
|
777
|
+
- Per-corner `border-radius` — only a uniform radius is supported (Adafruit_GFX draws one corner value)
|
|
778
|
+
- Runtime theme switching — themes are compile-time only (one `themeClass` per build; swap in config and rebuild)
|
|
779
|
+
|
|
780
|
+
## State and interaction
|
|
781
|
+
|
|
782
|
+
### The `.value` property
|
|
783
|
+
|
|
784
|
+
Every interactive element has a `.value` property — a number that is both readable and writable:
|
|
785
|
+
|
|
786
|
+
```typescript
|
|
787
|
+
// Read
|
|
788
|
+
const count = screen.counter.value;
|
|
789
|
+
|
|
790
|
+
// Write (updates the display immediately)
|
|
791
|
+
screen.counter.value = 42;
|
|
792
|
+
```
|
|
793
|
+
|
|
794
|
+
### Pin input
|
|
795
|
+
|
|
796
|
+
```typescript
|
|
797
|
+
// Watch a pin for falling edges — runs in the frame loop (no ISR)
|
|
798
|
+
ui.watchPin(4, () => {
|
|
799
|
+
screen.counter.value = screen.counter.value + 1;
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
// Toggle an element's .value on pin press (0 ↔ 1)
|
|
803
|
+
screen.ledBox.onToggle(5);
|
|
804
|
+
|
|
805
|
+
// Cycle through options (0 → 1 → 2 → 0 → ...)
|
|
806
|
+
screen.modeSelect.onChange(15, 3);
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
### Reactive bindings
|
|
810
|
+
|
|
811
|
+
Bindings compute a display property from `.value` or signals each frame:
|
|
812
|
+
|
|
813
|
+
```typescript
|
|
814
|
+
// Color binding
|
|
815
|
+
ui.bind(screen.counter, 'color', () =>
|
|
816
|
+
(screen.counter.value % 2 === 0 ? 'limegreen' : 'orange')
|
|
817
|
+
);
|
|
818
|
+
|
|
819
|
+
// Background binding
|
|
820
|
+
ui.bind(screen.btn, 'background', () =>
|
|
821
|
+
(screen.btn.value > 0 ? 'limegreen' : 'darkgreen')
|
|
822
|
+
);
|
|
823
|
+
|
|
824
|
+
// Border color binding
|
|
825
|
+
ui.bind(screen.ledBox, 'borderColor', () =>
|
|
826
|
+
(screen.ledBox.value ? 'limegreen' : '#808080')
|
|
827
|
+
);
|
|
828
|
+
|
|
829
|
+
// Value binding — drive a progress/range node live from loop()
|
|
830
|
+
ui.bind(screen.progress, 'value', () => sensorPercent);
|
|
831
|
+
|
|
832
|
+
// Visibility binding - preallocate both branches and toggle which one draws
|
|
833
|
+
ui.bind(screen.enteredBranch, 'visible', () => screen.input.value > 0);
|
|
834
|
+
ui.bind(screen.emptyBranch, 'visible', () => screen.input.value === 0);
|
|
835
|
+
|
|
836
|
+
// Text binding — number to string
|
|
837
|
+
ui.bind(screen.counter, 'text', () => String(screen.counter.value));
|
|
838
|
+
|
|
839
|
+
// Text binding — ternary chain (for selectors)
|
|
840
|
+
ui.bind(screen.modeValue, 'text', () => (
|
|
841
|
+
screen.modeValue.value === 0 ? 'Auto' :
|
|
842
|
+
screen.modeValue.value === 1 ? 'Manual' : 'Off'
|
|
843
|
+
));
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
`visible` bindings are for fixed-layout conditional rendering. The nodes stay in
|
|
847
|
+
the retained UI tree; hidden branches are skipped for drawing and hit testing,
|
|
848
|
+
and shown branches repaint their subtree.
|
|
849
|
+
|
|
850
|
+
### Two-way input binding
|
|
851
|
+
|
|
852
|
+
When the user types into an `<input>` via the on-screen keyboard, push the text
|
|
853
|
+
back into app state with `ui.bindInput`:
|
|
854
|
+
|
|
855
|
+
```typescript
|
|
856
|
+
let ssid = '';
|
|
857
|
+
ui.bindInput(screen.ssid, (text) => { ssid = text; });
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
The callback fires whenever the input's text changes (after the keyboard commits).
|
|
861
|
+
|
|
862
|
+
### Slider change callbacks
|
|
863
|
+
|
|
864
|
+
A `<range>` fires `onChange` on every value change while dragging — read
|
|
865
|
+
`.value` inside the callback for the new value:
|
|
866
|
+
|
|
867
|
+
```typescript
|
|
868
|
+
screen.brightness.onChange(() => {
|
|
869
|
+
// Fires continuously during the drag.
|
|
870
|
+
ledPwm = screen.brightness.value;
|
|
871
|
+
});
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
### Data-bound lists
|
|
875
|
+
|
|
876
|
+
`<list>` is a virtualized, callback-driven list — it renders only the visible
|
|
877
|
+
items to a dedicated scroll canvas, so a thousand-item list has the same memory
|
|
878
|
+
footprint as a ten-item one. Bind it with `ui.bindList`:
|
|
879
|
+
|
|
880
|
+
```html
|
|
881
|
+
<list id="networks" item-height="28px"></list>
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
```typescript
|
|
885
|
+
ui.bindList(
|
|
886
|
+
screen.networks,
|
|
887
|
+
() => scanResults.length, // count
|
|
888
|
+
(i) => `${scanResults[i].ssid} (${scanResults[i].rssi} dBm)`, // item text
|
|
889
|
+
(i) => { connectTo(scanResults[i].ssid); }, // optional tap handler
|
|
890
|
+
);
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
The count function re-evaluates each frame; if it changes, the list recomputes
|
|
894
|
+
its content height and repaints. Drag to scroll; tap an item to fire the
|
|
895
|
+
optional third callback.
|
|
896
|
+
|
|
897
|
+
### User-drawn canvas (`<canvas>`)
|
|
898
|
+
|
|
899
|
+
`<canvas>` is an element whose contents you draw yourself, every frame, using
|
|
900
|
+
the display graphics primitives. It follows all CSS rules (layout, borders,
|
|
901
|
+
transforms, z-index) like any other element, but its pixels come from your
|
|
902
|
+
callback. Use it for sparkline graphs, analog gauges, or custom-shaped controls.
|
|
903
|
+
|
|
904
|
+
```html
|
|
905
|
+
<canvas id="spark" width="120" height="40"></canvas>
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
`width`/`height` set the **drawing buffer** size (px). The CSS box is the
|
|
909
|
+
**layout** size — size them to match unless you want clipping.
|
|
910
|
+
|
|
911
|
+
```typescript
|
|
912
|
+
ui.drawCanvas(screen.spark, (ctx) => {
|
|
913
|
+
ctx.fillScreen('black');
|
|
914
|
+
ctx.line(0, 30, ctx.width, 30, 'limegreen'); // baseline
|
|
915
|
+
ctx.rect(2, 2, ctx.width - 4, ctx.height - 4, '#333');
|
|
916
|
+
ctx.fillCircle(needleX, 30, 3, 'red');
|
|
917
|
+
ctx.text(4, 12, `${temp}°`, 'white'); // optional color arg
|
|
918
|
+
});
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
Coordinates are **canvas-relative** (`(0,0)` = element top-left) and drawing is
|
|
922
|
+
**auto-clipped** to the buffer — you cannot accidentally paint over neighbors.
|
|
923
|
+
Color arguments are CSS color strings resolved to RGB565 at build time.
|
|
924
|
+
|
|
925
|
+
The callback runs **every frame**; to animate, mutate state in a `setInterval`
|
|
926
|
+
or signal and the canvas picks it up next frame. Taps hit-test as the full CSS
|
|
927
|
+
box, so `screen.spark.onClick(...)` works for interactive canvases.
|
|
928
|
+
|
|
929
|
+
#### `ctx` methods (the display graphics primitives)
|
|
930
|
+
|
|
931
|
+
| Method | Notes |
|
|
932
|
+
|---|---|
|
|
933
|
+
| `ctx.fillRect(x,y,w,h,color)` / `ctx.rect(...)` | Filled / outline rectangle |
|
|
934
|
+
| `ctx.fillRoundRect(x,y,w,h,r,color)` / `ctx.roundRect(...)` | Rounded variant |
|
|
935
|
+
| `ctx.line(x0,y0,x1,y1,color)` | Arbitrary line |
|
|
936
|
+
| `ctx.hline(x,y,w,color)` / `ctx.vline(x,y,h,color)` | Fast horizontal / vertical line |
|
|
937
|
+
| `ctx.fillCircle(x,y,r,color)` / `ctx.circle(...)` | Filled / outline circle |
|
|
938
|
+
| `ctx.drawPixel(x,y,color)` | Single pixel |
|
|
939
|
+
| `ctx.text(x,y,str,color?)` | Bitmap text (built-in font) |
|
|
940
|
+
| `ctx.fillScreen(color)` | Clear the whole buffer |
|
|
941
|
+
| `ctx.width` / `ctx.height` | Read-only buffer dimensions |
|
|
942
|
+
|
|
943
|
+
### Signals
|
|
944
|
+
|
|
945
|
+
For reactive state not tied to an element:
|
|
946
|
+
|
|
947
|
+
```typescript
|
|
948
|
+
const temperature = ui.signal(22);
|
|
949
|
+
|
|
950
|
+
// Read
|
|
951
|
+
const t = temperature();
|
|
952
|
+
|
|
953
|
+
// Write
|
|
954
|
+
temperature.set(25);
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
`ui.signal()` accepts `number`, `string`, or `boolean` literals — these lower to
|
|
958
|
+
`int`/`double`, `const char*`, and `bool` on the device. Other initializers
|
|
959
|
+
(objects, arrays, `null`, identifiers) are rejected at type-check time
|
|
960
|
+
(`Signal<T extends SignalValue>`) and at build time with a `ui-signal-initializer`
|
|
961
|
+
warning that defaults the signal to `0` (`int`).
|
|
962
|
+
|
|
963
|
+
## Element id errors
|
|
964
|
+
|
|
965
|
+
If a binding or event handler references an element id that doesn't exist in the
|
|
966
|
+
screen, the build fails with a `ui-unknown-element` error rather than silently
|
|
967
|
+
re-targeting the wrong node. This applies to every call that takes a screen
|
|
968
|
+
element:
|
|
969
|
+
|
|
970
|
+
```typescript
|
|
971
|
+
ui.bind(screen.typo, 'color', ...) // ✗ error: element "typo" not found
|
|
972
|
+
ui.bindInput(screen.typo, ...) // ✗ error
|
|
973
|
+
ui.bindList(screen.typo, ...) // ✗ error
|
|
974
|
+
ui.drawCanvas(screen.typo, ...) // ✗ error
|
|
975
|
+
await ui.onTap(screen.typo) // ✗ error (no silent fallback to any-tap)
|
|
976
|
+
screen.typo.onClick(...) // ✗ error
|
|
977
|
+
screen.typo.onToggle(...) // ✗ error
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
Fix the typo in your `.ui.html` / `.ui` file's `id` attribute and rebuild.
|
|
981
|
+
|
|
982
|
+
## Composing custom elements
|
|
983
|
+
|
|
984
|
+
Don't see the element you need? Build it from `<view>` + `<text>` + bindings:
|
|
985
|
+
|
|
986
|
+
### Custom checkbox
|
|
987
|
+
|
|
988
|
+
```html
|
|
989
|
+
<view id="ledRow">
|
|
990
|
+
<view id="ledBox"></view>
|
|
991
|
+
<text id="ledLabel">Enable LED</text>
|
|
992
|
+
</view>
|
|
993
|
+
```
|
|
994
|
+
```css
|
|
995
|
+
#ledBox {
|
|
996
|
+
width: 16px;
|
|
997
|
+
height: 16px;
|
|
998
|
+
border: 2px solid #808080;
|
|
999
|
+
}
|
|
1000
|
+
```
|
|
1001
|
+
```typescript
|
|
1002
|
+
screen.ledBox.onToggle(5); // toggles .value 0↔1
|
|
1003
|
+
|
|
1004
|
+
ui.bind(screen.ledBox, 'background', () =>
|
|
1005
|
+
(screen.ledBox.value ? 'limegreen' : 'transparent')
|
|
1006
|
+
);
|
|
1007
|
+
ui.bind(screen.ledBox, 'borderColor', () =>
|
|
1008
|
+
(screen.ledBox.value ? 'limegreen' : '#808080')
|
|
1009
|
+
);
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
### Custom selector
|
|
1013
|
+
|
|
1014
|
+
```html
|
|
1015
|
+
<view id="modeRow">
|
|
1016
|
+
<text id="modeLabel">Mode:</text>
|
|
1017
|
+
<text id="modeValue">Auto</text>
|
|
1018
|
+
</view>
|
|
1019
|
+
```
|
|
1020
|
+
```typescript
|
|
1021
|
+
screen.modeValue.onChange(15, 3); // cycles 0→1→2→0
|
|
1022
|
+
|
|
1023
|
+
ui.bind(screen.modeValue, 'text', () => (
|
|
1024
|
+
screen.modeValue.value === 0 ? 'Auto' :
|
|
1025
|
+
screen.modeValue.value === 1 ? 'Manual' : 'Off'
|
|
1026
|
+
));
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
### Custom progress bar
|
|
1030
|
+
|
|
1031
|
+
```html
|
|
1032
|
+
<view id="barContainer">
|
|
1033
|
+
<view id="barFill"></view>
|
|
1034
|
+
</view>
|
|
1035
|
+
```
|
|
1036
|
+
```css
|
|
1037
|
+
#barContainer { width: 200px; height: 20px; border: 1px solid #808080; }
|
|
1038
|
+
#barFill { background: limegreen; height: 100%; }
|
|
1039
|
+
```
|
|
1040
|
+
```typescript
|
|
1041
|
+
ui.bind(screen.barFill, 'background', () =>
|
|
1042
|
+
(screen.barFill.value > 50 ? 'limegreen' : 'orange')
|
|
1043
|
+
);
|
|
1044
|
+
```
|
|
1045
|
+
|
|
1046
|
+
## Timers
|
|
1047
|
+
|
|
1048
|
+
```typescript
|
|
1049
|
+
// Auto-update every 2 seconds
|
|
1050
|
+
setInterval(() => {
|
|
1051
|
+
screen.counter.value = screen.counter.value + 1;
|
|
1052
|
+
}, 2000);
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
## Architecture
|
|
1056
|
+
|
|
1057
|
+
```
|
|
1058
|
+
.ui.html / .ui.css → parse (linkedom + css-tree) → resolve styles
|
|
1059
|
+
↓
|
|
1060
|
+
layout (Yoga flexbox)
|
|
1061
|
+
↓
|
|
1062
|
+
lower to C++ UINode[] table
|
|
1063
|
+
↓
|
|
1064
|
+
ui_mount → display.init (Adafruit_ILI9341)
|
|
1065
|
+
ui_tick → poll inputs → eval bindings → transitions → draw
|
|
1066
|
+
ui_init → mark all dirty for first frame
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
The runtime is a retained-mode tree: the HTML/CSS is fully resolved at transpile time. The device only sees static tables + a tiny draw loop. No DOM, no CSS engine, no HTML parser on the MCU.
|
|
1070
|
+
|
|
1071
|
+
### Rendering & performance
|
|
1072
|
+
|
|
1073
|
+
Each frame, `ui_tick` re-evaluates bindings, advances transitions/animations,
|
|
1074
|
+
and redraws only the nodes marked dirty (most frames touch a handful of nodes,
|
|
1075
|
+
not the whole screen). Dirty paint regions are composed in an offscreen RGB565
|
|
1076
|
+
canvas and pushed as one rectangle when memory allows.
|
|
1077
|
+
|
|
1078
|
+
**Framebuffer (PSRAM-gated).** When the board has PSRAM (`BOARD_HAS_PSRAM`
|
|
1079
|
+
defined + `psramFound()`), the runtime allocates a full-screen `GFXcanvas16`
|
|
1080
|
+
framebuffer and renders the entire dirty-node pass into it, then pushes once
|
|
1081
|
+
via a single SPI transaction. This eliminates the per-primitive transaction
|
|
1082
|
+
storm that otherwise limits redraw rate on ILI9341 over SPI. Without PSRAM the
|
|
1083
|
+
runtime falls back to direct per-node drawing (no behavior change). The
|
|
1084
|
+
framebuffer activates automatically — no config needed beyond enabling PSRAM in
|
|
1085
|
+
the Arduino build flags.
|
|
1086
|
+
|
|
1087
|
+
### Diagnostics (warnings)
|
|
1088
|
+
|
|
1089
|
+
Unknown HTML tags and unknown CSS properties are **reported as warnings**, not
|
|
1090
|
+
silently dropped. They appear in the build output (yellow, to stderr) and do
|
|
1091
|
+
not abort the build:
|
|
1092
|
+
|
|
1093
|
+
```
|
|
1094
|
+
⚠ Unknown CSS property "bogus-prop" — ignored.
|
|
1095
|
+
⚠ Unknown HTML tag <marquee> — ignored.
|
|
1096
|
+
⚠ Unsupported @media (orientation: portrait) has an unsupported condition — rule ignored.
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
This surfaces typos and unsupported features early instead of leaving styles
|
|
1100
|
+
mysteriously unapplied.
|
|
1101
|
+
|
|
1102
|
+
## Display profiles
|
|
1103
|
+
|
|
1104
|
+
The display hardware is described in `cuttlefish.config.ts` under the `display` field. This drives all transpile-time decisions: dimensions, color format, rotation, SPI pins, backlight, and touch.
|
|
1105
|
+
|
|
1106
|
+
### Config reference
|
|
1107
|
+
|
|
1108
|
+
```typescript
|
|
1109
|
+
// cuttlefish.config.ts
|
|
1110
|
+
display: {
|
|
1111
|
+
// Either reference a built-in profile by name:
|
|
1112
|
+
profile: 'ili9341-spi',
|
|
1113
|
+
|
|
1114
|
+
// Or inline everything:
|
|
1115
|
+
// driver: 'ili9341',
|
|
1116
|
+
// width: 320, height: 240,
|
|
1117
|
+
// colorFormat: 'rgb565',
|
|
1118
|
+
// rotation: 1,
|
|
1119
|
+
|
|
1120
|
+
// Wiring (always project-specific)
|
|
1121
|
+
cs: 5,
|
|
1122
|
+
dc: 21,
|
|
1123
|
+
rst: 22,
|
|
1124
|
+
backlight: 17, // optional — pin number for backlight
|
|
1125
|
+
|
|
1126
|
+
// Antialiasing (optional)
|
|
1127
|
+
antialias: true, // smooths shapes and text; text can opt out with font-smoothing:none
|
|
1128
|
+
|
|
1129
|
+
// Theming (optional — compile-time)
|
|
1130
|
+
themeCss: './src/hello.dark.css', // override the sibling .ui.css file
|
|
1131
|
+
themeClass: 'dark', // select a class-scoped variable block (.dark { ... })
|
|
1132
|
+
|
|
1133
|
+
// Touch (optional)
|
|
1134
|
+
touch: {
|
|
1135
|
+
library: 'XPT2046_Touchscreen',
|
|
1136
|
+
cs: 14, // touch controller CS pin
|
|
1137
|
+
irq: 2, // optional — interrupt pin
|
|
1138
|
+
calibration: { xMin: 375, xMax: 3950, yMin: 200, yMax: 3750 },
|
|
1139
|
+
minPressure: 10,
|
|
1140
|
+
},
|
|
1141
|
+
}
|
|
1142
|
+
```
|
|
1143
|
+
|
|
1144
|
+
### Profile fields
|
|
1145
|
+
|
|
1146
|
+
| Field | Type | Description |
|
|
1147
|
+
|---|---|---|
|
|
1148
|
+
| `profile` | string | Built-in profile name (e.g. `"ili9341-spi"`) |
|
|
1149
|
+
| `driver` | string | Display driver id (e.g. `"ili9341"`) |
|
|
1150
|
+
| `width` | number | Display width in pixels (after rotation) |
|
|
1151
|
+
| `height` | number | Display height in pixels (after rotation) |
|
|
1152
|
+
| `colorFormat` | `"rgb565"` \| `"mono"` | Color depth |
|
|
1153
|
+
| `rotation` | number | 0=portrait, 1=landscape, 2-3=inverted |
|
|
1154
|
+
| `backlight` | number | Backlight pin (optional) |
|
|
1155
|
+
| `cs` / `dc` / `rst` | number | Display wiring pins |
|
|
1156
|
+
|
|
1157
|
+
### Built-in profiles
|
|
1158
|
+
|
|
1159
|
+
| Name | Display | Dimensions | Color | Touch |
|
|
1160
|
+
|---|---|---|---|---|
|
|
1161
|
+
| `ili9341-spi` | ILI9341 (SPI) | 320×240 | RGB565 | Add via `touch` config |
|
|
1162
|
+
| `ssd1309-i2c` | SSD1309 OLED (I2C) | 128×64 | Mono | None |
|
|
1163
|
+
|
|
1164
|
+
### Adding a new display
|
|
1165
|
+
|
|
1166
|
+
Adding a new display driver requires two parts: a **display profile** (the hardware config) and a **display adapter** (the generated C++ code that drives it).
|
|
1167
|
+
|
|
1168
|
+
#### 1. Register a display adapter
|
|
1169
|
+
|
|
1170
|
+
A display adapter is a TypeScript function that generates C++ code for a specific driver. Register it in a module that runs before the build:
|
|
1171
|
+
|
|
1172
|
+
```typescript
|
|
1173
|
+
// my-project/display-adapters.ts
|
|
1174
|
+
import { registerDisplayAdapter } from '@typecad/cuttlefish/api/shared/display-adapter';
|
|
1175
|
+
|
|
1176
|
+
registerDisplayAdapter('ssd1306', (display) => {
|
|
1177
|
+
return {
|
|
1178
|
+
includes: `#include <Adafruit_GFX.h>\n#include <Adafruit_SSD1306.h>\n#include <Wire.h>`,
|
|
1179
|
+
declaration: `Adafruit_SSD1306 __tc_display(128, 64, &Wire, -1);`,
|
|
1180
|
+
functions: [
|
|
1181
|
+
'static int16_t __addrX = 0, __addrY = 0, __addrW = 0, __addrH = 0;',
|
|
1182
|
+
'static uint32_t __addrCursor = 0;',
|
|
1183
|
+
'static inline void display_init() {',
|
|
1184
|
+
' __tc_display.begin(SSD1306_SWITCHCAPVCC, 0x3C);',
|
|
1185
|
+
' __tc_display.clearDisplay();',
|
|
1186
|
+
' __tc_display.display();',
|
|
1187
|
+
'}',
|
|
1188
|
+
'static inline void display_fillScreen(uint16_t color) {',
|
|
1189
|
+
' __tc_display.fillScreen(color ? 1 : 0);',
|
|
1190
|
+
'}',
|
|
1191
|
+
'static inline void display_startWrite() { }',
|
|
1192
|
+
'static inline void display_endWrite() { }',
|
|
1193
|
+
'static inline void display_setAddrWindow(int16_t x, int16_t y, int16_t w, int16_t h) {',
|
|
1194
|
+
' __addrX = x; __addrY = y; __addrW = w; __addrH = h; __addrCursor = 0;',
|
|
1195
|
+
'}',
|
|
1196
|
+
'static inline void display_writePixels(uint16_t* pixels, uint32_t count) {',
|
|
1197
|
+
' if (__addrW <= 0 || __addrH <= 0) return;',
|
|
1198
|
+
' uint32_t total = (uint32_t)__addrW * (uint32_t)__addrH;',
|
|
1199
|
+
' for (uint32_t i = 0; i < count; i++) {',
|
|
1200
|
+
' if (__addrCursor >= total) break;',
|
|
1201
|
+
' uint32_t pos = __addrCursor++;',
|
|
1202
|
+
' __tc_display.drawPixel(__addrX + (pos % __addrW), __addrY + (pos / __addrW), pixels[i] ? 1 : 0);',
|
|
1203
|
+
' }',
|
|
1204
|
+
'}',
|
|
1205
|
+
'static inline void display_partial_refresh(int16_t x, int16_t y, int16_t w, int16_t h) {',
|
|
1206
|
+
' (void)x; (void)y; (void)w; (void)h;',
|
|
1207
|
+
' __tc_display.display();',
|
|
1208
|
+
'}',
|
|
1209
|
+
].join('\\n'),
|
|
1210
|
+
};
|
|
1211
|
+
});
|
|
1212
|
+
```
|
|
1213
|
+
|
|
1214
|
+
The runtime calls these core display functions:
|
|
1215
|
+
|
|
1216
|
+
| Function | Purpose |
|
|
1217
|
+
|----------|---------|
|
|
1218
|
+
| `display_init()` | Initialize the display (begin, rotation, clear) |
|
|
1219
|
+
| `display_fillScreen(color)` | Fill the entire screen with a color |
|
|
1220
|
+
| `display_startWrite()` | Begin an SPI transaction (no-op for I2C) |
|
|
1221
|
+
| `display_endWrite()` | End a transaction (often a no-op for page-buffered I2C) |
|
|
1222
|
+
| `display_setAddrWindow(x, y, w, h)` | Set the active write region |
|
|
1223
|
+
| `display_writePixels(pixels, count)` | Write pixels into the active region |
|
|
1224
|
+
| `display_partial_refresh(x, y, w, h)` | Publish the dirty region on deferred displays |
|
|
1225
|
+
|
|
1226
|
+
For monochrome displays, the adapter wraps each color argument with a conversion function. For deferred displays, the current runtime publishes from `display_partial_refresh()`.
|
|
1227
|
+
|
|
1228
|
+
#### 2. Create a display profile
|
|
1229
|
+
|
|
1230
|
+
```typescript
|
|
1231
|
+
// displays/my-display.ts
|
|
1232
|
+
import type { DisplayProfile } from '@typecad/cuttlefish/api/shared';
|
|
1233
|
+
|
|
1234
|
+
export const MY_DISPLAY: DisplayProfile = {
|
|
1235
|
+
driver: 'ssd1306', // must match the adapter name
|
|
1236
|
+
width: 128,
|
|
1237
|
+
height: 64,
|
|
1238
|
+
colorFormat: 'mono', // 'rgb565' or 'mono'
|
|
1239
|
+
rotation: 0,
|
|
1240
|
+
};
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1243
|
+
#### 3. Reference it in config
|
|
1244
|
+
|
|
1245
|
+
```typescript
|
|
1246
|
+
display: {
|
|
1247
|
+
driver: 'ssd1306',
|
|
1248
|
+
width: 128, height: 64,
|
|
1249
|
+
colorFormat: 'mono',
|
|
1250
|
+
rotation: 0,
|
|
1251
|
+
}
|
|
1252
|
+
```
|
|
1253
|
+
|
|
1254
|
+
#### 4. Import the adapter module before building
|
|
1255
|
+
|
|
1256
|
+
Make sure your adapter module is imported (side-effect import) so the registration runs:
|
|
1257
|
+
|
|
1258
|
+
```typescript
|
|
1259
|
+
// cuttlefish.config.ts or main.ts
|
|
1260
|
+
import './display-adapters'; // registers the 'ssd1306' adapter
|
|
1261
|
+
```
|
|
1262
|
+
|
|
1263
|
+
#### Built-in adapters
|
|
1264
|
+
|
|
1265
|
+
| Driver | Display | Color | Notes |
|
|
1266
|
+
|--------|---------|-------|-------|
|
|
1267
|
+
| `ili9341` | ILI9341 (320×240) | RGB565 | Default, hardware SPI |
|
|
1268
|
+
|
|
1269
|
+
To add more built-in adapters, contribute a file to `packages/framework-arduino/src/graphics/` and register it.
|
|
1270
|
+
|
|
1271
|
+
## Touch input
|
|
1272
|
+
|
|
1273
|
+
Touch is configured via the `touch` field in the display profile. The system uses an adapter pattern: built-in libraries generate C++ automatically; custom libraries use a TypeScript adapter file.
|
|
1274
|
+
|
|
1275
|
+
### Built-in touch libraries
|
|
1276
|
+
|
|
1277
|
+
| Library | Controllers | Interface | Config |
|
|
1278
|
+
|---|---|---|---|
|
|
1279
|
+
| `XPT2046_Touchscreen` | XPT2046 (common ILI9341 shields) | SPI (shared with display) | `{ library, cs, irq? }` |
|
|
1280
|
+
| `Adafruit_TouchScreen` | Resistive 4-wire | Analog (no SPI) | `{ library, analogPins: { xp, yp, xm, ym, rx } }` |
|
|
1281
|
+
| `Adafruit_STMPE610` | STMPE610 (capacitive) | SPI or I2C | `{ library, cs }` |
|
|
1282
|
+
|
|
1283
|
+
### Config examples
|
|
1284
|
+
|
|
1285
|
+
**XPT2046 (most common with ILI9341 TFT shields):**
|
|
1286
|
+
|
|
1287
|
+
```typescript
|
|
1288
|
+
display: {
|
|
1289
|
+
profile: 'ili9341-spi',
|
|
1290
|
+
cs: 5, dc: 21, rst: 22,
|
|
1291
|
+
touch: {
|
|
1292
|
+
library: 'XPT2046_Touchscreen',
|
|
1293
|
+
cs: 14, // touch CS pin (separate from display CS)
|
|
1294
|
+
irq: 2, // optional
|
|
1295
|
+
calibration: { xMin: 375, xMax: 3950, yMin: 200, yMax: 3750 },
|
|
1296
|
+
minPressure: 10,
|
|
1297
|
+
},
|
|
1298
|
+
}
|
|
1299
|
+
```
|
|
1300
|
+
|
|
1301
|
+
**Adafruit resistive 4-wire:**
|
|
1302
|
+
|
|
1303
|
+
```typescript
|
|
1304
|
+
touch: {
|
|
1305
|
+
library: 'Adafruit_TouchScreen',
|
|
1306
|
+
analogPins: { xp: 'A3', yp: 'A2', xm: 8, ym: 9, rx: 300 },
|
|
1307
|
+
calibration: { xMin: 100, xMax: 900, yMin: 100, yMax: 900 },
|
|
1308
|
+
minPressure: 10,
|
|
1309
|
+
}
|
|
1310
|
+
```
|
|
1311
|
+
|
|
1312
|
+
### Calibration
|
|
1313
|
+
|
|
1314
|
+
Calibration maps the touch controller's raw ADC values to display pixel coordinates. To calibrate your panel:
|
|
1315
|
+
|
|
1316
|
+
1. Add `Serial.printf("raw=(%d,%d,%d)\n", p.x, p.y, p.z)` to the touch poll
|
|
1317
|
+
2. Touch the four corners of the screen and note the raw values
|
|
1318
|
+
3. Set `xMin`/`xMax` from the left/right edges, `yMin`/`yMax` from the top/bottom
|
|
1319
|
+
|
|
1320
|
+
The transpiler handles rotation (axis swap + inversion) automatically based on the `rotation` field in the display profile.
|
|
1321
|
+
|
|
1322
|
+
### Touch events (onClick, onHold, onRelease)
|
|
1323
|
+
|
|
1324
|
+
```typescript
|
|
1325
|
+
// Short tap (finger down + up within 600ms)
|
|
1326
|
+
screen.btn.onClick(() => {
|
|
1327
|
+
console.log("tapped");
|
|
1328
|
+
screen.counter.value = screen.counter.value + 1;
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
// Long press (finger held ≥600ms)
|
|
1332
|
+
screen.btn.onHold(() => {
|
|
1333
|
+
console.log("held");
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
// Finger lift (always fires after click or hold)
|
|
1337
|
+
screen.btn.onRelease(() => {
|
|
1338
|
+
console.log("released");
|
|
1339
|
+
});
|
|
1340
|
+
```
|
|
1341
|
+
|
|
1342
|
+
The touch system implements a state machine:
|
|
1343
|
+
- **50ms debounce** — prevents rapid re-triggering
|
|
1344
|
+
- **Click** — touch down + up within 600ms
|
|
1345
|
+
- **Hold** — touch held ≥600ms (fires once)
|
|
1346
|
+
- **Release** — finger lifts (clears `.value` to 0)
|
|
1347
|
+
- **Visual feedback** — `.value` set to 1 on touch down, 0 on release
|
|
1348
|
+
|
|
1349
|
+
Hit-testing walks nodes topmost-first and skips containers without click handlers.
|
|
1350
|
+
|
|
1351
|
+
### Awaitable tap notifications (`ui.onTap`)
|
|
1352
|
+
|
|
1353
|
+
`ui.onTap()` is an **awaitable** tap signal — use it inside an `async` function
|
|
1354
|
+
to suspend until the next tap. It's the building block for display-sleep /
|
|
1355
|
+
screensaver behavior and custom flow control ("tap to continue"):
|
|
1356
|
+
|
|
1357
|
+
```typescript
|
|
1358
|
+
// Display-sleep: wake on ANY touch, dim again after 10s of inactivity.
|
|
1359
|
+
async function screensaver() {
|
|
1360
|
+
while (true) {
|
|
1361
|
+
backlightOff();
|
|
1362
|
+
await ui.onTap(); // resume on the next tap, anywhere on the screen
|
|
1363
|
+
backlightOn();
|
|
1364
|
+
await delay(10000); // keep the display awake for 10 seconds
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
```
|
|
1368
|
+
|
|
1369
|
+
With no argument it resumes on the next tap **anywhere** — including empty
|
|
1370
|
+
space, which is what makes "wake on any touch" work even when the finger lands
|
|
1371
|
+
on no element. Pass an element to resume only when that element is tapped:
|
|
1372
|
+
|
|
1373
|
+
```typescript
|
|
1374
|
+
// Tap-to-continue wizard: wait for the Start button specifically.
|
|
1375
|
+
async function setupWizard() {
|
|
1376
|
+
await ui.onTap(screen.start);
|
|
1377
|
+
beginSetup();
|
|
1378
|
+
}
|
|
1379
|
+
```
|
|
1380
|
+
|
|
1381
|
+
A tap fires **both** the tapped element's `onClick` handler **and** resumes any
|
|
1382
|
+
`await ui.onTap()` awaiter — they don't compete. `ui.onTap()` is a resume signal,
|
|
1383
|
+
not a value: there is nothing to read from it (it returns `Promise<void>`).
|
|
1384
|
+
|
|
1385
|
+
| Call | Resumes on |
|
|
1386
|
+
|---|---|
|
|
1387
|
+
| `await ui.onTap()` | the next tap anywhere (including empty space) |
|
|
1388
|
+
| `await ui.onTap(screen.elem)` | the next tap on that specific element |
|
|
1389
|
+
|
|
1390
|
+
Under the hood this lowers to a cooperative state-machine state that polls a
|
|
1391
|
+
tap counter incremented by the touch driver each frame — no ISRs, natural
|
|
1392
|
+
debounce from the ~16ms tick, same model as `await delay()`.
|
|
1393
|
+
|
|
1394
|
+
### Custom touch adapters
|
|
1395
|
+
|
|
1396
|
+
For libraries not in the built-in list, write a TypeScript adapter:
|
|
1397
|
+
|
|
1398
|
+
```typescript
|
|
1399
|
+
// my-touch-adapter.ts
|
|
1400
|
+
import { SomeTouchLib } from '../lib/SomeTouchLib/SomeTouchLib';
|
|
1401
|
+
|
|
1402
|
+
const ts = new SomeTouchLib(14, 2);
|
|
1403
|
+
ts.begin();
|
|
1404
|
+
|
|
1405
|
+
export const touch = {
|
|
1406
|
+
isTouched: () => ts.touched(),
|
|
1407
|
+
read: () => {
|
|
1408
|
+
const p = ts.getPoint();
|
|
1409
|
+
return { x: p.x, y: p.y, z: p.z };
|
|
1410
|
+
},
|
|
1411
|
+
};
|
|
1412
|
+
```
|
|
1413
|
+
|
|
1414
|
+
Reference it in config:
|
|
1415
|
+
|
|
1416
|
+
```typescript
|
|
1417
|
+
touch: {
|
|
1418
|
+
adapter: './my-touch-adapter',
|
|
1419
|
+
calibration: { xMin: 100, xMax: 4000, yMin: 100, yMax: 4000 },
|
|
1420
|
+
minPressure: 10,
|
|
1421
|
+
}
|
|
1422
|
+
```
|
|
1423
|
+
|
|
1424
|
+
The adapter only provides raw `{x, y, z}` — the transpiler handles calibration, rotation, and coordinate mapping.
|
|
1425
|
+
|
|
1426
|
+
## GPIO input (buttons without touch)
|
|
1427
|
+
|
|
1428
|
+
For physical buttons on GPIO pins (no touchscreen required):
|
|
1429
|
+
|
|
1430
|
+
```typescript
|
|
1431
|
+
// Watch a pin for falling edges — runs in the frame loop
|
|
1432
|
+
ui.watchPin(4, () => {
|
|
1433
|
+
screen.counter.value = screen.counter.value + 1;
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
// Toggle an element's .value on pin press
|
|
1437
|
+
screen.ledBox.onToggle(5);
|
|
1438
|
+
|
|
1439
|
+
// Cycle through options
|
|
1440
|
+
screen.modeValue.onChange(15, 3); // 3 options: 0→1→2→0
|
|
1441
|
+
```
|
|
1442
|
+
|
|
1443
|
+
Natural debounce from the ~16ms frame rate — no ISR, no `volatile`.
|