@typecad/ui 1.0.0-alpha.6 → 1.0.0-alpha.8
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 +6 -0
- package/dist/engine-index.js +2 -2
- package/dist/preview/host-ui-runtime.d.ts +1 -0
- package/dist/preview/host-ui-runtime.js +56 -11
- package/dist/ui-engine/font-assets.d.ts +8 -0
- package/dist/ui-engine/font-assets.js +13 -0
- package/dist/ui-engine/layout-engine.js +19 -6
- package/dist/ui-engine/model.d.ts +1 -1
- package/dist/ui-engine/model.js +10 -4
- package/dist/ui-engine/runtime-header/antialiasing.js +52 -32
- package/dist/ui-engine/runtime-header/blend-bodies.js +4 -4
- package/dist/ui-engine/runtime-header/canvas-helpers.js +42 -69
- package/dist/ui-engine/runtime-header/canvas-scrollbar.js +22 -10
- package/dist/ui-engine/runtime-header/color-mono-refresh.js +10 -10
- package/dist/ui-engine/runtime-header/cuttlefish-gfx.d.ts +1 -0
- package/dist/ui-engine/runtime-header/cuttlefish-gfx.js +563 -0
- package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +26 -10
- package/dist/ui-engine/runtime-header/display-shim.js +8 -8
- package/dist/ui-engine/runtime-header/forward-decls.js +52 -10
- package/dist/ui-engine/runtime-header/image-drawing.js +154 -84
- package/dist/ui-engine/runtime-header/init-press-input.js +20 -5
- package/dist/ui-engine/runtime-header/keyboard.js +62 -18
- package/dist/ui-engine/runtime-header/node-decoration.js +8 -9
- package/dist/ui-engine/runtime-header/node-draw-body.d.ts +1 -0
- package/dist/ui-engine/runtime-header/node-draw-body.js +1402 -0
- package/dist/ui-engine/runtime-header/paint-order-coords.js +48 -18
- package/dist/ui-engine/runtime-header/paint-rects-repair.js +38 -17
- package/dist/ui-engine/runtime-header/scroll-physics.js +15 -18
- package/dist/ui-engine/runtime-header/state-bindings-nav.js +23 -13
- package/dist/ui-engine/runtime-header/structs.js +12 -10
- package/dist/ui-engine/runtime-header/text-rendering.js +132 -71
- package/dist/ui-engine/runtime-header/tick/bindings-phase.js +29 -10
- package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +336 -749
- package/dist/ui-engine/runtime-header/tick/flush-phase.js +7 -3
- package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +19 -3
- package/dist/ui-engine/runtime-header/tick/transitions-phase.js +31 -24
- package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +46 -41
- package/dist/ui-engine/runtime-header/types-defines.js +23 -3
- package/dist/ui-engine/runtime-header.d.ts +10 -1
- package/dist/ui-engine/runtime-header.js +7 -1
- package/dist/ui-engine/transpile-ui.js +2 -2
- package/dist/ui-engine/ua-stylesheet.js +46 -3
- package/dist/ui-engine/ui-lowering.d.ts +1 -3
- package/dist/ui-engine/ui-lowering.js +18 -24
- package/dist/ui-engine/ui-registry.js +1 -6
- package/package.json +2 -2
- package/src/engine-index.ts +2 -2
- package/src/preview/host-ui-runtime.ts +56 -12
- package/src/ui-engine/font-assets.ts +13 -0
- package/src/ui-engine/layout-engine.ts +19 -6
- package/src/ui-engine/model.ts +10 -5
- package/src/ui-engine/runtime-header/antialiasing.ts +52 -32
- package/src/ui-engine/runtime-header/blend-bodies.ts +4 -4
- package/src/ui-engine/runtime-header/canvas-helpers.ts +42 -69
- package/src/ui-engine/runtime-header/canvas-scrollbar.ts +22 -10
- package/src/ui-engine/runtime-header/color-mono-refresh.ts +10 -10
- package/src/ui-engine/runtime-header/cuttlefish-gfx.ts +566 -0
- package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +26 -10
- package/src/ui-engine/runtime-header/display-shim.ts +8 -8
- package/src/ui-engine/runtime-header/forward-decls.ts +52 -10
- package/src/ui-engine/runtime-header/image-drawing.ts +154 -84
- package/src/ui-engine/runtime-header/init-press-input.ts +20 -5
- package/src/ui-engine/runtime-header/keyboard.ts +62 -18
- package/src/ui-engine/runtime-header/node-decoration.ts +8 -9
- package/src/ui-engine/runtime-header/node-draw-body.ts +1402 -0
- package/src/ui-engine/runtime-header/paint-order-coords.ts +48 -18
- package/src/ui-engine/runtime-header/paint-rects-repair.ts +38 -17
- package/src/ui-engine/runtime-header/scroll-physics.ts +15 -18
- package/src/ui-engine/runtime-header/state-bindings-nav.ts +23 -13
- package/src/ui-engine/runtime-header/structs.ts +12 -10
- package/src/ui-engine/runtime-header/text-rendering.ts +132 -71
- package/src/ui-engine/runtime-header/tick/bindings-phase.ts +29 -10
- package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +336 -749
- package/src/ui-engine/runtime-header/tick/flush-phase.ts +7 -3
- package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +19 -3
- package/src/ui-engine/runtime-header/tick/transitions-phase.ts +31 -24
- package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +46 -41
- package/src/ui-engine/runtime-header/types-defines.ts +23 -3
- package/src/ui-engine/runtime-header.ts +17 -1
- package/src/ui-engine/transpile-ui.ts +2 -2
- package/src/ui-engine/ua-stylesheet.ts +46 -3
- package/src/ui-engine/ui-lowering.ts +19 -26
- package/src/ui-engine/ui-registry.ts +1 -7
- package/dist/ui-engine/scroll-memory-diagnostics.d.ts +0 -16
- package/dist/ui-engine/scroll-memory-diagnostics.js +0 -58
- package/src/ui-engine/scroll-memory-diagnostics.ts +0 -79
|
@@ -27,9 +27,48 @@ export function emitTickDirtyDrawPhase(): string {
|
|
|
27
27
|
if (__ui_kb_dirty == 1) {
|
|
28
28
|
ui_kb_draw();
|
|
29
29
|
} else if (__ui_kb_dirty == 2) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
+
}
|
|
33
72
|
}
|
|
34
73
|
}
|
|
35
74
|
__ui_kb_dirty = 0;
|
|
@@ -37,6 +76,46 @@ export function emitTickDirtyDrawPhase(): string {
|
|
|
37
76
|
return;
|
|
38
77
|
}
|
|
39
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
|
+
if (__ui_fb && __ui_fb_needs_compose) {
|
|
92
|
+
UI_COLOR_T fbBg = (UI_COLOR_T)0;
|
|
93
|
+
if (__ui_active_screen_bg_node < __ui_node_count) {
|
|
94
|
+
fbBg = __ui_nodes[__ui_active_screen_bg_node].hasBg
|
|
95
|
+
? __ui_nodes[__ui_active_screen_bg_node].bg
|
|
96
|
+
: __ui_nodes[__ui_active_screen_bg_node].clearColor;
|
|
97
|
+
}
|
|
98
|
+
display_canvasFillScreen(__ui_fb, fbBg);
|
|
99
|
+
ui_fb_add_rect(0, 0, display_width(), display_height());
|
|
100
|
+
// A retained framebuffer cannot reuse the previous screen's pixels. Mark
|
|
101
|
+
// every node on the active screen dirty for one coherent composition.
|
|
102
|
+
for (uint16_t f = 0; f < __ui_node_count; f++) {
|
|
103
|
+
if (__ui_nodes[f].screenId == __ui_active_screen) {
|
|
104
|
+
__ui_nodes[f].dirty = 1;
|
|
105
|
+
__ui_nodes[f].lastTextHeight = 0;
|
|
106
|
+
__ui_nodes[f].layoutCacheKey = 0;
|
|
107
|
+
if (__ui_nodes[f].kind == NODE_PROGRESS || __ui_nodes[f].kind == NODE_RANGE) __ui_nodes[f].lastTextWidth = -1;
|
|
108
|
+
else __ui_nodes[f].lastTextWidth = 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
__ui_fb_needs_compose = 0;
|
|
112
|
+
__ui_fb_frame_dirty = 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (__ui_scroll_render_locked) {
|
|
116
|
+
for (uint16_t lock = 0; lock < __ui_node_count; lock++) __ui_scroll_render_locked[lock] = 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
40
119
|
// Process each dirty scroll container (Mode B shift-and-repair). A scroll
|
|
41
120
|
// delta shifts existing canvas pixels by the delta and repaints only the
|
|
42
121
|
// newly-exposed strip; a full invalidation (or no canvas) redraws the subtree.
|
|
@@ -48,7 +127,20 @@ export function emitTickDirtyDrawPhase(): string {
|
|
|
48
127
|
CuttlefishCanvas16* bufferedScrollRepaintCanvas = nullptr;
|
|
49
128
|
int16_t bufferedScrollRepaintY = 0;
|
|
50
129
|
int16_t bufferedScrollRepaintH = 0;
|
|
51
|
-
|
|
130
|
+
// Kept as a compatibility flag for the old compositor state machine. It is
|
|
131
|
+
// never enabled: direct per-node SPI drawing is a visible tearing path, so a
|
|
132
|
+
// missing canvas freezes the affected viewport instead.
|
|
133
|
+
uint8_t bufferedScrollDirectFull = 0;
|
|
134
|
+
// Local retained-canvas repair: dirty descendants draw directly into the
|
|
135
|
+
// full-frame framebuffer while the existing scroll canvas is invalidated for
|
|
136
|
+
// the next actual scroll. This avoids copying an unchanged viewport.
|
|
137
|
+
uint8_t bufferedScrollLocalRepair = 0;
|
|
138
|
+
// Band renderer pending: like bufferedScrollDirectFull but the actual paint
|
|
139
|
+
// is deferred to the scroll owner's z-order slot in the main draw loop. The
|
|
140
|
+
// band composites the whole subtree at once, so it must paint AFTER lower-z
|
|
141
|
+
// nodes (e.g. the screen background fill) but BEFORE higher-z ones (header).
|
|
142
|
+
// Painting it here (before the main loop) lets the bg fill overwrite it.
|
|
143
|
+
uint8_t bufferedScrollBands = 0;
|
|
52
144
|
for (uint16_t oi = 0; ; oi++) {
|
|
53
145
|
uint16_t s;
|
|
54
146
|
if (__ui_scroll_owners) {
|
|
@@ -62,7 +154,17 @@ export function emitTickDirtyDrawPhase(): string {
|
|
|
62
154
|
if (!ui_is_effectively_visible(s)) continue;
|
|
63
155
|
if (__ui_nodes[s].screenId != __ui_active_screen) continue;
|
|
64
156
|
if (__ui_nodes[s].contentHeight <= __ui_nodes[s].box.h) continue;
|
|
65
|
-
|
|
157
|
+
if (!__ui_nodes[s].dirty) continue;
|
|
158
|
+
// Virtualized lists manage their own rendering (NODE_LIST case in
|
|
159
|
+
// ui_draw_node_body). Skip them in the scroll dispatch — the band
|
|
160
|
+
// renderer walks their subtree (which is empty for virtualized lists),
|
|
161
|
+
// leaving the list node itself unpainted.
|
|
162
|
+
if (__ui_nodes[s].virtualized) continue;
|
|
163
|
+
// The retained framebuffer only needs the full viewport for actual scroll
|
|
164
|
+
// movement or the first composition. A local child update is repaired in
|
|
165
|
+
// the existing scroll canvas and contributes its own paint rect later;
|
|
166
|
+
// pushing the whole viewport here is the Forms-screen flash.
|
|
167
|
+
if (__ui_fb) __ui_fb_frame_dirty = 1;
|
|
66
168
|
|
|
67
169
|
int16_t vw = __ui_nodes[s].box.w;
|
|
68
170
|
int16_t vh = __ui_nodes[s].box.h;
|
|
@@ -76,7 +178,7 @@ export function emitTickDirtyDrawPhase(): string {
|
|
|
76
178
|
__ui_scroll_canvas_ok[s] = bufferedScrollCanvas ? 1 : 0;
|
|
77
179
|
}
|
|
78
180
|
if (bufferedScrollCanvas) {
|
|
79
|
-
bufferedScrollNode = (
|
|
181
|
+
bufferedScrollNode = static_cast<int16_t>(s);
|
|
80
182
|
bufferedScrollVX = vox;
|
|
81
183
|
bufferedScrollVY = voy;
|
|
82
184
|
|
|
@@ -95,7 +197,7 @@ export function emitTickDirtyDrawPhase(): string {
|
|
|
95
197
|
bufferedScrollRepaintY = exposedY;
|
|
96
198
|
bufferedScrollRepaintH = exposedH;
|
|
97
199
|
display_canvasFillScreen(bufferedScrollRepaintCanvas, scrollBg);
|
|
98
|
-
UIRect exposed = { vox,
|
|
200
|
+
UIRect exposed = { vox, static_cast<int16_t>(voy + exposedY), vw, exposedH };
|
|
99
201
|
for (uint16_t c = s + 1; c < __ui_nodes[s].subtreeEnd; c++) {
|
|
100
202
|
if (!ui_is_effectively_visible(c) || __ui_nodes[c].screenId != __ui_active_screen) {
|
|
101
203
|
__ui_nodes[c].dirty = 0;
|
|
@@ -120,66 +222,82 @@ export function emitTickDirtyDrawPhase(): string {
|
|
|
120
222
|
}
|
|
121
223
|
}
|
|
122
224
|
if (!canShift) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
225
|
+
// A zero-delta dirty scroll owner is usually a local child update (a
|
|
226
|
+
// form binding, button transition, or animation). Keep the retained
|
|
227
|
+
// canvas and its existing pixels in that case; the dirty descendants
|
|
228
|
+
// will redraw in place and the final framebuffer union stays local.
|
|
229
|
+
uint8_t localRepair = __ui_fb && deltaY == 0 &&
|
|
230
|
+
__ui_nodes[s].lastPaintedScrollY == __ui_nodes[s].scrollY &&
|
|
231
|
+
ui_scroll_subtree_has_dirty(s);
|
|
232
|
+
if (localRepair) {
|
|
233
|
+
// The framebuffer is already the authoritative composition surface for
|
|
234
|
+
// this zero-delta update. Let dirty descendants draw there directly;
|
|
235
|
+
// invalidate the retained scroll canvas after each child draw so it is
|
|
236
|
+
// rebuilt before the next scroll gesture.
|
|
237
|
+
bufferedScrollLocalRepair = 1;
|
|
238
|
+
bufferedScrollCanvas = nullptr;
|
|
239
|
+
bufferedScrollRepaintCanvas = nullptr;
|
|
240
|
+
} else {
|
|
241
|
+
// Initial composition, a large jump, or an owner-only invalidation:
|
|
242
|
+
// seed the whole viewport and repaint its complete stacking subtree.
|
|
243
|
+
for (uint16_t c = s; c < __ui_nodes[s].subtreeEnd; c++) {
|
|
244
|
+
__ui_nodes[c].dirty = 1;
|
|
245
|
+
if (__ui_nodes[c].kind == NODE_PROGRESS) __ui_nodes[c].lastTextWidth = -1;
|
|
246
|
+
else if (__ui_nodes[c].kind == NODE_RANGE) __ui_nodes[c].lastTextWidth = -1;
|
|
247
|
+
__ui_nodes[c].lastTextHeight = 0;
|
|
248
|
+
}
|
|
249
|
+
display_canvasFillScreen(bufferedScrollCanvas, scrollBg);
|
|
250
|
+
if (__ui_fb) ui_fb_add_rect(__ui_nodes[s].box.x, __ui_nodes[s].box.y,
|
|
251
|
+
__ui_nodes[s].box.w, __ui_nodes[s].box.h);
|
|
128
252
|
}
|
|
129
|
-
|
|
253
|
+
} else if (__ui_fb) {
|
|
254
|
+
// Shift-and-repair changes the entire viewport's visible mapping.
|
|
255
|
+
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);
|
|
130
257
|
}
|
|
131
258
|
// The scroll owner is represented by bufferedScrollCanvas this frame.
|
|
132
259
|
// Drawing it directly first clears the live display and makes scrolling
|
|
133
260
|
// visibly flash before the canvas is pushed.
|
|
134
261
|
__ui_nodes[s].dirty = 0;
|
|
135
262
|
} else {
|
|
136
|
-
// Canvas won't fit
|
|
137
|
-
//
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (absDelta > 0 && absDelta < vh) {
|
|
143
|
-
bufferedScrollNode = (int16_t)s;
|
|
144
|
-
bufferedScrollDirectStrip = 1;
|
|
145
|
-
ui_warn_scroll_memory((uint16_t)s, 2);
|
|
146
|
-
ui_scroll_direct_prepare(s, &bufferedScrollVX, &bufferedScrollVY);
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
ui_warn_scroll_memory((uint16_t)s, scrollNeed > (uint32_t)UI_SCROLL_CANVAS_BUDGET_BYTES ? 1 : 0);
|
|
263
|
+
// Canvas won't fit (no PSRAM / over budget). The band renderer composes the
|
|
264
|
+
// whole visible subtree into a short horizontal band canvas (vw ×
|
|
265
|
+
// UI_STRIP_BAND_HEIGHT) and pushes one band at a time, so each band
|
|
266
|
+
// completes before it touches the panel — tear-free, with ~10KB of SRAM
|
|
267
|
+
// regardless of program size. If the band canvas also cannot allocate,
|
|
268
|
+
// the viewport is retained rather than using direct per-node SPI draws.
|
|
150
269
|
if (__ui_scroll_canvas_ok) __ui_scroll_canvas_ok[s] = 0;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
display_canvasFillScreen(__ui_fb, fbBg);
|
|
174
|
-
}
|
|
270
|
+
// Defer the band render to the scroll owner's z-order slot in the main
|
|
271
|
+
// draw loop (see bufferedScrollBands handling below). Painting here would
|
|
272
|
+
// let lower-z nodes (screen bg fill) draw over the bands afterward. The
|
|
273
|
+
// owner stays dirty so the main loop reaches its z-slot and triggers the
|
|
274
|
+
// render; the band code clears it once painted. The subtree children are
|
|
275
|
+
// cleared now so the main loop never draws them directly — the band path
|
|
276
|
+
// is the sole renderer for the subtree this frame.
|
|
277
|
+
bufferedScrollNode = static_cast<int16_t>(s);
|
|
278
|
+
bufferedScrollVX = vox;
|
|
279
|
+
bufferedScrollVY = voy;
|
|
280
|
+
bufferedScrollBands = 1;
|
|
281
|
+
for (uint16_t c = s + 1; c < __ui_nodes[s].subtreeEnd; c++) {
|
|
282
|
+
__ui_nodes[c].dirty = 0;
|
|
283
|
+
}
|
|
284
|
+
break;
|
|
285
|
+
} // Only one scroll container per frame (the canvas is reused for subsequent
|
|
286
|
+
// ones in the next dirty frame). This matches the original design.
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
// The framebuffer target was selected and seeded before scroll compositors;
|
|
290
|
+
// the stacking draw pass below uses the same __ui_draw_target. The retained
|
|
291
|
+
// single buffer is a composition surface, not a synchronized panel swap.
|
|
175
292
|
|
|
176
293
|
// Draw dirty nodes in stacking order: lower z-index first, then source order.
|
|
177
294
|
// __ui_draw_order is built once in ui_init so each frame is O(N).
|
|
178
295
|
for (uint16_t __ui_draw_pass = 0; __ui_draw_pass < __ui_node_count; ) {
|
|
179
296
|
int16_t i;
|
|
180
297
|
if (__ui_draw_order) {
|
|
181
|
-
i = (
|
|
298
|
+
i = static_cast<int16_t>(__ui_draw_order[__ui_draw_pass++]);
|
|
182
299
|
if (!__ui_nodes[i].dirty) continue;
|
|
300
|
+
if (__ui_fb) __ui_fb_frame_dirty = 1;
|
|
183
301
|
if (!ui_is_effectively_visible(i)) { __ui_nodes[i].dirty = 0; continue; }
|
|
184
302
|
if (__ui_nodes[i].screenId != __ui_active_screen) { __ui_nodes[i].dirty = 0; continue; }
|
|
185
303
|
} else {
|
|
@@ -187,41 +305,85 @@ export function emitTickDirtyDrawPhase(): string {
|
|
|
187
305
|
i = -1;
|
|
188
306
|
for (uint16_t candidate = 0; candidate < __ui_node_count; candidate++) {
|
|
189
307
|
if (!__ui_nodes[candidate].dirty) continue;
|
|
308
|
+
if (__ui_fb) __ui_fb_frame_dirty = 1;
|
|
190
309
|
if (!ui_is_effectively_visible(candidate)) { __ui_nodes[candidate].dirty = 0; continue; }
|
|
191
310
|
if (__ui_nodes[candidate].screenId != __ui_active_screen) { __ui_nodes[candidate].dirty = 0; continue; }
|
|
192
|
-
if (i < 0 || ui_node_draws_before(candidate, (
|
|
311
|
+
if (i < 0 || ui_node_draws_before(candidate, static_cast<uint16_t>(i))) i = static_cast<int16_t>(candidate);
|
|
193
312
|
}
|
|
194
313
|
if (i < 0) break;
|
|
195
314
|
__ui_draw_pass++;
|
|
196
315
|
}
|
|
197
316
|
|
|
198
317
|
// Defer direct display draws for overflow scroll subtrees not composited this
|
|
199
|
-
// frame (Mode B canvas
|
|
200
|
-
// viewport and produces sequential
|
|
318
|
+
// frame (Mode B canvas, Mode C strip, direct-full fallback, or band render).
|
|
319
|
+
// Drawing them directly clears the live viewport and produces sequential
|
|
320
|
+
// flashes (AGENTS.md) — except in direct-full mode, where there is no canvas
|
|
321
|
+
// and direct draw is the only way to show content.
|
|
201
322
|
{
|
|
202
|
-
int16_t scrollComp = ui_overflow_scroll_compositor((
|
|
323
|
+
int16_t scrollComp = ui_overflow_scroll_compositor(static_cast<uint16_t>(i));
|
|
203
324
|
if (scrollComp >= 0) {
|
|
204
325
|
uint8_t compositing = scrollComp == bufferedScrollNode &&
|
|
205
|
-
(bufferedScrollCanvas ||
|
|
326
|
+
(bufferedScrollCanvas || bufferedScrollDirectFull || bufferedScrollBands || bufferedScrollLocalRepair);
|
|
206
327
|
if (!compositing) {
|
|
207
|
-
if ((
|
|
328
|
+
if (static_cast<uint16_t>(i) == static_cast<uint16_t>(scrollComp)) break;
|
|
208
329
|
__ui_nodes[i].dirty = 0;
|
|
209
330
|
continue;
|
|
210
331
|
}
|
|
211
332
|
}
|
|
212
333
|
}
|
|
213
334
|
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
335
|
+
// Band renderer: paint the whole subtree at the scroll owner's z-order slot.
|
|
336
|
+
// This must run AFTER lower-z nodes (e.g. the screen background fill, which
|
|
337
|
+
// has lower source order) have been drawn, so they don't overwrite the bands.
|
|
338
|
+
// When the main loop reaches the scroll owner itself, render the bands now
|
|
339
|
+
// (its z-position), mark the owner handled, and continue.
|
|
340
|
+
if (bufferedScrollBands && bufferedScrollNode >= 0 &&
|
|
341
|
+
static_cast<uint16_t>(i) == static_cast<uint16_t>(bufferedScrollNode)) {
|
|
342
|
+
if (ui_render_scroll_bands(static_cast<uint16_t>(bufferedScrollNode))) {
|
|
343
|
+
bufferedScrollBands = 0;
|
|
344
|
+
bufferedScrollNode = -1;
|
|
345
|
+
__ui_nodes[i].dirty = 0;
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
// Band canvas allocation failed. Retain the existing viewport pixels rather
|
|
349
|
+
// than falling back to direct-full SPI primitives; direct-full is the
|
|
350
|
+
// tearing path this compositor is designed to prevent.
|
|
351
|
+
bufferedScrollBands = 0;
|
|
352
|
+
if (__ui_scroll_render_locked) __ui_scroll_render_locked[static_cast<uint16_t>(bufferedScrollNode)] = 1;
|
|
353
|
+
__ui_nodes[static_cast<uint16_t>(bufferedScrollNode)].dirty = 0;
|
|
354
|
+
bufferedScrollNode = -1;
|
|
355
|
+
} // Band renderer early-push: if we're about to draw a node that comes AFTER
|
|
356
|
+
// the scroll owner in draw order (e.g. a header with higher source order)
|
|
357
|
+
// and the bands haven't rendered yet, render them first so they land below
|
|
358
|
+
// that higher-z node. Mirrors the Mode B canvas early-push below.
|
|
359
|
+
if (bufferedScrollBands && bufferedScrollNode >= 0 &&
|
|
360
|
+
!(i > bufferedScrollNode && i < __ui_nodes[bufferedScrollNode].subtreeEnd) &&
|
|
361
|
+
ui_node_draws_before(static_cast<uint16_t>(bufferedScrollNode), static_cast<uint16_t>(i))) {
|
|
362
|
+
if (ui_render_scroll_bands(static_cast<uint16_t>(bufferedScrollNode))) {
|
|
363
|
+
bufferedScrollBands = 0;
|
|
364
|
+
bufferedScrollNode = -1;
|
|
365
|
+
} else {
|
|
366
|
+
// Band alloc failed — retain the previous viewport rather than exposing
|
|
367
|
+
// a direct-full per-primitive SPI repaint.
|
|
368
|
+
bufferedScrollBands = 0;
|
|
369
|
+
if (__ui_scroll_render_locked) __ui_scroll_render_locked[static_cast<uint16_t>(bufferedScrollNode)] = 1;
|
|
370
|
+
__ui_nodes[static_cast<uint16_t>(bufferedScrollNode)].dirty = 0;
|
|
371
|
+
bufferedScrollNode = -1;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Mode C strip / direct-full: scroll owner is not drawn directly (would fill
|
|
376
|
+
// the viewport); its children are drawn directly below.
|
|
377
|
+
if ((bufferedScrollDirectFull || bufferedScrollLocalRepair) && bufferedScrollNode >= 0 &&
|
|
378
|
+
static_cast<uint16_t>(i) == static_cast<uint16_t>(bufferedScrollNode)) {
|
|
217
379
|
__ui_nodes[i].dirty = 0;
|
|
218
380
|
continue;
|
|
219
381
|
}
|
|
220
382
|
|
|
221
383
|
if (bufferedScrollNode >= 0 && bufferedScrollCanvas &&
|
|
222
384
|
!(i > bufferedScrollNode && i < __ui_nodes[bufferedScrollNode].subtreeEnd) &&
|
|
223
|
-
ui_node_draws_before((
|
|
224
|
-
!ui_scroll_subtree_has_dirty((
|
|
385
|
+
ui_node_draws_before(static_cast<uint16_t>(bufferedScrollNode), static_cast<uint16_t>(i)) &&
|
|
386
|
+
!ui_scroll_subtree_has_dirty(static_cast<uint16_t>(bufferedScrollNode))) {
|
|
225
387
|
ui_push_buffered_scroll_canvas(bufferedScrollCanvas, bufferedScrollRepaintCanvas,
|
|
226
388
|
bufferedScrollNode, bufferedScrollVX, bufferedScrollVY,
|
|
227
389
|
bufferedScrollRepaintY, bufferedScrollRepaintH, __ui_draw_target);
|
|
@@ -230,8 +392,16 @@ export function emitTickDirtyDrawPhase(): string {
|
|
|
230
392
|
bufferedScrollRepaintCanvas = nullptr;
|
|
231
393
|
}
|
|
232
394
|
|
|
233
|
-
// Redirect to the scroll canvas if this node is inside the buffered container
|
|
234
|
-
|
|
395
|
+
// Redirect to the scroll canvas if this node is inside the buffered container
|
|
396
|
+
// AND a real canvas is active this frame (Mode B only). Direct-strip and
|
|
397
|
+
// direct-full have no canvas — their children must draw to the display target
|
|
398
|
+
// (and thus qualify for the per-node paint-canvas optimization), not redirect
|
|
399
|
+
// to a null scroll canvas. Without this, a strip-drag frame sets
|
|
400
|
+
// drawingBufferedScroll=1, which skips the paint canvas and forces the
|
|
401
|
+
// repainting children to direct-draw per-pixel to SPI (~100ms/text node).
|
|
402
|
+
uint8_t drawingBufferedScroll = !bufferedScrollDirectFull &&
|
|
403
|
+
(bufferedScrollCanvas != nullptr) && bufferedScrollNode >= 0 &&
|
|
404
|
+
i > bufferedScrollNode && i < __ui_nodes[bufferedScrollNode].subtreeEnd;
|
|
235
405
|
int16_t origBoxX = __ui_nodes[i].box.x;
|
|
236
406
|
int16_t origBoxY = __ui_nodes[i].box.y;
|
|
237
407
|
if (drawingBufferedScroll) {
|
|
@@ -259,11 +429,11 @@ export function emitTickDirtyDrawPhase(): string {
|
|
|
259
429
|
ui_node_text_layout_metrics(i, textMaxW, &tw, &th);
|
|
260
430
|
uint16_t paintTextW = tw;
|
|
261
431
|
uint16_t paintTextH = th;
|
|
262
|
-
if (__ui_nodes[i].kind == NODE_TEXT) {
|
|
263
|
-
uint16_t hInset = (
|
|
264
|
-
uint16_t vInset = (
|
|
265
|
-
paintTextW =
|
|
266
|
-
paintTextH =
|
|
432
|
+
if (__ui_nodes[i].kind == NODE_TEXT || __ui_nodes[i].kind == NODE_SELECT) {
|
|
433
|
+
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;
|
|
434
|
+
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;
|
|
435
|
+
paintTextW = static_cast<uint16_t>(tw + hInset);
|
|
436
|
+
paintTextH = static_cast<uint16_t>(th + vInset);
|
|
267
437
|
}
|
|
268
438
|
if (__ui_nodes[i].kind == NODE_CHECK || __ui_nodes[i].kind == NODE_RADIO) {
|
|
269
439
|
paintTextW = tw + 22;
|
|
@@ -272,10 +442,6 @@ export function emitTickDirtyDrawPhase(): string {
|
|
|
272
442
|
|
|
273
443
|
UIRect paintRect;
|
|
274
444
|
ui_node_paint_rect(i, baseDrawX, baseDrawY, drawX, drawY, paintTextW, paintTextH, &paintRect);
|
|
275
|
-
int16_t cullX = paintRect.x;
|
|
276
|
-
int16_t cullY = paintRect.y;
|
|
277
|
-
int16_t cullW = paintRect.w;
|
|
278
|
-
int16_t cullH = paintRect.h;
|
|
279
445
|
if (drawingBufferedScroll) {
|
|
280
446
|
// Canvas-local clip: skip nodes fully outside the viewport (0..vw, 0..vh).
|
|
281
447
|
// Use the node's face rect (box.w/h), NOT the paint rect — the paint rect
|
|
@@ -289,18 +455,24 @@ export function emitTickDirtyDrawPhase(): string {
|
|
|
289
455
|
int16_t faceW = __ui_nodes[i].box.w;
|
|
290
456
|
int16_t faceH = __ui_nodes[i].box.h;
|
|
291
457
|
CuttlefishCanvas16* scrollDrawCanvas = bufferedScrollRepaintCanvas ? bufferedScrollRepaintCanvas : bufferedScrollCanvas;
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
458
|
+
// Guard against a null canvas (the subtree was flagged drawingBufferedScroll
|
|
459
|
+
// but no canvas is available this frame — e.g. a second scroll owner whose
|
|
460
|
+
// canvas couldn't allocate). Skip the canvas-local clip and draw the node
|
|
461
|
+
// to the display target; the scroll-viewport clip below still bounds it.
|
|
462
|
+
if (scrollDrawCanvas) {
|
|
463
|
+
int16_t scrollDrawW = display_canvasWidth(scrollDrawCanvas);
|
|
464
|
+
int16_t scrollDrawH = display_canvasHeight(scrollDrawCanvas);
|
|
465
|
+
if (bufferedScrollRepaintCanvas) {
|
|
466
|
+
scrollDrawW = __ui_nodes[bufferedScrollNode].box.w;
|
|
467
|
+
scrollDrawH = bufferedScrollRepaintH;
|
|
468
|
+
}
|
|
469
|
+
if (faceY + faceH <= 0 || faceY >= scrollDrawH ||
|
|
470
|
+
faceX + faceW <= 0 || faceX >= scrollDrawW) {
|
|
471
|
+
__ui_nodes[i].box.x = origBoxX;
|
|
472
|
+
__ui_nodes[i].box.y = origBoxY;
|
|
473
|
+
__ui_nodes[i].dirty = 0;
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
304
476
|
}
|
|
305
477
|
} else if (ui_is_rect_clipped_by_scroll(i, drawX, drawY, __ui_nodes[i].box.w, __ui_nodes[i].box.h)) {
|
|
306
478
|
__ui_nodes[i].box.x = origBoxX;
|
|
@@ -317,12 +489,20 @@ export function emitTickDirtyDrawPhase(): string {
|
|
|
317
489
|
int16_t paintCanvasH = paintRect.h;
|
|
318
490
|
// RAM-composite pixel-heavy nodes before SPI push. Skip when already drawing
|
|
319
491
|
// into a scroll canvas or a full-screen framebuffer (both are RAM targets).
|
|
320
|
-
|
|
492
|
+
uint8_t wantedBuffer = !drawingBufferedScroll && !__ui_fb && ui_should_buffer_paint(i, paintCanvasW, paintCanvasH);
|
|
493
|
+
// Large paint rects prefer the band renderer (small ~10KB SRAM canvas,
|
|
494
|
+
// strip-at-a-time) over the repair canvas even when the repair canvas could
|
|
495
|
+
// allocate (e.g. in PSRAM). A large repair canvas has higher alloc/seed/push
|
|
496
|
+
// latency than banding, which shows as a press/scroll flash; the band canvas
|
|
497
|
+
// lives in fast internal SRAM and pushes smaller per-band transactions.
|
|
498
|
+
uint8_t preferBand = wantedBuffer && (UI_BAND_PREFER_PIXELS > 0) &&
|
|
499
|
+
(static_cast<uint32_t>(paintCanvasW) * static_cast<uint32_t>(paintCanvasH) > static_cast<uint32_t>(UI_BAND_PREFER_PIXELS));
|
|
500
|
+
if (wantedBuffer && !preferBand) {
|
|
321
501
|
paintCanvas = ui_get_repair_canvas(paintCanvasW, paintCanvasH);
|
|
322
502
|
if (paintCanvas) {
|
|
323
503
|
drawingPaintCanvas = 1;
|
|
324
504
|
ui_display_set_target(paintCanvas);
|
|
325
|
-
ui_seed_paint_canvas_for_node(i, paintCanvas, paintCanvasX, paintCanvasY);
|
|
505
|
+
ui_seed_paint_canvas_for_node(i, paintCanvas, paintCanvasX, paintCanvasY, 0);
|
|
326
506
|
baseDrawX -= paintCanvasX;
|
|
327
507
|
baseDrawY -= paintCanvasY;
|
|
328
508
|
drawX -= paintCanvasX;
|
|
@@ -333,7 +513,49 @@ export function emitTickDirtyDrawPhase(): string {
|
|
|
333
513
|
}
|
|
334
514
|
}
|
|
335
515
|
if (!drawingPaintCanvas) {
|
|
336
|
-
|
|
516
|
+
// No repair canvas this frame — either the paint rect is large enough to
|
|
517
|
+
// prefer the band renderer (preferBand: lower-latency than a big PSRAM
|
|
518
|
+
// repair canvas), or the repair canvas wouldn't allocate (over budget /
|
|
519
|
+
// heap fragmentation). Composite the node into the ~10KB band canvas one
|
|
520
|
+
// horizontal strip at a time, pushing each band tear-free. Avoids the
|
|
521
|
+
// direct clear→redraw-to-SPI that visibly flashes on press/scroll.
|
|
522
|
+
if (wantedBuffer && ui_render_node_bands(static_cast<uint16_t>(i), paintCanvasX, paintCanvasY, paintCanvasW, paintCanvasH)) {
|
|
523
|
+
if (!drawingBufferedScroll) {
|
|
524
|
+
ui_invalidate_scroll_canvas_for_node(i);
|
|
525
|
+
}
|
|
526
|
+
__ui_nodes[i].box.x = origBoxX;
|
|
527
|
+
__ui_nodes[i].box.y = origBoxY;
|
|
528
|
+
__ui_nodes[i].dirty = 0;
|
|
529
|
+
ui_refresh_add_rect(__ui_nodes[i].box.x, __ui_nodes[i].box.y, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
530
|
+
continue;
|
|
531
|
+
}
|
|
532
|
+
// Band canvas also failed — last resort: retry the repair canvas (it
|
|
533
|
+
// might fit when the band canvas is contended). If both allocations fail,
|
|
534
|
+
// lock this scroll owner for the frame and retain its previous pixels;
|
|
535
|
+
// never fall through to direct primitive SPI draws, which visibly tear.
|
|
536
|
+
if (wantedBuffer && preferBand) {
|
|
537
|
+
paintCanvas = ui_get_repair_canvas(paintCanvasW, paintCanvasH);
|
|
538
|
+
if (paintCanvas) {
|
|
539
|
+
drawingPaintCanvas = 1;
|
|
540
|
+
ui_display_set_target(paintCanvas);
|
|
541
|
+
ui_seed_paint_canvas_for_node(i, paintCanvas, paintCanvasX, paintCanvasY, 0);
|
|
542
|
+
baseDrawX -= paintCanvasX;
|
|
543
|
+
baseDrawY -= paintCanvasY;
|
|
544
|
+
drawX -= paintCanvasX;
|
|
545
|
+
drawY -= paintCanvasY;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (!drawingPaintCanvas) {
|
|
549
|
+
if (wantedBuffer && preferBand && __ui_scroll_render_locked &&
|
|
550
|
+
__ui_nodes[i].scrollable) {
|
|
551
|
+
__ui_scroll_render_locked[i] = 1;
|
|
552
|
+
__ui_nodes[i].dirty = 0;
|
|
553
|
+
__ui_nodes[i].box.x = origBoxX;
|
|
554
|
+
__ui_nodes[i].box.y = origBoxY;
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
ui_clear_press_offset_area(i, baseDrawX, baseDrawY, drawX, drawY, paintTextW, paintTextH);
|
|
558
|
+
}
|
|
337
559
|
}
|
|
338
560
|
uint8_t skipListOutsetShadow =
|
|
339
561
|
(__ui_nodes[i].kind == NODE_LIST && !drawingBufferedScroll && !__ui_fb);
|
|
@@ -361,666 +583,30 @@ export function emitTickDirtyDrawPhase(): string {
|
|
|
361
583
|
bColor = ui_blend(bColor, backdrop, __ui_nodes[i].opacity);
|
|
362
584
|
fillBg = ui_blend(__ui_nodes[i].bg, backdrop, __ui_nodes[i].opacity);
|
|
363
585
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
int16_t insetL = (int16_t)__ui_nodes[i].borderWidth + (int16_t)__ui_nodes[i].paddingLeft;
|
|
389
|
-
int16_t insetR = (int16_t)__ui_nodes[i].borderWidth + (int16_t)__ui_nodes[i].paddingRight;
|
|
390
|
-
int16_t insetT = (int16_t)__ui_nodes[i].borderWidth + (int16_t)__ui_nodes[i].paddingTop;
|
|
391
|
-
int16_t textX = __ui_nodes[i].box.x + insetL;
|
|
392
|
-
int16_t textY = drawY + insetT;
|
|
393
|
-
int16_t textW = (int16_t)__ui_nodes[i].box.w - insetL - insetR;
|
|
394
|
-
if (textW < 1) textW = 1;
|
|
395
|
-
uint8_t textBoxPainted = 0;
|
|
396
|
-
if (__ui_nodes[i].gradientEnabled > 0) {
|
|
397
|
-
ui_draw_gradient_fill(i, drawY);
|
|
398
|
-
textBoxPainted = 1;
|
|
399
|
-
} else if (__ui_nodes[i].borderRadius > 0 && __ui_nodes[i].hasBg) {
|
|
400
|
-
ui_display_fill_round_rect(__ui_nodes[i].box.x, drawY,
|
|
401
|
-
__ui_nodes[i].box.w, __ui_nodes[i].box.h, __ui_nodes[i].borderRadius, fillBg);
|
|
402
|
-
textBoxPainted = 1;
|
|
403
|
-
} else if (__ui_nodes[i].hasBg) {
|
|
404
|
-
ui_display_fill_rect(__ui_nodes[i].box.x, drawY,
|
|
405
|
-
__ui_nodes[i].box.w, __ui_nodes[i].box.h, fillBg);
|
|
406
|
-
textBoxPainted = 1;
|
|
407
|
-
}
|
|
408
|
-
{
|
|
409
|
-
uint16_t clearW = __ui_nodes[i].box.w;
|
|
410
|
-
int16_t paintedTextW = (int16_t)__ui_nodes[i].lastTextWidth + insetL + insetR;
|
|
411
|
-
if (__ui_nodes[i].lastTextWidth > 0 && paintedTextW > (int16_t)clearW) {
|
|
412
|
-
clearW = (uint16_t)paintedTextW;
|
|
413
|
-
}
|
|
414
|
-
uint16_t paddedTw = (uint16_t)((int16_t)tw + insetL + insetR);
|
|
415
|
-
if (paddedTw > clearW) clearW = paddedTw;
|
|
416
|
-
// overflow:hidden/scroll: never clear past the node's own box. A nowrap
|
|
417
|
-
// line wider than its box would otherwise erase the parent's border.
|
|
418
|
-
if (__ui_nodes[i].scrollable) clearW = __ui_nodes[i].box.w;
|
|
419
|
-
uint16_t clearH = __ui_nodes[i].box.h;
|
|
420
|
-
int16_t paintedTextH = (int16_t)__ui_nodes[i].lastTextHeight + insetT + (int16_t)__ui_nodes[i].paddingBottom + (int16_t)__ui_nodes[i].borderWidth;
|
|
421
|
-
if (__ui_nodes[i].lastTextHeight > 0 && paintedTextH > (int16_t)clearH) {
|
|
422
|
-
clearH = (uint16_t)paintedTextH;
|
|
423
|
-
}
|
|
424
|
-
uint16_t paddedTh = (uint16_t)((int16_t)th + insetT + (int16_t)__ui_nodes[i].paddingBottom + (int16_t)__ui_nodes[i].borderWidth);
|
|
425
|
-
if (paddedTh > clearH) clearH = paddedTh;
|
|
426
|
-
// Dynamic transparent text still needs a clear, otherwise old glyph
|
|
427
|
-
// pixels accumulate when only this text node is dirty.
|
|
428
|
-
UI_COLOR_T clearCol = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor;
|
|
429
|
-
// Blend the clear toward the backdrop by opacity so a translucent text
|
|
430
|
-
// node (inherited from an opacity:<1 parent) doesn't repaint a solid
|
|
431
|
-
// block of its parent's fill around the glyphs.
|
|
432
|
-
if (__ui_nodes[i].opacity < 100) {
|
|
433
|
-
clearCol = ui_blend(clearCol, ui_parent_clear_color(i), __ui_nodes[i].opacity);
|
|
434
|
-
}
|
|
435
|
-
if (!textBoxPainted) {
|
|
436
|
-
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, clearW, clearH, clearCol);
|
|
437
|
-
}
|
|
438
|
-
__ui_nodes[i].lastTextWidth = tw;
|
|
439
|
-
__ui_nodes[i].lastTextHeight = th;
|
|
440
|
-
}
|
|
441
|
-
ui_draw_shadow(i, drawY, 1);
|
|
442
|
-
if (__ui_nodes[i].borderStyle != 0) {
|
|
443
|
-
ui_draw_node_border(i, __ui_nodes[i].box.x, drawY, bColor);
|
|
444
|
-
}
|
|
445
|
-
// Rich-text (inline runs): draw from precomputed geometry instead of the
|
|
446
|
-
// single-string wrapped path. Geometry is baked at transpile time; the
|
|
447
|
-
// runtime does not re-wrap.
|
|
448
|
-
if (__ui_nodes[i].runCount > 0) {
|
|
449
|
-
UI_COLOR_T richTextBg;
|
|
450
|
-
if (__ui_nodes[i].opacity < 100) {
|
|
451
|
-
uint16_t p = __ui_nodes[i].parent;
|
|
452
|
-
UI_COLOR_T source = (p != UI_NO_PARENT && __ui_nodes[p].hasBg) ? __ui_nodes[p].bg
|
|
453
|
-
: (__ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor);
|
|
454
|
-
richTextBg = ui_blend(source, __ui_nodes[i].clearColor, __ui_nodes[i].opacity);
|
|
455
|
-
} else {
|
|
456
|
-
richTextBg = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : ui_parent_clear_color(i);
|
|
457
|
-
}
|
|
458
|
-
if (__ui_nodes[i].textShadowCount > 0) {
|
|
459
|
-
UI_COLOR_T tsClear = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor;
|
|
460
|
-
uint32_t tsCol = ui_blend(__ui_nodes[i].textShadowColor, tsClear, __ui_nodes[i].textShadowAlpha);
|
|
461
|
-
// Shadow pass: draw the rich block in the shadow color at the offset.
|
|
462
|
-
// (Per-segment shadow color is approximated by drawing the whole
|
|
463
|
-
// block once in tsCol.)
|
|
464
|
-
ui_draw_rich_text(i,
|
|
465
|
-
textX + __ui_nodes[i].textShadowOffsetX,
|
|
466
|
-
textY + __ui_nodes[i].textShadowOffsetY,
|
|
467
|
-
tsClear, __ui_nodes[i].fontAntialias, 1, tsCol, (uint16_t)textW);
|
|
468
|
-
}
|
|
469
|
-
ui_draw_rich_text(i, textX, textY, richTextBg, __ui_nodes[i].fontAntialias, 0, 0, (uint16_t)textW);
|
|
470
|
-
break;
|
|
471
|
-
}
|
|
472
|
-
{
|
|
473
|
-
// Text shadow: draw the text in the shadow color at the offset first.
|
|
474
|
-
UI_COLOR_T tsClear = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor;
|
|
475
|
-
if (__ui_nodes[i].textShadowCount > 0) {
|
|
476
|
-
uint32_t tsCol = ui_blend(__ui_nodes[i].textShadowColor, tsClear, __ui_nodes[i].textShadowAlpha);
|
|
477
|
-
ui_draw_wrapped_text(displayText,
|
|
478
|
-
textX + __ui_nodes[i].textShadowOffsetX,
|
|
479
|
-
textY + __ui_nodes[i].textShadowOffsetY,
|
|
480
|
-
(uint16_t)textW, tsCol, tsCol, ts, __ui_nodes[i].fontAntialias,
|
|
481
|
-
__ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing, __ui_nodes[i].lineHeight,
|
|
482
|
-
__ui_nodes[i].whiteSpaceMode, __ui_nodes[i].textAlign, 0, __ui_nodes[i].textOverflow);
|
|
483
|
-
}
|
|
484
|
-
// Use the parent's clear color as the text background when the node
|
|
485
|
-
// has no own background. This makes Adafruit_GFX's opaque glyph-cell
|
|
486
|
-
// fill blend with the parent (instead of drawing solid fg blocks that
|
|
487
|
-
// overlap adjacent lines/elements). For AA text, fg != bg so the
|
|
488
|
-
// edge-detection path still runs correctly.
|
|
489
|
-
// Glyph-cell background. For a translucent text node sitting on a
|
|
490
|
-
// filled translucent parent, the glyph cells must match the parent's
|
|
491
|
-
// blended fill: blend565(parent.bg, backdrop, opacity). The node's own
|
|
492
|
-
// clearColor carries the backdrop (set by flatten), and opacity has
|
|
493
|
-
// already inherited from the parent. Use the parent's raw bg as the
|
|
494
|
-
// source so the glyph cells reproduce the parent's translucent fill.
|
|
495
|
-
UI_COLOR_T textBg;
|
|
496
|
-
if (__ui_nodes[i].opacity < 100) {
|
|
497
|
-
uint16_t p = __ui_nodes[i].parent;
|
|
498
|
-
UI_COLOR_T source = (p != UI_NO_PARENT && __ui_nodes[p].hasBg) ? __ui_nodes[p].bg
|
|
499
|
-
: (__ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor);
|
|
500
|
-
textBg = ui_blend(source, __ui_nodes[i].clearColor, __ui_nodes[i].opacity);
|
|
501
|
-
} else {
|
|
502
|
-
textBg = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : ui_parent_clear_color(i);
|
|
503
|
-
}
|
|
504
|
-
ui_draw_wrapped_text(displayText, textX, textY, (uint16_t)textW,
|
|
505
|
-
__ui_nodes[i].fg, textBg, ts, __ui_nodes[i].fontAntialias,
|
|
506
|
-
__ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing, __ui_nodes[i].lineHeight,
|
|
507
|
-
__ui_nodes[i].whiteSpaceMode, __ui_nodes[i].textAlign, __ui_nodes[i].underline, __ui_nodes[i].textOverflow);
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
break;
|
|
511
|
-
case NODE_BUTTON:
|
|
512
|
-
if (__ui_nodes[i].borderRadius > 0 && __ui_nodes[i].hasBg)
|
|
513
|
-
ui_display_fill_round_rect(__ui_nodes[i].box.x, drawY, __ui_nodes[i].box.w, __ui_nodes[i].box.h, __ui_nodes[i].borderRadius, __ui_nodes[i].bg);
|
|
514
|
-
else if (__ui_nodes[i].hasBg)
|
|
515
|
-
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, __ui_nodes[i].box.w, __ui_nodes[i].box.h, __ui_nodes[i].bg);
|
|
516
|
-
ui_draw_shadow(i, drawY, 1);
|
|
517
|
-
if (__ui_nodes[i].borderStyle != 0) {
|
|
518
|
-
ui_draw_node_border(i, __ui_nodes[i].box.x, drawY, bColor);
|
|
519
|
-
}
|
|
520
|
-
{
|
|
521
|
-
int16_t insetL = (int16_t)__ui_nodes[i].borderWidth + (int16_t)__ui_nodes[i].paddingLeft;
|
|
522
|
-
int16_t insetR = (int16_t)__ui_nodes[i].borderWidth + (int16_t)__ui_nodes[i].paddingRight;
|
|
523
|
-
int16_t insetT = (int16_t)__ui_nodes[i].borderWidth + (int16_t)__ui_nodes[i].paddingTop;
|
|
524
|
-
int16_t insetB = (int16_t)__ui_nodes[i].borderWidth + (int16_t)__ui_nodes[i].paddingBottom;
|
|
525
|
-
int16_t textX = __ui_nodes[i].box.x + insetL;
|
|
526
|
-
int16_t textY = drawY + insetT;
|
|
527
|
-
int16_t textW = (int16_t)__ui_nodes[i].box.w - insetL - insetR;
|
|
528
|
-
int16_t textH = (int16_t)__ui_nodes[i].box.h - insetT - insetB;
|
|
529
|
-
if (textW < 1) textW = 1;
|
|
530
|
-
if (textH < 1) textH = (int16_t)th;
|
|
531
|
-
ui_draw_wrapped_text(displayText,
|
|
532
|
-
textX,
|
|
533
|
-
textY + (textH - (int16_t)th) / 2,
|
|
534
|
-
(uint16_t)textW,
|
|
535
|
-
__ui_nodes[i].fg,
|
|
536
|
-
__ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor,
|
|
537
|
-
ts, __ui_nodes[i].fontAntialias, __ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing,
|
|
538
|
-
__ui_nodes[i].lineHeight, __ui_nodes[i].whiteSpaceMode, __ui_nodes[i].textAlign, __ui_nodes[i].underline, __ui_nodes[i].textOverflow);
|
|
539
|
-
}
|
|
540
|
-
break;
|
|
541
|
-
case NODE_CHECK:
|
|
542
|
-
{
|
|
543
|
-
uint16_t clearW = __ui_nodes[i].box.w;
|
|
544
|
-
if (__ui_nodes[i].lastTextWidth > 0 && __ui_nodes[i].lastTextWidth > (int16_t)clearW) {
|
|
545
|
-
clearW = (uint16_t)__ui_nodes[i].lastTextWidth;
|
|
546
|
-
}
|
|
547
|
-
if (paintTextW > clearW) clearW = paintTextW;
|
|
548
|
-
uint16_t clearH = __ui_nodes[i].box.h;
|
|
549
|
-
if (__ui_nodes[i].lastTextHeight > 0 && __ui_nodes[i].lastTextHeight > (int16_t)clearH) {
|
|
550
|
-
clearH = (uint16_t)__ui_nodes[i].lastTextHeight;
|
|
551
|
-
}
|
|
552
|
-
if (paintTextH > clearH) clearH = paintTextH;
|
|
553
|
-
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, clearW, clearH,
|
|
554
|
-
__ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor);
|
|
555
|
-
__ui_nodes[i].lastTextWidth = paintTextW;
|
|
556
|
-
__ui_nodes[i].lastTextHeight = paintTextH;
|
|
557
|
-
}
|
|
558
|
-
{
|
|
559
|
-
int16_t cbX = __ui_nodes[i].box.x;
|
|
560
|
-
int16_t cbY = drawY;
|
|
561
|
-
if (__ui_nodes[i].value) {
|
|
562
|
-
ui_display_fill_rect(cbX, cbY, 16, 16, __ui_nodes[i].fg);
|
|
563
|
-
UI_COLOR_T inv = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor;
|
|
564
|
-
#ifdef UI_AA
|
|
565
|
-
{
|
|
566
|
-
// Draw the checkmark to a 16×16 AA canvas for smooth diagonals.
|
|
567
|
-
CuttlefishCanvas16* c = ui_aa_begin(16, 16, __ui_nodes[i].fg);
|
|
568
|
-
// First stroke: down-left (3,8 → 7,12)
|
|
569
|
-
ui_aa_line(c, 4.0f, 8.0f, 7.0f, 12.0f, inv);
|
|
570
|
-
ui_aa_line(c, 5.0f, 8.0f, 8.0f, 12.0f, inv);
|
|
571
|
-
// Second stroke: up-right (7,11 → 13,4)
|
|
572
|
-
ui_aa_line(c, 7.0f, 11.0f, 13.0f, 4.0f, inv);
|
|
573
|
-
ui_aa_line(c, 8.0f, 11.0f, 14.0f, 4.0f, inv);
|
|
574
|
-
ui_aa_push(c, cbX, cbY);
|
|
575
|
-
}
|
|
576
|
-
#else
|
|
577
|
-
ui_display_draw_line(cbX + 3, cbY + 8, cbX + 7, cbY + 12, inv);
|
|
578
|
-
ui_display_draw_line(cbX + 4, cbY + 8, cbX + 8, cbY + 12, inv);
|
|
579
|
-
ui_display_draw_line(cbX + 3, cbY + 9, cbX + 7, cbY + 13, inv);
|
|
580
|
-
ui_display_draw_line(cbX + 7, cbY + 12, cbX + 13, cbY + 4, inv);
|
|
581
|
-
ui_display_draw_line(cbX + 8, cbY + 12, cbX + 14, cbY + 4, inv);
|
|
582
|
-
ui_display_draw_line(cbX + 7, cbY + 13, cbX + 13, cbY + 5, inv);
|
|
583
|
-
#endif
|
|
584
|
-
} else {
|
|
585
|
-
ui_display_draw_rect(cbX, cbY, 16, 16, __ui_nodes[i].fg);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
ui_draw_wrapped_text(displayText, __ui_nodes[i].box.x + 22, drawY, textMaxW,
|
|
589
|
-
__ui_nodes[i].fg, __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor,
|
|
590
|
-
ts, __ui_nodes[i].fontAntialias, __ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing,
|
|
591
|
-
__ui_nodes[i].lineHeight, __ui_nodes[i].whiteSpaceMode, 0, __ui_nodes[i].underline, __ui_nodes[i].textOverflow);
|
|
592
|
-
break;
|
|
593
|
-
case NODE_RADIO:
|
|
594
|
-
{
|
|
595
|
-
uint16_t clearW = __ui_nodes[i].box.w;
|
|
596
|
-
if (__ui_nodes[i].lastTextWidth > 0 && __ui_nodes[i].lastTextWidth > (int16_t)clearW) {
|
|
597
|
-
clearW = (uint16_t)__ui_nodes[i].lastTextWidth;
|
|
598
|
-
}
|
|
599
|
-
if (paintTextW > clearW) clearW = paintTextW;
|
|
600
|
-
uint16_t clearH = __ui_nodes[i].box.h;
|
|
601
|
-
if (__ui_nodes[i].lastTextHeight > 0 && __ui_nodes[i].lastTextHeight > (int16_t)clearH) {
|
|
602
|
-
clearH = (uint16_t)__ui_nodes[i].lastTextHeight;
|
|
603
|
-
}
|
|
604
|
-
if (paintTextH > clearH) clearH = paintTextH;
|
|
605
|
-
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, clearW, clearH,
|
|
606
|
-
__ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor);
|
|
607
|
-
__ui_nodes[i].lastTextWidth = paintTextW;
|
|
608
|
-
__ui_nodes[i].lastTextHeight = paintTextH;
|
|
609
|
-
int16_t cbX = __ui_nodes[i].box.x;
|
|
610
|
-
int16_t cbY = drawY;
|
|
611
|
-
#ifdef UI_AA
|
|
612
|
-
{
|
|
613
|
-
// Render the radio circle to a 16×16 AA canvas, then push.
|
|
614
|
-
UI_COLOR_T radioBg = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor;
|
|
615
|
-
CuttlefishCanvas16* c = ui_aa_begin(16, 16, radioBg);
|
|
616
|
-
if (__ui_nodes[i].value) {
|
|
617
|
-
ui_aa_fill_circle(c, 8, 8, 7.0f, __ui_nodes[i].fg);
|
|
618
|
-
ui_aa_fill_circle(c, 8, 8, 3.0f, radioBg);
|
|
619
|
-
} else {
|
|
620
|
-
ui_aa_circle(c, 8, 8, 7.0f, __ui_nodes[i].fg);
|
|
621
|
-
}
|
|
622
|
-
ui_aa_push(c, cbX, cbY);
|
|
623
|
-
}
|
|
624
|
-
#else
|
|
625
|
-
if (__ui_nodes[i].value) {
|
|
626
|
-
ui_display_fill_circle(cbX + 8, cbY + 8, 7, __ui_nodes[i].fg);
|
|
627
|
-
ui_display_fill_circle(cbX + 8, cbY + 8, 3, __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor);
|
|
628
|
-
} else {
|
|
629
|
-
ui_display_draw_circle(cbX + 8, cbY + 8, 7, __ui_nodes[i].fg);
|
|
630
|
-
}
|
|
631
|
-
#endif
|
|
632
|
-
}
|
|
633
|
-
ui_draw_wrapped_text(displayText, __ui_nodes[i].box.x + 22, drawY, textMaxW,
|
|
634
|
-
__ui_nodes[i].fg, __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor,
|
|
635
|
-
ts, __ui_nodes[i].fontAntialias, __ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing,
|
|
636
|
-
__ui_nodes[i].lineHeight, __ui_nodes[i].whiteSpaceMode, 0, __ui_nodes[i].underline, __ui_nodes[i].textOverflow);
|
|
637
|
-
break;
|
|
638
|
-
case NODE_PROGRESS:
|
|
639
|
-
// Progress bar: outline track + filled portion based on .value (0-100).
|
|
640
|
-
// Incremental redraw — only draws/clears the delta to avoid flashing.
|
|
641
|
-
{
|
|
642
|
-
int16_t bx = __ui_nodes[i].box.x;
|
|
643
|
-
int16_t by = drawY;
|
|
644
|
-
int16_t bw = __ui_nodes[i].box.w;
|
|
645
|
-
int16_t bh = __ui_nodes[i].box.h;
|
|
646
|
-
UI_COLOR_T bgCol = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor;
|
|
647
|
-
UI_COLOR_T fgCol = __ui_nodes[i].fg;
|
|
648
|
-
|
|
649
|
-
// On first draw (lastTextWidth < 0), draw everything.
|
|
650
|
-
// Otherwise incremental: only update the changed portion.
|
|
651
|
-
uint8_t pct = constrain(__ui_nodes[i].value, 0, 100);
|
|
652
|
-
int16_t fillW = ((int32_t)(bw - 2) * pct) / 100;
|
|
653
|
-
int16_t prevW = __ui_nodes[i].lastTextWidth; // reused as previous fill width
|
|
654
|
-
|
|
655
|
-
if (prevW < 0) {
|
|
656
|
-
// Full redraw: outline + background + fill
|
|
657
|
-
ui_display_draw_rect(bx, by, bw, bh, fgCol);
|
|
658
|
-
ui_display_fill_rect(bx + 1, by + 1, bw - 2, bh - 2, bgCol);
|
|
659
|
-
if (fillW > 0) {
|
|
660
|
-
ui_display_fill_rect(bx + 1, by + 1, fillW, bh - 2, fgCol);
|
|
661
|
-
}
|
|
662
|
-
} else if (fillW > prevW) {
|
|
663
|
-
// Value increased: draw new fill segment on top (no clear needed)
|
|
664
|
-
ui_display_fill_rect(bx + 1 + prevW, by + 1, fillW - prevW, bh - 2, fgCol);
|
|
665
|
-
} else if (fillW < prevW) {
|
|
666
|
-
// Value decreased: clear the removed portion
|
|
667
|
-
ui_display_fill_rect(bx + 1 + fillW, by + 1, prevW - fillW, bh - 2, bgCol);
|
|
668
|
-
}
|
|
669
|
-
// Remember current fill width for next incremental update
|
|
670
|
-
__ui_nodes[i].lastTextWidth = fillW;
|
|
671
|
-
}
|
|
672
|
-
break;
|
|
673
|
-
case NODE_RANGE:
|
|
674
|
-
// Range slider: horizontal track + draggable thumb.
|
|
675
|
-
// Incremental redraw (like NODE_PROGRESS): lastTextWidth holds the
|
|
676
|
-
// previous fill width. We erase the delta region between old and new
|
|
677
|
-
// thumb positions with the background, then redraw the track portion
|
|
678
|
-
// and the new thumb — so dragging backward doesn't leave ghost thumbs.
|
|
679
|
-
{
|
|
680
|
-
int16_t bx = __ui_nodes[i].box.x;
|
|
681
|
-
int16_t by = drawY;
|
|
682
|
-
int16_t bw = __ui_nodes[i].box.w;
|
|
683
|
-
int16_t bh = __ui_nodes[i].box.h;
|
|
684
|
-
UI_COLOR_T fgCol = __ui_nodes[i].fg;
|
|
685
|
-
UI_COLOR_T bgCol = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor;
|
|
686
|
-
UI_COLOR_T dimFg = (UI_COLOR_T)((fgCol >> 1) & UI_DIM_MASK);
|
|
687
|
-
|
|
688
|
-
int16_t trackY = by + bh / 2;
|
|
689
|
-
int16_t rMin = __ui_nodes[i].rangeMin;
|
|
690
|
-
int16_t rMax = __ui_nodes[i].rangeMax;
|
|
691
|
-
int16_t range = rMax - rMin;
|
|
692
|
-
if (range <= 0) range = 100;
|
|
693
|
-
int16_t pct = constrain(__ui_nodes[i].value, rMin, rMax) - rMin;
|
|
694
|
-
int16_t fillW = ((int32_t)(bw - 8) * pct) / range;
|
|
695
|
-
// lastTextWidth carries the previous fill width, or -1 if this node
|
|
696
|
-
// has never been drawn (fillW=0 at value=min is a valid thumb pos).
|
|
697
|
-
int16_t prevFillW = __ui_nodes[i].lastTextWidth;
|
|
698
|
-
int16_t newThumbX = bx + 4 + fillW - 3;
|
|
699
|
-
|
|
700
|
-
if (prevFillW < 0) {
|
|
701
|
-
// First draw: redraw the whole track + fill from scratch.
|
|
702
|
-
ui_display_draw_fast_hline(bx, trackY, bw, dimFg);
|
|
703
|
-
ui_display_draw_fast_hline(bx + 4, trackY, fillW, fgCol);
|
|
704
|
-
} else {
|
|
705
|
-
// Incremental: wipe the strip between the old and new thumb
|
|
706
|
-
// positions (whichever extends further on each side), then restore
|
|
707
|
-
// the track line. This is symmetric — old thumbs disappear whether
|
|
708
|
-
// the drag moves forward or backward.
|
|
709
|
-
int16_t prevThumbX = bx + 4 + prevFillW - 3;
|
|
710
|
-
int16_t left = prevThumbX < newThumbX ? prevThumbX : newThumbX;
|
|
711
|
-
int16_t right = prevThumbX + 6 > newThumbX + 6 ? prevThumbX + 6 : newThumbX + 6;
|
|
712
|
-
if (left < bx) left = bx;
|
|
713
|
-
if (right > bx + bw) right = bx + bw;
|
|
714
|
-
// Erase the thumb band (10px tall) to background.
|
|
715
|
-
ui_display_fill_rect(left, trackY - 5, right - left, 10, bgCol);
|
|
716
|
-
// Restore the track line over the wiped strip: bright up to the
|
|
717
|
-
// current fill end, dim beyond it.
|
|
718
|
-
int16_t fillEnd = bx + 4 + fillW;
|
|
719
|
-
if (right <= fillEnd) {
|
|
720
|
-
ui_display_draw_fast_hline(left, trackY, right - left, fgCol);
|
|
721
|
-
} else if (left >= fillEnd) {
|
|
722
|
-
ui_display_draw_fast_hline(left, trackY, right - left, dimFg);
|
|
723
|
-
} else {
|
|
724
|
-
ui_display_draw_fast_hline(left, trackY, fillEnd - left, fgCol);
|
|
725
|
-
ui_display_draw_fast_hline(fillEnd, trackY, right - fillEnd, dimFg);
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
// Thumb: small filled rectangle at the current position.
|
|
730
|
-
if (newThumbX < bx + 1) newThumbX = bx + 1;
|
|
731
|
-
if (newThumbX > bx + bw - 7) newThumbX = bx + bw - 7;
|
|
732
|
-
ui_display_fill_rect(newThumbX, trackY - 5, 6, 10, fgCol);
|
|
733
|
-
|
|
734
|
-
// Remember current fill width for the next incremental update.
|
|
735
|
-
__ui_nodes[i].lastTextWidth = fillW;
|
|
736
|
-
}
|
|
737
|
-
break;
|
|
738
|
-
case NODE_INPUT:
|
|
739
|
-
// Input field: bordered rect + current text (or placeholder), clipped to box width.
|
|
740
|
-
{
|
|
741
|
-
int16_t bx = __ui_nodes[i].box.x;
|
|
742
|
-
int16_t by = drawY;
|
|
743
|
-
int16_t bw = __ui_nodes[i].box.w;
|
|
744
|
-
int16_t bh = __ui_nodes[i].box.h;
|
|
745
|
-
UI_COLOR_T fgCol = __ui_nodes[i].fg;
|
|
746
|
-
UI_COLOR_T bgCol = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor;
|
|
747
|
-
if (__ui_nodes[i].borderRadius > 0) {
|
|
748
|
-
ui_display_fill_round_rect(bx, by, bw, bh, __ui_nodes[i].borderRadius, bgCol);
|
|
749
|
-
} else {
|
|
750
|
-
ui_display_fill_rect(bx, by, bw, bh, bgCol);
|
|
751
|
-
}
|
|
752
|
-
UI_COLOR_T inputBorder = __ui_nodes[i].borderColor ? __ui_nodes[i].borderColor : fgCol;
|
|
753
|
-
uint8_t inputBorderStyle = __ui_nodes[i].borderStyle ? __ui_nodes[i].borderStyle : 1;
|
|
754
|
-
uint8_t inputBorderWidth = __ui_nodes[i].borderWidth ? __ui_nodes[i].borderWidth : 1;
|
|
755
|
-
ui_draw_rect_outline(bx, by, bw, bh, __ui_nodes[i].borderRadius, inputBorderStyle, inputBorderWidth, inputBorder);
|
|
756
|
-
// Show typed text (textBuffer) in fg color, or placeholder (.text)
|
|
757
|
-
// dimmed gray when the buffer is empty.
|
|
758
|
-
UI_COLOR_T textCol = fgCol;
|
|
759
|
-
const char* disp = (__ui_nodes[i].textBuffer[0] != 0)
|
|
760
|
-
? __ui_nodes[i].textBuffer
|
|
761
|
-
: (__ui_nodes[i].text ? __ui_nodes[i].text : "");
|
|
762
|
-
#if defined(UI_HIDE_OSK)
|
|
763
|
-
// Desktop target: when this input is the active edit target, suppress
|
|
764
|
-
// the placeholder. The editing buffer is loaded from the (likely empty)
|
|
765
|
-
// textBuffer on focus, so without this the placeholder ("enter name")
|
|
766
|
-
// would render with the caret at its end. Hide it so the caret shows
|
|
767
|
-
// on a clean field at position 0 until the user types.
|
|
768
|
-
if (__ui_kb_visible && (int16_t)__ui_kb_target == (int16_t)i && __ui_nodes[i].textBuffer[0] == 0) {
|
|
769
|
-
disp = "";
|
|
770
|
-
}
|
|
771
|
-
#endif
|
|
772
|
-
if (__ui_nodes[i].textBuffer[0] == 0) {
|
|
773
|
-
#if UI_COLOR_DEPTH == 888
|
|
774
|
-
textCol = 0x848484;
|
|
775
|
-
#else
|
|
776
|
-
textCol = 0x8410;
|
|
777
|
-
#endif
|
|
778
|
-
}
|
|
779
|
-
// Note: the actual text draw is via ui_draw_text (which uses __ui_gfx).
|
|
780
|
-
// The setCursor/setTextColor/setTextSize below are legacy — ui_draw_text
|
|
781
|
-
// handles its own cursor/colors. Keep them on __ui_gfx for consistency
|
|
782
|
-
// (in case __ui_gfx is the scroll canvas, not __tc_display).
|
|
783
|
-
ui_display_set_cursor(bx + 4, by + (bh - ts * 8) / 2);
|
|
784
|
-
ui_display_set_text_color(textCol, bgCol);
|
|
785
|
-
ui_display_set_text_size(ts);
|
|
786
|
-
// Clip: at textSize ts, each char is ts*6px advance. Only print chars
|
|
787
|
-
// that fit within the box (bw - 8px margin), so text never overflows
|
|
788
|
-
// the border or wraps to the next line.
|
|
789
|
-
int16_t maxChars = (bw - 8) / (ts * 6);
|
|
790
|
-
if (maxChars < 0) maxChars = 0;
|
|
791
|
-
int16_t len = (int16_t)strlen(disp);
|
|
792
|
-
if (len > maxChars) len = maxChars;
|
|
793
|
-
if (len > UI_TEXT_BUF) len = UI_TEXT_BUF;
|
|
794
|
-
char clipped[UI_TEXT_BUF + 1];
|
|
795
|
-
for (int16_t c = 0; c < len; c++) {
|
|
796
|
-
clipped[c] = disp[c];
|
|
797
|
-
}
|
|
798
|
-
clipped[len] = 0;
|
|
799
|
-
ui_draw_text(clipped, bx + 4, by + (bh - ui_text_height(ts, __ui_nodes[i].fontFace)) / 2,
|
|
800
|
-
textCol, bgCol, ts, __ui_nodes[i].fontAntialias, __ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing);
|
|
801
|
-
#if defined(UI_HIDE_OSK)
|
|
802
|
-
// Desktop target: with no OSK grid there's no focus indicator. Draw a
|
|
803
|
-
// blinking caret at the end of the typed text on the active edit target
|
|
804
|
-
// so the user sees which field they're editing. Blink ~3×/sec via the
|
|
805
|
-
// top bits of __ui_kb_blink (mask 0x20 toggles every 32 ticks ≈ 530ms).
|
|
806
|
-
if (__ui_kb_visible && (int16_t)__ui_kb_target == (int16_t)i && (__ui_kb_blink & 0x20)) {
|
|
807
|
-
uint16_t caretW = ui_text_width(clipped, ts, __ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing);
|
|
808
|
-
int16_t caretX = bx + 4 + (int16_t)caretW;
|
|
809
|
-
int16_t caretYTop = by + (bh - ts * 8) / 2;
|
|
810
|
-
// fgCol (not textCol): the placeholder-dimming path sets textCol to
|
|
811
|
-
// gray, which would make the caret nearly invisible on a focused
|
|
812
|
-
// empty field. The caret should always be the input's foreground.
|
|
813
|
-
ui_display_fill_rect(caretX, caretYTop, (int16_t)(ts > 1 ? 2 : 1), (int16_t)(ts * 8), fgCol);
|
|
814
|
-
}
|
|
815
|
-
#endif
|
|
816
|
-
}
|
|
817
|
-
break;
|
|
818
|
-
case NODE_IMG:
|
|
819
|
-
{
|
|
820
|
-
UI_COLOR_T imgBg = __ui_nodes[i].hasBg ? fillBg : __ui_nodes[i].clearColor;
|
|
821
|
-
int16_t fillW = ui_rotated_face_w(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
822
|
-
int16_t fillH = ui_rotated_face_h(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
823
|
-
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, fillW, fillH, imgBg);
|
|
824
|
-
}
|
|
825
|
-
if (__ui_nodes[i].imgDataId < __ui_image_count) {
|
|
826
|
-
const UIImage* img = &__ui_images[__ui_nodes[i].imgDataId];
|
|
827
|
-
int16_t targetW = __ui_nodes[i].box.w;
|
|
828
|
-
int16_t targetH = __ui_nodes[i].box.h;
|
|
829
|
-
ui_draw_image_with_fit(img, __ui_nodes[i].box.x, drawY, __ui_nodes[i].rotateDeg, __ui_nodes[i].objectFit, targetW, targetH);
|
|
830
|
-
}
|
|
831
|
-
if (__ui_nodes[i].borderStyle != 0) {
|
|
832
|
-
int16_t borderW = ui_rotated_face_w(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
833
|
-
int16_t borderH = ui_rotated_face_h(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
834
|
-
ui_draw_rect_outline(__ui_nodes[i].box.x, drawY, borderW, borderH,
|
|
835
|
-
__ui_nodes[i].borderRadius, __ui_nodes[i].borderStyle, __ui_nodes[i].borderWidth, bColor);
|
|
836
|
-
}
|
|
837
|
-
break;
|
|
838
|
-
case NODE_CANVAS: {
|
|
839
|
-
// Find this node's draw callback.
|
|
840
|
-
void (*__ui_canvas_fn)(CuttlefishCanvas16*) = nullptr;
|
|
841
|
-
for (uint16_t b = 0; b < __ui_canvas_binding_count; b++) {
|
|
842
|
-
if (__ui_canvas_bindings[b].node == i) { __ui_canvas_fn = __ui_canvas_bindings[b].fn; break; }
|
|
843
|
-
}
|
|
844
|
-
if (__ui_canvas_fn) {
|
|
845
|
-
int16_t __ui_cw = __ui_nodes[i].canvasW;
|
|
846
|
-
int16_t __ui_ch = __ui_nodes[i].canvasH;
|
|
847
|
-
if (__ui_cw > 0 && __ui_ch > 0) {
|
|
848
|
-
uint8_t __ui_canvas_drawn = 0;
|
|
849
|
-
if (ui_display_is_default_target()) {
|
|
850
|
-
// Cache a canvas sized to the buffer for direct hardware draws so
|
|
851
|
-
// the callback's many primitives push as one bitmap. When the
|
|
852
|
-
// active target is already a memory canvas (scroll/repair/full
|
|
853
|
-
// framebuffer), skip the nested allocation and draw directly below.
|
|
854
|
-
// A canvas can construct but fail its internal pixel-buffer malloc
|
|
855
|
-
// (non-null canvas, null buffer). Treat that as "no canvas" so a
|
|
856
|
-
// transient malloc failure self-heals instead of blanking the node.
|
|
857
|
-
if (!__ui_node_canvas || !display_canvasBuffer(__ui_node_canvas) ||
|
|
858
|
-
display_canvasWidth(__ui_node_canvas) != __ui_cw || display_canvasHeight(__ui_node_canvas) != __ui_ch) {
|
|
859
|
-
display_deleteCanvas(__ui_node_canvas);
|
|
860
|
-
__ui_node_canvas = display_createCanvas(__ui_cw, __ui_ch);
|
|
861
|
-
}
|
|
862
|
-
CuttlefishCanvas16* __ui_lc = __ui_node_canvas;
|
|
863
|
-
if (__ui_lc && display_canvasBuffer(__ui_lc)) {
|
|
864
|
-
display_canvasFillScreen(__ui_lc, __ui_nodes[i].clearColor);
|
|
865
|
-
CuttlefishDisplayTarget* __ui_prev_target = ui_display_get_target();
|
|
866
|
-
ui_display_set_target((CuttlefishDisplayTarget*)__ui_lc);
|
|
867
|
-
__ui_canvas_fn(__ui_lc);
|
|
868
|
-
ui_display_set_target(__ui_prev_target);
|
|
869
|
-
ui_draw_canvas_rect(__ui_lc, __ui_nodes[i].box.x, drawY, __ui_cw, __ui_ch);
|
|
870
|
-
__ui_canvas_drawn = 1;
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
if (!__ui_canvas_drawn) {
|
|
874
|
-
UI_COLOR_T __ui_canvas_bg = __ui_nodes[i].hasBg ? fillBg : __ui_nodes[i].clearColor;
|
|
875
|
-
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, __ui_nodes[i].box.w, __ui_nodes[i].box.h, __ui_canvas_bg);
|
|
876
|
-
int16_t __ui_prev_off_x = __ui_draw_off_x;
|
|
877
|
-
int16_t __ui_prev_off_y = __ui_draw_off_y;
|
|
878
|
-
int16_t __ui_prev_canvas_w = __ui_canvas_fallback_w;
|
|
879
|
-
int16_t __ui_prev_canvas_h = __ui_canvas_fallback_h;
|
|
880
|
-
__ui_canvas_fallback_w = __ui_cw;
|
|
881
|
-
__ui_canvas_fallback_h = __ui_ch;
|
|
882
|
-
__ui_draw_off_x = (int16_t)(__ui_prev_off_x + __ui_nodes[i].box.x);
|
|
883
|
-
__ui_draw_off_y = (int16_t)(__ui_prev_off_y + drawY);
|
|
884
|
-
__ui_canvas_fn(nullptr);
|
|
885
|
-
__ui_draw_off_x = __ui_prev_off_x;
|
|
886
|
-
__ui_draw_off_y = __ui_prev_off_y;
|
|
887
|
-
__ui_canvas_fallback_w = __ui_prev_canvas_w;
|
|
888
|
-
__ui_canvas_fallback_h = __ui_prev_canvas_h;
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
break;
|
|
893
|
-
}
|
|
894
|
-
case NODE_LIST: {
|
|
895
|
-
// Virtualized list: state lives on the node now (listCountFn/listItemFn/
|
|
896
|
-
// listCount/scrollY/contentHeight/listItemHeight), not in a side table.
|
|
897
|
-
if (!__ui_nodes[i].listItemFn) break;
|
|
898
|
-
int16_t bx = __ui_nodes[i].box.x;
|
|
899
|
-
int16_t by = drawY;
|
|
900
|
-
int16_t bw = __ui_nodes[i].box.w;
|
|
901
|
-
int16_t bh = __ui_nodes[i].box.h;
|
|
902
|
-
uint16_t ih = __ui_nodes[i].listItemHeight > 0 ? __ui_nodes[i].listItemHeight : 24;
|
|
903
|
-
uint16_t itemCount = __ui_nodes[i].listCount;
|
|
904
|
-
int16_t listScrollY = __ui_nodes[i].scrollY;
|
|
905
|
-
int16_t listContentH = __ui_nodes[i].contentHeight;
|
|
906
|
-
UI_COLOR_T clearCol = __ui_nodes[i].clearColor;
|
|
907
|
-
uint8_t listFullRepaint = 0;
|
|
908
|
-
// Render to a viewport-sized canvas so edge glyphs are naturally clipped.
|
|
909
|
-
// Treat a null buffer (failed internal malloc) as "no canvas" and retry —
|
|
910
|
-
// see __ui_node_canvas for the zombie-caching rationale.
|
|
911
|
-
// __ui_list_canvas is file-scoped so ui_release_canvas_state() can free it.
|
|
912
|
-
if (!__ui_list_canvas || !display_canvasBuffer(__ui_list_canvas) ||
|
|
913
|
-
display_canvasWidth(__ui_list_canvas) != bw || display_canvasHeight(__ui_list_canvas) != bh) {
|
|
914
|
-
display_deleteCanvas(__ui_list_canvas);
|
|
915
|
-
__ui_list_canvas_node = -1;
|
|
916
|
-
__ui_list_canvas = display_createCanvas(bw, bh);
|
|
917
|
-
listFullRepaint = 1;
|
|
918
|
-
}
|
|
919
|
-
CuttlefishCanvas16* lc = __ui_list_canvas;
|
|
920
|
-
if (!lc || !display_canvasBuffer(lc)) {
|
|
921
|
-
break;
|
|
922
|
-
}
|
|
923
|
-
if (__ui_list_canvas_node != (int16_t)i) listFullRepaint = 1;
|
|
924
|
-
int16_t repaintY = 0;
|
|
925
|
-
int16_t repaintH = bh;
|
|
926
|
-
int16_t deltaY = listScrollY - __ui_nodes[i].lastPaintedScrollY;
|
|
927
|
-
int16_t absDelta = deltaY < 0 ? -deltaY : deltaY;
|
|
928
|
-
uint8_t canShiftList = (!listFullRepaint && deltaY != 0 && absDelta < bh);
|
|
929
|
-
if (canShiftList) {
|
|
930
|
-
ui_shift_container_canvas(lc, deltaY, clearCol, &repaintY, &repaintH);
|
|
931
|
-
} else {
|
|
932
|
-
display_canvasFillScreen(lc, clearCol);
|
|
933
|
-
repaintY = 0;
|
|
934
|
-
repaintH = bh;
|
|
935
|
-
}
|
|
936
|
-
// GFXcanvas text has no clipping. For shift-and-repair frames, draw row
|
|
937
|
-
// text into a strip-sized repair canvas first, then blit only that strip
|
|
938
|
-
// into the shifted list canvas. This prevents a 1-5px repair from
|
|
939
|
-
// repainting full glyphs across pixels that were already shifted.
|
|
940
|
-
CuttlefishCanvas16* listTextCanvas = lc;
|
|
941
|
-
int16_t listTextOffsetY = 0;
|
|
942
|
-
uint8_t drawingListRepair = 0;
|
|
943
|
-
if (canShiftList && repaintH > 0 && repaintH < bh) {
|
|
944
|
-
CuttlefishCanvas16* rc = ui_get_repair_canvas(bw, repaintH);
|
|
945
|
-
if (rc) {
|
|
946
|
-
display_canvasFillScreen(rc, clearCol);
|
|
947
|
-
listTextCanvas = rc;
|
|
948
|
-
listTextOffsetY = repaintY;
|
|
949
|
-
drawingListRepair = 1;
|
|
950
|
-
} else {
|
|
951
|
-
display_canvasFillScreen(lc, clearCol);
|
|
952
|
-
repaintY = 0;
|
|
953
|
-
repaintH = bh;
|
|
954
|
-
canShiftList = 0;
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
// Compute visible range for the repainted strip. Previously-rendered
|
|
958
|
-
// pixels are shifted in-place; only the exposed band needs new rows.
|
|
959
|
-
uint16_t first = (listScrollY + repaintY) / ih;
|
|
960
|
-
uint16_t last = (listScrollY + repaintY + repaintH - 1) / ih + 1;
|
|
961
|
-
if (itemCount > 0 && last >= itemCount) last = itemCount - 1;
|
|
962
|
-
// Draw each visible item (canvas-local coords: 0,0 = viewport top).
|
|
963
|
-
char listBuf[UI_TEXT_BUF + 1];
|
|
964
|
-
display_targetSetTextWrap((CuttlefishDisplayTarget*)listTextCanvas, false);
|
|
965
|
-
if (itemCount > 0 && repaintH > 0) {
|
|
966
|
-
for (uint16_t idx = first; idx <= last; idx++) {
|
|
967
|
-
int16_t itemY = (int16_t)(idx * ih) - listScrollY - listTextOffsetY;
|
|
968
|
-
__ui_nodes[i].listItemFn(idx, listBuf, UI_TEXT_BUF + 1);
|
|
969
|
-
listBuf[UI_TEXT_BUF] = 0;
|
|
970
|
-
display_targetSetCursor((CuttlefishDisplayTarget*)listTextCanvas, 4, itemY + (ih - 16) / 2);
|
|
971
|
-
display_targetSetTextColor((CuttlefishDisplayTarget*)listTextCanvas, __ui_nodes[i].fg);
|
|
972
|
-
display_targetSetTextSize((CuttlefishDisplayTarget*)listTextCanvas, 2);
|
|
973
|
-
display_targetPrint((CuttlefishDisplayTarget*)listTextCanvas, listBuf);
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
if (drawingListRepair) {
|
|
977
|
-
CuttlefishDisplayTarget* prevTarget = ui_display_get_target();
|
|
978
|
-
ui_display_set_target((CuttlefishDisplayTarget*)lc);
|
|
979
|
-
ui_draw_canvas_rect(listTextCanvas, 0, repaintY, bw, repaintH);
|
|
980
|
-
ui_display_set_target(prevTarget);
|
|
981
|
-
}
|
|
982
|
-
// Scrollbar (canvas-local coords).
|
|
983
|
-
if (listContentH > bh) {
|
|
984
|
-
int16_t tx = bw - 4;
|
|
985
|
-
uint16_t thumbH = (uint32_t)bh * bh / listContentH;
|
|
986
|
-
if (thumbH < 8) thumbH = 8;
|
|
987
|
-
int16_t maxScroll = listContentH - bh;
|
|
988
|
-
uint16_t thumbY = maxScroll > 0 ? (uint32_t)(bh - thumbH) * listScrollY / maxScroll : 0;
|
|
989
|
-
UI_COLOR_T dimFg = (UI_COLOR_T)((__ui_nodes[i].fg >> 1) & UI_DIM_MASK);
|
|
990
|
-
display_canvasFillRect(lc, tx, 0, 3, bh, dimFg);
|
|
991
|
-
display_canvasFillRect(lc, tx, thumbY, 3, thumbH, __ui_nodes[i].fg);
|
|
992
|
-
}
|
|
993
|
-
// Outset shadows are static decoration. Redrawing the hard shadow
|
|
994
|
-
// directly to the panel before every small scroll-frame creates a
|
|
995
|
-
// visible shadow-then-content intermediate state on SPI TFTs. Keep it
|
|
996
|
-
// for full list repaints, but skip it for shift-and-repair scrolls.
|
|
997
|
-
if (!drawingBufferedScroll && !__ui_fb && !canShiftList) {
|
|
998
|
-
ui_draw_shadow(i, by, 0);
|
|
999
|
-
}
|
|
1000
|
-
// Standalone lists push directly. Lists inside a buffered scroll
|
|
1001
|
-
// container must composite into that scroll canvas; their box has
|
|
1002
|
-
// already been translated to canvas-local coordinates.
|
|
1003
|
-
if (drawingBufferedScroll) {
|
|
1004
|
-
ui_draw_canvas_rect(lc, bx, by, bw, bh);
|
|
1005
|
-
} else {
|
|
1006
|
-
ui_push_canvas_rect(lc, bx, by, bw, bh);
|
|
1007
|
-
}
|
|
1008
|
-
// Draw static decoration after the scrollable pixels are composited.
|
|
1009
|
-
// Keeping border rows out of __ui_list_canvas prevents the cached
|
|
1010
|
-
// shift step from dragging top/bottom border pixels through the list.
|
|
1011
|
-
ui_draw_shadow(i, by, 1);
|
|
1012
|
-
if (__ui_nodes[i].borderStyle != 0) {
|
|
1013
|
-
ui_draw_node_border(i, bx, by, bColor);
|
|
1014
|
-
}
|
|
1015
|
-
ui_draw_node_outline(i, bx, by);
|
|
1016
|
-
__ui_list_canvas_node = (int16_t)i;
|
|
1017
|
-
__ui_nodes[i].lastPaintedScrollY = listScrollY;
|
|
1018
|
-
__ui_nodes[i].dirty = 0;
|
|
1019
|
-
ui_display_set_target(__ui_draw_target);
|
|
1020
|
-
__ui_nodes[i].box.x = origBoxX;
|
|
1021
|
-
__ui_nodes[i].box.y = origBoxY;
|
|
1022
|
-
continue; // list handled canvas push, decoration, and coordinate restore
|
|
1023
|
-
}
|
|
586
|
+
// ── Per-kind draw dispatch ─────────────────────────────────────────────
|
|
587
|
+
// The kind switch lives in ui_draw_node_body (node-draw-body slice) so the
|
|
588
|
+
// strip/band renderer can reuse it. The ctx carries the draw state both
|
|
589
|
+
// paths share; the list case may return 1 (it handled canvas push,
|
|
590
|
+
// decoration, coord restore, and dirty-clear itself) in which case this
|
|
591
|
+
// node is done — skip the post-switch epilogue exactly as the old inline
|
|
592
|
+
// 'continue;' did.
|
|
593
|
+
UINodeDrawCtx __ui_ctx;
|
|
594
|
+
__ui_ctx.drawY = drawY;
|
|
595
|
+
__ui_ctx.bColor = bColor;
|
|
596
|
+
__ui_ctx.fillBg = fillBg;
|
|
597
|
+
__ui_ctx.ts = ts;
|
|
598
|
+
__ui_ctx.textMaxW = textMaxW;
|
|
599
|
+
__ui_ctx.tw = tw;
|
|
600
|
+
__ui_ctx.th = th;
|
|
601
|
+
__ui_ctx.paintTextW = paintTextW;
|
|
602
|
+
__ui_ctx.paintTextH = paintTextH;
|
|
603
|
+
__ui_ctx.displayText = displayText;
|
|
604
|
+
__ui_ctx.drawingBufferedScroll = drawingBufferedScroll;
|
|
605
|
+
__ui_ctx.drawTarget = __ui_draw_target;
|
|
606
|
+
__ui_ctx.origBoxX = origBoxX;
|
|
607
|
+
__ui_ctx.origBoxY = origBoxY;
|
|
608
|
+
if (ui_draw_node_body(i, &__ui_ctx)) {
|
|
609
|
+
continue;
|
|
1024
610
|
}
|
|
1025
611
|
if (__ui_nodes[i].kind == NODE_FILL && ui_rotation_quadrant(__ui_nodes[i].rotateDeg) != 0 &&
|
|
1026
612
|
__ui_nodes[i].outlineStyle != 0 && __ui_nodes[i].outlineWidth > 0) {
|
|
@@ -1047,5 +633,6 @@ export function emitTickDirtyDrawPhase(): string {
|
|
|
1047
633
|
// Report this node's bounding box to the deferred-refresh accumulator. Phase
|
|
1048
634
|
// 4 uses the box as the dirty rect (a safe over-estimate); Phase 5 tightens
|
|
1049
635
|
// to the actual paint rect. No-op on TFT (compiles to nothing).
|
|
636
|
+
if (__ui_fb) ui_fb_add_rect(paintRect.x, paintRect.y, paintRect.w, paintRect.h);
|
|
1050
637
|
ui_refresh_add_rect(__ui_nodes[i].box.x, __ui_nodes[i].box.y, __ui_nodes[i].box.w, __ui_nodes[i].box.h);`;
|
|
1051
638
|
}
|