@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
|
@@ -2,901 +2,901 @@
|
|
|
2
2
|
// refresh begin, kb dirty flush, scroll Mode B/C, fb target, stacking draw loop.
|
|
3
3
|
// See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
|
|
4
4
|
export function emitTickDirtyDrawPhase() {
|
|
5
|
-
return `
|
|
6
|
-
|
|
7
|
-
// ② Draw dirty nodes directly to the display object.
|
|
8
|
-
// Begin a deferred-refresh frame: resets the dirty-rect accumulator. On TFT
|
|
9
|
-
// (no backing store) this compiles to a no-op.
|
|
10
|
-
ui_refresh_begin_frame();
|
|
11
|
-
// Skip the node draw pass while the keyboard overlay is visible — its opaque
|
|
12
|
-
// background covers everything underneath, so redrawing app nodes wastes SPI
|
|
13
|
-
// bandwidth and causes flashing. Nodes redraw once when the keyboard closes
|
|
14
|
-
// (ui_kb_close marks the edited input dirty; ui_kb_open had marked all dirty
|
|
15
|
-
// on open so they're stale-but-covered while the keyboard is up).
|
|
16
|
-
//
|
|
17
|
-
// UI_HIDE_OSK (desktop SDL): the editing session still runs (buffer, target,
|
|
18
|
-
// commit-on-close) so real-keyboard typing works, but the 6×4 grid isn't drawn
|
|
19
|
-
// and the node pass ISN'T skipped — the app keeps rendering normally with the
|
|
20
|
-
// edited input's textBuffer showing the typed text. The grid is redundant when
|
|
21
|
-
// the host has a real keyboard.
|
|
22
|
-
#if !defined(UI_HIDE_OSK)
|
|
23
|
-
if (__ui_kb_visible) {
|
|
24
|
-
// Redraw only what changed:
|
|
25
|
-
// 1 = full redraw (open, shift toggle, page swap)
|
|
26
|
-
// 2 = text row + single key (char insert, delete, highlight change)
|
|
27
|
-
if (__ui_kb_dirty == 1) {
|
|
28
|
-
ui_kb_draw();
|
|
29
|
-
} else if (__ui_kb_dirty == 2) {
|
|
30
|
-
// Buffer the targeted update through the keyboard canvas too — drawing
|
|
31
|
-
// the text row directly to the display (fill_rect clear + text redraw)
|
|
32
|
-
// flashes on SPI TFTs because the clear is visible for one frame.
|
|
33
|
-
// Re-render into the canvas and push in one transaction. The canvas
|
|
34
|
-
// already holds the last full-keyboard frame, so we only need to redraw
|
|
35
|
-
// the text row + the single changed key on top of it.
|
|
36
|
-
// (__ui_kb_canvas is declared static in the keyboard slice, earlier in
|
|
37
|
-
// this same translation unit — no extern needed.)
|
|
38
|
-
if (__ui_kb_canvas && display_canvasBuffer(__ui_kb_canvas)) {
|
|
39
|
-
int16_t kw = __ui_kb_box.w;
|
|
40
|
-
int16_t saveBoxX = __ui_kb_box.x;
|
|
41
|
-
int16_t saveBoxY = __ui_kb_box.y;
|
|
42
|
-
CuttlefishDisplayTarget* __kb_prev = ui_display_get_target();
|
|
43
|
-
ui_display_set_target(__ui_kb_canvas);
|
|
44
|
-
__ui_kb_box.x = 0;
|
|
45
|
-
__ui_kb_box.y = 0;
|
|
46
|
-
ui_kb_draw_text_row();
|
|
47
|
-
uint8_t has_key_repaint = (__ui_kb_repaint_key >= 0 && __ui_kb_keys[__ui_kb_repaint_key].special != 255) ? 1 : 0;
|
|
48
|
-
if (has_key_repaint) {
|
|
49
|
-
ui_kb_draw_key(static_cast<uint8_t>(__ui_kb_repaint_key));
|
|
50
|
-
}
|
|
51
|
-
__ui_kb_box.x = saveBoxX;
|
|
52
|
-
__ui_kb_box.y = saveBoxY;
|
|
53
|
-
ui_display_set_target(__kb_prev);
|
|
54
|
-
// Push only the changed region: text row alone (UI_KB_TEXT_H) when no
|
|
55
|
-
// key highlight changed, or the full canvas when a key was repainted
|
|
56
|
-
// (the key could be anywhere in the keyboard grid).
|
|
57
|
-
display_startWrite();
|
|
58
|
-
if (has_key_repaint) {
|
|
59
|
-
display_setAddrWindow(saveBoxX, saveBoxY, kw, __ui_kb_box.h);
|
|
60
|
-
display_writePixels(display_canvasBuffer(__ui_kb_canvas), static_cast<uint32_t>(kw) * __ui_kb_box.h);
|
|
61
|
-
} else {
|
|
62
|
-
display_setAddrWindow(saveBoxX, saveBoxY, kw, UI_KB_TEXT_H);
|
|
63
|
-
display_writePixels(display_canvasBuffer(__ui_kb_canvas), static_cast<uint32_t>(kw) * UI_KB_TEXT_H);
|
|
64
|
-
}
|
|
65
|
-
display_endWrite();
|
|
66
|
-
} else {
|
|
67
|
-
// No canvas — fall back to direct draw (slow but correct).
|
|
68
|
-
ui_kb_draw_text_row();
|
|
69
|
-
if (__ui_kb_repaint_key >= 0 && __ui_kb_keys[__ui_kb_repaint_key].special != 255) {
|
|
70
|
-
ui_kb_draw_key(static_cast<uint8_t>(__ui_kb_repaint_key));
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
__ui_kb_dirty = 0;
|
|
75
|
-
__ui_kb_repaint_key = -1;
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
#endif
|
|
79
|
-
// Select and seed the framebuffer before scroll compositors run. Band/list
|
|
80
|
-
// renderers can then composite into this same off-screen target instead of
|
|
81
|
-
// pushing their bands to the live panel before the main draw pass starts.
|
|
82
|
-
// This prevents intermediate software-rendering seams; the final SPI transfer
|
|
83
|
-
// remains sequential on panels without TE/vblank synchronization.
|
|
84
|
-
CuttlefishCanvas16* __ui_fb = ui_get_framebuffer();
|
|
85
|
-
CuttlefishDisplayTarget* __ui_draw_target = __ui_fb ? (CuttlefishDisplayTarget*)__ui_fb : display_defaultTarget();
|
|
86
|
-
ui_fb_begin_frame();
|
|
87
|
-
// A retained framebuffer only needs an SPI transfer when composition changed.
|
|
88
|
-
// Avoid pushing a full frame on idle ticks; this is the main steady-state cost
|
|
89
|
-
// of the PSRAM path on otherwise static screens.
|
|
90
|
-
uint8_t __ui_fb_frame_dirty = 0;
|
|
91
|
-
// Whether any node painted this tick. The select overlay below re-stamps
|
|
92
|
-
// only when it or the frame beneath changed; an unconditional stamp pushes
|
|
93
|
-
// the modal region over SPI every frame, which reads as constant
|
|
94
|
-
// refreshing/tearing on no-TE TFT panels.
|
|
95
|
-
uint8_t __ui_frame_painted = 0;
|
|
96
|
-
if (__ui_fb && __ui_fb_needs_compose) {
|
|
97
|
-
UI_COLOR_T fbBg = (UI_COLOR_T)0;
|
|
98
|
-
if (__ui_active_screen_bg_node < __ui_node_count) {
|
|
99
|
-
fbBg = __ui_nodes[__ui_active_screen_bg_node].hasBg
|
|
100
|
-
? __ui_nodes[__ui_active_screen_bg_node].bg
|
|
101
|
-
: __ui_nodes[__ui_active_screen_bg_node].clearColor;
|
|
102
|
-
}
|
|
103
|
-
display_canvasFillScreen(__ui_fb, fbBg);
|
|
104
|
-
ui_fb_add_rect(0, 0, display_width(), display_height());
|
|
105
|
-
// A retained framebuffer cannot reuse the previous screen's pixels. Mark
|
|
106
|
-
// every node on the active screen dirty for one coherent composition.
|
|
107
|
-
for (uint16_t f = 0; f < __ui_node_count; f++) {
|
|
108
|
-
if (__ui_nodes[f].screenId == __ui_active_screen) {
|
|
109
|
-
__ui_nodes[f].dirty = 1;
|
|
110
|
-
__ui_nodes[f].lastTextHeight = 0;
|
|
111
|
-
__ui_nodes[f].layoutCacheKey = 0;
|
|
112
|
-
if (__ui_nodes[f].kind == NODE_PROGRESS || __ui_nodes[f].kind == NODE_RANGE) __ui_nodes[f].lastTextWidth = -1;
|
|
113
|
-
else __ui_nodes[f].lastTextWidth = 0;
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
__ui_fb_needs_compose = 0;
|
|
117
|
-
__ui_fb_frame_dirty = 1;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (__ui_scroll_render_locked) {
|
|
121
|
-
for (uint16_t lock = 0; lock < __ui_node_count; lock++) __ui_scroll_render_locked[lock] = 0;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// Process each dirty scroll container (Mode B shift-and-repair). A scroll
|
|
125
|
-
// delta shifts existing canvas pixels by the delta and repaints only the
|
|
126
|
-
// newly-exposed strip; a full invalidation (or no canvas) redraws the subtree.
|
|
127
|
-
// One container per frame — the active scroll owner repaints via its canvas.
|
|
128
|
-
int16_t bufferedScrollNode = -1; // int16: node index can exceed 127
|
|
129
|
-
int16_t bufferedScrollVX = 0; // viewport origin X for coord translation
|
|
130
|
-
int16_t bufferedScrollVY = 0; // viewport origin Y
|
|
131
|
-
CuttlefishCanvas16* bufferedScrollCanvas = nullptr;
|
|
132
|
-
CuttlefishCanvas16* bufferedScrollRepaintCanvas = nullptr;
|
|
133
|
-
int16_t bufferedScrollRepaintY = 0;
|
|
134
|
-
int16_t bufferedScrollRepaintH = 0;
|
|
135
|
-
// Kept as a compatibility flag for the old compositor state machine. It is
|
|
136
|
-
// never enabled: direct per-node SPI drawing is a visible tearing path, so a
|
|
137
|
-
// missing canvas freezes the affected viewport instead.
|
|
138
|
-
uint8_t bufferedScrollDirectFull = 0;
|
|
139
|
-
// Local retained-canvas repair: dirty descendants draw directly into the
|
|
140
|
-
// full-frame framebuffer while the existing scroll canvas is invalidated for
|
|
141
|
-
// the next actual scroll. This avoids copying an unchanged viewport.
|
|
142
|
-
uint8_t bufferedScrollLocalRepair = 0;
|
|
143
|
-
// Band renderer pending: like bufferedScrollDirectFull but the actual paint
|
|
144
|
-
// is deferred to the scroll owner's z-order slot in the main draw loop. The
|
|
145
|
-
// band composites the whole subtree at once, so it must paint AFTER lower-z
|
|
146
|
-
// nodes (e.g. the screen background fill) but BEFORE higher-z ones (header).
|
|
147
|
-
// Painting it here (before the main loop) lets the bg fill overwrite it.
|
|
148
|
-
uint8_t bufferedScrollBands = 0;
|
|
149
|
-
for (uint16_t oi = 0; ; oi++) {
|
|
150
|
-
uint16_t s;
|
|
151
|
-
if (__ui_scroll_owners) {
|
|
152
|
-
if (oi >= __ui_scroll_owner_count) break;
|
|
153
|
-
s = __ui_scroll_owners[oi];
|
|
154
|
-
} else {
|
|
155
|
-
if (oi >= __ui_node_count) break;
|
|
156
|
-
s = oi;
|
|
157
|
-
if (!__ui_nodes[s].scrollable || __ui_nodes[s].virtualized) continue;
|
|
158
|
-
}
|
|
159
|
-
if (!ui_is_effectively_visible(s)) continue;
|
|
160
|
-
if (__ui_nodes[s].screenId != __ui_active_screen) continue;
|
|
161
|
-
if (__ui_nodes[s].contentHeight <= __ui_nodes[s].box.h) continue;
|
|
162
|
-
if (!__ui_nodes[s].dirty) continue;
|
|
163
|
-
// Virtualized lists manage their own rendering (NODE_LIST case in
|
|
164
|
-
// ui_draw_node_body). Skip them in the scroll dispatch — the band
|
|
165
|
-
// renderer walks their subtree (which is empty for virtualized lists),
|
|
166
|
-
// leaving the list node itself unpainted.
|
|
167
|
-
if (__ui_nodes[s].virtualized) continue;
|
|
168
|
-
// The retained framebuffer only needs the full viewport for actual scroll
|
|
169
|
-
// movement or the first composition. A local child update is repaired in
|
|
170
|
-
// the existing scroll canvas and contributes its own paint rect later;
|
|
171
|
-
// pushing the whole viewport here is the Forms-screen flash.
|
|
172
|
-
if (__ui_fb) __ui_fb_frame_dirty = 1;
|
|
173
|
-
|
|
174
|
-
int16_t vw = __ui_nodes[s].box.w;
|
|
175
|
-
int16_t vh = __ui_nodes[s].box.h;
|
|
176
|
-
int16_t vox = __ui_nodes[s].box.x;
|
|
177
|
-
int16_t voy = __ui_nodes[s].box.y;
|
|
178
|
-
UI_COLOR_T scrollBg = __ui_nodes[s].hasBg ? __ui_nodes[s].bg : __ui_nodes[s].clearColor;
|
|
179
|
-
bufferedScrollCanvas = ui_get_container_canvas(vw, vh);
|
|
180
|
-
// Track canvas-allocation success so ui_apply_scroll_delta can lock scrolling
|
|
181
|
-
// for containers whose canvas won't fit (frozen-but-not-torn contract).
|
|
182
|
-
if (__ui_scroll_canvas_ok) {
|
|
183
|
-
__ui_scroll_canvas_ok[s] = bufferedScrollCanvas ? 1 : 0;
|
|
184
|
-
}
|
|
185
|
-
if (bufferedScrollCanvas) {
|
|
186
|
-
bufferedScrollNode = static_cast<int16_t>(s);
|
|
187
|
-
bufferedScrollVX = vox;
|
|
188
|
-
bufferedScrollVY = voy;
|
|
189
|
-
|
|
190
|
-
// Mode B: shift delta = how far scrollY moved since this canvas was last
|
|
191
|
-
// painted. Small non-zero delta within one viewport → shift + repair strip.
|
|
192
|
-
int16_t deltaY = __ui_nodes[s].scrollY - __ui_nodes[s].lastPaintedScrollY;
|
|
193
|
-
int16_t absDelta = deltaY < 0 ? -deltaY : deltaY;
|
|
194
|
-
uint8_t canShift = (deltaY != 0 && absDelta < vh);
|
|
195
|
-
|
|
196
|
-
if (canShift) {
|
|
197
|
-
int16_t exposedY = 0;
|
|
198
|
-
int16_t exposedH = 0;
|
|
199
|
-
ui_shift_container_canvas(bufferedScrollCanvas, deltaY, scrollBg, &exposedY, &exposedH);
|
|
200
|
-
bufferedScrollRepaintCanvas = ui_get_repair_canvas(vw, exposedH);
|
|
201
|
-
if (bufferedScrollRepaintCanvas) {
|
|
202
|
-
bufferedScrollRepaintY = exposedY;
|
|
203
|
-
bufferedScrollRepaintH = exposedH;
|
|
204
|
-
display_canvasFillScreen(bufferedScrollRepaintCanvas, scrollBg);
|
|
205
|
-
UIRect exposed = { vox, static_cast<int16_t>(voy + exposedY), vw, exposedH };
|
|
206
|
-
for (uint16_t c = s + 1; c < __ui_nodes[s].subtreeEnd; c++) {
|
|
207
|
-
if (!ui_is_effectively_visible(c) || __ui_nodes[c].screenId != __ui_active_screen) {
|
|
208
|
-
__ui_nodes[c].dirty = 0;
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
if (!__ui_nodes[c].dirty && exposedH > 0) {
|
|
212
|
-
UIRect cr;
|
|
213
|
-
ui_node_current_paint_rect(c, &cr);
|
|
214
|
-
if (cr.w > 0 && cr.h > 0 &&
|
|
215
|
-
ui_rects_intersect(cr.x, cr.y, cr.w, cr.h, exposed.x, exposed.y, exposed.w, exposed.h)) {
|
|
216
|
-
__ui_nodes[c].dirty = 1;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
if (__ui_nodes[c].dirty) {
|
|
220
|
-
if (__ui_nodes[c].kind == NODE_PROGRESS) __ui_nodes[c].lastTextWidth = -1;
|
|
221
|
-
else if (__ui_nodes[c].kind == NODE_RANGE) __ui_nodes[c].lastTextWidth = -1;
|
|
222
|
-
__ui_nodes[c].lastTextHeight = 0;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
} else {
|
|
226
|
-
canShift = 0;
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
if (!canShift) {
|
|
230
|
-
// A zero-delta dirty scroll owner is usually a local child update (a
|
|
231
|
-
// form binding, button transition, or animation). Keep the retained
|
|
232
|
-
// canvas and its existing pixels in that case; the dirty descendants
|
|
233
|
-
// will redraw in place and the final framebuffer union stays local.
|
|
234
|
-
uint8_t localRepair = __ui_fb && deltaY == 0 &&
|
|
235
|
-
__ui_nodes[s].lastPaintedScrollY == __ui_nodes[s].scrollY &&
|
|
236
|
-
ui_scroll_subtree_has_dirty(s);
|
|
237
|
-
if (localRepair) {
|
|
238
|
-
// The framebuffer is already the authoritative composition surface for
|
|
239
|
-
// this zero-delta update. Let dirty descendants draw there directly;
|
|
240
|
-
// invalidate the retained scroll canvas after each child draw so it is
|
|
241
|
-
// rebuilt before the next scroll gesture.
|
|
242
|
-
bufferedScrollLocalRepair = 1;
|
|
243
|
-
bufferedScrollCanvas = nullptr;
|
|
244
|
-
bufferedScrollRepaintCanvas = nullptr;
|
|
245
|
-
} else {
|
|
246
|
-
// Initial composition, a large jump, or an owner-only invalidation:
|
|
247
|
-
// seed the whole viewport and repaint its complete stacking subtree.
|
|
248
|
-
for (uint16_t c = s; c < __ui_nodes[s].subtreeEnd; c++) {
|
|
249
|
-
__ui_nodes[c].dirty = 1;
|
|
250
|
-
if (__ui_nodes[c].kind == NODE_PROGRESS) __ui_nodes[c].lastTextWidth = -1;
|
|
251
|
-
else if (__ui_nodes[c].kind == NODE_RANGE) __ui_nodes[c].lastTextWidth = -1;
|
|
252
|
-
__ui_nodes[c].lastTextHeight = 0;
|
|
253
|
-
}
|
|
254
|
-
display_canvasFillScreen(bufferedScrollCanvas, scrollBg);
|
|
255
|
-
if (__ui_fb) ui_fb_add_rect(__ui_nodes[s].box.x, __ui_nodes[s].box.y,
|
|
256
|
-
__ui_nodes[s].box.w, __ui_nodes[s].box.h);
|
|
257
|
-
}
|
|
258
|
-
} else if (__ui_fb) {
|
|
259
|
-
// Shift-and-repair changes the entire viewport's visible mapping.
|
|
260
|
-
ui_fb_add_rect(__ui_nodes[s].box.x, __ui_nodes[s].box.y,
|
|
261
|
-
__ui_nodes[s].box.w, __ui_nodes[s].box.h);
|
|
262
|
-
}
|
|
263
|
-
// The scroll owner is represented by bufferedScrollCanvas this frame.
|
|
264
|
-
// Drawing it directly first clears the live display and makes scrolling
|
|
265
|
-
// visibly flash before the canvas is pushed.
|
|
266
|
-
__ui_nodes[s].dirty = 0;
|
|
267
|
-
} else {
|
|
268
|
-
// Canvas won't fit (no PSRAM / over budget). The band renderer composes the
|
|
269
|
-
// whole visible subtree into a short horizontal band canvas (vw ×
|
|
270
|
-
// UI_STRIP_BAND_HEIGHT) and pushes one band at a time, so each band
|
|
271
|
-
// completes before it touches the panel — tear-free, with ~10KB of SRAM
|
|
272
|
-
// regardless of program size. If the band canvas also cannot allocate,
|
|
273
|
-
// the viewport is retained rather than using direct per-node SPI draws.
|
|
274
|
-
if (__ui_scroll_canvas_ok) __ui_scroll_canvas_ok[s] = 0;
|
|
275
|
-
// Defer the band render to the scroll owner's z-order slot in the main
|
|
276
|
-
// draw loop (see bufferedScrollBands handling below). Painting here would
|
|
277
|
-
// let lower-z nodes (screen bg fill) draw over the bands afterward. The
|
|
278
|
-
// owner stays dirty so the main loop reaches its z-slot and triggers the
|
|
279
|
-
// render; the band code clears it once painted. The subtree children are
|
|
280
|
-
// cleared now so the main loop never draws them directly — the band path
|
|
281
|
-
// is the sole renderer for the subtree this frame.
|
|
282
|
-
bufferedScrollNode = static_cast<int16_t>(s);
|
|
283
|
-
bufferedScrollVX = vox;
|
|
284
|
-
bufferedScrollVY = voy;
|
|
285
|
-
bufferedScrollBands = 1;
|
|
286
|
-
for (uint16_t c = s + 1; c < __ui_nodes[s].subtreeEnd; c++) {
|
|
287
|
-
__ui_nodes[c].dirty = 0;
|
|
288
|
-
}
|
|
289
|
-
break;
|
|
290
|
-
} // Only one scroll container per frame (the canvas is reused for subsequent
|
|
291
|
-
// ones in the next dirty frame). This matches the original design.
|
|
292
|
-
break;
|
|
293
|
-
}
|
|
294
|
-
// The framebuffer target was selected and seeded before scroll compositors;
|
|
295
|
-
// the stacking draw pass below uses the same __ui_draw_target. The retained
|
|
296
|
-
// single buffer is a composition surface, not a synchronized panel swap.
|
|
297
|
-
|
|
298
|
-
// Per-frame compose-region inflation (set by the overlap classification
|
|
299
|
-
// below, consumed by the drawer subtree compose).
|
|
300
|
-
for (uint8_t ds = 0; ds < UI_DRAWER_MAX; ds++) __ui_drawer_inflate_used[ds] = 0;
|
|
301
|
-
|
|
302
|
-
// ── Dirty-rect merging (LVGL refresh-cycle technique) ─────────────────────
|
|
303
|
-
// When several nodes repaint this frame, per-node transactions each pay
|
|
304
|
-
// SPI setup overhead. Group the frame's dirty paint rects into merged
|
|
305
|
-
// regions (inflate + greedy absorb, waste-capped) and composite each
|
|
306
|
-
// multi-node region through the band renderer in ONE set of pushes.
|
|
307
|
-
// Single-node and unmergeable rects keep the ordinary per-node paths.
|
|
308
|
-
// Skipped entirely when a retained framebuffer composes the frame (it
|
|
309
|
-
// already unions), when the keyboard overlay owns the screen, and for
|
|
310
|
-
// nodes inside overflow scroll subtrees (the Mode B machinery owns them).
|
|
311
|
-
if (!__ui_fb) {
|
|
312
|
-
UIRect __ui_merge_rects[UI_MERGE_MAX_REGIONS];
|
|
313
|
-
uint16_t __ui_merge_nodes[UI_MERGE_MAX_REGIONS][UI_MERGE_MAX_NODES];
|
|
314
|
-
uint16_t __ui_merge_counts[UI_MERGE_MAX_REGIONS];
|
|
315
|
-
uint16_t __ui_merge_region_count = 0;
|
|
316
|
-
for (uint16_t mi = 0; mi < __ui_node_count && __ui_merge_region_count < UI_MERGE_MAX_REGIONS; mi++) {
|
|
317
|
-
uint16_t m = __ui_draw_order ? __ui_draw_order[mi] : mi;
|
|
318
|
-
if (!__ui_nodes[m].dirty) continue;
|
|
319
|
-
if (__ui_nodes[m].screenId != __ui_active_screen) continue;
|
|
320
|
-
if (!ui_is_effectively_visible(m)) continue;
|
|
321
|
-
if (__ui_nodes[m].kind == NODE_LIST || __ui_nodes[m].virtualized) continue;
|
|
322
|
-
if (ui_overflow_scroll_compositor(m) >= 0) continue; // scroll machinery owns it
|
|
323
|
-
// A dirty ancestor's ladder turn owns this node's repaint: the ancestor
|
|
324
|
-
// composes its whole subtree region (or ladders and re-marks its
|
|
325
|
-
// descendants). Merging the node here composes it first, and the
|
|
326
|
-
// ancestor's clear+fill then erases it — the erase + re-pop sequence
|
|
327
|
-
// was the visible flash when pressing a button inside an open drawer.
|
|
328
|
-
{
|
|
329
|
-
uint16_t ap = __ui_nodes[m].parent;
|
|
330
|
-
while (ap != UI_NO_PARENT && ap < __ui_node_count) {
|
|
331
|
-
if (__ui_nodes[ap].dirty &&
|
|
332
|
-
__ui_nodes[ap].screenId == __ui_active_screen) break;
|
|
333
|
-
ap = __ui_nodes[ap].parent;
|
|
334
|
-
}
|
|
335
|
-
if (ap != UI_NO_PARENT && ap < __ui_node_count) continue;
|
|
336
|
-
}
|
|
337
|
-
UIRect mr;
|
|
338
|
-
ui_node_current_paint_rect(m, &mr);
|
|
339
|
-
if (mr.w <= 0 || mr.h <= 0) continue;
|
|
340
|
-
// Greedy absorb into an existing region when they overlap after a small
|
|
341
|
-
// inflation and the merged waste stays bounded.
|
|
342
|
-
uint8_t absorbed = 0;
|
|
343
|
-
for (uint16_t r = 0; r < __ui_merge_region_count && !absorbed; r++) {
|
|
344
|
-
UIRect* reg = &__ui_merge_rects[r];
|
|
345
|
-
int16_t x0 = reg->x - UI_MERGE_INFLATE, y0 = reg->y - UI_MERGE_INFLATE;
|
|
346
|
-
int16_t x1 = static_cast<int16_t>(reg->x + reg->w + UI_MERGE_INFLATE), y1 = static_cast<int16_t>(reg->y + reg->h + UI_MERGE_INFLATE);
|
|
347
|
-
if (mr.x >= x1 || static_cast<int16_t>(mr.x + mr.w) <= x0 ||
|
|
348
|
-
mr.y >= y1 || static_cast<int16_t>(mr.y + mr.h) <= y0) continue;
|
|
349
|
-
int16_t nx0 = reg->x < mr.x ? reg->x : mr.x;
|
|
350
|
-
int16_t ny0 = reg->y < mr.y ? reg->y : mr.y;
|
|
351
|
-
int16_t nx1 = static_cast<int16_t>(reg->x + reg->w > mr.x + mr.w ? reg->x + reg->w : mr.x + mr.w);
|
|
352
|
-
int16_t ny1 = static_cast<int16_t>(reg->y + reg->h > mr.y + mr.h ? reg->y + reg->h : mr.y + mr.h);
|
|
353
|
-
uint32_t mergedArea = static_cast<uint32_t>(nx1 - nx0) * static_cast<uint32_t>(ny1 - ny0);
|
|
354
|
-
if (mergedArea > UI_MAX_BUFFERED_PAINT_PIXELS) continue; // too big to band-composite
|
|
355
|
-
if (__ui_merge_counts[r] >= UI_MERGE_MAX_NODES) continue;
|
|
356
|
-
reg->x = nx0; reg->y = ny0;
|
|
357
|
-
reg->w = static_cast<int16_t>(nx1 - nx0); reg->h = static_cast<int16_t>(ny1 - ny0);
|
|
358
|
-
__ui_merge_nodes[r][__ui_merge_counts[r]++] = m;
|
|
359
|
-
absorbed = 1;
|
|
360
|
-
}
|
|
361
|
-
if (!absorbed) {
|
|
362
|
-
__ui_merge_rects[__ui_merge_region_count] = mr;
|
|
363
|
-
__ui_merge_nodes[__ui_merge_region_count][0] = m;
|
|
364
|
-
__ui_merge_counts[__ui_merge_region_count] = 1;
|
|
365
|
-
__ui_merge_region_count++;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
// Composite multi-node regions as one banded render; singletons keep the
|
|
369
|
-
// per-node ladder (repair canvas etc. is cheaper for one node).
|
|
370
|
-
for (uint16_t r = 0; r < __ui_merge_region_count; r++) {
|
|
371
|
-
if (__ui_merge_counts[r] < 2) continue;
|
|
372
|
-
const UIRect* reg = &__ui_merge_rects[r];
|
|
373
|
-
if (ui_render_screen_bands(reg->x, reg->y, reg->w, reg->h)) {
|
|
374
|
-
for (uint16_t k = 0; k < __ui_merge_counts[r]; k++) {
|
|
375
|
-
__ui_nodes[__ui_merge_nodes[r][k]].dirty = 0;
|
|
376
|
-
ui_invalidate_scroll_canvas_for_node(__ui_merge_nodes[r][k]);
|
|
377
|
-
ui_refresh_add_rect(__ui_nodes[__ui_merge_nodes[r][k]].box.x, __ui_nodes[__ui_merge_nodes[r][k]].box.y,
|
|
378
|
-
__ui_nodes[__ui_merge_nodes[r][k]].box.w, __ui_nodes[__ui_merge_nodes[r][k]].box.h);
|
|
379
|
-
}
|
|
380
|
-
__ui_frame_painted = 1;
|
|
381
|
-
}
|
|
382
|
-
// Band allocation failure: leave the nodes dirty; the per-node ladder
|
|
383
|
-
// below still repaints them correctly.
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// Draw dirty nodes in stacking order: lower z-index first, then source order.
|
|
388
|
-
// __ui_draw_order is built once in ui_init so each frame is O(N).
|
|
389
|
-
for (uint16_t __ui_draw_pass = 0; __ui_draw_pass < __ui_node_count; ) {
|
|
390
|
-
int16_t i;
|
|
391
|
-
if (__ui_draw_order) {
|
|
392
|
-
i = static_cast<int16_t>(__ui_draw_order[__ui_draw_pass++]);
|
|
393
|
-
if (!__ui_nodes[i].dirty) continue;
|
|
394
|
-
if (__ui_fb) __ui_fb_frame_dirty = 1;
|
|
395
|
-
if (!ui_is_effectively_visible(i)) { __ui_nodes[i].dirty = 0; continue; }
|
|
396
|
-
if (__ui_nodes[i].screenId != __ui_active_screen) { __ui_nodes[i].dirty = 0; continue; }
|
|
397
|
-
} else {
|
|
398
|
-
// malloc failed at startup: preserve correct z-order via selection sort.
|
|
399
|
-
i = -1;
|
|
400
|
-
for (uint16_t candidate = 0; candidate < __ui_node_count; candidate++) {
|
|
401
|
-
if (!__ui_nodes[candidate].dirty) continue;
|
|
402
|
-
if (__ui_fb) __ui_fb_frame_dirty = 1;
|
|
403
|
-
if (!ui_is_effectively_visible(candidate)) { __ui_nodes[candidate].dirty = 0; continue; }
|
|
404
|
-
if (__ui_nodes[candidate].screenId != __ui_active_screen) { __ui_nodes[candidate].dirty = 0; continue; }
|
|
405
|
-
if (i < 0 || ui_node_draws_before(candidate, static_cast<uint16_t>(i))) i = static_cast<int16_t>(candidate);
|
|
406
|
-
}
|
|
407
|
-
if (i < 0) break;
|
|
408
|
-
__ui_draw_pass++;
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
// Open-drawer overlap classification: the drawer subtree draws above other
|
|
412
|
-
// content (z-order + later draw slot). A dirty node underneath would
|
|
413
|
-
// paint its clear+redraw over the panel's pixels — a bound text behind an
|
|
414
|
-
// open drawer erases the drawer's buttons on repaint. Fully covered by an
|
|
415
|
-
// opaque open drawer → none of it is visible; skip the repaint entirely.
|
|
416
|
-
// Partially covered → re-dirty that drawer's subtree so it re-stamps on
|
|
417
|
-
// top later in this same pass (its z sorts it after the covered node).
|
|
418
|
-
if (__ui_drawer_slots > 0) {
|
|
419
|
-
for (uint8_t dslot = 0; dslot < __ui_drawer_slots; dslot++) {
|
|
420
|
-
if (__ui_drawer_idx[dslot] < 0) continue;
|
|
421
|
-
uint16_t droot = static_cast<uint16_t>(__ui_drawer_idx[dslot]);
|
|
422
|
-
if (static_cast<uint16_t>(i) == droot ||
|
|
423
|
-
(i > static_cast<int16_t>(droot) && static_cast<uint16_t>(i) < __ui_nodes[droot].subtreeEnd)) continue;
|
|
424
|
-
if (__ui_drawer_progress[dslot] < 255) continue; // closed or sliding
|
|
425
|
-
int16_t dX = ui_draw_x_for_node(droot);
|
|
426
|
-
int16_t dY = ui_draw_y_for_node(droot);
|
|
427
|
-
int16_t nX = ui_draw_x_for_node(static_cast<uint16_t>(i));
|
|
428
|
-
int16_t nY = ui_draw_y_for_node(static_cast<uint16_t>(i));
|
|
429
|
-
if (!ui_rects_intersect(nX, nY, __ui_nodes[i].box.w, __ui_nodes[i].box.h,
|
|
430
|
-
dX, dY, __ui_nodes[droot].box.w, __ui_nodes[droot].box.h)) continue;
|
|
431
|
-
uint8_t fullyCovered = (nX >= dX && nY >= dY &&
|
|
432
|
-
nX + __ui_nodes[i].box.w <= dX + __ui_nodes[droot].box.w &&
|
|
433
|
-
nY + __ui_nodes[i].box.h <= dY + __ui_nodes[droot].box.h) ? 1 : 0;
|
|
434
|
-
if (fullyCovered && __ui_nodes[droot].hasBg && __ui_nodes[droot].opacity >= 100) {
|
|
435
|
-
__ui_nodes[i].dirty = 0;
|
|
436
|
-
} else if (ui_overflow_scroll_compositor(droot) >= 0) {
|
|
437
|
-
// Partial coverage, but the drawer sits inside an overflow scroll
|
|
438
|
-
// subtree that is NOT being composited this frame. Re-dirtying the
|
|
439
|
-
// drawer here sends its whole subtree through the defer fall-through
|
|
440
|
-
// (direct band/paint-canvas pushes over a scroll viewport) — the
|
|
441
|
-
// tearing path AGENTS.md forbids, and on hardware it corrupts the
|
|
442
|
-
// panel content (drawer text and buttons vanish while remaining
|
|
443
|
-
// tappable). Skip the covered node instead: its uncovered sliver
|
|
444
|
-
// (typically a few px of a full-width label) is not worth the
|
|
445
|
-
// unsafe redraw. When the scroll owner IS composited (dirty this
|
|
446
|
-
// frame), the re-dirty below is safe — the composition redraws the
|
|
447
|
-
// whole viewport including the drawer, in order.
|
|
448
|
-
__ui_nodes[i].dirty = 0;
|
|
449
|
-
} else {
|
|
450
|
-
// Partial coverage, display path. The node's own ladder would paint
|
|
451
|
-
// its FULL rect — including the covered part — straight over the
|
|
452
|
-
// open panel, flashing underlying content as a bar until the panel
|
|
453
|
-
// re-stamps. Skip its direct repaint and let the drawer's subtree
|
|
454
|
-
// compose paint it (stacking order puts it under the panel); that
|
|
455
|
-
// compose region is inflated to reach the node's uncovered sliver.
|
|
456
|
-
UIRect nr = { nX, nY, __ui_nodes[i].box.w, __ui_nodes[i].box.h };
|
|
457
|
-
if (!__ui_drawer_inflate_used[dslot]) {
|
|
458
|
-
__ui_drawer_inflate_rect[dslot] = nr;
|
|
459
|
-
__ui_drawer_inflate_used[dslot] = 1;
|
|
460
|
-
} else {
|
|
461
|
-
UIRect* inf = &__ui_drawer_inflate_rect[dslot];
|
|
462
|
-
int16_t x1 = static_cast<int16_t>(inf->x + inf->w > nr.x + nr.w ? inf->x + inf->w : nr.x + nr.w);
|
|
463
|
-
int16_t y1 = static_cast<int16_t>(inf->y + inf->h > nr.y + nr.h ? inf->y + inf->h : nr.y + nr.h);
|
|
464
|
-
if (nr.x < inf->x) inf->x = nr.x;
|
|
465
|
-
if (nr.y < inf->y) inf->y = nr.y;
|
|
466
|
-
inf->w = static_cast<int16_t>(x1 - inf->x);
|
|
467
|
-
inf->h = static_cast<int16_t>(y1 - inf->y);
|
|
468
|
-
}
|
|
469
|
-
__ui_nodes[i].dirty = 0;
|
|
470
|
-
__ui_nodes[droot].dirty = 1;
|
|
471
|
-
__ui_nodes[droot].lastTextHeight = 0;
|
|
472
|
-
for (uint16_t c = droot + 1; c < __ui_nodes[droot].subtreeEnd; c++) {
|
|
473
|
-
__ui_nodes[c].dirty = 1;
|
|
474
|
-
__ui_nodes[c].lastTextHeight = 0;
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
break;
|
|
478
|
-
}
|
|
479
|
-
if (!__ui_nodes[i].dirty) continue;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
// Defer direct display draws for overflow scroll subtrees not composited this
|
|
483
|
-
// frame (Mode B canvas, Mode C strip, direct-full fallback, or band render).
|
|
484
|
-
// Drawing them directly clears the live viewport and produces sequential
|
|
485
|
-
// flashes (AGENTS.md) — except in direct-full mode, where there is no canvas
|
|
486
|
-
// and direct draw is the only way to show content.
|
|
487
|
-
{
|
|
488
|
-
int16_t scrollComp = ui_overflow_scroll_compositor(static_cast<uint16_t>(i));
|
|
489
|
-
if (scrollComp >= 0) {
|
|
490
|
-
uint8_t compositing = scrollComp == bufferedScrollNode &&
|
|
491
|
-
(bufferedScrollCanvas || bufferedScrollDirectFull || bufferedScrollBands || bufferedScrollLocalRepair);
|
|
492
|
-
if (!compositing) {
|
|
493
|
-
if (static_cast<uint16_t>(i) == static_cast<uint16_t>(scrollComp)) break;
|
|
494
|
-
// Not composited this frame. A child FULLY inside its scroll
|
|
495
|
-
// viewport repaints directly through its own buffered paint (paint
|
|
496
|
-
// canvas or bands — a single atomic push; the epilogue invalidates
|
|
497
|
-
// the retained scroll canvas so the next scroll recomposes). Only
|
|
498
|
-
// partially/fully clipped children defer: their edge pixels must
|
|
499
|
-
// come from the composited canvas, and drawing them directly would
|
|
500
|
-
// paint outside the viewport.
|
|
501
|
-
if (ui_is_rect_clipped_by_scroll(static_cast<uint16_t>(i),
|
|
502
|
-
ui_draw_x_for_node(static_cast<uint16_t>(i)),
|
|
503
|
-
ui_draw_y_for_node(static_cast<uint16_t>(i)),
|
|
504
|
-
__ui_nodes[i].box.w, __ui_nodes[i].box.h)) {
|
|
505
|
-
__ui_nodes[i].dirty = 0;
|
|
506
|
-
continue;
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
// Band renderer: paint the whole subtree at the scroll owner's z-order slot.
|
|
513
|
-
// This must run AFTER lower-z nodes (e.g. the screen background fill, which
|
|
514
|
-
// has lower source order) have been drawn, so they don't overwrite the bands.
|
|
515
|
-
// When the main loop reaches the scroll owner itself, render the bands now
|
|
516
|
-
// (its z-position), mark the owner handled, and continue.
|
|
517
|
-
if (bufferedScrollBands && bufferedScrollNode >= 0 &&
|
|
518
|
-
static_cast<uint16_t>(i) == static_cast<uint16_t>(bufferedScrollNode)) {
|
|
519
|
-
if (ui_render_scroll_bands(static_cast<uint16_t>(bufferedScrollNode))) {
|
|
520
|
-
__ui_frame_painted = 1;
|
|
521
|
-
bufferedScrollBands = 0;
|
|
522
|
-
bufferedScrollNode = -1;
|
|
523
|
-
__ui_nodes[i].dirty = 0;
|
|
524
|
-
continue;
|
|
525
|
-
}
|
|
526
|
-
// Band canvas allocation failed. Retain the existing viewport pixels rather
|
|
527
|
-
// than falling back to direct-full SPI primitives; direct-full is the
|
|
528
|
-
// tearing path this compositor is designed to prevent.
|
|
529
|
-
bufferedScrollBands = 0;
|
|
530
|
-
if (__ui_scroll_render_locked) __ui_scroll_render_locked[static_cast<uint16_t>(bufferedScrollNode)] = 1;
|
|
531
|
-
__ui_nodes[static_cast<uint16_t>(bufferedScrollNode)].dirty = 0;
|
|
532
|
-
bufferedScrollNode = -1;
|
|
533
|
-
} // Band renderer early-push: if we're about to draw a node that comes AFTER
|
|
534
|
-
// the scroll owner in draw order (e.g. a header with higher source order)
|
|
535
|
-
// and the bands haven't rendered yet, render them first so they land below
|
|
536
|
-
// that higher-z node. Mirrors the Mode B canvas early-push below.
|
|
537
|
-
if (bufferedScrollBands && bufferedScrollNode >= 0 &&
|
|
538
|
-
!(i > bufferedScrollNode && i < __ui_nodes[bufferedScrollNode].subtreeEnd) &&
|
|
539
|
-
ui_node_draws_before(static_cast<uint16_t>(bufferedScrollNode), static_cast<uint16_t>(i))) {
|
|
540
|
-
if (ui_render_scroll_bands(static_cast<uint16_t>(bufferedScrollNode))) {
|
|
541
|
-
__ui_frame_painted = 1;
|
|
542
|
-
bufferedScrollBands = 0;
|
|
543
|
-
bufferedScrollNode = -1;
|
|
544
|
-
} else {
|
|
545
|
-
// Band alloc failed — retain the previous viewport rather than exposing
|
|
546
|
-
// a direct-full per-primitive SPI repaint.
|
|
547
|
-
bufferedScrollBands = 0;
|
|
548
|
-
if (__ui_scroll_render_locked) __ui_scroll_render_locked[static_cast<uint16_t>(bufferedScrollNode)] = 1;
|
|
549
|
-
__ui_nodes[static_cast<uint16_t>(bufferedScrollNode)].dirty = 0;
|
|
550
|
-
bufferedScrollNode = -1;
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Mode C strip / direct-full: scroll owner is not drawn directly (would fill
|
|
555
|
-
// the viewport); its children are drawn directly below.
|
|
556
|
-
if ((bufferedScrollDirectFull || bufferedScrollLocalRepair) && bufferedScrollNode >= 0 &&
|
|
557
|
-
static_cast<uint16_t>(i) == static_cast<uint16_t>(bufferedScrollNode)) {
|
|
558
|
-
__ui_nodes[i].dirty = 0;
|
|
559
|
-
continue;
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
if (bufferedScrollNode >= 0 && bufferedScrollCanvas &&
|
|
563
|
-
!(i > bufferedScrollNode && i < __ui_nodes[bufferedScrollNode].subtreeEnd) &&
|
|
564
|
-
ui_node_draws_before(static_cast<uint16_t>(bufferedScrollNode), static_cast<uint16_t>(i)) &&
|
|
565
|
-
!ui_scroll_subtree_has_dirty(static_cast<uint16_t>(bufferedScrollNode))) {
|
|
566
|
-
ui_push_buffered_scroll_canvas(bufferedScrollCanvas, bufferedScrollRepaintCanvas,
|
|
567
|
-
bufferedScrollNode, bufferedScrollVX, bufferedScrollVY,
|
|
568
|
-
bufferedScrollRepaintY, bufferedScrollRepaintH, __ui_draw_target);
|
|
569
|
-
__ui_frame_painted = 1;
|
|
570
|
-
bufferedScrollNode = -1;
|
|
571
|
-
bufferedScrollCanvas = nullptr;
|
|
572
|
-
bufferedScrollRepaintCanvas = nullptr;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
// Ladder-drawing a CONTAINER (fill/view panel) clears and redraws its
|
|
576
|
-
// whole paint rect — everything drawn inside it (its descendants) is
|
|
577
|
-
// erased by this pass. Mark-time propagation (ui_mark_dirty) already
|
|
578
|
-
// marks descendants, but the merge pass composes and CLEARS some of
|
|
579
|
-
// them before this node's ladder turn — a press inside an open drawer
|
|
580
|
-
// left the panel for the ladder (too big to merge) while its texts and
|
|
581
|
-
// buttons were merge-composed first; the panel's clear+fill then erased
|
|
582
|
-
// them and nothing re-drew them. Re-mark the descendants here: they sort
|
|
583
|
-
// after this node, so they restore their pixels later in this same pass.
|
|
584
|
-
// Scroll owners never reach this point (the compositor paths skip them),
|
|
585
|
-
// so scroll-subtree repaints do not amplify.
|
|
586
|
-
if (__ui_nodes[i].subtreeEnd > static_cast<uint16_t>(i) + 1 &&
|
|
587
|
-
__ui_nodes[i].kind != NODE_LIST) {
|
|
588
|
-
for (uint16_t c = static_cast<uint16_t>(i) + 1;
|
|
589
|
-
c < __ui_nodes[i].subtreeEnd && c < __ui_node_count; c++) {
|
|
590
|
-
if (!__ui_nodes[c].visible) continue;
|
|
591
|
-
if (__ui_nodes[c].screenId != __ui_active_screen) continue;
|
|
592
|
-
__ui_nodes[c].dirty = 1;
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// Redirect to the scroll canvas if this node is inside the buffered container
|
|
597
|
-
// AND a real canvas is active this frame (Mode B only). Direct-strip and
|
|
598
|
-
// direct-full have no canvas — their children must draw to the display target
|
|
599
|
-
// (and thus qualify for the per-node paint-canvas optimization), not redirect
|
|
600
|
-
// to a null scroll canvas. Without this, a strip-drag frame sets
|
|
601
|
-
// drawingBufferedScroll=1, which skips the paint canvas and forces the
|
|
602
|
-
// repainting children to direct-draw per-pixel to SPI (~100ms/text node).
|
|
603
|
-
uint8_t drawingBufferedScroll = !bufferedScrollDirectFull &&
|
|
604
|
-
(bufferedScrollCanvas != nullptr) && bufferedScrollNode >= 0 &&
|
|
605
|
-
i > bufferedScrollNode && i < __ui_nodes[bufferedScrollNode].subtreeEnd;
|
|
606
|
-
// A container with descendants reaches its ladder turn only when the
|
|
607
|
-
// merge pass couldn't own it (too big to merge, or a dirty ancestor
|
|
608
|
-
// excluded the children). The ladder clears + redraws just this node —
|
|
609
|
-
// erasing every descendant pixel — and the re-marked descendants would
|
|
610
|
-
// then pop back one ladder at a time: an erase + re-pop the eye reads
|
|
611
|
-
// as a flash (pressing a button inside an open drawer). Instead compose
|
|
612
|
-
// the container's whole subtree REGION through the band renderer — one
|
|
613
|
-
// pass, correct stacking order, no intermediate state — and clear every
|
|
614
|
-
// dirty node inside the region (the compose already painted them in
|
|
615
|
-
// final form). Falls back to the ladder + re-marks when the band canvas
|
|
616
|
-
// can't allocate. Display path only: into an active scroll canvas the
|
|
617
|
-
// ladder draws (children re-mark into the canvas afterwards).
|
|
618
|
-
if (!drawingBufferedScroll &&
|
|
619
|
-
__ui_nodes[i].subtreeEnd > static_cast<uint16_t>(i) + 1 &&
|
|
620
|
-
__ui_nodes[i].kind != NODE_LIST) {
|
|
621
|
-
UIRect subtreeRegion;
|
|
622
|
-
if (ui_subtree_current_paint_rect(static_cast<uint16_t>(i), &subtreeRegion) &&
|
|
623
|
-
subtreeRegion.w > 0 && subtreeRegion.h > 0) {
|
|
624
|
-
// An open drawer's compose region reaches the nodes the overlap
|
|
625
|
-
// classification skipped this frame — they owe their uncovered
|
|
626
|
-
// sliver, and the compose paints them under the panel in stacking
|
|
627
|
-
// order.
|
|
628
|
-
int8_t islot = __ui_drawer_slot_of(static_cast<uint16_t>(i));
|
|
629
|
-
if (islot >= 0 && __ui_drawer_inflate_used[islot]) {
|
|
630
|
-
const UIRect* inf = &__ui_drawer_inflate_rect[islot];
|
|
631
|
-
int16_t x1 = static_cast<int16_t>(subtreeRegion.x + subtreeRegion.w > inf->x + inf->w ? subtreeRegion.x + subtreeRegion.w : inf->x + inf->w);
|
|
632
|
-
int16_t y1 = static_cast<int16_t>(subtreeRegion.y + subtreeRegion.h > inf->y + inf->h ? subtreeRegion.y + subtreeRegion.h : inf->y + inf->h);
|
|
633
|
-
if (inf->x < subtreeRegion.x) subtreeRegion.x = inf->x;
|
|
634
|
-
if (inf->y < subtreeRegion.y) subtreeRegion.y = inf->y;
|
|
635
|
-
subtreeRegion.w = static_cast<int16_t>(x1 - subtreeRegion.x);
|
|
636
|
-
subtreeRegion.h = static_cast<int16_t>(y1 - subtreeRegion.y);
|
|
637
|
-
__ui_drawer_inflate_used[islot] = 0;
|
|
638
|
-
}
|
|
639
|
-
if (ui_render_screen_bands(subtreeRegion.x, subtreeRegion.y, subtreeRegion.w, subtreeRegion.h)) {
|
|
640
|
-
__ui_frame_painted = 1;
|
|
641
|
-
ui_invalidate_scroll_canvas_for_node(static_cast<uint16_t>(i));
|
|
642
|
-
for (uint16_t q = 0; q < __ui_node_count; q++) {
|
|
643
|
-
if (__ui_nodes[q].screenId != __ui_active_screen) continue;
|
|
644
|
-
if (!__ui_nodes[q].dirty) continue;
|
|
645
|
-
UIRect qr;
|
|
646
|
-
ui_node_current_paint_rect(q, &qr);
|
|
647
|
-
if (qr.w <= 0 || qr.h <= 0) continue;
|
|
648
|
-
if (qr.x >= subtreeRegion.x && qr.y >= subtreeRegion.y &&
|
|
649
|
-
static_cast<int16_t>(qr.x + qr.w) <= static_cast<int16_t>(subtreeRegion.x + subtreeRegion.w) &&
|
|
650
|
-
static_cast<int16_t>(qr.y + qr.h) <= static_cast<int16_t>(subtreeRegion.y + subtreeRegion.h)) {
|
|
651
|
-
__ui_nodes[q].dirty = 0;
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
continue;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
int16_t origBoxX = __ui_nodes[i].box.x;
|
|
659
|
-
int16_t origBoxY = __ui_nodes[i].box.y;
|
|
660
|
-
if (drawingBufferedScroll) {
|
|
661
|
-
CuttlefishCanvas16* scrollDrawCanvas = bufferedScrollRepaintCanvas ? bufferedScrollRepaintCanvas : bufferedScrollCanvas;
|
|
662
|
-
ui_display_set_target(scrollDrawCanvas);
|
|
663
|
-
// Translate display coords → canvas-local coords (subtract viewport origin).
|
|
664
|
-
__ui_nodes[i].box.x = origBoxX - bufferedScrollVX;
|
|
665
|
-
__ui_nodes[i].box.y = origBoxY - bufferedScrollVY - (bufferedScrollRepaintCanvas ? bufferedScrollRepaintY : 0);
|
|
666
|
-
} else {
|
|
667
|
-
ui_display_set_target(__ui_draw_target);
|
|
668
|
-
}
|
|
669
|
-
int16_t baseDrawX = ui_base_draw_x_for_node(i);
|
|
670
|
-
int16_t baseDrawY = ui_base_draw_y_for_node(i);
|
|
671
|
-
int16_t drawX = ui_draw_x_for_node(i);
|
|
672
|
-
int16_t drawY = ui_draw_y_for_node(i);
|
|
673
|
-
// Source selection: text-bound nodes show their dynamic buffer; others show
|
|
674
|
-
// the immutable flash literal.
|
|
675
|
-
const char* displayText = __ui_nodes[i].hasTextBinding
|
|
676
|
-
? __ui_nodes[i].textBuffer
|
|
677
|
-
: __ui_nodes[i].text;
|
|
678
|
-
uint8_t ts = __ui_nodes[i].textSize ? __ui_nodes[i].textSize : 2;
|
|
679
|
-
uint16_t textMaxW = ui_node_text_max_width(i);
|
|
680
|
-
uint16_t tw = 0;
|
|
681
|
-
uint16_t th = 0;
|
|
682
|
-
ui_node_text_layout_metrics(i, textMaxW, &tw, &th);
|
|
683
|
-
uint16_t paintTextW = tw;
|
|
684
|
-
uint16_t paintTextH = th;
|
|
685
|
-
if (__ui_nodes[i].kind == NODE_TEXT || __ui_nodes[i].kind == NODE_SELECT) {
|
|
686
|
-
uint16_t hInset = static_cast<uint16_t>(__ui_nodes[i].paddingLeft) + static_cast<uint16_t>(__ui_nodes[i].paddingRight) + static_cast<uint16_t>(__ui_nodes[i].borderWidth) * 2;
|
|
687
|
-
uint16_t vInset = static_cast<uint16_t>(__ui_nodes[i].paddingTop) + static_cast<uint16_t>(__ui_nodes[i].paddingBottom) + static_cast<uint16_t>(__ui_nodes[i].borderWidth) * 2;
|
|
688
|
-
paintTextW = static_cast<uint16_t>(tw + hInset);
|
|
689
|
-
paintTextH = static_cast<uint16_t>(th + vInset);
|
|
690
|
-
}
|
|
691
|
-
if (__ui_nodes[i].kind == NODE_CHECK || __ui_nodes[i].kind == NODE_RADIO) {
|
|
692
|
-
paintTextW = tw + 22;
|
|
693
|
-
if (paintTextH < 16) paintTextH = 16;
|
|
694
|
-
}
|
|
695
|
-
|
|
696
|
-
UIRect paintRect;
|
|
697
|
-
ui_node_paint_rect(i, baseDrawX, baseDrawY, drawX, drawY, paintTextW, paintTextH, &paintRect);
|
|
698
|
-
if (drawingBufferedScroll) {
|
|
699
|
-
// Canvas-local clip: skip nodes fully outside the viewport (0..vw, 0..vh).
|
|
700
|
-
// Use the node's face rect (box.w/h), NOT the paint rect — the paint rect
|
|
701
|
-
// includes shadow/border extents, which legitimately overflow a scroll
|
|
702
|
-
// viewport. Clipping on the paint rect falsely rejects nodes whose shadow
|
|
703
|
-
// pokes past the container edge while the face is fully inside (showed up
|
|
704
|
-
// as the home-nav buttons never painting on ST7796S, where the nav
|
|
705
|
-
// container is exactly button-width).
|
|
706
|
-
int16_t faceX = drawX;
|
|
707
|
-
int16_t faceY = drawY;
|
|
708
|
-
int16_t faceW = __ui_nodes[i].box.w;
|
|
709
|
-
int16_t faceH = __ui_nodes[i].box.h;
|
|
710
|
-
CuttlefishCanvas16* scrollDrawCanvas = bufferedScrollRepaintCanvas ? bufferedScrollRepaintCanvas : bufferedScrollCanvas;
|
|
711
|
-
// Guard against a null canvas (the subtree was flagged drawingBufferedScroll
|
|
712
|
-
// but no canvas is available this frame — e.g. a second scroll owner whose
|
|
713
|
-
// canvas couldn't allocate). Skip the canvas-local clip and draw the node
|
|
714
|
-
// to the display target; the scroll-viewport clip below still bounds it.
|
|
715
|
-
if (scrollDrawCanvas) {
|
|
716
|
-
int16_t scrollDrawW = display_canvasWidth(scrollDrawCanvas);
|
|
717
|
-
int16_t scrollDrawH = display_canvasHeight(scrollDrawCanvas);
|
|
718
|
-
if (bufferedScrollRepaintCanvas) {
|
|
719
|
-
scrollDrawW = __ui_nodes[bufferedScrollNode].box.w;
|
|
720
|
-
scrollDrawH = bufferedScrollRepaintH;
|
|
721
|
-
}
|
|
722
|
-
if (faceY + faceH <= 0 || faceY >= scrollDrawH ||
|
|
723
|
-
faceX + faceW <= 0 || faceX >= scrollDrawW) {
|
|
724
|
-
__ui_nodes[i].box.x = origBoxX;
|
|
725
|
-
__ui_nodes[i].box.y = origBoxY;
|
|
726
|
-
__ui_nodes[i].dirty = 0;
|
|
727
|
-
continue;
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
} else if (ui_is_rect_clipped_by_scroll(i, drawX, drawY, __ui_nodes[i].box.w, __ui_nodes[i].box.h)) {
|
|
731
|
-
__ui_nodes[i].box.x = origBoxX;
|
|
732
|
-
__ui_nodes[i].box.y = origBoxY;
|
|
733
|
-
__ui_nodes[i].dirty = 0;
|
|
734
|
-
continue;
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
uint8_t drawingPaintCanvas = 0;
|
|
738
|
-
CuttlefishCanvas16* paintCanvas = nullptr;
|
|
739
|
-
int16_t paintCanvasX = paintRect.x;
|
|
740
|
-
int16_t paintCanvasY = paintRect.y;
|
|
741
|
-
int16_t paintCanvasW = paintRect.w;
|
|
742
|
-
int16_t paintCanvasH = paintRect.h;
|
|
743
|
-
// RAM-composite pixel-heavy nodes before SPI push. Skip when already drawing
|
|
744
|
-
// into a scroll canvas or a full-screen framebuffer (both are RAM targets).
|
|
745
|
-
uint8_t wantedBuffer = !drawingBufferedScroll && !__ui_fb && ui_should_buffer_paint(i, paintCanvasW, paintCanvasH);
|
|
746
|
-
// Large paint rects prefer the band renderer (small ~10KB SRAM canvas,
|
|
747
|
-
// strip-at-a-time) over the repair canvas even when the repair canvas could
|
|
748
|
-
// allocate (e.g. in PSRAM). A large repair canvas has higher alloc/seed/push
|
|
749
|
-
// latency than banding, which shows as a press/scroll flash; the band canvas
|
|
750
|
-
// lives in fast internal SRAM and pushes smaller per-band transactions.
|
|
751
|
-
uint8_t preferBand = wantedBuffer && (UI_BAND_PREFER_PIXELS > 0) &&
|
|
752
|
-
(static_cast<uint32_t>(paintCanvasW) * static_cast<uint32_t>(paintCanvasH) > static_cast<uint32_t>(UI_BAND_PREFER_PIXELS));
|
|
753
|
-
if (wantedBuffer && !preferBand) {
|
|
754
|
-
paintCanvas = ui_get_repair_canvas(paintCanvasW, paintCanvasH);
|
|
755
|
-
if (paintCanvas) {
|
|
756
|
-
drawingPaintCanvas = 1;
|
|
757
|
-
ui_display_set_target(paintCanvas);
|
|
758
|
-
ui_seed_paint_canvas_for_node(i, paintCanvas, paintCanvasX, paintCanvasY, 0);
|
|
759
|
-
baseDrawX -= paintCanvasX;
|
|
760
|
-
baseDrawY -= paintCanvasY;
|
|
761
|
-
drawX -= paintCanvasX;
|
|
762
|
-
drawY -= paintCanvasY;
|
|
763
|
-
if (__ui_nodes[i].kind == NODE_PROGRESS || __ui_nodes[i].kind == NODE_RANGE) {
|
|
764
|
-
__ui_nodes[i].lastTextWidth = -1;
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
if (!drawingPaintCanvas) {
|
|
769
|
-
// No repair canvas this frame — either the paint rect is large enough to
|
|
770
|
-
// prefer the band renderer (preferBand: lower-latency than a big PSRAM
|
|
771
|
-
// repair canvas), or the repair canvas wouldn't allocate (over budget /
|
|
772
|
-
// heap fragmentation). Composite the node into the ~10KB band canvas one
|
|
773
|
-
// horizontal strip at a time, pushing each band tear-free. Avoids the
|
|
774
|
-
// direct clear→redraw-to-SPI that visibly flashes on press/scroll.
|
|
775
|
-
if (wantedBuffer && ui_render_node_bands(static_cast<uint16_t>(i), paintCanvasX, paintCanvasY, paintCanvasW, paintCanvasH)) {
|
|
776
|
-
__ui_frame_painted = 1;
|
|
777
|
-
if (!drawingBufferedScroll) {
|
|
778
|
-
ui_invalidate_scroll_canvas_for_node(i);
|
|
779
|
-
}
|
|
780
|
-
__ui_nodes[i].box.x = origBoxX;
|
|
781
|
-
__ui_nodes[i].box.y = origBoxY;
|
|
782
|
-
__ui_nodes[i].dirty = 0;
|
|
783
|
-
ui_refresh_add_rect(__ui_nodes[i].box.x, __ui_nodes[i].box.y, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
784
|
-
continue;
|
|
785
|
-
}
|
|
786
|
-
// Band canvas also failed — last resort: retry the repair canvas (it
|
|
787
|
-
// might fit when the band canvas is contended). If both allocations fail,
|
|
788
|
-
// lock this scroll owner for the frame and retain its previous pixels;
|
|
789
|
-
// never fall through to direct primitive SPI draws, which visibly tear.
|
|
790
|
-
if (wantedBuffer && preferBand) {
|
|
791
|
-
paintCanvas = ui_get_repair_canvas(paintCanvasW, paintCanvasH);
|
|
792
|
-
if (paintCanvas) {
|
|
793
|
-
drawingPaintCanvas = 1;
|
|
794
|
-
ui_display_set_target(paintCanvas);
|
|
795
|
-
ui_seed_paint_canvas_for_node(i, paintCanvas, paintCanvasX, paintCanvasY, 0);
|
|
796
|
-
baseDrawX -= paintCanvasX;
|
|
797
|
-
baseDrawY -= paintCanvasY;
|
|
798
|
-
drawX -= paintCanvasX;
|
|
799
|
-
drawY -= paintCanvasY;
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
if (!drawingPaintCanvas) {
|
|
803
|
-
if (wantedBuffer && preferBand && __ui_scroll_render_locked &&
|
|
804
|
-
__ui_nodes[i].scrollable) {
|
|
805
|
-
__ui_scroll_render_locked[i] = 1;
|
|
806
|
-
__ui_nodes[i].dirty = 0;
|
|
807
|
-
__ui_nodes[i].box.x = origBoxX;
|
|
808
|
-
__ui_nodes[i].box.y = origBoxY;
|
|
809
|
-
continue;
|
|
810
|
-
}
|
|
811
|
-
ui_clear_press_offset_area(i, baseDrawX, baseDrawY, drawX, drawY, paintTextW, paintTextH);
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
uint8_t skipListOutsetShadow =
|
|
815
|
-
(__ui_nodes[i].kind == NODE_LIST && !drawingBufferedScroll && !__ui_fb);
|
|
816
|
-
if (!skipListOutsetShadow) {
|
|
817
|
-
__ui_nodes[i].box.x = baseDrawX;
|
|
818
|
-
ui_draw_shadow(i, baseDrawY, 0);
|
|
819
|
-
__ui_nodes[i].box.x = drawX;
|
|
820
|
-
} else {
|
|
821
|
-
__ui_nodes[i].box.x = drawX;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
// Border color: use borderColor if set, otherwise fg.
|
|
825
|
-
UI_COLOR_T bColor = __ui_nodes[i].borderColor ? __ui_nodes[i].borderColor : __ui_nodes[i].fg;
|
|
826
|
-
// Background color blended toward clearColor by opacity (raw bg when 100%).
|
|
827
|
-
// NODE_FILL draws the fill at this color so opacity actually fades the
|
|
828
|
-
// element's background toward what's behind it.
|
|
829
|
-
UI_COLOR_T fillBg = __ui_nodes[i].bg;
|
|
830
|
-
// Apply opacity: blend fg/bg/border toward what's BEHIND the node when <100%.
|
|
831
|
-
// NOTE: blend toward the parent's clear color (ui_parent_clear_color), not
|
|
832
|
-
// the node's own clearColor — a filled node's clearColor IS its own bg, so
|
|
833
|
-
// blending bg toward it is a no-op (red toward red = red). The parent clear
|
|
834
|
-
// is the actual backdrop showing through the translucent element.
|
|
835
|
-
if (__ui_nodes[i].opacity < 100) {
|
|
836
|
-
UI_COLOR_T backdrop = ui_parent_clear_color(i);
|
|
837
|
-
bColor = ui_blend(bColor, backdrop, __ui_nodes[i].opacity);
|
|
838
|
-
fillBg = ui_blend(__ui_nodes[i].bg, backdrop, __ui_nodes[i].opacity);
|
|
839
|
-
}
|
|
840
|
-
// ── Per-kind draw dispatch ─────────────────────────────────────────────
|
|
841
|
-
// The kind switch lives in ui_draw_node_body (node-draw-body slice) so the
|
|
842
|
-
// strip/band renderer can reuse it. The ctx carries the draw state both
|
|
843
|
-
// paths share; the list case may return 1 (it handled canvas push,
|
|
844
|
-
// decoration, coord restore, and dirty-clear itself) in which case this
|
|
845
|
-
// node is done — skip the post-switch epilogue exactly as the old inline
|
|
846
|
-
// 'continue;' did.
|
|
847
|
-
UINodeDrawCtx __ui_ctx;
|
|
848
|
-
__ui_ctx.drawY = drawY;
|
|
849
|
-
__ui_ctx.bColor = bColor;
|
|
850
|
-
__ui_ctx.fillBg = fillBg;
|
|
851
|
-
__ui_ctx.ts = ts;
|
|
852
|
-
__ui_ctx.textMaxW = textMaxW;
|
|
853
|
-
__ui_ctx.tw = tw;
|
|
854
|
-
__ui_ctx.th = th;
|
|
855
|
-
__ui_ctx.paintTextW = paintTextW;
|
|
856
|
-
__ui_ctx.paintTextH = paintTextH;
|
|
857
|
-
__ui_ctx.displayText = displayText;
|
|
858
|
-
__ui_ctx.drawingBufferedScroll = drawingBufferedScroll;
|
|
859
|
-
__ui_ctx.drawTarget = __ui_draw_target;
|
|
860
|
-
__ui_ctx.origBoxX = origBoxX;
|
|
861
|
-
__ui_ctx.origBoxY = origBoxY;
|
|
862
|
-
__ui_frame_painted = 1;
|
|
863
|
-
if (ui_draw_node_body(i, &__ui_ctx)) {
|
|
864
|
-
continue;
|
|
865
|
-
}
|
|
866
|
-
if (__ui_nodes[i].kind == NODE_FILL && ui_rotation_quadrant(__ui_nodes[i].rotateDeg) != 0 &&
|
|
867
|
-
__ui_nodes[i].outlineStyle != 0 && __ui_nodes[i].outlineWidth > 0) {
|
|
868
|
-
uint8_t w = __ui_nodes[i].outlineWidth;
|
|
869
|
-
int16_t outlineW = ui_rotated_face_w(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
870
|
-
int16_t outlineH = ui_rotated_face_h(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
871
|
-
ui_draw_rect_outline(__ui_nodes[i].box.x - w, drawY - w,
|
|
872
|
-
outlineW + 2 * w, outlineH + 2 * w,
|
|
873
|
-
__ui_nodes[i].borderRadius + w, __ui_nodes[i].outlineStyle, w, __ui_nodes[i].outlineColor);
|
|
874
|
-
} else {
|
|
875
|
-
ui_draw_node_outline(i, __ui_nodes[i].box.x, drawY);
|
|
876
|
-
}
|
|
877
|
-
if (drawingPaintCanvas) {
|
|
878
|
-
ui_display_set_target(__ui_draw_target);
|
|
879
|
-
ui_push_canvas_rect(paintCanvas, paintCanvasX, paintCanvasY, paintCanvasW, paintCanvasH);
|
|
880
|
-
}
|
|
881
|
-
if (!drawingBufferedScroll) {
|
|
882
|
-
ui_invalidate_scroll_canvas_for_node(i);
|
|
883
|
-
}
|
|
884
|
-
// Restore original box coords (translated for canvas-local drawing above).
|
|
885
|
-
__ui_nodes[i].box.x = origBoxX;
|
|
886
|
-
__ui_nodes[i].box.y = origBoxY;
|
|
887
|
-
__ui_nodes[i].dirty = 0;
|
|
888
|
-
// Report this node's bounding box to the deferred-refresh accumulator. Phase
|
|
889
|
-
// 4 uses the box as the dirty rect (a safe over-estimate); Phase 5 tightens
|
|
890
|
-
// to the actual paint rect. No-op on TFT (compiles to nothing).
|
|
891
|
-
if (__ui_fb) ui_fb_add_rect(paintRect.x, paintRect.y, paintRect.w, paintRect.h);
|
|
892
|
-
ui_refresh_add_rect(__ui_nodes[i].box.x, __ui_nodes[i].box.y, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
// Modal <select> list: stamp the overlay after the dirty pass so tree
|
|
896
|
-
// redraws never bury it. Stamped only when the overlay itself is dirty
|
|
897
|
-
// (open) or something painted beneath it this frame; the close path marks
|
|
898
|
-
// the whole tree dirty for the erase repaint.
|
|
899
|
-
if (__ui_select_menu >= 0 && (__ui_select_menu_dirty || __ui_frame_painted)) {
|
|
900
|
-
ui_select_menu_draw();
|
|
5
|
+
return `
|
|
6
|
+
|
|
7
|
+
// ② Draw dirty nodes directly to the display object.
|
|
8
|
+
// Begin a deferred-refresh frame: resets the dirty-rect accumulator. On TFT
|
|
9
|
+
// (no backing store) this compiles to a no-op.
|
|
10
|
+
ui_refresh_begin_frame();
|
|
11
|
+
// Skip the node draw pass while the keyboard overlay is visible — its opaque
|
|
12
|
+
// background covers everything underneath, so redrawing app nodes wastes SPI
|
|
13
|
+
// bandwidth and causes flashing. Nodes redraw once when the keyboard closes
|
|
14
|
+
// (ui_kb_close marks the edited input dirty; ui_kb_open had marked all dirty
|
|
15
|
+
// on open so they're stale-but-covered while the keyboard is up).
|
|
16
|
+
//
|
|
17
|
+
// UI_HIDE_OSK (desktop SDL): the editing session still runs (buffer, target,
|
|
18
|
+
// commit-on-close) so real-keyboard typing works, but the 6×4 grid isn't drawn
|
|
19
|
+
// and the node pass ISN'T skipped — the app keeps rendering normally with the
|
|
20
|
+
// edited input's textBuffer showing the typed text. The grid is redundant when
|
|
21
|
+
// the host has a real keyboard.
|
|
22
|
+
#if !defined(UI_HIDE_OSK)
|
|
23
|
+
if (__ui_kb_visible) {
|
|
24
|
+
// Redraw only what changed:
|
|
25
|
+
// 1 = full redraw (open, shift toggle, page swap)
|
|
26
|
+
// 2 = text row + single key (char insert, delete, highlight change)
|
|
27
|
+
if (__ui_kb_dirty == 1) {
|
|
28
|
+
ui_kb_draw();
|
|
29
|
+
} else if (__ui_kb_dirty == 2) {
|
|
30
|
+
// Buffer the targeted update through the keyboard canvas too — drawing
|
|
31
|
+
// the text row directly to the display (fill_rect clear + text redraw)
|
|
32
|
+
// flashes on SPI TFTs because the clear is visible for one frame.
|
|
33
|
+
// Re-render into the canvas and push in one transaction. The canvas
|
|
34
|
+
// already holds the last full-keyboard frame, so we only need to redraw
|
|
35
|
+
// the text row + the single changed key on top of it.
|
|
36
|
+
// (__ui_kb_canvas is declared static in the keyboard slice, earlier in
|
|
37
|
+
// this same translation unit — no extern needed.)
|
|
38
|
+
if (__ui_kb_canvas && display_canvasBuffer(__ui_kb_canvas)) {
|
|
39
|
+
int16_t kw = __ui_kb_box.w;
|
|
40
|
+
int16_t saveBoxX = __ui_kb_box.x;
|
|
41
|
+
int16_t saveBoxY = __ui_kb_box.y;
|
|
42
|
+
CuttlefishDisplayTarget* __kb_prev = ui_display_get_target();
|
|
43
|
+
ui_display_set_target(__ui_kb_canvas);
|
|
44
|
+
__ui_kb_box.x = 0;
|
|
45
|
+
__ui_kb_box.y = 0;
|
|
46
|
+
ui_kb_draw_text_row();
|
|
47
|
+
uint8_t has_key_repaint = (__ui_kb_repaint_key >= 0 && __ui_kb_keys[__ui_kb_repaint_key].special != 255) ? 1 : 0;
|
|
48
|
+
if (has_key_repaint) {
|
|
49
|
+
ui_kb_draw_key(static_cast<uint8_t>(__ui_kb_repaint_key));
|
|
50
|
+
}
|
|
51
|
+
__ui_kb_box.x = saveBoxX;
|
|
52
|
+
__ui_kb_box.y = saveBoxY;
|
|
53
|
+
ui_display_set_target(__kb_prev);
|
|
54
|
+
// Push only the changed region: text row alone (UI_KB_TEXT_H) when no
|
|
55
|
+
// key highlight changed, or the full canvas when a key was repainted
|
|
56
|
+
// (the key could be anywhere in the keyboard grid).
|
|
57
|
+
display_startWrite();
|
|
58
|
+
if (has_key_repaint) {
|
|
59
|
+
display_setAddrWindow(saveBoxX, saveBoxY, kw, __ui_kb_box.h);
|
|
60
|
+
display_writePixels(display_canvasBuffer(__ui_kb_canvas), static_cast<uint32_t>(kw) * __ui_kb_box.h);
|
|
61
|
+
} else {
|
|
62
|
+
display_setAddrWindow(saveBoxX, saveBoxY, kw, UI_KB_TEXT_H);
|
|
63
|
+
display_writePixels(display_canvasBuffer(__ui_kb_canvas), static_cast<uint32_t>(kw) * UI_KB_TEXT_H);
|
|
64
|
+
}
|
|
65
|
+
display_endWrite();
|
|
66
|
+
} else {
|
|
67
|
+
// No canvas — fall back to direct draw (slow but correct).
|
|
68
|
+
ui_kb_draw_text_row();
|
|
69
|
+
if (__ui_kb_repaint_key >= 0 && __ui_kb_keys[__ui_kb_repaint_key].special != 255) {
|
|
70
|
+
ui_kb_draw_key(static_cast<uint8_t>(__ui_kb_repaint_key));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
__ui_kb_dirty = 0;
|
|
75
|
+
__ui_kb_repaint_key = -1;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
#endif
|
|
79
|
+
// Select and seed the framebuffer before scroll compositors run. Band/list
|
|
80
|
+
// renderers can then composite into this same off-screen target instead of
|
|
81
|
+
// pushing their bands to the live panel before the main draw pass starts.
|
|
82
|
+
// This prevents intermediate software-rendering seams; the final SPI transfer
|
|
83
|
+
// remains sequential on panels without TE/vblank synchronization.
|
|
84
|
+
CuttlefishCanvas16* __ui_fb = ui_get_framebuffer();
|
|
85
|
+
CuttlefishDisplayTarget* __ui_draw_target = __ui_fb ? (CuttlefishDisplayTarget*)__ui_fb : display_defaultTarget();
|
|
86
|
+
ui_fb_begin_frame();
|
|
87
|
+
// A retained framebuffer only needs an SPI transfer when composition changed.
|
|
88
|
+
// Avoid pushing a full frame on idle ticks; this is the main steady-state cost
|
|
89
|
+
// of the PSRAM path on otherwise static screens.
|
|
90
|
+
uint8_t __ui_fb_frame_dirty = 0;
|
|
91
|
+
// Whether any node painted this tick. The select overlay below re-stamps
|
|
92
|
+
// only when it or the frame beneath changed; an unconditional stamp pushes
|
|
93
|
+
// the modal region over SPI every frame, which reads as constant
|
|
94
|
+
// refreshing/tearing on no-TE TFT panels.
|
|
95
|
+
uint8_t __ui_frame_painted = 0;
|
|
96
|
+
if (__ui_fb && __ui_fb_needs_compose) {
|
|
97
|
+
UI_COLOR_T fbBg = (UI_COLOR_T)0;
|
|
98
|
+
if (__ui_active_screen_bg_node < __ui_node_count) {
|
|
99
|
+
fbBg = __ui_nodes[__ui_active_screen_bg_node].hasBg
|
|
100
|
+
? __ui_nodes[__ui_active_screen_bg_node].bg
|
|
101
|
+
: __ui_nodes[__ui_active_screen_bg_node].clearColor;
|
|
102
|
+
}
|
|
103
|
+
display_canvasFillScreen(__ui_fb, fbBg);
|
|
104
|
+
ui_fb_add_rect(0, 0, display_width(), display_height());
|
|
105
|
+
// A retained framebuffer cannot reuse the previous screen's pixels. Mark
|
|
106
|
+
// every node on the active screen dirty for one coherent composition.
|
|
107
|
+
for (uint16_t f = 0; f < __ui_node_count; f++) {
|
|
108
|
+
if (__ui_nodes[f].screenId == __ui_active_screen) {
|
|
109
|
+
__ui_nodes[f].dirty = 1;
|
|
110
|
+
__ui_nodes[f].lastTextHeight = 0;
|
|
111
|
+
__ui_nodes[f].layoutCacheKey = 0;
|
|
112
|
+
if (__ui_nodes[f].kind == NODE_PROGRESS || __ui_nodes[f].kind == NODE_RANGE) __ui_nodes[f].lastTextWidth = -1;
|
|
113
|
+
else __ui_nodes[f].lastTextWidth = 0;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
__ui_fb_needs_compose = 0;
|
|
117
|
+
__ui_fb_frame_dirty = 1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (__ui_scroll_render_locked) {
|
|
121
|
+
for (uint16_t lock = 0; lock < __ui_node_count; lock++) __ui_scroll_render_locked[lock] = 0;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Process each dirty scroll container (Mode B shift-and-repair). A scroll
|
|
125
|
+
// delta shifts existing canvas pixels by the delta and repaints only the
|
|
126
|
+
// newly-exposed strip; a full invalidation (or no canvas) redraws the subtree.
|
|
127
|
+
// One container per frame — the active scroll owner repaints via its canvas.
|
|
128
|
+
int16_t bufferedScrollNode = -1; // int16: node index can exceed 127
|
|
129
|
+
int16_t bufferedScrollVX = 0; // viewport origin X for coord translation
|
|
130
|
+
int16_t bufferedScrollVY = 0; // viewport origin Y
|
|
131
|
+
CuttlefishCanvas16* bufferedScrollCanvas = nullptr;
|
|
132
|
+
CuttlefishCanvas16* bufferedScrollRepaintCanvas = nullptr;
|
|
133
|
+
int16_t bufferedScrollRepaintY = 0;
|
|
134
|
+
int16_t bufferedScrollRepaintH = 0;
|
|
135
|
+
// Kept as a compatibility flag for the old compositor state machine. It is
|
|
136
|
+
// never enabled: direct per-node SPI drawing is a visible tearing path, so a
|
|
137
|
+
// missing canvas freezes the affected viewport instead.
|
|
138
|
+
uint8_t bufferedScrollDirectFull = 0;
|
|
139
|
+
// Local retained-canvas repair: dirty descendants draw directly into the
|
|
140
|
+
// full-frame framebuffer while the existing scroll canvas is invalidated for
|
|
141
|
+
// the next actual scroll. This avoids copying an unchanged viewport.
|
|
142
|
+
uint8_t bufferedScrollLocalRepair = 0;
|
|
143
|
+
// Band renderer pending: like bufferedScrollDirectFull but the actual paint
|
|
144
|
+
// is deferred to the scroll owner's z-order slot in the main draw loop. The
|
|
145
|
+
// band composites the whole subtree at once, so it must paint AFTER lower-z
|
|
146
|
+
// nodes (e.g. the screen background fill) but BEFORE higher-z ones (header).
|
|
147
|
+
// Painting it here (before the main loop) lets the bg fill overwrite it.
|
|
148
|
+
uint8_t bufferedScrollBands = 0;
|
|
149
|
+
for (uint16_t oi = 0; ; oi++) {
|
|
150
|
+
uint16_t s;
|
|
151
|
+
if (__ui_scroll_owners) {
|
|
152
|
+
if (oi >= __ui_scroll_owner_count) break;
|
|
153
|
+
s = __ui_scroll_owners[oi];
|
|
154
|
+
} else {
|
|
155
|
+
if (oi >= __ui_node_count) break;
|
|
156
|
+
s = oi;
|
|
157
|
+
if (!__ui_nodes[s].scrollable || __ui_nodes[s].virtualized) continue;
|
|
158
|
+
}
|
|
159
|
+
if (!ui_is_effectively_visible(s)) continue;
|
|
160
|
+
if (__ui_nodes[s].screenId != __ui_active_screen) continue;
|
|
161
|
+
if (__ui_nodes[s].contentHeight <= __ui_nodes[s].box.h) continue;
|
|
162
|
+
if (!__ui_nodes[s].dirty) continue;
|
|
163
|
+
// Virtualized lists manage their own rendering (NODE_LIST case in
|
|
164
|
+
// ui_draw_node_body). Skip them in the scroll dispatch — the band
|
|
165
|
+
// renderer walks their subtree (which is empty for virtualized lists),
|
|
166
|
+
// leaving the list node itself unpainted.
|
|
167
|
+
if (__ui_nodes[s].virtualized) continue;
|
|
168
|
+
// The retained framebuffer only needs the full viewport for actual scroll
|
|
169
|
+
// movement or the first composition. A local child update is repaired in
|
|
170
|
+
// the existing scroll canvas and contributes its own paint rect later;
|
|
171
|
+
// pushing the whole viewport here is the Forms-screen flash.
|
|
172
|
+
if (__ui_fb) __ui_fb_frame_dirty = 1;
|
|
173
|
+
|
|
174
|
+
int16_t vw = __ui_nodes[s].box.w;
|
|
175
|
+
int16_t vh = __ui_nodes[s].box.h;
|
|
176
|
+
int16_t vox = __ui_nodes[s].box.x;
|
|
177
|
+
int16_t voy = __ui_nodes[s].box.y;
|
|
178
|
+
UI_COLOR_T scrollBg = __ui_nodes[s].hasBg ? __ui_nodes[s].bg : __ui_nodes[s].clearColor;
|
|
179
|
+
bufferedScrollCanvas = ui_get_container_canvas(vw, vh);
|
|
180
|
+
// Track canvas-allocation success so ui_apply_scroll_delta can lock scrolling
|
|
181
|
+
// for containers whose canvas won't fit (frozen-but-not-torn contract).
|
|
182
|
+
if (__ui_scroll_canvas_ok) {
|
|
183
|
+
__ui_scroll_canvas_ok[s] = bufferedScrollCanvas ? 1 : 0;
|
|
184
|
+
}
|
|
185
|
+
if (bufferedScrollCanvas) {
|
|
186
|
+
bufferedScrollNode = static_cast<int16_t>(s);
|
|
187
|
+
bufferedScrollVX = vox;
|
|
188
|
+
bufferedScrollVY = voy;
|
|
189
|
+
|
|
190
|
+
// Mode B: shift delta = how far scrollY moved since this canvas was last
|
|
191
|
+
// painted. Small non-zero delta within one viewport → shift + repair strip.
|
|
192
|
+
int16_t deltaY = __ui_nodes[s].scrollY - __ui_nodes[s].lastPaintedScrollY;
|
|
193
|
+
int16_t absDelta = deltaY < 0 ? -deltaY : deltaY;
|
|
194
|
+
uint8_t canShift = (deltaY != 0 && absDelta < vh);
|
|
195
|
+
|
|
196
|
+
if (canShift) {
|
|
197
|
+
int16_t exposedY = 0;
|
|
198
|
+
int16_t exposedH = 0;
|
|
199
|
+
ui_shift_container_canvas(bufferedScrollCanvas, deltaY, scrollBg, &exposedY, &exposedH);
|
|
200
|
+
bufferedScrollRepaintCanvas = ui_get_repair_canvas(vw, exposedH);
|
|
201
|
+
if (bufferedScrollRepaintCanvas) {
|
|
202
|
+
bufferedScrollRepaintY = exposedY;
|
|
203
|
+
bufferedScrollRepaintH = exposedH;
|
|
204
|
+
display_canvasFillScreen(bufferedScrollRepaintCanvas, scrollBg);
|
|
205
|
+
UIRect exposed = { vox, static_cast<int16_t>(voy + exposedY), vw, exposedH };
|
|
206
|
+
for (uint16_t c = s + 1; c < __ui_nodes[s].subtreeEnd; c++) {
|
|
207
|
+
if (!ui_is_effectively_visible(c) || __ui_nodes[c].screenId != __ui_active_screen) {
|
|
208
|
+
__ui_nodes[c].dirty = 0;
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (!__ui_nodes[c].dirty && exposedH > 0) {
|
|
212
|
+
UIRect cr;
|
|
213
|
+
ui_node_current_paint_rect(c, &cr);
|
|
214
|
+
if (cr.w > 0 && cr.h > 0 &&
|
|
215
|
+
ui_rects_intersect(cr.x, cr.y, cr.w, cr.h, exposed.x, exposed.y, exposed.w, exposed.h)) {
|
|
216
|
+
__ui_nodes[c].dirty = 1;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (__ui_nodes[c].dirty) {
|
|
220
|
+
if (__ui_nodes[c].kind == NODE_PROGRESS) __ui_nodes[c].lastTextWidth = -1;
|
|
221
|
+
else if (__ui_nodes[c].kind == NODE_RANGE) __ui_nodes[c].lastTextWidth = -1;
|
|
222
|
+
__ui_nodes[c].lastTextHeight = 0;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
canShift = 0;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (!canShift) {
|
|
230
|
+
// A zero-delta dirty scroll owner is usually a local child update (a
|
|
231
|
+
// form binding, button transition, or animation). Keep the retained
|
|
232
|
+
// canvas and its existing pixels in that case; the dirty descendants
|
|
233
|
+
// will redraw in place and the final framebuffer union stays local.
|
|
234
|
+
uint8_t localRepair = __ui_fb && deltaY == 0 &&
|
|
235
|
+
__ui_nodes[s].lastPaintedScrollY == __ui_nodes[s].scrollY &&
|
|
236
|
+
ui_scroll_subtree_has_dirty(s);
|
|
237
|
+
if (localRepair) {
|
|
238
|
+
// The framebuffer is already the authoritative composition surface for
|
|
239
|
+
// this zero-delta update. Let dirty descendants draw there directly;
|
|
240
|
+
// invalidate the retained scroll canvas after each child draw so it is
|
|
241
|
+
// rebuilt before the next scroll gesture.
|
|
242
|
+
bufferedScrollLocalRepair = 1;
|
|
243
|
+
bufferedScrollCanvas = nullptr;
|
|
244
|
+
bufferedScrollRepaintCanvas = nullptr;
|
|
245
|
+
} else {
|
|
246
|
+
// Initial composition, a large jump, or an owner-only invalidation:
|
|
247
|
+
// seed the whole viewport and repaint its complete stacking subtree.
|
|
248
|
+
for (uint16_t c = s; c < __ui_nodes[s].subtreeEnd; c++) {
|
|
249
|
+
__ui_nodes[c].dirty = 1;
|
|
250
|
+
if (__ui_nodes[c].kind == NODE_PROGRESS) __ui_nodes[c].lastTextWidth = -1;
|
|
251
|
+
else if (__ui_nodes[c].kind == NODE_RANGE) __ui_nodes[c].lastTextWidth = -1;
|
|
252
|
+
__ui_nodes[c].lastTextHeight = 0;
|
|
253
|
+
}
|
|
254
|
+
display_canvasFillScreen(bufferedScrollCanvas, scrollBg);
|
|
255
|
+
if (__ui_fb) ui_fb_add_rect(__ui_nodes[s].box.x, __ui_nodes[s].box.y,
|
|
256
|
+
__ui_nodes[s].box.w, __ui_nodes[s].box.h);
|
|
257
|
+
}
|
|
258
|
+
} else if (__ui_fb) {
|
|
259
|
+
// Shift-and-repair changes the entire viewport's visible mapping.
|
|
260
|
+
ui_fb_add_rect(__ui_nodes[s].box.x, __ui_nodes[s].box.y,
|
|
261
|
+
__ui_nodes[s].box.w, __ui_nodes[s].box.h);
|
|
262
|
+
}
|
|
263
|
+
// The scroll owner is represented by bufferedScrollCanvas this frame.
|
|
264
|
+
// Drawing it directly first clears the live display and makes scrolling
|
|
265
|
+
// visibly flash before the canvas is pushed.
|
|
266
|
+
__ui_nodes[s].dirty = 0;
|
|
267
|
+
} else {
|
|
268
|
+
// Canvas won't fit (no PSRAM / over budget). The band renderer composes the
|
|
269
|
+
// whole visible subtree into a short horizontal band canvas (vw ×
|
|
270
|
+
// UI_STRIP_BAND_HEIGHT) and pushes one band at a time, so each band
|
|
271
|
+
// completes before it touches the panel — tear-free, with ~10KB of SRAM
|
|
272
|
+
// regardless of program size. If the band canvas also cannot allocate,
|
|
273
|
+
// the viewport is retained rather than using direct per-node SPI draws.
|
|
274
|
+
if (__ui_scroll_canvas_ok) __ui_scroll_canvas_ok[s] = 0;
|
|
275
|
+
// Defer the band render to the scroll owner's z-order slot in the main
|
|
276
|
+
// draw loop (see bufferedScrollBands handling below). Painting here would
|
|
277
|
+
// let lower-z nodes (screen bg fill) draw over the bands afterward. The
|
|
278
|
+
// owner stays dirty so the main loop reaches its z-slot and triggers the
|
|
279
|
+
// render; the band code clears it once painted. The subtree children are
|
|
280
|
+
// cleared now so the main loop never draws them directly — the band path
|
|
281
|
+
// is the sole renderer for the subtree this frame.
|
|
282
|
+
bufferedScrollNode = static_cast<int16_t>(s);
|
|
283
|
+
bufferedScrollVX = vox;
|
|
284
|
+
bufferedScrollVY = voy;
|
|
285
|
+
bufferedScrollBands = 1;
|
|
286
|
+
for (uint16_t c = s + 1; c < __ui_nodes[s].subtreeEnd; c++) {
|
|
287
|
+
__ui_nodes[c].dirty = 0;
|
|
288
|
+
}
|
|
289
|
+
break;
|
|
290
|
+
} // Only one scroll container per frame (the canvas is reused for subsequent
|
|
291
|
+
// ones in the next dirty frame). This matches the original design.
|
|
292
|
+
break;
|
|
293
|
+
}
|
|
294
|
+
// The framebuffer target was selected and seeded before scroll compositors;
|
|
295
|
+
// the stacking draw pass below uses the same __ui_draw_target. The retained
|
|
296
|
+
// single buffer is a composition surface, not a synchronized panel swap.
|
|
297
|
+
|
|
298
|
+
// Per-frame compose-region inflation (set by the overlap classification
|
|
299
|
+
// below, consumed by the drawer subtree compose).
|
|
300
|
+
for (uint8_t ds = 0; ds < UI_DRAWER_MAX; ds++) __ui_drawer_inflate_used[ds] = 0;
|
|
301
|
+
|
|
302
|
+
// ── Dirty-rect merging (LVGL refresh-cycle technique) ─────────────────────
|
|
303
|
+
// When several nodes repaint this frame, per-node transactions each pay
|
|
304
|
+
// SPI setup overhead. Group the frame's dirty paint rects into merged
|
|
305
|
+
// regions (inflate + greedy absorb, waste-capped) and composite each
|
|
306
|
+
// multi-node region through the band renderer in ONE set of pushes.
|
|
307
|
+
// Single-node and unmergeable rects keep the ordinary per-node paths.
|
|
308
|
+
// Skipped entirely when a retained framebuffer composes the frame (it
|
|
309
|
+
// already unions), when the keyboard overlay owns the screen, and for
|
|
310
|
+
// nodes inside overflow scroll subtrees (the Mode B machinery owns them).
|
|
311
|
+
if (!__ui_fb) {
|
|
312
|
+
UIRect __ui_merge_rects[UI_MERGE_MAX_REGIONS];
|
|
313
|
+
uint16_t __ui_merge_nodes[UI_MERGE_MAX_REGIONS][UI_MERGE_MAX_NODES];
|
|
314
|
+
uint16_t __ui_merge_counts[UI_MERGE_MAX_REGIONS];
|
|
315
|
+
uint16_t __ui_merge_region_count = 0;
|
|
316
|
+
for (uint16_t mi = 0; mi < __ui_node_count && __ui_merge_region_count < UI_MERGE_MAX_REGIONS; mi++) {
|
|
317
|
+
uint16_t m = __ui_draw_order ? __ui_draw_order[mi] : mi;
|
|
318
|
+
if (!__ui_nodes[m].dirty) continue;
|
|
319
|
+
if (__ui_nodes[m].screenId != __ui_active_screen) continue;
|
|
320
|
+
if (!ui_is_effectively_visible(m)) continue;
|
|
321
|
+
if (__ui_nodes[m].kind == NODE_LIST || __ui_nodes[m].virtualized) continue;
|
|
322
|
+
if (ui_overflow_scroll_compositor(m) >= 0) continue; // scroll machinery owns it
|
|
323
|
+
// A dirty ancestor's ladder turn owns this node's repaint: the ancestor
|
|
324
|
+
// composes its whole subtree region (or ladders and re-marks its
|
|
325
|
+
// descendants). Merging the node here composes it first, and the
|
|
326
|
+
// ancestor's clear+fill then erases it — the erase + re-pop sequence
|
|
327
|
+
// was the visible flash when pressing a button inside an open drawer.
|
|
328
|
+
{
|
|
329
|
+
uint16_t ap = __ui_nodes[m].parent;
|
|
330
|
+
while (ap != UI_NO_PARENT && ap < __ui_node_count) {
|
|
331
|
+
if (__ui_nodes[ap].dirty &&
|
|
332
|
+
__ui_nodes[ap].screenId == __ui_active_screen) break;
|
|
333
|
+
ap = __ui_nodes[ap].parent;
|
|
334
|
+
}
|
|
335
|
+
if (ap != UI_NO_PARENT && ap < __ui_node_count) continue;
|
|
336
|
+
}
|
|
337
|
+
UIRect mr;
|
|
338
|
+
ui_node_current_paint_rect(m, &mr);
|
|
339
|
+
if (mr.w <= 0 || mr.h <= 0) continue;
|
|
340
|
+
// Greedy absorb into an existing region when they overlap after a small
|
|
341
|
+
// inflation and the merged waste stays bounded.
|
|
342
|
+
uint8_t absorbed = 0;
|
|
343
|
+
for (uint16_t r = 0; r < __ui_merge_region_count && !absorbed; r++) {
|
|
344
|
+
UIRect* reg = &__ui_merge_rects[r];
|
|
345
|
+
int16_t x0 = reg->x - UI_MERGE_INFLATE, y0 = reg->y - UI_MERGE_INFLATE;
|
|
346
|
+
int16_t x1 = static_cast<int16_t>(reg->x + reg->w + UI_MERGE_INFLATE), y1 = static_cast<int16_t>(reg->y + reg->h + UI_MERGE_INFLATE);
|
|
347
|
+
if (mr.x >= x1 || static_cast<int16_t>(mr.x + mr.w) <= x0 ||
|
|
348
|
+
mr.y >= y1 || static_cast<int16_t>(mr.y + mr.h) <= y0) continue;
|
|
349
|
+
int16_t nx0 = reg->x < mr.x ? reg->x : mr.x;
|
|
350
|
+
int16_t ny0 = reg->y < mr.y ? reg->y : mr.y;
|
|
351
|
+
int16_t nx1 = static_cast<int16_t>(reg->x + reg->w > mr.x + mr.w ? reg->x + reg->w : mr.x + mr.w);
|
|
352
|
+
int16_t ny1 = static_cast<int16_t>(reg->y + reg->h > mr.y + mr.h ? reg->y + reg->h : mr.y + mr.h);
|
|
353
|
+
uint32_t mergedArea = static_cast<uint32_t>(nx1 - nx0) * static_cast<uint32_t>(ny1 - ny0);
|
|
354
|
+
if (mergedArea > UI_MAX_BUFFERED_PAINT_PIXELS) continue; // too big to band-composite
|
|
355
|
+
if (__ui_merge_counts[r] >= UI_MERGE_MAX_NODES) continue;
|
|
356
|
+
reg->x = nx0; reg->y = ny0;
|
|
357
|
+
reg->w = static_cast<int16_t>(nx1 - nx0); reg->h = static_cast<int16_t>(ny1 - ny0);
|
|
358
|
+
__ui_merge_nodes[r][__ui_merge_counts[r]++] = m;
|
|
359
|
+
absorbed = 1;
|
|
360
|
+
}
|
|
361
|
+
if (!absorbed) {
|
|
362
|
+
__ui_merge_rects[__ui_merge_region_count] = mr;
|
|
363
|
+
__ui_merge_nodes[__ui_merge_region_count][0] = m;
|
|
364
|
+
__ui_merge_counts[__ui_merge_region_count] = 1;
|
|
365
|
+
__ui_merge_region_count++;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// Composite multi-node regions as one banded render; singletons keep the
|
|
369
|
+
// per-node ladder (repair canvas etc. is cheaper for one node).
|
|
370
|
+
for (uint16_t r = 0; r < __ui_merge_region_count; r++) {
|
|
371
|
+
if (__ui_merge_counts[r] < 2) continue;
|
|
372
|
+
const UIRect* reg = &__ui_merge_rects[r];
|
|
373
|
+
if (ui_render_screen_bands(reg->x, reg->y, reg->w, reg->h)) {
|
|
374
|
+
for (uint16_t k = 0; k < __ui_merge_counts[r]; k++) {
|
|
375
|
+
__ui_nodes[__ui_merge_nodes[r][k]].dirty = 0;
|
|
376
|
+
ui_invalidate_scroll_canvas_for_node(__ui_merge_nodes[r][k]);
|
|
377
|
+
ui_refresh_add_rect(__ui_nodes[__ui_merge_nodes[r][k]].box.x, __ui_nodes[__ui_merge_nodes[r][k]].box.y,
|
|
378
|
+
__ui_nodes[__ui_merge_nodes[r][k]].box.w, __ui_nodes[__ui_merge_nodes[r][k]].box.h);
|
|
379
|
+
}
|
|
380
|
+
__ui_frame_painted = 1;
|
|
381
|
+
}
|
|
382
|
+
// Band allocation failure: leave the nodes dirty; the per-node ladder
|
|
383
|
+
// below still repaints them correctly.
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Draw dirty nodes in stacking order: lower z-index first, then source order.
|
|
388
|
+
// __ui_draw_order is built once in ui_init so each frame is O(N).
|
|
389
|
+
for (uint16_t __ui_draw_pass = 0; __ui_draw_pass < __ui_node_count; ) {
|
|
390
|
+
int16_t i;
|
|
391
|
+
if (__ui_draw_order) {
|
|
392
|
+
i = static_cast<int16_t>(__ui_draw_order[__ui_draw_pass++]);
|
|
393
|
+
if (!__ui_nodes[i].dirty) continue;
|
|
394
|
+
if (__ui_fb) __ui_fb_frame_dirty = 1;
|
|
395
|
+
if (!ui_is_effectively_visible(i)) { __ui_nodes[i].dirty = 0; continue; }
|
|
396
|
+
if (__ui_nodes[i].screenId != __ui_active_screen) { __ui_nodes[i].dirty = 0; continue; }
|
|
397
|
+
} else {
|
|
398
|
+
// malloc failed at startup: preserve correct z-order via selection sort.
|
|
399
|
+
i = -1;
|
|
400
|
+
for (uint16_t candidate = 0; candidate < __ui_node_count; candidate++) {
|
|
401
|
+
if (!__ui_nodes[candidate].dirty) continue;
|
|
402
|
+
if (__ui_fb) __ui_fb_frame_dirty = 1;
|
|
403
|
+
if (!ui_is_effectively_visible(candidate)) { __ui_nodes[candidate].dirty = 0; continue; }
|
|
404
|
+
if (__ui_nodes[candidate].screenId != __ui_active_screen) { __ui_nodes[candidate].dirty = 0; continue; }
|
|
405
|
+
if (i < 0 || ui_node_draws_before(candidate, static_cast<uint16_t>(i))) i = static_cast<int16_t>(candidate);
|
|
406
|
+
}
|
|
407
|
+
if (i < 0) break;
|
|
408
|
+
__ui_draw_pass++;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Open-drawer overlap classification: the drawer subtree draws above other
|
|
412
|
+
// content (z-order + later draw slot). A dirty node underneath would
|
|
413
|
+
// paint its clear+redraw over the panel's pixels — a bound text behind an
|
|
414
|
+
// open drawer erases the drawer's buttons on repaint. Fully covered by an
|
|
415
|
+
// opaque open drawer → none of it is visible; skip the repaint entirely.
|
|
416
|
+
// Partially covered → re-dirty that drawer's subtree so it re-stamps on
|
|
417
|
+
// top later in this same pass (its z sorts it after the covered node).
|
|
418
|
+
if (__ui_drawer_slots > 0) {
|
|
419
|
+
for (uint8_t dslot = 0; dslot < __ui_drawer_slots; dslot++) {
|
|
420
|
+
if (__ui_drawer_idx[dslot] < 0) continue;
|
|
421
|
+
uint16_t droot = static_cast<uint16_t>(__ui_drawer_idx[dslot]);
|
|
422
|
+
if (static_cast<uint16_t>(i) == droot ||
|
|
423
|
+
(i > static_cast<int16_t>(droot) && static_cast<uint16_t>(i) < __ui_nodes[droot].subtreeEnd)) continue;
|
|
424
|
+
if (__ui_drawer_progress[dslot] < 255) continue; // closed or sliding
|
|
425
|
+
int16_t dX = ui_draw_x_for_node(droot);
|
|
426
|
+
int16_t dY = ui_draw_y_for_node(droot);
|
|
427
|
+
int16_t nX = ui_draw_x_for_node(static_cast<uint16_t>(i));
|
|
428
|
+
int16_t nY = ui_draw_y_for_node(static_cast<uint16_t>(i));
|
|
429
|
+
if (!ui_rects_intersect(nX, nY, __ui_nodes[i].box.w, __ui_nodes[i].box.h,
|
|
430
|
+
dX, dY, __ui_nodes[droot].box.w, __ui_nodes[droot].box.h)) continue;
|
|
431
|
+
uint8_t fullyCovered = (nX >= dX && nY >= dY &&
|
|
432
|
+
nX + __ui_nodes[i].box.w <= dX + __ui_nodes[droot].box.w &&
|
|
433
|
+
nY + __ui_nodes[i].box.h <= dY + __ui_nodes[droot].box.h) ? 1 : 0;
|
|
434
|
+
if (fullyCovered && __ui_nodes[droot].hasBg && __ui_nodes[droot].opacity >= 100) {
|
|
435
|
+
__ui_nodes[i].dirty = 0;
|
|
436
|
+
} else if (ui_overflow_scroll_compositor(droot) >= 0) {
|
|
437
|
+
// Partial coverage, but the drawer sits inside an overflow scroll
|
|
438
|
+
// subtree that is NOT being composited this frame. Re-dirtying the
|
|
439
|
+
// drawer here sends its whole subtree through the defer fall-through
|
|
440
|
+
// (direct band/paint-canvas pushes over a scroll viewport) — the
|
|
441
|
+
// tearing path AGENTS.md forbids, and on hardware it corrupts the
|
|
442
|
+
// panel content (drawer text and buttons vanish while remaining
|
|
443
|
+
// tappable). Skip the covered node instead: its uncovered sliver
|
|
444
|
+
// (typically a few px of a full-width label) is not worth the
|
|
445
|
+
// unsafe redraw. When the scroll owner IS composited (dirty this
|
|
446
|
+
// frame), the re-dirty below is safe — the composition redraws the
|
|
447
|
+
// whole viewport including the drawer, in order.
|
|
448
|
+
__ui_nodes[i].dirty = 0;
|
|
449
|
+
} else {
|
|
450
|
+
// Partial coverage, display path. The node's own ladder would paint
|
|
451
|
+
// its FULL rect — including the covered part — straight over the
|
|
452
|
+
// open panel, flashing underlying content as a bar until the panel
|
|
453
|
+
// re-stamps. Skip its direct repaint and let the drawer's subtree
|
|
454
|
+
// compose paint it (stacking order puts it under the panel); that
|
|
455
|
+
// compose region is inflated to reach the node's uncovered sliver.
|
|
456
|
+
UIRect nr = { nX, nY, __ui_nodes[i].box.w, __ui_nodes[i].box.h };
|
|
457
|
+
if (!__ui_drawer_inflate_used[dslot]) {
|
|
458
|
+
__ui_drawer_inflate_rect[dslot] = nr;
|
|
459
|
+
__ui_drawer_inflate_used[dslot] = 1;
|
|
460
|
+
} else {
|
|
461
|
+
UIRect* inf = &__ui_drawer_inflate_rect[dslot];
|
|
462
|
+
int16_t x1 = static_cast<int16_t>(inf->x + inf->w > nr.x + nr.w ? inf->x + inf->w : nr.x + nr.w);
|
|
463
|
+
int16_t y1 = static_cast<int16_t>(inf->y + inf->h > nr.y + nr.h ? inf->y + inf->h : nr.y + nr.h);
|
|
464
|
+
if (nr.x < inf->x) inf->x = nr.x;
|
|
465
|
+
if (nr.y < inf->y) inf->y = nr.y;
|
|
466
|
+
inf->w = static_cast<int16_t>(x1 - inf->x);
|
|
467
|
+
inf->h = static_cast<int16_t>(y1 - inf->y);
|
|
468
|
+
}
|
|
469
|
+
__ui_nodes[i].dirty = 0;
|
|
470
|
+
__ui_nodes[droot].dirty = 1;
|
|
471
|
+
__ui_nodes[droot].lastTextHeight = 0;
|
|
472
|
+
for (uint16_t c = droot + 1; c < __ui_nodes[droot].subtreeEnd; c++) {
|
|
473
|
+
__ui_nodes[c].dirty = 1;
|
|
474
|
+
__ui_nodes[c].lastTextHeight = 0;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
if (!__ui_nodes[i].dirty) continue;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Defer direct display draws for overflow scroll subtrees not composited this
|
|
483
|
+
// frame (Mode B canvas, Mode C strip, direct-full fallback, or band render).
|
|
484
|
+
// Drawing them directly clears the live viewport and produces sequential
|
|
485
|
+
// flashes (AGENTS.md) — except in direct-full mode, where there is no canvas
|
|
486
|
+
// and direct draw is the only way to show content.
|
|
487
|
+
{
|
|
488
|
+
int16_t scrollComp = ui_overflow_scroll_compositor(static_cast<uint16_t>(i));
|
|
489
|
+
if (scrollComp >= 0) {
|
|
490
|
+
uint8_t compositing = scrollComp == bufferedScrollNode &&
|
|
491
|
+
(bufferedScrollCanvas || bufferedScrollDirectFull || bufferedScrollBands || bufferedScrollLocalRepair);
|
|
492
|
+
if (!compositing) {
|
|
493
|
+
if (static_cast<uint16_t>(i) == static_cast<uint16_t>(scrollComp)) break;
|
|
494
|
+
// Not composited this frame. A child FULLY inside its scroll
|
|
495
|
+
// viewport repaints directly through its own buffered paint (paint
|
|
496
|
+
// canvas or bands — a single atomic push; the epilogue invalidates
|
|
497
|
+
// the retained scroll canvas so the next scroll recomposes). Only
|
|
498
|
+
// partially/fully clipped children defer: their edge pixels must
|
|
499
|
+
// come from the composited canvas, and drawing them directly would
|
|
500
|
+
// paint outside the viewport.
|
|
501
|
+
if (ui_is_rect_clipped_by_scroll(static_cast<uint16_t>(i),
|
|
502
|
+
ui_draw_x_for_node(static_cast<uint16_t>(i)),
|
|
503
|
+
ui_draw_y_for_node(static_cast<uint16_t>(i)),
|
|
504
|
+
__ui_nodes[i].box.w, __ui_nodes[i].box.h)) {
|
|
505
|
+
__ui_nodes[i].dirty = 0;
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
// Band renderer: paint the whole subtree at the scroll owner's z-order slot.
|
|
513
|
+
// This must run AFTER lower-z nodes (e.g. the screen background fill, which
|
|
514
|
+
// has lower source order) have been drawn, so they don't overwrite the bands.
|
|
515
|
+
// When the main loop reaches the scroll owner itself, render the bands now
|
|
516
|
+
// (its z-position), mark the owner handled, and continue.
|
|
517
|
+
if (bufferedScrollBands && bufferedScrollNode >= 0 &&
|
|
518
|
+
static_cast<uint16_t>(i) == static_cast<uint16_t>(bufferedScrollNode)) {
|
|
519
|
+
if (ui_render_scroll_bands(static_cast<uint16_t>(bufferedScrollNode))) {
|
|
520
|
+
__ui_frame_painted = 1;
|
|
521
|
+
bufferedScrollBands = 0;
|
|
522
|
+
bufferedScrollNode = -1;
|
|
523
|
+
__ui_nodes[i].dirty = 0;
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
// Band canvas allocation failed. Retain the existing viewport pixels rather
|
|
527
|
+
// than falling back to direct-full SPI primitives; direct-full is the
|
|
528
|
+
// tearing path this compositor is designed to prevent.
|
|
529
|
+
bufferedScrollBands = 0;
|
|
530
|
+
if (__ui_scroll_render_locked) __ui_scroll_render_locked[static_cast<uint16_t>(bufferedScrollNode)] = 1;
|
|
531
|
+
__ui_nodes[static_cast<uint16_t>(bufferedScrollNode)].dirty = 0;
|
|
532
|
+
bufferedScrollNode = -1;
|
|
533
|
+
} // Band renderer early-push: if we're about to draw a node that comes AFTER
|
|
534
|
+
// the scroll owner in draw order (e.g. a header with higher source order)
|
|
535
|
+
// and the bands haven't rendered yet, render them first so they land below
|
|
536
|
+
// that higher-z node. Mirrors the Mode B canvas early-push below.
|
|
537
|
+
if (bufferedScrollBands && bufferedScrollNode >= 0 &&
|
|
538
|
+
!(i > bufferedScrollNode && i < __ui_nodes[bufferedScrollNode].subtreeEnd) &&
|
|
539
|
+
ui_node_draws_before(static_cast<uint16_t>(bufferedScrollNode), static_cast<uint16_t>(i))) {
|
|
540
|
+
if (ui_render_scroll_bands(static_cast<uint16_t>(bufferedScrollNode))) {
|
|
541
|
+
__ui_frame_painted = 1;
|
|
542
|
+
bufferedScrollBands = 0;
|
|
543
|
+
bufferedScrollNode = -1;
|
|
544
|
+
} else {
|
|
545
|
+
// Band alloc failed — retain the previous viewport rather than exposing
|
|
546
|
+
// a direct-full per-primitive SPI repaint.
|
|
547
|
+
bufferedScrollBands = 0;
|
|
548
|
+
if (__ui_scroll_render_locked) __ui_scroll_render_locked[static_cast<uint16_t>(bufferedScrollNode)] = 1;
|
|
549
|
+
__ui_nodes[static_cast<uint16_t>(bufferedScrollNode)].dirty = 0;
|
|
550
|
+
bufferedScrollNode = -1;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// Mode C strip / direct-full: scroll owner is not drawn directly (would fill
|
|
555
|
+
// the viewport); its children are drawn directly below.
|
|
556
|
+
if ((bufferedScrollDirectFull || bufferedScrollLocalRepair) && bufferedScrollNode >= 0 &&
|
|
557
|
+
static_cast<uint16_t>(i) == static_cast<uint16_t>(bufferedScrollNode)) {
|
|
558
|
+
__ui_nodes[i].dirty = 0;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (bufferedScrollNode >= 0 && bufferedScrollCanvas &&
|
|
563
|
+
!(i > bufferedScrollNode && i < __ui_nodes[bufferedScrollNode].subtreeEnd) &&
|
|
564
|
+
ui_node_draws_before(static_cast<uint16_t>(bufferedScrollNode), static_cast<uint16_t>(i)) &&
|
|
565
|
+
!ui_scroll_subtree_has_dirty(static_cast<uint16_t>(bufferedScrollNode))) {
|
|
566
|
+
ui_push_buffered_scroll_canvas(bufferedScrollCanvas, bufferedScrollRepaintCanvas,
|
|
567
|
+
bufferedScrollNode, bufferedScrollVX, bufferedScrollVY,
|
|
568
|
+
bufferedScrollRepaintY, bufferedScrollRepaintH, __ui_draw_target);
|
|
569
|
+
__ui_frame_painted = 1;
|
|
570
|
+
bufferedScrollNode = -1;
|
|
571
|
+
bufferedScrollCanvas = nullptr;
|
|
572
|
+
bufferedScrollRepaintCanvas = nullptr;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Ladder-drawing a CONTAINER (fill/view panel) clears and redraws its
|
|
576
|
+
// whole paint rect — everything drawn inside it (its descendants) is
|
|
577
|
+
// erased by this pass. Mark-time propagation (ui_mark_dirty) already
|
|
578
|
+
// marks descendants, but the merge pass composes and CLEARS some of
|
|
579
|
+
// them before this node's ladder turn — a press inside an open drawer
|
|
580
|
+
// left the panel for the ladder (too big to merge) while its texts and
|
|
581
|
+
// buttons were merge-composed first; the panel's clear+fill then erased
|
|
582
|
+
// them and nothing re-drew them. Re-mark the descendants here: they sort
|
|
583
|
+
// after this node, so they restore their pixels later in this same pass.
|
|
584
|
+
// Scroll owners never reach this point (the compositor paths skip them),
|
|
585
|
+
// so scroll-subtree repaints do not amplify.
|
|
586
|
+
if (__ui_nodes[i].subtreeEnd > static_cast<uint16_t>(i) + 1 &&
|
|
587
|
+
__ui_nodes[i].kind != NODE_LIST) {
|
|
588
|
+
for (uint16_t c = static_cast<uint16_t>(i) + 1;
|
|
589
|
+
c < __ui_nodes[i].subtreeEnd && c < __ui_node_count; c++) {
|
|
590
|
+
if (!__ui_nodes[c].visible) continue;
|
|
591
|
+
if (__ui_nodes[c].screenId != __ui_active_screen) continue;
|
|
592
|
+
__ui_nodes[c].dirty = 1;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Redirect to the scroll canvas if this node is inside the buffered container
|
|
597
|
+
// AND a real canvas is active this frame (Mode B only). Direct-strip and
|
|
598
|
+
// direct-full have no canvas — their children must draw to the display target
|
|
599
|
+
// (and thus qualify for the per-node paint-canvas optimization), not redirect
|
|
600
|
+
// to a null scroll canvas. Without this, a strip-drag frame sets
|
|
601
|
+
// drawingBufferedScroll=1, which skips the paint canvas and forces the
|
|
602
|
+
// repainting children to direct-draw per-pixel to SPI (~100ms/text node).
|
|
603
|
+
uint8_t drawingBufferedScroll = !bufferedScrollDirectFull &&
|
|
604
|
+
(bufferedScrollCanvas != nullptr) && bufferedScrollNode >= 0 &&
|
|
605
|
+
i > bufferedScrollNode && i < __ui_nodes[bufferedScrollNode].subtreeEnd;
|
|
606
|
+
// A container with descendants reaches its ladder turn only when the
|
|
607
|
+
// merge pass couldn't own it (too big to merge, or a dirty ancestor
|
|
608
|
+
// excluded the children). The ladder clears + redraws just this node —
|
|
609
|
+
// erasing every descendant pixel — and the re-marked descendants would
|
|
610
|
+
// then pop back one ladder at a time: an erase + re-pop the eye reads
|
|
611
|
+
// as a flash (pressing a button inside an open drawer). Instead compose
|
|
612
|
+
// the container's whole subtree REGION through the band renderer — one
|
|
613
|
+
// pass, correct stacking order, no intermediate state — and clear every
|
|
614
|
+
// dirty node inside the region (the compose already painted them in
|
|
615
|
+
// final form). Falls back to the ladder + re-marks when the band canvas
|
|
616
|
+
// can't allocate. Display path only: into an active scroll canvas the
|
|
617
|
+
// ladder draws (children re-mark into the canvas afterwards).
|
|
618
|
+
if (!drawingBufferedScroll &&
|
|
619
|
+
__ui_nodes[i].subtreeEnd > static_cast<uint16_t>(i) + 1 &&
|
|
620
|
+
__ui_nodes[i].kind != NODE_LIST) {
|
|
621
|
+
UIRect subtreeRegion;
|
|
622
|
+
if (ui_subtree_current_paint_rect(static_cast<uint16_t>(i), &subtreeRegion) &&
|
|
623
|
+
subtreeRegion.w > 0 && subtreeRegion.h > 0) {
|
|
624
|
+
// An open drawer's compose region reaches the nodes the overlap
|
|
625
|
+
// classification skipped this frame — they owe their uncovered
|
|
626
|
+
// sliver, and the compose paints them under the panel in stacking
|
|
627
|
+
// order.
|
|
628
|
+
int8_t islot = __ui_drawer_slot_of(static_cast<uint16_t>(i));
|
|
629
|
+
if (islot >= 0 && __ui_drawer_inflate_used[islot]) {
|
|
630
|
+
const UIRect* inf = &__ui_drawer_inflate_rect[islot];
|
|
631
|
+
int16_t x1 = static_cast<int16_t>(subtreeRegion.x + subtreeRegion.w > inf->x + inf->w ? subtreeRegion.x + subtreeRegion.w : inf->x + inf->w);
|
|
632
|
+
int16_t y1 = static_cast<int16_t>(subtreeRegion.y + subtreeRegion.h > inf->y + inf->h ? subtreeRegion.y + subtreeRegion.h : inf->y + inf->h);
|
|
633
|
+
if (inf->x < subtreeRegion.x) subtreeRegion.x = inf->x;
|
|
634
|
+
if (inf->y < subtreeRegion.y) subtreeRegion.y = inf->y;
|
|
635
|
+
subtreeRegion.w = static_cast<int16_t>(x1 - subtreeRegion.x);
|
|
636
|
+
subtreeRegion.h = static_cast<int16_t>(y1 - subtreeRegion.y);
|
|
637
|
+
__ui_drawer_inflate_used[islot] = 0;
|
|
638
|
+
}
|
|
639
|
+
if (ui_render_screen_bands(subtreeRegion.x, subtreeRegion.y, subtreeRegion.w, subtreeRegion.h)) {
|
|
640
|
+
__ui_frame_painted = 1;
|
|
641
|
+
ui_invalidate_scroll_canvas_for_node(static_cast<uint16_t>(i));
|
|
642
|
+
for (uint16_t q = 0; q < __ui_node_count; q++) {
|
|
643
|
+
if (__ui_nodes[q].screenId != __ui_active_screen) continue;
|
|
644
|
+
if (!__ui_nodes[q].dirty) continue;
|
|
645
|
+
UIRect qr;
|
|
646
|
+
ui_node_current_paint_rect(q, &qr);
|
|
647
|
+
if (qr.w <= 0 || qr.h <= 0) continue;
|
|
648
|
+
if (qr.x >= subtreeRegion.x && qr.y >= subtreeRegion.y &&
|
|
649
|
+
static_cast<int16_t>(qr.x + qr.w) <= static_cast<int16_t>(subtreeRegion.x + subtreeRegion.w) &&
|
|
650
|
+
static_cast<int16_t>(qr.y + qr.h) <= static_cast<int16_t>(subtreeRegion.y + subtreeRegion.h)) {
|
|
651
|
+
__ui_nodes[q].dirty = 0;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
int16_t origBoxX = __ui_nodes[i].box.x;
|
|
659
|
+
int16_t origBoxY = __ui_nodes[i].box.y;
|
|
660
|
+
if (drawingBufferedScroll) {
|
|
661
|
+
CuttlefishCanvas16* scrollDrawCanvas = bufferedScrollRepaintCanvas ? bufferedScrollRepaintCanvas : bufferedScrollCanvas;
|
|
662
|
+
ui_display_set_target(scrollDrawCanvas);
|
|
663
|
+
// Translate display coords → canvas-local coords (subtract viewport origin).
|
|
664
|
+
__ui_nodes[i].box.x = origBoxX - bufferedScrollVX;
|
|
665
|
+
__ui_nodes[i].box.y = origBoxY - bufferedScrollVY - (bufferedScrollRepaintCanvas ? bufferedScrollRepaintY : 0);
|
|
666
|
+
} else {
|
|
667
|
+
ui_display_set_target(__ui_draw_target);
|
|
668
|
+
}
|
|
669
|
+
int16_t baseDrawX = ui_base_draw_x_for_node(i);
|
|
670
|
+
int16_t baseDrawY = ui_base_draw_y_for_node(i);
|
|
671
|
+
int16_t drawX = ui_draw_x_for_node(i);
|
|
672
|
+
int16_t drawY = ui_draw_y_for_node(i);
|
|
673
|
+
// Source selection: text-bound nodes show their dynamic buffer; others show
|
|
674
|
+
// the immutable flash literal.
|
|
675
|
+
const char* displayText = __ui_nodes[i].hasTextBinding
|
|
676
|
+
? __ui_nodes[i].textBuffer
|
|
677
|
+
: __ui_nodes[i].text;
|
|
678
|
+
uint8_t ts = __ui_nodes[i].textSize ? __ui_nodes[i].textSize : 2;
|
|
679
|
+
uint16_t textMaxW = ui_node_text_max_width(i);
|
|
680
|
+
uint16_t tw = 0;
|
|
681
|
+
uint16_t th = 0;
|
|
682
|
+
ui_node_text_layout_metrics(i, textMaxW, &tw, &th);
|
|
683
|
+
uint16_t paintTextW = tw;
|
|
684
|
+
uint16_t paintTextH = th;
|
|
685
|
+
if (__ui_nodes[i].kind == NODE_TEXT || __ui_nodes[i].kind == NODE_SELECT) {
|
|
686
|
+
uint16_t hInset = static_cast<uint16_t>(__ui_nodes[i].paddingLeft) + static_cast<uint16_t>(__ui_nodes[i].paddingRight) + static_cast<uint16_t>(__ui_nodes[i].borderWidth) * 2;
|
|
687
|
+
uint16_t vInset = static_cast<uint16_t>(__ui_nodes[i].paddingTop) + static_cast<uint16_t>(__ui_nodes[i].paddingBottom) + static_cast<uint16_t>(__ui_nodes[i].borderWidth) * 2;
|
|
688
|
+
paintTextW = static_cast<uint16_t>(tw + hInset);
|
|
689
|
+
paintTextH = static_cast<uint16_t>(th + vInset);
|
|
690
|
+
}
|
|
691
|
+
if (__ui_nodes[i].kind == NODE_CHECK || __ui_nodes[i].kind == NODE_RADIO) {
|
|
692
|
+
paintTextW = tw + 22;
|
|
693
|
+
if (paintTextH < 16) paintTextH = 16;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
UIRect paintRect;
|
|
697
|
+
ui_node_paint_rect(i, baseDrawX, baseDrawY, drawX, drawY, paintTextW, paintTextH, &paintRect);
|
|
698
|
+
if (drawingBufferedScroll) {
|
|
699
|
+
// Canvas-local clip: skip nodes fully outside the viewport (0..vw, 0..vh).
|
|
700
|
+
// Use the node's face rect (box.w/h), NOT the paint rect — the paint rect
|
|
701
|
+
// includes shadow/border extents, which legitimately overflow a scroll
|
|
702
|
+
// viewport. Clipping on the paint rect falsely rejects nodes whose shadow
|
|
703
|
+
// pokes past the container edge while the face is fully inside (showed up
|
|
704
|
+
// as the home-nav buttons never painting on ST7796S, where the nav
|
|
705
|
+
// container is exactly button-width).
|
|
706
|
+
int16_t faceX = drawX;
|
|
707
|
+
int16_t faceY = drawY;
|
|
708
|
+
int16_t faceW = __ui_nodes[i].box.w;
|
|
709
|
+
int16_t faceH = __ui_nodes[i].box.h;
|
|
710
|
+
CuttlefishCanvas16* scrollDrawCanvas = bufferedScrollRepaintCanvas ? bufferedScrollRepaintCanvas : bufferedScrollCanvas;
|
|
711
|
+
// Guard against a null canvas (the subtree was flagged drawingBufferedScroll
|
|
712
|
+
// but no canvas is available this frame — e.g. a second scroll owner whose
|
|
713
|
+
// canvas couldn't allocate). Skip the canvas-local clip and draw the node
|
|
714
|
+
// to the display target; the scroll-viewport clip below still bounds it.
|
|
715
|
+
if (scrollDrawCanvas) {
|
|
716
|
+
int16_t scrollDrawW = display_canvasWidth(scrollDrawCanvas);
|
|
717
|
+
int16_t scrollDrawH = display_canvasHeight(scrollDrawCanvas);
|
|
718
|
+
if (bufferedScrollRepaintCanvas) {
|
|
719
|
+
scrollDrawW = __ui_nodes[bufferedScrollNode].box.w;
|
|
720
|
+
scrollDrawH = bufferedScrollRepaintH;
|
|
721
|
+
}
|
|
722
|
+
if (faceY + faceH <= 0 || faceY >= scrollDrawH ||
|
|
723
|
+
faceX + faceW <= 0 || faceX >= scrollDrawW) {
|
|
724
|
+
__ui_nodes[i].box.x = origBoxX;
|
|
725
|
+
__ui_nodes[i].box.y = origBoxY;
|
|
726
|
+
__ui_nodes[i].dirty = 0;
|
|
727
|
+
continue;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
} else if (ui_is_rect_clipped_by_scroll(i, drawX, drawY, __ui_nodes[i].box.w, __ui_nodes[i].box.h)) {
|
|
731
|
+
__ui_nodes[i].box.x = origBoxX;
|
|
732
|
+
__ui_nodes[i].box.y = origBoxY;
|
|
733
|
+
__ui_nodes[i].dirty = 0;
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
uint8_t drawingPaintCanvas = 0;
|
|
738
|
+
CuttlefishCanvas16* paintCanvas = nullptr;
|
|
739
|
+
int16_t paintCanvasX = paintRect.x;
|
|
740
|
+
int16_t paintCanvasY = paintRect.y;
|
|
741
|
+
int16_t paintCanvasW = paintRect.w;
|
|
742
|
+
int16_t paintCanvasH = paintRect.h;
|
|
743
|
+
// RAM-composite pixel-heavy nodes before SPI push. Skip when already drawing
|
|
744
|
+
// into a scroll canvas or a full-screen framebuffer (both are RAM targets).
|
|
745
|
+
uint8_t wantedBuffer = !drawingBufferedScroll && !__ui_fb && ui_should_buffer_paint(i, paintCanvasW, paintCanvasH);
|
|
746
|
+
// Large paint rects prefer the band renderer (small ~10KB SRAM canvas,
|
|
747
|
+
// strip-at-a-time) over the repair canvas even when the repair canvas could
|
|
748
|
+
// allocate (e.g. in PSRAM). A large repair canvas has higher alloc/seed/push
|
|
749
|
+
// latency than banding, which shows as a press/scroll flash; the band canvas
|
|
750
|
+
// lives in fast internal SRAM and pushes smaller per-band transactions.
|
|
751
|
+
uint8_t preferBand = wantedBuffer && (UI_BAND_PREFER_PIXELS > 0) &&
|
|
752
|
+
(static_cast<uint32_t>(paintCanvasW) * static_cast<uint32_t>(paintCanvasH) > static_cast<uint32_t>(UI_BAND_PREFER_PIXELS));
|
|
753
|
+
if (wantedBuffer && !preferBand) {
|
|
754
|
+
paintCanvas = ui_get_repair_canvas(paintCanvasW, paintCanvasH);
|
|
755
|
+
if (paintCanvas) {
|
|
756
|
+
drawingPaintCanvas = 1;
|
|
757
|
+
ui_display_set_target(paintCanvas);
|
|
758
|
+
ui_seed_paint_canvas_for_node(i, paintCanvas, paintCanvasX, paintCanvasY, 0);
|
|
759
|
+
baseDrawX -= paintCanvasX;
|
|
760
|
+
baseDrawY -= paintCanvasY;
|
|
761
|
+
drawX -= paintCanvasX;
|
|
762
|
+
drawY -= paintCanvasY;
|
|
763
|
+
if (__ui_nodes[i].kind == NODE_PROGRESS || __ui_nodes[i].kind == NODE_RANGE) {
|
|
764
|
+
__ui_nodes[i].lastTextWidth = -1;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (!drawingPaintCanvas) {
|
|
769
|
+
// No repair canvas this frame — either the paint rect is large enough to
|
|
770
|
+
// prefer the band renderer (preferBand: lower-latency than a big PSRAM
|
|
771
|
+
// repair canvas), or the repair canvas wouldn't allocate (over budget /
|
|
772
|
+
// heap fragmentation). Composite the node into the ~10KB band canvas one
|
|
773
|
+
// horizontal strip at a time, pushing each band tear-free. Avoids the
|
|
774
|
+
// direct clear→redraw-to-SPI that visibly flashes on press/scroll.
|
|
775
|
+
if (wantedBuffer && ui_render_node_bands(static_cast<uint16_t>(i), paintCanvasX, paintCanvasY, paintCanvasW, paintCanvasH)) {
|
|
776
|
+
__ui_frame_painted = 1;
|
|
777
|
+
if (!drawingBufferedScroll) {
|
|
778
|
+
ui_invalidate_scroll_canvas_for_node(i);
|
|
779
|
+
}
|
|
780
|
+
__ui_nodes[i].box.x = origBoxX;
|
|
781
|
+
__ui_nodes[i].box.y = origBoxY;
|
|
782
|
+
__ui_nodes[i].dirty = 0;
|
|
783
|
+
ui_refresh_add_rect(__ui_nodes[i].box.x, __ui_nodes[i].box.y, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
784
|
+
continue;
|
|
785
|
+
}
|
|
786
|
+
// Band canvas also failed — last resort: retry the repair canvas (it
|
|
787
|
+
// might fit when the band canvas is contended). If both allocations fail,
|
|
788
|
+
// lock this scroll owner for the frame and retain its previous pixels;
|
|
789
|
+
// never fall through to direct primitive SPI draws, which visibly tear.
|
|
790
|
+
if (wantedBuffer && preferBand) {
|
|
791
|
+
paintCanvas = ui_get_repair_canvas(paintCanvasW, paintCanvasH);
|
|
792
|
+
if (paintCanvas) {
|
|
793
|
+
drawingPaintCanvas = 1;
|
|
794
|
+
ui_display_set_target(paintCanvas);
|
|
795
|
+
ui_seed_paint_canvas_for_node(i, paintCanvas, paintCanvasX, paintCanvasY, 0);
|
|
796
|
+
baseDrawX -= paintCanvasX;
|
|
797
|
+
baseDrawY -= paintCanvasY;
|
|
798
|
+
drawX -= paintCanvasX;
|
|
799
|
+
drawY -= paintCanvasY;
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if (!drawingPaintCanvas) {
|
|
803
|
+
if (wantedBuffer && preferBand && __ui_scroll_render_locked &&
|
|
804
|
+
__ui_nodes[i].scrollable) {
|
|
805
|
+
__ui_scroll_render_locked[i] = 1;
|
|
806
|
+
__ui_nodes[i].dirty = 0;
|
|
807
|
+
__ui_nodes[i].box.x = origBoxX;
|
|
808
|
+
__ui_nodes[i].box.y = origBoxY;
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
ui_clear_press_offset_area(i, baseDrawX, baseDrawY, drawX, drawY, paintTextW, paintTextH);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
uint8_t skipListOutsetShadow =
|
|
815
|
+
(__ui_nodes[i].kind == NODE_LIST && !drawingBufferedScroll && !__ui_fb);
|
|
816
|
+
if (!skipListOutsetShadow) {
|
|
817
|
+
__ui_nodes[i].box.x = baseDrawX;
|
|
818
|
+
ui_draw_shadow(i, baseDrawY, 0);
|
|
819
|
+
__ui_nodes[i].box.x = drawX;
|
|
820
|
+
} else {
|
|
821
|
+
__ui_nodes[i].box.x = drawX;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Border color: use borderColor if set, otherwise fg.
|
|
825
|
+
UI_COLOR_T bColor = __ui_nodes[i].borderColor ? __ui_nodes[i].borderColor : __ui_nodes[i].fg;
|
|
826
|
+
// Background color blended toward clearColor by opacity (raw bg when 100%).
|
|
827
|
+
// NODE_FILL draws the fill at this color so opacity actually fades the
|
|
828
|
+
// element's background toward what's behind it.
|
|
829
|
+
UI_COLOR_T fillBg = __ui_nodes[i].bg;
|
|
830
|
+
// Apply opacity: blend fg/bg/border toward what's BEHIND the node when <100%.
|
|
831
|
+
// NOTE: blend toward the parent's clear color (ui_parent_clear_color), not
|
|
832
|
+
// the node's own clearColor — a filled node's clearColor IS its own bg, so
|
|
833
|
+
// blending bg toward it is a no-op (red toward red = red). The parent clear
|
|
834
|
+
// is the actual backdrop showing through the translucent element.
|
|
835
|
+
if (__ui_nodes[i].opacity < 100) {
|
|
836
|
+
UI_COLOR_T backdrop = ui_parent_clear_color(i);
|
|
837
|
+
bColor = ui_blend(bColor, backdrop, __ui_nodes[i].opacity);
|
|
838
|
+
fillBg = ui_blend(__ui_nodes[i].bg, backdrop, __ui_nodes[i].opacity);
|
|
839
|
+
}
|
|
840
|
+
// ── Per-kind draw dispatch ─────────────────────────────────────────────
|
|
841
|
+
// The kind switch lives in ui_draw_node_body (node-draw-body slice) so the
|
|
842
|
+
// strip/band renderer can reuse it. The ctx carries the draw state both
|
|
843
|
+
// paths share; the list case may return 1 (it handled canvas push,
|
|
844
|
+
// decoration, coord restore, and dirty-clear itself) in which case this
|
|
845
|
+
// node is done — skip the post-switch epilogue exactly as the old inline
|
|
846
|
+
// 'continue;' did.
|
|
847
|
+
UINodeDrawCtx __ui_ctx;
|
|
848
|
+
__ui_ctx.drawY = drawY;
|
|
849
|
+
__ui_ctx.bColor = bColor;
|
|
850
|
+
__ui_ctx.fillBg = fillBg;
|
|
851
|
+
__ui_ctx.ts = ts;
|
|
852
|
+
__ui_ctx.textMaxW = textMaxW;
|
|
853
|
+
__ui_ctx.tw = tw;
|
|
854
|
+
__ui_ctx.th = th;
|
|
855
|
+
__ui_ctx.paintTextW = paintTextW;
|
|
856
|
+
__ui_ctx.paintTextH = paintTextH;
|
|
857
|
+
__ui_ctx.displayText = displayText;
|
|
858
|
+
__ui_ctx.drawingBufferedScroll = drawingBufferedScroll;
|
|
859
|
+
__ui_ctx.drawTarget = __ui_draw_target;
|
|
860
|
+
__ui_ctx.origBoxX = origBoxX;
|
|
861
|
+
__ui_ctx.origBoxY = origBoxY;
|
|
862
|
+
__ui_frame_painted = 1;
|
|
863
|
+
if (ui_draw_node_body(i, &__ui_ctx)) {
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
if (__ui_nodes[i].kind == NODE_FILL && ui_rotation_quadrant(__ui_nodes[i].rotateDeg) != 0 &&
|
|
867
|
+
__ui_nodes[i].outlineStyle != 0 && __ui_nodes[i].outlineWidth > 0) {
|
|
868
|
+
uint8_t w = __ui_nodes[i].outlineWidth;
|
|
869
|
+
int16_t outlineW = ui_rotated_face_w(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
870
|
+
int16_t outlineH = ui_rotated_face_h(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
871
|
+
ui_draw_rect_outline(__ui_nodes[i].box.x - w, drawY - w,
|
|
872
|
+
outlineW + 2 * w, outlineH + 2 * w,
|
|
873
|
+
__ui_nodes[i].borderRadius + w, __ui_nodes[i].outlineStyle, w, __ui_nodes[i].outlineColor);
|
|
874
|
+
} else {
|
|
875
|
+
ui_draw_node_outline(i, __ui_nodes[i].box.x, drawY);
|
|
876
|
+
}
|
|
877
|
+
if (drawingPaintCanvas) {
|
|
878
|
+
ui_display_set_target(__ui_draw_target);
|
|
879
|
+
ui_push_canvas_rect(paintCanvas, paintCanvasX, paintCanvasY, paintCanvasW, paintCanvasH);
|
|
880
|
+
}
|
|
881
|
+
if (!drawingBufferedScroll) {
|
|
882
|
+
ui_invalidate_scroll_canvas_for_node(i);
|
|
883
|
+
}
|
|
884
|
+
// Restore original box coords (translated for canvas-local drawing above).
|
|
885
|
+
__ui_nodes[i].box.x = origBoxX;
|
|
886
|
+
__ui_nodes[i].box.y = origBoxY;
|
|
887
|
+
__ui_nodes[i].dirty = 0;
|
|
888
|
+
// Report this node's bounding box to the deferred-refresh accumulator. Phase
|
|
889
|
+
// 4 uses the box as the dirty rect (a safe over-estimate); Phase 5 tightens
|
|
890
|
+
// to the actual paint rect. No-op on TFT (compiles to nothing).
|
|
891
|
+
if (__ui_fb) ui_fb_add_rect(paintRect.x, paintRect.y, paintRect.w, paintRect.h);
|
|
892
|
+
ui_refresh_add_rect(__ui_nodes[i].box.x, __ui_nodes[i].box.y, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// Modal <select> list: stamp the overlay after the dirty pass so tree
|
|
896
|
+
// redraws never bury it. Stamped only when the overlay itself is dirty
|
|
897
|
+
// (open) or something painted beneath it this frame; the close path marks
|
|
898
|
+
// the whole tree dirty for the erase repaint.
|
|
899
|
+
if (__ui_select_menu >= 0 && (__ui_select_menu_dirty || __ui_frame_painted)) {
|
|
900
|
+
ui_select_menu_draw();
|
|
901
901
|
}`;
|
|
902
902
|
}
|