@typecad/ui 1.0.0-alpha.7 → 1.0.0-alpha.9

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 (84) hide show
  1. package/README.md +6 -0
  2. package/dist/engine-index.js +0 -2
  3. package/dist/preview/host-ui-runtime.d.ts +1 -0
  4. package/dist/preview/host-ui-runtime.js +56 -11
  5. package/dist/ui-engine/font-assets.d.ts +8 -0
  6. package/dist/ui-engine/font-assets.js +13 -0
  7. package/dist/ui-engine/layout-engine.js +19 -6
  8. package/dist/ui-engine/model.d.ts +1 -1
  9. package/dist/ui-engine/model.js +10 -4
  10. package/dist/ui-engine/runtime-header/antialiasing.js +52 -32
  11. package/dist/ui-engine/runtime-header/blend-bodies.js +4 -4
  12. package/dist/ui-engine/runtime-header/canvas-helpers.js +32 -65
  13. package/dist/ui-engine/runtime-header/canvas-scrollbar.js +22 -10
  14. package/dist/ui-engine/runtime-header/color-mono-refresh.js +10 -10
  15. package/dist/ui-engine/runtime-header/cuttlefish-gfx.js +59 -29
  16. package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +26 -10
  17. package/dist/ui-engine/runtime-header/display-shim.js +8 -8
  18. package/dist/ui-engine/runtime-header/forward-decls.js +48 -6
  19. package/dist/ui-engine/runtime-header/image-drawing.js +154 -84
  20. package/dist/ui-engine/runtime-header/init-press-input.js +20 -5
  21. package/dist/ui-engine/runtime-header/keyboard.js +19 -16
  22. package/dist/ui-engine/runtime-header/node-decoration.js +8 -8
  23. package/dist/ui-engine/runtime-header/node-draw-body.d.ts +1 -0
  24. package/dist/ui-engine/runtime-header/node-draw-body.js +1402 -0
  25. package/dist/ui-engine/runtime-header/paint-order-coords.js +48 -18
  26. package/dist/ui-engine/runtime-header/paint-rects-repair.js +38 -17
  27. package/dist/ui-engine/runtime-header/scroll-physics.js +15 -14
  28. package/dist/ui-engine/runtime-header/state-bindings-nav.js +23 -13
  29. package/dist/ui-engine/runtime-header/structs.js +12 -10
  30. package/dist/ui-engine/runtime-header/text-rendering.js +132 -71
  31. package/dist/ui-engine/runtime-header/tick/bindings-phase.js +29 -10
  32. package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +299 -747
  33. package/dist/ui-engine/runtime-header/tick/flush-phase.js +7 -3
  34. package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +19 -3
  35. package/dist/ui-engine/runtime-header/tick/transitions-phase.js +31 -24
  36. package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +42 -40
  37. package/dist/ui-engine/runtime-header/types-defines.js +23 -3
  38. package/dist/ui-engine/runtime-header.js +2 -0
  39. package/dist/ui-engine/transpile-ui.js +2 -2
  40. package/dist/ui-engine/ua-stylesheet.js +46 -3
  41. package/dist/ui-engine/ui-lowering.d.ts +1 -3
  42. package/dist/ui-engine/ui-lowering.js +4 -23
  43. package/dist/ui-engine/ui-registry.js +1 -7
  44. package/package.json +2 -2
  45. package/src/engine-index.ts +0 -2
  46. package/src/preview/host-ui-runtime.ts +56 -12
  47. package/src/ui-engine/font-assets.ts +13 -0
  48. package/src/ui-engine/layout-engine.ts +19 -6
  49. package/src/ui-engine/model.ts +10 -5
  50. package/src/ui-engine/runtime-header/antialiasing.ts +52 -32
  51. package/src/ui-engine/runtime-header/blend-bodies.ts +4 -4
  52. package/src/ui-engine/runtime-header/canvas-helpers.ts +32 -65
  53. package/src/ui-engine/runtime-header/canvas-scrollbar.ts +22 -10
  54. package/src/ui-engine/runtime-header/color-mono-refresh.ts +10 -10
  55. package/src/ui-engine/runtime-header/cuttlefish-gfx.ts +59 -29
  56. package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +26 -10
  57. package/src/ui-engine/runtime-header/display-shim.ts +8 -8
  58. package/src/ui-engine/runtime-header/forward-decls.ts +48 -6
  59. package/src/ui-engine/runtime-header/image-drawing.ts +154 -84
  60. package/src/ui-engine/runtime-header/init-press-input.ts +20 -5
  61. package/src/ui-engine/runtime-header/keyboard.ts +19 -16
  62. package/src/ui-engine/runtime-header/node-decoration.ts +8 -8
  63. package/src/ui-engine/runtime-header/node-draw-body.ts +1402 -0
  64. package/src/ui-engine/runtime-header/paint-order-coords.ts +48 -18
  65. package/src/ui-engine/runtime-header/paint-rects-repair.ts +38 -17
  66. package/src/ui-engine/runtime-header/scroll-physics.ts +15 -14
  67. package/src/ui-engine/runtime-header/state-bindings-nav.ts +23 -13
  68. package/src/ui-engine/runtime-header/structs.ts +12 -10
  69. package/src/ui-engine/runtime-header/text-rendering.ts +132 -71
  70. package/src/ui-engine/runtime-header/tick/bindings-phase.ts +29 -10
  71. package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +299 -747
  72. package/src/ui-engine/runtime-header/tick/flush-phase.ts +7 -3
  73. package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +19 -3
  74. package/src/ui-engine/runtime-header/tick/transitions-phase.ts +31 -24
  75. package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +42 -40
  76. package/src/ui-engine/runtime-header/types-defines.ts +23 -3
  77. package/src/ui-engine/runtime-header.ts +2 -0
  78. package/src/ui-engine/transpile-ui.ts +2 -2
  79. package/src/ui-engine/ua-stylesheet.ts +46 -3
  80. package/src/ui-engine/ui-lowering.ts +3 -25
  81. package/src/ui-engine/ui-registry.ts +1 -8
  82. package/dist/ui-engine/scroll-memory-diagnostics.d.ts +0 -16
  83. package/dist/ui-engine/scroll-memory-diagnostics.js +0 -58
  84. package/src/ui-engine/scroll-memory-diagnostics.ts +0 -79
