@typecad/ui 1.0.0-alpha.13 → 1.0.0-alpha.15
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.
- package/README.md +1457 -1469
- package/dist/cli.js +22 -19
- package/dist/preview/host-ui-runtime.js +0 -1
- package/dist/ui-engine/model.js +7 -1
- package/dist/ui-engine/runtime-header/canvas-scrollbar.js +115 -115
- package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +182 -182
- package/dist/ui-engine/runtime-header/forward-decls.js +227 -216
- package/dist/ui-engine/runtime-header/init-press-input.js +138 -138
- package/dist/ui-engine/runtime-header/keyboard.js +676 -676
- package/dist/ui-engine/runtime-header/node-draw-body.js +1658 -1656
- package/dist/ui-engine/runtime-header/paint-order-coords.js +259 -259
- package/dist/ui-engine/runtime-header/paint-rects-repair.js +736 -736
- package/dist/ui-engine/runtime-header/scroll-physics.js +4 -4
- package/dist/ui-engine/runtime-header/structs.js +228 -228
- package/dist/ui-engine/runtime-header/text-rendering.js +888 -888
- package/dist/ui-engine/runtime-header/tick/bindings-phase.js +118 -118
- package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +896 -896
- package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +24 -24
- package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +11 -11
- package/dist/ui-engine/runtime-header/types-defines.js +110 -110
- package/dist/ui-engine/shadcn-kit.d.ts +1 -1
- package/dist/ui-engine/shadcn-kit.js +4 -0
- package/dist/ui-engine/ui-lowering.d.ts +1 -1
- package/dist/ui-engine/ui-lowering.js +14 -5
- package/dist/ui-engine/ui-registry.js +9 -1
- package/dist/wizard/display-catalog.d.ts +0 -3
- package/dist/wizard/display-catalog.js +3 -12
- package/dist/wizard/index.d.ts +2 -0
- package/dist/wizard/index.js +1 -0
- package/dist/wizard/integration-wizard.d.ts +3 -0
- package/dist/wizard/integration-wizard.js +69 -20
- package/dist/wizard/package-writer.d.ts +20 -0
- package/dist/wizard/package-writer.js +79 -0
- package/package.json +4 -4
- package/src/cli.ts +90 -87
- package/src/preview/host-ui-runtime.ts +0 -1
- package/src/ui-engine/model.ts +6 -1
- package/src/ui-engine/runtime-header/canvas-scrollbar.ts +121 -121
- package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +188 -188
- package/src/ui-engine/runtime-header/forward-decls.ts +238 -227
- package/src/ui-engine/runtime-header/init-press-input.ts +144 -144
- package/src/ui-engine/runtime-header/keyboard.ts +689 -689
- package/src/ui-engine/runtime-header/node-draw-body.ts +1683 -1681
- package/src/ui-engine/runtime-header/paint-order-coords.ts +265 -265
- package/src/ui-engine/runtime-header/paint-rects-repair.ts +742 -742
- package/src/ui-engine/runtime-header/scroll-physics.ts +4 -4
- package/src/ui-engine/runtime-header/structs.ts +234 -234
- package/src/ui-engine/runtime-header/text-rendering.ts +894 -894
- package/src/ui-engine/runtime-header/tick/bindings-phase.ts +124 -124
- package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +902 -902
- package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +30 -30
- package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +11 -11
- package/src/ui-engine/runtime-header/types-defines.ts +116 -116
- package/src/ui-engine/shadcn-kit.ts +4 -0
- package/src/ui-engine/ui-lowering.ts +12 -4
- package/src/ui-engine/ui-registry.ts +9 -1
- package/src/wizard/display-catalog.ts +261 -273
- package/src/wizard/index.ts +46 -38
- package/src/wizard/integration-wizard.ts +679 -619
- package/src/wizard/package-writer.ts +98 -0
|
@@ -1,742 +1,742 @@
|
|
|
1
|
-
// Slice of the C++ runtime header (original source lines 1895-2441).
|
|
2
|
-
// Paint rects, overlapping layers dirty, buffer decisions, seed/repair, scroll-clip tests, clipped primitives, clear/repair, set_visible.
|
|
3
|
-
// See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
|
|
4
|
-
export function emitPaintRectsRepair(): string {
|
|
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
|
-
}
|
|
741
|
-
`;
|
|
742
|
-
}
|
|
1
|
+
// Slice of the C++ runtime header (original source lines 1895-2441).
|
|
2
|
+
// Paint rects, overlapping layers dirty, buffer decisions, seed/repair, scroll-clip tests, clipped primitives, clear/repair, set_visible.
|
|
3
|
+
// See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
|
|
4
|
+
export function emitPaintRectsRepair(): string {
|
|
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
|
+
}
|
|
741
|
+
`;
|
|
742
|
+
}
|