@typecad/ui 1.0.0-alpha.12 → 1.0.0-alpha.13

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 (113) hide show
  1. package/assets/fonts/dejavu/DejaVuSans-Bold.ttf +0 -0
  2. package/assets/fonts/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
  3. package/assets/fonts/dejavu/DejaVuSans-Oblique.ttf +0 -0
  4. package/assets/fonts/dejavu/DejaVuSans.ttf +0 -0
  5. package/assets/fonts/dejavu/DejaVuSansMono-Bold.ttf +0 -0
  6. package/assets/fonts/dejavu/DejaVuSansMono.ttf +0 -0
  7. package/assets/fonts/dejavu/LICENSE +187 -0
  8. package/assets/fonts/dejavu/README.md +49 -0
  9. package/dist/engine-index.js +2 -0
  10. package/dist/preview/build-program.d.ts +7 -1
  11. package/dist/preview/build-program.js +94 -15
  12. package/dist/preview/host-gfx.d.ts +3 -0
  13. package/dist/preview/host-gfx.js +30 -3
  14. package/dist/preview/host-ui-runtime.d.ts +115 -0
  15. package/dist/preview/host-ui-runtime.js +928 -46
  16. package/dist/ui-engine/block-layout.js +25 -11
  17. package/dist/ui-engine/color.d.ts +6 -0
  18. package/dist/ui-engine/color.js +80 -2
  19. package/dist/ui-engine/compat-report.d.ts +8 -0
  20. package/dist/ui-engine/compat-report.js +124 -0
  21. package/dist/ui-engine/css-imports.d.ts +4 -0
  22. package/dist/ui-engine/css-imports.js +70 -0
  23. package/dist/ui-engine/css-parser.js +58 -14
  24. package/dist/ui-engine/default-font.d.ts +25 -0
  25. package/dist/ui-engine/default-font.js +80 -0
  26. package/dist/ui-engine/font-assets.d.ts +2 -2
  27. package/dist/ui-engine/font-assets.js +22 -5
  28. package/dist/ui-engine/html-parser.d.ts +4 -0
  29. package/dist/ui-engine/html-parser.js +241 -26
  30. package/dist/ui-engine/image-assets.d.ts +8 -1
  31. package/dist/ui-engine/image-assets.js +35 -2
  32. package/dist/ui-engine/image-decode.d.ts +22 -0
  33. package/dist/ui-engine/image-decode.js +194 -0
  34. package/dist/ui-engine/inline-parser.js +1 -1
  35. package/dist/ui-engine/layout-engine.d.ts +19 -0
  36. package/dist/ui-engine/layout-engine.js +59 -3
  37. package/dist/ui-engine/model.d.ts +18 -0
  38. package/dist/ui-engine/model.js +75 -7
  39. package/dist/ui-engine/runtime-header/blend-bodies.js +29 -9
  40. package/dist/ui-engine/runtime-header/canvas-helpers.js +3 -0
  41. package/dist/ui-engine/runtime-header/canvas-scrollbar.js +115 -111
  42. package/dist/ui-engine/runtime-header/cuttlefish-gfx.js +21 -0
  43. package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +182 -179
  44. package/dist/ui-engine/runtime-header/forward-decls.js +215 -167
  45. package/dist/ui-engine/runtime-header/init-press-input.js +138 -135
  46. package/dist/ui-engine/runtime-header/keyboard.js +682 -380
  47. package/dist/ui-engine/runtime-header/node-draw-body.js +1656 -1386
  48. package/dist/ui-engine/runtime-header/paint-order-coords.js +259 -214
  49. package/dist/ui-engine/runtime-header/paint-rects-repair.js +736 -568
  50. package/dist/ui-engine/runtime-header/scroll-physics.js +25 -1
  51. package/dist/ui-engine/runtime-header/state-bindings-nav.js +9 -0
  52. package/dist/ui-engine/runtime-header/structs.js +228 -216
  53. package/dist/ui-engine/runtime-header/text-rendering.js +888 -770
  54. package/dist/ui-engine/runtime-header/tick/bindings-phase.js +118 -116
  55. package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +897 -633
  56. package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +24 -25
  57. package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +78 -23
  58. package/dist/ui-engine/runtime-header/types-defines.js +110 -93
  59. package/dist/ui-engine/shadcn-kit.d.ts +1 -0
  60. package/dist/ui-engine/shadcn-kit.js +502 -0
  61. package/dist/ui-engine/style-resolver.d.ts +4 -0
  62. package/dist/ui-engine/style-resolver.js +58 -6
  63. package/dist/ui-engine/transpile-ui.js +12 -5
  64. package/dist/ui-engine/ua-stylesheet.d.ts +22 -1
  65. package/dist/ui-engine/ua-stylesheet.js +119 -32
  66. package/dist/ui-engine/ui-lowering.js +37 -3
  67. package/dist/ui-engine/ui-registry.js +32 -4
  68. package/dist/ui-engine/yoga-layout.js +57 -52
  69. package/package.json +9 -3
  70. package/src/engine-index.ts +53 -51
  71. package/src/preview/build-program.ts +810 -734
  72. package/src/preview/host-gfx.ts +30 -3
  73. package/src/preview/host-ui-runtime.ts +4133 -3289
  74. package/src/ui-engine/block-layout.ts +24 -11
  75. package/src/ui-engine/color.ts +77 -2
  76. package/src/ui-engine/compat-report.ts +139 -0
  77. package/src/ui-engine/css-imports.ts +69 -0
  78. package/src/ui-engine/css-parser.ts +64 -15
  79. package/src/ui-engine/default-font.ts +95 -0
  80. package/src/ui-engine/font-assets.ts +545 -525
  81. package/src/ui-engine/html-parser.ts +615 -397
  82. package/src/ui-engine/image-assets.ts +37 -2
  83. package/src/ui-engine/image-decode.ts +244 -0
  84. package/src/ui-engine/inline-parser.ts +1 -1
  85. package/src/ui-engine/layout-engine.ts +61 -3
  86. package/src/ui-engine/model.ts +1317 -1230
  87. package/src/ui-engine/runtime-header/blend-bodies.ts +29 -9
  88. package/src/ui-engine/runtime-header/canvas-helpers.ts +3 -0
  89. package/src/ui-engine/runtime-header/canvas-scrollbar.ts +121 -117
  90. package/src/ui-engine/runtime-header/cuttlefish-gfx.ts +21 -0
  91. package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +188 -185
  92. package/src/ui-engine/runtime-header/forward-decls.ts +227 -179
  93. package/src/ui-engine/runtime-header/init-press-input.ts +144 -141
  94. package/src/ui-engine/runtime-header/keyboard.ts +689 -386
  95. package/src/ui-engine/runtime-header/node-draw-body.ts +1681 -1411
  96. package/src/ui-engine/runtime-header/paint-order-coords.ts +265 -220
  97. package/src/ui-engine/runtime-header/paint-rects-repair.ts +742 -574
  98. package/src/ui-engine/runtime-header/scroll-physics.ts +25 -1
  99. package/src/ui-engine/runtime-header/state-bindings-nav.ts +9 -0
  100. package/src/ui-engine/runtime-header/structs.ts +234 -222
  101. package/src/ui-engine/runtime-header/text-rendering.ts +894 -776
  102. package/src/ui-engine/runtime-header/tick/bindings-phase.ts +124 -122
  103. package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +902 -638
  104. package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +30 -31
  105. package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +78 -23
  106. package/src/ui-engine/runtime-header/types-defines.ts +116 -99
  107. package/src/ui-engine/shadcn-kit.ts +503 -0
  108. package/src/ui-engine/style-resolver.ts +471 -416
  109. package/src/ui-engine/transpile-ui.ts +12 -5
  110. package/src/ui-engine/ua-stylesheet.ts +137 -31
  111. package/src/ui-engine/ui-lowering.ts +486 -452
  112. package/src/ui-engine/ui-registry.ts +556 -524
  113. package/src/ui-engine/yoga-layout.ts +307 -309
@@ -2,573 +2,741 @@
2
2
  // Paint rects, overlapping layers dirty, buffer decisions, seed/repair, scroll-clip tests, clipped primitives, clear/repair, set_visible.
3
3
  // See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
