@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
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// YogaLayoutEngine — flexbox layout via Facebook's Yoga engine.
|
|
3
|
+
//
|
|
4
|
+
// Implements the LayoutEngine interface using yoga-layout (WASM, ~2MB).
|
|
5
|
+
// Handles display:flex, flex-direction, gap, align-self, padding (shorthand),
|
|
6
|
+
// border, and content-sized leaf nodes. Falls back gracefully when flex
|
|
7
|
+
// properties are absent.
|
|
8
|
+
//
|
|
9
|
+
// The critical contract: arrange() returns Box[] in pre-order DFS order
|
|
10
|
+
// matching the StyledNode tree, so lowerUIToCpp's flatten() can index
|
|
11
|
+
// boxes[] in lockstep with the styled nodes.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
import { isDisplayNone, parseAspectRatio } from "./layout-engine.js";
|
|
14
|
+
import Yoga from "yoga-layout";
|
|
15
|
+
/** Parse the first numeric value from a CSS string ("8px", "8px 16px" → 8). */
|
|
16
|
+
/** Parse a single CSS length to device pixels.
|
|
17
|
+
* rem/em × 16 (root font size), px/bare as-is, decimals supported. */
|
|
18
|
+
function cssLength(val) {
|
|
19
|
+
const v = val.trim();
|
|
20
|
+
const remM = /^(-?[\d.]+)rem$/.exec(v);
|
|
21
|
+
if (remM)
|
|
22
|
+
return Math.round(parseFloat(remM[1]) * 16);
|
|
23
|
+
const emM = /^(-?[\d.]+)em$/.exec(v);
|
|
24
|
+
if (emM)
|
|
25
|
+
return Math.round(parseFloat(emM[1]) * 16);
|
|
26
|
+
const m = /^(-?[\d.]+)(?:px|%)?$/.exec(v);
|
|
27
|
+
return m ? parseFloat(m[1]) : 0;
|
|
28
|
+
}
|
|
29
|
+
function cssNum(val) {
|
|
30
|
+
if (!val)
|
|
31
|
+
return 0;
|
|
32
|
+
return cssLength(val);
|
|
33
|
+
}
|
|
34
|
+
/** Parse horizontal padding from shorthand ("8px 16px" → 16 for left/right). */
|
|
35
|
+
function cssPadH(val) {
|
|
36
|
+
if (!val)
|
|
37
|
+
return 0;
|
|
38
|
+
const parts = val.trim().split(/\s+/);
|
|
39
|
+
if (parts.length <= 1)
|
|
40
|
+
return cssLength(parts[0] ?? "0");
|
|
41
|
+
return cssLength(parts[1] ?? parts[0]);
|
|
42
|
+
}
|
|
43
|
+
/** Parse vertical padding from shorthand ("8px 16px" → 8 for top/bottom). */
|
|
44
|
+
function cssPadV(val) {
|
|
45
|
+
if (!val)
|
|
46
|
+
return 0;
|
|
47
|
+
const parts = val.trim().split(/\s+/);
|
|
48
|
+
return cssLength(parts[0] ?? "0");
|
|
49
|
+
}
|
|
50
|
+
/** Parse border width from "2px solid #808080" → 2. */
|
|
51
|
+
function cssBorderWidth(val) {
|
|
52
|
+
if (!val)
|
|
53
|
+
return 0;
|
|
54
|
+
// border shorthand: "<width> <style> <color>" — width is the first token.
|
|
55
|
+
const first = val.trim().split(/\s+/)[0];
|
|
56
|
+
return cssLength(first);
|
|
57
|
+
}
|
|
58
|
+
export class YogaLayoutEngine {
|
|
59
|
+
id = "flex";
|
|
60
|
+
arrange(root, viewport, measureFn) {
|
|
61
|
+
const metaArray = [];
|
|
62
|
+
// Build the Yoga tree from the StyledNode tree.
|
|
63
|
+
const yogaRoot = this.buildTree(root, metaArray, measureFn);
|
|
64
|
+
yogaRoot.setWidth(viewport.w);
|
|
65
|
+
yogaRoot.setHeight(viewport.h);
|
|
66
|
+
yogaRoot.calculateLayout(viewport.w, viewport.h, Yoga.DIRECTION_LTR);
|
|
67
|
+
// Extract boxes in pre-order DFS matching the StyledNode tree.
|
|
68
|
+
const boxes = [];
|
|
69
|
+
let extractIdx = 0;
|
|
70
|
+
this.extractBoxes(yogaRoot, boxes, () => metaArray[extractIdx++]);
|
|
71
|
+
return boxes;
|
|
72
|
+
}
|
|
73
|
+
/** Recursively build a Yoga node tree from a StyledNode tree. */
|
|
74
|
+
buildTree(node, metaArray, measureFn, myIndex = metaArray.length) {
|
|
75
|
+
const yn = Yoga.Node.create();
|
|
76
|
+
const idx = metaArray.length;
|
|
77
|
+
metaArray.push({ style: node.style });
|
|
78
|
+
const s = node.style;
|
|
79
|
+
if (isDisplayNone(node)) {
|
|
80
|
+
yn.setDisplay?.(Yoga.DISPLAY_NONE);
|
|
81
|
+
}
|
|
82
|
+
// Flex container
|
|
83
|
+
// flex-direction: row | row-reverse | column | column-reverse.
|
|
84
|
+
// Each value maps to a distinct Yoga enum (reverse was previously
|
|
85
|
+
// dropped — any non-"row" value collapsed to column).
|
|
86
|
+
const fd = s.display === "flex" ? s.flexDirection : "column";
|
|
87
|
+
if (fd === "row")
|
|
88
|
+
yn.setFlexDirection(Yoga.FLEX_DIRECTION_ROW);
|
|
89
|
+
else if (fd === "row-reverse")
|
|
90
|
+
yn.setFlexDirection(Yoga.FLEX_DIRECTION_ROW_REVERSE);
|
|
91
|
+
else if (fd === "column-reverse")
|
|
92
|
+
yn.setFlexDirection(Yoga.FLEX_DIRECTION_COLUMN_REVERSE);
|
|
93
|
+
else
|
|
94
|
+
yn.setFlexDirection(Yoga.FLEX_DIRECTION_COLUMN);
|
|
95
|
+
// Padding (all edges from shorthand, using the vertical value for uniformity)
|
|
96
|
+
const padV = cssPadV(s.padding);
|
|
97
|
+
const padH = cssPadH(s.padding);
|
|
98
|
+
if (padV || padH) {
|
|
99
|
+
yn.setPadding(Yoga.EDGE_TOP, padV);
|
|
100
|
+
yn.setPadding(Yoga.EDGE_BOTTOM, padV);
|
|
101
|
+
yn.setPadding(Yoga.EDGE_LEFT, padH);
|
|
102
|
+
yn.setPadding(Yoga.EDGE_RIGHT, padH);
|
|
103
|
+
}
|
|
104
|
+
// Margin: per-side overrides win over the `margin` shorthand when set.
|
|
105
|
+
const marginV = cssPadV(s.margin);
|
|
106
|
+
const marginH = cssPadH(s.margin);
|
|
107
|
+
const mTop = s.marginTop !== undefined ? cssLength(s.marginTop) : marginV;
|
|
108
|
+
const mBottom = s.marginBottom !== undefined ? cssLength(s.marginBottom) : marginV;
|
|
109
|
+
const mLeft = s.marginLeft !== undefined ? cssLength(s.marginLeft) : marginH;
|
|
110
|
+
const mRight = s.marginRight !== undefined ? cssLength(s.marginRight) : marginH;
|
|
111
|
+
if (mTop || mBottom || mLeft || mRight) {
|
|
112
|
+
yn.setMargin(Yoga.EDGE_TOP, mTop);
|
|
113
|
+
yn.setMargin(Yoga.EDGE_BOTTOM, mBottom);
|
|
114
|
+
yn.setMargin(Yoga.EDGE_LEFT, mLeft);
|
|
115
|
+
yn.setMargin(Yoga.EDGE_RIGHT, mRight);
|
|
116
|
+
}
|
|
117
|
+
// Gap: row-gap / column-gap are applied per-axis. Uniform `gap`
|
|
118
|
+
// (both equal) uses GUTTER_ALL for efficiency; mismatched values
|
|
119
|
+
// set GUTTER_ROW and GUTTER_COLUMN separately.
|
|
120
|
+
const rowGap = cssNum(s.rowGap);
|
|
121
|
+
const colGap = cssNum(s.columnGap);
|
|
122
|
+
if (rowGap && colGap && rowGap === colGap) {
|
|
123
|
+
yn.setGap(Yoga.GUTTER_ALL, rowGap);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
if (rowGap)
|
|
127
|
+
yn.setGap(Yoga.GUTTER_ROW, rowGap);
|
|
128
|
+
if (colGap)
|
|
129
|
+
yn.setGap(Yoga.GUTTER_COLUMN, colGap);
|
|
130
|
+
}
|
|
131
|
+
// Border
|
|
132
|
+
const borderW = cssBorderWidth(s.border);
|
|
133
|
+
if (borderW) {
|
|
134
|
+
yn.setBorder(Yoga.EDGE_ALL, borderW);
|
|
135
|
+
}
|
|
136
|
+
// Align-self
|
|
137
|
+
if (s.alignSelf === "flex-start")
|
|
138
|
+
yn.setAlignSelf(Yoga.ALIGN_FLEX_START);
|
|
139
|
+
else if (s.alignSelf === "center")
|
|
140
|
+
yn.setAlignSelf(Yoga.ALIGN_CENTER);
|
|
141
|
+
else if (s.alignSelf === "stretch")
|
|
142
|
+
yn.setAlignSelf(Yoga.ALIGN_STRETCH);
|
|
143
|
+
else if (s.alignSelf === "flex-end")
|
|
144
|
+
yn.setAlignSelf(Yoga.ALIGN_FLEX_END);
|
|
145
|
+
else if (s.alignSelf === "baseline")
|
|
146
|
+
yn.setAlignSelf(Yoga.ALIGN_BASELINE);
|
|
147
|
+
// Align-items / justify-content (container properties)
|
|
148
|
+
if (s.alignItems) {
|
|
149
|
+
if (s.alignItems === "flex-start")
|
|
150
|
+
yn.setAlignItems(Yoga.ALIGN_FLEX_START);
|
|
151
|
+
else if (s.alignItems === "center")
|
|
152
|
+
yn.setAlignItems(Yoga.ALIGN_CENTER);
|
|
153
|
+
else if (s.alignItems === "stretch")
|
|
154
|
+
yn.setAlignItems(Yoga.ALIGN_STRETCH);
|
|
155
|
+
else if (s.alignItems === "flex-end")
|
|
156
|
+
yn.setAlignItems(Yoga.ALIGN_FLEX_END);
|
|
157
|
+
}
|
|
158
|
+
// Align-content (multi-line flex-wrap cross-axis alignment)
|
|
159
|
+
if (s.alignContent) {
|
|
160
|
+
if (s.alignContent === "flex-start")
|
|
161
|
+
yn.setAlignContent(Yoga.ALIGN_FLEX_START);
|
|
162
|
+
else if (s.alignContent === "center")
|
|
163
|
+
yn.setAlignContent(Yoga.ALIGN_CENTER);
|
|
164
|
+
else if (s.alignContent === "flex-end")
|
|
165
|
+
yn.setAlignContent(Yoga.ALIGN_FLEX_END);
|
|
166
|
+
else if (s.alignContent === "stretch")
|
|
167
|
+
yn.setAlignContent(Yoga.ALIGN_STRETCH);
|
|
168
|
+
else if (s.alignContent === "space-between")
|
|
169
|
+
yn.setAlignContent(Yoga.ALIGN_SPACE_BETWEEN);
|
|
170
|
+
else if (s.alignContent === "space-around")
|
|
171
|
+
yn.setAlignContent(Yoga.ALIGN_SPACE_AROUND);
|
|
172
|
+
else if (s.alignContent === "space-evenly")
|
|
173
|
+
yn.setAlignContent(Yoga.ALIGN_SPACE_EVENLY);
|
|
174
|
+
}
|
|
175
|
+
if (s.justifyContent) {
|
|
176
|
+
if (s.justifyContent === "flex-start")
|
|
177
|
+
yn.setJustifyContent(Yoga.JUSTIFY_FLEX_START);
|
|
178
|
+
else if (s.justifyContent === "center")
|
|
179
|
+
yn.setJustifyContent(Yoga.JUSTIFY_CENTER);
|
|
180
|
+
else if (s.justifyContent === "flex-end")
|
|
181
|
+
yn.setJustifyContent(Yoga.JUSTIFY_FLEX_END);
|
|
182
|
+
else if (s.justifyContent === "space-between")
|
|
183
|
+
yn.setJustifyContent(Yoga.JUSTIFY_SPACE_BETWEEN);
|
|
184
|
+
else if (s.justifyContent === "space-around")
|
|
185
|
+
yn.setJustifyContent(Yoga.JUSTIFY_SPACE_AROUND);
|
|
186
|
+
else if (s.justifyContent === "space-evenly")
|
|
187
|
+
yn.setJustifyContent(Yoga.JUSTIFY_SPACE_EVENLY);
|
|
188
|
+
}
|
|
189
|
+
// Flex wrap
|
|
190
|
+
if (s.flexWrap === "wrap")
|
|
191
|
+
yn.setFlexWrap(Yoga.WRAP_WRAP);
|
|
192
|
+
else if (s.flexWrap === "wrap-reverse")
|
|
193
|
+
yn.setFlexWrap(Yoga.WRAP_WRAP_REVERSE);
|
|
194
|
+
else if (s.flexWrap === "nowrap")
|
|
195
|
+
yn.setFlexWrap(Yoga.WRAP_NO_WRAP);
|
|
196
|
+
// Flex grow/shrink/basis
|
|
197
|
+
if (s.flexGrow)
|
|
198
|
+
yn.setFlexGrow(cssNum(s.flexGrow));
|
|
199
|
+
if (s.flexShrink)
|
|
200
|
+
yn.setFlexShrink(cssNum(s.flexShrink));
|
|
201
|
+
if (s.flexBasis) {
|
|
202
|
+
const basis = cssNum(s.flexBasis);
|
|
203
|
+
if (s.flexBasis === "auto")
|
|
204
|
+
yn.setFlexBasisAuto();
|
|
205
|
+
else
|
|
206
|
+
yn.setFlexBasis(basis);
|
|
207
|
+
}
|
|
208
|
+
// Order (Yoga may not expose setOrder in its types, but the runtime has it)
|
|
209
|
+
if (s.order)
|
|
210
|
+
yn.setOrder?.(cssNum(s.order));
|
|
211
|
+
// Min/max dimensions
|
|
212
|
+
if (s.minWidth)
|
|
213
|
+
yn.setMinWidth(cssNum(s.minWidth));
|
|
214
|
+
if (s.maxWidth)
|
|
215
|
+
yn.setMaxWidth(cssNum(s.maxWidth));
|
|
216
|
+
if (s.minHeight)
|
|
217
|
+
yn.setMinHeight(cssNum(s.minHeight));
|
|
218
|
+
if (s.maxHeight)
|
|
219
|
+
yn.setMaxHeight(cssNum(s.maxHeight));
|
|
220
|
+
// Box sizing
|
|
221
|
+
if (s.boxSizing === "border-box")
|
|
222
|
+
yn.setBoxSizing?.(Yoga.BOX_SIZING_BORDER_BOX);
|
|
223
|
+
// Overflow
|
|
224
|
+
if (s.overflow === "hidden")
|
|
225
|
+
yn.setOverflow?.(Yoga.OVERFLOW_HIDDEN);
|
|
226
|
+
else if (s.overflow === "scroll")
|
|
227
|
+
yn.setOverflow?.(Yoga.OVERFLOW_SCROLL);
|
|
228
|
+
// Position
|
|
229
|
+
if (s.position === "relative")
|
|
230
|
+
yn.setPositionType(Yoga.POSITION_TYPE_RELATIVE);
|
|
231
|
+
else if (s.position === "absolute")
|
|
232
|
+
yn.setPositionType(Yoga.POSITION_TYPE_ABSOLUTE);
|
|
233
|
+
else
|
|
234
|
+
yn.setPositionType(Yoga.POSITION_TYPE_STATIC);
|
|
235
|
+
if (s.top !== undefined)
|
|
236
|
+
yn.setPosition(Yoga.EDGE_TOP, cssNum(s.top));
|
|
237
|
+
if (s.right !== undefined)
|
|
238
|
+
yn.setPosition(Yoga.EDGE_RIGHT, cssNum(s.right));
|
|
239
|
+
if (s.bottom !== undefined)
|
|
240
|
+
yn.setPosition(Yoga.EDGE_BOTTOM, cssNum(s.bottom));
|
|
241
|
+
if (s.left !== undefined)
|
|
242
|
+
yn.setPosition(Yoga.EDGE_LEFT, cssNum(s.left));
|
|
243
|
+
// Width/height (explicit)
|
|
244
|
+
if (s.width)
|
|
245
|
+
yn.setWidth(cssNum(s.width));
|
|
246
|
+
if (s.height)
|
|
247
|
+
yn.setHeight(cssNum(s.height));
|
|
248
|
+
const aspectRatio = parseAspectRatio(s.aspectRatio);
|
|
249
|
+
if (aspectRatio !== undefined)
|
|
250
|
+
yn.setAspectRatio(aspectRatio);
|
|
251
|
+
// Children. The yoga binding in use has no setOrder API, so CSS `order`
|
|
252
|
+
// can't be applied via yoga — sort children by their order value first
|
|
253
|
+
// (stable sort: source order preserved for equal order) so the flex engine
|
|
254
|
+
// lays items out in their order sequence.
|
|
255
|
+
const orderedChildren = [...node.children].sort((a, b) => {
|
|
256
|
+
const ao = cssNum(a.style.order);
|
|
257
|
+
const bo = cssNum(b.style.order);
|
|
258
|
+
return ao - bo;
|
|
259
|
+
});
|
|
260
|
+
// Build yoga nodes in the sorted (visual) order so yoga positions them by
|
|
261
|
+
// order, but record the SOURCE index of each so extractBoxes can emit boxes
|
|
262
|
+
// in source-tree order (the contract lowerUIToModel relies on).
|
|
263
|
+
for (let si = 0; si < orderedChildren.length; si++) {
|
|
264
|
+
const child = orderedChildren[si];
|
|
265
|
+
const sourceIndex = node.children.indexOf(child);
|
|
266
|
+
const childNode = this.buildTree(child, metaArray, measureFn);
|
|
267
|
+
// Tag the yoga node with its source index so extraction can reorder.
|
|
268
|
+
childNode.__sourceIndex = sourceIndex;
|
|
269
|
+
yn.insertChild(childNode, yn.getChildCount());
|
|
270
|
+
}
|
|
271
|
+
// Leaf nodes with text: let Yoga pass available width into measurement so
|
|
272
|
+
// wrapped text can expand height under constraints.
|
|
273
|
+
if (node.children.length === 0) {
|
|
274
|
+
const textLike = node.tag === "text" || node.tag === "button" || node.tag === "check" || node.tag === "radio";
|
|
275
|
+
if (textLike) {
|
|
276
|
+
yn.setMeasureFunc((width, widthMode) => {
|
|
277
|
+
const hasWidth = widthMode !== Yoga.MEASURE_MODE_UNDEFINED && Number.isFinite(width) && width > 0;
|
|
278
|
+
const intrinsic = measureFn(node, hasWidth ? width : undefined);
|
|
279
|
+
return {
|
|
280
|
+
width: Math.ceil(intrinsic.w),
|
|
281
|
+
height: Math.ceil(intrinsic.h),
|
|
282
|
+
};
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
const intrinsic = measureFn(node);
|
|
287
|
+
const childPadV = cssPadV(s.padding);
|
|
288
|
+
const childPadH = cssPadH(s.padding);
|
|
289
|
+
const childBorder = cssBorderWidth(s.border);
|
|
290
|
+
if (!s.width && intrinsic.w > 0) {
|
|
291
|
+
yn.setWidth(Math.ceil(intrinsic.w + childPadH * 2 + childBorder * 2));
|
|
292
|
+
}
|
|
293
|
+
if (!s.height && intrinsic.h > 0) {
|
|
294
|
+
yn.setHeight(intrinsic.h + childPadV * 2 + childBorder * 2);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return yn;
|
|
299
|
+
}
|
|
300
|
+
/** Walk the Yoga tree in pre-order DFS, extracting absolute {x,y,w,h} per node.
|
|
301
|
+
* Yoga returns positions relative to the parent — we accumulate parentX/Y
|
|
302
|
+
* to produce absolute coordinates. Must match the StyledNode tree's DFS. */
|
|
303
|
+
extractBoxes(node, out, nextMeta, parentX = 0, parentY = 0) {
|
|
304
|
+
const x = parentX + Math.round(node.getComputedLeft());
|
|
305
|
+
const y = parentY + Math.round(node.getComputedTop());
|
|
306
|
+
const w = Math.round(node.getComputedWidth());
|
|
307
|
+
const h = Math.round(node.getComputedHeight());
|
|
308
|
+
out.push({ x, y, w, h });
|
|
309
|
+
nextMeta();
|
|
310
|
+
for (let i = 0; i < node.getChildCount(); i++) {
|
|
311
|
+
this.extractBoxes(node.getChild(i), out, nextMeta, x, y);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@typecad/ui",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "TypeCAD UI authoring library — HTML/CSS-driven graphics for microcontrollers",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./engine": {
|
|
14
|
+
"types": "./dist/engine-index.d.ts",
|
|
15
|
+
"default": "./dist/engine-index.js"
|
|
16
|
+
},
|
|
17
|
+
"./runtime-header": {
|
|
18
|
+
"types": "./dist/ui-engine/runtime-header.d.ts",
|
|
19
|
+
"default": "./dist/ui-engine/runtime-header.js"
|
|
20
|
+
},
|
|
21
|
+
"./ui-engine/*": {
|
|
22
|
+
"types": "./dist/ui-engine/*.d.ts",
|
|
23
|
+
"default": "./dist/ui-engine/*.js"
|
|
24
|
+
},
|
|
25
|
+
"./preview/*": {
|
|
26
|
+
"types": "./dist/preview/*.d.ts",
|
|
27
|
+
"default": "./dist/preview/*.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"files": [
|
|
32
|
+
"dist",
|
|
33
|
+
"src"
|
|
34
|
+
],
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@typecad/cuttlefish": "0.1.0-alpha.1"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"css-tree": "^3.2.1",
|
|
40
|
+
"linkedom": "^0.18.12",
|
|
41
|
+
"opentype.js": "^2.0.0",
|
|
42
|
+
"yoga-layout": "^3.2.1"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsc",
|
|
46
|
+
"prepublishOnly": "npm run build"
|
|
47
|
+
},
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"repository": {
|
|
50
|
+
"type": "git",
|
|
51
|
+
"url": "git+https://github.com/justind000/typecode.git",
|
|
52
|
+
"directory": "packages/ui"
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://github.com/justind000/typecode/tree/main/packages/ui",
|
|
55
|
+
"bugs": {
|
|
56
|
+
"url": "https://github.com/justind000/typecode/issues"
|
|
57
|
+
},
|
|
58
|
+
"keywords": [
|
|
59
|
+
"arduino",
|
|
60
|
+
"embedded",
|
|
61
|
+
"microcontroller",
|
|
62
|
+
"esp32",
|
|
63
|
+
"tft",
|
|
64
|
+
"ili9341",
|
|
65
|
+
"ui",
|
|
66
|
+
"html",
|
|
67
|
+
"css",
|
|
68
|
+
"gui",
|
|
69
|
+
"typecad",
|
|
70
|
+
"cuttlefish",
|
|
71
|
+
"typescript",
|
|
72
|
+
"cpp",
|
|
73
|
+
"firmware"
|
|
74
|
+
],
|
|
75
|
+
"engines": {
|
|
76
|
+
"node": ">=18"
|
|
77
|
+
},
|
|
78
|
+
"devDependencies": {
|
|
79
|
+
"@types/node": "^22.10.7",
|
|
80
|
+
"@types/opentype.js": "^1.3.10"
|
|
81
|
+
},
|
|
82
|
+
"publishConfig": {
|
|
83
|
+
"access": "public"
|
|
84
|
+
},
|
|
85
|
+
"author": "typecad0"
|
|
86
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// Engine entry point for @typecad/ui.
|
|
3
|
+
//
|
|
4
|
+
// Cuttlefish dynamically imports this module when UI usage is detected:
|
|
5
|
+
// const engine = await import("@typecad/ui/engine");
|
|
6
|
+
// const hook = await engine.registerTranspilerUI();
|
|
7
|
+
//
|
|
8
|
+
// registerTranspilerUI() returns an object implementing the TranspilerUIHook
|
|
9
|
+
// interface defined in @typecad/cuttlefish. This is the single runtime
|
|
10
|
+
// contract between the two packages.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
import type { TranspilerUIHook } from "@typecad/cuttlefish/ui-hook";
|
|
14
|
+
import {
|
|
15
|
+
resetUIRegistry, loadUIModule, loadUIModuleFromText, getUIModule,
|
|
16
|
+
hasUIModule, allUIModules, allLoweredUIModules,
|
|
17
|
+
markEntryHasUI, entryHasUI, clearEntryHasUI, lowerOnMount,
|
|
18
|
+
generateProjectUITypeDeclarations,
|
|
19
|
+
} from "./ui-engine/ui-registry.js";
|
|
20
|
+
import { resolveColor, resolveColorInternal } from "./ui-engine/color.js";
|
|
21
|
+
import { emitRuntimeHeader } from "./ui-engine/runtime-header.js";
|
|
22
|
+
import { splitUiFile } from "./ui-engine/ui-file-splitter.js";
|
|
23
|
+
import { analyzeScrollMemory } from "./ui-engine/scroll-memory-diagnostics.js";
|
|
24
|
+
|
|
25
|
+
export type { TranspilerUIHook };
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Build and return the TranspilerUIHook — the object cuttlefish core uses
|
|
29
|
+
* to access all UI capabilities (parsing, layout, lowering, emission).
|
|
30
|
+
*/
|
|
31
|
+
export function registerTranspilerUI(): TranspilerUIHook {
|
|
32
|
+
return {
|
|
33
|
+
resetUIRegistry,
|
|
34
|
+
loadUIModule,
|
|
35
|
+
loadUIModuleFromText,
|
|
36
|
+
getUIModule,
|
|
37
|
+
hasUIModule,
|
|
38
|
+
allUIModules,
|
|
39
|
+
allLoweredUIModules,
|
|
40
|
+
markEntryHasUI,
|
|
41
|
+
entryHasUI,
|
|
42
|
+
clearEntryHasUI,
|
|
43
|
+
lowerOnMount,
|
|
44
|
+
resolveColor,
|
|
45
|
+
resolveColorInternal,
|
|
46
|
+
emitRuntimeHeader,
|
|
47
|
+
splitUiFile,
|
|
48
|
+
generateProjectUITypeDeclarations,
|
|
49
|
+
analyzeScrollMemory,
|
|
50
|
+
};
|
|
51
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// @typecad/ui — public authoring API.
|
|
3
|
+
//
|
|
4
|
+
// These functions are compile-time constructs: the transpiler intercepts
|
|
5
|
+
// ui.mount / ui.signal / ui.bind and lowers them to device variables and
|
|
6
|
+
// binding-table entries. They have no runtime implementation in the emitted
|
|
7
|
+
// C++; their bodies exist only so TypeScript authoring type-checks.
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export type { ScreenTree, TextElement, ButtonElement, ViewElement, PressBinding, CheckElement, SelectElement, RadioElement, ProgressElement, RangeElement, InputElement, CanvasElement, CanvasCtx } from "./types.js";
|
|
11
|
+
import type { ScreenTree, CanvasCtx } from "./types.js";
|
|
12
|
+
|
|
13
|
+
/** Allowed signal value types. The transpiler lowers each of these to a C++
|
|
14
|
+
* scalar: `number` → int/double, `string` → const char*, `boolean` → bool.
|
|
15
|
+
* Other types (objects, arrays, null) are rejected at type-check time by
|
|
16
|
+
* Signal<T extends SignalValue> and at build time by the `ui-signal-initializer`
|
|
17
|
+
* diagnostic. */
|
|
18
|
+
export type SignalValue = number | string | boolean;
|
|
19
|
+
|
|
20
|
+
/** A reactive signal whose value lives on the device. */
|
|
21
|
+
export interface Signal<T extends SignalValue> {
|
|
22
|
+
(): T;
|
|
23
|
+
set(value: T): void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface MountOptions {
|
|
27
|
+
/** Display driver id (must be in the framework's supportedDisplayDrivers()). */
|
|
28
|
+
display?: string;
|
|
29
|
+
/** Bus identifier, e.g. "SPI" or "Wire". */
|
|
30
|
+
bus?: string;
|
|
31
|
+
/** Chip-select pin. */
|
|
32
|
+
cs?: number;
|
|
33
|
+
/** Data/command pin. */
|
|
34
|
+
dc?: number;
|
|
35
|
+
/** Reset pin. */
|
|
36
|
+
rst?: number;
|
|
37
|
+
/** Rotation override. Defaults to the project display profile. */
|
|
38
|
+
rotation?: number;
|
|
39
|
+
/** Backlight pin override. Defaults to the project display profile. */
|
|
40
|
+
backlight?: number;
|
|
41
|
+
/** SPI frequency override. Defaults to the project display profile. */
|
|
42
|
+
spiFrequency?: number;
|
|
43
|
+
/** I2C address override for I2C displays. Defaults to the project display profile. */
|
|
44
|
+
address?: number;
|
|
45
|
+
/** Reset pin override for I2C displays. Defaults to the project display profile. */
|
|
46
|
+
reset?: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Mount a baked UI tree to the display configured in `cuttlefish.config.ts`.
|
|
51
|
+
* Optional overrides are available for advanced cases, but ordinary apps should
|
|
52
|
+
* keep hardware setup in the project config and call `ui.mount(screen)`.
|
|
53
|
+
*
|
|
54
|
+
* Validates the driver against the active framework at transpile time
|
|
55
|
+
* (fail-fast). The tree is lowered to a static C++ node table; this call lowers
|
|
56
|
+
* to display.init + the first-frame draw.
|
|
57
|
+
*
|
|
58
|
+
* `tree` is typed loosely (a record of element handles) so the concrete
|
|
59
|
+
* ScreenTree interface generated per .ui.html file — which has concrete named
|
|
60
|
+
* fields rather than an index signature — is assignable. The transpiler
|
|
61
|
+
* intercepts the call structurally; the type only needs to permit it.
|
|
62
|
+
*/
|
|
63
|
+
export declare function mount(tree: unknown, opts?: MountOptions): void;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Declare a reactive signal. Lowers to a plain device variable + dirty flag.
|
|
67
|
+
*
|
|
68
|
+
* Accepts `number`, `string`, or `boolean` literals. Literal values are widened
|
|
69
|
+
* to their primitive type so `ui.signal(0)` returns `Signal<number>` (and
|
|
70
|
+
* `.set(1)` works), not `Signal<0>`. Other types (objects, arrays, null) are
|
|
71
|
+
* rejected at type-check time and at build time with a `ui-signal-initializer`
|
|
72
|
+
* diagnostic.
|
|
73
|
+
*/
|
|
74
|
+
export declare function signal(initial: number): Signal<number>;
|
|
75
|
+
export declare function signal(initial: string): Signal<string>;
|
|
76
|
+
export declare function signal(initial: boolean): Signal<boolean>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Bind a node property to a computed value, re-evaluated each tick. When the
|
|
80
|
+
* value changes, the node is marked dirty and (for transition-able properties)
|
|
81
|
+
* the transition is armed.
|
|
82
|
+
*/
|
|
83
|
+
export declare function bind<K extends string>(
|
|
84
|
+
node: unknown,
|
|
85
|
+
property: K,
|
|
86
|
+
compute: () => unknown,
|
|
87
|
+
): void;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Subscribe to changes on an `<input>` element's text. The callback fires after
|
|
91
|
+
* the on-screen keyboard commits, with the new text. Use it to push keyboard
|
|
92
|
+
* input back into app state.
|
|
93
|
+
*
|
|
94
|
+
* let ssid = '';
|
|
95
|
+
* ui.bindInput(screen.ssid, (text) => { ssid = text; });
|
|
96
|
+
*/
|
|
97
|
+
export declare function bindInput(node: unknown, onText: (text: string) => void): void;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Watch a GPIO pin for falling edges (button press). The callback runs as an
|
|
101
|
+
* async task in the existing microtask pump — no ISRs, natural debounce from
|
|
102
|
+
* the ~20ms poll interval. Safe to write to signals inside the callback.
|
|
103
|
+
*
|
|
104
|
+
* ui.watchPin(4, () => { count.set(count() + 1); });
|
|
105
|
+
*/
|
|
106
|
+
export declare function watchPin(pin: number, onFalling: () => void): void;
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Bind a `<list>` element to dynamic data via two callbacks.
|
|
110
|
+
*
|
|
111
|
+
* ui.bindList(screen.myList,
|
|
112
|
+
* () => itemCount, // total number of items
|
|
113
|
+
* (i) => `Item ${i}` // text for item at index i
|
|
114
|
+
* );
|
|
115
|
+
*
|
|
116
|
+
* The list virtualizes: only visible items are rendered. Scroll by dragging.
|
|
117
|
+
* The count function is called each frame; if the count changes, the list
|
|
118
|
+
* refreshes automatically.
|
|
119
|
+
*/
|
|
120
|
+
export declare function bindList(
|
|
121
|
+
node: unknown,
|
|
122
|
+
countFn: () => number,
|
|
123
|
+
itemFn: (index: number) => string,
|
|
124
|
+
onTap?: (index: number) => void,
|
|
125
|
+
): void;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Await the next tap. Must be used inside an `async` function.
|
|
129
|
+
*
|
|
130
|
+
* async function screensaver() {
|
|
131
|
+
* while (true) {
|
|
132
|
+
* await ui.onTap(); // resume on the next tap, anywhere
|
|
133
|
+
* backlightOn();
|
|
134
|
+
* }
|
|
135
|
+
* }
|
|
136
|
+
*
|
|
137
|
+
* With no argument it resumes on the next tap on the screen (including empty
|
|
138
|
+
* space — useful for "wake on any touch"). Pass an element to resume only when
|
|
139
|
+
* that element is tapped:
|
|
140
|
+
*
|
|
141
|
+
* await ui.onTap(screen.btn);
|
|
142
|
+
*
|
|
143
|
+
* A tap fires BOTH the tapped element's onClick handler AND resumes any
|
|
144
|
+
* `await ui.onTap()` awaiter. Returns a Promise<void>; it is a resume signal,
|
|
145
|
+
* not a value — there is nothing to read from it.
|
|
146
|
+
*/
|
|
147
|
+
export declare function onTap(node?: unknown): Promise<void>;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Register a draw callback for a `<canvas>` element. The callback runs every
|
|
151
|
+
* frame and receives a `ctx` whose methods map to the display graphics
|
|
152
|
+
* primitives. Coordinates are canvas-relative ((0,0) = top-left of the element);
|
|
153
|
+
* drawing is clipped to the canvas buffer.
|
|
154
|
+
*
|
|
155
|
+
* ui.drawCanvas(screen.spark, (ctx) => {
|
|
156
|
+
* ctx.fillScreen('black');
|
|
157
|
+
* ctx.line(0, ctx.height / 2, ctx.width, ctx.height / 2, 'limegreen');
|
|
158
|
+
* ctx.fillCircle(needleX, 20, 3, 'red');
|
|
159
|
+
* ctx.text(4, 12, `${temp}°`, 'white');
|
|
160
|
+
* });
|
|
161
|
+
*
|
|
162
|
+
* Color arguments are CSS color strings resolved to RGB565 at transpile time.
|
|
163
|
+
* `ctx.width` / `ctx.height` are the canvas buffer dimensions.
|
|
164
|
+
*/
|
|
165
|
+
export declare function drawCanvas(node: unknown, callback: (ctx: CanvasCtx) => void): void;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Native desktop (SDL) window controls. These are no-ops on hardware targets
|
|
169
|
+
* (Arduino/AVR/ESP32 have no window); on the SDL target they map to the
|
|
170
|
+
* underlying SDL window calls. The initial title/icon come from
|
|
171
|
+
* `cuttlefish.config.ts` (`display.title` / `display.icon`); these methods
|
|
172
|
+
* change them at runtime.
|
|
173
|
+
*/
|
|
174
|
+
export interface WindowApi {
|
|
175
|
+
/** Set the OS window title. Native SDL only; no-op on hardware. */
|
|
176
|
+
setTitle(title: string): void;
|
|
177
|
+
/**
|
|
178
|
+
* Set the window/taskbar icon from an image file path. Native SDL only.
|
|
179
|
+
* Note: runtime icon changes require SDL_image; prefer the `display.icon`
|
|
180
|
+
* config option for the common case (loaded once at launch).
|
|
181
|
+
*/
|
|
182
|
+
setIcon(path: string): void;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Runtime fallback.
|
|
187
|
+
//
|
|
188
|
+
// Every function above is a compile-time construct: the @typecad/cuttlefish
|
|
189
|
+
// transpiler intercepts `ui.mount` / `ui.signal` / `ui.bind` / ... calls and
|
|
190
|
+
// lowers them to device variables and binding-table entries, so these bodies
|
|
191
|
+
// never run on the device. But the package still has to be importable from
|
|
192
|
+
// plain Node (editor language servers, the test runner, tooling) without
|
|
193
|
+
// crashing, and a forgotten build step should fail loudly rather than silently
|
|
194
|
+
// no-op. Each fallback throws an explicit "compile-time construct" error.
|
|
195
|
+
//
|
|
196
|
+
// The signatures intentionally match the `export declare function` contracts
|
|
197
|
+
// above so the public type surface is unchanged.
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
const COMPILE_TIME_ERROR = () => {
|
|
201
|
+
throw new Error(
|
|
202
|
+
"@typecad/ui: this function is a compile-time construct. It must be " +
|
|
203
|
+
"lowered by the @typecad/cuttlefish transpiler at build time, not " +
|
|
204
|
+
"called at runtime.",
|
|
205
|
+
);
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export const ui: {
|
|
209
|
+
mount: typeof mount;
|
|
210
|
+
signal: typeof signal;
|
|
211
|
+
bind: typeof bind;
|
|
212
|
+
bindInput: typeof bindInput;
|
|
213
|
+
bindList: typeof bindList;
|
|
214
|
+
watchPin: typeof watchPin;
|
|
215
|
+
onTap: typeof onTap;
|
|
216
|
+
drawCanvas: typeof drawCanvas;
|
|
217
|
+
window: WindowApi;
|
|
218
|
+
} = {
|
|
219
|
+
mount: COMPILE_TIME_ERROR,
|
|
220
|
+
signal: COMPILE_TIME_ERROR,
|
|
221
|
+
bind: COMPILE_TIME_ERROR,
|
|
222
|
+
bindInput: COMPILE_TIME_ERROR,
|
|
223
|
+
bindList: COMPILE_TIME_ERROR,
|
|
224
|
+
watchPin: COMPILE_TIME_ERROR,
|
|
225
|
+
onTap: COMPILE_TIME_ERROR,
|
|
226
|
+
drawCanvas: COMPILE_TIME_ERROR,
|
|
227
|
+
window: {
|
|
228
|
+
setTitle: COMPILE_TIME_ERROR,
|
|
229
|
+
setIcon: COMPILE_TIME_ERROR,
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
export default ui;
|