@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,257 @@
1
+ import { whiteSpaceMode, WhiteSpaceMode } from "./text-layout.js";
2
+
3
+ export interface RichSegment {
4
+ /** Index into the source runs[] this segment came from. A run that wraps
5
+ * across multiple lines produces one segment per line, all with the same
6
+ * runIndex. */
7
+ runIndex: number;
8
+ text: string;
9
+ /** Offset from the line's left edge (pre-alignment). */
10
+ x: number;
11
+ width: number;
12
+ }
13
+ export interface RichLine {
14
+ segments: RichSegment[];
15
+ /** Sum of segment widths + inter-word spaces on this line (no trailing space). */
16
+ width: number;
17
+ /** Tallest run height on this line. */
18
+ height: number;
19
+ /** Tallest ascent on this line (for baseline alignment). */
20
+ ascent: number;
21
+ }
22
+ export interface RichLayoutResult {
23
+ lines: RichLine[];
24
+ /** Max line width. */
25
+ width: number;
26
+ /** Sum of line heights. */
27
+ height: number;
28
+ }
29
+
30
+ export interface LayoutRun {
31
+ text: string;
32
+ hardBreak?: boolean;
33
+ /** Measure a string at THIS run's style (font/size advance). */
34
+ measureText: (s: string) => number;
35
+ /** Rendered content height of this run's font (e.g. 8*textSize). */
36
+ height: number;
37
+ /** Ascent of this run's font (e.g. 7*textSize). */
38
+ ascent: number;
39
+ }
40
+
41
+ /** Greedy multi-run wrapper. Generalizes the single-string wrap to a sequence
42
+ * of styled runs: each candidate is measured with its OWN measureText, breaks
43
+ * happen at spaces and at run boundaries, and per-segment geometry is recorded
44
+ * so the draw layer and link hit-test know where each run sits on each line.
45
+ *
46
+ * Whitespace handling mirrors text-layout.ts: `normal` collapses runs of
47
+ * whitespace and wraps on word boundaries; `nowrap` keeps one line; `pre`
48
+ * preserves whitespace and breaks only on hard breaks/newlines; `pre-line`
49
+ * collapses spaces but keeps newlines. Per-run text is normalized within the
50
+ * run; a space straddling a run boundary collapses to one (owned by the
51
+ * earlier run). */
52
+ export function layoutRuns(
53
+ runs: LayoutRun[],
54
+ options: { maxWidth?: number; whiteSpace?: string },
55
+ ): RichLayoutResult {
56
+ const mode = whiteSpaceMode(options.whiteSpace);
57
+ const maxWidth = options.maxWidth && options.maxWidth > 0 ? Math.trunc(options.maxWidth) : undefined;
58
+ const canWrap = mode !== "nowrap" && mode !== "pre" && maxWidth !== undefined;
59
+
60
+ // Tokenize each run into words (split on spaces), preserving which run + the
61
+ // run's measure function. Each word carries `spaceBefore`: true when source
62
+ // whitespace preceded it (within the run, or trailing whitespace of the
63
+ // previous run before this run's first word). This keeps placement faithful
64
+ // to source whitespace — adjacent runs with no whitespace between them join
65
+ // with no separator.
66
+ type Word = { runIndex: number; text: string; mt: (s: string) => number; h: number; a: number; spaceBefore: boolean };
67
+ type Break = { runIndex: number; mt: (s: string) => number; h: number; a: number };
68
+ type Token = { kind: "word"; w: Word } | { kind: "break"; b: Break };
69
+ const tokens: Token[] = [];
70
+ let prevRunTrailingSpace = false; // did the previous run's text end with whitespace?
71
+ runs.forEach((run, runIndex) => {
72
+ if (run.hardBreak) {
73
+ tokens.push({ kind: "break", b: { runIndex, mt: run.measureText, h: run.height, a: run.ascent } });
74
+ prevRunTrailingSpace = false;
75
+ return;
76
+ }
77
+ const text = normalizeRunText(run.text, mode);
78
+ const leadingSpace = /^\s/.test(run.text);
79
+ if (mode === "pre") {
80
+ // Preserve internal newlines as hard breaks; keep text segments between.
81
+ const parts = text.split("\n");
82
+ parts.forEach((part, i) => {
83
+ if (part) tokens.push({ kind: "word", w: { runIndex, text: part, mt: run.measureText, h: run.height, a: run.ascent, spaceBefore: i === 0 ? (leadingSpace || prevRunTrailingSpace) : false } });
84
+ if (i < parts.length - 1) tokens.push({ kind: "break", b: { runIndex, mt: run.measureText, h: run.height, a: run.ascent } });
85
+ });
86
+ prevRunTrailingSpace = /\s$/.test(text);
87
+ return;
88
+ }
89
+ // normal / nowrap / pre-line: split on whitespace runs into words.
90
+ const words = text.split(/\s+/).filter(Boolean);
91
+ words.forEach((w, i) => {
92
+ // Internal words (i > 0) had whitespace before them within the run.
93
+ // The first word (i === 0) has a space before only if the run's text
94
+ // started with whitespace OR the previous run ended with whitespace.
95
+ const spaceBefore = i > 0 ? true : (leadingSpace || prevRunTrailingSpace);
96
+ tokens.push({ kind: "word", w: { runIndex, text: w, mt: run.measureText, h: run.height, a: run.ascent, spaceBefore } });
97
+ });
98
+ prevRunTrailingSpace = /\s$/.test(run.text);
99
+ });
100
+
101
+ const lines: RichLine[] = [];
102
+ // Current line state.
103
+ let segs: RichSegment[] = [];
104
+ let lineW = 0;
105
+ let lineH = 0;
106
+ let lineA = 0;
107
+ // Current in-progress segment for a run (accumulating words of the same run
108
+ // on the current line, joined by spaces).
109
+ let cur: { runIndex: number; text: string; mt: (s: string) => number; h: number; a: number } | null = null;
110
+
111
+ const flushCurrent = () => {
112
+ if (cur) {
113
+ segs.push({ runIndex: cur.runIndex, text: cur.text, x: lineW, width: cur.mt(cur.text) });
114
+ lineW += cur.mt(cur.text);
115
+ cur = null;
116
+ }
117
+ };
118
+ const commitLine = (minHeight = 0, minAscent = 0) => {
119
+ flushCurrent();
120
+ lineH = Math.max(lineH, minHeight);
121
+ lineA = Math.max(lineA, minAscent);
122
+ lines.push({ segments: segs, width: lineW, height: lineH, ascent: lineA });
123
+ segs = [];
124
+ lineW = 0;
125
+ lineH = 0;
126
+ lineA = 0;
127
+ };
128
+
129
+ for (const tok of tokens) {
130
+ if (tok.kind === "break") {
131
+ commitLine(tok.b.h, tok.b.a);
132
+ continue;
133
+ }
134
+ const word = tok.w;
135
+ // A space precedes this word when the source had whitespace before it
136
+ // (word.spaceBefore) AND we're not at the very start of a line (standard
137
+ // whitespace collapsing trims a leading space on a fresh line). The space
138
+ // is measured with the run that owns the word being added.
139
+ const atLineStart = cur === null && lineW === 0;
140
+ const wantSpace = word.spaceBefore && !atLineStart;
141
+ const sameRun = cur !== null && cur.runIndex === word.runIndex;
142
+
143
+ if (sameRun) {
144
+ // Extending the current segment: "cur word" (space included only if wanted).
145
+ if (wantSpace) {
146
+ const candidate = cur!.text + " " + word.text;
147
+ // lineW holds only committed/flushed width + separator spaces; the
148
+ // in-progress segment `cur` is NOT yet in lineW (only flushCurrent
149
+ // adds it). So the true line width after extending cur is lineW plus
150
+ // the candidate's full width — not lineW minus cur plus candidate
151
+ // (that would under-count by cur's width and fail to wrap intra-run).
152
+ const candidateW = lineW + cur!.mt(candidate);
153
+ if (canWrap && candidateW > maxWidth! && cur!.text) {
154
+ commitLine();
155
+ cur = { runIndex: word.runIndex, text: word.text, mt: word.mt, h: word.h, a: word.a };
156
+ lineH = word.h; lineA = word.a;
157
+ } else {
158
+ cur!.text = candidate;
159
+ lineH = Math.max(lineH, word.h);
160
+ lineA = Math.max(lineA, word.a);
161
+ }
162
+ } else {
163
+ // No space: append directly (adjacent words with no source whitespace).
164
+ const candidate = cur!.text + word.text;
165
+ const candidateW = lineW + cur!.mt(candidate);
166
+ if (canWrap && candidateW > maxWidth! && cur!.text) {
167
+ commitLine();
168
+ cur = { runIndex: word.runIndex, text: word.text, mt: word.mt, h: word.h, a: word.a };
169
+ lineH = word.h; lineA = word.a;
170
+ } else {
171
+ cur!.text = candidate;
172
+ lineH = Math.max(lineH, word.h);
173
+ lineA = Math.max(lineA, word.a);
174
+ }
175
+ }
176
+ } else {
177
+ // Different run (or first word on the line).
178
+ const spaceW = wantSpace ? word.mt(" ") : 0;
179
+ const wordW = word.mt(word.text);
180
+ if (cur) flushCurrent();
181
+ if (canWrap && lineW + spaceW + wordW > maxWidth! && lineW > 0) {
182
+ // Doesn't fit: wrap to a new line. (Space suppressed at line start.)
183
+ commitLine();
184
+ if (canWrap && wordW > maxWidth!) {
185
+ const parts = splitLongWord(word.text, maxWidth!, word.mt);
186
+ for (let i = 0; i < parts.length; i++) {
187
+ if (i < parts.length - 1) {
188
+ segs.push({ runIndex: word.runIndex, text: parts[i], x: 0, width: word.mt(parts[i]) });
189
+ lines.push({ segments: segs, width: word.mt(parts[i]), height: word.h, ascent: word.a });
190
+ segs = [];
191
+ } else {
192
+ cur = { runIndex: word.runIndex, text: parts[i], mt: word.mt, h: word.h, a: word.a };
193
+ lineW = 0;
194
+ lineH = word.h; lineA = word.a;
195
+ }
196
+ }
197
+ } else {
198
+ cur = { runIndex: word.runIndex, text: word.text, mt: word.mt, h: word.h, a: word.a };
199
+ lineH = word.h; lineA = word.a;
200
+ }
201
+ } else if (canWrap && wordW > maxWidth!) {
202
+ // Single word longer than the whole line: split char-by-char.
203
+ if (lineW > 0) commitLine();
204
+ const parts = splitLongWord(word.text, maxWidth!, word.mt);
205
+ for (let i = 0; i < parts.length; i++) {
206
+ if (i < parts.length - 1) {
207
+ segs.push({ runIndex: word.runIndex, text: parts[i], x: 0, width: word.mt(parts[i]) });
208
+ lines.push({ segments: segs, width: word.mt(parts[i]), height: word.h, ascent: word.a });
209
+ segs = [];
210
+ } else {
211
+ cur = { runIndex: word.runIndex, text: parts[i], mt: word.mt, h: word.h, a: word.a };
212
+ lineW = 0;
213
+ lineH = word.h; lineA = word.a;
214
+ }
215
+ }
216
+ } else {
217
+ // Place the separator space (advances lineW, not a segment) then start
218
+ // a new in-progress segment for this word.
219
+ lineW += spaceW;
220
+ cur = { runIndex: word.runIndex, text: word.text, mt: word.mt, h: word.h, a: word.a };
221
+ lineH = Math.max(lineH, word.h);
222
+ lineA = Math.max(lineA, word.a);
223
+ }
224
+ }
225
+ }
226
+ commitLine();
227
+
228
+ const width = lines.reduce((m, l) => Math.max(m, l.width), 0);
229
+ const height = lines.reduce((s, l) => s + l.height, 0);
230
+ return { lines, width, height };
231
+ }
232
+
233
+ /** Character-by-character break of a single word longer than maxWidth. Returns
234
+ * the pieces (each <= maxWidth). Mirrors text-layout.ts splitLongWord but
235
+ * returns plain strings (geometry computed by the caller). */
236
+ function splitLongWord(word: string, maxWidth: number, mt: (s: string) => number): string[] {
237
+ const out: string[] = [];
238
+ let current = "";
239
+ for (const ch of word) {
240
+ const next = current + ch;
241
+ if (current && mt(next) > maxWidth) {
242
+ out.push(current);
243
+ current = ch;
244
+ } else {
245
+ current = next;
246
+ }
247
+ }
248
+ if (current) out.push(current);
249
+ return out;
250
+ }
251
+
252
+ function normalizeRunText(text: string, mode: WhiteSpaceMode): string {
253
+ const nl = text.replace(/\r\n?/g, "\n");
254
+ if (mode === "pre") return nl;
255
+ if (mode === "pre-line") return nl.replace(/[ \t\f\v]+/g, " ");
256
+ return nl.replace(/\s+/g, " ");
257
+ }
@@ -0,0 +1,14 @@
1
+ import { CSSProperty } from "./css-parser.js";
2
+
3
+ /** A resolver-side rich-text run: one piece of styled inline text within a
4
+ * text node's `runs[]`. The run's `style` holds the inherited-or-overridden
5
+ * keys; the parent text node's resolved style is the base, the inline
6
+ * element's own matched rules and the styling-tag defaults layer on top. */
7
+ export interface TextRun {
8
+ text: string;
9
+ style: Partial<CSSProperty>;
10
+ /** Resolved screen target if this run came from an <a href="#screenId">. */
11
+ href?: string;
12
+ /** <br> sentinel: forces a hard line break (no text rendered). */
13
+ hardBreak?: boolean;
14
+ }
@@ -0,0 +1,141 @@
1
+ // Slice of the C++ runtime header (original source lines 5299-5433).
2
+ // AA subsystem: begin/push/pixel/line/circle/fill.
3
+ // See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
4
+ export function emitAntialiasing(): string {
5
+ return `
6
+ ui_refresh_flush();
7
+ }
8
+
9
+ // ── Antialiasing subsystem (offscreen canvas + coverage blending) ──────────
10
+ // Enabled via #define UI_AA 1 (from display profile antialias:true).
11
+ // Shapes are rendered to a GFXcanvas16, edges blended via getPixel read-back,
12
+ // then pushed to the display. The ILI9341 has no efficient SPI read-back, so
13
+ // all blending happens in RAM.
14
+ #ifdef UI_AA
15
+
16
+ // Get (or allocate) a canvas sized to the element being drawn.
17
+ static inline CuttlefishCanvas16* ui_aa_begin(int16_t w, int16_t h, UI_COLOR_T bg) {
18
+ if (w <= 0 || h <= 0) return nullptr;
19
+ if (!__ui_aa_canvas || display_canvasWidth(__ui_aa_canvas) < w || display_canvasHeight(__ui_aa_canvas) < h) {
20
+ display_deleteCanvas(__ui_aa_canvas);
21
+ __ui_aa_canvas = display_createCanvas(w > 0 ? w : 1, h > 0 ? h : 1);
22
+ }
23
+ if (!__ui_aa_canvas || !display_canvasBuffer(__ui_aa_canvas)) return nullptr;
24
+ display_canvasFillScreen(__ui_aa_canvas, bg);
25
+ return __ui_aa_canvas;
26
+ }
27
+
28
+ // Push the canvas rect to the display at (dx, dy).
29
+ static inline void ui_aa_push(CuttlefishCanvas16* c, int16_t dx, int16_t dy) {
30
+ if (!c || !display_canvasBuffer(c)) return;
31
+ int16_t w = display_canvasWidth(c), h = display_canvasHeight(c);
32
+ // Push via __ui_gfx so the AA output goes to the scroll canvas when active,
33
+ // or the display directly when not. Row-by-row drawRGBBitmap (no transparent
34
+ // alpha — the AA canvas already has the blended pixels).
35
+ for (int16_t row = 0; row < h; row++) {
36
+ ui_display_draw_rgb_bitmap(dx, dy + row, display_canvasBuffer(c) + (int32_t)row * w, w, 1);
37
+ }
38
+ }
39
+
40
+ // Blend a pixel at integer coords with a coverage fraction (0-255).
41
+ static inline void ui_aa_pixel(CuttlefishCanvas16* c, int16_t x, int16_t y, UI_COLOR_T color, uint8_t cov) {
42
+ if (!c || !display_canvasBuffer(c)) return;
43
+ if (cov == 0) return;
44
+ if (x < 0 || y < 0 || x >= display_canvasWidth(c) || y >= display_canvasHeight(c)) return;
45
+ if (cov >= 255) { display_targetDrawPixel((CuttlefishDisplayTarget*)c, x, y, color); return; }
46
+ UI_COLOR_T bg = display_canvasGetPixel(c, x, y);
47
+ uint8_t op = (uint8_t)((uint16_t)cov * 100 / 255);
48
+ display_targetDrawPixel((CuttlefishDisplayTarget*)c, x, y, ui_blend(color, bg, op));
49
+ }
50
+
51
+ // Xiaolin Wu antialiased line. Coordinates are in canvas-local space.
52
+ static inline void ui_aa_line(CuttlefishCanvas16* c, float x0, float y0, float x1, float y1, UI_COLOR_T color) {
53
+ if (!c || !display_canvasBuffer(c)) return;
54
+ auto ipart = [](float f) { return (int16_t)f; };
55
+ auto round_f = [](float f) { return (int16_t)(f + 0.5f); };
56
+ auto fpart = [](float f) { return f - (float)(int16_t)f; };
57
+ auto rfpart = [&](float f) { return 1.0f - fpart(f); };
58
+
59
+ bool steep = fabs(y1 - y0) > fabs(x1 - x0);
60
+ if (steep) { float t = x0; x0 = y0; y0 = t; t = x1; x1 = y1; y1 = t; }
61
+ if (x0 > x1) { float t = x0; x0 = x1; x1 = t; t = y0; y0 = y1; y1 = t; }
62
+
63
+ float dx = x1 - x0, dy = y1 - y0;
64
+ float gradient = (dx == 0) ? 1.0f : dy / dx;
65
+
66
+ int16_t xpx1 = round_f(x0);
67
+ float xend = x0 + 0.5f * (xpx1 - x0) * (xpx1 - x0 < 0 ? -1 : 0); // simplified
68
+ float intery = y0 + gradient * (xpx1 - x0);
69
+
70
+ for (int16_t x = xpx1; x <= ipart(x1); x++) {
71
+ if (steep) {
72
+ ui_aa_pixel(c, ipart(intery), x, color, (uint8_t)(rfpart(intery) * 255));
73
+ ui_aa_pixel(c, ipart(intery) + 1, x, color, (uint8_t)(fpart(intery) * 255));
74
+ } else {
75
+ ui_aa_pixel(c, x, ipart(intery), color, (uint8_t)(rfpart(intery) * 255));
76
+ ui_aa_pixel(c, x, ipart(intery) + 1, color, (uint8_t)(fpart(intery) * 255));
77
+ }
78
+ intery += gradient;
79
+ }
80
+ }
81
+
82
+ // Antialiased circle outline. cx,cy,r are in canvas-local space.
83
+ static inline void ui_aa_circle(CuttlefishCanvas16* c, int16_t cx, int16_t cy, float r, UI_COLOR_T color) {
84
+ if (!c || !display_canvasBuffer(c)) return;
85
+ if (r <= 0) return;
86
+ // Walk each scanline from top to bottom of the bounding box.
87
+ int16_t y0 = (int16_t)floor(cy - r);
88
+ int16_t y1 = (int16_t)ceil(cy + r);
89
+ for (int16_t y = y0; y <= y1; y++) {
90
+ float dy = (float)y - cy;
91
+ float dx = r * r - dy * dy;
92
+ if (dx < 0) continue;
93
+ float halfW = sqrtf(dx);
94
+ float leftX = (float)cx - halfW;
95
+ float rightX = (float)cx + halfW;
96
+ // Left edge: blend two pixels at the coverage split.
97
+ int16_t lx = (int16_t)floor(leftX);
98
+ float lfrac = leftX - lx;
99
+ ui_aa_pixel(c, lx, y, color, (uint8_t)((1.0f - lfrac) * 255));
100
+ ui_aa_pixel(c, lx + 1, y, color, 0); // interior starts here (drawn solid below)
101
+ // Right edge.
102
+ int16_t rx = (int16_t)floor(rightX);
103
+ float rfrac = rightX - rx;
104
+ ui_aa_pixel(c, rx, y, color, (uint8_t)(rfrac * 255));
105
+ ui_aa_pixel(c, rx + 1, y, color, 0);
106
+ // Solid fill between edges (skip the edge pixels already blended).
107
+ for (int16_t x = lx + 1; x < rx; x++) {
108
+ if (x >= 0 && x < display_canvasWidth(c)) display_targetDrawPixel((CuttlefishDisplayTarget*)c, x, y, color);
109
+ }
110
+ }
111
+ }
112
+
113
+ // Antialiased filled circle.
114
+ static inline void ui_aa_fill_circle(CuttlefishCanvas16* c, int16_t cx, int16_t cy, float r, UI_COLOR_T color) {
115
+ if (!c || !display_canvasBuffer(c)) return;
116
+ if (r <= 0) return;
117
+ int16_t y0 = (int16_t)floor(cy - r);
118
+ int16_t y1 = (int16_t)ceil(cy + r);
119
+ for (int16_t y = y0; y <= y1; y++) {
120
+ float dy = (float)y - cy;
121
+ float dx = r * r - dy * dy;
122
+ if (dx < 0) continue;
123
+ float halfW = sqrtf(dx);
124
+ float leftX = (float)cx - halfW;
125
+ float rightX = (float)cx + halfW;
126
+ int16_t lx = (int16_t)floor(leftX);
127
+ int16_t rx = (int16_t)ceil(rightX);
128
+ // Blend left edge.
129
+ ui_aa_pixel(c, lx, y, color, (uint8_t)((1.0f - (leftX - lx)) * 255));
130
+ // Blend right edge.
131
+ ui_aa_pixel(c, rx, y, color, (uint8_t)((rightX - (rx - 1)) * 255));
132
+ // Solid interior.
133
+ for (int16_t x = lx + 1; x < rx; x++) {
134
+ if (x >= 0 && x < display_canvasWidth(c) && y >= 0 && y < display_canvasHeight(c)) display_targetDrawPixel((CuttlefishDisplayTarget*)c, x, y, color);
135
+ }
136
+ }
137
+ }
138
+
139
+ #endif // UI_AA
140
+ `;
141
+ }
@@ -0,0 +1,39 @@
1
+ // Slice of the C++ runtime header (original source lines 2962-2994).
2
+ // ui_blend565/ui_blend888 bodies. Tiny slice sitting between touch and text in source.
3
+ // See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
4
+ export function emitBlendBodies(): string {
5
+ return `
6
+ static inline uint16_t ui_blend565(uint16_t fg, uint16_t bg, uint8_t opacity) {
7
+ if (opacity >= 100) return fg;
8
+ if (opacity == 0) return bg;
9
+ // Blend in 888 internally for higher precision: unpack 565→888 (replicating
10
+ // high bits), blend at 8-bit, then re-quantize to 565. This produces smoother
11
+ // intermediate values for AA text edges, opacity, shadows, and gradients —
12
+ // the 565-channel blend (32 red levels) was too coarse and showed banding.
13
+ uint8_t fr = (fg >> 11) & 0x1F, fg5 = (fg >> 5) & 0x3F, fb = fg & 0x1F;
14
+ uint8_t br = (bg >> 11) & 0x1F, bg5 = (bg >> 5) & 0x3F, bb = bg & 0x1F;
15
+ // Unpack to 8-bit (5-bit → 8-bit: (v << 3) | (v >> 2)).
16
+ uint16_t fr8 = (fr << 3) | (fr >> 2), fg8 = (fg5 << 2) | (fg5 >> 4), fb8 = (fb << 3) | (fb >> 2);
17
+ uint16_t br8 = (br << 3) | (br >> 2), bg8 = (bg5 << 2) | (bg5 >> 4), bb8 = (bb << 3) | (bb >> 2);
18
+ uint16_t r = (uint16_t)((fr8 * opacity + br8 * (100 - opacity)) / 100);
19
+ uint16_t g = (uint16_t)((fg8 * opacity + bg8 * (100 - opacity)) / 100);
20
+ uint16_t b = (uint16_t)((fb8 * opacity + bb8 * (100 - opacity)) / 100);
21
+ return ((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3);
22
+ }
23
+
24
+ // Blend two RGB888 colors by opacity (0-100). Added for Phase 2 (RGB888/RGB666
25
+ // targets); unused in Phase 1, whose TFT path keeps 565 node values and blends
26
+ // via ui_blend565 above. Kept alongside so the 888 path is ready when the
27
+ // descriptor routes emit through resolveColor888.
28
+ static inline uint32_t ui_blend888(uint32_t fg, uint32_t bg, uint8_t opacity) {
29
+ if (opacity >= 100) return fg;
30
+ if (opacity == 0) return bg;
31
+ uint8_t fr = (fg >> 16) & 0xff, fg8 = (fg >> 8) & 0xff, fb = fg & 0xff;
32
+ uint8_t br = (bg >> 16) & 0xff, bg8 = (bg >> 8) & 0xff, bb = bg & 0xff;
33
+ uint8_t r = (fr * opacity + br * (100 - opacity)) / 100;
34
+ uint8_t g = (fg8 * opacity + bg8 * (100 - opacity)) / 100;
35
+ uint8_t b = (fb * opacity + bb * (100 - opacity)) / 100;
36
+ return ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
37
+ }
38
+ `;
39
+ }
@@ -0,0 +1,155 @@
1
+ // Slice of the C++ runtime header (original source lines 753-901).
2
+ // Persistent canvas release/create/get/shift/repair/warn helpers.
3
+ // See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
4
+ export function emitCanvasHelpers(): string {
5
+ return `
6
+ // heap. Called from ui_navigate. Safe with null pointers.
7
+ static inline void ui_release_canvas_state() {
8
+ display_deleteCanvas(__ui_container_canvas); __ui_container_canvas = nullptr;
9
+ display_deleteCanvas(__ui_list_canvas); __ui_list_canvas = nullptr;
10
+ __ui_list_canvas_node = -1;
11
+ display_deleteCanvas(__ui_node_canvas); __ui_node_canvas = nullptr;
12
+ display_deleteCanvas(__ui_repair_canvas); __ui_repair_canvas = nullptr;
13
+ }
14
+
15
+ // Lazily allocate/reuse a viewport-sized canvas for a scroll container. Resizes
16
+ // when the container's box changes; returns null if allocation fails (caller
17
+ // falls back to Mode C direct redraw). Only used on full render tiers.
18
+ // Prefers PSRAM when available (ESP32 + BOARD_HAS_PSRAM + psramFound) so large
19
+ // viewport canvases (e.g. 116KB+ for a full-width scroll region on a 480x320
20
+ // panel) don't exhaust internal SRAM. Falls back to internal SRAM otherwise.
21
+ static inline CuttlefishCanvas16* ui_create_canvas_best(int16_t w, int16_t h) {
22
+ #if defined(ESP32) && defined(BOARD_HAS_PSRAM)
23
+ if (psramFound()) {
24
+ CuttlefishCanvas16* c = display_createCanvasPsram(w, h);
25
+ if (c && display_canvasBuffer(c)) return c;
26
+ // PSRAM allocation failed (rare — fragmented PSRAM) → fall through to SRAM.
27
+ }
28
+ #endif
29
+ return display_createCanvas(w, h);
30
+ }
31
+
32
+ static inline CuttlefishCanvas16* ui_get_container_canvas(int16_t w, int16_t h) {
33
+ if (w <= 0 || h <= 0) return nullptr;
34
+ if (!__ui_container_canvas ||
35
+ display_canvasWidth(__ui_container_canvas) != w ||
36
+ display_canvasHeight(__ui_container_canvas) != h) {
37
+ display_deleteCanvas(__ui_container_canvas);
38
+ __ui_container_canvas = ui_create_canvas_best(w, h);
39
+ }
40
+ return (__ui_container_canvas && display_canvasBuffer(__ui_container_canvas))
41
+ ? __ui_container_canvas : nullptr;
42
+ }
43
+
44
+ // Defined after the node table by UI lowering (scroll overflow nodes with ids).
45
+ static inline const char* __ui_scroll_node_id(uint16_t idx);
46
+
47
+ // Emit a one-time Serial warning when scroll rendering cannot be accurate due
48
+ // to heap limits. reason: 0=canvas alloc failed, 1=viewport exceeds budget,
49
+ // 2=Mode C strip fallback (reduced accuracy).
50
+ static inline void ui_warn_scroll_memory(uint16_t nodeIdx, uint8_t reason) {
51
+ if (nodeIdx >= __ui_node_count) return;
52
+ if (!__ui_scroll_mem_warned) return;
53
+ if (__ui_scroll_mem_warned[nodeIdx]) return;
54
+ __ui_scroll_mem_warned[nodeIdx] = 1;
55
+
56
+ int16_t vw = __ui_nodes[nodeIdx].box.w;
57
+ int16_t vh = __ui_nodes[nodeIdx].box.h;
58
+ uint32_t need = (uint32_t)(vw > 0 ? vw : 0) * (uint32_t)(vh > 0 ? vh : 0) * 2u;
59
+ const char* id = __ui_scroll_node_id(nodeIdx);
60
+ const char* label = (id && id[0]) ? id : "scroll viewport";
61
+ const char* reasonText = "scroll canvas allocation failed";
62
+ if (reason == 1) reasonText = "scroll viewport exceeds compile-time canvas budget";
63
+ else if (reason == 2) reasonText = "using Mode C strip fallback (smooth scroll canvas unavailable)";
64
+
65
+ #if defined(ESP32)
66
+ uint32_t freeHeap = ESP.getFreeHeap();
67
+ uint32_t maxAlloc = ESP.getMaxAllocHeap();
68
+ Serial.printf(
69
+ "[cuttlefish] WARNING: #%s (%dx%d) needs %lu bytes for accurate scroll — %s. "
70
+ "heap free=%lu max_alloc=%lu budget=%d. "
71
+ "Shrink the scroll viewport in CSS, trim fonts/images, or use PSRAM.\\n",
72
+ label, vw, vh, (unsigned long)need, reasonText,
73
+ (unsigned long)freeHeap, (unsigned long)maxAlloc, UI_SCROLL_CANVAS_BUDGET_BYTES);
74
+ #elif defined(ESP8266)
75
+ uint32_t freeHeap = ESP.getFreeHeap();
76
+ Serial.printf(
77
+ "[cuttlefish] WARNING: #%s (%dx%d) needs %lu bytes for accurate scroll — %s. "
78
+ "heap free=%lu budget=%d. "
79
+ "Shrink the scroll viewport in CSS, trim fonts/images, or reduce UI footprint.\\n",
80
+ label, vw, vh, (unsigned long)need, reasonText,
81
+ (unsigned long)freeHeap, UI_SCROLL_CANVAS_BUDGET_BYTES);
82
+ #else
83
+ // Non-Arduino target (e.g. SDL native): no Serial, so use standard-C printf.
84
+ // The native framework forces <cstdio> so printf is available here.
85
+ printf(
86
+ "[cuttlefish] WARNING: #%s (%dx%d) needs %lu bytes for accurate scroll — %s. "
87
+ "budget=%d. Shrink the scroll viewport in CSS or trim UI assets.\\n",
88
+ label, vw, vh, (unsigned long)need, reasonText, UI_SCROLL_CANVAS_BUDGET_BYTES);
89
+ #endif
90
+ }
91
+
92
+ // Shift the canvas buffer vertically by deltaY (cheap memmove of existing
93
+ // pixels), then fill the exposed band with bg. Reports the exposed band via
94
+ // *exposedY/*exposedH so the caller can redraw only that strip (Mode B).
95
+ static inline void ui_shift_container_canvas(CuttlefishCanvas16* canvas, int16_t deltaY, UI_COLOR_T bg,
96
+ int16_t* exposedY, int16_t* exposedH) {
97
+ if (exposedY) *exposedY = 0;
98
+ if (exposedH) *exposedH = 0;
99
+ if (!canvas || !display_canvasBuffer(canvas)) return;
100
+ int16_t w = display_canvasWidth(canvas);
101
+ int16_t h = display_canvasHeight(canvas);
102
+ int16_t shift = deltaY < 0 ? -deltaY : deltaY;
103
+ if (shift <= 0 || shift >= h) {
104
+ display_canvasFillScreen(canvas, bg);
105
+ if (exposedY) *exposedY = 0;
106
+ if (exposedH) *exposedH = h;
107
+ return;
108
+ }
109
+ UI_COLOR_T* pixels = display_canvasBuffer(canvas);
110
+ int16_t stride = display_canvasWidth(canvas);
111
+ // Reserve the rightmost 4px gutter so the memmove never smears scrollbar
112
+ // pixels; the gutter is repainted separately by ui_draw_scrollbar.
113
+ int16_t contentW = w > 4 ? w - 4 : w;
114
+ // deltaY > 0: scrollY increased → finger moved up → content moves up.
115
+ // Cached rows shift toward LOWER indices; the exposed band is at the BOTTOM.
116
+ // deltaY < 0: content moves down → rows shift toward higher indices; exposed
117
+ // band at the TOP. (Matches the proven pre-rewrite direction.)
118
+ if (deltaY > 0) {
119
+ for (int16_t row = 0; row < h - shift; row++) {
120
+ memmove(pixels + (int32_t)row * stride,
121
+ pixels + (int32_t)(row + shift) * stride,
122
+ (size_t)contentW * sizeof(UI_COLOR_T));
123
+ }
124
+ if (exposedY) *exposedY = h - shift;
125
+ } else {
126
+ for (int16_t row = h - shift - 1; row >= 0; row--) {
127
+ memmove(pixels + (int32_t)(row + shift) * stride,
128
+ pixels + (int32_t)row * stride,
129
+ (size_t)contentW * sizeof(UI_COLOR_T));
130
+ }
131
+ if (exposedY) *exposedY = 0;
132
+ }
133
+ int16_t fillY = deltaY > 0 ? h - shift : 0;
134
+ display_canvasFillRect(canvas, 0, fillY, contentW, shift, bg);
135
+ if (w > contentW) {
136
+ display_canvasFillRect(canvas, contentW, 0, w - contentW, h, bg);
137
+ }
138
+ if (exposedH) *exposedH = shift;
139
+ }
140
+
141
+ // Repair canvas: parent-seeded background repaints + exposed-strip redraws.
142
+ // Grow-only between navigations so small scroll-delta changes do not allocate
143
+ // and free a new strip canvas during drag.
144
+ static inline CuttlefishCanvas16* ui_get_repair_canvas(int16_t w, int16_t h) {
145
+ if (w <= 0 || h <= 0) return nullptr;
146
+ if (!__ui_repair_canvas || !display_canvasBuffer(__ui_repair_canvas) ||
147
+ display_canvasWidth(__ui_repair_canvas) < w ||
148
+ display_canvasHeight(__ui_repair_canvas) < h) {
149
+ display_deleteCanvas(__ui_repair_canvas);
150
+ __ui_repair_canvas = ui_create_canvas_best(w, h);
151
+ }
152
+ return (__ui_repair_canvas && display_canvasBuffer(__ui_repair_canvas))
153
+ ? __ui_repair_canvas : nullptr;
154
+ }`;
155
+ }