@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.
Files changed (183) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1443 -0
  3. package/dist/engine-index.d.ts +7 -0
  4. package/dist/engine-index.js +41 -0
  5. package/dist/index.d.ts +165 -0
  6. package/dist/index.js +42 -0
  7. package/dist/preview/build-program.d.ts +7 -0
  8. package/dist/preview/build-program.js +665 -0
  9. package/dist/preview/host-gfx.d.ts +90 -0
  10. package/dist/preview/host-gfx.js +552 -0
  11. package/dist/preview/host-ui-runtime.d.ts +241 -0
  12. package/dist/preview/host-ui-runtime.js +3236 -0
  13. package/dist/preview/types.d.ts +83 -0
  14. package/dist/preview/types.js +1 -0
  15. package/dist/types.d.ts +108 -0
  16. package/dist/types.js +10 -0
  17. package/dist/ui-engine/block-layout.d.ts +8 -0
  18. package/dist/ui-engine/block-layout.js +95 -0
  19. package/dist/ui-engine/color.d.ts +49 -0
  20. package/dist/ui-engine/color.js +273 -0
  21. package/dist/ui-engine/css-parser.d.ts +155 -0
  22. package/dist/ui-engine/css-parser.js +1058 -0
  23. package/dist/ui-engine/default-keyboards.d.ts +3 -0
  24. package/dist/ui-engine/default-keyboards.js +53 -0
  25. package/dist/ui-engine/easing.d.ts +12 -0
  26. package/dist/ui-engine/easing.js +76 -0
  27. package/dist/ui-engine/font-assets.d.ts +50 -0
  28. package/dist/ui-engine/font-assets.js +431 -0
  29. package/dist/ui-engine/html-parser.d.ts +116 -0
  30. package/dist/ui-engine/html-parser.js +284 -0
  31. package/dist/ui-engine/image-assets.d.ts +21 -0
  32. package/dist/ui-engine/image-assets.js +125 -0
  33. package/dist/ui-engine/inline-parser.d.ts +30 -0
  34. package/dist/ui-engine/inline-parser.js +66 -0
  35. package/dist/ui-engine/keyframes.d.ts +3 -0
  36. package/dist/ui-engine/keyframes.js +39 -0
  37. package/dist/ui-engine/layout-engine.d.ts +35 -0
  38. package/dist/ui-engine/layout-engine.js +203 -0
  39. package/dist/ui-engine/model.d.ts +239 -0
  40. package/dist/ui-engine/model.js +987 -0
  41. package/dist/ui-engine/rich-layout.d.ts +51 -0
  42. package/dist/ui-engine/rich-layout.js +220 -0
  43. package/dist/ui-engine/run-types.d.ts +13 -0
  44. package/dist/ui-engine/run-types.js +1 -0
  45. package/dist/ui-engine/runtime-header/antialiasing.d.ts +1 -0
  46. package/dist/ui-engine/runtime-header/antialiasing.js +141 -0
  47. package/dist/ui-engine/runtime-header/blend-bodies.d.ts +1 -0
  48. package/dist/ui-engine/runtime-header/blend-bodies.js +39 -0
  49. package/dist/ui-engine/runtime-header/canvas-helpers.d.ts +1 -0
  50. package/dist/ui-engine/runtime-header/canvas-helpers.js +155 -0
  51. package/dist/ui-engine/runtime-header/canvas-scrollbar.d.ts +1 -0
  52. package/dist/ui-engine/runtime-header/canvas-scrollbar.js +105 -0
  53. package/dist/ui-engine/runtime-header/color-mono-refresh.d.ts +1 -0
  54. package/dist/ui-engine/runtime-header/color-mono-refresh.js +154 -0
  55. package/dist/ui-engine/runtime-header/dirty-scroll-mutators.d.ts +1 -0
  56. package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +169 -0
  57. package/dist/ui-engine/runtime-header/display-shim.d.ts +1 -0
  58. package/dist/ui-engine/runtime-header/display-shim.js +104 -0
  59. package/dist/ui-engine/runtime-header/forward-decls.d.ts +1 -0
  60. package/dist/ui-engine/runtime-header/forward-decls.js +137 -0
  61. package/dist/ui-engine/runtime-header/guard-close.d.ts +1 -0
  62. package/dist/ui-engine/runtime-header/guard-close.js +8 -0
  63. package/dist/ui-engine/runtime-header/image-drawing.d.ts +1 -0
  64. package/dist/ui-engine/runtime-header/image-drawing.js +300 -0
  65. package/dist/ui-engine/runtime-header/init-press-input.d.ts +1 -0
  66. package/dist/ui-engine/runtime-header/init-press-input.js +126 -0
  67. package/dist/ui-engine/runtime-header/keyboard.d.ts +1 -0
  68. package/dist/ui-engine/runtime-header/keyboard.js +342 -0
  69. package/dist/ui-engine/runtime-header/node-decoration.d.ts +1 -0
  70. package/dist/ui-engine/runtime-header/node-decoration.js +212 -0
  71. package/dist/ui-engine/runtime-header/paint-order-coords.d.ts +1 -0
  72. package/dist/ui-engine/runtime-header/paint-order-coords.js +190 -0
  73. package/dist/ui-engine/runtime-header/paint-rects-repair.d.ts +1 -0
  74. package/dist/ui-engine/runtime-header/paint-rects-repair.js +553 -0
  75. package/dist/ui-engine/runtime-header/scroll-physics.d.ts +1 -0
  76. package/dist/ui-engine/runtime-header/scroll-physics.js +161 -0
  77. package/dist/ui-engine/runtime-header/state-bindings-nav.d.ts +1 -0
  78. package/dist/ui-engine/runtime-header/state-bindings-nav.js +176 -0
  79. package/dist/ui-engine/runtime-header/structs.d.ts +1 -0
  80. package/dist/ui-engine/runtime-header/structs.js +220 -0
  81. package/dist/ui-engine/runtime-header/text-rendering.d.ts +1 -0
  82. package/dist/ui-engine/runtime-header/text-rendering.js +715 -0
  83. package/dist/ui-engine/runtime-header/tick/bindings-phase.d.ts +1 -0
  84. package/dist/ui-engine/runtime-header/tick/bindings-phase.js +103 -0
  85. package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.d.ts +1 -0
  86. package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +1051 -0
  87. package/dist/ui-engine/runtime-header/tick/flush-phase.d.ts +1 -0
  88. package/dist/ui-engine/runtime-header/tick/flush-phase.js +17 -0
  89. package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.d.ts +1 -0
  90. package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +15 -0
  91. package/dist/ui-engine/runtime-header/tick/transitions-phase.d.ts +1 -0
  92. package/dist/ui-engine/runtime-header/tick/transitions-phase.js +213 -0
  93. package/dist/ui-engine/runtime-header/tick-motion-helpers.d.ts +1 -0
  94. package/dist/ui-engine/runtime-header/tick-motion-helpers.js +26 -0
  95. package/dist/ui-engine/runtime-header/tick.d.ts +1 -0
  96. package/dist/ui-engine/runtime-header/tick.js +17 -0
  97. package/dist/ui-engine/runtime-header/touch-keyboard-fwd.d.ts +1 -0
  98. package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +406 -0
  99. package/dist/ui-engine/runtime-header/types-defines.d.ts +1 -0
  100. package/dist/ui-engine/runtime-header/types-defines.js +79 -0
  101. package/dist/ui-engine/runtime-header.d.ts +1 -0
  102. package/dist/ui-engine/runtime-header.js +69 -0
  103. package/dist/ui-engine/scroll-memory-diagnostics.d.ts +16 -0
  104. package/dist/ui-engine/scroll-memory-diagnostics.js +58 -0
  105. package/dist/ui-engine/select-engine.d.ts +4 -0
  106. package/dist/ui-engine/select-engine.js +19 -0
  107. package/dist/ui-engine/style-resolver.d.ts +77 -0
  108. package/dist/ui-engine/style-resolver.js +341 -0
  109. package/dist/ui-engine/text-layout.d.ts +19 -0
  110. package/dist/ui-engine/text-layout.js +92 -0
  111. package/dist/ui-engine/transpile-ui.d.ts +11 -0
  112. package/dist/ui-engine/transpile-ui.js +33 -0
  113. package/dist/ui-engine/ua-stylesheet.d.ts +3 -0
  114. package/dist/ui-engine/ua-stylesheet.js +56 -0
  115. package/dist/ui-engine/ui-file-splitter.d.ts +14 -0
  116. package/dist/ui-engine/ui-file-splitter.js +47 -0
  117. package/dist/ui-engine/ui-lowering.d.ts +33 -0
  118. package/dist/ui-engine/ui-lowering.js +390 -0
  119. package/dist/ui-engine/ui-registry.d.ts +81 -0
  120. package/dist/ui-engine/ui-registry.js +429 -0
  121. package/dist/ui-engine/yoga-layout.d.ts +12 -0
  122. package/dist/ui-engine/yoga-layout.js +314 -0
  123. package/package.json +86 -0
  124. package/src/engine-index.ts +51 -0
  125. package/src/index.ts +232 -0
  126. package/src/preview/build-program.ts +734 -0
  127. package/src/preview/host-gfx.ts +580 -0
  128. package/src/preview/host-ui-runtime.ts +3245 -0
  129. package/src/preview/types.ts +96 -0
  130. package/src/types.ts +127 -0
  131. package/src/ui-engine/block-layout.ts +104 -0
  132. package/src/ui-engine/color.ts +263 -0
  133. package/src/ui-engine/css-parser.ts +1003 -0
  134. package/src/ui-engine/default-keyboards.ts +58 -0
  135. package/src/ui-engine/easing.ts +53 -0
  136. package/src/ui-engine/font-assets.ts +512 -0
  137. package/src/ui-engine/html-parser.ts +397 -0
  138. package/src/ui-engine/image-assets.ts +170 -0
  139. package/src/ui-engine/inline-parser.ts +78 -0
  140. package/src/ui-engine/keyframes.ts +47 -0
  141. package/src/ui-engine/layout-engine.ts +228 -0
  142. package/src/ui-engine/model.ts +1225 -0
  143. package/src/ui-engine/rich-layout.ts +257 -0
  144. package/src/ui-engine/run-types.ts +14 -0
  145. package/src/ui-engine/runtime-header/antialiasing.ts +141 -0
  146. package/src/ui-engine/runtime-header/blend-bodies.ts +39 -0
  147. package/src/ui-engine/runtime-header/canvas-helpers.ts +155 -0
  148. package/src/ui-engine/runtime-header/canvas-scrollbar.ts +105 -0
  149. package/src/ui-engine/runtime-header/color-mono-refresh.ts +154 -0
  150. package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +169 -0
  151. package/src/ui-engine/runtime-header/display-shim.ts +104 -0
  152. package/src/ui-engine/runtime-header/forward-decls.ts +137 -0
  153. package/src/ui-engine/runtime-header/guard-close.ts +8 -0
  154. package/src/ui-engine/runtime-header/image-drawing.ts +300 -0
  155. package/src/ui-engine/runtime-header/init-press-input.ts +126 -0
  156. package/src/ui-engine/runtime-header/keyboard.ts +342 -0
  157. package/src/ui-engine/runtime-header/node-decoration.ts +212 -0
  158. package/src/ui-engine/runtime-header/paint-order-coords.ts +190 -0
  159. package/src/ui-engine/runtime-header/paint-rects-repair.ts +553 -0
  160. package/src/ui-engine/runtime-header/scroll-physics.ts +161 -0
  161. package/src/ui-engine/runtime-header/state-bindings-nav.ts +176 -0
  162. package/src/ui-engine/runtime-header/structs.ts +220 -0
  163. package/src/ui-engine/runtime-header/text-rendering.ts +715 -0
  164. package/src/ui-engine/runtime-header/tick/bindings-phase.ts +103 -0
  165. package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +1051 -0
  166. package/src/ui-engine/runtime-header/tick/flush-phase.ts +17 -0
  167. package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +15 -0
  168. package/src/ui-engine/runtime-header/tick/transitions-phase.ts +213 -0
  169. package/src/ui-engine/runtime-header/tick-motion-helpers.ts +26 -0
  170. package/src/ui-engine/runtime-header/tick.ts +18 -0
  171. package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +406 -0
  172. package/src/ui-engine/runtime-header/types-defines.ts +79 -0
  173. package/src/ui-engine/runtime-header.ts +71 -0
  174. package/src/ui-engine/scroll-memory-diagnostics.ts +79 -0
  175. package/src/ui-engine/select-engine.ts +21 -0
  176. package/src/ui-engine/style-resolver.ts +416 -0
  177. package/src/ui-engine/text-layout.ts +114 -0
  178. package/src/ui-engine/transpile-ui.ts +45 -0
  179. package/src/ui-engine/ua-stylesheet.ts +60 -0
  180. package/src/ui-engine/ui-file-splitter.ts +59 -0
  181. package/src/ui-engine/ui-lowering.ts +459 -0
  182. package/src/ui-engine/ui-registry.ts +530 -0
  183. package/src/ui-engine/yoga-layout.ts +309 -0