@@ -5,9 +5,13 @@ export function emitTickFlushPhase() {
5
5
  return `
6
6
  }
7
7
  // ── Framebuffer bulk push ────────────────────────────────────────────────
8
- // When a framebuffer was used this frame, flush it to the display in a single
9
- // SPI transaction and restore the direct-draw target. No-op without one.
10
- if (__ui_fb) {
8
+ // When a framebuffer was used this frame, publish only its changed bounds.
9
+ // Local touch updates therefore avoid a full-screen SPI burst (which appears
10
+ // as a brightness dip on panels without a synchronized frame latch).
11
+ // Repairs performed before the dirty-node pass (for example transform
12
+ // geometry cleanup) may update the retained buffer without leaving a node
13
+ // dirty. The dirty-bounds accumulator is the authoritative publish signal.
14
+ if (__ui_fb && (__ui_fb_frame_dirty || __ui_fb_dirty)) {
11
15
  ui_push_framebuffer();
12
16
  }
13
17
  ui_display_use_default_target();
@@ -9,7 +9,23 @@ export function emitTickScrollCanvasPhase() {
9
9
  ui_push_buffered_scroll_canvas(bufferedScrollCanvas, bufferedScrollRepaintCanvas,
10
10
  bufferedScrollNode, bufferedScrollVX, bufferedScrollVY,
11
11
  bufferedScrollRepaintY, bufferedScrollRepaintH, __ui_draw_target);
12
- } else if (bufferedScrollNode >= 0 && bufferedScrollDirectStrip) {
13
- ui_draw_scrollbar_direct(bufferedScrollNode, bufferedScrollVX, bufferedScrollVY);
14
- __ui_nodes[bufferedScrollNode].lastPaintedScrollY = __ui_nodes[bufferedScrollNode].scrollY;`;
12
+ } else if (bufferedScrollNode >= 0 && bufferedScrollBands) {
13
+ // Band render fallback: the main loop didn't trigger it at the owner's
14
+ // z-slot (e.g. the owner was filtered out after dispatch). Render now so
15
+ // the subtree is never left unpainted. ui_render_scroll_bands sets
16
+ // lastPaintedScrollY itself on success; only the failure fallback (band
17
+ // canvas wouldn't allocate) needs to record the scrollY here so the next
18
+ // frame's delta is correct.
19
+ if (!ui_render_scroll_bands(static_cast<uint16_t>(bufferedScrollNode))) {
20
+ // No band canvas means there is no coherent frame to submit. Retain the
21
+ // previous viewport rather than drawing the scrollbar/direct subtree to
22
+ // the live SPI target, which would reintroduce tearing.
23
+ if (__ui_scroll_render_locked) __ui_scroll_render_locked[static_cast<uint16_t>(bufferedScrollNode)] = 1;
24
+ __ui_nodes[bufferedScrollNode].dirty = 0;
25
+ }
26
+ } else if (bufferedScrollNode >= 0 && bufferedScrollDirectFull) {
27
+ // The direct-full path is intentionally disabled for tear-sensitive scroll
28
+ // rendering. Keep the retained pixels when no compositor is available.
29
+ if (__ui_scroll_render_locked) __ui_scroll_render_locked[static_cast<uint16_t>(bufferedScrollNode)] = 1;
30
+ __ui_nodes[bufferedScrollNode].dirty = 0;`;
15
31
  }
@@ -10,9 +10,10 @@ export function emitTickTransitionsPhase() {
10
10
  __ui_trans[i].elapsed += deltaMs;
11
11
  uint16_t k = __ui_trans[i].durationMs == 0
12
12
  ? 100
13
- : (uint16_t)((uint32_t)__ui_trans[i].elapsed * 100 / __ui_trans[i].durationMs);
13
+ : static_cast<uint16_t>(static_cast<uint32_t>(__ui_trans[i].elapsed > __ui_trans[i].durationMs
14
+ ? __ui_trans[i].durationMs : __ui_trans[i].elapsed) * 100 / __ui_trans[i].durationMs);
14
15
  if (__ui_trans[i].durationMs > 0 && __ui_trans[i].durationMs <= UI_TRANSITION_SNAP_MS) k = 100;
15
- uint32_t v = UI_LERP_COLOR(__ui_trans[i].prevValue, __ui_trans[i].targetValue, (uint8_t)k);
16
+ uint32_t v = UI_LERP_COLOR(__ui_trans[i].prevValue, __ui_trans[i].targetValue, static_cast<uint8_t>(k));
16
17
  if (__ui_trans[i].prop == PROP_FG) {
17
18
  __ui_nodes[__ui_trans[i].node].fg = v;
18
19
  } else {
@@ -44,14 +45,20 @@ export function emitTickTransitionsPhase() {
44
45
  ui_keyframe_set_has_scroll_sensitive_geometry(__ui_anims[i].keyframeSet)) {
45
46
  continue;
46
47
  }
47
- __ui_anims[i].elapsed += (uint32_t)deltaMs;
48
+ __ui_anims[i].elapsed += static_cast<uint32_t>(deltaMs);
48
49
  uint32_t elapsedNoDelay = __ui_anims[i].elapsed;
49
50
  if (elapsedNoDelay < __ui_anims[i].delayMs) continue;
50
51
  elapsedNoDelay -= __ui_anims[i].delayMs;
51
52
  // Check iteration limit (finite).
52
53
  uint8_t completing = 0;
53
54
  if (__ui_anims[i].iterations > 0) {
54
- uint32_t totalDuration = (uint32_t)__ui_anims[i].iterations * (uint32_t)__ui_anims[i].durationMs;
55
+ // Saturate the multiplication so a long duration × iteration count
56
+ // cannot wrap and complete immediately after ~49 days of uptime.
57
+ uint32_t totalDuration = static_cast<uint32_t>(__ui_anims[i].iterations) * static_cast<uint32_t>(__ui_anims[i].durationMs);
58
+ if (__ui_anims[i].durationMs != 0 &&
59
+ static_cast<uint32_t>(__ui_anims[i].iterations) > 0xFFFFFFFFUL / static_cast<uint32_t>(__ui_anims[i].durationMs)) {
60
+ totalDuration = 0xFFFFFFFFUL;
61
+ }
55
62
  if (elapsedNoDelay >= totalDuration) {
56
63
  elapsedNoDelay = totalDuration;
57
64
  completing = 1;
@@ -61,7 +68,7 @@ export function emitTickTransitionsPhase() {
61
68
  uint8_t pct = 100;
62
69
  if (!completing && __ui_anims[i].durationMs > 0) {
63
70
  uint32_t cycleMs = elapsedNoDelay % __ui_anims[i].durationMs;
64
- pct = (uint8_t)((uint32_t)cycleMs * 100 / __ui_anims[i].durationMs);
71
+ pct = static_cast<uint8_t>(static_cast<uint32_t>(cycleMs) * 100 / __ui_anims[i].durationMs);
65
72
  }
66
73
  // Find surrounding keyframe stops.
67
74
  if (__ui_anims[i].keyframeSet >= __ui_keyframe_set_count) continue;
@@ -77,7 +84,7 @@ export function emitTickTransitionsPhase() {
77
84
  const UIKeyframeStop* sHi = &ks->stops[hi];
78
85
  // Lerp factor between lo and hi.
79
86
  uint8_t range = sHi->percent - sLo->percent;
80
- uint8_t lerpK = range > 0 ? (uint8_t)((uint16_t)(pct - sLo->percent) * 100 / range) : 0;
87
+ uint8_t lerpK = range > 0 ? static_cast<uint8_t>(static_cast<uint16_t>(pct - sLo->percent) * 100 / range) : 0;
81
88
  // Shape the lerp by the animation's timing function (ease-in-out, etc.).
82
89
  // CSS attaches it to the animation and applies it between stops.
83
90
  lerpK = ui_ease_lerp_k(__ui_anims[i].timingFunction, lerpK);
@@ -95,7 +102,7 @@ export function emitTickTransitionsPhase() {
95
102
  }
96
103
  if ((sLo->props & UI_KF_OPACITY) && (sHi->props & UI_KF_OPACITY)) {
97
104
  uint8_t newOp = range > 0
98
- ? (uint8_t)((int16_t)sLo->opacity + ((int16_t)sHi->opacity - (int16_t)sLo->opacity) * lerpK / 100)
105
+ ? static_cast<uint8_t>(static_cast<int16_t>(sLo->opacity) + (static_cast<int16_t>(sHi->opacity) - static_cast<int16_t>(sLo->opacity)) * lerpK / 100)
99
106
  : sLo->opacity;
100
107
  if (newOp != __ui_nodes[n].opacity) { __ui_nodes[n].opacity = newOp; changed = 1; }
101
108
  }
@@ -108,10 +115,10 @@ export function emitTickTransitionsPhase() {
108
115
  uint8_t hasSizeFrame = (sLo->props & UI_KF_SIZE) && (sHi->props & UI_KF_SIZE);
109
116
  if (hasSizeFrame) {
110
117
  nextWidth = range > 0
111
- ? (int16_t)((int32_t)sLo->width + ((int32_t)sHi->width - (int32_t)sLo->width) * lerpK / 100)
118
+ ? static_cast<int16_t>(static_cast<int32_t>(sLo->width) + (static_cast<int32_t>(sHi->width) - static_cast<int32_t>(sLo->width)) * lerpK / 100)
112
119
  : sLo->width;
113
120
  nextHeight = range > 0
114
- ? (int16_t)((int32_t)sLo->height + ((int32_t)sHi->height - (int32_t)sLo->height) * lerpK / 100)
121
+ ? static_cast<int16_t>(static_cast<int32_t>(sLo->height) + (static_cast<int32_t>(sHi->height) - static_cast<int32_t>(sLo->height)) * lerpK / 100)
115
122
  : sLo->height;
116
123
  if (nextWidth < 0) nextWidth = 0;
117
124
  if (nextHeight < 0) nextHeight = 0;
@@ -122,25 +129,25 @@ export function emitTickTransitionsPhase() {
122
129
  }
123
130
  if ((sLo->props & UI_KF_TRANSFORM) && (sHi->props & UI_KF_TRANSFORM)) {
124
131
  int16_t pxX = range > 0
125
- ? (int16_t)((int32_t)sLo->transformOffsetX + ((int32_t)sHi->transformOffsetX - (int32_t)sLo->transformOffsetX) * lerpK / 100)
132
+ ? static_cast<int16_t>(static_cast<int32_t>(sLo->transformOffsetX) + (static_cast<int32_t>(sHi->transformOffsetX) - static_cast<int32_t>(sLo->transformOffsetX)) * lerpK / 100)
126
133
  : sLo->transformOffsetX;
127
134
  int16_t pxY = range > 0
128
- ? (int16_t)((int32_t)sLo->transformOffsetY + ((int32_t)sHi->transformOffsetY - (int32_t)sLo->transformOffsetY) * lerpK / 100)
135
+ ? static_cast<int16_t>(static_cast<int32_t>(sLo->transformOffsetY) + (static_cast<int32_t>(sHi->transformOffsetY) - static_cast<int32_t>(sLo->transformOffsetY)) * lerpK / 100)
129
136
  : sLo->transformOffsetY;
130
137
  int16_t pctX = range > 0
131
- ? (int16_t)((int32_t)sLo->translatePctX + ((int32_t)sHi->translatePctX - (int32_t)sLo->translatePctX) * lerpK / 100)
138
+ ? static_cast<int16_t>(static_cast<int32_t>(sLo->translatePctX) + (static_cast<int32_t>(sHi->translatePctX) - static_cast<int32_t>(sLo->translatePctX)) * lerpK / 100)
132
139
  : sLo->translatePctX;
133
140
  int16_t pctY = range > 0
134
- ? (int16_t)((int32_t)sLo->translatePctY + ((int32_t)sHi->translatePctY - (int32_t)sLo->translatePctY) * lerpK / 100)
141
+ ? static_cast<int16_t>(static_cast<int32_t>(sLo->translatePctY) + (static_cast<int32_t>(sHi->translatePctY) - static_cast<int32_t>(sLo->translatePctY)) * lerpK / 100)
135
142
  : sLo->translatePctY;
136
143
  int16_t scaleX = range > 0
137
- ? (int16_t)((int32_t)sLo->scaleX + ((int32_t)sHi->scaleX - (int32_t)sLo->scaleX) * lerpK / 100)
144
+ ? static_cast<int16_t>(static_cast<int32_t>(sLo->scaleX) + (static_cast<int32_t>(sHi->scaleX) - static_cast<int32_t>(sLo->scaleX)) * lerpK / 100)
138
145
  : sLo->scaleX;
139
146
  int16_t scaleY = range > 0
140
- ? (int16_t)((int32_t)sLo->scaleY + ((int32_t)sHi->scaleY - (int32_t)sLo->scaleY) * lerpK / 100)
147
+ ? static_cast<int16_t>(static_cast<int32_t>(sLo->scaleY) + (static_cast<int32_t>(sHi->scaleY) - static_cast<int32_t>(sLo->scaleY)) * lerpK / 100)
141
148
  : sLo->scaleY;
142
149
  nextRotateDeg = range > 0
143
- ? (int16_t)((int32_t)sLo->rotateDeg + ((int32_t)sHi->rotateDeg - (int32_t)sLo->rotateDeg) * lerpK / 100)
150
+ ? static_cast<int16_t>(static_cast<int32_t>(sLo->rotateDeg) + (static_cast<int32_t>(sHi->rotateDeg) - static_cast<int32_t>(sLo->rotateDeg)) * lerpK / 100)
144
151
  : sLo->rotateDeg;
145
152
  if (scaleX < 0) scaleX = 0;
146
153
  if (scaleY < 0) scaleY = 0;
@@ -148,14 +155,14 @@ export function emitTickTransitionsPhase() {
148
155
  int16_t refH = hasSizeFrame ? nextHeight : __ui_anims[i].baseHeight;
149
156
  if (refW <= 0) refW = __ui_nodes[n].box.w;
150
157
  if (refH <= 0) refH = __ui_nodes[n].box.h;
151
- int16_t originPxX = (int16_t)((int32_t)refW * __ui_anims[i].originX / 100);
152
- int16_t originPxY = (int16_t)((int32_t)refH * __ui_anims[i].originY / 100);
153
- int16_t scaledW = (int16_t)((int32_t)refW * scaleX / 100);
154
- int16_t scaledH = (int16_t)((int32_t)refH * scaleY / 100);
155
- int16_t scaleOffsetX = originPxX - (int16_t)((int32_t)originPxX * scaleX / 100);
156
- int16_t scaleOffsetY = originPxY - (int16_t)((int32_t)originPxY * scaleY / 100);
157
- nextTransformX = pxX + (int16_t)((int32_t)refW * pctX / 100) + scaleOffsetX;
158
- nextTransformY = pxY + (int16_t)((int32_t)refH * pctY / 100) + scaleOffsetY;
158
+ int16_t originPxX = static_cast<int16_t>(static_cast<int32_t>(refW) * __ui_anims[i].originX / 100);
159
+ int16_t originPxY = static_cast<int16_t>(static_cast<int32_t>(refH) * __ui_anims[i].originY / 100);
160
+ int16_t scaledW = static_cast<int16_t>(static_cast<int32_t>(refW) * scaleX / 100);
161
+ int16_t scaledH = static_cast<int16_t>(static_cast<int32_t>(refH) * scaleY / 100);
162
+ int16_t scaleOffsetX = originPxX - static_cast<int16_t>(static_cast<int32_t>(originPxX) * scaleX / 100);
163
+ int16_t scaleOffsetY = originPxY - static_cast<int16_t>(static_cast<int32_t>(originPxY) * scaleY / 100);
164
+ nextTransformX = pxX + static_cast<int16_t>(static_cast<int32_t>(refW) * pctX / 100) + scaleOffsetX;
165
+ nextTransformY = pxY + static_cast<int16_t>(static_cast<int32_t>(refH) * pctY / 100) + scaleOffsetY;
159
166
  nextWidth = scaledW < 0 ? 0 : scaledW;
160
167
  nextHeight = scaledH < 0 ? 0 : scaledH;
161
168
  if (nextTransformX != __ui_nodes[n].transformOffsetX ||
@@ -92,15 +92,15 @@ static int16_t ui_hit_test(int16_t tx, int16_t ty) {
92
92
  for (uint16_t i = 0; i < __ui_node_count; i++) {
93
93
  if (!ui_is_effectively_visible(i)) continue;
94
94
  if (__ui_nodes[i].screenId != __ui_active_screen) continue;
95
- int16_t drawX = ui_draw_x_for_node((uint16_t)i);
96
- int16_t drawY = ui_draw_y_for_node((uint16_t)i);
95
+ int16_t drawX = ui_draw_x_for_node(static_cast<uint16_t>(i));
96
+ int16_t drawY = ui_draw_y_for_node(static_cast<uint16_t>(i));
97
97
  if (tx >= drawX && tx < drawX + __ui_nodes[i].box.w &&
98
98
  ty >= drawY && ty < drawY + __ui_nodes[i].box.h) {
99
99
  // The node's box contains the tap. For nodes inside a scroll container,
100
100
  // the tap point (not the whole box) must lie within the visible viewport:
101
101
  // a tall wrapped paragraph can overflow below the fold yet still have a
102
102
  // tappable link segment in its visible portion.
103
- if (ui_is_point_clipped_by_scroll((uint16_t)i, tx, ty)) continue;
103
+ if (ui_is_point_clipped_by_scroll(static_cast<uint16_t>(i), tx, ty)) continue;
104
104
  // Skip nodes without any click handler — they're containers, not targets.
105
105
  // Exceptions: NODE_RANGE (horizontal drag), NODE_INPUT (opens keyboard),
106
106
  // NODE_LIST (virtualized item tap), and NODE_BUTTON — the last so a button
@@ -121,7 +121,7 @@ static int16_t ui_hit_test(int16_t tx, int16_t ty) {
121
121
 
122
122
  // Dispatch a handler from the given table if registered for the node.
123
123
  static void ui_dispatch(void (**table)(), uint16_t count, int16_t node) {
124
- if (node >= 0 && (uint16_t)node < count && table[node]) {
124
+ if (node >= 0 && static_cast<uint16_t>(node) < count && table[node]) {
125
125
  table[node]();
126
126
  }
127
127
  }
@@ -165,28 +165,13 @@ static void ui_touch_down(int16_t tx, int16_t ty) {
165
165
  // One owner per gesture; the double-delta bug class (node in both a container
166
166
  // and a list) is gone because there's no second mechanism.
167
167
  #if UI_SCROLL_HAS_TOUCH
168
- int16_t bestScroll = -1;
169
- for (uint16_t i = 0; i < __ui_node_count; i++) {
170
- if (!__ui_nodes[i].scrollable || !ui_is_effectively_visible(i)) continue;
171
- if (__ui_nodes[i].screenId != __ui_active_screen) continue;
172
- // Only scrollable if content overflows the viewport.
173
- if (__ui_nodes[i].contentHeight <= __ui_nodes[i].box.h) continue;
174
- int16_t drawX = ui_draw_x_for_node((uint16_t)i);
175
- int16_t drawY = ui_draw_y_for_node((uint16_t)i);
176
- if (tx >= drawX && tx < drawX + __ui_nodes[i].box.w &&
177
- ty >= drawY && ty < drawY + __ui_nodes[i].box.h) {
178
- if (bestScroll < 0 || ui_node_draws_before(bestScroll, i)) bestScroll = i;
179
- }
180
- }
181
- __ui_scroll_node = bestScroll;
168
+ __ui_scroll_node = ui_scroll_node_at(tx, ty);
182
169
  #else
183
170
  (void)tx; (void)ty;
184
171
  #endif
185
172
  if (node >= 0) {
186
- uint8_t handledTouchTarget = 0;
187
173
  if (__ui_nodes[node].kind == NODE_BUTTON) {
188
- ui_set_pressed((uint16_t)node, 1);
189
- handledTouchTarget = 1;
174
+ ui_set_pressed(static_cast<uint16_t>(node), 1);
190
175
  }
191
176
  // Track range nodes for horizontal drag
192
177
  if (__ui_nodes[node].kind == NODE_RANGE) {
@@ -199,20 +184,22 @@ static void ui_touch_down(int16_t tx, int16_t ty) {
199
184
  int16_t rMax = __ui_nodes[node].rangeMax;
200
185
  int16_t range = rMax - rMin;
201
186
  if (range <= 0) range = 100;
202
- int16_t relX = tx - ui_draw_x_for_node((uint16_t)node) - 4;
187
+ int16_t relX = tx - ui_draw_x_for_node(static_cast<uint16_t>(node)) - 4;
203
188
  int16_t usable = __ui_nodes[node].box.w - 8;
204
189
  if (usable <= 0) usable = 1;
205
- int16_t nextVal = rMin + ((int32_t)relX * range) / usable;
190
+ int16_t nextVal = rMin + (static_cast<int32_t>(relX) * range) / usable;
206
191
  nextVal = constrain(nextVal, rMin, rMax);
207
192
  if (nextVal != __ui_nodes[node].value) {
208
193
  __ui_nodes[node].value = nextVal;
209
194
  ui_mark_dirty(node);
210
195
  }
211
- handledTouchTarget = 1;
212
196
  }
213
- uint8_t touchStartsScrollableView =
214
- (__ui_scroll_node >= 0 && node == __ui_scroll_node);
215
- if (!handledTouchTarget && !touchStartsScrollableView) ui_mark_dirty(node);
197
+ // Only controls with a visual state change need a dirty repaint on touch-down:
198
+ // buttons were marked by ui_set_pressed() above and ranges were marked when
199
+ // their value changed. Links, inputs, checks, radios, selects, and list
200
+ // containers do not have a pressed state; repainting them here is redundant
201
+ // and can expose a clear/redraw flash on direct SPI targets. They repaint
202
+ // after the click/value change (or when scrolling actually begins).
216
203
  }
217
204
  }
218
205
 
@@ -241,21 +228,29 @@ static void ui_touch_up() {
241
228
  int16_t clickedNode = __ui_touch_node;
242
229
  if (elapsed < UI_TOUCH_HOLD_MS) {
243
230
  if (__ui_nodes[clickedNode].kind == NODE_INPUT) {
244
- ui_open_keyboard_for_input((uint16_t)clickedNode);
231
+ ui_open_keyboard_for_input(static_cast<uint16_t>(clickedNode));
245
232
  }
246
233
  ui_dispatch(__ui_click_handlers, __ui_click_handler_count, __ui_touch_node);
247
234
  // Rich-text inline link: if the tapped node has link runs, find which link
248
235
  // segment the tap falls within and navigate to its target screen. Copies
249
236
  // the list-item subdivision precedent with measured run rects.
250
237
  if (__ui_nodes[clickedNode].runCount > 0) {
251
- int16_t ndx = __ui_last_touch_x - ui_draw_x_for_node((uint16_t)clickedNode);
252
- int16_t ndy = __ui_last_touch_y - ui_draw_y_for_node((uint16_t)clickedNode);
253
- int8_t target = ui_rich_link_hit((uint16_t)clickedNode, ndx, ndy);
254
- if (target >= 0) ui_navigate((uint8_t)target);
238
+ int16_t ndx = __ui_last_touch_x - ui_draw_x_for_node(static_cast<uint16_t>(clickedNode));
239
+ int16_t ndy = __ui_last_touch_y - ui_draw_y_for_node(static_cast<uint16_t>(clickedNode));
240
+ int8_t target = ui_rich_link_hit(static_cast<uint16_t>(clickedNode), ndx, ndy);
241
+ if (target >= 0) ui_navigate(static_cast<uint8_t>(target));
255
242
  }
256
243
  }
257
244
  ui_dispatch(__ui_release_handlers, __ui_click_handler_count, __ui_touch_node);
258
- ui_mark_dirty(clickedNode);
245
+ // Built-in check/radio/select handlers mutate the node value during click
246
+ // dispatch and need one repaint. Buttons are already dirty from releasing
247
+ // :pressed; links, lists, inputs, and ordinary callbacks are either handled
248
+ // by navigation/keyboard state or observed by bindings on the next tick.
249
+ if (__ui_nodes[clickedNode].kind == NODE_CHECK ||
250
+ __ui_nodes[clickedNode].kind == NODE_RADIO ||
251
+ __ui_nodes[clickedNode].kind == NODE_SELECT) {
252
+ ui_mark_dirty(static_cast<uint16_t>(clickedNode));
253
+ }
259
254
  }
260
255
  // Release the pressed button's :pressed state unconditionally — even when a
261
256
  // drag/scroll hijacked the gesture (the click above is correctly gated on
@@ -263,7 +258,7 @@ static void ui_touch_up() {
263
258
  // otherwise a touch-down on a button followed by a scroll leaves it stuck).
264
259
  if (__ui_touch_node >= 0 && __ui_touch_node < __ui_node_count &&
265
260
  __ui_nodes[__ui_touch_node].kind == NODE_BUTTON && __ui_nodes[__ui_touch_node].value != 0) {
266
- ui_set_pressed((uint16_t)__ui_touch_node, 0);
261
+ ui_set_pressed(static_cast<uint16_t>(__ui_touch_node), 0);
267
262
  }
268
263
  // Resume any \`await ui.onTap()\` awaiter. Runs for EVERY completed tap —
269
264
  // including holds (released above) and taps on empty space (__ui_touch_node
@@ -283,9 +278,9 @@ static void ui_touch_up() {
283
278
  int16_t relY = __ui_last_touch_y - drawY;
284
279
  int16_t totalMove = abs(__ui_last_touch_y - __ui_touch_down_y_pos);
285
280
  uint16_t ih = __ui_nodes[n].listItemHeight > 0 ? __ui_nodes[n].listItemHeight : 24;
286
- if (totalMove < (int16_t)(ih / 2) &&
281
+ if (totalMove < static_cast<int16_t>(ih / 2) &&
287
282
  relY >= 0 && relY < __ui_nodes[n].box.h) {
288
- uint16_t itemIdx = (uint16_t)((relY + __ui_nodes[n].scrollY) / ih);
283
+ uint16_t itemIdx = static_cast<uint16_t>((relY + __ui_nodes[n].scrollY) / ih);
289
284
  if (itemIdx < __ui_nodes[n].listCount) {
290
285
  __ui_nodes[n].listTapFn(itemIdx);
291
286
  }
@@ -349,16 +344,16 @@ static inline void ui_handle_touch(int16_t tx, int16_t ty) {
349
344
  int16_t rMax = __ui_nodes[__ui_range_node].rangeMax;
350
345
  int16_t range = rMax - rMin;
351
346
  if (range <= 0) range = 100;
352
- int16_t relX = tx - ui_draw_x_for_node((uint16_t)__ui_range_node) - 4;
347
+ int16_t relX = tx - ui_draw_x_for_node(static_cast<uint16_t>(__ui_range_node)) - 4;
353
348
  int16_t usable = __ui_nodes[__ui_range_node].box.w - 8;
354
349
  if (usable <= 0) usable = 1;
355
- int16_t newVal = rMin + ((int32_t)relX * range) / usable;
350
+ int16_t newVal = rMin + (static_cast<int32_t>(relX) * range) / usable;
356
351
  newVal = constrain(newVal, rMin, rMax);
357
352
  if (newVal != __ui_nodes[__ui_range_node].value) {
358
353
  __ui_nodes[__ui_range_node].value = newVal;
359
354
  ui_mark_dirty(__ui_range_node);
360
355
  // Fire the onChange callback (if any) — every value change during drag.
361
- if (__ui_range_node < (int16_t)__ui_rangechange_handler_count &&
356
+ if (__ui_range_node < static_cast<int16_t>(__ui_rangechange_handler_count) &&
362
357
  __ui_rangechange_handlers[__ui_range_node]) {
363
358
  __ui_rangechange_handlers[__ui_range_node]();
364
359
  }
@@ -376,10 +371,17 @@ static inline void ui_handle_touch(int16_t tx, int16_t ty) {
376
371
  ui_apply_scroll_delta(__ui_scroll_node, dy);
377
372
  __ui_drag_start_y = ty;
378
373
  #if UI_SCROLL_DEBUG
374
+ #if defined(ESP32) && defined(ARDUINO)
379
375
  Serial.printf("scroll dy=%d sy=%d ov=%d virt=%d\\n",
380
376
  dy, __ui_nodes[__ui_scroll_node].scrollY,
381
377
  __ui_nodes[__ui_scroll_node].overscrollPx,
382
- (int)__ui_nodes[__ui_scroll_node].virtualized);
378
+ static_cast<int>(__ui_nodes[__ui_scroll_node].virtualized));
379
+ #else
380
+ printk("scroll dy=%d sy=%d ov=%d virt=%d\\n",
381
+ dy, __ui_nodes[__ui_scroll_node].scrollY,
382
+ __ui_nodes[__ui_scroll_node].overscrollPx,
383
+ static_cast<int>(__ui_nodes[__ui_scroll_node].virtualized));
384
+ #endif
383
385
  #endif
384
386
  }
385
387
  }
@@ -7,7 +7,8 @@ export function emitTypesDefines() {
7
7
  #ifndef __TC_UI_RUNTIME
8
8
  #define __TC_UI_RUNTIME
9
9
  #include <stdint.h>
10
- #include <string.h>
10
+ #include <cstring>
11
+ #include <new>
11
12
  #define UI_TEXT_BUF 32 // max stored UI text chars, excluding the trailing NUL
12
13
  #define UI_TEXT_LINE_BUF 96
13
14
  #define UI_WS_NORMAL 0
@@ -17,6 +18,17 @@ export function emitTypesDefines() {
17
18
  #ifndef UI_MAX_BUFFERED_PAINT_PIXELS
18
19
  #define UI_MAX_BUFFERED_PAINT_PIXELS 20000
19
20
  #endif
21
+ // Above this many pixels, prefer the band renderer (small ~10KB SRAM canvas,
22
+ // strip-at-a-time) over the repair canvas even when the repair canvas could
23
+ // allocate (e.g. in PSRAM). A large repair canvas (a full-width button is
24
+ // ~15-30KB) has higher alloc/seed/push latency than banding — the per-band
25
+ // push is smaller and the band canvas lives in fast internal SRAM. Set below
26
+ // UI_MAX_BUFFERED_PAINT_PIXELS so large rects band while small repaints keep
27
+ // the fast single-canvas composite. Tune per target; 0 = always prefer the
28
+ // repair canvas when it allocates (the pre-PSRAM behavior).
29
+ #ifndef UI_BAND_PREFER_PIXELS
30
+ #define UI_BAND_PREFER_PIXELS 8000
31
+ #endif
20
32
  #ifndef UI_USE_FULL_FRAMEBUFFER
21
33
  #define UI_USE_FULL_FRAMEBUFFER 0
22
34
  #endif
@@ -69,8 +81,16 @@ export function emitTypesDefines() {
69
81
  #ifndef UI_SCROLL_DEBUG
70
82
  #define UI_SCROLL_DEBUG 0
71
83
  #endif
72
- #ifndef UI_SCROLL_CANVAS_BUDGET_BYTES
73
- #define UI_SCROLL_CANVAS_BUDGET_BYTES 88000
84
+ // Band renderer: height (px) of the horizontal band canvas used to composite
85
+ // scroll subtrees tear-free when no viewport canvas fits (no PSRAM / over
86
+ // budget). Each band is vw × UI_STRIP_BAND_HEIGHT. Must be tall enough that a
87
+ // single line of AA text (glyph height ~lineHeight, typically 16-22px at ts=2)
88
+ // fits within one band — if a glyph straddles a band boundary, its anti-
89
+ // aliased edge pixels blend toward the wrong bg in the adjacent band, visible
90
+ // as a clipped/faded text bottom. 32 ensures most fonts fit; at 480px wide
91
+ // that's ~30KB (fits SRAM on most ESP32 targets).
92
+ #ifndef UI_STRIP_BAND_HEIGHT
93
+ #define UI_STRIP_BAND_HEIGHT 32
74
94
  #endif
75
95
  #if defined(ESP32) || defined(ESP8266)
76
96
  #include <Esp.h>
@@ -35,6 +35,7 @@ import { emitTouchKeyboardFwd } from "./runtime-header/touch-keyboard-fwd.js";
35
35
  import { emitBlendBodies } from "./runtime-header/blend-bodies.js";
36
36
  import { emitTextRendering } from "./runtime-header/text-rendering.js";
37
37
  import { emitNodeDecoration } from "./runtime-header/node-decoration.js";
38
+ import { emitNodeDrawBody } from "./runtime-header/node-draw-body.js";
38
39
  import { emitTickMotionHelpers } from "./runtime-header/tick-motion-helpers.js";
39
40
  import { emitTick } from "./runtime-header/tick.js";
40
41
  import { emitAntialiasing } from "./runtime-header/antialiasing.js";
@@ -64,6 +65,7 @@ export function emitRuntimeHeader(opts) {
64
65
  emitBlendBodies(),
65
66
  emitTextRendering(),
66
67
  emitNodeDecoration(),
68
+ emitNodeDrawBody(),
67
69
  emitTickMotionHelpers(),
68
70
  emitTick(),
69
71
  emitAntialiasing(),
@@ -13,7 +13,7 @@ import { parseHtml } from "./html-parser.js";
13
13
  import { parseCss, parseFontFaces, parseKeyframes } from "./css-parser.js";
14
14
  import { resolveStyles } from "./style-resolver.js";
15
15
  import { selectEngine } from "./select-engine.js";
16
- import { measure } from "./layout-engine.js";
16
+ import { measureWithFonts } from "./layout-engine.js";
17
17
  import { lowerUIToCpp } from "./ui-lowering.js";
18
18
  import { buildUIFontAssets } from "./font-assets.js";
19
19
  import { buildKeyframeSets } from "./keyframes.js";
@@ -27,7 +27,7 @@ export function transpileUI(html, css, opts) {
27
27
  // Select layout engine: Yoga for flexbox, BlockLayout as fallback.
28
28
  const engine = selectEngine(styled);
29
29
  const viewport = { x: 0, y: 0, w: opts.viewport.width, h: opts.viewport.height };
30
- const boxes = engine.arrange(styled, viewport, measure);
30
+ const boxes = engine.arrange(styled, viewport, measureWithFonts(fontAssets));
31
31
  const keyframeSets = buildKeyframeSets(rawKeyframes, opts.colorFormat);
32
32
  return lowerUIToCpp(styled, boxes, opts.colorFormat, opts.storage, [], rules, undefined, fontAssets, [], new Map(), keyframeSets);
33
33
  }
@@ -6,9 +6,15 @@
6
6
  // ---------------------------------------------------------------------------
7
7
  import { parseCss } from "./css-parser.js";
8
8
  const UA_CSS = `
9
+ /* Browser-like light defaults: dark text on a near-white background.
10
+ A dark theme overrides these via user CSS (e.g. screen { color: #f0f0f0;
11
+ background: #1a1a1a }). Without these, screen fg defaults to white
12
+ (0xFFFF at the model level), making text invisible on light backgrounds. */
9
13
  screen {
10
14
  display: flex;
11
15
  flex-direction: column;
16
+ color: #1a1a1a;
17
+ background: #ffffff;
12
18
  }
13
19
  body {
14
20
  display: flex;
@@ -20,14 +26,41 @@ view {
20
26
  display: flex;
21
27
  flex-direction: column;
22
28
  }
29
+ /* Scrollable content area — the standard pattern for multi-screen apps.
30
+ align-items:center + gap give readable card-style layouts out of the box. */
31
+ .scrollBody {
32
+ overflow: scroll;
33
+ flex: 1;
34
+ align-items: center;
35
+ gap: 10px;
36
+ }
37
+ /* Header bar (back link + title). flex-direction:row is the expected layout;
38
+ without it the back link and title stack vertically. */
39
+ .screenHeader {
40
+ flex-direction: row;
41
+ align-items: center;
42
+ gap: 8px;
43
+ }
23
44
  button {
24
- padding: 4px 10px;
45
+ padding: 10px 16px;
25
46
  border: 1px solid;
47
+ min-height: 48px;
26
48
  text-align: center;
27
49
  }
28
50
  input {
29
- padding: 4px 8px;
51
+ padding: 8px;
52
+ border: 1px solid;
53
+ }
54
+ select {
55
+ padding: 10px 12px;
30
56
  border: 1px solid;
57
+ min-height: 48px;
58
+ }
59
+ check, radio {
60
+ min-height: 48px;
61
+ }
62
+ range {
63
+ height: 40px;
31
64
  }
32
65
  list {
33
66
  overflow: scroll;
@@ -39,13 +72,23 @@ a {
39
72
  label {
40
73
  text-align: left;
41
74
  }
75
+ /* h3 is non-bold by default: the runtime bumps textSize for bold text
76
+ (roughly doubling per-char advance), which wraps multi-word captions
77
+ like "width / height / min / max" to 3 lines. Normal weight keeps
78
+ them readable. Users can override with font-weight:bold if needed. */
42
79
  h1 { font-size: 24px; font-weight: bold; }
43
80
  h2 { font-size: 20px; font-weight: bold; }
44
- h3 { font-size: 18px; font-weight: bold; }
81
+ h3 { font-size: 18px; font-weight: normal; }
45
82
  h4 { font-size: 16px; font-weight: bold; }
46
83
  h5 { font-size: 14px; }
47
84
  h6 { font-size: 12px; }
48
85
  p { font-size: 16px; }
86
+ /* Keyboard structural defaults (the OSK is runtime-generated, not
87
+ user-authored HTML). Border keeps keys visually separated. Colors
88
+ are design choices left to user CSS. */
89
+ .ui-key {
90
+ border: 1px solid;
91
+ }
49
92
  `;
50
93
  let UA_RULES = null;
51
94
  /** Get the parsed UA default rules. Lazily computed, cached. */
@@ -21,13 +21,11 @@ export interface LoweredUI {
21
21
  imageTables: string;
22
22
  /** C++ keyframe data arrays + animation table. */
23
23
  keyframeTables: string;
24
- /** Compile-time warnings for scroll viewports that exceed the canvas budget. */
25
- scrollMemoryDiagnostics: Diagnostic[];
26
24
  /** Layout-time diagnostics (viewport overflow, text overflow). Populated by
27
25
  * lowerOnMount after the boxes are computed; empty until then. */
28
26
  diagnostics: Diagnostic[];
29
27
  }
30
28
  type ColorFormat = "rgb565" | "rgb666" | "rgb888" | "mono";
31
29
  type Storage = "progmem" | "flash";
32
- export declare function lowerUIToCpp(root: StyledNode, boxes: Box[], colorFormat: ColorFormat, storage: Storage, keyboards?: KeyboardTemplate[], rules?: CSSRule[], display?: DisplayProfile, fontAssets?: UIFontAssetModel[], allScreens?: StyledNode[], imageAssetIds?: Map<string, number>, keyframeSets?: KeyframeSetModel[], scrollCanvasBudgetBytes?: number): LoweredUI;
30
+ export declare function lowerUIToCpp(root: StyledNode, boxes: Box[], colorFormat: ColorFormat, storage: Storage, keyboards?: KeyboardTemplate[], rules?: CSSRule[], display?: DisplayProfile, fontAssets?: UIFontAssetModel[], allScreens?: StyledNode[], imageAssetIds?: Map<string, number>, keyframeSets?: KeyframeSetModel[]): LoweredUI;
33
31
  export {};
@@ -14,16 +14,14 @@
14
14
  import { lowerUIToModel } from "./model.js";
15
15
  import { resolveColor } from "./color.js";
16
16
  import { DEFAULT_ALPHA_KEYBOARD, DEFAULT_NUMBER_KEYBOARD } from "./default-keyboards.js";
17
- import { analyzeScrollMemory } from "./scroll-memory-diagnostics.js";
18
17
  import { getListBindings } from "@typecad/cuttlefish/ir/transformers/ui-reactive";
19
- export function lowerUIToCpp(root, boxes, colorFormat, storage, keyboards = [], rules = [], display, fontAssets = [], allScreens = [], imageAssetIds = new Map(), keyframeSets = [], scrollCanvasBudgetBytes) {
18
+ export function lowerUIToCpp(root, boxes, colorFormat, storage, keyboards = [], rules = [], display, fontAssets = [], allScreens = [], imageAssetIds = new Map(), keyframeSets = []) {
20
19
  void storage;
21
20
  const model = lowerUIToModel(root, boxes, colorFormat, display, fontAssets, allScreens, imageAssetIds, keyframeSets);
22
- const scrollMemoryDiagnostics = analyzeScrollMemory(model.nodes, scrollCanvasBudgetBytes);
23
21
  // Tables are mutable RAM (ui_tick updates bg/dirty/elapsed/active each
24
22
  // frame), so no PROGMEM/flash storage keyword — those imply read-only.
25
23
  const fontTables = emitFontTables(model);
26
- const nodeTable = emitNodeTable(model) + "\n" + emitScrollNodeIdLookup(model);
24
+ const nodeTable = emitNodeTable(model);
27
25
  const transitionTable = emitTransitionTable(model);
28
26
  const typeDecl = emitTypeDecl(root);
29
27
  // Keyboard loaders + dispatch: collect input nodes in tree order, resolve
@@ -63,7 +61,7 @@ export function lowerUIToCpp(root, boxes, colorFormat, storage, keyboards = [],
63
61
  const screenCount = model.nodes.length > 0 ? Math.max(...model.nodes.map(n => n.screenId)) + 1 : 1;
64
62
  const imageTables = "const UIImage __ui_images[] = {};\nconst uint16_t __ui_image_count = 0;";
65
63
  const keyframeTables = emitKeyframeTables(model);
66
- return { fontTables, nodeTable, transitionTable, typeDecl, keyboardLoaders, keyboardDispatch, screenCount, imageTables, keyframeTables, scrollMemoryDiagnostics, diagnostics: [] };
64
+ return { fontTables, nodeTable, transitionTable, typeDecl, keyboardLoaders, keyboardDispatch, screenCount, imageTables, keyframeTables, diagnostics: [] };
67
65
  }
68
66
  function sanitizedId(id) {
69
67
  return id.replace(/[^a-zA-Z0-9_]/g, "_");
@@ -177,6 +175,7 @@ function cppKind(kind) {
177
175
  case "img": return "NODE_IMG";
178
176
  case "list": return "NODE_LIST";
179
177
  case "canvas": return "NODE_CANVAS";
178
+ case "select": return "NODE_SELECT";
180
179
  case "text": return "NODE_TEXT";
181
180
  }
182
181
  }
@@ -226,24 +225,6 @@ function emitFontTables(model) {
226
225
  lines.push(`const uint16_t __ui_font_face_count = ${assets.length};`);
227
226
  return lines.join("\n");
228
227
  }
229
- function emitScrollNodeIdLookup(model) {
230
- const cases = [];
231
- for (const n of model.nodes) {
232
- if (!n.scrollable || n.contentHeight <= n.box.h || !n.id)
233
- continue;
234
- const id = n.id.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
235
- cases.push(` if (idx == ${n.index}) return "${id}";`);
236
- }
237
- if (cases.length === 0) {
238
- return "static inline const char* __ui_scroll_node_id(uint16_t idx) { (void)idx; return nullptr; }";
239
- }
240
- return [
241
- "static inline const char* __ui_scroll_node_id(uint16_t idx) {",
242
- ...cases,
243
- " return nullptr;",
244
- "}",
245
- ].join("\n");
246
- }
247
228
  function emitNodeTable(model) {
248
229
  // Map node index → list binding so virtualized nodes carry their count/item/
249
230
  // tap function pointers on-node (no UIListBinding side table at runtime).
@@ -25,7 +25,6 @@ import { selectEngine } from "./select-engine.js";
25
25
  import { measureWithFonts } from "./layout-engine.js";
26
26
  import { lowerUIToCpp } from "./ui-lowering.js";
27
27
  import { getDisplayProfile } from "@typecad/cuttlefish/stores/display-profile-store";
28
- import { resolveScrollConfig } from "@typecad/cuttlefish/api/shared";
29
28
  import { buildUIFontAssets } from "./font-assets.js";
30
29
  import { emitImageTables, loadImageAssets } from "./image-assets.js";
31
30
  import { splitUiFile } from "./ui-file-splitter.js";
@@ -145,12 +144,7 @@ export function lowerOnMount(htmlPath, opts) {
145
144
  const keyframeSets = buildKeyframeSets(mod.rawKeyframes || [], opts.colorFormat);
146
145
  // Load image assets before lowering so imgDataId can be set.
147
146
  const imageAssets = loadImageAssets(allStyled.length > 0 ? allStyled : [mod.styled], path.dirname(abs));
148
- const _p = getDisplayProfile();
149
- const scrollBudget = resolveScrollConfig(_p, { buildTarget: _p._buildTarget, psram: _p._psram }).scrollCanvasBudgetBytes;
150
- const result = lowerUIToCpp(mod.styled, allBoxes, opts.colorFormat, opts.storage, mod.keyboards, mod.rules, getDisplayProfile(), mod.fontAssets, allStyled, imageAssets.nodeIdToAssetIndex, keyframeSets, scrollBudget);
151
- for (const d of result.scrollMemoryDiagnostics) {
152
- mod.mountDiagnostics.push({ ...d, source: d.source ?? path.basename(mod.htmlPath) });
153
- }
147
+ const result = lowerUIToCpp(mod.styled, allBoxes, opts.colorFormat, opts.storage, mod.keyboards, mod.rules, getDisplayProfile(), mod.fontAssets, allStyled, imageAssets.nodeIdToAssetIndex, keyframeSets);
154
148
  // Layout diagnostics: viewport-overflow (node bottom past the viewport) and
155
149
  // text-overflow (node right edge past its parent). Both reference the node by
156
150
  // a human-label (#id / <tag "text"> / <tag>) so the author can find it in