4
4
  export function emitPaintRectsRepair() {
5
- return `
6
- static inline void ui_node_paint_rect(uint16_t nodeIdx, int16_t baseX, int16_t baseY, int16_t drawX, int16_t drawY, uint16_t textW, uint16_t textH, UIRect* out) {
7
- int16_t shadowL, shadowT, shadowR, shadowB;
8
- ui_shadow_extents(nodeIdx, &shadowL, &shadowT, &shadowR, &shadowB);
9
-
10
- int16_t faceW = __ui_nodes[nodeIdx].box.w;
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 || __ui_nodes[nodeIdx].kind == NODE_SELECT) {
13
- if (__ui_nodes[nodeIdx].lastTextWidth > faceW) faceW = __ui_nodes[nodeIdx].lastTextWidth;
14
- if (__ui_nodes[nodeIdx].lastTextHeight > faceH) faceH = __ui_nodes[nodeIdx].lastTextHeight;
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
- }
18
- int16_t unrotatedFaceW = faceW;
19
- int16_t unrotatedFaceH = faceH;
20
- faceW = ui_rotated_face_w(nodeIdx, unrotatedFaceW, unrotatedFaceH);
21
- faceH = ui_rotated_face_h(nodeIdx, unrotatedFaceW, unrotatedFaceH);
22
-
23
- int16_t shadowX0 = baseX - shadowL;
24
- int16_t shadowY0 = baseY - shadowT;
25
- int16_t shadowX1 = baseX + faceW + shadowR;
26
- int16_t shadowY1 = baseY + faceH + shadowB;
27
- int16_t faceX0 = drawX;
28
- int16_t faceY0 = drawY;
29
- int16_t faceX1 = drawX + faceW;
30
- int16_t faceY1 = drawY + faceH;
31
- int16_t x0 = shadowX0 < faceX0 ? shadowX0 : faceX0;
32
- int16_t y0 = shadowY0 < faceY0 ? shadowY0 : faceY0;
33
- int16_t x1 = shadowX1 > faceX1 ? shadowX1 : faceX1;
34
- int16_t y1 = shadowY1 > faceY1 ? shadowY1 : faceY1;
35
- if (__ui_nodes[nodeIdx].outlineStyle != 0 && __ui_nodes[nodeIdx].outlineWidth > 0) {
36
- int16_t o = __ui_nodes[nodeIdx].outlineWidth;
37
- ui_expand_rect(&x0, &y0, &x1, &y1, drawX - o, drawY - o, drawX + faceW + o, drawY + faceH + o);
38
- }
39
- out->x = x0;
40
- out->y = y0;
41
- out->w = x1 - x0;
42
- out->h = y1 - y0;
43
- }
44
-
45
- static inline void ui_node_current_paint_rect(uint16_t nodeIdx, UIRect* out) {
46
- uint16_t tw = 0;
47
- uint16_t th = 0;
48
- uint16_t textMaxW = ui_node_text_max_width(nodeIdx);
49
- ui_node_text_layout_metrics(nodeIdx, textMaxW, &tw, &th);
50
- if (__ui_nodes[nodeIdx].kind == NODE_CHECK || __ui_nodes[nodeIdx].kind == NODE_RADIO) {
51
- tw += 22;
52
- if (th < 16) th = 16;
53
- }
54
- ui_node_paint_rect(nodeIdx,
55
- ui_base_draw_x_for_node(nodeIdx),
56
- ui_base_draw_y_for_node(nodeIdx),
57
- ui_draw_x_for_node(nodeIdx),
58
- ui_draw_y_for_node(nodeIdx),
59
- tw,
60
- th,
61
- out);
62
- }
63
-
64
- static inline uint8_t ui_subtree_current_paint_rect(uint16_t nodeIdx, UIRect* out) {
65
- if (nodeIdx >= __ui_node_count) return 0;
66
- int16_t end = __ui_nodes[nodeIdx].subtreeEnd;
67
- if (end > __ui_node_count) end = __ui_node_count;
68
- uint8_t hasRect = 0;
69
- int16_t x0 = 0;
70
- int16_t y0 = 0;
71
- int16_t x1 = 0;
72
- int16_t y1 = 0;
73
- for (uint16_t c = nodeIdx; c < end; c++) {
74
- if (__ui_nodes[c].screenId != __ui_active_screen) continue;
75
- if (!ui_is_effectively_visible(c)) continue;
76
- UIRect r;
77
- ui_node_current_paint_rect(c, &r);
78
- if (r.w <= 0 || r.h <= 0) continue;
79
- if (!hasRect) {
80
- x0 = r.x;
81
- y0 = r.y;
82
- x1 = r.x + r.w;
83
- y1 = r.y + r.h;
84
- hasRect = 1;
85
- } else {
86
- ui_expand_rect(&x0, &y0, &x1, &y1, r.x, r.y, r.x + r.w, r.y + r.h);
87
- }
88
- }
89
- if (!hasRect) return 0;
90
- out->x = x0;
91
- out->y = y0;
92
- out->w = x1 - x0;
93
- out->h = y1 - y0;
94
- return 1;
95
- }
96
-
97
- static inline void ui_mark_overlapping_higher_layers_dirty(uint16_t nodeIdx) {
98
- if (nodeIdx >= __ui_node_count) return;
99
- if (!ui_is_effectively_visible(nodeIdx)) return;
100
- if (__ui_nodes[nodeIdx].screenId != __ui_active_screen) return;
101
- UIRect r;
102
- ui_node_current_paint_rect(nodeIdx, &r);
103
- ui_mark_overlapping_higher_layers_dirty_for_rect(nodeIdx, &r);
104
- }
105
-
106
- static inline void ui_mark_overlapping_higher_layers_dirty_for_rect(uint16_t nodeIdx, const UIRect* r) {
107
- if (nodeIdx >= __ui_node_count || !r) return;
108
- if (!ui_is_effectively_visible(nodeIdx)) return;
109
- if (__ui_nodes[nodeIdx].screenId != __ui_active_screen) return;
110
- if (r->w <= 0 || r->h <= 0) return;
111
- for (uint16_t c = 0; c < __ui_node_count; c++) {
112
- if (c == nodeIdx) continue;
113
- if (__ui_nodes[c].dirty) continue;
114
- if (!ui_is_effectively_visible(c)) continue;
115
- if (__ui_nodes[c].screenId != __ui_active_screen) continue;
116
- if (!ui_node_draws_before(nodeIdx, c)) continue;
117
- UIRect cr;
118
- ui_node_current_paint_rect(c, &cr);
119
- if (cr.w <= 0 || cr.h <= 0) continue;
120
- if (ui_rects_intersect(r->x, r->y, r->w, r->h, cr.x, cr.y, cr.w, cr.h)) {
121
- __ui_nodes[c].dirty = 1;
122
- }
123
- }
124
- }
125
-
126
- static inline void ui_clear_press_offset_area(uint16_t nodeIdx, int16_t baseX, int16_t baseY, int16_t drawX, int16_t drawY, uint16_t textW, uint16_t textH) {
127
- if (__ui_nodes[nodeIdx].pressedOffsetX == 0 && __ui_nodes[nodeIdx].pressedOffsetY == 0) return;
128
- UIRect r;
129
- ui_node_paint_rect(nodeIdx, baseX, baseY, drawX, drawY, textW, textH, &r);
130
- int16_t x0 = r.x;
131
- int16_t y0 = r.y;
132
- int16_t x1 = r.x + r.w;
133
- int16_t y1 = r.y + r.h;
134
- if (ui_display_is_default_target() && ui_repair_current_node_paint_with_parent(nodeIdx, &r)) return;
135
- ui_display_fill_rect(x0, y0, x1 - x0, y1 - y0, ui_parent_clear_color(nodeIdx));
136
- }
137
-
138
- // Generated-font / AA text and images draw per-pixel when sent straight to SPI.
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.
147
- static inline uint8_t ui_pixel_heavy_node(uint16_t nodeIdx) {
148
- if (nodeIdx >= __ui_node_count) return 0;
149
- if (__ui_nodes[nodeIdx].kind == NODE_IMG) return 1;
150
- if (__ui_nodes[nodeIdx].kind == NODE_TEXT) return 1;
151
- if (__ui_nodes[nodeIdx].kind == NODE_SELECT) return 1;
152
- return 0;
153
- }
154
-
155
- static inline uint8_t ui_should_buffer_paint(uint16_t nodeIdx, int16_t w, int16_t h) {
156
- if (w <= 0 || h <= 0) return 0;
157
- if (__ui_nodes[nodeIdx].kind == NODE_LIST) return 0;
158
- // <canvas> elements batch via __ui_node_canvas when drawing to the display.
159
- if (__ui_nodes[nodeIdx].kind == NODE_CANVAS) return 0;
160
- if (static_cast<uint32_t>(w) * static_cast<uint32_t>(h) > UI_MAX_BUFFERED_PAINT_PIXELS) return 0;
161
- if (ui_pixel_heavy_node(nodeIdx)) return 1;
162
- if (__ui_nodes[nodeIdx].kind == NODE_FILL &&
163
- __ui_nodes[nodeIdx].hasBg &&
164
- __ui_nodes[nodeIdx].gradientEnabled == 0 &&
165
- __ui_nodes[nodeIdx].borderRadius == 0 &&
166
- __ui_nodes[nodeIdx].borderStyle == 0 &&
167
- __ui_nodes[nodeIdx].outlineStyle == 0 &&
168
- __ui_nodes[nodeIdx].shadowCount == 0) return 0;
169
- if (__ui_nodes[nodeIdx].kind == NODE_FILL &&
170
- !__ui_nodes[nodeIdx].hasBg &&
171
- __ui_nodes[nodeIdx].borderStyle == 0 &&
172
- __ui_nodes[nodeIdx].outlineStyle == 0 &&
173
- __ui_nodes[nodeIdx].shadowCount == 0) return 0;
174
- return 1;
175
- }
176
-
177
- static inline void ui_seed_paint_canvas_for_node(uint16_t nodeIdx, CuttlefishCanvas16* canvas, int16_t canvasX, int16_t canvasY, int16_t bandTop) {
178
- if (!canvas || !display_canvasBuffer(canvas)) return;
179
- uint16_t p = __ui_nodes[nodeIdx].parent;
180
- if (p == UI_NO_PARENT || p >= __ui_node_count) {
181
- display_canvasFillScreen(canvas, __ui_nodes[nodeIdx].clearColor);
182
- return;
183
- }
184
-
185
- uint16_t parentDecorated =
186
- (__ui_nodes[p].hasBg && (__ui_nodes[p].borderRadius > 0 || __ui_nodes[p].gradientEnabled > 0)) ||
187
- __ui_nodes[p].borderStyle != 0 ||
188
- __ui_nodes[p].outlineStyle != 0;
189
- if (!parentDecorated) {
190
- display_canvasFillScreen(canvas, ui_parent_clear_color(nodeIdx));
191
- return;
192
- }
193
-
194
- display_canvasFillScreen(canvas, ui_parent_clear_color(p));
195
- int16_t parentDrawX = ui_draw_x_for_node(p);
196
- int16_t parentDrawY = ui_draw_y_for_node(p);
197
- int16_t origParentX = __ui_nodes[p].box.x;
198
- CuttlefishDisplayTarget* previousGfx = ui_display_get_target();
199
- ui_display_set_target(canvas);
200
- __ui_nodes[p].box.x = parentDrawX - canvasX;
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;
206
-
207
- // Parent fill color: blended toward the parent's backdrop when the parent is
208
- // translucent (matching the main NODE_FILL draw, which uses fillBg). Without
209
- // this, scroll repair seeds the canvas with the parent's RAW bg while the
210
- // main draw used the blended color → shearing on translucent nodes during scroll.
211
- UI_COLOR_T parentFillBg = __ui_nodes[p].bg;
212
- if (__ui_nodes[p].opacity < 100) {
213
- parentFillBg = ui_blend(__ui_nodes[p].bg, ui_parent_clear_color(p), __ui_nodes[p].opacity);
214
- }
215
-
216
- if (__ui_nodes[p].gradientEnabled > 0) {
217
- ui_draw_gradient_fill(p, localY);
218
- } else if (__ui_nodes[p].borderRadius > 0 && __ui_nodes[p].hasBg) {
219
- ui_display_fill_round_rect(__ui_nodes[p].box.x, localY, __ui_nodes[p].box.w, __ui_nodes[p].box.h,
220
- __ui_nodes[p].borderRadius, parentFillBg);
221
- } else if (__ui_nodes[p].hasBg) {
222
- ui_display_fill_rect(__ui_nodes[p].box.x, localY, __ui_nodes[p].box.w, __ui_nodes[p].box.h, parentFillBg);
223
- }
224
- if (__ui_nodes[p].borderStyle != 0) {
225
- UI_COLOR_T bColor = __ui_nodes[p].borderColor ? __ui_nodes[p].borderColor : __ui_nodes[p].fg;
226
- ui_draw_node_border(p, __ui_nodes[p].box.x, localY, bColor);
227
- }
228
- ui_draw_node_outline(p, __ui_nodes[p].box.x, localY);
229
-
230
- __ui_nodes[p].box.x = origParentX;
231
- ui_display_set_target(previousGfx);
232
- }
233
-
234
- static inline uint8_t ui_repair_current_node_paint_with_parent(uint16_t nodeIdx, UIRect* r) {
235
- if (!r || r->w <= 0 || r->h <= 0) return 0;
236
- if (static_cast<uint32_t>(r->w) * static_cast<uint32_t>(r->h) > UI_MAX_BUFFERED_PAINT_PIXELS) return 0;
237
- CuttlefishCanvas16* repairCanvas = ui_get_repair_canvas(r->w, r->h);
238
- if (!repairCanvas) return 0;
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();
245
- ui_push_canvas_rect(repairCanvas, r->x, r->y, r->w, r->h);
246
- ui_display_use_default_target();
247
- return 1;
248
- }
249
-
250
- static inline uint8_t ui_is_rect_clipped_by_scroll(uint16_t nodeIdx, int16_t drawX, int16_t drawY, int16_t drawW, int16_t drawH) {
251
- uint16_t p = __ui_nodes[nodeIdx].parent;
252
- while (p != UI_NO_PARENT && p < __ui_node_count) {
253
- if (__ui_nodes[p].scrollable) {
254
- if (drawX < __ui_nodes[p].box.x ||
255
- drawX + drawW > __ui_nodes[p].box.x + __ui_nodes[p].box.w ||
256
- drawY < __ui_nodes[p].box.y ||
257
- drawY + drawH > __ui_nodes[p].box.y + __ui_nodes[p].box.h) {
258
- return 1;
259
- }
260
- }
261
- p = __ui_nodes[p].parent;
262
- }
263
- return 0;
264
- }
265
-
266
- static inline uint8_t ui_is_clipped_by_scroll(uint16_t nodeIdx, int16_t drawX, int16_t drawY) {
267
- return ui_is_rect_clipped_by_scroll(nodeIdx, drawX, drawY, __ui_nodes[nodeIdx].box.w, __ui_nodes[nodeIdx].box.h);
268
- }
269
-
270
- // Point-in-viewport test for hit-testing. A tap point is tappable if it lies
271
- // within EVERY scrollable ancestor's viewport (logical AND, matching the
272
- // preview's intersected scroll clip). This differs from ui_is_clipped_by_scroll,
273
- // which tests the node's whole bounding box — a tall node (e.g. a wrapped
274
- // rich-text paragraph) can overflow below the fold yet have a tappable link in
275
- // its visible portion. Use this in the hit-test path; keep the whole-box check
276
- // for draw culling, where a partially-visible node still needs repainting.
277
- static inline uint8_t ui_is_point_clipped_by_scroll(uint16_t nodeIdx, int16_t px, int16_t py) {
278
- uint16_t p = __ui_nodes[nodeIdx].parent;
279
- while (p != UI_NO_PARENT && p < __ui_node_count) {
280
- if (__ui_nodes[p].scrollable) {
281
- if (px < __ui_nodes[p].box.x ||
282
- px >= __ui_nodes[p].box.x + __ui_nodes[p].box.w ||
283
- py < __ui_nodes[p].box.y ||
284
- py >= __ui_nodes[p].box.y + __ui_nodes[p].box.h) {
285
- return 1;
286
- }
287
- }
288
- p = __ui_nodes[p].parent;
289
- }
290
- return 0;
291
- }
292
-
293
- static inline uint8_t ui_clip_rect_to_rect(UIRect* r, const UIRect* clip) {
294
- int16_t x0 = r->x > clip->x ? r->x : clip->x;
295
- int16_t y0 = r->y > clip->y ? r->y : clip->y;
296
- int16_t x1 = r->x + r->w < clip->x + clip->w ? r->x + r->w : clip->x + clip->w;
297
- int16_t y1 = r->y + r->h < clip->y + clip->h ? r->y + r->h : clip->y + clip->h;
298
- if (x1 <= x0 || y1 <= y0) return 0;
299
- r->x = x0;
300
- r->y = y0;
301
- r->w = x1 - x0;
302
- r->h = y1 - y0;
303
- return 1;
304
- }
305
-
306
- static inline void ui_fill_rect_clipped(int16_t x, int16_t y, int16_t w, int16_t h, const UIRect* clip, UI_COLOR_T color) {
307
- UIRect r = { x, y, w, h };
308
- if (!ui_clip_rect_to_rect(&r, clip)) return;
309
- ui_display_fill_rect(r.x, r.y, r.w, r.h, color);
310
- }
311
-
312
- static inline void ui_hline_clipped(int16_t x, int16_t y, int16_t w, const UIRect* clip, UI_COLOR_T color) {
313
- if (w <= 0 || y < clip->y || y >= clip->y + clip->h) return;
314
- int16_t x0 = x > clip->x ? x : clip->x;
315
- int16_t x1 = x + w < clip->x + clip->w ? x + w : clip->x + clip->w;
316
- if (x1 <= x0) return;
317
- ui_display_draw_fast_hline(x0, y, x1 - x0, color);
318
- }
319
-
320
- static inline void ui_vline_clipped(int16_t x, int16_t y, int16_t h, const UIRect* clip, UI_COLOR_T color) {
321
- if (h <= 0 || x < clip->x || x >= clip->x + clip->w) return;
322
- int16_t y0 = y > clip->y ? y : clip->y;
323
- int16_t y1 = y + h < clip->y + clip->h ? y + h : clip->y + clip->h;
324
- if (y1 <= y0) return;
325
- ui_display_draw_fast_vline(x, y0, y1 - y0, color);
326
- }
327
-
328
- static inline void ui_draw_rect_outline_clipped(int16_t x, int16_t y, int16_t w, int16_t h, uint8_t style, uint8_t width, const UIRect* clip, UI_COLOR_T color) {
329
- if (style == 0 || width == 0 || w <= 0 || h <= 0) return;
330
- for (uint8_t b = 0; b < width; b++) {
331
- int16_t rx = x + b;
332
- int16_t ry = y + b;
333
- int16_t rw = w - 2 * b;
334
- int16_t rh = h - 2 * b;
335
- if (rw <= 0 || rh <= 0) return;
336
- if (style == 1) {
337
- ui_hline_clipped(rx, ry, rw, clip, color);
338
- ui_hline_clipped(rx, ry + rh - 1, rw, clip, color);
339
- ui_vline_clipped(rx, ry, rh, clip, color);
340
- ui_vline_clipped(rx + rw - 1, ry, rh, clip, color);
341
- } else {
342
- for (int16_t dx = 0; dx < rw; dx += 8) {
343
- int16_t seg = (dx + 4 <= rw) ? 4 : (rw - dx);
344
- ui_hline_clipped(rx + dx, ry, seg, clip, color);
345
- ui_hline_clipped(rx + dx, ry + rh - 1, seg, clip, color);
346
- }
347
- for (int16_t dy = 0; dy < rh; dy += 8) {
348
- int16_t seg = (dy + 4 <= rh) ? 4 : (rh - dy);
349
- ui_vline_clipped(rx, ry + dy, seg, clip, color);
350
- ui_vline_clipped(rx + rw - 1, ry + dy, seg, clip, color);
351
- }
352
- }
353
- }
354
- }
355
-
356
- static inline void ui_draw_node_decoration_clipped(uint16_t nodeIdx, int16_t drawY, const UIRect* clip) {
357
- if (nodeIdx >= __ui_node_count) return;
358
- if (__ui_nodes[nodeIdx].borderStyle != 0) {
359
- UI_COLOR_T bColor = __ui_nodes[nodeIdx].borderColor ? __ui_nodes[nodeIdx].borderColor : __ui_nodes[nodeIdx].fg;
360
- int16_t x = __ui_nodes[nodeIdx].box.x;
361
- int16_t y = drawY;
362
- int16_t w = __ui_nodes[nodeIdx].box.w;
363
- int16_t h = __ui_nodes[nodeIdx].box.h;
364
- if (__ui_nodes[nodeIdx].borderRadius > 0) {
365
- if (x >= clip->x && y >= clip->y && x + w <= clip->x + clip->w && y + h <= clip->y + clip->h) {
366
- ui_draw_node_border(nodeIdx, x, drawY, bColor);
367
- }
368
- } else {
369
- ui_draw_rect_outline_clipped(x, y, w, h,
370
- __ui_nodes[nodeIdx].borderStyle, __ui_nodes[nodeIdx].borderWidth, clip, bColor);
371
- }
372
- }
373
- if (__ui_nodes[nodeIdx].outlineStyle != 0 && __ui_nodes[nodeIdx].outlineWidth > 0) {
374
- uint8_t w = __ui_nodes[nodeIdx].outlineWidth;
375
- int16_t x = __ui_nodes[nodeIdx].box.x - w;
376
- int16_t y = drawY - w;
377
- int16_t ow = __ui_nodes[nodeIdx].box.w + 2 * w;
378
- int16_t oh = __ui_nodes[nodeIdx].box.h + 2 * w;
379
- if (__ui_nodes[nodeIdx].borderRadius > 0) {
380
- if (x >= clip->x && y >= clip->y && x + ow <= clip->x + clip->w && y + oh <= clip->y + clip->h) {
381
- ui_draw_node_outline(nodeIdx, __ui_nodes[nodeIdx].box.x, drawY);
382
- }
383
- } else {
384
- ui_draw_rect_outline_clipped(x, y, ow, oh,
385
- __ui_nodes[nodeIdx].outlineStyle, w, clip, __ui_nodes[nodeIdx].outlineColor);
386
- }
387
- }
388
- }
389
-
390
- static inline void ui_clear_node_paint_rect(uint16_t nodeIdx, const UIRect* paintRect) {
391
- if (nodeIdx >= __ui_node_count || !paintRect || paintRect->w <= 0 || paintRect->h <= 0) return;
392
- int16_t scrollParent = ui_scroll_ancestor_for_node(nodeIdx);
393
- UIRect r = *paintRect;
394
- if (__ui_fb) ui_display_set_target((CuttlefishDisplayTarget*)__ui_fb);
395
- else ui_display_use_default_target();
396
- if (scrollParent >= 0) {
397
- UIRect clip = {
398
- __ui_nodes[scrollParent].box.x,
399
- __ui_nodes[scrollParent].box.y,
400
- __ui_nodes[scrollParent].box.w,
401
- __ui_nodes[scrollParent].box.h
402
- };
403
- UIRect clipped = r;
404
- if (!ui_clip_rect_to_rect(&clipped, &clip)) return;
405
- if (ui_repair_current_node_paint_with_parent(nodeIdx, &clipped)) return;
406
- ui_fill_rect_clipped(clipped.x, clipped.y, clipped.w, clipped.h, &clip, ui_parent_clear_color(nodeIdx));
407
- uint16_t p = __ui_nodes[nodeIdx].parent;
408
- if (p != UI_NO_PARENT && p < __ui_node_count) {
409
- ui_draw_node_decoration_clipped(p, ui_draw_y_for_node(p), &clip);
410
- }
411
- return;
412
- }
413
- if (ui_is_rect_clipped_by_scroll(nodeIdx, r.x, r.y, r.w, r.h)) return;
414
- ui_display_fill_rect(r.x, r.y, r.w, r.h, ui_parent_clear_color(nodeIdx));
415
- uint16_t p = __ui_nodes[nodeIdx].parent;
416
- if (p != UI_NO_PARENT && p < __ui_node_count) {
417
- int16_t parentDrawY = ui_draw_y_for_node(p);
418
- if (__ui_nodes[p].borderStyle != 0) {
419
- UI_COLOR_T bColor = __ui_nodes[p].borderColor ? __ui_nodes[p].borderColor : __ui_nodes[p].fg;
420
- ui_draw_node_border(p, ui_draw_x_for_node(p), parentDrawY, bColor);
421
- }
422
- ui_draw_node_outline(p, ui_draw_x_for_node(p), parentDrawY);
423
- }
424
- }
425
-
426
- static inline void ui_clear_current_node_paint(uint16_t nodeIdx) {
427
- if (nodeIdx >= __ui_node_count) return;
428
- uint16_t tw = 0;
429
- uint16_t th = 0;
430
- uint16_t textMaxW = ui_node_text_max_width(nodeIdx);
431
- ui_node_text_layout_metrics(nodeIdx, textMaxW, &tw, &th);
432
- if (__ui_nodes[nodeIdx].kind == NODE_CHECK || __ui_nodes[nodeIdx].kind == NODE_RADIO) tw += 22;
433
- int16_t baseDrawX = ui_base_draw_x_for_node(nodeIdx);
434
- int16_t baseDrawY = ui_base_draw_y_for_node(nodeIdx);
435
- int16_t drawX = ui_draw_x_for_node(nodeIdx);
436
- int16_t drawY = ui_draw_y_for_node(nodeIdx);
437
- UIRect r;
438
- ui_node_paint_rect(nodeIdx, baseDrawX, baseDrawY, drawX, drawY, tw, th, &r);
439
- ui_clear_node_paint_rect(nodeIdx, &r);
440
- }
441
-
442
- static inline uint8_t ui_try_repair_geometry_fill(uint16_t nodeIdx, const UIRect* oldRect) {
443
- if (nodeIdx >= __ui_node_count || !oldRect || oldRect->w <= 0 || oldRect->h <= 0) return 0;
444
- if (__ui_nodes[nodeIdx].kind != NODE_FILL) return 0;
445
- if (!__ui_nodes[nodeIdx].hasBg) return 0;
446
- if (__ui_nodes[nodeIdx].gradientEnabled != 0) return 0;
447
- if (__ui_nodes[nodeIdx].borderRadius != 0 ||
448
- __ui_nodes[nodeIdx].borderStyle != 0 ||
449
- __ui_nodes[nodeIdx].outlineStyle != 0 ||
450
- __ui_nodes[nodeIdx].shadowCount != 0) return 0;
451
-
452
- UIRect newRect;
453
- ui_node_current_paint_rect(nodeIdx, &newRect);
454
- if (newRect.w <= 0 || newRect.h <= 0) return 0;
455
- int16_t x0 = oldRect->x < newRect.x ? oldRect->x : newRect.x;
456
- int16_t y0 = oldRect->y < newRect.y ? oldRect->y : newRect.y;
457
- int16_t x1 = oldRect->x + oldRect->w > newRect.x + newRect.w ? oldRect->x + oldRect->w : newRect.x + newRect.w;
458
- int16_t y1 = oldRect->y + oldRect->h > newRect.y + newRect.h ? oldRect->y + oldRect->h : newRect.y + newRect.h;
459
- UIRect repair = { x0, y0, static_cast<int16_t>(x1 - x0), static_cast<int16_t>(y1 - y0) };
460
- int16_t scrollParent = ui_scroll_ancestor_for_node(nodeIdx);
461
- if (scrollParent >= 0) {
462
- UIRect clip = {
463
- __ui_nodes[scrollParent].box.x,
464
- __ui_nodes[scrollParent].box.y,
465
- __ui_nodes[scrollParent].box.w,
466
- __ui_nodes[scrollParent].box.h
467
- };
468
- if (!ui_clip_rect_to_rect(&repair, &clip)) {
469
- ui_invalidate_scroll_canvas_for_node(nodeIdx);
470
- return 1;
471
- }
472
- } else if (ui_is_rect_clipped_by_scroll(nodeIdx, repair.x, repair.y, repair.w, repair.h)) {
473
- return 0;
474
- }
475
- if (repair.w <= 0 || repair.h <= 0) return 0;
476
- if (static_cast<uint32_t>(repair.w) * static_cast<uint32_t>(repair.h) > UI_MAX_BUFFERED_PAINT_PIXELS) return 0;
477
- CuttlefishCanvas16* repairCanvas = ui_get_repair_canvas(repair.w, repair.h);
478
- if (!repairCanvas) return 0;
479
-
480
- ui_seed_paint_canvas_for_node(nodeIdx, repairCanvas, repair.x, repair.y, 0);
481
- CuttlefishDisplayTarget* previousGfx = ui_display_get_target();
482
- ui_display_set_target(repairCanvas);
483
- UI_COLOR_T fillBg = __ui_nodes[nodeIdx].bg;
484
- if (__ui_nodes[nodeIdx].opacity < 100) {
485
- fillBg = ui_blend(__ui_nodes[nodeIdx].bg, ui_parent_clear_color(nodeIdx), __ui_nodes[nodeIdx].opacity);
486
- }
487
- int16_t drawX = ui_draw_x_for_node(nodeIdx) - repair.x;
488
- int16_t drawY = ui_draw_y_for_node(nodeIdx) - repair.y;
489
- int16_t fillW = ui_rotated_face_w(nodeIdx, __ui_nodes[nodeIdx].box.w, __ui_nodes[nodeIdx].box.h);
490
- int16_t fillH = ui_rotated_face_h(nodeIdx, __ui_nodes[nodeIdx].box.w, __ui_nodes[nodeIdx].box.h);
491
- ui_display_fill_rect(drawX, drawY, fillW, fillH, fillBg);
492
- ui_display_set_target(previousGfx);
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();
497
- ui_push_canvas_rect(repairCanvas, repair.x, repair.y, repair.w, repair.h);
498
- ui_display_use_default_target();
499
- if (scrollParent >= 0) ui_invalidate_scroll_canvas_for_node(nodeIdx);
500
- ui_mark_overlapping_higher_layers_dirty_for_rect(nodeIdx, &repair);
501
- return 1;
502
- }
503
-
504
- static inline void ui_clear_subtree_current_paint(uint16_t nodeIdx) {
505
- if (nodeIdx >= __ui_node_count) return;
506
- UIRect r;
507
- if (!ui_subtree_current_paint_rect(nodeIdx, &r)) return;
508
- ui_display_use_default_target();
509
- int16_t scrollParent = ui_scroll_ancestor_for_node(nodeIdx);
510
- if (scrollParent >= 0) {
511
- UIRect clip = {
512
- __ui_nodes[scrollParent].box.x,
513
- __ui_nodes[scrollParent].box.y,
514
- __ui_nodes[scrollParent].box.w,
515
- __ui_nodes[scrollParent].box.h
516
- };
517
- UIRect clipped = r;
518
- if (!ui_clip_rect_to_rect(&clipped, &clip)) return;
519
- if (ui_repair_current_node_paint_with_parent(nodeIdx, &clipped)) return;
520
- ui_fill_rect_clipped(clipped.x, clipped.y, clipped.w, clipped.h, &clip, ui_parent_clear_color(nodeIdx));
521
- uint16_t p = __ui_nodes[nodeIdx].parent;
522
- if (p != UI_NO_PARENT && p < __ui_node_count) {
523
- ui_draw_node_decoration_clipped(p, ui_draw_y_for_node(p), &clip);
524
- }
525
- return;
526
- }
527
- if (ui_repair_current_node_paint_with_parent(nodeIdx, &r)) return;
528
- ui_display_fill_rect(r.x, r.y, r.w, r.h, ui_parent_clear_color(nodeIdx));
529
- uint16_t p = __ui_nodes[nodeIdx].parent;
530
- if (p != UI_NO_PARENT && p < __ui_node_count) {
531
- int16_t parentDrawY = ui_draw_y_for_node(p);
532
- if (__ui_nodes[p].borderStyle != 0) {
533
- UI_COLOR_T bColor = __ui_nodes[p].borderColor ? __ui_nodes[p].borderColor : __ui_nodes[p].fg;
534
- ui_draw_node_border(p, ui_draw_x_for_node(p), parentDrawY, bColor);
535
- }
536
- ui_draw_node_outline(p, ui_draw_x_for_node(p), parentDrawY);
537
- }
538
- }
539
-
540
- static inline void ui_set_visible(uint16_t nodeIdx, uint8_t visible) {
541
- if (nodeIdx >= __ui_node_count) return;
542
- visible = visible ? 1 : 0;
543
- if (__ui_nodes[nodeIdx].visible == visible) return;
544
-
545
- if (!visible) {
546
- // Clear the whole subtree in one clipped repair. This removes child pixels
547
- // even when the container itself is transparent, without separate display
548
- // writes for each descendant.
549
- UIRect subtreeRect;
550
- uint8_t hasSubtreeRect = ui_subtree_current_paint_rect(nodeIdx, &subtreeRect);
551
- ui_clear_subtree_current_paint(nodeIdx);
552
- int16_t end = __ui_nodes[nodeIdx].subtreeEnd;
553
- if (end > __ui_node_count) end = __ui_node_count;
554
- for (int16_t c = end - 1; c >= static_cast<int16_t>(nodeIdx); c--) {
555
- if (__ui_nodes[c].screenId != __ui_active_screen) continue;
556
- __ui_nodes[c].dirty = 0;
557
- }
558
- if (hasSubtreeRect) ui_mark_overlapping_higher_layers_dirty_for_rect(nodeIdx, &subtreeRect);
559
- __ui_nodes[nodeIdx].visible = 0;
560
- return;
561
- }
562
-
563
- __ui_nodes[nodeIdx].visible = 1;
564
- int16_t end = __ui_nodes[nodeIdx].subtreeEnd;
565
- if (end > __ui_node_count) end = __ui_node_count;
566
- for (uint16_t c = nodeIdx; c < end; c++) {
567
- if (__ui_nodes[c].screenId == __ui_active_screen && ui_is_effectively_visible(c)) {
568
- __ui_nodes[c].dirty = 1;
569
- ui_mark_overlapping_higher_layers_dirty(c);
570
- }
571
- }
572
- }
5
+ return `
6
+ static inline void ui_node_paint_rect(uint16_t nodeIdx, int16_t baseX, int16_t baseY, int16_t drawX, int16_t drawY, uint16_t textW, uint16_t textH, UIRect* out) {
7
+ int16_t shadowL, shadowT, shadowR, shadowB;
8
+ ui_shadow_extents(nodeIdx, &shadowL, &shadowT, &shadowR, &shadowB);
9
+
10
+ int16_t faceW = __ui_nodes[nodeIdx].box.w;
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 || __ui_nodes[nodeIdx].kind == NODE_SELECT) {
13
+ if (__ui_nodes[nodeIdx].lastTextWidth > faceW) faceW = __ui_nodes[nodeIdx].lastTextWidth;
14
+ if (__ui_nodes[nodeIdx].lastTextHeight > faceH) faceH = __ui_nodes[nodeIdx].lastTextHeight;
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
+ }
18
+ int16_t unrotatedFaceW = faceW;
19
+ int16_t unrotatedFaceH = faceH;
20
+ faceW = ui_rotated_face_w(nodeIdx, unrotatedFaceW, unrotatedFaceH);
21
+ faceH = ui_rotated_face_h(nodeIdx, unrotatedFaceW, unrotatedFaceH);
22
+
23
+ int16_t shadowX0 = baseX - shadowL;
24
+ int16_t shadowY0 = baseY - shadowT;
25
+ int16_t shadowX1 = baseX + faceW + shadowR;
26
+ int16_t shadowY1 = baseY + faceH + shadowB;
27
+ int16_t faceX0 = drawX;
28
+ int16_t faceY0 = drawY;
29
+ int16_t faceX1 = drawX + faceW;
30
+ int16_t faceY1 = drawY + faceH;
31
+ int16_t x0 = shadowX0 < faceX0 ? shadowX0 : faceX0;
32
+ int16_t y0 = shadowY0 < faceY0 ? shadowY0 : faceY0;
33
+ int16_t x1 = shadowX1 > faceX1 ? shadowX1 : faceX1;
34
+ int16_t y1 = shadowY1 > faceY1 ? shadowY1 : faceY1;
35
+ if (__ui_nodes[nodeIdx].outlineStyle != 0 && __ui_nodes[nodeIdx].outlineWidth > 0) {
36
+ int16_t o = __ui_nodes[nodeIdx].outlineWidth;
37
+ ui_expand_rect(&x0, &y0, &x1, &y1, drawX - o, drawY - o, drawX + faceW + o, drawY + faceH + o);
38
+ }
39
+ out->x = x0;
40
+ out->y = y0;
41
+ out->w = x1 - x0;
42
+ out->h = y1 - y0;
43
+ }
44
+
45
+ static inline void ui_node_current_paint_rect(uint16_t nodeIdx, UIRect* out) {
46
+ uint16_t tw = 0;
47
+ uint16_t th = 0;
48
+ uint16_t textMaxW = ui_node_text_max_width(nodeIdx);
49
+ ui_node_text_layout_metrics(nodeIdx, textMaxW, &tw, &th);
50
+ if (__ui_nodes[nodeIdx].kind == NODE_CHECK || __ui_nodes[nodeIdx].kind == NODE_RADIO) {
51
+ tw += 22;
52
+ if (th < 16) th = 16;
53
+ }
54
+ ui_node_paint_rect(nodeIdx,
55
+ ui_base_draw_x_for_node(nodeIdx),
56
+ ui_base_draw_y_for_node(nodeIdx),
57
+ ui_draw_x_for_node(nodeIdx),
58
+ ui_draw_y_for_node(nodeIdx),
59
+ tw,
60
+ th,
61
+ out);
62
+ }
63
+
64
+ static inline uint8_t ui_subtree_current_paint_rect(uint16_t nodeIdx, UIRect* out) {
65
+ if (nodeIdx >= __ui_node_count) return 0;
66
+ int16_t end = __ui_nodes[nodeIdx].subtreeEnd;
67
+ if (end > __ui_node_count) end = __ui_node_count;
68
+ uint8_t hasRect = 0;
69
+ int16_t x0 = 0;
70
+ int16_t y0 = 0;
71
+ int16_t x1 = 0;
72
+ int16_t y1 = 0;
73
+ for (uint16_t c = nodeIdx; c < end; c++) {
74
+ if (__ui_nodes[c].screenId != __ui_active_screen) continue;
75
+ if (!ui_is_effectively_visible(c)) continue;
76
+ UIRect r;
77
+ ui_node_current_paint_rect(c, &r);
78
+ if (r.w <= 0 || r.h <= 0) continue;
79
+ if (!hasRect) {
80
+ x0 = r.x;
81
+ y0 = r.y;
82
+ x1 = r.x + r.w;
83
+ y1 = r.y + r.h;
84
+ hasRect = 1;
85
+ } else {
86
+ ui_expand_rect(&x0, &y0, &x1, &y1, r.x, r.y, r.x + r.w, r.y + r.h);
87
+ }
88
+ }
89
+ if (!hasRect) return 0;
90
+ out->x = x0;
91
+ out->y = y0;
92
+ out->w = x1 - x0;
93
+ out->h = y1 - y0;
94
+ return 1;
95
+ }
96
+
97
+ static inline void ui_mark_overlapping_higher_layers_dirty(uint16_t nodeIdx) {
98
+ if (nodeIdx >= __ui_node_count) return;
99
+ if (!ui_is_effectively_visible(nodeIdx)) return;
100
+ if (__ui_nodes[nodeIdx].screenId != __ui_active_screen) return;
101
+ UIRect r;
102
+ ui_node_current_paint_rect(nodeIdx, &r);
103
+ ui_mark_overlapping_higher_layers_dirty_for_rect(nodeIdx, &r);
104
+ }
105
+
106
+ static inline void ui_mark_overlapping_higher_layers_dirty_for_rect(uint16_t nodeIdx, const UIRect* r) {
107
+ if (nodeIdx >= __ui_node_count || !r) return;
108
+ if (!ui_is_effectively_visible(nodeIdx)) return;
109
+ if (__ui_nodes[nodeIdx].screenId != __ui_active_screen) return;
110
+ if (r->w <= 0 || r->h <= 0) return;
111
+ for (uint16_t c = 0; c < __ui_node_count; c++) {
112
+ if (c == nodeIdx) continue;
113
+ if (__ui_nodes[c].dirty) continue;
114
+ if (!ui_is_effectively_visible(c)) continue;
115
+ if (__ui_nodes[c].screenId != __ui_active_screen) continue;
116
+ if (!ui_node_draws_before(nodeIdx, c)) continue;
117
+ UIRect cr;
118
+ ui_node_current_paint_rect(c, &cr);
119
+ if (cr.w <= 0 || cr.h <= 0) continue;
120
+ if (ui_rects_intersect(r->x, r->y, r->w, r->h, cr.x, cr.y, cr.w, cr.h)) {
121
+ __ui_nodes[c].dirty = 1;
122
+ // Repainting a container erases everything drawn inside it — its own
123
+ // descendants. Mark them too, or the ladder redraws the container face
124
+ // alone and wipes its children: a press under a dialog marked the scrim
125
+ // and card dirty (overlapping higher layers) but not the card's title/
126
+ // buttons, and their solo repaint erased the just-composed dialog. The
127
+ // dirty pass clears flags for invisible descendants, and scroll-subtree
128
+ // children route through the scroll defer as usual.
129
+ for (uint16_t k = c + 1; k < __ui_nodes[c].subtreeEnd && k < __ui_node_count; k++) {
130
+ if (__ui_nodes[k].screenId != __ui_active_screen) continue;
131
+ __ui_nodes[k].dirty = 1;
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ static inline void ui_clear_press_offset_area(uint16_t nodeIdx, int16_t baseX, int16_t baseY, int16_t drawX, int16_t drawY, uint16_t textW, uint16_t textH) {
138
+ if (__ui_nodes[nodeIdx].pressedOffsetX == 0 && __ui_nodes[nodeIdx].pressedOffsetY == 0) return;
139
+ UIRect r;
140
+ ui_node_paint_rect(nodeIdx, baseX, baseY, drawX, drawY, textW, textH, &r);
141
+ int16_t x0 = r.x;
142
+ int16_t y0 = r.y;
143
+ int16_t x1 = r.x + r.w;
144
+ int16_t y1 = r.y + r.h;
145
+ if (ui_display_is_default_target() && ui_repair_current_node_paint_with_parent(nodeIdx, &r)) return;
146
+ ui_display_fill_rect(x0, y0, x1 - x0, y1 - y0, ui_parent_clear_color(nodeIdx));
147
+ }
148
+
149
+ // Generated-font / AA text and images draw per-pixel when sent straight to SPI.
150
+ // Prefer the RAM paint canvas for these kinds (within the pixel budget).
151
+ // NOTE: ALL NODE_TEXT is treated as pixel-heavy. Even the classic GFX bitmap
152
+ // font renders via drawPixel per font pixel, and on an SPI TFT each drawPixel
153
+ // is a full set_window+RAMWR SPI transaction (~30us) — a single 460x8 text band
154
+ // costs >100ms direct-drawn. Forcing every text node through the paint canvas
155
+ // turns that into one RAM render + one SPI push (~3ms). AA/generated-font text
156
+ // additionally requires getPixel readback for coverage blending, which is only
157
+ // correct against a RAM target.
158
+ static inline uint8_t ui_pixel_heavy_node(uint16_t nodeIdx) {
159
+ if (nodeIdx >= __ui_node_count) return 0;
160
+ if (__ui_nodes[nodeIdx].kind == NODE_IMG) return 1;
161
+ if (__ui_nodes[nodeIdx].kind == NODE_TEXT) return 1;
162
+ if (__ui_nodes[nodeIdx].kind == NODE_SELECT) return 1;
163
+ return 0;
164
+ }
165
+
166
+ static inline uint8_t ui_should_buffer_paint(uint16_t nodeIdx, int16_t w, int16_t h) {
167
+ if (w <= 0 || h <= 0) return 0;
168
+ if (__ui_nodes[nodeIdx].kind == NODE_LIST) return 0;
169
+ // <canvas> elements batch via __ui_node_canvas when drawing to the display.
170
+ if (__ui_nodes[nodeIdx].kind == NODE_CANVAS) return 0;
171
+ // NO hard pixel cap here: oversized paint rects route to the band renderer
172
+ // via preferBand (UI_BAND_PREFER_PIXELS), and the repair canvas is never
173
+ // attempted above that threshold either. A cap here pushed just-over-budget
174
+ // repaints (a 452x48 full-width button is 21696px) onto the direct
175
+ // clear-then-redraw path — the visible button press flash.
176
+ if (ui_pixel_heavy_node(nodeIdx)) return 1;
177
+ if (__ui_nodes[nodeIdx].kind == NODE_FILL &&
178
+ __ui_nodes[nodeIdx].hasBg &&
179
+ __ui_nodes[nodeIdx].gradientEnabled == 0 &&
180
+ __ui_nodes[nodeIdx].borderRadius == 0 &&
181
+ __ui_nodes[nodeIdx].borderStyle == 0 &&
182
+ __ui_nodes[nodeIdx].outlineStyle == 0 &&
183
+ __ui_nodes[nodeIdx].shadowCount == 0) return 0;
184
+ if (__ui_nodes[nodeIdx].kind == NODE_FILL &&
185
+ !__ui_nodes[nodeIdx].hasBg &&
186
+ __ui_nodes[nodeIdx].borderStyle == 0 &&
187
+ __ui_nodes[nodeIdx].outlineStyle == 0 &&
188
+ __ui_nodes[nodeIdx].shadowCount == 0) return 0;
189
+ return 1;
190
+ }
191
+
192
+ static inline void ui_seed_paint_canvas_for_node(uint16_t nodeIdx, CuttlefishCanvas16* canvas, int16_t canvasX, int16_t canvasY, int16_t bandTop) {
193
+ if (!canvas || !display_canvasBuffer(canvas)) return;
194
+ uint16_t p = __ui_nodes[nodeIdx].parent;
195
+ if (p == UI_NO_PARENT || p >= __ui_node_count) {
196
+ display_canvasFillScreen(canvas, __ui_nodes[nodeIdx].clearColor);
197
+ return;
198
+ }
199
+
200
+ uint16_t parentDecorated =
201
+ (__ui_nodes[p].hasBg && (__ui_nodes[p].borderRadius > 0 || __ui_nodes[p].gradientEnabled > 0)) ||
202
+ __ui_nodes[p].borderStyle != 0 ||
203
+ __ui_nodes[p].outlineStyle != 0;
204
+ if (!parentDecorated) {
205
+ display_canvasFillScreen(canvas, ui_parent_clear_color(nodeIdx));
206
+ return;
207
+ }
208
+
209
+ display_canvasFillScreen(canvas, ui_parent_clear_color(p));
210
+ int16_t parentDrawX = ui_draw_x_for_node(p);
211
+ int16_t parentDrawY = ui_draw_y_for_node(p);
212
+ int16_t origParentX = __ui_nodes[p].box.x;
213
+ CuttlefishDisplayTarget* previousGfx = ui_display_get_target();
214
+ ui_display_set_target(canvas);
215
+ __ui_nodes[p].box.x = parentDrawX - canvasX;
216
+ // Shift the parent's draw Y up by bandTop so the band canvas shows the
217
+ // correct slice of the parent fill/border for this band's Y range. bandTop=0
218
+ // (the main-loop caller) is a no-op shift. canvasY already carries the paint
219
+ // rect's display-space origin; bandTop further offsets within the rect.
220
+ int16_t localY = parentDrawY - canvasY - bandTop;
221
+
222
+ // Parent fill color: blended toward the parent's backdrop when the parent is
223
+ // translucent (matching the main NODE_FILL draw, which uses fillBg). Without
224
+ // this, scroll repair seeds the canvas with the parent's RAW bg while the
225
+ // main draw used the blended color → shearing on translucent nodes during scroll.
226
+ UI_COLOR_T parentFillBg = __ui_nodes[p].bg;
227
+ if (__ui_nodes[p].opacity < 100) {
228
+ parentFillBg = ui_blend(__ui_nodes[p].bg, ui_parent_clear_color(p), __ui_nodes[p].opacity);
229
+ }
230
+
231
+ if (__ui_nodes[p].gradientEnabled > 0) {
232
+ ui_draw_gradient_fill(p, localY);
233
+ } else if (__ui_nodes[p].borderRadius > 0 && __ui_nodes[p].hasBg) {
234
+ ui_display_fill_round_rect(__ui_nodes[p].box.x, localY, __ui_nodes[p].box.w, __ui_nodes[p].box.h,
235
+ __ui_nodes[p].borderRadius, parentFillBg);
236
+ } else if (__ui_nodes[p].hasBg) {
237
+ ui_display_fill_rect(__ui_nodes[p].box.x, localY, __ui_nodes[p].box.w, __ui_nodes[p].box.h, parentFillBg);
238
+ }
239
+ if (__ui_nodes[p].borderStyle != 0) {
240
+ UI_COLOR_T bColor = __ui_nodes[p].borderColor ? __ui_nodes[p].borderColor : __ui_nodes[p].fg;
241
+ ui_draw_node_border(p, __ui_nodes[p].box.x, localY, bColor);
242
+ }
243
+ ui_draw_node_outline(p, __ui_nodes[p].box.x, localY);
244
+
245
+ __ui_nodes[p].box.x = origParentX;
246
+ ui_display_set_target(previousGfx);
247
+ }
248
+
249
+ static inline uint8_t ui_repair_current_node_paint_with_parent(uint16_t nodeIdx, UIRect* r) {
250
+ if (!r || r->w <= 0 || r->h <= 0) return 0;
251
+ if (static_cast<uint32_t>(r->w) * static_cast<uint32_t>(r->h) > UI_MAX_BUFFERED_PAINT_PIXELS) return 0;
252
+ CuttlefishCanvas16* repairCanvas = ui_get_repair_canvas(r->w, r->h);
253
+ if (!repairCanvas) return 0;
254
+ ui_seed_paint_canvas_for_node(nodeIdx, repairCanvas, r->x, r->y, 0);
255
+ // During a retained-framebuffer tick, publish the repair into the RAM
256
+ // composition surface. Sending it to the live panel here creates the visible
257
+ // erase/redraw flash seen on animated transforms.
258
+ if (__ui_fb) ui_display_set_target((CuttlefishDisplayTarget*)__ui_fb);
259
+ else ui_display_use_default_target();
260
+ ui_push_canvas_rect(repairCanvas, r->x, r->y, r->w, r->h);
261
+ ui_display_use_default_target();
262
+ return 1;
263
+ }
264
+
265
+ static inline uint8_t ui_is_rect_clipped_by_scroll(uint16_t nodeIdx, int16_t drawX, int16_t drawY, int16_t drawW, int16_t drawH) {
266
+ uint16_t p = __ui_nodes[nodeIdx].parent;
267
+ while (p != UI_NO_PARENT && p < __ui_node_count) {
268
+ if (__ui_nodes[p].scrollable) {
269
+ if (drawX < __ui_nodes[p].box.x ||
270
+ drawX + drawW > __ui_nodes[p].box.x + __ui_nodes[p].box.w ||
271
+ drawY < __ui_nodes[p].box.y ||
272
+ drawY + drawH > __ui_nodes[p].box.y + __ui_nodes[p].box.h) {
273
+ return 1;
274
+ }
275
+ }
276
+ p = __ui_nodes[p].parent;
277
+ }
278
+ return 0;
279
+ }
280
+
281
+ static inline uint8_t ui_is_clipped_by_scroll(uint16_t nodeIdx, int16_t drawX, int16_t drawY) {
282
+ return ui_is_rect_clipped_by_scroll(nodeIdx, drawX, drawY, __ui_nodes[nodeIdx].box.w, __ui_nodes[nodeIdx].box.h);
283
+ }
284
+
285
+ // Point-in-viewport test for hit-testing. A tap point is tappable if it lies
286
+ // within EVERY scrollable ancestor's viewport (logical AND, matching the
287
+ // preview's intersected scroll clip). This differs from ui_is_clipped_by_scroll,
288
+ // which tests the node's whole bounding box — a tall node (e.g. a wrapped
289
+ // rich-text paragraph) can overflow below the fold yet have a tappable link in
290
+ // its visible portion. Use this in the hit-test path; keep the whole-box check
291
+ // for draw culling, where a partially-visible node still needs repainting.
292
+ static inline uint8_t ui_is_point_clipped_by_scroll(uint16_t nodeIdx, int16_t px, int16_t py) {
293
+ uint16_t p = __ui_nodes[nodeIdx].parent;
294
+ while (p != UI_NO_PARENT && p < __ui_node_count) {
295
+ if (__ui_nodes[p].scrollable) {
296
+ if (px < __ui_nodes[p].box.x ||
297
+ px >= __ui_nodes[p].box.x + __ui_nodes[p].box.w ||
298
+ py < __ui_nodes[p].box.y ||
299
+ py >= __ui_nodes[p].box.y + __ui_nodes[p].box.h) {
300
+ return 1;
301
+ }
302
+ }
303
+ p = __ui_nodes[p].parent;
304
+ }
305
+ return 0;
306
+ }
307
+
308
+ static inline uint8_t ui_clip_rect_to_rect(UIRect* r, const UIRect* clip) {
309
+ int16_t x0 = r->x > clip->x ? r->x : clip->x;
310
+ int16_t y0 = r->y > clip->y ? r->y : clip->y;
311
+ int16_t x1 = r->x + r->w < clip->x + clip->w ? r->x + r->w : clip->x + clip->w;
312
+ int16_t y1 = r->y + r->h < clip->y + clip->h ? r->y + r->h : clip->y + clip->h;
313
+ if (x1 <= x0 || y1 <= y0) return 0;
314
+ r->x = x0;
315
+ r->y = y0;
316
+ r->w = x1 - x0;
317
+ r->h = y1 - y0;
318
+ return 1;
319
+ }
320
+
321
+ static inline void ui_fill_rect_clipped(int16_t x, int16_t y, int16_t w, int16_t h, const UIRect* clip, UI_COLOR_T color) {
322
+ UIRect r = { x, y, w, h };
323
+ if (!ui_clip_rect_to_rect(&r, clip)) return;
324
+ ui_display_fill_rect(r.x, r.y, r.w, r.h, color);
325
+ }
326
+
327
+ static inline void ui_hline_clipped(int16_t x, int16_t y, int16_t w, const UIRect* clip, UI_COLOR_T color) {
328
+ if (w <= 0 || y < clip->y || y >= clip->y + clip->h) return;
329
+ int16_t x0 = x > clip->x ? x : clip->x;
330
+ int16_t x1 = x + w < clip->x + clip->w ? x + w : clip->x + clip->w;
331
+ if (x1 <= x0) return;
332
+ ui_display_draw_fast_hline(x0, y, x1 - x0, color);
333
+ }
334
+
335
+ static inline void ui_vline_clipped(int16_t x, int16_t y, int16_t h, const UIRect* clip, UI_COLOR_T color) {
336
+ if (h <= 0 || x < clip->x || x >= clip->x + clip->w) return;
337
+ int16_t y0 = y > clip->y ? y : clip->y;
338
+ int16_t y1 = y + h < clip->y + clip->h ? y + h : clip->y + clip->h;
339
+ if (y1 <= y0) return;
340
+ ui_display_draw_fast_vline(x, y0, y1 - y0, color);
341
+ }
342
+
343
+ static inline void ui_draw_rect_outline_clipped(int16_t x, int16_t y, int16_t w, int16_t h, uint8_t style, uint8_t width, const UIRect* clip, UI_COLOR_T color) {
344
+ if (style == 0 || width == 0 || w <= 0 || h <= 0) return;
345
+ for (uint8_t b = 0; b < width; b++) {
346
+ int16_t rx = x + b;
347
+ int16_t ry = y + b;
348
+ int16_t rw = w - 2 * b;
349
+ int16_t rh = h - 2 * b;
350
+ if (rw <= 0 || rh <= 0) return;
351
+ if (style == 1) {
352
+ ui_hline_clipped(rx, ry, rw, clip, color);
353
+ ui_hline_clipped(rx, ry + rh - 1, rw, clip, color);
354
+ ui_vline_clipped(rx, ry, rh, clip, color);
355
+ ui_vline_clipped(rx + rw - 1, ry, rh, clip, color);
356
+ } else {
357
+ for (int16_t dx = 0; dx < rw; dx += 8) {
358
+ int16_t seg = (dx + 4 <= rw) ? 4 : (rw - dx);
359
+ ui_hline_clipped(rx + dx, ry, seg, clip, color);
360
+ ui_hline_clipped(rx + dx, ry + rh - 1, seg, clip, color);
361
+ }
362
+ for (int16_t dy = 0; dy < rh; dy += 8) {
363
+ int16_t seg = (dy + 4 <= rh) ? 4 : (rh - dy);
364
+ ui_vline_clipped(rx, ry + dy, seg, clip, color);
365
+ ui_vline_clipped(rx + rw - 1, ry + dy, seg, clip, color);
366
+ }
367
+ }
368
+ }
369
+ }
370
+
371
+ static inline void ui_draw_node_decoration_clipped(uint16_t nodeIdx, int16_t drawY, const UIRect* clip) {
372
+ if (nodeIdx >= __ui_node_count) return;
373
+ if (__ui_nodes[nodeIdx].borderStyle != 0) {
374
+ UI_COLOR_T bColor = __ui_nodes[nodeIdx].borderColor ? __ui_nodes[nodeIdx].borderColor : __ui_nodes[nodeIdx].fg;
375
+ int16_t x = __ui_nodes[nodeIdx].box.x;
376
+ int16_t y = drawY;
377
+ int16_t w = __ui_nodes[nodeIdx].box.w;
378
+ int16_t h = __ui_nodes[nodeIdx].box.h;
379
+ if (__ui_nodes[nodeIdx].borderRadius > 0) {
380
+ if (x >= clip->x && y >= clip->y && x + w <= clip->x + clip->w && y + h <= clip->y + clip->h) {
381
+ ui_draw_node_border(nodeIdx, x, drawY, bColor);
382
+ }
383
+ } else {
384
+ ui_draw_rect_outline_clipped(x, y, w, h,
385
+ __ui_nodes[nodeIdx].borderStyle, __ui_nodes[nodeIdx].borderWidth, clip, bColor);
386
+ }
387
+ }
388
+ if (__ui_nodes[nodeIdx].outlineStyle != 0 && __ui_nodes[nodeIdx].outlineWidth > 0) {
389
+ uint8_t w = __ui_nodes[nodeIdx].outlineWidth;
390
+ int16_t x = __ui_nodes[nodeIdx].box.x - w;
391
+ int16_t y = drawY - w;
392
+ int16_t ow = __ui_nodes[nodeIdx].box.w + 2 * w;
393
+ int16_t oh = __ui_nodes[nodeIdx].box.h + 2 * w;
394
+ if (__ui_nodes[nodeIdx].borderRadius > 0) {
395
+ if (x >= clip->x && y >= clip->y && x + ow <= clip->x + clip->w && y + oh <= clip->y + clip->h) {
396
+ ui_draw_node_outline(nodeIdx, __ui_nodes[nodeIdx].box.x, drawY);
397
+ }
398
+ } else {
399
+ ui_draw_rect_outline_clipped(x, y, ow, oh,
400
+ __ui_nodes[nodeIdx].outlineStyle, w, clip, __ui_nodes[nodeIdx].outlineColor);
401
+ }
402
+ }
403
+ }
404
+
405
+ static inline void ui_clear_node_paint_rect(uint16_t nodeIdx, const UIRect* paintRect) {
406
+ if (nodeIdx >= __ui_node_count || !paintRect || paintRect->w <= 0 || paintRect->h <= 0) return;
407
+ int16_t scrollParent = ui_scroll_ancestor_for_node(nodeIdx);
408
+ UIRect r = *paintRect;
409
+ if (__ui_fb) ui_display_set_target((CuttlefishDisplayTarget*)__ui_fb);
410
+ else ui_display_use_default_target();
411
+ if (scrollParent >= 0) {
412
+ UIRect clip = {
413
+ __ui_nodes[scrollParent].box.x,
414
+ __ui_nodes[scrollParent].box.y,
415
+ __ui_nodes[scrollParent].box.w,
416
+ __ui_nodes[scrollParent].box.h
417
+ };
418
+ UIRect clipped = r;
419
+ if (!ui_clip_rect_to_rect(&clipped, &clip)) return;
420
+ if (ui_repair_current_node_paint_with_parent(nodeIdx, &clipped)) return;
421
+ ui_fill_rect_clipped(clipped.x, clipped.y, clipped.w, clipped.h, &clip, ui_parent_clear_color(nodeIdx));
422
+ uint16_t p = __ui_nodes[nodeIdx].parent;
423
+ if (p != UI_NO_PARENT && p < __ui_node_count) {
424
+ ui_draw_node_decoration_clipped(p, ui_draw_y_for_node(p), &clip);
425
+ }
426
+ return;
427
+ }
428
+ if (ui_is_rect_clipped_by_scroll(nodeIdx, r.x, r.y, r.w, r.h)) return;
429
+ ui_display_fill_rect(r.x, r.y, r.w, r.h, ui_parent_clear_color(nodeIdx));
430
+ uint16_t p = __ui_nodes[nodeIdx].parent;
431
+ if (p != UI_NO_PARENT && p < __ui_node_count) {
432
+ int16_t parentDrawY = ui_draw_y_for_node(p);
433
+ if (__ui_nodes[p].borderStyle != 0) {
434
+ UI_COLOR_T bColor = __ui_nodes[p].borderColor ? __ui_nodes[p].borderColor : __ui_nodes[p].fg;
435
+ ui_draw_node_border(p, ui_draw_x_for_node(p), parentDrawY, bColor);
436
+ }
437
+ ui_draw_node_outline(p, ui_draw_x_for_node(p), parentDrawY);
438
+ }
439
+ }
440
+
441
+ static inline void ui_clear_current_node_paint(uint16_t nodeIdx) {
442
+ if (nodeIdx >= __ui_node_count) return;
443
+ uint16_t tw = 0;
444
+ uint16_t th = 0;
445
+ uint16_t textMaxW = ui_node_text_max_width(nodeIdx);
446
+ ui_node_text_layout_metrics(nodeIdx, textMaxW, &tw, &th);
447
+ if (__ui_nodes[nodeIdx].kind == NODE_CHECK || __ui_nodes[nodeIdx].kind == NODE_RADIO) tw += 22;
448
+ int16_t baseDrawX = ui_base_draw_x_for_node(nodeIdx);
449
+ int16_t baseDrawY = ui_base_draw_y_for_node(nodeIdx);
450
+ int16_t drawX = ui_draw_x_for_node(nodeIdx);
451
+ int16_t drawY = ui_draw_y_for_node(nodeIdx);
452
+ UIRect r;
453
+ ui_node_paint_rect(nodeIdx, baseDrawX, baseDrawY, drawX, drawY, tw, th, &r);
454
+ ui_clear_node_paint_rect(nodeIdx, &r);
455
+ }
456
+
457
+ static inline uint8_t ui_try_repair_geometry_fill(uint16_t nodeIdx, const UIRect* oldRect) {
458
+ if (nodeIdx >= __ui_node_count || !oldRect || oldRect->w <= 0 || oldRect->h <= 0) return 0;
459
+ if (__ui_nodes[nodeIdx].kind != NODE_FILL) return 0;
460
+ if (!__ui_nodes[nodeIdx].hasBg) return 0;
461
+ if (__ui_nodes[nodeIdx].gradientEnabled != 0) return 0;
462
+ // borderRadius is NOT a bail-out: rounded fills (pill badges, keyframe
463
+ // boxes) repair through the same union bitmap — the corner arcs draw into
464
+ // the seeded backdrop. Rejecting them forced every geometry keyframe on a
465
+ // rounded box onto the direct clear→redraw fallback, which tears on SPI
466
+ // TFTs at animation frame rate. Border/outline/shadow chrome still bails:
467
+ // the repair body below doesn't render it.
468
+ if (__ui_nodes[nodeIdx].borderStyle != 0 ||
469
+ __ui_nodes[nodeIdx].outlineStyle != 0 ||
470
+ __ui_nodes[nodeIdx].shadowCount != 0) return 0;
471
+
472
+ UIRect newRect;
473
+ ui_node_current_paint_rect(nodeIdx, &newRect);
474
+ if (newRect.w <= 0 || newRect.h <= 0) return 0;
475
+ int16_t x0 = oldRect->x < newRect.x ? oldRect->x : newRect.x;
476
+ int16_t y0 = oldRect->y < newRect.y ? oldRect->y : newRect.y;
477
+ int16_t x1 = oldRect->x + oldRect->w > newRect.x + newRect.w ? oldRect->x + oldRect->w : newRect.x + newRect.w;
478
+ int16_t y1 = oldRect->y + oldRect->h > newRect.y + newRect.h ? oldRect->y + oldRect->h : newRect.y + newRect.h;
479
+ UIRect repair = { x0, y0, static_cast<int16_t>(x1 - x0), static_cast<int16_t>(y1 - y0) };
480
+ int16_t scrollParent = ui_scroll_ancestor_for_node(nodeIdx);
481
+ if (scrollParent >= 0) {
482
+ UIRect clip = {
483
+ __ui_nodes[scrollParent].box.x,
484
+ __ui_nodes[scrollParent].box.y,
485
+ __ui_nodes[scrollParent].box.w,
486
+ __ui_nodes[scrollParent].box.h
487
+ };
488
+ if (!ui_clip_rect_to_rect(&repair, &clip)) {
489
+ ui_invalidate_scroll_canvas_for_node(nodeIdx);
490
+ return 1;
491
+ }
492
+ } else if (ui_is_rect_clipped_by_scroll(nodeIdx, repair.x, repair.y, repair.w, repair.h)) {
493
+ return 0;
494
+ }
495
+ if (repair.w <= 0 || repair.h <= 0) return 0;
496
+ if (static_cast<uint32_t>(repair.w) * static_cast<uint32_t>(repair.h) > UI_MAX_BUFFERED_PAINT_PIXELS) return 0;
497
+ CuttlefishCanvas16* repairCanvas = ui_get_repair_canvas(repair.w, repair.h);
498
+ if (!repairCanvas) return 0;
499
+
500
+ ui_seed_paint_canvas_for_node(nodeIdx, repairCanvas, repair.x, repair.y, 0);
501
+ CuttlefishDisplayTarget* previousGfx = ui_display_get_target();
502
+ ui_display_set_target(repairCanvas);
503
+ UI_COLOR_T fillBg = __ui_nodes[nodeIdx].bg;
504
+ if (__ui_nodes[nodeIdx].opacity < 100) {
505
+ fillBg = ui_blend(__ui_nodes[nodeIdx].bg, ui_parent_clear_color(nodeIdx), __ui_nodes[nodeIdx].opacity);
506
+ }
507
+ int16_t drawX = ui_draw_x_for_node(nodeIdx) - repair.x;
508
+ int16_t drawY = ui_draw_y_for_node(nodeIdx) - repair.y;
509
+ int16_t fillW = ui_rotated_face_w(nodeIdx, __ui_nodes[nodeIdx].box.w, __ui_nodes[nodeIdx].box.h);
510
+ int16_t fillH = ui_rotated_face_h(nodeIdx, __ui_nodes[nodeIdx].box.w, __ui_nodes[nodeIdx].box.h);
511
+ if (__ui_nodes[nodeIdx].borderRadius > 0) {
512
+ ui_display_fill_round_rect(drawX, drawY, fillW, fillH,
513
+ __ui_nodes[nodeIdx].borderRadius, fillBg);
514
+ } else {
515
+ ui_display_fill_rect(drawX, drawY, fillW, fillH, fillBg);
516
+ }
517
+ ui_display_set_target(previousGfx);
518
+ // Keep geometry repair off the physical panel when a retained framebuffer is
519
+ // active; the final dirty-bounds publish will send the old+new union once.
520
+ if (__ui_fb) ui_display_set_target((CuttlefishDisplayTarget*)__ui_fb);
521
+ else ui_display_use_default_target();
522
+ ui_push_canvas_rect(repairCanvas, repair.x, repair.y, repair.w, repair.h);
523
+ ui_display_use_default_target();
524
+ if (scrollParent >= 0) ui_invalidate_scroll_canvas_for_node(nodeIdx);
525
+ ui_mark_overlapping_higher_layers_dirty_for_rect(nodeIdx, &repair);
526
+ return 1;
527
+ }
528
+
529
+ static inline void ui_clear_subtree_current_paint(uint16_t nodeIdx) {
530
+ if (nodeIdx >= __ui_node_count) return;
531
+ UIRect r;
532
+ if (!ui_subtree_current_paint_rect(nodeIdx, &r)) return;
533
+ ui_display_use_default_target();
534
+ int16_t scrollParent = ui_scroll_ancestor_for_node(nodeIdx);
535
+ if (scrollParent >= 0) {
536
+ UIRect clip = {
537
+ __ui_nodes[scrollParent].box.x,
538
+ __ui_nodes[scrollParent].box.y,
539
+ __ui_nodes[scrollParent].box.w,
540
+ __ui_nodes[scrollParent].box.h
541
+ };
542
+ UIRect clipped = r;
543
+ if (!ui_clip_rect_to_rect(&clipped, &clip)) return;
544
+ if (ui_repair_current_node_paint_with_parent(nodeIdx, &clipped)) return;
545
+ ui_fill_rect_clipped(clipped.x, clipped.y, clipped.w, clipped.h, &clip, ui_parent_clear_color(nodeIdx));
546
+ uint16_t p = __ui_nodes[nodeIdx].parent;
547
+ if (p != UI_NO_PARENT && p < __ui_node_count) {
548
+ ui_draw_node_decoration_clipped(p, ui_draw_y_for_node(p), &clip);
549
+ }
550
+ return;
551
+ }
552
+ if (ui_repair_current_node_paint_with_parent(nodeIdx, &r)) return;
553
+ ui_display_fill_rect(r.x, r.y, r.w, r.h, ui_parent_clear_color(nodeIdx));
554
+ uint16_t p = __ui_nodes[nodeIdx].parent;
555
+ if (p != UI_NO_PARENT && p < __ui_node_count) {
556
+ int16_t parentDrawY = ui_draw_y_for_node(p);
557
+ if (__ui_nodes[p].borderStyle != 0) {
558
+ UI_COLOR_T bColor = __ui_nodes[p].borderColor ? __ui_nodes[p].borderColor : __ui_nodes[p].fg;
559
+ ui_draw_node_border(p, ui_draw_x_for_node(p), parentDrawY, bColor);
560
+ }
561
+ ui_draw_node_outline(p, ui_draw_x_for_node(p), parentDrawY);
562
+ }
563
+ }
564
+
565
+ // ── Visibility reflow ──────────────────────────────────────────────────────
566
+ // Layout runs at build time (yoga); a hidden subtree keeps its baked boxes,
567
+ // so toggling the visible flag alone leaves a collapsed pane's space
568
+ // reserved (an accordion card sized for its open state shows empty space
569
+ // when closed). When a node's visibility changes, the runtime re-stacks each
570
+ // ancestor flow container's in-flow children along the emitted axis
571
+ // (flowAxis/flowGap/flowFlags metadata) and re-sizes content-sized
572
+ // containers, cascading upward until a container stops changing. Preview
573
+ // parity: the preview's layout engine excludes hidden nodes from flow on
574
+ // every frame.
575
+
576
+ // Outer extent of c's subtree along the flow axis, relative to c's own
577
+ // origin. Descendants positioned above c's origin (overlays) don't count.
578
+ static int16_t ui_subtree_flow_extent(uint16_t c, uint8_t axis) {
579
+ int16_t base = axis == 1 ? __ui_nodes[c].box.y : __ui_nodes[c].box.x;
580
+ int16_t maxEnd = static_cast<int16_t>(base + (axis == 1 ? __ui_nodes[c].box.h : __ui_nodes[c].box.w));
581
+ uint16_t end = __ui_nodes[c].subtreeEnd;
582
+ if (end > __ui_node_count) end = __ui_node_count;
583
+ for (uint16_t j = c + 1; j < end; j++) {
584
+ int16_t off = axis == 1 ? __ui_nodes[j].box.y : __ui_nodes[j].box.x;
585
+ int16_t rel = static_cast<int16_t>(off - base);
586
+ if (rel < 0) continue;
587
+ int16_t jEnd = static_cast<int16_t>(rel + (axis == 1 ? __ui_nodes[j].box.h : __ui_nodes[j].box.w));
588
+ if (jEnd > maxEnd) maxEnd = jEnd;
589
+ }
590
+ return static_cast<int16_t>(maxEnd - base);
591
+ }
592
+
593
+ static void ui_shift_subtree_main(uint16_t c, uint8_t axis, int16_t delta) {
594
+ uint16_t end = __ui_nodes[c].subtreeEnd;
595
+ if (end > __ui_node_count) end = __ui_node_count;
596
+ for (uint16_t j = c; j < end; j++) {
597
+ if (axis == 1) __ui_nodes[j].box.y = static_cast<int16_t>(__ui_nodes[j].box.y + delta);
598
+ else __ui_nodes[j].box.x = static_cast<int16_t>(__ui_nodes[j].box.x + delta);
599
+ }
600
+ }
601
+
602
+ // Re-stack p's in-flow children from the first in-flow child's slot (hidden
603
+ // children take no space). Returns 1 when a child moved or p's content-sized
604
+ // main dimension changed.
605
+ static uint8_t ui_restack_flow_container(uint16_t p) {
606
+ if (p >= __ui_node_count) return 0;
607
+ uint8_t axis = __ui_nodes[p].flowAxis == 2 ? 2U : (__ui_nodes[p].flowAxis == 1 ? 1U : 0U);
608
+ if (axis == 0) return 0;
609
+ int16_t gap = static_cast<int16_t>(__ui_nodes[p].flowGap);
610
+ int16_t startSlot = 0;
611
+ int16_t cursor = 0;
612
+ uint8_t sawFlowChild = 0;
613
+ uint8_t sawVisibleChild = 0;
614
+ uint8_t changed = 0;
615
+ uint16_t end = __ui_nodes[p].subtreeEnd;
616
+ if (end > __ui_node_count) end = __ui_node_count;
617
+ for (uint16_t c = p + 1; c < end; c++) {
618
+ if (__ui_nodes[c].parent != p) continue;
619
+ if ((__ui_nodes[c].flowFlags & 0x4U) != 0U) continue; // out-of-flow child
620
+ int16_t rel = axis == 1
621
+ ? static_cast<int16_t>(__ui_nodes[c].box.y - __ui_nodes[p].box.y)
622
+ : static_cast<int16_t>(__ui_nodes[c].box.x - __ui_nodes[p].box.x);
623
+ // The first in-flow child's slot anchors the stack (padding + its
624
+ // margins); a hidden first child must not drag the anchor down.
625
+ if (sawFlowChild == 0U) { startSlot = rel; sawFlowChild = 1; }
626
+ if (!ui_is_effectively_visible(c)) continue; // collapsed: takes no space
627
+ if (sawVisibleChild == 0U) { cursor = startSlot; sawVisibleChild = 1; }
628
+ int16_t shift = static_cast<int16_t>(cursor - rel);
629
+ if (shift != 0) {
630
+ ui_shift_subtree_main(c, axis, shift);
631
+ changed = 1;
632
+ }
633
+ cursor = static_cast<int16_t>(cursor + ui_subtree_flow_extent(c, axis) + gap);
634
+ }
635
+ if (sawVisibleChild == 0U) return changed;
636
+ // A content-sized main dimension follows the stack: children's offsets
637
+ // already include the leading padding; trailing padding completes the box.
638
+ uint8_t autoMain = axis == 1
639
+ ? static_cast<uint8_t>(__ui_nodes[p].flowFlags & 0x1U)
640
+ : static_cast<uint8_t>(__ui_nodes[p].flowFlags & 0x2U);
641
+ if (autoMain != 0U) {
642
+ int16_t contentEnd = static_cast<int16_t>(cursor - gap);
643
+ int16_t padMain = axis == 1
644
+ ? static_cast<int16_t>(__ui_nodes[p].paddingBottom)
645
+ : static_cast<int16_t>(__ui_nodes[p].paddingRight);
646
+ int16_t newSize = static_cast<int16_t>(contentEnd + padMain);
647
+ if (newSize < 1) newSize = 1;
648
+ int16_t curSize = axis == 1 ? static_cast<int16_t>(__ui_nodes[p].box.h) : static_cast<int16_t>(__ui_nodes[p].box.w);
649
+ if (newSize != curSize) {
650
+ if (axis == 1) __ui_nodes[p].box.h = newSize;
651
+ else __ui_nodes[p].box.w = newSize;
652
+ changed = 1;
653
+ }
654
+ }
655
+ return changed;
656
+ }
657
+
658
+ // Recompute a scrollable container's contentHeight over its visible subtree
659
+ // (mirrors the build-time computation), clamp the scroll offset, and force
660
+ // the next paint to reseed the container canvas.
661
+ static void ui_reflow_scroll_content(uint16_t p) {
662
+ if (!__ui_nodes[p].scrollable || __ui_nodes[p].virtualized) return;
663
+ int16_t maxBottom = static_cast<int16_t>(__ui_nodes[p].box.y);
664
+ uint16_t end = __ui_nodes[p].subtreeEnd;
665
+ if (end > __ui_node_count) end = __ui_node_count;
666
+ for (uint16_t j = p + 1; j < end; j++) {
667
+ if (!ui_is_effectively_visible(j)) continue;
668
+ int16_t bottom = static_cast<int16_t>(__ui_nodes[j].box.y + __ui_nodes[j].box.h);
669
+ if (bottom > maxBottom) maxBottom = bottom;
670
+ }
671
+ int16_t ch = static_cast<int16_t>(maxBottom - __ui_nodes[p].box.y);
672
+ if (ch < static_cast<int16_t>(__ui_nodes[p].box.h)) ch = static_cast<int16_t>(__ui_nodes[p].box.h);
673
+ __ui_nodes[p].contentHeight = ch;
674
+ int16_t maxScroll = static_cast<int16_t>(ch - __ui_nodes[p].box.h);
675
+ if (maxScroll < 0) maxScroll = 0;
676
+ if (__ui_nodes[p].scrollY > maxScroll) __ui_nodes[p].scrollY = maxScroll;
677
+ __ui_nodes[p].lastPaintedScrollY = static_cast<int16_t>(
678
+ __ui_nodes[p].scrollY - (__ui_nodes[p].box.h > 0 ? __ui_nodes[p].box.h : 1));
679
+ }
680
+
681
+ // Called after a node's visible flag changed: re-stack ancestor flow
682
+ // containers bottom-up, then repaint the active screen once.
683
+ static void ui_reflow_visibility(uint16_t changedNode) {
684
+ if (changedNode >= __ui_node_count) return;
685
+ uint8_t anyChange = 0;
686
+ uint16_t p = __ui_nodes[changedNode].parent;
687
+ uint16_t guard = 0;
688
+ while (p != UI_NO_PARENT && p < __ui_node_count && guard < 64U) {
689
+ guard++;
690
+ uint8_t changedHere = ui_restack_flow_container(p);
691
+ ui_reflow_scroll_content(p);
692
+ if (changedHere == 0U) break; // nothing moved and the size held: ancestors unaffected
693
+ anyChange = 1;
694
+ p = __ui_nodes[p].parent;
695
+ }
696
+ if (anyChange == 0U) return;
697
+ // One full active-screen repaint — the same contract as navigation; a
698
+ // visibility collapse is a discrete user event, not an animation.
699
+ for (uint16_t i = 0; i < __ui_node_count; i++) {
700
+ if (__ui_nodes[i].screenId != __ui_active_screen) continue;
701
+ __ui_nodes[i].dirty = 1;
702
+ __ui_nodes[i].lastTextHeight = 0;
703
+ }
704
+ }
705
+
706
+ static inline void ui_set_visible(uint16_t nodeIdx, uint8_t visible) {
707
+ if (nodeIdx >= __ui_node_count) return;
708
+ visible = visible ? 1 : 0;
709
+ if (__ui_nodes[nodeIdx].visible == visible) return;
710
+
711
+ if (!visible) {
712
+ // Clear the whole subtree in one clipped repair. This removes child pixels
713
+ // even when the container itself is transparent, without separate display
714
+ // writes for each descendant.
715
+ UIRect subtreeRect;
716
+ uint8_t hasSubtreeRect = ui_subtree_current_paint_rect(nodeIdx, &subtreeRect);
717
+ ui_clear_subtree_current_paint(nodeIdx);
718
+ int16_t end = __ui_nodes[nodeIdx].subtreeEnd;
719
+ if (end > __ui_node_count) end = __ui_node_count;
720
+ for (int16_t c = end - 1; c >= static_cast<int16_t>(nodeIdx); c--) {
721
+ if (__ui_nodes[c].screenId != __ui_active_screen) continue;
722
+ __ui_nodes[c].dirty = 0;
723
+ }
724
+ if (hasSubtreeRect) ui_mark_overlapping_higher_layers_dirty_for_rect(nodeIdx, &subtreeRect);
725
+ __ui_nodes[nodeIdx].visible = 0;
726
+ ui_reflow_visibility(nodeIdx);
727
+ return;
728
+ }
729
+
730
+ __ui_nodes[nodeIdx].visible = 1;
731
+ int16_t end = __ui_nodes[nodeIdx].subtreeEnd;
732
+ if (end > __ui_node_count) end = __ui_node_count;
733
+ for (uint16_t c = nodeIdx; c < end; c++) {
734
+ if (__ui_nodes[c].screenId == __ui_active_screen && ui_is_effectively_visible(c)) {
735
+ __ui_nodes[c].dirty = 1;
736
+ ui_mark_overlapping_higher_layers_dirty(c);
737
+ }
738
+ }
739
+ ui_reflow_visibility(nodeIdx);
740
+ }
573
741
  `;
574
742
  }