@@ -0,0 +1,47 @@
1
+ import { resolveColor } from "./color.js";
2
+ import type { KeyframeSet } from "./css-parser.js";
3
+ import {
4
+ clampInt16,
5
+ cssOpacityToPercent,
6
+ cssPx,
7
+ KEYFRAME_PROP_BG,
8
+ KEYFRAME_PROP_FG,
9
+ KEYFRAME_PROP_OPACITY,
10
+ KEYFRAME_PROP_SIZE,
11
+ KEYFRAME_PROP_TRANSFORM,
12
+ parseTransform,
13
+ type KeyframeSetModel,
14
+ } from "./model.js";
15
+
16
+ export function buildKeyframeSets(rawKeyframes: KeyframeSet[], colorFormat: "rgb565" | "rgb666" | "rgb888" | "mono"): KeyframeSetModel[] {
17
+ return rawKeyframes.map((ks) => ({
18
+ name: ks.name,
19
+ stops: ks.stops.map((stop) => {
20
+ let props = 0;
21
+ if (stop.background) props |= KEYFRAME_PROP_BG;
22
+ if (stop.color) props |= KEYFRAME_PROP_FG;
23
+ if (stop.opacity) props |= KEYFRAME_PROP_OPACITY;
24
+ if (stop.transform || stop.left || stop.top) props |= KEYFRAME_PROP_TRANSFORM;
25
+ if (stop.width || stop.height) props |= KEYFRAME_PROP_SIZE;
26
+ const transform = parseTransform(stop.transform);
27
+ const x = transform.x + cssPx(stop.left);
28
+ const y = transform.y + cssPx(stop.top);
29
+ return {
30
+ percent: stop.percent,
31
+ props,
32
+ bg: stop.background ? resolveColor(stop.background, colorFormat) : 0,
33
+ fg: stop.color ? resolveColor(stop.color, colorFormat) : 0,
34
+ opacity: stop.opacity ? cssOpacityToPercent(stop.opacity) : 100,
35
+ transformOffsetX: clampInt16(x),
36
+ transformOffsetY: clampInt16(y),
37
+ translatePctX: clampInt16(transform.pctX),
38
+ translatePctY: clampInt16(transform.pctY),
39
+ scaleX: transform.scaleX,
40
+ scaleY: transform.scaleY,
41
+ rotateDeg: clampInt16(transform.rotateDeg),
42
+ width: stop.width ? Math.max(0, Math.min(32767, cssPx(stop.width))) : 0,
43
+ height: stop.height ? Math.max(0, Math.min(32767, cssPx(stop.height))) : 0,
44
+ };
45
+ }),
46
+ }));
47
+ }
@@ -0,0 +1,228 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Layout engine — pluggable, staged layout.
3
+ //
4
+ // style tree → measure pass → arrange pass (engine) → computed boxes
5
+ //
6
+ // v1 ships BlockLayoutEngine only. The LayoutEngine interface + measure()
7
+ // are built so that FlexLayoutEngine (v2) is a pure addition: no rewrite of
8
+ // measure, the style tree, the node representation, or the draw layer.
9
+ //
10
+ // measure() is general (returns intrinsic {w,h}) even though block layout
11
+ // only needs it for text — flex reuses it for flex-basis:content.
12
+ // ---------------------------------------------------------------------------
13
+
14
+ import { StyledNode } from "./style-resolver.js";
15
+ import { layoutText } from "./text-layout.js";
16
+ import { layoutRuns } from "./rich-layout.js";
17
+ import { assetTextWidth } from "./font-assets.js";
18
+ import type { UIFontAssetModel } from "./font-assets.js";
19
+
20
+ export interface Box { x: number; y: number; w: number; h: number; }
21
+
22
+ export interface IntrinsicSize { w: number; h: number; }
23
+
24
+ export interface LayoutEngine {
25
+ /** Engine id, used to select on the `display` style property. */
26
+ readonly id: "block" | "flex" | "grid";
27
+ /**
28
+ * Arrange a styled tree into a flat list of boxes (pre-order, root first).
29
+ * measureFn provides intrinsic sizes (e.g. text dimensions from the font).
30
+ */
31
+ arrange(
32
+ root: StyledNode,
33
+ viewport: Box,
34
+ measureFn: (node: StyledNode, availableWidth?: number) => IntrinsicSize,
35
+ ): Box[];
36
+ }
37
+
38
+ /** Static display:none predicate shared by layout engines and lowering. */
39
+ export function isDisplayNone(node: StyledNode): boolean {
40
+ return node.style.display?.trim().toLowerCase() === "none";
41
+ }
42
+
43
+ /** Parse CSS aspect-ratio values: "16 / 9", "1/1", or "1.777". */
44
+ export function parseAspectRatio(value: string | undefined): number | undefined {
45
+ const raw = value?.trim().toLowerCase();
46
+ if (!raw || raw === "auto") return undefined;
47
+ const parts = raw.split("/").map((part) => part.trim()).filter(Boolean);
48
+ if (parts.length === 1) {
49
+ const ratio = parseFloat(parts[0]);
50
+ return Number.isFinite(ratio) && ratio > 0 ? ratio : undefined;
51
+ }
52
+ if (parts.length === 2) {
53
+ const width = parseFloat(parts[0]);
54
+ const height = parseFloat(parts[1]);
55
+ if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
56
+ return width / height;
57
+ }
58
+ }
59
+ return undefined;
60
+ }
61
+
62
+ // The Adafruit GFX default font: 5×7 pixel glyphs, 6px advance width per char.
63
+ // At setTextSize(N), advance = 6×N, height = 8×N (7 glyph + 1 descender line).
64
+ // The draw dispatch in the runtime header uses setTextSize(2).
65
+ const GFX_TEXT_SIZE = 2;
66
+ const GFX_ADVANCE_PER_CHAR = 6 * GFX_TEXT_SIZE; // 12px at size 2
67
+ const GFX_CHAR_HEIGHT = 8 * GFX_TEXT_SIZE; // 16px at size 2
68
+
69
+ /** Compute the GFX text size from a node's CSS font-size.
70
+ * Mirrors the logic in model.ts textSizeOf(). font-weight does NOT inflate
71
+ * the bucket (matching web behavior); see model.ts:textSizeOf for rationale. */
72
+ function gfxTextSizeOf(node: StyledNode): number {
73
+ let size = GFX_TEXT_SIZE;
74
+ if (node.style.fontSize) {
75
+ const px = parseInt(node.style.fontSize, 10);
76
+ if (!isNaN(px)) {
77
+ if (px <= 12) size = 1;
78
+ else if (px <= 20) size = 2;
79
+ else if (px <= 28) size = 3;
80
+ else size = 4;
81
+ }
82
+ }
83
+ return size;
84
+ }
85
+
86
+ function letterSpacingOf(node: StyledNode): number {
87
+ if (!node.style.letterSpacing) return 0;
88
+ const px = parseInt(node.style.letterSpacing, 10);
89
+ return Number.isFinite(px) ? px : 0;
90
+ }
91
+
92
+ function lineHeightOf(node: StyledNode, charH: number): number {
93
+ const raw = node.style.lineHeight?.trim();
94
+ if (!raw || raw === "normal") return charH;
95
+ if (raw.endsWith("%")) {
96
+ const pct = parseFloat(raw);
97
+ return Number.isFinite(pct) ? Math.max(1, Math.round(charH * pct / 100)) : charH;
98
+ }
99
+ if (/^-?\d*\.?\d+$/.test(raw)) {
100
+ const n = parseFloat(raw);
101
+ return Number.isFinite(n) ? Math.max(1, Math.round(charH * n)) : charH;
102
+ }
103
+ const px = parseInt(raw, 10);
104
+ return Number.isFinite(px) && px > 0 ? px : charH;
105
+ }
106
+
107
+ function applyTextTransform(text: string | undefined, node: StyledNode): string {
108
+ const value = text ?? "";
109
+ switch (node.style.textTransform) {
110
+ case "uppercase": return value.toUpperCase();
111
+ case "lowercase": return value.toLowerCase();
112
+ case "capitalize": return value.replace(/\b\w/g, (c) => c.toUpperCase());
113
+ default: return value;
114
+ }
115
+ }
116
+
117
+ function textWidthOf(text: string, advance: number): number {
118
+ let w = 0;
119
+ for (const _ch of text) w += advance;
120
+ return w;
121
+ }
122
+
123
+ /** Measure a node's intrinsic size. Accounts for the Adafruit GFX font metrics
124
+ * and the text size the draw dispatch will use. When `fontAssets` is provided,
125
+ * text widths use the real per-glyph advances of any matching custom
126
+ * @font-face (the runtime draws at those advances); otherwise they fall back
127
+ * to the 6*ts default-font advance. */
128
+ export function measure(node: StyledNode, availableWidth?: number, fontAssets: UIFontAssetModel[] = []): IntrinsicSize {
129
+ // Rich-text (inline runs): size the node from its multi-run layout. Each run
130
+ // is measured at its own font/size advance (per-run style), and the laid-out
131
+ // width/height drive the node's box. This keeps flex sizing in sync with the
132
+ // precomputed runLines the lowering bakes (and the runtime draws).
133
+ if (node.runs && node.runs.length > 0) {
134
+ const layoutInput = node.runs.map(r => {
135
+ const rs = { ...node.style, ...r.style } as typeof node.style;
136
+ const rTs = gfxTextSizeOf({ ...node, style: rs });
137
+ const rAdvance = 6 * rTs + letterSpacingOf({ ...node, style: rs });
138
+ return {
139
+ text: applyTextTransform(r.text, { ...node, style: rs }),
140
+ hardBreak: r.hardBreak,
141
+ measureText: (s: string) => assetTextWidth(s, rs, fontAssets) ?? textWidthOf(s, rAdvance),
142
+ height: 8 * rTs,
143
+ ascent: 7 * rTs,
144
+ };
145
+ });
146
+ const layout = layoutRuns(layoutInput, { maxWidth: availableWidth, whiteSpace: node.style.whiteSpace });
147
+ return { w: layout.width, h: layout.height };
148
+ }
149
+ // Per-node text size (from font-size + font-weight CSS).
150
+ const ts = gfxTextSizeOf(node);
151
+ const advance = 6 * ts + letterSpacingOf(node);
152
+ const charH = 8 * ts;
153
+ // For interpolation text (e.g. "taps: {count}"), strip the braces so layout
154
+ // measures "taps: count" — closer to the resolved runtime width than the
155
+ // literal "{count}". The braces are an authoring delimiter, not rendered.
156
+ const stripInterp = (value: string | undefined): string =>
157
+ node.hasInterpolation ? (value ?? "").replace(/[{}]/g, "") : (value ?? "");
158
+ // Width of a string using the asset font's real advances when the node has
159
+ // one, else the 6*ts default-font advance. This keeps the measured box width
160
+ // in sync with what the runtime actually draws (avoids overflow/clipping).
161
+ const widthOf = (value: string): number =>
162
+ assetTextWidth(stripInterp(value), node.style, fontAssets) ?? textWidthOf(stripInterp(value), advance);
163
+ if (node.tag === "text" || node.tag === "button" || node.tag === "select") {
164
+ if (node.tag === "select") {
165
+ // Size to the longest option, not the full comma-separated text
166
+ const options = node.options && node.options.length > 0
167
+ ? node.options.map((option) => option.text)
168
+ : (node.text ?? "").split(",").map(s => s.trim()).filter(Boolean);
169
+ const longest = options.length > 0 ? options.reduce((a, b) => a.length >= b.length ? a : b) : "";
170
+ return { w: widthOf(applyTextTransform(longest, node)), h: lineHeightOf(node, charH) };
171
+ }
172
+ const text = applyTextTransform(stripInterp(node.text), node);
173
+ const layout = layoutText(text, {
174
+ maxWidth: availableWidth,
175
+ whiteSpace: node.style.whiteSpace,
176
+ lineHeight: lineHeightOf(node, charH),
177
+ measureText: widthOf,
178
+ });
179
+ return { w: layout.width, h: layout.height };
180
+ }
181
+ if (node.tag === "check" || node.tag === "radio") {
182
+ // Checkbox/radio: 16px indicator + 6px gap + label text
183
+ const text = applyTextTransform(stripInterp(node.text), node);
184
+ const labelMaxWidth = availableWidth !== undefined ? Math.max(0, availableWidth - 22) : undefined;
185
+ const layout = layoutText(text, {
186
+ maxWidth: labelMaxWidth,
187
+ whiteSpace: node.style.whiteSpace,
188
+ lineHeight: lineHeightOf(node, charH),
189
+ measureText: widthOf,
190
+ });
191
+ return { w: 16 + 6 + layout.width, h: Math.max(layout.height, 16) };
192
+ }
193
+ if (node.tag === "progress") {
194
+ // Progress bar: default 200px wide, 12px tall
195
+ return { w: 200, h: 12 };
196
+ }
197
+ if (node.tag === "range") {
198
+ // Slider: default 200px wide, 20px tall (roomy track for touch)
199
+ return { w: 200, h: 20 };
200
+ }
201
+ if (node.tag === "input") {
202
+ // Input field: sized to the placeholder or a default width, 20px tall.
203
+ const text = node.placeholder ?? "";
204
+ const textW = text.length > 0 ? text.length * GFX_ADVANCE_PER_CHAR : 120;
205
+ return { w: Math.max(textW + 16, 120), h: 20 };
206
+ }
207
+ if (node.tag === "img") {
208
+ return { w: (node as any).imgWidth ?? 32, h: (node as any).imgHeight ?? 32 };
209
+ }
210
+ if (node.tag === "canvas") {
211
+ // Canvas intrinsic size = its width/height attributes. Without this the
212
+ // layout collapsed the node to 0x0 (no intrinsic size), so ui.drawCanvas
213
+ // content drew into a 2x2 sliver and was invisible.
214
+ return { w: (node as any).canvasW ?? 0, h: (node as any).canvasH ?? 0 };
215
+ }
216
+ if (node.tag === "list") {
217
+ return { w: 0, h: 100 }; // lists default to 100px tall, expand via flex
218
+ }
219
+ // Containers have no intrinsic size in block layout — they fill available.
220
+ return { w: 0, h: 0 };
221
+ }
222
+
223
+ /** Bind a set of font assets to measure(), returning a measureFn the layout
224
+ * engines accept. Text nodes using a custom @font-face then measure at the
225
+ * font's real glyph advances; nodes on the default font are unaffected. */
226
+ export function measureWithFonts(fontAssets: UIFontAssetModel[]): (node: StyledNode, availableWidth?: number) => IntrinsicSize {
227
+ return (node, availableWidth) => measure(node, availableWidth, fontAssets);
228
+ }