@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
@@ -30,12 +30,12 @@ static inline uint8_t ui_node_draws_before(uint16_t a, uint16_t b) {
30
30
  // re-scanning all nodes for every dirty repaint (O(D·N)).
31
31
  static inline void ui_build_draw_order() {
32
32
  if (__ui_draw_order || __ui_node_count == 0) return;
33
- __ui_draw_order = (uint16_t*)malloc((size_t)__ui_node_count * sizeof(uint16_t));
33
+ __ui_draw_order = new (std::nothrow) uint16_t[__ui_node_count];
34
34
  if (!__ui_draw_order) return;
35
35
  for (uint16_t i = 0; i < __ui_node_count; i++) __ui_draw_order[i] = i;
36
36
  for (uint16_t a = 1; a < __ui_node_count; a++) {
37
37
  uint16_t key = __ui_draw_order[a];
38
- int16_t j = (int16_t)a - 1;
38
+ int16_t j = static_cast<int16_t>(a) - 1;
39
39
  while (j >= 0 && ui_node_draws_before(key, __ui_draw_order[j])) {
40
40
  __ui_draw_order[j + 1] = __ui_draw_order[j];
41
41
  j--;
@@ -44,24 +44,24 @@ static inline void ui_build_draw_order() {
44
44
  }
45
45
  }
46
46
 
47
- // Index generic (non-list) scroll containers once so scroll prep doesn't scan
48
- // every node each frame.
47
+ // Index scroll containers once so touch/wheel ownership doesn't scan unrelated
48
+ // nodes each frame.
49
49
  static inline void ui_build_scroll_owner_table() {
50
50
  if (__ui_scroll_owners || __ui_node_count == 0) return;
51
51
  uint16_t count = 0;
52
52
  for (uint16_t i = 0; i < __ui_node_count; i++) {
53
- if (__ui_nodes[i].scrollable && !__ui_nodes[i].virtualized) count++;
53
+ if (__ui_nodes[i].scrollable) count++;
54
54
  }
55
55
  __ui_scroll_owner_count = count;
56
56
  if (!count) return;
57
- __ui_scroll_owners = (uint16_t*)malloc((size_t)count * sizeof(uint16_t));
57
+ __ui_scroll_owners = new (std::nothrow) uint16_t[count];
58
58
  if (!__ui_scroll_owners) {
59
59
  __ui_scroll_owner_count = 0;
60
60
  return;
61
61
  }
62
62
  uint16_t w = 0;
63
63
  for (uint16_t i = 0; i < __ui_node_count; i++) {
64
- if (__ui_nodes[i].scrollable && !__ui_nodes[i].virtualized) {
64
+ if (__ui_nodes[i].scrollable) {
65
65
  __ui_scroll_owners[w++] = i;
66
66
  }
67
67
  }
@@ -138,24 +138,54 @@ static inline int16_t ui_draw_y_for_node(uint16_t nodeIdx) {
138
138
  // misses. Returns the topmost such node by draw order, or -1 if none.
139
139
  static inline int16_t ui_scroll_node_at(int16_t tx, int16_t ty) {
140
140
  int16_t bestScroll = -1;
141
- for (uint16_t i = 0; i < __ui_node_count; i++) {
142
- if (!__ui_nodes[i].scrollable || !ui_is_effectively_visible(i)) continue;
143
- if (__ui_nodes[i].screenId != __ui_active_screen) continue;
144
- if (__ui_nodes[i].contentHeight <= __ui_nodes[i].box.h) continue;
145
- int16_t drawX = ui_draw_x_for_node((uint16_t)i);
146
- int16_t drawY = ui_draw_y_for_node((uint16_t)i);
147
- if (tx >= drawX && tx < drawX + __ui_nodes[i].box.w &&
148
- ty >= drawY && ty < drawY + __ui_nodes[i].box.h) {
149
- if (bestScroll < 0 || ui_node_draws_before((uint16_t)bestScroll, i)) bestScroll = (int16_t)i;
141
+ if (__ui_scroll_owners) {
142
+ for (uint16_t oi = 0; oi < __ui_scroll_owner_count; oi++) {
143
+ uint16_t i = __ui_scroll_owners[oi];
144
+ if (!ui_is_effectively_visible(i)) continue;
145
+ if (__ui_nodes[i].screenId != __ui_active_screen) continue;
146
+ if (__ui_nodes[i].contentHeight <= __ui_nodes[i].box.h) continue;
147
+ int16_t drawX = ui_draw_x_for_node(i);
148
+ int16_t drawY = ui_draw_y_for_node(i);
149
+ if (tx >= drawX && tx < drawX + __ui_nodes[i].box.w &&
150
+ ty >= drawY && ty < drawY + __ui_nodes[i].box.h &&
151
+ (bestScroll < 0 || ui_node_draws_before(static_cast<uint16_t>(bestScroll), i))) {
152
+ bestScroll = static_cast<int16_t>(i);
153
+ }
154
+ }
155
+ } else {
156
+ for (uint16_t i = 0; i < __ui_node_count; i++) {
157
+ if (!__ui_nodes[i].scrollable || !ui_is_effectively_visible(i)) continue;
158
+ if (__ui_nodes[i].screenId != __ui_active_screen) continue;
159
+ if (__ui_nodes[i].contentHeight <= __ui_nodes[i].box.h) continue;
160
+ int16_t drawX = ui_draw_x_for_node(i);
161
+ int16_t drawY = ui_draw_y_for_node(i);
162
+ if (tx >= drawX && tx < drawX + __ui_nodes[i].box.w &&
163
+ ty >= drawY && ty < drawY + __ui_nodes[i].box.h &&
164
+ (bestScroll < 0 || ui_node_draws_before(static_cast<uint16_t>(bestScroll), i))) {
165
+ bestScroll = static_cast<int16_t>(i);
166
+ }
150
167
  }
151
168
  }
152
169
  return bestScroll;
153
170
  }
154
171
 
172
+ // Resolve the actual backdrop for a node by walking painted ancestors. A direct
173
+ // parent may be transparent; using its clearColor there can blend against the
174
+ // wrong layer when siblings or an opaque grandparent are behind the node.
155
175
  static inline UI_COLOR_T ui_parent_clear_color(uint16_t nodeIdx) {
156
176
  uint16_t p = __ui_nodes[nodeIdx].parent;
157
- if (p != UI_NO_PARENT && p < __ui_node_count) {
158
- return __ui_nodes[p].hasBg ? __ui_nodes[p].bg : __ui_nodes[p].clearColor;
177
+ while (p != UI_NO_PARENT && p < __ui_node_count) {
178
+ if (__ui_nodes[p].hasBg) {
179
+ UI_COLOR_T backdrop = __ui_nodes[p].bg;
180
+ if (__ui_nodes[p].opacity < 100) {
181
+ uint16_t pp = __ui_nodes[p].parent;
182
+ UI_COLOR_T under = __ui_nodes[p].clearColor;
183
+ if (pp != UI_NO_PARENT && pp < __ui_node_count) under = ui_parent_clear_color(p);
184
+ backdrop = ui_blend(backdrop, under, __ui_nodes[p].opacity);
185
+ }
186
+ return backdrop;
187
+ }
188
+ p = __ui_nodes[p].parent;
159
189
  }
160
190
  return __ui_nodes[nodeIdx].clearColor;
161
191
  }
@@ -9,11 +9,11 @@ static inline void ui_node_paint_rect(uint16_t nodeIdx, int16_t baseX, int16_t b
9
9
 
10
10
  int16_t faceW = __ui_nodes[nodeIdx].box.w;
11
11
  int16_t faceH = __ui_nodes[nodeIdx].box.h;
12
- if (__ui_nodes[nodeIdx].kind == NODE_TEXT || __ui_nodes[nodeIdx].kind == NODE_CHECK || __ui_nodes[nodeIdx].kind == NODE_RADIO) {
12
+ if (__ui_nodes[nodeIdx].kind == NODE_TEXT || __ui_nodes[nodeIdx].kind == NODE_CHECK || __ui_nodes[nodeIdx].kind == NODE_RADIO || __ui_nodes[nodeIdx].kind == NODE_SELECT) {
13
13
  if (__ui_nodes[nodeIdx].lastTextWidth > faceW) faceW = __ui_nodes[nodeIdx].lastTextWidth;
14
14
  if (__ui_nodes[nodeIdx].lastTextHeight > faceH) faceH = __ui_nodes[nodeIdx].lastTextHeight;
15
- if ((int16_t)textW > faceW) faceW = (int16_t)textW;
16
- if ((int16_t)textH > faceH) faceH = (int16_t)textH;
15
+ if (static_cast<int16_t>(textW) > faceW) faceW = static_cast<int16_t>(textW);
16
+ if (static_cast<int16_t>(textH) > faceH) faceH = static_cast<int16_t>(textH);
17
17
  }
18
18
  int16_t unrotatedFaceW = faceW;
19
19
  int16_t unrotatedFaceH = faceH;
@@ -137,11 +137,18 @@ static inline void ui_clear_press_offset_area(uint16_t nodeIdx, int16_t baseX, i
137
137
 
138
138
  // Generated-font / AA text and images draw per-pixel when sent straight to SPI.
139
139
  // Prefer the RAM paint canvas for these kinds (within the pixel budget).
140
+ // NOTE: ALL NODE_TEXT is treated as pixel-heavy. Even the classic GFX bitmap
141
+ // font renders via drawPixel per font pixel, and on an SPI TFT each drawPixel
142
+ // is a full set_window+RAMWR SPI transaction (~30us) — a single 460x8 text band
143
+ // costs >100ms direct-drawn. Forcing every text node through the paint canvas
144
+ // turns that into one RAM render + one SPI push (~3ms). AA/generated-font text
145
+ // additionally requires getPixel readback for coverage blending, which is only
146
+ // correct against a RAM target.
140
147
  static inline uint8_t ui_pixel_heavy_node(uint16_t nodeIdx) {
141
148
  if (nodeIdx >= __ui_node_count) return 0;
142
149
  if (__ui_nodes[nodeIdx].kind == NODE_IMG) return 1;
143
- if (__ui_nodes[nodeIdx].kind == NODE_TEXT &&
144
- (__ui_nodes[nodeIdx].fontFace || __ui_nodes[nodeIdx].fontAntialias)) return 1;
150
+ if (__ui_nodes[nodeIdx].kind == NODE_TEXT) return 1;
151
+ if (__ui_nodes[nodeIdx].kind == NODE_SELECT) return 1;
145
152
  return 0;
146
153
  }
147
154
 
@@ -150,7 +157,7 @@ static inline uint8_t ui_should_buffer_paint(uint16_t nodeIdx, int16_t w, int16_
150
157
  if (__ui_nodes[nodeIdx].kind == NODE_LIST) return 0;
151
158
  // <canvas> elements batch via __ui_node_canvas when drawing to the display.
152
159
  if (__ui_nodes[nodeIdx].kind == NODE_CANVAS) return 0;
153
- if ((uint32_t)w * (uint32_t)h > UI_MAX_BUFFERED_PAINT_PIXELS) return 0;
160
+ if (static_cast<uint32_t>(w) * static_cast<uint32_t>(h) > UI_MAX_BUFFERED_PAINT_PIXELS) return 0;
154
161
  if (ui_pixel_heavy_node(nodeIdx)) return 1;
155
162
  if (__ui_nodes[nodeIdx].kind == NODE_FILL &&
156
163
  __ui_nodes[nodeIdx].hasBg &&
@@ -167,7 +174,7 @@ static inline uint8_t ui_should_buffer_paint(uint16_t nodeIdx, int16_t w, int16_
167
174
  return 1;
168
175
  }
169
176
 
170
- static inline void ui_seed_paint_canvas_for_node(uint16_t nodeIdx, CuttlefishCanvas16* canvas, int16_t canvasX, int16_t canvasY) {
177
+ static inline void ui_seed_paint_canvas_for_node(uint16_t nodeIdx, CuttlefishCanvas16* canvas, int16_t canvasX, int16_t canvasY, int16_t bandTop) {
171
178
  if (!canvas || !display_canvasBuffer(canvas)) return;
172
179
  uint16_t p = __ui_nodes[nodeIdx].parent;
173
180
  if (p == UI_NO_PARENT || p >= __ui_node_count) {
@@ -191,7 +198,11 @@ static inline void ui_seed_paint_canvas_for_node(uint16_t nodeIdx, CuttlefishCan
191
198
  CuttlefishDisplayTarget* previousGfx = ui_display_get_target();
192
199
  ui_display_set_target(canvas);
193
200
  __ui_nodes[p].box.x = parentDrawX - canvasX;
194
- int16_t localY = parentDrawY - canvasY;
201
+ // Shift the parent's draw Y up by bandTop so the band canvas shows the
202
+ // correct slice of the parent fill/border for this band's Y range. bandTop=0
203
+ // (the main-loop caller) is a no-op shift. canvasY already carries the paint
204
+ // rect's display-space origin; bandTop further offsets within the rect.
205
+ int16_t localY = parentDrawY - canvasY - bandTop;
195
206
 
196
207
  // Parent fill color: blended toward the parent's backdrop when the parent is
197
208
  // translucent (matching the main NODE_FILL draw, which uses fillBg). Without
@@ -222,12 +233,17 @@ static inline void ui_seed_paint_canvas_for_node(uint16_t nodeIdx, CuttlefishCan
222
233
 
223
234
  static inline uint8_t ui_repair_current_node_paint_with_parent(uint16_t nodeIdx, UIRect* r) {
224
235
  if (!r || r->w <= 0 || r->h <= 0) return 0;
225
- if ((uint32_t)r->w * (uint32_t)r->h > UI_MAX_BUFFERED_PAINT_PIXELS) return 0;
236
+ if (static_cast<uint32_t>(r->w) * static_cast<uint32_t>(r->h) > UI_MAX_BUFFERED_PAINT_PIXELS) return 0;
226
237
  CuttlefishCanvas16* repairCanvas = ui_get_repair_canvas(r->w, r->h);
227
238
  if (!repairCanvas) return 0;
228
- ui_seed_paint_canvas_for_node(nodeIdx, repairCanvas, r->x, r->y);
229
- ui_display_use_default_target();
239
+ ui_seed_paint_canvas_for_node(nodeIdx, repairCanvas, r->x, r->y, 0);
240
+ // During a retained-framebuffer tick, publish the repair into the RAM
241
+ // composition surface. Sending it to the live panel here creates the visible
242
+ // erase/redraw flash seen on animated transforms.
243
+ if (__ui_fb) ui_display_set_target((CuttlefishDisplayTarget*)__ui_fb);
244
+ else ui_display_use_default_target();
230
245
  ui_push_canvas_rect(repairCanvas, r->x, r->y, r->w, r->h);
246
+ ui_display_use_default_target();
231
247
  return 1;
232
248
  }
233
249
 
@@ -375,7 +391,8 @@ static inline void ui_clear_node_paint_rect(uint16_t nodeIdx, const UIRect* pain
375
391
  if (nodeIdx >= __ui_node_count || !paintRect || paintRect->w <= 0 || paintRect->h <= 0) return;
376
392
  int16_t scrollParent = ui_scroll_ancestor_for_node(nodeIdx);
377
393
  UIRect r = *paintRect;
378
- ui_display_use_default_target();
394
+ if (__ui_fb) ui_display_set_target((CuttlefishDisplayTarget*)__ui_fb);
395
+ else ui_display_use_default_target();
379
396
  if (scrollParent >= 0) {
380
397
  UIRect clip = {
381
398
  __ui_nodes[scrollParent].box.x,
@@ -439,7 +456,7 @@ static inline uint8_t ui_try_repair_geometry_fill(uint16_t nodeIdx, const UIRect
439
456
  int16_t y0 = oldRect->y < newRect.y ? oldRect->y : newRect.y;
440
457
  int16_t x1 = oldRect->x + oldRect->w > newRect.x + newRect.w ? oldRect->x + oldRect->w : newRect.x + newRect.w;
441
458
  int16_t y1 = oldRect->y + oldRect->h > newRect.y + newRect.h ? oldRect->y + oldRect->h : newRect.y + newRect.h;
442
- UIRect repair = { x0, y0, (int16_t)(x1 - x0), (int16_t)(y1 - y0) };
459
+ UIRect repair = { x0, y0, static_cast<int16_t>(x1 - x0), static_cast<int16_t>(y1 - y0) };
443
460
  int16_t scrollParent = ui_scroll_ancestor_for_node(nodeIdx);
444
461
  if (scrollParent >= 0) {
445
462
  UIRect clip = {
@@ -456,11 +473,11 @@ static inline uint8_t ui_try_repair_geometry_fill(uint16_t nodeIdx, const UIRect
456
473
  return 0;
457
474
  }
458
475
  if (repair.w <= 0 || repair.h <= 0) return 0;
459
- if ((uint32_t)repair.w * (uint32_t)repair.h > UI_MAX_BUFFERED_PAINT_PIXELS) return 0;
476
+ if (static_cast<uint32_t>(repair.w) * static_cast<uint32_t>(repair.h) > UI_MAX_BUFFERED_PAINT_PIXELS) return 0;
460
477
  CuttlefishCanvas16* repairCanvas = ui_get_repair_canvas(repair.w, repair.h);
461
478
  if (!repairCanvas) return 0;
462
479
 
463
- ui_seed_paint_canvas_for_node(nodeIdx, repairCanvas, repair.x, repair.y);
480
+ ui_seed_paint_canvas_for_node(nodeIdx, repairCanvas, repair.x, repair.y, 0);
464
481
  CuttlefishDisplayTarget* previousGfx = ui_display_get_target();
465
482
  ui_display_set_target(repairCanvas);
466
483
  UI_COLOR_T fillBg = __ui_nodes[nodeIdx].bg;
@@ -473,8 +490,12 @@ static inline uint8_t ui_try_repair_geometry_fill(uint16_t nodeIdx, const UIRect
473
490
  int16_t fillH = ui_rotated_face_h(nodeIdx, __ui_nodes[nodeIdx].box.w, __ui_nodes[nodeIdx].box.h);
474
491
  ui_display_fill_rect(drawX, drawY, fillW, fillH, fillBg);
475
492
  ui_display_set_target(previousGfx);
476
- ui_display_use_default_target();
493
+ // Keep geometry repair off the physical panel when a retained framebuffer is
494
+ // active; the final dirty-bounds publish will send the old+new union once.
495
+ if (__ui_fb) ui_display_set_target((CuttlefishDisplayTarget*)__ui_fb);
496
+ else ui_display_use_default_target();
477
497
  ui_push_canvas_rect(repairCanvas, repair.x, repair.y, repair.w, repair.h);
498
+ ui_display_use_default_target();
478
499
  if (scrollParent >= 0) ui_invalidate_scroll_canvas_for_node(nodeIdx);
479
500
  ui_mark_overlapping_higher_layers_dirty_for_rect(nodeIdx, &repair);
480
501
  return 1;
@@ -530,7 +551,7 @@ static inline void ui_set_visible(uint16_t nodeIdx, uint8_t visible) {
530
551
  ui_clear_subtree_current_paint(nodeIdx);
531
552
  int16_t end = __ui_nodes[nodeIdx].subtreeEnd;
532
553
  if (end > __ui_node_count) end = __ui_node_count;
533
- for (int16_t c = end - 1; c >= (int16_t)nodeIdx; c--) {
554
+ for (int16_t c = end - 1; c >= static_cast<int16_t>(nodeIdx); c--) {
534
555
  if (__ui_nodes[c].screenId != __ui_active_screen) continue;
535
556
  __ui_nodes[c].dirty = 0;
536
557
  }
@@ -9,15 +9,15 @@ export function emitScrollPhysics() {
9
9
  // 'none' tier compiles drag scroll out entirely.
10
10
 
11
11
  static inline int16_t ui_scroll_scale_dy(int16_t dy) {
12
- int32_t scaled = (int32_t)dy * (int32_t)UI_SCROLL_DRAG_SCALE_X10;
13
- return (int16_t)(scaled >= 0 ? (scaled + 5) / 10 : (scaled - 5) / 10);
12
+ int32_t scaled = static_cast<int32_t>(dy) * static_cast<int32_t>(UI_SCROLL_DRAG_SCALE_X10);
13
+ return static_cast<int16_t>(scaled >= 0 ? (scaled + 5) / 10 : (scaled - 5) / 10);
14
14
  }
15
15
 
16
16
  static inline int16_t ui_scroll_smooth_dy(int16_t dy) {
17
17
  #if UI_SCROLL_INPUT_TIER_CAPACITIVE
18
18
  return ui_scroll_scale_dy(dy);
19
19
  #elif UI_SCROLL_INPUT_TIER_RESISTIVE
20
- int16_t db = (int16_t)UI_SCROLL_DEADBAND_PX;
20
+ int16_t db = static_cast<int16_t>(UI_SCROLL_DEADBAND_PX);
21
21
  if (dy >= -db && dy <= db) {
22
22
  // Deadband: kill per-sample jitter around zero. Steady drag (|dy|>db) below
23
23
  // passes through unchanged, so steady-state is 1:1 (spec Q1).
@@ -44,11 +44,11 @@ static inline int16_t ui_scroll_max(int16_t node) {
44
44
  // r = maxOverscroll * d / (d + stiffness). stiffness held as X10 fixed-point.
45
45
  static inline int16_t ui_scroll_overscroll_for(int16_t d) {
46
46
  if (d <= 0) return 0;
47
- int16_t maxOv = (int16_t)UI_SCROLL_MAX_OVERSCROLL;
48
- int16_t stiffX10 = (int16_t)UI_SCROLL_STIFFNESS_X10;
47
+ int16_t maxOv = static_cast<int16_t>(UI_SCROLL_MAX_OVERSCROLL);
48
+ int16_t stiffX10 = static_cast<int16_t>(UI_SCROLL_STIFFNESS_X10);
49
49
  if (stiffX10 <= 0) stiffX10 = 1;
50
- int32_t r = ((int32_t)maxOv * (int32_t)d) / ((int32_t)d + (int32_t)stiffX10);
51
- return r > maxOv ? maxOv : (int16_t)r;
50
+ int32_t r = (static_cast<int32_t>(maxOv) * static_cast<int32_t>(d)) / (static_cast<int32_t>(d) + static_cast<int32_t>(stiffX10));
51
+ return r > maxOv ? maxOv : static_cast<int16_t>(r);
52
52
  }
53
53
 
54
54
  // Apply a smoothed drag delta to the owning scroll node. Returns 1 if the view
@@ -56,6 +56,7 @@ static inline int16_t ui_scroll_overscroll_for(int16_t d) {
56
56
  // itself never leaves [0, maxScroll] so the committed position stays valid.
57
57
  static inline uint8_t ui_apply_scroll_delta(int16_t node, int16_t dy) {
58
58
  if (node < 0 || dy == 0) return 0;
59
+ if (__ui_scroll_render_locked && __ui_scroll_render_locked[node]) return 0;
59
60
  int16_t sy = __ui_nodes[node].scrollY;
60
61
  int16_t maxS = ui_scroll_max(node);
61
62
  int16_t nextY = sy - dy;
@@ -88,7 +89,7 @@ static inline uint8_t ui_apply_scroll_delta(int16_t node, int16_t dy) {
88
89
  }
89
90
  __ui_nodes[node].overscrollPx = nextOv;
90
91
  uint8_t changed = (__ui_nodes[node].scrollY != sy) || (nextOv != prevOv);
91
- if (changed) ui_mark_scroll_view_dirty((uint16_t)node);
92
+ if (changed) ui_mark_scroll_view_dirty(static_cast<uint16_t>(node));
92
93
  return changed;
93
94
  }
94
95
 
@@ -106,7 +107,7 @@ static inline uint8_t ui_scroll_release(int16_t node) {
106
107
  } else {
107
108
  int16_t sy = __ui_nodes[node].scrollY;
108
109
  int16_t maxS = ui_scroll_max(node);
109
- int16_t snap = (int16_t)UI_SCROLL_EDGE_SNAP_PX;
110
+ int16_t snap = static_cast<int16_t>(UI_SCROLL_EDGE_SNAP_PX);
110
111
  if (sy > 0 && sy <= snap) {
111
112
  __ui_nodes[node].settling = 1;
112
113
  __ui_settle_from_scrollY = sy; // positive → snap toward 0
@@ -126,27 +127,27 @@ static inline uint8_t ui_scroll_release(int16_t node) {
126
127
 
127
128
  // Advance the settle animation for a node (called from ui_tick). Ease-out over
128
129
  // UI_SCROLL_SETTLE_MS, terminating at the boundary. Bounded — always ends.
129
- static inline void ui_scroll_advance_settle(uint8_t node, uint16_t deltaMs) {
130
+ static inline void ui_scroll_advance_settle(uint16_t node, uint16_t deltaMs) {
130
131
  (void)deltaMs;
131
132
  if (node >= __ui_node_count || !__ui_nodes[node].settling) return;
132
133
  uint32_t elapsed = millis() - __ui_settle_start_ms;
133
- uint16_t dur = (uint16_t)UI_SCROLL_SETTLE_MS;
134
+ uint16_t dur = static_cast<uint16_t>(UI_SCROLL_SETTLE_MS);
134
135
  // ease-out: k = 1 - (1 - t)^2, t in [0,1]
135
136
  uint32_t t = elapsed >= dur ? 100 : (elapsed * 100) / dur;
136
137
  uint32_t k = 100 - ((100 - t) * (100 - t)) / 100;
137
138
  if (__ui_nodes[node].overscrollPx != 0) {
138
139
  int16_t from = __ui_settle_from_overscroll;
139
- __ui_nodes[node].overscrollPx = (int16_t)(from - (int32_t)(from * k) / 100);
140
+ __ui_nodes[node].overscrollPx = static_cast<int16_t>(from - static_cast<int32_t>(from * k) / 100);
140
141
  if (t >= 100) __ui_nodes[node].overscrollPx = 0;
141
142
  } else if (__ui_settle_from_scrollY != 0) {
142
143
  int16_t from = __ui_settle_from_scrollY; // +toward 0, -toward max
143
144
  int16_t maxS = ui_scroll_max(node);
144
145
  if (from > 0) {
145
- __ui_nodes[node].scrollY = (int16_t)(from - (int32_t)(from * k) / 100);
146
+ __ui_nodes[node].scrollY = static_cast<int16_t>(from - static_cast<int32_t>(from * k) / 100);
146
147
  if (t >= 100) __ui_nodes[node].scrollY = 0;
147
148
  } else { // from < 0: snap toward maxS
148
149
  int16_t target = maxS;
149
- __ui_nodes[node].scrollY = target + (int16_t)((int32_t)from * (100 - k) / 100);
150
+ __ui_nodes[node].scrollY = target + static_cast<int16_t>(static_cast<int32_t>(from) * (100 - k) / 100);
150
151
  if (t >= 100) __ui_nodes[node].scrollY = target;
151
152
  }
152
153
  }
@@ -17,14 +17,24 @@ static int16_t __ui_scroll_node = -1; // owning scroll container (int
17
17
  // frozen-but-not-torn rather than allowed to scroll with unclipped children.
18
18
  // Sized at runtime via __ui_node_count; defaults to all-zero (lock until proven).
19
19
  static uint8_t* __ui_scroll_canvas_ok = nullptr;
20
- // One-shot scroll memory warnings (indexed by node).
21
- static uint8_t* __ui_scroll_mem_warned = nullptr;
22
20
  // Precomputed draw order (lower z-index first, then source index). Built once in
23
21
  // ui_init; zIndex is static after mount so this stays valid for the app lifetime.
24
22
  static uint16_t* __ui_draw_order = nullptr;
25
- // Non-virtualized scroll-container node indices (built once in ui_init).
23
+ // Scroll-container node indices (built once in ui_init). Includes lists for
24
+ // hit-testing; the generic scroll compositor filters virtualized lists out.
26
25
  static uint16_t* __ui_scroll_owners = nullptr;
27
26
  static uint16_t __ui_scroll_owner_count = 0;
27
+ // Set when neither a viewport nor a band canvas can be allocated. The current
28
+ // pixels are retained and future drag deltas are rejected rather than exposing
29
+ // a direct per-primitive SPI fallback (which tears).
30
+ static uint8_t* __ui_scroll_render_locked = nullptr;
31
+ struct UIScrollPaintCandidate {
32
+ uint16_t node;
33
+ int16_t screenY;
34
+ int16_t faceH;
35
+ };
36
+ static UIScrollPaintCandidate* __ui_scroll_candidates = nullptr;
37
+ static uint16_t __ui_scroll_candidate_count = 0;
28
38
  // First NODE_FILL on the active screen (for framebuffer bg seed).
29
39
  static uint16_t __ui_active_screen_bg_node = 0xFFFF;
30
40
  static uint32_t __ui_settle_start_ms = 0; // when the active settle animation began
@@ -112,24 +122,24 @@ static inline uint8_t ui_ease_lerp_k(uint8_t timing, uint8_t k) {
112
122
  // converges. Products of four /1000 values are /1e12; t³ is /1e9 (×1000 to align).
113
123
  // int64 accumulation avoids overflow (3e12 > INT32_MAX). Identical algorithm +
114
124
  // control points to easeCurveLerpK in model.ts — preview and device must agree.
115
- int32_t targetX = (int32_t)k * 10; // input on the /1000 x axis
125
+ int32_t targetX = static_cast<int32_t>(k) * 10; // input on the /1000 x axis
116
126
  int32_t lo = 0, hi = 1000;
117
127
  for (uint8_t i = 0; i < 14; i++) {
118
128
  int32_t t = (lo + hi) >> 1;
119
129
  int32_t mt = 1000 - t;
120
- int64_t termX1 = (int64_t)3 * mt * mt * t * x1; // /1e12
121
- int64_t termX2 = (int64_t)3 * mt * t * t * x2; // /1e12
122
- int64_t termX3 = (int64_t)t * t * t * 1000; // /1e12 (t³ was /1e9)
123
- int32_t X = (int32_t)((termX1 + termX2 + termX3) / 1000000000LL); // back to /1000
130
+ int64_t termX1 = static_cast<int64_t>(3) * mt * mt * t * x1; // /1e12
131
+ int64_t termX2 = static_cast<int64_t>(3) * mt * t * t * x2; // /1e12
132
+ int64_t termX3 = static_cast<int64_t>(t) * t * t * 1000; // /1e12 (t³ was /1e9)
133
+ int32_t X = static_cast<int32_t>((termX1 + termX2 + termX3) / 1000000000LL); // back to /1000
124
134
  if (X < targetX) lo = t; else hi = t;
125
135
  }
126
136
  int32_t t = (lo + hi) >> 1;
127
137
  int32_t mt = 1000 - t;
128
- int64_t termY1 = (int64_t)3 * mt * mt * t * y1;
129
- int64_t termY2 = (int64_t)3 * mt * t * t * y2;
130
- int64_t termY3 = (int64_t)t * t * t * 1000;
131
- int32_t Y = (int32_t)((termY1 + termY2 + termY3) / 1000000000LL); // /1000
132
- return (uint8_t)(Y / 10); // back to /100
138
+ int64_t termY1 = static_cast<int64_t>(3) * mt * mt * t * y1;
139
+ int64_t termY2 = static_cast<int64_t>(3) * mt * t * t * y2;
140
+ int64_t termY3 = static_cast<int64_t>(t) * t * t * 1000;
141
+ int32_t Y = static_cast<int32_t>((termY1 + termY2 + termY3) / 1000000000LL); // /1000
142
+ return static_cast<uint8_t>(Y / 10); // back to /100
133
143
  }
134
144
  extern const UIKeyframeSet __ui_keyframe_sets[];
135
145
  extern const uint16_t __ui_keyframe_set_count;
@@ -3,7 +3,7 @@
3
3
  // See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
4
4
  export function emitStructs() {
5
5
  return `
6
- enum UINodeKind { NODE_FILL, NODE_TEXT, NODE_BUTTON, NODE_CHECK, NODE_RADIO, NODE_PROGRESS, NODE_RANGE, NODE_INPUT, NODE_IMG, NODE_LIST, NODE_CANVAS };
6
+ enum UINodeKind { NODE_FILL, NODE_TEXT, NODE_BUTTON, NODE_CHECK, NODE_RADIO, NODE_PROGRESS, NODE_RANGE, NODE_INPUT, NODE_IMG, NODE_LIST, NODE_CANVAS, NODE_SELECT };
7
7
  enum UIProperty { PROP_BG, PROP_FG, PROP_TEXT, PROP_VISIBLE, PROP_BORDER_COLOR, PROP_VALUE };
8
8
 
9
9
  struct UIRect { int16_t x, y, w, h; };
@@ -160,7 +160,7 @@ struct UITransition {
160
160
  uint32_t pressedTarget;
161
161
  uint32_t baseTarget;
162
162
  // runtime
163
- uint16_t elapsed;
163
+ uint32_t elapsed;
164
164
  uint32_t prevValue;
165
165
  uint32_t targetValue;
166
166
  uint8_t active;
@@ -170,6 +170,8 @@ struct UIBinding {
170
170
  UIProperty prop;
171
171
  uint32_t (*fn)(void); // for color/numeric bindings
172
172
  void (*textFn)(char* buf, uint8_t size); // for text bindings (PROP_TEXT): fills buf
173
+ uint32_t lastValue; // numeric binding cache; avoids redundant dirty work
174
+ uint8_t initialized; // first evaluation establishes the baseline
173
175
  };
174
176
 
175
177
  // Color lerp for transitions (rgb565). For mono, this collapses to a snap.
@@ -181,10 +183,10 @@ static inline uint16_t lerp_color(uint16_t a, uint16_t b, uint8_t k100) {
181
183
  uint8_t br5 = (b >> 11) & 0x1f, bg6 = (b >> 5) & 0x3f, bb5 = b & 0x1f;
182
184
  uint16_t ar8 = (ar5 << 3) | (ar5 >> 2), ag8 = (ag6 << 2) | (ag6 >> 4), ab8 = (ab5 << 3) | (ab5 >> 2);
183
185
  uint16_t br8 = (br5 << 3) | (br5 >> 2), bg8 = (bg6 << 2) | (bg6 >> 4), bb8 = (bb5 << 3) | (bb5 >> 2);
184
- int16_t r = (int16_t)(ar8 + (int16_t)((br8 - ar8) * k100 / 100));
185
- int16_t g = (int16_t)(ag8 + (int16_t)((bg8 - ag8) * k100 / 100));
186
- int16_t bl = (int16_t)(ab8 + (int16_t)((bb8 - ab8) * k100 / 100));
187
- return ((uint16_t)((r >> 3) & 0x1f) << 11) | ((uint16_t)((g >> 2) & 0x3f) << 5) | (uint16_t)((bl >> 3) & 0x1f);
186
+ int16_t r = static_cast<int16_t>(ar8 + static_cast<int16_t>((br8 - ar8) * k100 / 100));
187
+ int16_t g = static_cast<int16_t>(ag8 + static_cast<int16_t>((bg8 - ag8) * k100 / 100));
188
+ int16_t bl = static_cast<int16_t>(ab8 + static_cast<int16_t>((bb8 - ab8) * k100 / 100));
189
+ return (static_cast<uint16_t>((r >> 3) & 0x1f) << 11) | (static_cast<uint16_t>((g >> 2) & 0x3f) << 5) | static_cast<uint16_t>((bl >> 3) & 0x1f);
188
190
  }
189
191
 
190
192
  // RGB888 lerp — for transitions on RGB888/RGB666 targets (Phase 2+). Unused in
@@ -194,10 +196,10 @@ static inline uint32_t lerp_color_888(uint32_t a, uint32_t b, uint8_t k100) {
194
196
  if (k100 >= 100) return b;
195
197
  uint8_t ar = (a >> 16) & 0xff, ag = (a >> 8) & 0xff, ab = a & 0xff;
196
198
  uint8_t br = (b >> 16) & 0xff, bg = (b >> 8) & 0xff, bb = b & 0xff;
197
- int16_t r = ar + (int16_t)(((int16_t)br - (int16_t)ar) * k100 / 100);
198
- int16_t g = ag + (int16_t)(((int16_t)bg - (int16_t)ag) * k100 / 100);
199
- int16_t bl = ab + (int16_t)(((int16_t)bb - (int16_t)ab) * k100 / 100);
200
- return ((uint32_t)(r & 0xff) << 16) | ((uint32_t)(g & 0xff) << 8) | (uint32_t)(bl & 0xff);
199
+ int16_t r = ar + static_cast<int16_t>((static_cast<int16_t>(br) - static_cast<int16_t>(ar)) * k100 / 100);
200
+ int16_t g = ag + static_cast<int16_t>((static_cast<int16_t>(bg) - static_cast<int16_t>(ag)) * k100 / 100);
201
+ int16_t bl = ab + static_cast<int16_t>((static_cast<int16_t>(bb) - static_cast<int16_t>(ab)) * k100 / 100);
202
+ return (static_cast<uint32_t>(r & 0xff) << 16) | (static_cast<uint32_t>(g & 0xff) << 8) | static_cast<uint32_t>(bl & 0xff);
201
203
  }
202
204
 
203
205
  // Declared by the lowering output (the tables). Matches the mutable (non-const)