@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,406 @@
1
+ // Slice of the C++ runtime header (original source lines 2562-2961).
2
+ // Touch hit-test/dispatch/state machine + keyboard forward-decls & state defines.
3
+ // See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
4
+ export function emitTouchKeyboardFwd() {
5
+ return `
6
+ // ── Touch hit-testing + click dispatch ─────────────────────────────────────
7
+ // Radio groups for mutual exclusion
8
+ struct UIRadioGroup {
9
+ uint16_t nodeIndices[8];
10
+ uint8_t count;
11
+ };
12
+ extern UIRadioGroup __ui_radio_groups[];
13
+ extern const uint16_t __ui_radio_group_count;
14
+
15
+ // Forward-declare the click handler type + tables (defined by the emit layer).
16
+ extern void (*__ui_click_handlers[])();
17
+ extern void (*__ui_hold_handlers[])();
18
+ extern void (*__ui_release_handlers[])();
19
+ extern void (*__ui_rangechange_handlers[])();
20
+ extern const uint16_t __ui_click_handler_count;
21
+ extern const uint16_t __ui_rangechange_handler_count;
22
+
23
+ // Touch state machine: tracks down → hold → up → click lifecycle
24
+ // Touch node/scroll node state is defined near navigation because ui_navigate resets it.
25
+ static uint32_t __ui_touch_down_time = 0; // millis() when touch started
26
+ static int16_t __ui_touch_down_y_pos = 0; // Y position when touch started (for tap vs drag detection)
27
+ static uint32_t __ui_last_touch_time = 0; // for debounce (updated on touch down only)
28
+ static uint32_t __ui_last_release_time = 0; // for release debounce
29
+ static int16_t __ui_drag_start_x = 0;
30
+ static int16_t __ui_drag_start_y = 0;
31
+ static uint8_t __ui_is_dragging = 0; // 1 once movement exceeds threshold
32
+ static int16_t __ui_range_node = -1; // range slider being dragged (int16: node index can exceed 127)
33
+
34
+ // ── Awaitable tap source (for \`await ui.onTap()\`) ─────────────────────────
35
+ // __ui_tap_seq increments on every completed tap (after click/release dispatch);
36
+ // async awaiters poll it for change. __ui_tap_node records the node hit by the
37
+ // last tap (-1 = empty space / non-interactive area) for per-element awaiters.
38
+ // volatile: written in the touch path, read from task .step() polls.
39
+ static volatile uint32_t __ui_tap_seq = 0;
40
+ static volatile int16_t __ui_tap_node = -1; // int16: node index can exceed 127
41
+ // Keyboard overlay state.
42
+ #define UI_KB_MAX 48 // max key cells (4 rows × 11 padded cols + margin)
43
+ #define UI_KB_HOLD_MS 600
44
+ #define UI_KB_REPEAT_MS 100
45
+ #define UI_KB_TEXT_H 24 // height reserved for the preview text row at the top
46
+ struct UIKey { char ch; uint8_t special; }; // special: 0=char,1=shift,2=bs,3=ok,4=page
47
+ struct UIKeyStyle { uint32_t bg, fg, borderColor; };
48
+ static UIRect __ui_kb_box;
49
+ static UIKey __ui_kb_keys[UI_KB_MAX];
50
+ static UIKeyStyle __ui_kb_styles[UI_KB_MAX];
51
+ static UI_COLOR_T __ui_kb_bg = 0x0000; // keyboard background (resolved from CSS)
52
+ static uint8_t __ui_kb_bs_held = 0;
53
+ static uint8_t __ui_kb_dirty = 0; // 0=clean, 1=full redraw, 2=text row + single key
54
+ // Forward-declared here (defined in the keyboard subsystem block below) so the
55
+ // UI_HIDE_OSK caret/blink paths in ui_tick and the input draw can reference it.
56
+ static int16_t __ui_kb_target;
57
+ #if defined(UI_HIDE_OSK)
58
+ // Caret blink phase for the desktop target (no OSK grid → no focus indicator).
59
+ // Incremented each tick; the input draw path blinks a caret on the active edit
60
+ // target every ~530ms (32 ticks ≈ 512ms at 60fps, even-power-of-2 mask).
61
+ static uint16_t __ui_kb_blink = 0;
62
+ #endif
63
+ static int8_t __ui_kb_pressed_key = -1; // key index under the current touch (-1=none)
64
+ static int8_t __ui_kb_repaint_key = -1; // key to repaint on a targeted (mode 2) redraw
65
+ static int16_t __ui_last_touch_x = 0;
66
+ static int16_t __ui_last_touch_y = 0;
67
+ // Keyboard function forward declarations (defined in the subsystem block below;
68
+ // needed because ui_touch_down/up/handle_touch reference them, AND to suppress
69
+ // Arduino's auto-prototyper which would inject prototypes before UIRect is defined).
70
+ static inline void ui_kb_insert(char c);
71
+ static inline void ui_kb_delete();
72
+ static inline void ui_kb_open(uint16_t nodeIdx, uint8_t inputPosition);
73
+ static inline void ui_kb_close();
74
+ static inline void ui_kb_tick(uint32_t now);
75
+ static inline void ui_kb_handle_touch(int16_t tx, int16_t ty);
76
+ static inline void ui_kb_handle_tap(int16_t tx, int16_t ty);
77
+ static inline void ui_kb_key_rect(uint8_t idx, UIRect* out);
78
+ static inline void ui_kb_draw();
79
+ static inline void ui_kb_draw_key(uint8_t i);
80
+ static inline void ui_kb_draw_text_row();
81
+ static inline void ui_kb_compute_box();
82
+ #define UI_TOUCH_DEBOUNCE_MS 50
83
+ #define UI_TOUCH_HOLD_MS 600
84
+ #define UI_DRAG_THRESHOLD 10
85
+
86
+ // Hit-test a touch point against all visible nodes (topmost first).
87
+ // Returns the node index of the topmost node that BOTH contains the point
88
+ // AND has a click handler registered. Returns -1 if none.
89
+ static int16_t ui_hit_test(int16_t tx, int16_t ty) {
90
+ int16_t best = -1;
91
+ for (uint16_t i = 0; i < __ui_node_count; i++) {
92
+ if (!ui_is_effectively_visible(i)) continue;
93
+ if (__ui_nodes[i].screenId != __ui_active_screen) continue;
94
+ int16_t drawX = ui_draw_x_for_node((uint16_t)i);
95
+ int16_t drawY = ui_draw_y_for_node((uint16_t)i);
96
+ if (tx >= drawX && tx < drawX + __ui_nodes[i].box.w &&
97
+ ty >= drawY && ty < drawY + __ui_nodes[i].box.h) {
98
+ // The node's box contains the tap. For nodes inside a scroll container,
99
+ // the tap point (not the whole box) must lie within the visible viewport:
100
+ // a tall wrapped paragraph can overflow below the fold yet still have a
101
+ // tappable link segment in its visible portion.
102
+ if (ui_is_point_clipped_by_scroll((uint16_t)i, tx, ty)) continue;
103
+ // Skip nodes without any click handler — they're containers, not targets.
104
+ // Exceptions: NODE_RANGE (horizontal drag), NODE_INPUT (opens keyboard),
105
+ // NODE_LIST (virtualized item tap), and NODE_BUTTON — the last so a button
106
+ // gets :pressed/transition visual feedback even with no JS onClick wired
107
+ // (a pure-CSS button like a demo "tap to transition" control).
108
+ if (__ui_nodes[i].kind == NODE_RANGE || __ui_nodes[i].kind == NODE_INPUT || __ui_nodes[i].kind == NODE_LIST || __ui_nodes[i].kind == NODE_BUTTON || __ui_nodes[i].kind == NODE_CHECK || __ui_nodes[i].kind == NODE_RADIO) {
109
+ if (best < 0 || ui_node_draws_before(best, i)) best = i;
110
+ continue;
111
+ }
112
+ if (i < __ui_click_handler_count &&
113
+ (__ui_click_handlers[i] || __ui_hold_handlers[i] || __ui_release_handlers[i])) {
114
+ if (best < 0 || ui_node_draws_before(best, i)) best = i;
115
+ }
116
+ }
117
+ }
118
+ return best;
119
+ }
120
+
121
+ // Dispatch a handler from the given table if registered for the node.
122
+ static void ui_dispatch(void (**table)(), uint16_t count, int16_t node) {
123
+ if (node >= 0 && (uint16_t)node < count && table[node]) {
124
+ table[node]();
125
+ }
126
+ }
127
+
128
+ static inline void ui_open_keyboard_for_input(uint16_t nodeIdx) {
129
+ if (__ui_nodes[nodeIdx].kind != NODE_INPUT) return;
130
+ #if defined(UI_HIDE_OSK)
131
+ // Desktop target: the OSK grid isn't shown, so inputs remain visible and
132
+ // tappable while another is being edited. Commit any open session before
133
+ // opening for the new target, so focus follows the tap instead of being
134
+ // locked to the first input. (ui_kb_open is a no-op if already closed.)
135
+ if (__ui_kb_visible) ui_kb_close();
136
+ #endif
137
+ if (__ui_kb_visible) return;
138
+ // Resolve the input's position in the loader dispatch table by scanning
139
+ // for the Nth NODE_INPUT. (The loader table is indexed by input order.)
140
+ // Counter is uint16_t: nodeIdx is uint16_t and inputs can live past node
141
+ // 255 (demo-ui's are at 263/265/309). A uint8_t counter wraps 255→0 and
142
+ // never reaches nodeIdx, deadlocking the device on input tap.
143
+ uint8_t inputPos = 0;
144
+ for (uint16_t j = 0; j < nodeIdx; j++) {
145
+ if (__ui_nodes[j].kind == NODE_INPUT) inputPos++;
146
+ }
147
+ ui_kb_open(nodeIdx, inputPos);
148
+ }
149
+
150
+ // Touch down: called when screen is first touched.
151
+ static void ui_touch_down(int16_t tx, int16_t ty) {
152
+ int16_t node = ui_hit_test(tx, ty);
153
+ __ui_touch_node = node;
154
+ __ui_touch_state = 1;
155
+ __ui_touch_down_time = millis();
156
+ __ui_touch_down_y_pos = ty;
157
+ __ui_drag_start_x = tx;
158
+ __ui_drag_start_y = ty;
159
+ __ui_is_dragging = 0;
160
+ __ui_scroll_node = -1;
161
+ __ui_range_node = -1;
162
+ // Unified scroll-scan: one pass over scrollable nodes (containers AND lists —
163
+ // lists are scrollable via the UA stylesheet) finds the owning container.
164
+ // One owner per gesture; the double-delta bug class (node in both a container
165
+ // and a list) is gone because there's no second mechanism.
166
+ #if UI_SCROLL_HAS_TOUCH
167
+ int16_t bestScroll = -1;
168
+ for (uint16_t i = 0; i < __ui_node_count; i++) {
169
+ if (!__ui_nodes[i].scrollable || !ui_is_effectively_visible(i)) continue;
170
+ if (__ui_nodes[i].screenId != __ui_active_screen) continue;
171
+ // Only scrollable if content overflows the viewport.
172
+ if (__ui_nodes[i].contentHeight <= __ui_nodes[i].box.h) continue;
173
+ int16_t drawX = ui_draw_x_for_node((uint16_t)i);
174
+ int16_t drawY = ui_draw_y_for_node((uint16_t)i);
175
+ if (tx >= drawX && tx < drawX + __ui_nodes[i].box.w &&
176
+ ty >= drawY && ty < drawY + __ui_nodes[i].box.h) {
177
+ if (bestScroll < 0 || ui_node_draws_before(bestScroll, i)) bestScroll = i;
178
+ }
179
+ }
180
+ __ui_scroll_node = bestScroll;
181
+ #else
182
+ (void)tx; (void)ty;
183
+ #endif
184
+ if (node >= 0) {
185
+ uint8_t handledTouchTarget = 0;
186
+ if (__ui_nodes[node].kind == NODE_BUTTON) {
187
+ ui_set_pressed((uint16_t)node, 1);
188
+ handledTouchTarget = 1;
189
+ }
190
+ // Track range nodes for horizontal drag
191
+ if (__ui_nodes[node].kind == NODE_RANGE) {
192
+ __ui_range_node = node;
193
+ // Range owns this gesture. Do not let vertical touch jitter also scroll
194
+ // the containing view, which would dirty and repaint the whole viewport.
195
+ __ui_scroll_node = -1;
196
+ // Immediately set value from touch position
197
+ int16_t rMin = __ui_nodes[node].rangeMin;
198
+ int16_t rMax = __ui_nodes[node].rangeMax;
199
+ int16_t range = rMax - rMin;
200
+ if (range <= 0) range = 100;
201
+ int16_t relX = tx - ui_draw_x_for_node((uint16_t)node) - 4;
202
+ int16_t usable = __ui_nodes[node].box.w - 8;
203
+ if (usable <= 0) usable = 1;
204
+ int16_t nextVal = rMin + ((int32_t)relX * range) / usable;
205
+ nextVal = constrain(nextVal, rMin, rMax);
206
+ if (nextVal != __ui_nodes[node].value) {
207
+ __ui_nodes[node].value = nextVal;
208
+ ui_mark_dirty(node);
209
+ }
210
+ handledTouchTarget = 1;
211
+ }
212
+ uint8_t touchStartsScrollableView =
213
+ (__ui_scroll_node >= 0 && node == __ui_scroll_node);
214
+ if (!handledTouchTarget && !touchStartsScrollableView) ui_mark_dirty(node);
215
+ }
216
+ }
217
+
218
+ // Touch up: called when touch is released. Determines click vs hold.
219
+ static void ui_touch_up() {
220
+ // Modal keyboard: route tap-up to the keyboard; swallow normal click logic.
221
+ // (Skipped on UI_HIDE_OSK desktop targets — touches pass through to the app,
222
+ // since the editing session is driven by the real keyboard, not the grid.)
223
+ #if !defined(UI_HIDE_OSK)
224
+ if (__ui_kb_visible) {
225
+ ui_kb_handle_tap(__ui_last_touch_x, __ui_last_touch_y);
226
+ __ui_kb_bs_held = 0;
227
+ __ui_touch_state = 0;
228
+ __ui_last_touch_time = millis();
229
+ return;
230
+ }
231
+ #endif
232
+ uint32_t elapsed = millis() - __ui_touch_down_time;
233
+ // Release the scroll owner: arm a bounded settle (bounce-back / edge-snap).
234
+ // No fling — motion ends with the finger (the settle animation is the only
235
+ // post-lift motion, terminating within UI_SCROLL_SETTLE_MS).
236
+ if (__ui_scroll_node >= 0) {
237
+ ui_scroll_release(__ui_scroll_node);
238
+ }
239
+ if (__ui_touch_node >= 0 && !__ui_is_dragging) {
240
+ int16_t clickedNode = __ui_touch_node;
241
+ if (elapsed < UI_TOUCH_HOLD_MS) {
242
+ if (__ui_nodes[clickedNode].kind == NODE_INPUT) {
243
+ ui_open_keyboard_for_input((uint16_t)clickedNode);
244
+ }
245
+ ui_dispatch(__ui_click_handlers, __ui_click_handler_count, __ui_touch_node);
246
+ // Rich-text inline link: if the tapped node has link runs, find which link
247
+ // segment the tap falls within and navigate to its target screen. Copies
248
+ // the list-item subdivision precedent with measured run rects.
249
+ if (__ui_nodes[clickedNode].runCount > 0) {
250
+ int16_t ndx = __ui_last_touch_x - ui_draw_x_for_node((uint16_t)clickedNode);
251
+ int16_t ndy = __ui_last_touch_y - ui_draw_y_for_node((uint16_t)clickedNode);
252
+ int8_t target = ui_rich_link_hit((uint16_t)clickedNode, ndx, ndy);
253
+ if (target >= 0) ui_navigate((uint8_t)target);
254
+ }
255
+ }
256
+ ui_dispatch(__ui_release_handlers, __ui_click_handler_count, __ui_touch_node);
257
+ ui_mark_dirty(clickedNode);
258
+ }
259
+ // Release the pressed button's :pressed state unconditionally — even when a
260
+ // drag/scroll hijacked the gesture (the click above is correctly gated on
261
+ // !__ui_is_dragging, but the visual press state should always reset on lift,
262
+ // otherwise a touch-down on a button followed by a scroll leaves it stuck).
263
+ if (__ui_touch_node >= 0 && __ui_touch_node < __ui_node_count &&
264
+ __ui_nodes[__ui_touch_node].kind == NODE_BUTTON && __ui_nodes[__ui_touch_node].value != 0) {
265
+ ui_set_pressed((uint16_t)__ui_touch_node, 0);
266
+ }
267
+ // Resume any \`await ui.onTap()\` awaiter. Runs for EVERY completed tap —
268
+ // including holds (released above) and taps on empty space (__ui_touch_node
269
+ // == -1), which is what makes "wake on any touch" work for display-sleep.
270
+ // Placed AFTER the click/release dispatch so onClick always fires first.
271
+ __ui_tap_seq++;
272
+ __ui_tap_node = __ui_touch_node;
273
+ // Virtualized list item tap: if the touch was inside a list, compute the item
274
+ // index from the touch Y. Use total movement (not drag flag) to distinguish
275
+ // tap from scroll: a tap moves < itemHeight/2 total; a scroll moves more.
276
+ if (__ui_scroll_node >= 0 && __ui_nodes[__ui_scroll_node].virtualized) {
277
+ int16_t n = __ui_scroll_node;
278
+ if (__ui_nodes[n].listTapFn) {
279
+ int16_t drawY = ui_draw_y_for_node(n);
280
+ int16_t relY = __ui_last_touch_y - drawY;
281
+ int16_t totalMove = abs(__ui_last_touch_y - __ui_touch_down_y_pos);
282
+ uint16_t ih = __ui_nodes[n].listItemHeight > 0 ? __ui_nodes[n].listItemHeight : 24;
283
+ if (totalMove < (int16_t)(ih / 2) &&
284
+ relY >= 0 && relY < __ui_nodes[n].box.h) {
285
+ uint16_t itemIdx = (uint16_t)((relY + __ui_nodes[n].scrollY) / ih);
286
+ if (itemIdx < __ui_nodes[n].listCount) {
287
+ __ui_nodes[n].listTapFn(itemIdx);
288
+ }
289
+ }
290
+ }
291
+ }
292
+ __ui_touch_state = 0;
293
+ __ui_touch_node = -1;
294
+ __ui_is_dragging = 0;
295
+ __ui_scroll_node = -1;
296
+ __ui_range_node = -1;
297
+ }
298
+
299
+ // Called each frame from ui_poll_touch when touch is detected.
300
+ // Implements debounce + the down/hold/up/click state machine.
301
+ static inline void ui_handle_touch(int16_t tx, int16_t ty) {
302
+ uint32_t now = millis();
303
+ // Track last touch coords for tap-up routing.
304
+ __ui_last_touch_x = tx;
305
+ __ui_last_touch_y = ty;
306
+
307
+ // Modal keyboard: if visible, route touch to the keyboard only.
308
+ // (Skipped on UI_HIDE_OSK desktop targets — touches pass through to the app,
309
+ // since the editing session is driven by the real keyboard, not the grid.)
310
+ #if !defined(UI_HIDE_OSK)
311
+ if (__ui_kb_visible) {
312
+ // Only process the down-edge for key actions (insert/delete-on-down).
313
+ // Repeat is handled by ui_kb_tick; release by ui_touch_up → ui_kb_handle_tap.
314
+ if (__ui_touch_state == 0) {
315
+ __ui_touch_state = 1;
316
+ __ui_touch_down_time = now;
317
+ if (tx >= __ui_kb_box.x && tx < __ui_kb_box.x + __ui_kb_box.w &&
318
+ ty >= __ui_kb_box.y && ty < __ui_kb_box.y + __ui_kb_box.h) {
319
+ ui_kb_handle_touch(tx, ty);
320
+ }
321
+ } else {
322
+ // Held: run auto-repeat (backspace).
323
+ ui_kb_tick(now);
324
+ }
325
+ __ui_last_touch_time = now;
326
+ return; // swallow all other touches while modal
327
+ }
328
+ #endif
329
+
330
+ if (__ui_touch_state == 0) {
331
+ // Idle: check debounce, then start touch
332
+ if (now - __ui_last_touch_time < UI_TOUCH_DEBOUNCE_MS) return;
333
+ ui_touch_down(tx, ty);
334
+ } else {
335
+ // Already touching: check for drag or hold
336
+ if (__ui_range_node < 0 && !__ui_is_dragging && __ui_scroll_node >= 0) {
337
+ // Check if movement exceeds drag threshold
338
+ int16_t dy = ty - __ui_drag_start_y;
339
+ if (abs(dy) >= UI_DRAG_THRESHOLD) {
340
+ __ui_is_dragging = 1;
341
+ }
342
+ }
343
+ // Range slider: update value from horizontal touch position
344
+ if (__ui_range_node >= 0) {
345
+ int16_t rMin = __ui_nodes[__ui_range_node].rangeMin;
346
+ int16_t rMax = __ui_nodes[__ui_range_node].rangeMax;
347
+ int16_t range = rMax - rMin;
348
+ if (range <= 0) range = 100;
349
+ int16_t relX = tx - ui_draw_x_for_node((uint16_t)__ui_range_node) - 4;
350
+ int16_t usable = __ui_nodes[__ui_range_node].box.w - 8;
351
+ if (usable <= 0) usable = 1;
352
+ int16_t newVal = rMin + ((int32_t)relX * range) / usable;
353
+ newVal = constrain(newVal, rMin, rMax);
354
+ if (newVal != __ui_nodes[__ui_range_node].value) {
355
+ __ui_nodes[__ui_range_node].value = newVal;
356
+ ui_mark_dirty(__ui_range_node);
357
+ // Fire the onChange callback (if any) — every value change during drag.
358
+ if (__ui_range_node < (int16_t)__ui_rangechange_handler_count &&
359
+ __ui_rangechange_handlers[__ui_range_node]) {
360
+ __ui_rangechange_handlers[__ui_range_node]();
361
+ }
362
+ }
363
+ }
364
+ // Unified scroll drag: one owning node, immediate-apply each frame (the
365
+ // list's proven model, now used for all scroll containers). No accumulator,
366
+ // no cadence gate, no pending buffer — the smoothed delta is applied via the
367
+ // physics layer (1:1 in-bounds, rubber-band at edges). Telemetry optional.
368
+ if (__ui_range_node < 0 && __ui_is_dragging && __ui_scroll_node >= 0) {
369
+ int16_t rawDy = ty - __ui_drag_start_y;
370
+ if (rawDy != 0) {
371
+ int16_t dy = ui_scroll_smooth_dy(rawDy);
372
+ if (dy != 0) {
373
+ ui_apply_scroll_delta(__ui_scroll_node, dy);
374
+ __ui_drag_start_y = ty;
375
+ #if UI_SCROLL_DEBUG
376
+ Serial.printf("scroll dy=%d sy=%d ov=%d virt=%d\\n",
377
+ dy, __ui_nodes[__ui_scroll_node].scrollY,
378
+ __ui_nodes[__ui_scroll_node].overscrollPx,
379
+ (int)__ui_nodes[__ui_scroll_node].virtualized);
380
+ #endif
381
+ }
382
+ }
383
+ }
384
+ if (__ui_touch_state == 1 && __ui_touch_node >= 0 && !__ui_is_dragging) {
385
+ if (now - __ui_touch_down_time >= UI_TOUCH_HOLD_MS) {
386
+ __ui_touch_state = 2;
387
+ ui_dispatch(__ui_hold_handlers, __ui_click_handler_count, __ui_touch_node);
388
+ }
389
+ }
390
+ }
391
+ __ui_last_touch_time = now;
392
+ }
393
+
394
+ // Called each frame when no touch is detected.
395
+ static inline void ui_handle_no_touch() {
396
+ if (__ui_touch_state != 0) {
397
+ // Debounce: require a gap since the last release before processing another.
398
+ // This prevents crash from rapid touch/no-touch flicker on resistive screens.
399
+ if (millis() - __ui_last_release_time < UI_TOUCH_DEBOUNCE_MS) return;
400
+ ui_touch_up();
401
+ __ui_last_release_time = millis();
402
+ }
403
+ }
404
+
405
+ // Blend two RGB565 colors by opacity (0-100). Returns fg faded toward bg.`;
406
+ }
@@ -0,0 +1 @@
1
+ export declare function emitTypesDefines(): string;
@@ -0,0 +1,79 @@
1
+ // Slice of the C++ runtime header (original source lines 17-89).
2
+ // Guard open, includes, and #define knobs.
3
+ // See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
4
+ export function emitTypesDefines() {
5
+ return `
6
+ // ── Cuttlefish UI runtime (emit once per TU) ──────────────────────────────
7
+ #ifndef __TC_UI_RUNTIME
8
+ #define __TC_UI_RUNTIME
9
+ #include <stdint.h>
10
+ #include <string.h>
11
+ #define UI_TEXT_BUF 32 // max stored UI text chars, excluding the trailing NUL
12
+ #define UI_TEXT_LINE_BUF 96
13
+ #define UI_WS_NORMAL 0
14
+ #define UI_WS_NOWRAP 1
15
+ #define UI_WS_PRE 2
16
+ #define UI_WS_PRE_LINE 3
17
+ #ifndef UI_MAX_BUFFERED_PAINT_PIXELS
18
+ #define UI_MAX_BUFFERED_PAINT_PIXELS 20000
19
+ #endif
20
+ #ifndef UI_USE_FULL_FRAMEBUFFER
21
+ #define UI_USE_FULL_FRAMEBUFFER 0
22
+ #endif
23
+ #ifndef UI_TRANSITION_SNAP_MS
24
+ #define UI_TRANSITION_SNAP_MS 100
25
+ #endif
26
+ // Scroll physics + capability defaults. The per-TU #define overrides emitted
27
+ // by the UI emitter (from resolveScrollConfig) hit BEFORE this header, so these
28
+ // #ifndef guards adopt the configured values. Spec: 2026-06-28-scroll-engine-rewrite-design.md
29
+ #ifndef UI_SCROLL_MAX_OVERSCROLL
30
+ #define UI_SCROLL_MAX_OVERSCROLL 40
31
+ #endif
32
+ #ifndef UI_SCROLL_STIFFNESS_X10
33
+ #define UI_SCROLL_STIFFNESS_X10 5
34
+ #endif
35
+ #ifndef UI_SCROLL_EDGE_SNAP_PX
36
+ #define UI_SCROLL_EDGE_SNAP_PX 12
37
+ #endif
38
+ #ifndef UI_SCROLL_DRAG_SCALE_X10
39
+ #define UI_SCROLL_DRAG_SCALE_X10 10
40
+ #endif
41
+ #ifndef UI_SCROLL_SETTLE_MS
42
+ #define UI_SCROLL_SETTLE_MS 180
43
+ #endif
44
+ #ifndef UI_SCROLL_DEADBAND_PX
45
+ #define UI_SCROLL_DEADBAND_PX 2
46
+ #endif
47
+ // Capability tier flags (emitted per-TU before this header; defaults = full).
48
+ #ifndef UI_SCROLL_INPUT_TIER_CAPACITIVE
49
+ #define UI_SCROLL_INPUT_TIER_CAPACITIVE 0
50
+ #endif
51
+ #ifndef UI_SCROLL_INPUT_TIER_RESISTIVE
52
+ #define UI_SCROLL_INPUT_TIER_RESISTIVE 0
53
+ #endif
54
+ #ifndef UI_SCROLL_INPUT_TIER_NONE
55
+ #define UI_SCROLL_INPUT_TIER_NONE 0
56
+ #endif
57
+ #ifndef UI_SCROLL_RENDER_TIER_FULL
58
+ #define UI_SCROLL_RENDER_TIER_FULL 1
59
+ #endif
60
+ #ifndef UI_SCROLL_RENDER_TIER_CONSTRAINED
61
+ #define UI_SCROLL_RENDER_TIER_CONSTRAINED 0
62
+ #endif
63
+ #if (UI_SCROLL_INPUT_TIER_CAPACITIVE + UI_SCROLL_INPUT_TIER_RESISTIVE + UI_SCROLL_INPUT_TIER_NONE) == 0
64
+ #define UI_SCROLL_INPUT_TIER_RESISTIVE 1
65
+ #endif
66
+ #define UI_SCROLL_HAS_TOUCH (UI_SCROLL_INPUT_TIER_CAPACITIVE || UI_SCROLL_INPUT_TIER_RESISTIVE)
67
+ #define UI_SCROLL_ELASTIC (UI_SCROLL_RENDER_TIER_FULL)
68
+ // Telemetry: emits per-frame scrollY/overscrollPx/dy over Serial when defined.
69
+ #ifndef UI_SCROLL_DEBUG
70
+ #define UI_SCROLL_DEBUG 0
71
+ #endif
72
+ #ifndef UI_SCROLL_CANVAS_BUDGET_BYTES
73
+ #define UI_SCROLL_CANVAS_BUDGET_BYTES 88000
74
+ #endif
75
+ #if defined(ESP32) || defined(ESP8266)
76
+ #include <Esp.h>
77
+ #endif
78
+ `;
79
+ }
@@ -0,0 +1 @@
1
+ export declare function emitRuntimeHeader(): string;
@@ -0,0 +1,69 @@
1
+ // ---------------------------------------------------------------------------
2
+ // C++ reactive runtime header — the driver code that walks the node/binding/
3
+ // transition tables each frame.
4
+ //
5
+ // This is emitted once per translation unit (guarded) so the static tables
6
+ // produced by the lowering transformer have something to drive them. It
7
+ // implements the three-phase frame from spec §7:
8
+ // 1. Advance transitions (lerp toward target)
9
+ // 2. Draw traversal (dirty nodes only)
10
+ // 3. Flush dirty rects
11
+ //
12
+ // Plus the press/release entry points that node.onPress(pin) lowers to.
13
+ //
14
+ // The C++ source is split into contiguous slice modules under
15
+ // ./runtime-header/, concatenated here in source order. Each slice is a
16
+ // verbatim range of the original template literal; the assembled output is
17
+ // byte-identical to the pre-split single-file version (guarded by
18
+ // tests/packages/cuttlefish/runtime-header-byte-identity.test.ts).
19
+ // ---------------------------------------------------------------------------
20
+ import { emitTypesDefines } from "./runtime-header/types-defines.js";
21
+ import { emitStructs } from "./runtime-header/structs.js";
22
+ import { emitColorMonoRefresh } from "./runtime-header/color-mono-refresh.js";
23
+ import { emitStateBindingsNav } from "./runtime-header/state-bindings-nav.js";
24
+ import { emitForwardDecls } from "./runtime-header/forward-decls.js";
25
+ import { emitCanvasHelpers } from "./runtime-header/canvas-helpers.js";
26
+ import { emitDisplayShim } from "./runtime-header/display-shim.js";
27
+ import { emitScrollPhysics } from "./runtime-header/scroll-physics.js";
28
+ import { emitImageDrawing } from "./runtime-header/image-drawing.js";
29
+ import { emitCanvasScrollbar } from "./runtime-header/canvas-scrollbar.js";
30
+ import { emitDirtyScrollMutators } from "./runtime-header/dirty-scroll-mutators.js";
31
+ import { emitPaintOrderCoords } from "./runtime-header/paint-order-coords.js";
32
+ import { emitPaintRectsRepair } from "./runtime-header/paint-rects-repair.js";
33
+ import { emitInitPressInput } from "./runtime-header/init-press-input.js";
34
+ import { emitTouchKeyboardFwd } from "./runtime-header/touch-keyboard-fwd.js";
35
+ import { emitBlendBodies } from "./runtime-header/blend-bodies.js";
36
+ import { emitTextRendering } from "./runtime-header/text-rendering.js";
37
+ import { emitNodeDecoration } from "./runtime-header/node-decoration.js";
38
+ import { emitTickMotionHelpers } from "./runtime-header/tick-motion-helpers.js";
39
+ import { emitTick } from "./runtime-header/tick.js";
40
+ import { emitAntialiasing } from "./runtime-header/antialiasing.js";
41
+ import { emitKeyboard } from "./runtime-header/keyboard.js";
42
+ import { emitGuardClose } from "./runtime-header/guard-close.js";
43
+ export function emitRuntimeHeader() {
44
+ return [
45
+ emitTypesDefines(),
46
+ emitStructs(),
47
+ emitColorMonoRefresh(),
48
+ emitStateBindingsNav(),
49
+ emitForwardDecls(),
50
+ emitCanvasHelpers(),
51
+ emitDisplayShim(),
52
+ emitScrollPhysics(),
53
+ emitImageDrawing(),
54
+ emitCanvasScrollbar(),
55
+ emitDirtyScrollMutators(),
56
+ emitPaintOrderCoords(),
57
+ emitPaintRectsRepair(),
58
+ emitInitPressInput(),
59
+ emitTouchKeyboardFwd(),
60
+ emitBlendBodies(),
61
+ emitTextRendering(),
62
+ emitNodeDecoration(),
63
+ emitTickMotionHelpers(),
64
+ emitTick(),
65
+ emitAntialiasing(),
66
+ emitKeyboard(),
67
+ emitGuardClose(),
68
+ ].join("");
69
+ }
@@ -0,0 +1,16 @@
1
+ import type { Diagnostic } from "@typecad/cuttlefish/api/shared";
2
+ import type { UINodeModel } from "./model.js";
3
+ import { DEFAULT_SCROLL_CANVAS_BUDGET_BYTES } from "@typecad/cuttlefish/api/shared";
4
+ export { DEFAULT_SCROLL_CANVAS_BUDGET_BYTES };
5
+ export interface ScrollMemoryIssue {
6
+ nodeIndex: number;
7
+ id?: string;
8
+ width: number;
9
+ height: number;
10
+ canvasBytes: number;
11
+ budgetBytes: number;
12
+ }
13
+ /** Collect overflow scroll nodes whose viewport canvas exceeds the budget. */
14
+ export declare function collectScrollMemoryIssues(nodes: UINodeModel[], budgetBytes?: number): ScrollMemoryIssue[];
15
+ /** Emit transpile warnings for scroll viewports that cannot fit a Mode B canvas. */
16
+ export declare function analyzeScrollMemory(nodes: UINodeModel[], budgetBytes?: number): Diagnostic[];
@@ -0,0 +1,58 @@
1
+ // Compile-time scroll viewport memory analysis. Overflow scroll containers
2
+ // need a viewport-sized RGB565 canvas (~w×h×2 bytes) for smooth Mode B
3
+ // scrolling; when that exceeds the device budget, runtime falls back to
4
+ // Mode C strip scroll or freezes until memory is available.
5
+ import { DEFAULT_SCROLL_CANVAS_BUDGET_BYTES } from "@typecad/cuttlefish/api/shared";
6
+ export { DEFAULT_SCROLL_CANVAS_BUDGET_BYTES };
7
+ /** Collect overflow scroll nodes whose viewport canvas exceeds the budget. */
8
+ export function collectScrollMemoryIssues(nodes, budgetBytes = DEFAULT_SCROLL_CANVAS_BUDGET_BYTES) {
9
+ const issues = [];
10
+ for (const node of nodes) {
11
+ if (!node.scrollable)
12
+ continue;
13
+ if (node.contentHeight <= node.box.h)
14
+ continue;
15
+ const canvasBytes = node.box.w * node.box.h * 2;
16
+ if (canvasBytes <= budgetBytes)
17
+ continue;
18
+ issues.push({
19
+ nodeIndex: node.index,
20
+ id: node.id,
21
+ width: node.box.w,
22
+ height: node.box.h,
23
+ canvasBytes,
24
+ budgetBytes,
25
+ });
26
+ }
27
+ return issues;
28
+ }
29
+ function formatKb(bytes) {
30
+ return `${Math.round(bytes / 1024)} KB`;
31
+ }
32
+ function minimizationHint(issue) {
33
+ const maxArea = Math.floor(issue.budgetBytes / 2);
34
+ const maxHAtWidth = issue.width > 0 ? Math.floor(maxArea / issue.width) : 0;
35
+ const maxWAtHeight = issue.height > 0 ? Math.floor(maxArea / issue.height) : 0;
36
+ return [
37
+ `Reduce the scroll viewport in CSS (e.g. height ≤ ${maxHAtWidth}px at ${issue.width}px width, or width ≤ ${maxWAtHeight}px at ${issue.height}px height).`,
38
+ "Trim fonts, images, or inactive screens to free heap for canvas allocation.",
39
+ "Use PSRAM or a board with more SRAM if you need a larger scroll viewport.",
40
+ "Adjust display.scroll.scrollCanvasBudgetBytes in cuttlefish.config.ts only if your target has more headroom (runtime allocation may still fail).",
41
+ ].join("\n");
42
+ }
43
+ /** Emit transpile warnings for scroll viewports that cannot fit a Mode B canvas. */
44
+ export function analyzeScrollMemory(nodes, budgetBytes = DEFAULT_SCROLL_CANVAS_BUDGET_BYTES) {
45
+ return collectScrollMemoryIssues(nodes, budgetBytes).map((issue) => {
46
+ const label = issue.id ? `#${issue.id}` : `node ${issue.nodeIndex}`;
47
+ return {
48
+ severity: "warning",
49
+ code: "scroll-canvas-memory",
50
+ message: `Scroll viewport ${label} (${issue.width}×${issue.height}px) needs ${issue.canvasBytes} bytes ` +
51
+ `(${formatKb(issue.canvasBytes)}) for accurate smooth scrolling, exceeding the ` +
52
+ `${issue.budgetBytes}-byte budget (~${formatKb(issue.budgetBytes)}). ` +
53
+ "Rendering will fall back to Mode C strip scroll or freeze updates until memory is available — " +
54
+ "scrolling may tear, stutter, or skip frames.",
55
+ hint: minimizationHint(issue),
56
+ };
57
+ });
58
+ }
@@ -0,0 +1,4 @@
1
+ import { LayoutEngine } from "./layout-engine.js";
2
+ import { StyledNode } from "./style-resolver.js";
3
+ /** Select the layout engine: Yoga for flex, BlockLayoutEngine as fallback. */
4
+ export declare function selectEngine(root: StyledNode): LayoutEngine;
@@ -0,0 +1,19 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Engine selection — picks Yoga (flexbox) or BlockLayout based on whether any
3
+ // node in the styled tree declares display: flex.
4
+ // ---------------------------------------------------------------------------
5
+ import { isDisplayNone } from "./layout-engine.js";
6
+ import { BlockLayoutEngine } from "./block-layout.js";
7
+ import { YogaLayoutEngine } from "./yoga-layout.js";
8
+ /** Check if any node in the tree uses display: flex. */
9
+ function usesFlex(node) {
10
+ if (isDisplayNone(node))
11
+ return false;
12
+ if (node.style.display === "flex")
13
+ return true;
14
+ return node.children.some(usesFlex);
15
+ }
16
+ /** Select the layout engine: Yoga for flex, BlockLayoutEngine as fallback. */
17
+ export function selectEngine(root) {
18
+ return usesFlex(root) ? new YogaLayoutEngine() : new BlockLayoutEngine();
19
+ }