@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,39 @@
1
+ import { resolveColor } from "./color.js";
2
+ import { clampInt16, cssOpacityToPercent, cssPx, KEYFRAME_PROP_BG, KEYFRAME_PROP_FG, KEYFRAME_PROP_OPACITY, KEYFRAME_PROP_SIZE, KEYFRAME_PROP_TRANSFORM, parseTransform, } from "./model.js";
3
+ export function buildKeyframeSets(rawKeyframes, colorFormat) {
4
+ return rawKeyframes.map((ks) => ({
5
+ name: ks.name,
6
+ stops: ks.stops.map((stop) => {
7
+ let props = 0;
8
+ if (stop.background)
9
+ props |= KEYFRAME_PROP_BG;
10
+ if (stop.color)
11
+ props |= KEYFRAME_PROP_FG;
12
+ if (stop.opacity)
13
+ props |= KEYFRAME_PROP_OPACITY;
14
+ if (stop.transform || stop.left || stop.top)
15
+ props |= KEYFRAME_PROP_TRANSFORM;
16
+ if (stop.width || stop.height)
17
+ props |= KEYFRAME_PROP_SIZE;
18
+ const transform = parseTransform(stop.transform);
19
+ const x = transform.x + cssPx(stop.left);
20
+ const y = transform.y + cssPx(stop.top);
21
+ return {
22
+ percent: stop.percent,
23
+ props,
24
+ bg: stop.background ? resolveColor(stop.background, colorFormat) : 0,
25
+ fg: stop.color ? resolveColor(stop.color, colorFormat) : 0,
26
+ opacity: stop.opacity ? cssOpacityToPercent(stop.opacity) : 100,
27
+ transformOffsetX: clampInt16(x),
28
+ transformOffsetY: clampInt16(y),
29
+ translatePctX: clampInt16(transform.pctX),
30
+ translatePctY: clampInt16(transform.pctY),
31
+ scaleX: transform.scaleX,
32
+ scaleY: transform.scaleY,
33
+ rotateDeg: clampInt16(transform.rotateDeg),
34
+ width: stop.width ? Math.max(0, Math.min(32767, cssPx(stop.width))) : 0,
35
+ height: stop.height ? Math.max(0, Math.min(32767, cssPx(stop.height))) : 0,
36
+ };
37
+ }),
38
+ }));
39
+ }
@@ -0,0 +1,35 @@
1
+ import { StyledNode } from "./style-resolver.js";
2
+ import type { UIFontAssetModel } from "./font-assets.js";
3
+ export interface Box {
4
+ x: number;
5
+ y: number;
6
+ w: number;
7
+ h: number;
8
+ }
9
+ export interface IntrinsicSize {
10
+ w: number;
11
+ h: number;
12
+ }
13
+ export interface LayoutEngine {
14
+ /** Engine id, used to select on the `display` style property. */
15
+ readonly id: "block" | "flex" | "grid";
16
+ /**
17
+ * Arrange a styled tree into a flat list of boxes (pre-order, root first).
18
+ * measureFn provides intrinsic sizes (e.g. text dimensions from the font).
19
+ */
20
+ arrange(root: StyledNode, viewport: Box, measureFn: (node: StyledNode, availableWidth?: number) => IntrinsicSize): Box[];
21
+ }
22
+ /** Static display:none predicate shared by layout engines and lowering. */
23
+ export declare function isDisplayNone(node: StyledNode): boolean;
24
+ /** Parse CSS aspect-ratio values: "16 / 9", "1/1", or "1.777". */
25
+ export declare function parseAspectRatio(value: string | undefined): number | undefined;
26
+ /** Measure a node's intrinsic size. Accounts for the Adafruit GFX font metrics
27
+ * and the text size the draw dispatch will use. When `fontAssets` is provided,
28
+ * text widths use the real per-glyph advances of any matching custom
29
+ * @font-face (the runtime draws at those advances); otherwise they fall back
30
+ * to the 6*ts default-font advance. */
31
+ export declare function measure(node: StyledNode, availableWidth?: number, fontAssets?: UIFontAssetModel[]): IntrinsicSize;
32
+ /** Bind a set of font assets to measure(), returning a measureFn the layout
33
+ * engines accept. Text nodes using a custom @font-face then measure at the
34
+ * font's real glyph advances; nodes on the default font are unaffected. */
35
+ export declare function measureWithFonts(fontAssets: UIFontAssetModel[]): (node: StyledNode, availableWidth?: number) => IntrinsicSize;
@@ -0,0 +1,203 @@
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
+ import { layoutText } from "./text-layout.js";
14
+ import { layoutRuns } from "./rich-layout.js";
15
+ import { assetTextWidth } from "./font-assets.js";
16
+ /** Static display:none predicate shared by layout engines and lowering. */
17
+ export function isDisplayNone(node) {
18
+ return node.style.display?.trim().toLowerCase() === "none";
19
+ }
20
+ /** Parse CSS aspect-ratio values: "16 / 9", "1/1", or "1.777". */
21
+ export function parseAspectRatio(value) {
22
+ const raw = value?.trim().toLowerCase();
23
+ if (!raw || raw === "auto")
24
+ return undefined;
25
+ const parts = raw.split("/").map((part) => part.trim()).filter(Boolean);
26
+ if (parts.length === 1) {
27
+ const ratio = parseFloat(parts[0]);
28
+ return Number.isFinite(ratio) && ratio > 0 ? ratio : undefined;
29
+ }
30
+ if (parts.length === 2) {
31
+ const width = parseFloat(parts[0]);
32
+ const height = parseFloat(parts[1]);
33
+ if (Number.isFinite(width) && Number.isFinite(height) && width > 0 && height > 0) {
34
+ return width / height;
35
+ }
36
+ }
37
+ return undefined;
38
+ }
39
+ // The Adafruit GFX default font: 5×7 pixel glyphs, 6px advance width per char.
40
+ // At setTextSize(N), advance = 6×N, height = 8×N (7 glyph + 1 descender line).
41
+ // The draw dispatch in the runtime header uses setTextSize(2).
42
+ const GFX_TEXT_SIZE = 2;
43
+ const GFX_ADVANCE_PER_CHAR = 6 * GFX_TEXT_SIZE; // 12px at size 2
44
+ const GFX_CHAR_HEIGHT = 8 * GFX_TEXT_SIZE; // 16px at size 2
45
+ /** Compute the GFX text size from a node's CSS font-size.
46
+ * Mirrors the logic in model.ts textSizeOf(). font-weight does NOT inflate
47
+ * the bucket (matching web behavior); see model.ts:textSizeOf for rationale. */
48
+ function gfxTextSizeOf(node) {
49
+ let size = GFX_TEXT_SIZE;
50
+ if (node.style.fontSize) {
51
+ const px = parseInt(node.style.fontSize, 10);
52
+ if (!isNaN(px)) {
53
+ if (px <= 12)
54
+ size = 1;
55
+ else if (px <= 20)
56
+ size = 2;
57
+ else if (px <= 28)
58
+ size = 3;
59
+ else
60
+ size = 4;
61
+ }
62
+ }
63
+ return size;
64
+ }
65
+ function letterSpacingOf(node) {
66
+ if (!node.style.letterSpacing)
67
+ return 0;
68
+ const px = parseInt(node.style.letterSpacing, 10);
69
+ return Number.isFinite(px) ? px : 0;
70
+ }
71
+ function lineHeightOf(node, charH) {
72
+ const raw = node.style.lineHeight?.trim();
73
+ if (!raw || raw === "normal")
74
+ return charH;
75
+ if (raw.endsWith("%")) {
76
+ const pct = parseFloat(raw);
77
+ return Number.isFinite(pct) ? Math.max(1, Math.round(charH * pct / 100)) : charH;
78
+ }
79
+ if (/^-?\d*\.?\d+$/.test(raw)) {
80
+ const n = parseFloat(raw);
81
+ return Number.isFinite(n) ? Math.max(1, Math.round(charH * n)) : charH;
82
+ }
83
+ const px = parseInt(raw, 10);
84
+ return Number.isFinite(px) && px > 0 ? px : charH;
85
+ }
86
+ function applyTextTransform(text, node) {
87
+ const value = text ?? "";
88
+ switch (node.style.textTransform) {
89
+ case "uppercase": return value.toUpperCase();
90
+ case "lowercase": return value.toLowerCase();
91
+ case "capitalize": return value.replace(/\b\w/g, (c) => c.toUpperCase());
92
+ default: return value;
93
+ }
94
+ }
95
+ function textWidthOf(text, advance) {
96
+ let w = 0;
97
+ for (const _ch of text)
98
+ w += advance;
99
+ return w;
100
+ }
101
+ /** Measure a node's intrinsic size. Accounts for the Adafruit GFX font metrics
102
+ * and the text size the draw dispatch will use. When `fontAssets` is provided,
103
+ * text widths use the real per-glyph advances of any matching custom
104
+ * @font-face (the runtime draws at those advances); otherwise they fall back
105
+ * to the 6*ts default-font advance. */
106
+ export function measure(node, availableWidth, fontAssets = []) {
107
+ // Rich-text (inline runs): size the node from its multi-run layout. Each run
108
+ // is measured at its own font/size advance (per-run style), and the laid-out
109
+ // width/height drive the node's box. This keeps flex sizing in sync with the
110
+ // precomputed runLines the lowering bakes (and the runtime draws).
111
+ if (node.runs && node.runs.length > 0) {
112
+ const layoutInput = node.runs.map(r => {
113
+ const rs = { ...node.style, ...r.style };
114
+ const rTs = gfxTextSizeOf({ ...node, style: rs });
115
+ const rAdvance = 6 * rTs + letterSpacingOf({ ...node, style: rs });
116
+ return {
117
+ text: applyTextTransform(r.text, { ...node, style: rs }),
118
+ hardBreak: r.hardBreak,
119
+ measureText: (s) => assetTextWidth(s, rs, fontAssets) ?? textWidthOf(s, rAdvance),
120
+ height: 8 * rTs,
121
+ ascent: 7 * rTs,
122
+ };
123
+ });
124
+ const layout = layoutRuns(layoutInput, { maxWidth: availableWidth, whiteSpace: node.style.whiteSpace });
125
+ return { w: layout.width, h: layout.height };
126
+ }
127
+ // Per-node text size (from font-size + font-weight CSS).
128
+ const ts = gfxTextSizeOf(node);
129
+ const advance = 6 * ts + letterSpacingOf(node);
130
+ const charH = 8 * ts;
131
+ // For interpolation text (e.g. "taps: {count}"), strip the braces so layout
132
+ // measures "taps: count" — closer to the resolved runtime width than the
133
+ // literal "{count}". The braces are an authoring delimiter, not rendered.
134
+ const stripInterp = (value) => node.hasInterpolation ? (value ?? "").replace(/[{}]/g, "") : (value ?? "");
135
+ // Width of a string using the asset font's real advances when the node has
136
+ // one, else the 6*ts default-font advance. This keeps the measured box width
137
+ // in sync with what the runtime actually draws (avoids overflow/clipping).
138
+ const widthOf = (value) => assetTextWidth(stripInterp(value), node.style, fontAssets) ?? textWidthOf(stripInterp(value), advance);
139
+ if (node.tag === "text" || node.tag === "button" || node.tag === "select") {
140
+ if (node.tag === "select") {
141
+ // Size to the longest option, not the full comma-separated text
142
+ const options = node.options && node.options.length > 0
143
+ ? node.options.map((option) => option.text)
144
+ : (node.text ?? "").split(",").map(s => s.trim()).filter(Boolean);
145
+ const longest = options.length > 0 ? options.reduce((a, b) => a.length >= b.length ? a : b) : "";
146
+ return { w: widthOf(applyTextTransform(longest, node)), h: lineHeightOf(node, charH) };
147
+ }
148
+ const text = applyTextTransform(stripInterp(node.text), node);
149
+ const layout = layoutText(text, {
150
+ maxWidth: availableWidth,
151
+ whiteSpace: node.style.whiteSpace,
152
+ lineHeight: lineHeightOf(node, charH),
153
+ measureText: widthOf,
154
+ });
155
+ return { w: layout.width, h: layout.height };
156
+ }
157
+ if (node.tag === "check" || node.tag === "radio") {
158
+ // Checkbox/radio: 16px indicator + 6px gap + label text
159
+ const text = applyTextTransform(stripInterp(node.text), node);
160
+ const labelMaxWidth = availableWidth !== undefined ? Math.max(0, availableWidth - 22) : undefined;
161
+ const layout = layoutText(text, {
162
+ maxWidth: labelMaxWidth,
163
+ whiteSpace: node.style.whiteSpace,
164
+ lineHeight: lineHeightOf(node, charH),
165
+ measureText: widthOf,
166
+ });
167
+ return { w: 16 + 6 + layout.width, h: Math.max(layout.height, 16) };
168
+ }
169
+ if (node.tag === "progress") {
170
+ // Progress bar: default 200px wide, 12px tall
171
+ return { w: 200, h: 12 };
172
+ }
173
+ if (node.tag === "range") {
174
+ // Slider: default 200px wide, 20px tall (roomy track for touch)
175
+ return { w: 200, h: 20 };
176
+ }
177
+ if (node.tag === "input") {
178
+ // Input field: sized to the placeholder or a default width, 20px tall.
179
+ const text = node.placeholder ?? "";
180
+ const textW = text.length > 0 ? text.length * GFX_ADVANCE_PER_CHAR : 120;
181
+ return { w: Math.max(textW + 16, 120), h: 20 };
182
+ }
183
+ if (node.tag === "img") {
184
+ return { w: node.imgWidth ?? 32, h: node.imgHeight ?? 32 };
185
+ }
186
+ if (node.tag === "canvas") {
187
+ // Canvas intrinsic size = its width/height attributes. Without this the
188
+ // layout collapsed the node to 0x0 (no intrinsic size), so ui.drawCanvas
189
+ // content drew into a 2x2 sliver and was invisible.
190
+ return { w: node.canvasW ?? 0, h: node.canvasH ?? 0 };
191
+ }
192
+ if (node.tag === "list") {
193
+ return { w: 0, h: 100 }; // lists default to 100px tall, expand via flex
194
+ }
195
+ // Containers have no intrinsic size in block layout — they fill available.
196
+ return { w: 0, h: 0 };
197
+ }
198
+ /** Bind a set of font assets to measure(), returning a measureFn the layout
199
+ * engines accept. Text nodes using a custom @font-face then measure at the
200
+ * font's real glyph advances; nodes on the default font are unaffected. */
201
+ export function measureWithFonts(fontAssets) {
202
+ return (node, availableWidth) => measure(node, availableWidth, fontAssets);
203
+ }
@@ -0,0 +1,239 @@
1
+ import type { DisplayProfile } from "@typecad/cuttlefish/api/shared";
2
+ import type { UIFontAssetModel } from "./font-assets.js";
3
+ import type { UIImageAsset } from "./image-assets.js";
4
+ import { type Box } from "./layout-engine.js";
5
+ import type { StyledNode } from "./style-resolver.js";
6
+ export { easeCurveLerpK, TIMING_EASE, TIMING_EASE_IN, TIMING_EASE_IN_OUT, TIMING_EASE_OUT, TIMING_LINEAR, timingFunctionCode, } from "./easing.js";
7
+ export type UINodeKindModel = "fill" | "text" | "button" | "check" | "radio" | "progress" | "range" | "input" | "img" | "list" | "canvas";
8
+ export type UIPropertyModel = "background" | "color" | "text" | "visible" | "borderColor";
9
+ export interface UINodeModel {
10
+ index: number;
11
+ tag: string;
12
+ id?: string;
13
+ classes: string[];
14
+ box: Box;
15
+ bg: number;
16
+ fg: number;
17
+ kind: UINodeKindModel;
18
+ text?: string;
19
+ placeholder?: string;
20
+ valueAttr?: string;
21
+ name?: string;
22
+ checked?: boolean;
23
+ textBuffer: string;
24
+ hasTextBinding: boolean;
25
+ hasBg: boolean;
26
+ textAlign: 0 | 1 | 2;
27
+ textSize: number;
28
+ lineHeight: number;
29
+ letterSpacing: number;
30
+ fontAntialias: boolean;
31
+ fontFace: number;
32
+ borderColor: number;
33
+ borderStyle: 0 | 1 | 2;
34
+ borderWidth: number;
35
+ /** Per-side border widths. When all four equal borderWidth, the runtime draws
36
+ * the uniform rect outline; when any differs, it draws per-side lines. */
37
+ borderTopWidth: number;
38
+ borderRightWidth: number;
39
+ borderBottomWidth: number;
40
+ borderLeftWidth: number;
41
+ borderRadius: number;
42
+ paddingTop?: number;
43
+ paddingRight?: number;
44
+ paddingBottom?: number;
45
+ paddingLeft?: number;
46
+ gradientEnabled: number;
47
+ gradientColor1: number;
48
+ gradientColor2: number;
49
+ outlineColor: number;
50
+ outlineStyle: 0 | 1 | 2;
51
+ outlineWidth: number;
52
+ zIndex: number;
53
+ transformOffsetX: number;
54
+ transformOffsetY: number;
55
+ rotateDeg: number;
56
+ pressedOffsetX: number;
57
+ pressedOffsetY: number;
58
+ shadowCount: number;
59
+ shadowOffsetX: number[];
60
+ shadowOffsetY: number[];
61
+ shadowBlur: number[];
62
+ shadowColor: number[];
63
+ shadowAlpha: number[];
64
+ shadowInset: boolean[];
65
+ textShadowCount: number;
66
+ textShadowOffsetX: number;
67
+ textShadowOffsetY: number;
68
+ textShadowBlur: number;
69
+ textShadowColor: number;
70
+ textShadowAlpha: number;
71
+ underline: number;
72
+ textOverflow: boolean;
73
+ nowrap: boolean;
74
+ whiteSpaceMode: 0 | 1 | 2 | 3;
75
+ visible: boolean;
76
+ opacity: number;
77
+ clearColor: number;
78
+ lastTextWidth: number;
79
+ lastTextHeight: number;
80
+ dirty: boolean;
81
+ value: number;
82
+ options?: Array<{
83
+ value: string;
84
+ text: string;
85
+ }>;
86
+ scrollable: boolean;
87
+ scrollY: number;
88
+ contentHeight: number;
89
+ /** Elastic excursion past a scroll boundary (0 in-bounds; +top, -bottom). */
90
+ overscrollPx: number;
91
+ /** True while a bounce-back / edge-snap settle animation is running. */
92
+ settling: boolean;
93
+ /** scrollY captured at the last container repaint (Mode B shift delta). */
94
+ lastPaintedScrollY: number;
95
+ rangeMin: number;
96
+ rangeMax: number;
97
+ /** For <input>: max character length (0 = use UI_TEXT_BUF). */
98
+ maxlen: number;
99
+ /** For <input>: text or number keyboard. */
100
+ inputType?: "text" | "number";
101
+ /** For <input>: custom keyboard template id. */
102
+ keyboard?: string;
103
+ parentIndex: number;
104
+ subtreeEnd: number;
105
+ screenId: number;
106
+ imgDataId: number;
107
+ objectFit: 0 | 1 | 2 | 3 | 4;
108
+ listItemHeight: number;
109
+ /** Children are produced by callbacks (virtualized), not static nodes. Set for <list>. */
110
+ virtualized: boolean;
111
+ /** Canvas buffer width in pixels (for kind "canvas"). */
112
+ canvasW: number;
113
+ /** Canvas buffer height in pixels (for kind "canvas"). */
114
+ canvasH: number;
115
+ /** Rich-text runs. Present only for text nodes with mixed inline content;
116
+ * absent for plain single-string text nodes. When present, the runs are the
117
+ * node's content and `text` is empty. */
118
+ runs?: UITextRunModel[];
119
+ /** Precomputed wrapped-line geometry for rich-text runs (struct-of-arrays).
120
+ * The parallel arrays are indexed together; segment i is
121
+ * (segRun[i], segText[i], segX[i], segW[i], segLine[i]). Baked at lower time
122
+ * because runs are static-content only (no runtime re-flow). */
123
+ runLines?: RunLines;
124
+ }
125
+ /** One rich-text run in lowered form: a piece of styled inline text with all
126
+ * style fields pre-resolved to their runtime representations. */
127
+ export interface UITextRunModel {
128
+ text: string;
129
+ /** Resolved RGB565/mono foreground color. */
130
+ fg: number;
131
+ /** GFX text size 1-4 (from run font-size + font-weight). */
132
+ textSize: number;
133
+ /** Font asset id (0 = classic GFX bitmap font). */
134
+ fontFace: number;
135
+ /** text-decoration: 0=none, 1=underline, 2=line-through, 3=both. */
136
+ underline: number;
137
+ /** Letter spacing in px. */
138
+ letterSpacing: number;
139
+ /** Resolved screen index for an <a href> run, or -1 if not a link. */
140
+ linkTarget: number;
141
+ }
142
+ /** Precomputed wrapped-line geometry for a rich-text node, as parallel arrays
143
+ * (mirrors how the C++ runtime emits them). Segment arrays are indexed
144
+ * together; each segment belongs to the line given by its segLine entry. */
145
+ export interface RunLines {
146
+ segRun: number[];
147
+ segText: string[];
148
+ segX: number[];
149
+ segW: number[];
150
+ segLine: number[];
151
+ lineY: number[];
152
+ lineH: number[];
153
+ lineBaseline: number[];
154
+ lineW: number[];
155
+ }
156
+ export interface UITransitionModel {
157
+ node: number;
158
+ prop: "background" | "color";
159
+ durationMs: number;
160
+ pressedTarget: number;
161
+ baseTarget: number;
162
+ elapsed: number;
163
+ prevValue: number;
164
+ targetValue: number;
165
+ active: boolean;
166
+ }
167
+ export interface KeyframeStopModel {
168
+ percent: number;
169
+ props: number;
170
+ bg: number;
171
+ fg: number;
172
+ opacity: number;
173
+ transformOffsetX: number;
174
+ transformOffsetY: number;
175
+ translatePctX: number;
176
+ translatePctY: number;
177
+ scaleX: number;
178
+ scaleY: number;
179
+ rotateDeg: number;
180
+ width: number;
181
+ height: number;
182
+ }
183
+ export declare const KEYFRAME_PROP_BG = 1;
184
+ export declare const KEYFRAME_PROP_FG = 2;
185
+ export declare const KEYFRAME_PROP_OPACITY = 4;
186
+ export declare const KEYFRAME_PROP_TRANSFORM = 8;
187
+ export declare const KEYFRAME_PROP_SIZE = 16;
188
+ export interface KeyframeSetModel {
189
+ name: string;
190
+ stops: KeyframeStopModel[];
191
+ }
192
+ export interface AnimationModel {
193
+ node: number;
194
+ keyframeSet: number;
195
+ durationMs: number;
196
+ delayMs: number;
197
+ iterations: number;
198
+ baseWidth: number;
199
+ baseHeight: number;
200
+ originX: number;
201
+ originY: number;
202
+ timingFunction: number;
203
+ }
204
+ export interface UIProgram {
205
+ width: number;
206
+ height: number;
207
+ colorFormat: "rgb565" | "rgb666" | "rgb888" | "mono";
208
+ display?: DisplayProfile;
209
+ fontAssets: UIFontAssetModel[];
210
+ imageAssets: UIImageAsset[];
211
+ nodes: UINodeModel[];
212
+ transitions: UITransitionModel[];
213
+ keyframeSets: KeyframeSetModel[];
214
+ animations: AnimationModel[];
215
+ }
216
+ type ColorFormat = "rgb565" | "rgb666" | "rgb888" | "mono";
217
+ export declare function cssPx(value: string | undefined): number;
218
+ export declare function clampInt8(n: number): number;
219
+ export declare function clampInt16(n: number): number;
220
+ export interface ParsedTransform {
221
+ x: number;
222
+ y: number;
223
+ pctX: number;
224
+ pctY: number;
225
+ scaleX: number;
226
+ scaleY: number;
227
+ rotateDeg: number;
228
+ }
229
+ export declare function parseTransform(transform: string | undefined): ParsedTransform;
230
+ export declare function transformOffset(transform: string | undefined): {
231
+ x: number;
232
+ y: number;
233
+ };
234
+ export declare function transformOriginPercent(value: string | undefined): {
235
+ x: number;
236
+ y: number;
237
+ };
238
+ export declare function cssOpacityToPercent(value: string | undefined): number;
239
+ export declare function lowerUIToModel(root: StyledNode, boxes: Box[], colorFormat: ColorFormat, display?: DisplayProfile, fontAssets?: UIFontAssetModel[], allScreens?: StyledNode[], imageAssetIds?: Map<string, number>, keyframeSets?: KeyframeSetModel[], imageAssets?: UIImageAsset[]): UIProgram;