@typecad/ui 1.0.0-alpha.13 → 1.0.0-alpha.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1457 -1469
- package/dist/cli.js +22 -19
- package/dist/preview/host-ui-runtime.js +0 -1
- package/dist/ui-engine/model.js +7 -1
- package/dist/ui-engine/runtime-header/canvas-scrollbar.js +115 -115
- package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +182 -182
- package/dist/ui-engine/runtime-header/forward-decls.js +227 -216
- package/dist/ui-engine/runtime-header/init-press-input.js +138 -138
- package/dist/ui-engine/runtime-header/keyboard.js +676 -676
- package/dist/ui-engine/runtime-header/node-draw-body.js +1658 -1656
- package/dist/ui-engine/runtime-header/paint-order-coords.js +259 -259
- package/dist/ui-engine/runtime-header/paint-rects-repair.js +736 -736
- package/dist/ui-engine/runtime-header/scroll-physics.js +4 -4
- package/dist/ui-engine/runtime-header/structs.js +228 -228
- package/dist/ui-engine/runtime-header/text-rendering.js +888 -888
- package/dist/ui-engine/runtime-header/tick/bindings-phase.js +118 -118
- package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +896 -896
- package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +24 -24
- package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +11 -11
- package/dist/ui-engine/runtime-header/types-defines.js +110 -110
- package/dist/ui-engine/shadcn-kit.d.ts +1 -1
- package/dist/ui-engine/shadcn-kit.js +4 -0
- package/dist/ui-engine/ui-lowering.d.ts +1 -1
- package/dist/ui-engine/ui-lowering.js +14 -5
- package/dist/ui-engine/ui-registry.js +9 -1
- package/dist/wizard/display-catalog.d.ts +0 -3
- package/dist/wizard/display-catalog.js +3 -12
- package/dist/wizard/index.d.ts +2 -0
- package/dist/wizard/index.js +1 -0
- package/dist/wizard/integration-wizard.d.ts +3 -0
- package/dist/wizard/integration-wizard.js +69 -20
- package/dist/wizard/package-writer.d.ts +20 -0
- package/dist/wizard/package-writer.js +79 -0
- package/package.json +4 -4
- package/src/cli.ts +90 -87
- package/src/preview/host-ui-runtime.ts +0 -1
- package/src/ui-engine/model.ts +6 -1
- package/src/ui-engine/runtime-header/canvas-scrollbar.ts +121 -121
- package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +188 -188
- package/src/ui-engine/runtime-header/forward-decls.ts +238 -227
- package/src/ui-engine/runtime-header/init-press-input.ts +144 -144
- package/src/ui-engine/runtime-header/keyboard.ts +689 -689
- package/src/ui-engine/runtime-header/node-draw-body.ts +1683 -1681
- package/src/ui-engine/runtime-header/paint-order-coords.ts +265 -265
- package/src/ui-engine/runtime-header/paint-rects-repair.ts +742 -742
- package/src/ui-engine/runtime-header/scroll-physics.ts +4 -4
- package/src/ui-engine/runtime-header/structs.ts +234 -234
- package/src/ui-engine/runtime-header/text-rendering.ts +894 -894
- package/src/ui-engine/runtime-header/tick/bindings-phase.ts +124 -124
- package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +902 -902
- package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +30 -30
- package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +11 -11
- package/src/ui-engine/runtime-header/types-defines.ts +116 -116
- package/src/ui-engine/shadcn-kit.ts +4 -0
- package/src/ui-engine/ui-lowering.ts +12 -4
- package/src/ui-engine/ui-registry.ts +9 -1
- package/src/wizard/display-catalog.ts +261 -273
- package/src/wizard/index.ts +46 -38
- package/src/wizard/integration-wizard.ts +679 -619
- package/src/wizard/package-writer.ts +98 -0
|
@@ -1,1681 +1,1683 @@
|
|
|
1
|
-
// Slice of the C++ runtime header.
|
|
2
|
-
// The per-node kind switch, extracted into a callable function so the
|
|
3
|
-
// strip/band renderer (ui_render_scroll_bands) can compose scroll-subtree
|
|
4
|
-
// nodes into a band canvas without duplicating ~700 lines of kind-specific
|
|
5
|
-
// draw code.
|
|
6
|
-
//
|
|
7
|
-
// The caller fills a UINodeDrawCtx with the computed draw state (drawY, text
|
|
8
|
-
// metrics, colors, text source) and invokes ui_draw_node_body. Both the main
|
|
9
|
-
// dirty-node draw loop (dirty-draw-phase.ts) and the band renderer set the ctx
|
|
10
|
-
// up the same way; the difference is only what __ui_gfx points at (display /
|
|
11
|
-
// framebuffer / scroll canvas / band canvas) and how the node's box coords are
|
|
12
|
-
// translated, both of which the caller handles before calling this.
|
|
13
|
-
//
|
|
14
|
-
// The NODE_LIST case is special: it manages its own canvas push, static
|
|
15
|
-
// decoration, coordinate restore, dirty-clear, and then returns 1 (a sentinel
|
|
16
|
-
// meaning "I fully handled this node — skip the caller's post-switch
|
|
17
|
-
// epilogue"). Every other case returns 0 and the caller runs its epilogue
|
|
18
|
-
// (outline, paint-canvas push, coordinate restore, dirty-clear, refresh rect).
|
|
19
|
-
// This return-sentinel preserves the original 'continue;' the list case used
|
|
20
|
-
// to short-circuit the rest of the per-node loop body.
|
|
21
|
-
//
|
|
22
|
-
// See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
|
|
23
|
-
export function emitNodeDrawBody(): string {
|
|
24
|
-
return `
|
|
25
|
-
// Computed draw state for one node, passed into ui_draw_node_body so it can be
|
|
26
|
-
// called from both the main dirty-node loop and the strip/band renderer.
|
|
27
|
-
struct UINodeDrawCtx {
|
|
28
|
-
int16_t drawY; // node's screen-space draw Y (scroll-adjusted)
|
|
29
|
-
UI_COLOR_T bColor; // border color (borderColor or fg, opacity-blended)
|
|
30
|
-
UI_COLOR_T fillBg; // fill background (bg, opacity-blended toward parent)
|
|
31
|
-
uint8_t ts; // text size
|
|
32
|
-
uint16_t textMaxW; // max text width (node box content width)
|
|
33
|
-
uint16_t tw; // laid-out text width
|
|
34
|
-
uint16_t th; // laid-out text height
|
|
35
|
-
uint16_t paintTextW; // paint-rect text width (tw + insets)
|
|
36
|
-
uint16_t paintTextH; // paint-rect text height (th + insets)
|
|
37
|
-
const char* displayText; // text source (binding buffer or flash literal)
|
|
38
|
-
// Call-site draw state the NODE_LIST case reads. These mirror the main draw
|
|
39
|
-
// loop's locals; the band renderer sets them to its own (target = band canvas,
|
|
40
|
-
// drawingBufferedScroll = 0, origBox = the node's untranslated box).
|
|
41
|
-
uint8_t drawingBufferedScroll; // 1 when drawing into the buffered scroll canvas
|
|
42
|
-
CuttlefishDisplayTarget* drawTarget; // __ui_draw_target (framebuffer or display)
|
|
43
|
-
int16_t origBoxX; // node's untranslated box.x (restored after draw)
|
|
44
|
-
int16_t origBoxY; // node's untranslated box.y (restored after draw)
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
// Returns 1 when the node fully handled its own canvas push, decoration,
|
|
48
|
-
// coordinate restore, and dirty-clear (NODE_LIST). Returns 0 otherwise, in
|
|
49
|
-
// which case the caller is responsible for the post-switch epilogue.
|
|
50
|
-
//
|
|
51
|
-
// The ctx parameter is typed const void* (cast back to UINodeDrawCtx* below):
|
|
52
|
-
// the
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
// ("'UINodeDrawCtx' does not name a type"). Primitive-only parameters keep
|
|
56
|
-
// the auto-generated prototype valid; call sites still pass &ctx, which
|
|
57
|
-
// converts implicitly to const void*.
|
|
58
|
-
static inline uint8_t ui_draw_node_body(int16_t i, const void* rawCtx) {
|
|
59
|
-
const UINodeDrawCtx* ctx = static_cast<const UINodeDrawCtx*>(rawCtx);
|
|
60
|
-
int16_t drawY = ctx->drawY;
|
|
61
|
-
UI_COLOR_T bColor = ctx->bColor;
|
|
62
|
-
UI_COLOR_T fillBg = ctx->fillBg;
|
|
63
|
-
uint8_t ts = ctx->ts;
|
|
64
|
-
uint16_t textMaxW = ctx->textMaxW;
|
|
65
|
-
uint16_t tw = ctx->tw;
|
|
66
|
-
uint16_t th = ctx->th;
|
|
67
|
-
uint16_t paintTextW = ctx->paintTextW;
|
|
68
|
-
uint16_t paintTextH = ctx->paintTextH;
|
|
69
|
-
const char* displayText = ctx->displayText;
|
|
70
|
-
uint8_t drawingBufferedScroll = ctx->drawingBufferedScroll;
|
|
71
|
-
CuttlefishDisplayTarget* __ui_draw_target = ctx->drawTarget;
|
|
72
|
-
int16_t origBoxX = ctx->origBoxX;
|
|
73
|
-
int16_t origBoxY = ctx->origBoxY;
|
|
74
|
-
switch (__ui_nodes[i].kind) {
|
|
75
|
-
case NODE_FILL:
|
|
76
|
-
if (__ui_nodes[i].gradientEnabled > 0) {
|
|
77
|
-
ui_draw_gradient_fill(i, drawY);
|
|
78
|
-
} else if (__ui_nodes[i].borderRadius > 0 && __ui_nodes[i].hasBg) {
|
|
79
|
-
int16_t fillW = ui_rotated_face_w(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
80
|
-
int16_t fillH = ui_rotated_face_h(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
81
|
-
ui_display_fill_round_rect(__ui_nodes[i].box.x, drawY, fillW, fillH, __ui_nodes[i].borderRadius, fillBg);
|
|
82
|
-
} else if (__ui_nodes[i].hasBg) {
|
|
83
|
-
int16_t fillW = ui_rotated_face_w(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
84
|
-
int16_t fillH = ui_rotated_face_h(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
85
|
-
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, fillW, fillH, fillBg);
|
|
86
|
-
}
|
|
87
|
-
ui_draw_shadow(i, drawY, 1);
|
|
88
|
-
if (__ui_nodes[i].borderStyle != 0) {
|
|
89
|
-
UI_COLOR_T bColor = __ui_nodes[i].borderColor ? __ui_nodes[i].borderColor : __ui_nodes[i].fg;
|
|
90
|
-
int16_t borderW = ui_rotated_face_w(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
91
|
-
int16_t borderH = ui_rotated_face_h(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
92
|
-
ui_draw_rect_outline(__ui_nodes[i].box.x, drawY, borderW, borderH,
|
|
93
|
-
__ui_nodes[i].borderRadius, __ui_nodes[i].borderStyle, __ui_nodes[i].borderWidth, bColor);
|
|
94
|
-
}
|
|
95
|
-
break;
|
|
96
|
-
case NODE_TEXT:
|
|
97
|
-
{
|
|
98
|
-
int16_t insetL = static_cast<int16_t>(__ui_nodes[i].borderWidth) + static_cast<int16_t>(__ui_nodes[i].paddingLeft);
|
|
99
|
-
int16_t insetR = static_cast<int16_t>(__ui_nodes[i].borderWidth) + static_cast<int16_t>(__ui_nodes[i].paddingRight);
|
|
100
|
-
int16_t insetT = static_cast<int16_t>(__ui_nodes[i].borderWidth) + static_cast<int16_t>(__ui_nodes[i].paddingTop);
|
|
101
|
-
int16_t textX = __ui_nodes[i].box.x + insetL;
|
|
102
|
-
int16_t textY = drawY + insetT;
|
|
103
|
-
int16_t textW = static_cast<int16_t>(__ui_nodes[i].box.w) - insetL - insetR;
|
|
104
|
-
if (textW < 1) textW = 1;
|
|
105
|
-
uint8_t textBoxPainted = 0;
|
|
106
|
-
if (__ui_nodes[i].gradientEnabled > 0) {
|
|
107
|
-
ui_draw_gradient_fill(i, drawY);
|
|
108
|
-
textBoxPainted = 1;
|
|
109
|
-
} else if (__ui_nodes[i].borderRadius > 0 && __ui_nodes[i].hasBg) {
|
|
110
|
-
ui_display_fill_round_rect(__ui_nodes[i].box.x, drawY,
|
|
111
|
-
__ui_nodes[i].box.w, __ui_nodes[i].box.h, __ui_nodes[i].borderRadius, fillBg);
|
|
112
|
-
textBoxPainted = 1;
|
|
113
|
-
} else if (__ui_nodes[i].hasBg) {
|
|
114
|
-
ui_display_fill_rect(__ui_nodes[i].box.x, drawY,
|
|
115
|
-
__ui_nodes[i].box.w, __ui_nodes[i].box.h, fillBg);
|
|
116
|
-
textBoxPainted = 1;
|
|
117
|
-
}
|
|
118
|
-
{
|
|
119
|
-
uint16_t clearW = __ui_nodes[i].box.w;
|
|
120
|
-
int16_t paintedTextW = static_cast<int16_t>(__ui_nodes[i].lastTextWidth) + insetL + insetR;
|
|
121
|
-
if (__ui_nodes[i].lastTextWidth > 0 && paintedTextW > static_cast<int16_t>(clearW)) {
|
|
122
|
-
clearW = static_cast<uint16_t>(paintedTextW);
|
|
123
|
-
}
|
|
124
|
-
uint16_t paddedTw = static_cast<uint16_t>(static_cast<int16_t>(tw) + insetL + insetR);
|
|
125
|
-
if (paddedTw > clearW) clearW = paddedTw;
|
|
126
|
-
// overflow:hidden/scroll: never clear past the node's own box. A nowrap
|
|
127
|
-
// line wider than its box would otherwise erase the parent's border.
|
|
128
|
-
if (__ui_nodes[i].scrollable) clearW = __ui_nodes[i].box.w;
|
|
129
|
-
uint16_t clearH = __ui_nodes[i].box.h;
|
|
130
|
-
int16_t paintedTextH = static_cast<int16_t>(__ui_nodes[i].lastTextHeight) + insetT + static_cast<int16_t>(__ui_nodes[i].paddingBottom) + static_cast<int16_t>(__ui_nodes[i].borderWidth);
|
|
131
|
-
if (__ui_nodes[i].lastTextHeight > 0 && paintedTextH > static_cast<int16_t>(clearH)) {
|
|
132
|
-
clearH = static_cast<uint16_t>(paintedTextH);
|
|
133
|
-
}
|
|
134
|
-
uint16_t paddedTh = static_cast<uint16_t>(static_cast<int16_t>(th) + insetT + static_cast<int16_t>(__ui_nodes[i].paddingBottom) + static_cast<int16_t>(__ui_nodes[i].borderWidth));
|
|
135
|
-
if (paddedTh > clearH) clearH = paddedTh;
|
|
136
|
-
// Dynamic transparent text still needs a clear, otherwise old glyph
|
|
137
|
-
// pixels accumulate when only this text node is dirty.
|
|
138
|
-
UI_COLOR_T clearCol = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor;
|
|
139
|
-
// Blend the clear toward the backdrop by opacity so a translucent text
|
|
140
|
-
// node (inherited from an opacity:<1 parent) doesn't repaint a solid
|
|
141
|
-
// block of its parent's fill around the glyphs.
|
|
142
|
-
if (__ui_nodes[i].opacity < 100) {
|
|
143
|
-
clearCol = ui_blend(clearCol, ui_parent_clear_color(i), __ui_nodes[i].opacity);
|
|
144
|
-
}
|
|
145
|
-
if (!textBoxPainted) {
|
|
146
|
-
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, clearW, clearH, clearCol);
|
|
147
|
-
}
|
|
148
|
-
__ui_nodes[i].lastTextWidth = tw;
|
|
149
|
-
__ui_nodes[i].lastTextHeight = th;
|
|
150
|
-
}
|
|
151
|
-
ui_draw_shadow(i, drawY, 1);
|
|
152
|
-
if (__ui_nodes[i].borderStyle != 0) {
|
|
153
|
-
ui_draw_node_border(i, __ui_nodes[i].box.x, drawY, bColor);
|
|
154
|
-
}
|
|
155
|
-
// Rich-text (inline runs): draw from precomputed geometry instead of the
|
|
156
|
-
// single-string wrapped path. Geometry is baked at transpile time; the
|
|
157
|
-
// runtime does not re-wrap.
|
|
158
|
-
if (__ui_nodes[i].runCount > 0) {
|
|
159
|
-
UI_COLOR_T richTextBg;
|
|
160
|
-
if (__ui_nodes[i].opacity < 100) {
|
|
161
|
-
uint16_t p = __ui_nodes[i].parent;
|
|
162
|
-
UI_COLOR_T source = (p != UI_NO_PARENT && __ui_nodes[p].hasBg) ? __ui_nodes[p].bg
|
|
163
|
-
: (__ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor);
|
|
164
|
-
richTextBg = ui_blend(source, __ui_nodes[i].clearColor, __ui_nodes[i].opacity);
|
|
165
|
-
} else {
|
|
166
|
-
richTextBg = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : ui_parent_clear_color(i);
|
|
167
|
-
}
|
|
168
|
-
if (__ui_nodes[i].textShadowCount > 0) {
|
|
169
|
-
UI_COLOR_T tsClear = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor;
|
|
170
|
-
uint32_t tsCol = ui_blend(__ui_nodes[i].textShadowColor, tsClear, __ui_nodes[i].textShadowAlpha);
|
|
171
|
-
// Shadow pass: draw the rich block in the shadow color at the offset.
|
|
172
|
-
// (Per-segment shadow color is approximated by drawing the whole
|
|
173
|
-
// block once in tsCol.)
|
|
174
|
-
ui_draw_rich_text(i,
|
|
175
|
-
textX + __ui_nodes[i].textShadowOffsetX,
|
|
176
|
-
textY + __ui_nodes[i].textShadowOffsetY,
|
|
177
|
-
tsClear, __ui_nodes[i].fontAntialias, 1, tsCol, static_cast<uint16_t>(textW));
|
|
178
|
-
}
|
|
179
|
-
ui_draw_rich_text(i, textX, textY, richTextBg, __ui_nodes[i].fontAntialias, 0, 0, static_cast<uint16_t>(textW));
|
|
180
|
-
break;
|
|
181
|
-
}
|
|
182
|
-
{
|
|
183
|
-
// Text shadow: draw the text in the shadow color at the offset first.
|
|
184
|
-
UI_COLOR_T tsClear = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor;
|
|
185
|
-
if (__ui_nodes[i].textShadowCount > 0) {
|
|
186
|
-
uint32_t tsCol = ui_blend(__ui_nodes[i].textShadowColor, tsClear, __ui_nodes[i].textShadowAlpha);
|
|
187
|
-
ui_draw_wrapped_text(displayText,
|
|
188
|
-
textX + __ui_nodes[i].textShadowOffsetX,
|
|
189
|
-
textY + __ui_nodes[i].textShadowOffsetY,
|
|
190
|
-
static_cast<uint16_t>(textW), tsCol, tsCol, ts, __ui_nodes[i].fontAntialias,
|
|
191
|
-
__ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing, __ui_nodes[i].lineHeight,
|
|
192
|
-
__ui_nodes[i].whiteSpaceMode, __ui_nodes[i].textAlign, 0, __ui_nodes[i].textOverflow);
|
|
193
|
-
}
|
|
194
|
-
// Use the parent's clear color as the text background when the node
|
|
195
|
-
// has no own background. This makes Adafruit_GFX's opaque glyph-cell
|
|
196
|
-
// fill blend with the parent (instead of drawing solid fg blocks that
|
|
197
|
-
// overlap adjacent lines/elements). For AA text, fg != bg so the
|
|
198
|
-
// edge-detection path still runs correctly.
|
|
199
|
-
// Glyph-cell background. For a translucent text node sitting on a
|
|
200
|
-
// filled translucent parent, the glyph cells must match the parent's
|
|
201
|
-
// blended fill: blend565(parent.bg, backdrop, opacity). The node's own
|
|
202
|
-
// clearColor carries the backdrop (set by flatten), and opacity has
|
|
203
|
-
// already inherited from the parent. Use the parent's raw bg as the
|
|
204
|
-
// source so the glyph cells reproduce the parent's translucent fill.
|
|
205
|
-
UI_COLOR_T textBg;
|
|
206
|
-
if (__ui_nodes[i].opacity < 100) {
|
|
207
|
-
uint16_t p = __ui_nodes[i].parent;
|
|
208
|
-
UI_COLOR_T source = (p != UI_NO_PARENT && __ui_nodes[p].hasBg) ? __ui_nodes[p].bg
|
|
209
|
-
: (__ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor);
|
|
210
|
-
textBg = ui_blend(source, __ui_nodes[i].clearColor, __ui_nodes[i].opacity);
|
|
211
|
-
} else {
|
|
212
|
-
textBg = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : ui_parent_clear_color(i);
|
|
213
|
-
}
|
|
214
|
-
ui_draw_wrapped_text(displayText, textX, textY, static_cast<uint16_t>(textW),
|
|
215
|
-
__ui_nodes[i].fg, textBg, ts, __ui_nodes[i].fontAntialias,
|
|
216
|
-
__ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing, __ui_nodes[i].lineHeight,
|
|
217
|
-
__ui_nodes[i].whiteSpaceMode, __ui_nodes[i].textAlign, __ui_nodes[i].underline, __ui_nodes[i].textOverflow);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
break;
|
|
221
|
-
case NODE_BUTTON:
|
|
222
|
-
// HTML disabled: halve the drawn colors (web-like faded control).
|
|
223
|
-
if (__ui_nodes[i].disabled) {
|
|
224
|
-
fillBg = (fillBg >> 1) & UI_DIM_MASK;
|
|
225
|
-
bColor = (bColor >> 1) & UI_DIM_MASK;
|
|
226
|
-
}
|
|
227
|
-
if (__ui_nodes[i].borderRadius > 0 && __ui_nodes[i].hasBg)
|
|
228
|
-
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, fillBg);
|
|
229
|
-
else if (__ui_nodes[i].hasBg)
|
|
230
|
-
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, __ui_nodes[i].box.w, __ui_nodes[i].box.h, fillBg);
|
|
231
|
-
ui_draw_shadow(i, drawY, 1);
|
|
232
|
-
if (__ui_nodes[i].borderStyle != 0) {
|
|
233
|
-
ui_draw_node_border(i, __ui_nodes[i].box.x, drawY, bColor);
|
|
234
|
-
}
|
|
235
|
-
{
|
|
236
|
-
int16_t insetL = static_cast<int16_t>(__ui_nodes[i].borderWidth) + static_cast<int16_t>(__ui_nodes[i].paddingLeft);
|
|
237
|
-
int16_t insetR = static_cast<int16_t>(__ui_nodes[i].borderWidth) + static_cast<int16_t>(__ui_nodes[i].paddingRight);
|
|
238
|
-
int16_t insetT = static_cast<int16_t>(__ui_nodes[i].borderWidth) + static_cast<int16_t>(__ui_nodes[i].paddingTop);
|
|
239
|
-
int16_t insetB = static_cast<int16_t>(__ui_nodes[i].borderWidth) + static_cast<int16_t>(__ui_nodes[i].paddingBottom);
|
|
240
|
-
int16_t textX = __ui_nodes[i].box.x + insetL;
|
|
241
|
-
int16_t textY = drawY + insetT;
|
|
242
|
-
int16_t textW = static_cast<int16_t>(__ui_nodes[i].box.w) - insetL - insetR;
|
|
243
|
-
int16_t textH = static_cast<int16_t>(__ui_nodes[i].box.h) - insetT - insetB;
|
|
244
|
-
if (textW < 1) textW = 1;
|
|
245
|
-
if (textH < 1) textH = static_cast<int16_t>(th);
|
|
246
|
-
ui_draw_wrapped_text(displayText,
|
|
247
|
-
textX,
|
|
248
|
-
textY + (textH - static_cast<int16_t>(th)) / 2,
|
|
249
|
-
static_cast<uint16_t>(textW),
|
|
250
|
-
__ui_nodes[i].fg,
|
|
251
|
-
__ui_nodes[i].hasBg ? fillBg : ui_parent_clear_color(i),
|
|
252
|
-
ts, __ui_nodes[i].fontAntialias, __ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing,
|
|
253
|
-
__ui_nodes[i].lineHeight, __ui_nodes[i].whiteSpaceMode, __ui_nodes[i].textAlign, __ui_nodes[i].underline, __ui_nodes[i].textOverflow);
|
|
254
|
-
}
|
|
255
|
-
break;
|
|
256
|
-
case NODE_SELECT:
|
|
257
|
-
// <select>: a bordered, optionally rounded box with its current option
|
|
258
|
-
// text drawn VERTICALLY CENTERED (unlike NODE_TEXT, which top-aligns).
|
|
259
|
-
// Mirrors NODE_BUTTON's centering so the label sits mid-box when the
|
|
260
|
-
// control is enlarged for touch (min-height). The label is dynamic
|
|
261
|
-
// (auto-bound to the selected option's text), so clear the previous
|
|
262
|
-
// text rect before redrawing, like NODE_CHECK does.
|
|
263
|
-
{
|
|
264
|
-
uint16_t clearW = __ui_nodes[i].box.w;
|
|
265
|
-
if (__ui_nodes[i].lastTextWidth > 0 && __ui_nodes[i].lastTextWidth > static_cast<int16_t>(clearW)) {
|
|
266
|
-
clearW = static_cast<uint16_t>(__ui_nodes[i].lastTextWidth);
|
|
267
|
-
}
|
|
268
|
-
if (paintTextW > clearW) clearW = paintTextW;
|
|
269
|
-
uint16_t clearH = __ui_nodes[i].box.h;
|
|
270
|
-
if (__ui_nodes[i].lastTextHeight > 0 && __ui_nodes[i].lastTextHeight > static_cast<int16_t>(clearH)) {
|
|
271
|
-
clearH = static_cast<uint16_t>(__ui_nodes[i].lastTextHeight);
|
|
272
|
-
}
|
|
273
|
-
if (paintTextH > clearH) clearH = paintTextH;
|
|
274
|
-
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, clearW, clearH,
|
|
275
|
-
__ui_nodes[i].hasBg ? fillBg : ui_parent_clear_color(i));
|
|
276
|
-
__ui_nodes[i].lastTextWidth = paintTextW;
|
|
277
|
-
__ui_nodes[i].lastTextHeight = paintTextH;
|
|
278
|
-
}
|
|
279
|
-
if (__ui_nodes[i].borderRadius > 0 && __ui_nodes[i].hasBg)
|
|
280
|
-
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, fillBg);
|
|
281
|
-
else if (__ui_nodes[i].hasBg)
|
|
282
|
-
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, __ui_nodes[i].box.w, __ui_nodes[i].box.h, fillBg);
|
|
283
|
-
ui_draw_shadow(i, drawY, 1);
|
|
284
|
-
if (__ui_nodes[i].borderStyle != 0) {
|
|
285
|
-
ui_draw_node_border(i, __ui_nodes[i].box.x, drawY, bColor);
|
|
286
|
-
}
|
|
287
|
-
{
|
|
288
|
-
int16_t insetL = static_cast<int16_t>(__ui_nodes[i].borderWidth) + static_cast<int16_t>(__ui_nodes[i].paddingLeft);
|
|
289
|
-
int16_t insetR = static_cast<int16_t>(__ui_nodes[i].borderWidth) + static_cast<int16_t>(__ui_nodes[i].paddingRight);
|
|
290
|
-
int16_t insetT = static_cast<int16_t>(__ui_nodes[i].borderWidth) + static_cast<int16_t>(__ui_nodes[i].paddingTop);
|
|
291
|
-
int16_t insetB = static_cast<int16_t>(__ui_nodes[i].borderWidth) + static_cast<int16_t>(__ui_nodes[i].paddingBottom);
|
|
292
|
-
int16_t textX = __ui_nodes[i].box.x + insetL;
|
|
293
|
-
int16_t textY = drawY + insetT;
|
|
294
|
-
// The right end reserves 14px for the dropdown chevron — the label
|
|
295
|
-
// wraps against the reduced width, never under the chevron.
|
|
296
|
-
int16_t textW = static_cast<int16_t>(__ui_nodes[i].box.w) - insetL - insetR - 14;
|
|
297
|
-
int16_t textH = static_cast<int16_t>(__ui_nodes[i].box.h) - insetT - insetB;
|
|
298
|
-
if (textW < 1) textW = 1;
|
|
299
|
-
if (textH < 1) textH = static_cast<int16_t>(th);
|
|
300
|
-
ui_draw_wrapped_text(displayText,
|
|
301
|
-
textX,
|
|
302
|
-
textY + (textH - static_cast<int16_t>(th)) / 2,
|
|
303
|
-
static_cast<uint16_t>(textW),
|
|
304
|
-
__ui_nodes[i].fg,
|
|
305
|
-
__ui_nodes[i].hasBg ? fillBg : ui_parent_clear_color(i),
|
|
306
|
-
ts, __ui_nodes[i].fontAntialias, __ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing,
|
|
307
|
-
__ui_nodes[i].lineHeight, __ui_nodes[i].whiteSpaceMode, __ui_nodes[i].textAlign, __ui_nodes[i].underline, __ui_nodes[i].textOverflow);
|
|
308
|
-
// Dropdown chevron: a small solid v at the right end — the
|
|
309
|
-
// affordance that the control opens a menu of options.
|
|
310
|
-
{
|
|
311
|
-
int16_t chX = __ui_nodes[i].box.x + static_cast<int16_t>(__ui_nodes[i].box.w) - insetR - 11;
|
|
312
|
-
int16_t chY = drawY + (static_cast<int16_t>(__ui_nodes[i].box.h) - 5) / 2;
|
|
313
|
-
ui_display_fill_rect(chX, chY, 9, 1, __ui_nodes[i].fg);
|
|
314
|
-
ui_display_fill_rect(chX + 1, chY + 1, 7, 1, __ui_nodes[i].fg);
|
|
315
|
-
ui_display_fill_rect(chX + 2, chY + 2, 5, 1, __ui_nodes[i].fg);
|
|
316
|
-
ui_display_fill_rect(chX + 3, chY + 3, 3, 1, __ui_nodes[i].fg);
|
|
317
|
-
ui_display_fill_rect(chX + 4, chY + 4, 1, 1, __ui_nodes[i].fg);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
break;
|
|
321
|
-
case NODE_CHECK:
|
|
322
|
-
{
|
|
323
|
-
uint16_t clearW = __ui_nodes[i].box.w;
|
|
324
|
-
if (__ui_nodes[i].lastTextWidth > 0 && __ui_nodes[i].lastTextWidth > static_cast<int16_t>(clearW)) {
|
|
325
|
-
clearW = static_cast<uint16_t>(__ui_nodes[i].lastTextWidth);
|
|
326
|
-
}
|
|
327
|
-
if (paintTextW > clearW) clearW = paintTextW;
|
|
328
|
-
uint16_t clearH = __ui_nodes[i].box.h;
|
|
329
|
-
if (__ui_nodes[i].lastTextHeight > 0 && __ui_nodes[i].lastTextHeight > static_cast<int16_t>(clearH)) {
|
|
330
|
-
clearH = static_cast<uint16_t>(__ui_nodes[i].lastTextHeight);
|
|
331
|
-
}
|
|
332
|
-
if (paintTextH > clearH) clearH = paintTextH;
|
|
333
|
-
// border-radius > 0 (kit .switch pills): clear the full text rect to
|
|
334
|
-
// the backdrop, then paint the rounded box. Mirrors the preview's
|
|
335
|
-
// drawCheckNode pill path. The :checked pair (kit wires it to
|
|
336
|
-
// --primary/--primary-foreground) swaps the TRACK color when on.
|
|
337
|
-
if (__ui_nodes[i].borderRadius > 0 && __ui_nodes[i].hasBg) {
|
|
338
|
-
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, clearW, clearH,
|
|
339
|
-
ui_parent_clear_color(i));
|
|
340
|
-
ui_display_fill_round_rect(__ui_nodes[i].box.x, drawY,
|
|
341
|
-
__ui_nodes[i].box.w, __ui_nodes[i].box.h,
|
|
342
|
-
__ui_nodes[i].borderRadius,
|
|
343
|
-
(__ui_nodes[i].value && __ui_nodes[i].hasCheckedBg) ? __ui_nodes[i].checkedBg : fillBg);
|
|
344
|
-
} else {
|
|
345
|
-
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, clearW, clearH,
|
|
346
|
-
__ui_nodes[i].hasBg ? fillBg : ui_parent_clear_color(i));
|
|
347
|
-
}
|
|
348
|
-
__ui_nodes[i].lastTextWidth = paintTextW;
|
|
349
|
-
__ui_nodes[i].lastTextHeight = paintTextH;
|
|
350
|
-
}
|
|
351
|
-
{
|
|
352
|
-
// Pills inset the 16px indicator from the left edge so the knob
|
|
353
|
-
// doesn't touch the rounded end; plain checkboxes keep it flush.
|
|
354
|
-
// The knob SLIDES with state — left when off, right when on — the
|
|
355
|
-
// switch affordance (static jump; no travel animation).
|
|
356
|
-
int16_t knobSlide = __ui_nodes[i].borderRadius > 0
|
|
357
|
-
? static_cast<int16_t>(__ui_nodes[i].box.w) - 16 - 6
|
|
358
|
-
: 0;
|
|
359
|
-
if (knobSlide < 0) knobSlide = 0;
|
|
360
|
-
int16_t cbX = __ui_nodes[i].box.x
|
|
361
|
-
+ (__ui_nodes[i].borderRadius > 0 ? 3 : 0)
|
|
362
|
-
+ (__ui_nodes[i].value ? knobSlide : 0);
|
|
363
|
-
// Vertically center the 16px indicator within the box so a tall
|
|
364
|
-
// (touch-friendly) checkbox doesn't pin the indicator to the top.
|
|
365
|
-
// (box.h - 16) / 2 is 0 for the default 16px-tall box, so existing
|
|
366
|
-
// checkboxes render byte-identically.
|
|
367
|
-
int16_t cbY = drawY + (static_cast<int16_t>(__ui_nodes[i].box.h) - 16) / 2;
|
|
368
|
-
if (cbY < drawY) cbY = drawY;
|
|
369
|
-
if (__ui_nodes[i].value && __ui_nodes[i].borderRadius > 0) {
|
|
370
|
-
// Switch pill: the knob stays a knob — a solid circle when on, no
|
|
371
|
-
// checkbox square + checkmark. Same geometry as the radio dot.
|
|
372
|
-
// The knob carries the :checked color (primary-foreground).
|
|
373
|
-
ui_display_fill_circle(cbX + 8, cbY + 8, 7,
|
|
374
|
-
__ui_nodes[i].hasCheckedFg ? __ui_nodes[i].checkedFg : __ui_nodes[i].fg);
|
|
375
|
-
} else if (__ui_nodes[i].value) {
|
|
376
|
-
// Checked face carries the :checked background (primary), the
|
|
377
|
-
// checkmark its color (primary-foreground).
|
|
378
|
-
ui_display_fill_rect(cbX, cbY, 16, 16,
|
|
379
|
-
__ui_nodes[i].hasCheckedBg ? __ui_nodes[i].checkedBg : __ui_nodes[i].fg);
|
|
380
|
-
UI_COLOR_T inv = __ui_nodes[i].hasCheckedFg ? __ui_nodes[i].checkedFg
|
|
381
|
-
: (__ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor);
|
|
382
|
-
#ifdef UI_AA
|
|
383
|
-
{
|
|
384
|
-
// Draw the checkmark to a 16×16 AA canvas for smooth diagonals.
|
|
385
|
-
CuttlefishCanvas16* c = ui_aa_begin(16, 16, __ui_nodes[i].fg);
|
|
386
|
-
// First stroke: down-left (3,8 → 7,12)
|
|
387
|
-
if (c) {
|
|
388
|
-
ui_aa_line(c, 4.0f, 8.0f, 7.0f, 12.0f, inv);
|
|
389
|
-
ui_aa_line(c, 5.0f, 8.0f, 8.0f, 12.0f, inv);
|
|
390
|
-
// Second stroke: up-right (7,11 → 13,4)
|
|
391
|
-
ui_aa_line(c, 7.0f, 11.0f, 13.0f, 4.0f, inv);
|
|
392
|
-
ui_aa_line(c, 8.0f, 11.0f, 14.0f, 4.0f, inv);
|
|
393
|
-
ui_aa_push(c, cbX, cbY);
|
|
394
|
-
} else {
|
|
395
|
-
ui_display_draw_line(cbX + 3, cbY + 8, cbX + 7, cbY + 12, inv);
|
|
396
|
-
ui_display_draw_line(cbX + 4, cbY + 8, cbX + 8, cbY + 12, inv);
|
|
397
|
-
ui_display_draw_line(cbX + 7, cbY + 12, cbX + 13, cbY + 4, inv);
|
|
398
|
-
ui_display_draw_line(cbX + 8, cbY + 12, cbX + 14, cbY + 4, inv);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
#else
|
|
402
|
-
ui_display_draw_line(cbX + 3, cbY + 8, cbX + 7, cbY + 12, inv);
|
|
403
|
-
ui_display_draw_line(cbX + 4, cbY + 8, cbX + 8, cbY + 12, inv);
|
|
404
|
-
ui_display_draw_line(cbX + 3, cbY + 9, cbX + 7, cbY + 13, inv);
|
|
405
|
-
ui_display_draw_line(cbX + 7, cbY + 12, cbX + 13, cbY + 4, inv);
|
|
406
|
-
ui_display_draw_line(cbX + 8, cbY + 12, cbX + 14, cbY + 4, inv);
|
|
407
|
-
ui_display_draw_line(cbX + 7, cbY + 13, cbX + 13, cbY + 5, inv);
|
|
408
|
-
#endif
|
|
409
|
-
} else if (__ui_nodes[i].borderRadius > 0) {
|
|
410
|
-
// Off-state pill: hollow circular knob outline on the track.
|
|
411
|
-
ui_display_draw_circle(cbX + 8, cbY + 8, 7, __ui_nodes[i].fg);
|
|
412
|
-
} else {
|
|
413
|
-
ui_display_draw_rect(cbX, cbY, 16, 16, __ui_nodes[i].fg);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
// Center the label on the same baseline as the indicator: apply the
|
|
417
|
-
// indicator's vertical offset to the text origin too, so a tall box
|
|
418
|
-
// keeps the indicator + label aligned as a row rather than straddling
|
|
419
|
-
// the box top/bottom.
|
|
420
|
-
{
|
|
421
|
-
int16_t checkOff = (static_cast<int16_t>(__ui_nodes[i].box.h) - 16) / 2;
|
|
422
|
-
if (checkOff < 0) checkOff = 0;
|
|
423
|
-
ui_draw_wrapped_text(displayText, __ui_nodes[i].box.x + 22, drawY + checkOff, textMaxW,
|
|
424
|
-
__ui_nodes[i].fg, __ui_nodes[i].hasBg ? fillBg : ui_parent_clear_color(i),
|
|
425
|
-
ts, __ui_nodes[i].fontAntialias, __ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing,
|
|
426
|
-
__ui_nodes[i].lineHeight, __ui_nodes[i].whiteSpaceMode, 0, __ui_nodes[i].underline, __ui_nodes[i].textOverflow);
|
|
427
|
-
}
|
|
428
|
-
break;
|
|
429
|
-
case NODE_RADIO:
|
|
430
|
-
{
|
|
431
|
-
uint16_t clearW = __ui_nodes[i].box.w;
|
|
432
|
-
if (__ui_nodes[i].lastTextWidth > 0 && __ui_nodes[i].lastTextWidth > static_cast<int16_t>(clearW)) {
|
|
433
|
-
clearW = static_cast<uint16_t>(__ui_nodes[i].lastTextWidth);
|
|
434
|
-
}
|
|
435
|
-
if (paintTextW > clearW) clearW = paintTextW;
|
|
436
|
-
uint16_t clearH = __ui_nodes[i].box.h;
|
|
437
|
-
if (__ui_nodes[i].lastTextHeight > 0 && __ui_nodes[i].lastTextHeight > static_cast<int16_t>(clearH)) {
|
|
438
|
-
clearH = static_cast<uint16_t>(__ui_nodes[i].lastTextHeight);
|
|
439
|
-
}
|
|
440
|
-
if (paintTextH > clearH) clearH = paintTextH;
|
|
441
|
-
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, clearW, clearH,
|
|
442
|
-
__ui_nodes[i].hasBg ? fillBg : ui_parent_clear_color(i));
|
|
443
|
-
__ui_nodes[i].lastTextWidth = paintTextW;
|
|
444
|
-
__ui_nodes[i].lastTextHeight = paintTextH;
|
|
445
|
-
int16_t cbX = __ui_nodes[i].box.x;
|
|
446
|
-
// Vertically center the 16px indicator within the box (see NODE_CHECK).
|
|
447
|
-
int16_t cbY = drawY + (static_cast<int16_t>(__ui_nodes[i].box.h) - 16) / 2;
|
|
448
|
-
if (cbY < drawY) cbY = drawY;
|
|
449
|
-
#ifdef UI_AA
|
|
450
|
-
{
|
|
451
|
-
// Render the radio circle to a 16×16 AA canvas, then push.
|
|
452
|
-
UI_COLOR_T radioBg = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor;
|
|
453
|
-
// The selected ring carries the :checked background (primary).
|
|
454
|
-
UI_COLOR_T ringCol = (__ui_nodes[i].value && __ui_nodes[i].hasCheckedBg)
|
|
455
|
-
? __ui_nodes[i].checkedBg : __ui_nodes[i].fg;
|
|
456
|
-
CuttlefishCanvas16* c = ui_aa_begin(16, 16, radioBg);
|
|
457
|
-
if (c) {
|
|
458
|
-
if (__ui_nodes[i].value) {
|
|
459
|
-
ui_aa_fill_circle(c, 8, 8, 7.0f, ringCol);
|
|
460
|
-
ui_aa_fill_circle(c, 8, 8, 3.0f, radioBg);
|
|
461
|
-
} else {
|
|
462
|
-
ui_aa_circle(c, 8, 8, 7.0f, __ui_nodes[i].fg);
|
|
463
|
-
}
|
|
464
|
-
ui_aa_push(c, cbX, cbY);
|
|
465
|
-
} else if (__ui_nodes[i].value) {
|
|
466
|
-
ui_display_fill_circle(cbX + 8, cbY + 8, 7, ringCol);
|
|
467
|
-
ui_display_fill_circle(cbX + 8, cbY + 8, 3, radioBg);
|
|
468
|
-
} else {
|
|
469
|
-
ui_display_draw_circle(cbX + 8, cbY + 8, 7, __ui_nodes[i].fg);
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
#else
|
|
473
|
-
if (__ui_nodes[i].value) {
|
|
474
|
-
ui_display_fill_circle(cbX + 8, cbY + 8, 7,
|
|
475
|
-
__ui_nodes[i].hasCheckedBg ? __ui_nodes[i].checkedBg : __ui_nodes[i].fg);
|
|
476
|
-
ui_display_fill_circle(cbX + 8, cbY + 8, 3, __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor);
|
|
477
|
-
} else {
|
|
478
|
-
ui_display_draw_circle(cbX + 8, cbY + 8, 7, __ui_nodes[i].fg);
|
|
479
|
-
}
|
|
480
|
-
#endif
|
|
481
|
-
}
|
|
482
|
-
// Center the label on the same baseline as the indicator (see NODE_CHECK).
|
|
483
|
-
{
|
|
484
|
-
int16_t radioOff = (static_cast<int16_t>(__ui_nodes[i].box.h) - 16) / 2;
|
|
485
|
-
if (radioOff < 0) radioOff = 0;
|
|
486
|
-
ui_draw_wrapped_text(displayText, __ui_nodes[i].box.x + 22, drawY + radioOff, textMaxW,
|
|
487
|
-
__ui_nodes[i].fg, __ui_nodes[i].hasBg ? fillBg : ui_parent_clear_color(i),
|
|
488
|
-
ts, __ui_nodes[i].fontAntialias, __ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing,
|
|
489
|
-
__ui_nodes[i].lineHeight, __ui_nodes[i].whiteSpaceMode, 0, __ui_nodes[i].underline, __ui_nodes[i].textOverflow);
|
|
490
|
-
}
|
|
491
|
-
break;
|
|
492
|
-
case NODE_PROGRESS:
|
|
493
|
-
// Progress bar: outline track + filled portion based on .value (0-100).
|
|
494
|
-
// Incremental redraw — only draws/clears the delta to avoid flashing.
|
|
495
|
-
{
|
|
496
|
-
int16_t bx = __ui_nodes[i].box.x;
|
|
497
|
-
int16_t by = drawY;
|
|
498
|
-
int16_t bw = __ui_nodes[i].box.w;
|
|
499
|
-
int16_t bh = __ui_nodes[i].box.h;
|
|
500
|
-
UI_COLOR_T bgCol = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor;
|
|
501
|
-
UI_COLOR_T fgCol = __ui_nodes[i].fg;
|
|
502
|
-
|
|
503
|
-
// On first draw (lastTextWidth < 0), draw everything.
|
|
504
|
-
// Otherwise incremental: only update the changed portion.
|
|
505
|
-
uint8_t pct =
|
|
506
|
-
int16_t fillW = (static_cast<int32_t>(bw - 2) * pct) / 100;
|
|
507
|
-
int16_t prevW = __ui_nodes[i].lastTextWidth; // reused as previous fill width
|
|
508
|
-
|
|
509
|
-
if (prevW < 0) {
|
|
510
|
-
// Full redraw: outline + background + fill
|
|
511
|
-
ui_display_draw_rect(bx, by, bw, bh, fgCol);
|
|
512
|
-
ui_display_fill_rect(bx + 1, by + 1, bw - 2, bh - 2, bgCol);
|
|
513
|
-
if (fillW > 0) {
|
|
514
|
-
ui_display_fill_rect(bx + 1, by + 1, fillW, bh - 2, fgCol);
|
|
515
|
-
}
|
|
516
|
-
} else if (fillW > prevW) {
|
|
517
|
-
// Value increased: draw new fill segment on top (no clear needed)
|
|
518
|
-
ui_display_fill_rect(bx + 1 + prevW, by + 1, fillW - prevW, bh - 2, fgCol);
|
|
519
|
-
} else if (fillW < prevW) {
|
|
520
|
-
// Value decreased: clear the removed portion
|
|
521
|
-
ui_display_fill_rect(bx + 1 + fillW, by + 1, prevW - fillW, bh - 2, bgCol);
|
|
522
|
-
}
|
|
523
|
-
// Remember current fill width for next incremental update
|
|
524
|
-
__ui_nodes[i].lastTextWidth = fillW;
|
|
525
|
-
}
|
|
526
|
-
break;
|
|
527
|
-
case NODE_RANGE:
|
|
528
|
-
// Range slider: horizontal track + draggable thumb.
|
|
529
|
-
// Incremental redraw (like NODE_PROGRESS): lastTextWidth holds the
|
|
530
|
-
// previous fill width. We erase the delta region between old and new
|
|
531
|
-
// thumb positions with the background, then redraw the track portion
|
|
532
|
-
// and the new thumb — so dragging backward doesn't leave ghost thumbs.
|
|
533
|
-
{
|
|
534
|
-
int16_t bx = __ui_nodes[i].box.x;
|
|
535
|
-
int16_t by = drawY;
|
|
536
|
-
int16_t bw = __ui_nodes[i].box.w;
|
|
537
|
-
int16_t bh = __ui_nodes[i].box.h;
|
|
538
|
-
UI_COLOR_T fgCol = __ui_nodes[i].fg;
|
|
539
|
-
UI_COLOR_T bgCol = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor;
|
|
540
|
-
UI_COLOR_T dimFg = (UI_COLOR_T)((fgCol >> 1) & UI_DIM_MASK);
|
|
541
|
-
|
|
542
|
-
int16_t trackY = by + bh / 2;
|
|
543
|
-
int16_t rMin = __ui_nodes[i].rangeMin;
|
|
544
|
-
int16_t rMax = __ui_nodes[i].rangeMax;
|
|
545
|
-
int16_t range = rMax - rMin;
|
|
546
|
-
if (range <= 0) range = 100;
|
|
547
|
-
int16_t pct =
|
|
548
|
-
int16_t fillW = (static_cast<int32_t>(bw - 8) * pct) / range;
|
|
549
|
-
// lastTextWidth carries the previous fill width, or -1 if this node
|
|
550
|
-
// has never been drawn (fillW=0 at value=min is a valid thumb pos).
|
|
551
|
-
int16_t prevFillW = __ui_nodes[i].lastTextWidth;
|
|
552
|
-
int16_t newThumbX = bx + 4 + fillW - 3;
|
|
553
|
-
|
|
554
|
-
if (prevFillW < 0) {
|
|
555
|
-
// First draw: redraw the whole track + fill from scratch.
|
|
556
|
-
ui_display_draw_fast_hline(bx, trackY, bw, dimFg);
|
|
557
|
-
ui_display_draw_fast_hline(bx + 4, trackY, fillW, fgCol);
|
|
558
|
-
} else {
|
|
559
|
-
// Incremental: wipe the strip between the old and new thumb
|
|
560
|
-
// positions (whichever extends further on each side), then restore
|
|
561
|
-
// the track line. This is symmetric — old thumbs disappear whether
|
|
562
|
-
// the drag moves forward or backward.
|
|
563
|
-
int16_t prevThumbX = bx + 4 + prevFillW - 3;
|
|
564
|
-
int16_t left = prevThumbX < newThumbX ? prevThumbX : newThumbX;
|
|
565
|
-
int16_t right = prevThumbX + 6 > newThumbX + 6 ? prevThumbX + 6 : newThumbX + 6;
|
|
566
|
-
if (left < bx) left = bx;
|
|
567
|
-
if (right > bx + bw) right = bx + bw;
|
|
568
|
-
// Erase the thumb band (10px tall) to background.
|
|
569
|
-
ui_display_fill_rect(left, trackY - 5, right - left, 10, bgCol);
|
|
570
|
-
// Restore the track line over the wiped strip: bright up to the
|
|
571
|
-
// current fill end, dim beyond it.
|
|
572
|
-
int16_t fillEnd = bx + 4 + fillW;
|
|
573
|
-
if (right <= fillEnd) {
|
|
574
|
-
ui_display_draw_fast_hline(left, trackY, right - left, fgCol);
|
|
575
|
-
} else if (left >= fillEnd) {
|
|
576
|
-
ui_display_draw_fast_hline(left, trackY, right - left, dimFg);
|
|
577
|
-
} else {
|
|
578
|
-
ui_display_draw_fast_hline(left, trackY, fillEnd - left, fgCol);
|
|
579
|
-
ui_display_draw_fast_hline(fillEnd, trackY, right - fillEnd, dimFg);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// Thumb: small filled rectangle at the current position.
|
|
584
|
-
if (newThumbX < bx + 1) newThumbX = bx + 1;
|
|
585
|
-
if (newThumbX > bx + bw - 7) newThumbX = bx + bw - 7;
|
|
586
|
-
ui_display_fill_rect(newThumbX, trackY - 5, 6, 10, fgCol);
|
|
587
|
-
|
|
588
|
-
// Remember current fill width for the next incremental update.
|
|
589
|
-
__ui_nodes[i].lastTextWidth = fillW;
|
|
590
|
-
}
|
|
591
|
-
break;
|
|
592
|
-
case NODE_INPUT:
|
|
593
|
-
// Input field: bordered rect + current text (or placeholder), clipped to box width.
|
|
594
|
-
{
|
|
595
|
-
int16_t bx = __ui_nodes[i].box.x;
|
|
596
|
-
int16_t by = drawY;
|
|
597
|
-
int16_t bw = __ui_nodes[i].box.w;
|
|
598
|
-
int16_t bh = __ui_nodes[i].box.h;
|
|
599
|
-
UI_COLOR_T fgCol = __ui_nodes[i].fg;
|
|
600
|
-
UI_COLOR_T bgCol = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor;
|
|
601
|
-
// HTML disabled: halve the drawn colors (web-like faded control).
|
|
602
|
-
if (__ui_nodes[i].disabled) {
|
|
603
|
-
fgCol = (fgCol >> 1) & UI_DIM_MASK;
|
|
604
|
-
bgCol = (bgCol >> 1) & UI_DIM_MASK;
|
|
605
|
-
}
|
|
606
|
-
if (__ui_nodes[i].borderRadius > 0) {
|
|
607
|
-
ui_display_fill_round_rect(bx, by, bw, bh, __ui_nodes[i].borderRadius, bgCol);
|
|
608
|
-
} else {
|
|
609
|
-
ui_display_fill_rect(bx, by, bw, bh, bgCol);
|
|
610
|
-
}
|
|
611
|
-
UI_COLOR_T inputBorder = __ui_nodes[i].borderColor ? __ui_nodes[i].borderColor : fgCol;
|
|
612
|
-
uint8_t inputBorderStyle = __ui_nodes[i].borderStyle ? __ui_nodes[i].borderStyle : 1;
|
|
613
|
-
uint8_t inputBorderWidth = __ui_nodes[i].borderWidth ? __ui_nodes[i].borderWidth : 1;
|
|
614
|
-
ui_draw_rect_outline(bx, by, bw, bh, __ui_nodes[i].borderRadius, inputBorderStyle, inputBorderWidth, inputBorder);
|
|
615
|
-
// Show typed text (textBuffer) in fg color, or placeholder (.text)
|
|
616
|
-
// dimmed gray when the buffer is empty.
|
|
617
|
-
UI_COLOR_T textCol = fgCol;
|
|
618
|
-
const char* disp = (__ui_nodes[i].textBuffer[0] != 0)
|
|
619
|
-
? __ui_nodes[i].textBuffer
|
|
620
|
-
: (__ui_nodes[i].text ? __ui_nodes[i].text : "");
|
|
621
|
-
#if defined(UI_HIDE_OSK)
|
|
622
|
-
// Desktop target: when this input is the active edit target, suppress
|
|
623
|
-
// the placeholder. The editing buffer is loaded from the (likely empty)
|
|
624
|
-
// textBuffer on focus, so without this the placeholder ("enter name")
|
|
625
|
-
// would render with the caret at its end. Hide it so the caret shows
|
|
626
|
-
// on a clean field at position 0 until the user types.
|
|
627
|
-
if (__ui_kb_visible && static_cast<int16_t>(__ui_kb_target) == static_cast<int16_t>(i) && __ui_nodes[i].textBuffer[0] == 0) {
|
|
628
|
-
disp = "";
|
|
629
|
-
}
|
|
630
|
-
#endif
|
|
631
|
-
if (__ui_nodes[i].textBuffer[0] == 0) {
|
|
632
|
-
#if UI_COLOR_DEPTH == 888
|
|
633
|
-
textCol = 0x848484;
|
|
634
|
-
#else
|
|
635
|
-
textCol = 0x8410;
|
|
636
|
-
#endif
|
|
637
|
-
}
|
|
638
|
-
// Note: the actual text draw is via ui_draw_text (which uses __ui_gfx).
|
|
639
|
-
// The setCursor/setTextColor/setTextSize below are legacy — ui_draw_text
|
|
640
|
-
// handles its own cursor/colors. Keep them on __ui_gfx for consistency
|
|
641
|
-
// (in case __ui_gfx is the scroll canvas, not __tc_display).
|
|
642
|
-
ui_display_set_cursor(bx + 4, by + (bh - ts * 8) / 2);
|
|
643
|
-
ui_display_set_text_color(textCol, bgCol);
|
|
644
|
-
ui_display_set_text_size(ts);
|
|
645
|
-
// Clip: at textSize ts, each char is ts*6px advance. Only print chars
|
|
646
|
-
// that fit within the box (bw - 8px margin), so text never overflows
|
|
647
|
-
// the border or wraps to the next line.
|
|
648
|
-
int16_t maxChars = (bw - 8) / (ts * 6);
|
|
649
|
-
if (maxChars < 0) maxChars = 0;
|
|
650
|
-
int16_t len = static_cast<int16_t>(strlen(disp));
|
|
651
|
-
if (len > maxChars) len = maxChars;
|
|
652
|
-
if (len > UI_TEXT_BUF) len = UI_TEXT_BUF;
|
|
653
|
-
char clipped[UI_TEXT_BUF + 1];
|
|
654
|
-
for (int16_t c = 0; c < len; c++) {
|
|
655
|
-
clipped[c] = disp[c];
|
|
656
|
-
}
|
|
657
|
-
clipped[len] = 0;
|
|
658
|
-
ui_draw_text(clipped, bx + 4, by + (bh - ui_text_height(ts, __ui_nodes[i].fontFace)) / 2,
|
|
659
|
-
textCol, bgCol, ts, __ui_nodes[i].fontAntialias, __ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing);
|
|
660
|
-
#if defined(UI_HIDE_OSK)
|
|
661
|
-
// Desktop target: with no OSK grid there's no focus indicator. Draw a
|
|
662
|
-
// blinking caret at the end of the typed text on the active edit target
|
|
663
|
-
// so the user sees which field they're editing. Blink ~3×/sec via the
|
|
664
|
-
// top bits of __ui_kb_blink (mask 0x20 toggles every 32 ticks ≈ 530ms).
|
|
665
|
-
if (__ui_kb_visible && static_cast<int16_t>(__ui_kb_target) == static_cast<int16_t>(i) && (__ui_kb_blink & 0x20)) {
|
|
666
|
-
uint16_t caretW = ui_text_width(clipped, ts, __ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing);
|
|
667
|
-
int16_t caretX = bx + 4 + static_cast<int16_t>(caretW);
|
|
668
|
-
int16_t caretYTop = by + (bh - ts * 8) / 2;
|
|
669
|
-
// fgCol (not textCol): the placeholder-dimming path sets textCol to
|
|
670
|
-
// gray, which would make the caret nearly invisible on a focused
|
|
671
|
-
// empty field. The caret should always be the input's foreground.
|
|
672
|
-
ui_display_fill_rect(caretX, caretYTop, static_cast<int16_t>(ts > 1 ? 2 : 1), static_cast<int16_t>(ts * 8), fgCol);
|
|
673
|
-
}
|
|
674
|
-
#endif
|
|
675
|
-
}
|
|
676
|
-
break;
|
|
677
|
-
case NODE_IMG:
|
|
678
|
-
{
|
|
679
|
-
UI_COLOR_T imgBg = __ui_nodes[i].hasBg ? fillBg : __ui_nodes[i].clearColor;
|
|
680
|
-
int16_t fillW = ui_rotated_face_w(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
681
|
-
int16_t fillH = ui_rotated_face_h(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
682
|
-
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, fillW, fillH, imgBg);
|
|
683
|
-
}
|
|
684
|
-
if (__ui_nodes[i].imgDataId < __ui_image_count) {
|
|
685
|
-
const UIImage* img = &__ui_images[__ui_nodes[i].imgDataId];
|
|
686
|
-
int16_t targetW = __ui_nodes[i].box.w;
|
|
687
|
-
int16_t targetH = __ui_nodes[i].box.h;
|
|
688
|
-
ui_draw_image_with_fit(img, __ui_nodes[i].box.x, drawY, __ui_nodes[i].rotateDeg, __ui_nodes[i].objectFit, targetW, targetH);
|
|
689
|
-
}
|
|
690
|
-
if (__ui_nodes[i].borderStyle != 0) {
|
|
691
|
-
int16_t borderW = ui_rotated_face_w(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
692
|
-
int16_t borderH = ui_rotated_face_h(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
693
|
-
ui_draw_rect_outline(__ui_nodes[i].box.x, drawY, borderW, borderH,
|
|
694
|
-
__ui_nodes[i].borderRadius, __ui_nodes[i].borderStyle, __ui_nodes[i].borderWidth, bColor);
|
|
695
|
-
}
|
|
696
|
-
break;
|
|
697
|
-
case NODE_CANVAS: {
|
|
698
|
-
// Find this node's draw callback.
|
|
699
|
-
void (*__ui_canvas_fn)(CuttlefishCanvas16*) = nullptr;
|
|
700
|
-
for (uint16_t b = 0; b < __ui_canvas_binding_count; b++) {
|
|
701
|
-
if (__ui_canvas_bindings[b].node == i) { __ui_canvas_fn = __ui_canvas_bindings[b].fn; break; }
|
|
702
|
-
}
|
|
703
|
-
if (__ui_canvas_fn) {
|
|
704
|
-
int16_t __ui_cw = __ui_nodes[i].canvasW;
|
|
705
|
-
int16_t __ui_ch = __ui_nodes[i].canvasH;
|
|
706
|
-
if (__ui_cw > 0 && __ui_ch > 0) {
|
|
707
|
-
uint8_t __ui_canvas_drawn = 0;
|
|
708
|
-
if (ui_display_is_default_target()) {
|
|
709
|
-
// Cache a canvas sized to the buffer for direct hardware draws so
|
|
710
|
-
// the callback's many primitives push as one bitmap. When the
|
|
711
|
-
// active target is already a memory canvas (scroll/repair/full
|
|
712
|
-
// framebuffer), skip the nested allocation and draw directly below.
|
|
713
|
-
// A canvas can construct but fail its internal pixel-buffer malloc
|
|
714
|
-
// (non-null canvas, null buffer). Treat that as "no canvas" so a
|
|
715
|
-
// transient malloc failure self-heals instead of blanking the node.
|
|
716
|
-
if (!__ui_node_canvas || !display_canvasBuffer(__ui_node_canvas) ||
|
|
717
|
-
display_canvasWidth(__ui_node_canvas) != __ui_cw || display_canvasHeight(__ui_node_canvas) != __ui_ch) {
|
|
718
|
-
display_deleteCanvas(__ui_node_canvas);
|
|
719
|
-
__ui_node_canvas = display_createCanvas(__ui_cw, __ui_ch);
|
|
720
|
-
}
|
|
721
|
-
CuttlefishCanvas16* __ui_lc = __ui_node_canvas;
|
|
722
|
-
if (__ui_lc && display_canvasBuffer(__ui_lc)) {
|
|
723
|
-
display_canvasFillScreen(__ui_lc, __ui_nodes[i].clearColor);
|
|
724
|
-
CuttlefishDisplayTarget* __ui_prev_target = ui_display_get_target();
|
|
725
|
-
ui_display_set_target((CuttlefishDisplayTarget*)__ui_lc);
|
|
726
|
-
__ui_canvas_fn(__ui_lc);
|
|
727
|
-
ui_display_set_target(__ui_prev_target);
|
|
728
|
-
ui_draw_canvas_rect(__ui_lc, __ui_nodes[i].box.x, drawY, __ui_cw, __ui_ch);
|
|
729
|
-
__ui_canvas_drawn = 1;
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
if (!__ui_canvas_drawn) {
|
|
733
|
-
UI_COLOR_T __ui_canvas_bg = __ui_nodes[i].hasBg ? fillBg : __ui_nodes[i].clearColor;
|
|
734
|
-
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, __ui_nodes[i].box.w, __ui_nodes[i].box.h, __ui_canvas_bg);
|
|
735
|
-
int16_t __ui_prev_off_x = __ui_draw_off_x;
|
|
736
|
-
int16_t __ui_prev_off_y = __ui_draw_off_y;
|
|
737
|
-
int16_t __ui_prev_canvas_w = __ui_canvas_fallback_w;
|
|
738
|
-
int16_t __ui_prev_canvas_h = __ui_canvas_fallback_h;
|
|
739
|
-
__ui_canvas_fallback_w = __ui_cw;
|
|
740
|
-
__ui_canvas_fallback_h = __ui_ch;
|
|
741
|
-
__ui_draw_off_x = static_cast<int16_t>(__ui_prev_off_x + __ui_nodes[i].box.x);
|
|
742
|
-
__ui_draw_off_y = static_cast<int16_t>(__ui_prev_off_y + drawY);
|
|
743
|
-
__ui_canvas_fn(nullptr);
|
|
744
|
-
__ui_draw_off_x = __ui_prev_off_x;
|
|
745
|
-
__ui_draw_off_y = __ui_prev_off_y;
|
|
746
|
-
__ui_canvas_fallback_w = __ui_prev_canvas_w;
|
|
747
|
-
__ui_canvas_fallback_h = __ui_prev_canvas_h;
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
break;
|
|
752
|
-
}
|
|
753
|
-
case NODE_LIST: {
|
|
754
|
-
// Virtualized list: state lives on the node now (listCountFn/listItemFn/
|
|
755
|
-
// listCount/scrollY/contentHeight/listItemHeight), not in a side table.
|
|
756
|
-
// Resolve lazily as a safety net for targets whose startup path runs
|
|
757
|
-
// before the generated binding table has been copied onto the node.
|
|
758
|
-
if (!__ui_nodes[i].listItemFn) {
|
|
759
|
-
for (uint16_t b = 0; b < __ui_list_binding_count; b++) {
|
|
760
|
-
if (__ui_list_bindings[b].node == static_cast<uint16_t>(i)) {
|
|
761
|
-
__ui_nodes[i].listCountFn = __ui_list_bindings[b].countFn;
|
|
762
|
-
__ui_nodes[i].listItemFn = __ui_list_bindings[b].itemFn;
|
|
763
|
-
__ui_nodes[i].listTapFn = __ui_list_bindings[b].tapFn;
|
|
764
|
-
break;
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
}
|
|
768
|
-
if (!__ui_nodes[i].listItemFn) break;
|
|
769
|
-
int16_t bx = __ui_nodes[i].box.x;
|
|
770
|
-
int16_t by = drawY;
|
|
771
|
-
int16_t bw = __ui_nodes[i].box.w;
|
|
772
|
-
int16_t bh = __ui_nodes[i].box.h;
|
|
773
|
-
uint16_t ih = __ui_nodes[i].listItemHeight > 0 ? __ui_nodes[i].listItemHeight : 24;
|
|
774
|
-
uint16_t itemCount = __ui_nodes[i].listCount;
|
|
775
|
-
int16_t listScrollY = __ui_nodes[i].scrollY;
|
|
776
|
-
int16_t listContentH = __ui_nodes[i].contentHeight;
|
|
777
|
-
UI_COLOR_T clearCol = __ui_nodes[i].clearColor;
|
|
778
|
-
uint8_t listFullRepaint = 0;
|
|
779
|
-
// Render to a viewport-sized canvas so edge glyphs are naturally clipped.
|
|
780
|
-
// Treat a null buffer (failed internal malloc) as "no canvas" and retry —
|
|
781
|
-
// see __ui_node_canvas for the zombie-caching rationale.
|
|
782
|
-
// __ui_list_canvas is file-scoped so ui_release_canvas_state() can free it.
|
|
783
|
-
if (!__ui_list_canvas || !display_canvasBuffer(__ui_list_canvas) ||
|
|
784
|
-
display_canvasWidth(__ui_list_canvas) != bw || display_canvasHeight(__ui_list_canvas) != bh) {
|
|
785
|
-
display_deleteCanvas(__ui_list_canvas);
|
|
786
|
-
__ui_list_canvas_node = -1;
|
|
787
|
-
__ui_list_canvas = ui_create_canvas_best(bw, bh);
|
|
788
|
-
listFullRepaint = 1;
|
|
789
|
-
}
|
|
790
|
-
CuttlefishCanvas16* lc = __ui_list_canvas;
|
|
791
|
-
if (!lc || !display_canvasBuffer(lc)) {
|
|
792
|
-
// Viewport canvas won't allocate (no PSRAM / over SRAM budget on a
|
|
793
|
-
// full-width list). Fall back to the band renderer: composite the
|
|
794
|
-
// visible rows into the ~10KB band canvas strip-by-strip, pushing
|
|
795
|
-
// each band tear-free. Mirrors the generic scroll path's Mode B →
|
|
796
|
-
// ui_render_scroll_bands fallback. If even the band canvas fails,
|
|
797
|
-
// give up (render nothing) — same as the old break.
|
|
798
|
-
if (ui_render_list_bands(static_cast<uint16_t>(i))) {
|
|
799
|
-
return 1; // band path handled push + decoration + dirty-clear
|
|
800
|
-
}
|
|
801
|
-
// Last-resort visible fallback: if even the small band canvas cannot
|
|
802
|
-
// allocate, draw the fully visible rows in one target transaction
|
|
803
|
-
// rather than leaving a black/empty list. This path is only reached
|
|
804
|
-
// under extreme memory pressure; normal renders remain atomic canvas
|
|
805
|
-
// pushes.
|
|
806
|
-
if (ui_render_list_direct(static_cast<uint16_t>(i))) {
|
|
807
|
-
__ui_nodes[i].box.x = origBoxX;
|
|
808
|
-
__ui_nodes[i].box.y = origBoxY;
|
|
809
|
-
return 1;
|
|
810
|
-
}
|
|
811
|
-
break;
|
|
812
|
-
}
|
|
813
|
-
if (__ui_list_canvas_node != static_cast<int16_t>(i)) listFullRepaint = 1;
|
|
814
|
-
int16_t repaintY = 0;
|
|
815
|
-
int16_t repaintH = bh;
|
|
816
|
-
int16_t deltaY = listScrollY - __ui_nodes[i].lastPaintedScrollY;
|
|
817
|
-
int16_t absDelta = deltaY < 0 ? -deltaY : deltaY;
|
|
818
|
-
uint8_t canShiftList = (!listFullRepaint && deltaY != 0 && absDelta < bh);
|
|
819
|
-
if (canShiftList) {
|
|
820
|
-
ui_shift_container_canvas(lc, deltaY, clearCol, &repaintY, &repaintH);
|
|
821
|
-
} else {
|
|
822
|
-
display_canvasFillScreen(lc, clearCol);
|
|
823
|
-
repaintY = 0;
|
|
824
|
-
repaintH = bh;
|
|
825
|
-
}
|
|
826
|
-
// GFXcanvas text has no clipping. For shift-and-repair frames, draw row
|
|
827
|
-
// text into a strip-sized repair canvas first, then blit only that strip
|
|
828
|
-
// into the shifted list canvas. This prevents a 1-5px repair from
|
|
829
|
-
// repainting full glyphs across pixels that were already shifted.
|
|
830
|
-
CuttlefishCanvas16* listTextCanvas = lc;
|
|
831
|
-
int16_t listTextOffsetY = 0;
|
|
832
|
-
uint8_t drawingListRepair = 0;
|
|
833
|
-
if (canShiftList && repaintH > 0 && repaintH < bh) {
|
|
834
|
-
CuttlefishCanvas16* rc = ui_get_repair_canvas(bw, repaintH);
|
|
835
|
-
if (rc) {
|
|
836
|
-
display_canvasFillScreen(rc, clearCol);
|
|
837
|
-
listTextCanvas = rc;
|
|
838
|
-
listTextOffsetY = repaintY;
|
|
839
|
-
drawingListRepair = 1;
|
|
840
|
-
} else {
|
|
841
|
-
display_canvasFillScreen(lc, clearCol);
|
|
842
|
-
repaintY = 0;
|
|
843
|
-
repaintH = bh;
|
|
844
|
-
canShiftList = 0;
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
// Compute visible range for the repainted strip. Previously-rendered
|
|
848
|
-
// pixels are shifted in-place; only the exposed band needs new rows.
|
|
849
|
-
uint16_t first = (listScrollY + repaintY) / ih;
|
|
850
|
-
uint16_t last = (listScrollY + repaintY + repaintH - 1) / ih + 1;
|
|
851
|
-
if (itemCount > 0 && last >= itemCount) last = itemCount - 1;
|
|
852
|
-
// Draw each visible item (canvas-local coords: 0,0 = viewport top).
|
|
853
|
-
char listBuf[UI_TEXT_BUF + 1];
|
|
854
|
-
CuttlefishDisplayTarget* listPrevTarget = ui_display_get_target();
|
|
855
|
-
if (itemCount > 0 && repaintH > 0) {
|
|
856
|
-
for (uint16_t idx = first; idx <= last; idx++) {
|
|
857
|
-
int16_t itemY = static_cast<int16_t>(idx * ih) - listScrollY - listTextOffsetY;
|
|
858
|
-
__ui_nodes[i].listItemFn(idx, listBuf, UI_TEXT_BUF + 1);
|
|
859
|
-
listBuf[UI_TEXT_BUF] = 0;
|
|
860
|
-
ui_display_set_target((CuttlefishDisplayTarget*)listTextCanvas);
|
|
861
|
-
int16_t __ui_saved_off_x2 = __ui_draw_off_x;
|
|
862
|
-
int16_t __ui_saved_off_y2 = __ui_draw_off_y;
|
|
863
|
-
__ui_draw_off_x = 0;
|
|
864
|
-
__ui_draw_off_y = 0;
|
|
865
|
-
// Keep classic list glyphs in the canvas pixel buffer. Native
|
|
866
|
-
// CuttlefishGFX text can disappear when copied from a canvas to
|
|
867
|
-
// the panel, while the explicit glyph path remains compositable.
|
|
868
|
-
ui_draw_list_text(listBuf, 4, itemY + static_cast<int16_t>(ih - 16) / 2,
|
|
869
|
-
__ui_nodes[i].fg, clearCol, 2, __ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing);
|
|
870
|
-
__ui_draw_off_x = __ui_saved_off_x2;
|
|
871
|
-
__ui_draw_off_y = __ui_saved_off_y2;
|
|
872
|
-
ui_display_set_target(listPrevTarget);
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
if (drawingListRepair) {
|
|
876
|
-
CuttlefishDisplayTarget* prevTarget = ui_display_get_target();
|
|
877
|
-
ui_display_set_target((CuttlefishDisplayTarget*)lc);
|
|
878
|
-
ui_draw_canvas_rect(listTextCanvas, 0, repaintY, bw, repaintH);
|
|
879
|
-
ui_display_set_target(prevTarget);
|
|
880
|
-
}
|
|
881
|
-
// Scrollbar (canvas-local coords).
|
|
882
|
-
if (listContentH > bh) {
|
|
883
|
-
int16_t tx = bw - 4;
|
|
884
|
-
uint16_t thumbH = static_cast<uint32_t>(bh) * bh / listContentH;
|
|
885
|
-
if (thumbH < 8) thumbH = 8;
|
|
886
|
-
int16_t maxScroll = listContentH - bh;
|
|
887
|
-
uint16_t thumbY = maxScroll > 0 ? static_cast<uint32_t>(bh - thumbH) * listScrollY / maxScroll : 0;
|
|
888
|
-
UI_COLOR_T dimFg = (UI_COLOR_T)((__ui_nodes[i].fg >> 1) & UI_DIM_MASK);
|
|
889
|
-
display_canvasFillRect(lc, tx, 0, 3, bh, dimFg);
|
|
890
|
-
display_canvasFillRect(lc, tx, thumbY, 3, thumbH, __ui_nodes[i].fg);
|
|
891
|
-
}
|
|
892
|
-
// Outset shadows are static decoration. Redrawing the hard shadow
|
|
893
|
-
// directly to the panel before every small scroll-frame creates a
|
|
894
|
-
// visible shadow-then-content intermediate state on SPI TFTs. Keep it
|
|
895
|
-
// for full list repaints, but skip it for shift-and-repair scrolls.
|
|
896
|
-
if (!drawingBufferedScroll && !__ui_fb && !canShiftList) {
|
|
897
|
-
ui_draw_shadow(i, by, 0);
|
|
898
|
-
}
|
|
899
|
-
// Standalone lists push directly. Lists inside a buffered scroll
|
|
900
|
-
// container must composite into that scroll canvas; their box has
|
|
901
|
-
// already been translated to canvas-local coordinates.
|
|
902
|
-
if (drawingBufferedScroll) {
|
|
903
|
-
ui_draw_canvas_rect(lc, bx, by, bw, bh);
|
|
904
|
-
} else {
|
|
905
|
-
ui_push_canvas_rect(lc, bx, by, bw, bh);
|
|
906
|
-
}
|
|
907
|
-
// Draw static decoration after the scrollable pixels are composited.
|
|
908
|
-
// Keeping border rows out of __ui_list_canvas prevents the cached
|
|
909
|
-
// shift step from dragging top/bottom border pixels through the list.
|
|
910
|
-
ui_draw_shadow(i, by, 1);
|
|
911
|
-
if (__ui_nodes[i].borderStyle != 0) {
|
|
912
|
-
ui_draw_node_border(i, bx, by, bColor);
|
|
913
|
-
}
|
|
914
|
-
ui_draw_node_outline(i, bx, by);
|
|
915
|
-
__ui_list_canvas_node = static_cast<int16_t>(i);
|
|
916
|
-
__ui_nodes[i].lastPaintedScrollY = listScrollY;
|
|
917
|
-
__ui_nodes[i].dirty = 0;
|
|
918
|
-
if (__ui_fb) {
|
|
919
|
-
UIRect listPaintRect;
|
|
920
|
-
ui_node_paint_rect(i, origBoxX, origBoxY, origBoxX, origBoxY,
|
|
921
|
-
static_cast<uint16_t>(bw), static_cast<uint16_t>(bh), &listPaintRect);
|
|
922
|
-
ui_fb_add_rect(listPaintRect.x, listPaintRect.y, listPaintRect.w, listPaintRect.h);
|
|
923
|
-
}
|
|
924
|
-
ui_display_set_target(__ui_draw_target);
|
|
925
|
-
__ui_nodes[i].box.x = origBoxX;
|
|
926
|
-
__ui_nodes[i].box.y = origBoxY;
|
|
927
|
-
return 1; // list handled canvas push, decoration, and coordinate restore
|
|
928
|
-
}
|
|
929
|
-
}
|
|
930
|
-
return 0;
|
|
931
|
-
}
|
|
932
|
-
|
|
933
|
-
// ── Band renderer: tear-free scroll compositing without a viewport canvas ──
|
|
934
|
-
// When a scroll container's viewport canvas won't allocate (no PSRAM, tight
|
|
935
|
-
// SRAM, or a viewport larger than the budget), the previous fallback
|
|
936
|
-
// (direct-full) painted each child straight to the display — one SPI
|
|
937
|
-
// transaction per primitive, visible as tearing during fast drags. The band
|
|
938
|
-
// renderer instead composes the whole visible subtree into a short horizontal
|
|
939
|
-
// band canvas (vw × UI_STRIP_BAND_HEIGHT, e.g. 320×16 ≈ 10KB at RGB565),
|
|
940
|
-
// pushes that band in one SPI transaction, then moves to the next band. Each
|
|
941
|
-
// band completes before it is pushed, so the panel never shows a half-drawn
|
|
942
|
-
// frame — eliminating tearing while keeping memory bounded regardless of
|
|
943
|
-
// program size (the canvas height is fixed; only width tracks the viewport).
|
|
944
|
-
//
|
|
945
|
-
// Returns 1 when the subtree was rendered and pushed (the caller must skip the
|
|
946
|
-
// normal direct-full child repaint). Returns 0 when the band canvas could not
|
|
947
|
-
// be allocated (caller falls back to direct-full).
|
|
948
|
-
// Shared band-canvas alloc helper. The three band renderers reuse the
|
|
949
|
-
// persistent __ui_band_canvas, reallocating only when the width changes so
|
|
950
|
-
// display_canvasWidth == w == stride (ui_push_canvas_rect's single-write fast
|
|
951
|
-
// path requires w == stride — a sub-width push takes a row-by-row path some
|
|
952
|
-
// ST7796S drivers mishandle). Returns the canvas (with a valid buffer) or null.
|
|
953
|
-
static inline CuttlefishCanvas16* ui_band_canvas_for_width(int16_t w) {
|
|
954
|
-
if (!__ui_band_canvas || !display_canvasBuffer(__ui_band_canvas) ||
|
|
955
|
-
display_canvasWidth(__ui_band_canvas) != w) {
|
|
956
|
-
display_deleteCanvas(__ui_band_canvas);
|
|
957
|
-
__ui_band_canvas = ui_create_canvas_best(w, UI_STRIP_BAND_HEIGHT);
|
|
958
|
-
}
|
|
959
|
-
CuttlefishCanvas16* band = __ui_band_canvas;
|
|
960
|
-
return (band && display_canvasBuffer(band)) ? band : nullptr;
|
|
961
|
-
}
|
|
962
|
-
static inline uint8_t ui_render_scroll_bands(uint16_t s) {
|
|
963
|
-
if (s >= __ui_node_count) return 0;
|
|
964
|
-
int16_t vw = __ui_nodes[s].box.w;
|
|
965
|
-
int16_t vh = __ui_nodes[s].box.h;
|
|
966
|
-
if (vw <= 0 || vh <= 0) return 0;
|
|
967
|
-
int16_t vox = __ui_nodes[s].box.x;
|
|
968
|
-
int16_t voy = __ui_nodes[s].box.y;
|
|
969
|
-
UI_COLOR_T scrollBg = __ui_nodes[s].hasBg ? __ui_nodes[s].bg : __ui_nodes[s].clearColor;
|
|
970
|
-
// One persistent band canvas, reused across bands and frames, reallocated
|
|
971
|
-
// only when the viewport width changes (so stride == vw for the single-write
|
|
972
|
-
// push fast path). See ui_band_canvas_for_width.
|
|
973
|
-
CuttlefishCanvas16* band = ui_band_canvas_for_width(vw);
|
|
974
|
-
if (!band) return 0;
|
|
975
|
-
int16_t bandH = display_canvasHeight(band);
|
|
976
|
-
// Build a compact list of descendants that can intersect the viewport once per
|
|
977
|
-
// scroll repaint. The old nested band×subtree walk repeated visibility, screen,
|
|
978
|
-
// and Y culling for every band; the candidate list keeps that work O(subtree)
|
|
979
|
-
// and each band only visits rows that can actually contribute pixels.
|
|
980
|
-
uint16_t subtreeEnd = __ui_nodes[s].subtreeEnd;
|
|
981
|
-
if (subtreeEnd > __ui_node_count) subtreeEnd = __ui_node_count;
|
|
982
|
-
// The scrollbar occupies the rightmost 4px gutter (tx = vw - 4, 3px wide).
|
|
983
|
-
// Rather than draw it separately at frame end (which made each band push erase
|
|
984
|
-
// the previous frame's scrollbar slice-by-slice, flashing it during scroll),
|
|
985
|
-
// composite this band's scrollbar slice into the gutter of the band canvas
|
|
986
|
-
// itself. Each band then pushes the FULL viewport width in one SPI transaction
|
|
987
|
-
// (the single-write fast path that works on every driver — a sub-width push
|
|
988
|
-
// forces a row-by-row path that some ST7796S drivers mishandle, showing only
|
|
989
|
-
// the first row). The scrollbar is thus part of each band's atomic push: no
|
|
990
|
-
// separate erase/redraw cycle, no flash, and the fast path is preserved.
|
|
991
|
-
int16_t contentW = vw > 4 ? static_cast<int16_t>(vw - 4) : vw;
|
|
992
|
-
// Scrollbar geometry (document/viewport coords): track spans [0, vh); thumb
|
|
993
|
-
// spans [thumbY, thumbY + thumbH). Both are 3px wide at canvas-x contentW.
|
|
994
|
-
uint16_t sbThumbH = 0;
|
|
995
|
-
uint16_t sbThumbY = 0;
|
|
996
|
-
uint8_t sbVisible = 0;
|
|
997
|
-
UI_COLOR_T sbTrackCol = (UI_COLOR_T)((__ui_nodes[s].fg >> 1) & UI_DIM_MASK);
|
|
998
|
-
UI_COLOR_T sbThumbCol = __ui_nodes[s].fg;
|
|
999
|
-
if (__ui_nodes[s].contentHeight > vh) {
|
|
1000
|
-
sbVisible = 1;
|
|
1001
|
-
sbThumbH = static_cast<uint32_t>(vh) * vh / __ui_nodes[s].contentHeight;
|
|
1002
|
-
if (sbThumbH < 8) sbThumbH = 8;
|
|
1003
|
-
// Clamp to vh so the (vh - sbThumbH) subtraction below can't go negative on
|
|
1004
|
-
// a degenerate sub-8px viewport (which would cast to a huge uint32_t and
|
|
1005
|
-
// place the thumb off-screen). ui_draw_scrollbar_direct has the same min-8
|
|
1006
|
-
// clamp; this extra guard keeps the band path's slice math safe.
|
|
1007
|
-
if (sbThumbH > static_cast<uint16_t>(vh)) sbThumbH = static_cast<uint16_t>(vh);
|
|
1008
|
-
int16_t maxScroll = __ui_nodes[s].contentHeight - vh;
|
|
1009
|
-
sbThumbY = maxScroll > 0 ? static_cast<uint32_t>(vh - sbThumbH) * __ui_nodes[s].scrollY / maxScroll : 0;
|
|
1010
|
-
}
|
|
1011
|
-
CuttlefishDisplayTarget* prevTarget = ui_display_get_target();
|
|
1012
|
-
if (!__ui_scroll_candidates) return 0;
|
|
1013
|
-
__ui_scroll_candidate_count = 0;
|
|
1014
|
-
for (uint16_t c = s + 1; c < subtreeEnd && __ui_scroll_candidate_count < __ui_node_count; c++) {
|
|
1015
|
-
if (!ui_is_effectively_visible(c) || __ui_nodes[c].screenId != __ui_active_screen) continue;
|
|
1016
|
-
UIScrollPaintCandidate& candidate = __ui_scroll_candidates[__ui_scroll_candidate_count++];
|
|
1017
|
-
candidate.node = c;
|
|
1018
|
-
candidate.screenY = ui_draw_y_for_node(c);
|
|
1019
|
-
candidate.faceH = __ui_nodes[c].box.h;
|
|
1020
|
-
}
|
|
1021
|
-
|
|
1022
|
-
// Top→bottom bands over the viewport.
|
|
1023
|
-
for (int16_t bandTop = 0; bandTop < vh; bandTop += bandH) {
|
|
1024
|
-
int16_t bandBot = bandTop + bandH;
|
|
1025
|
-
if (bandBot > vh) bandBot = vh;
|
|
1026
|
-
int16_t thisH = static_cast<int16_t>(bandBot - bandTop);
|
|
1027
|
-
display_canvasFillRect(band, 0, 0, contentW, bandH, scrollBg);
|
|
1028
|
-
ui_display_set_target(band);
|
|
1029
|
-
for (uint16_t candidateIdx = 0; candidateIdx < __ui_scroll_candidate_count; candidateIdx++) {
|
|
1030
|
-
uint16_t c = __ui_scroll_candidates[candidateIdx].node;
|
|
1031
|
-
int16_t screenY = __ui_scroll_candidates[candidateIdx].screenY;
|
|
1032
|
-
int16_t faceH = __ui_scroll_candidates[candidateIdx].faceH;
|
|
1033
|
-
// Cull nodes whose face does not intersect this band's Y range before
|
|
1034
|
-
// doing any expensive per-node work (AGENTS.md: clip before the inner
|
|
1035
|
-
// loop). A node fully above or below the band contributes no pixels.
|
|
1036
|
-
if (screenY + faceH <= voy + bandTop) continue;
|
|
1037
|
-
if (screenY >= voy + bandBot) continue;
|
|
1038
|
-
// Translate the node's box into band-local coordinates, mirroring the main
|
|
1039
|
-
// loop's drawingBufferedScroll setup: subtract the viewport origin and the
|
|
1040
|
-
// band's top row. ui_draw_y_for_node then re-derives the band-local Y
|
|
1041
|
-
// (document Y - scrollY - voy - bandTop) = (screenY - voy - bandTop). The
|
|
1042
|
-
// box.x translate below is read by ui_draw_x_for_node (which adds
|
|
1043
|
-
// transformOffsetX + pressedOffsetX), so it must be set before computing
|
|
1044
|
-
// drawX; the body needs box.x = drawX (including the pressed offset,
|
|
1045
|
-
// matching the main loop and ui_render_node_bands).
|
|
1046
|
-
int16_t origBoxX = __ui_nodes[c].box.x;
|
|
1047
|
-
int16_t origBoxY = __ui_nodes[c].box.y;
|
|
1048
|
-
__ui_nodes[c].box.x = static_cast<int16_t>(origBoxX - vox);
|
|
1049
|
-
__ui_nodes[c].box.y = static_cast<int16_t>(origBoxY - voy - bandTop);
|
|
1050
|
-
int16_t drawX = ui_draw_x_for_node(c);
|
|
1051
|
-
int16_t drawY = ui_draw_y_for_node(c);
|
|
1052
|
-
// Outset shadow at the rest position, under the face — parity with the
|
|
1053
|
-
// main dirty loop and ui_render_node_bands. This renderer omitted it,
|
|
1054
|
-
// so band-composited scroll viewports (viewport canvas won't allocate)
|
|
1055
|
-
// lost every shadow: theme --shadow-* tokens rendered in the preview
|
|
1056
|
-
// but not on device. Lists manage their own shadow elsewhere.
|
|
1057
|
-
if (__ui_nodes[c].kind != NODE_LIST) {
|
|
1058
|
-
__ui_nodes[c].box.x = ui_base_draw_x_for_node(c);
|
|
1059
|
-
ui_draw_shadow(c, ui_base_draw_y_for_node(c), 0);
|
|
1060
|
-
}
|
|
1061
|
-
__ui_nodes[c].box.x = drawX;
|
|
1062
|
-
uint8_t ts = __ui_nodes[c].textSize ? __ui_nodes[c].textSize : 2;
|
|
1063
|
-
uint16_t textMaxW = ui_node_text_max_width(c);
|
|
1064
|
-
uint16_t tw = 0;
|
|
1065
|
-
uint16_t th = 0;
|
|
1066
|
-
ui_node_text_layout_metrics(c, textMaxW, &tw, &th);
|
|
1067
|
-
uint16_t paintTextW = tw;
|
|
1068
|
-
uint16_t paintTextH = th;
|
|
1069
|
-
if (__ui_nodes[c].kind == NODE_TEXT || __ui_nodes[c].kind == NODE_SELECT) {
|
|
1070
|
-
uint16_t hInset = static_cast<uint16_t>(__ui_nodes[c].paddingLeft) + static_cast<uint16_t>(__ui_nodes[c].paddingRight) + static_cast<uint16_t>(__ui_nodes[c].borderWidth) * 2;
|
|
1071
|
-
uint16_t vInset = static_cast<uint16_t>(__ui_nodes[c].paddingTop) + static_cast<uint16_t>(__ui_nodes[c].paddingBottom) + static_cast<uint16_t>(__ui_nodes[c].borderWidth) * 2;
|
|
1072
|
-
paintTextW = static_cast<uint16_t>(tw + hInset);
|
|
1073
|
-
paintTextH = static_cast<uint16_t>(th + vInset);
|
|
1074
|
-
}
|
|
1075
|
-
if (__ui_nodes[c].kind == NODE_CHECK || __ui_nodes[c].kind == NODE_RADIO) {
|
|
1076
|
-
paintTextW = static_cast<uint16_t>(tw + 22);
|
|
1077
|
-
if (paintTextH < 16) paintTextH = 16;
|
|
1078
|
-
}
|
|
1079
|
-
const char* displayText = __ui_nodes[c].hasTextBinding
|
|
1080
|
-
? __ui_nodes[c].textBuffer
|
|
1081
|
-
: __ui_nodes[c].text;
|
|
1082
|
-
UI_COLOR_T bColor = __ui_nodes[c].borderColor ? __ui_nodes[c].borderColor : __ui_nodes[c].fg;
|
|
1083
|
-
UI_COLOR_T fillBg = __ui_nodes[c].bg;
|
|
1084
|
-
if (__ui_nodes[c].opacity < 100) {
|
|
1085
|
-
UI_COLOR_T backdrop = ui_parent_clear_color(c);
|
|
1086
|
-
bColor = ui_blend(bColor, backdrop, __ui_nodes[c].opacity);
|
|
1087
|
-
fillBg = ui_blend(__ui_nodes[c].bg, backdrop, __ui_nodes[c].opacity);
|
|
1088
|
-
}
|
|
1089
|
-
UINodeDrawCtx ctx;
|
|
1090
|
-
ctx.drawY = drawY;
|
|
1091
|
-
ctx.bColor = bColor;
|
|
1092
|
-
ctx.fillBg = fillBg;
|
|
1093
|
-
ctx.ts = ts;
|
|
1094
|
-
ctx.textMaxW = textMaxW;
|
|
1095
|
-
ctx.tw = tw;
|
|
1096
|
-
ctx.th = th;
|
|
1097
|
-
ctx.paintTextW = paintTextW;
|
|
1098
|
-
ctx.paintTextH = paintTextH;
|
|
1099
|
-
ctx.displayText = displayText;
|
|
1100
|
-
ctx.drawingBufferedScroll = 0;
|
|
1101
|
-
ctx.drawTarget = prevTarget;
|
|
1102
|
-
ctx.origBoxX = origBoxX;
|
|
1103
|
-
ctx.origBoxY = origBoxY;
|
|
1104
|
-
// The band draws onto a freshly cleared canvas every band, so the node's
|
|
1105
|
-
// incremental-paint caches (lastTextWidth/lastTextHeight, which carry
|
|
1106
|
-
// across frames for the main loop's NODE_TEXT/NODE_PROGRESS/NODE_RANGE
|
|
1107
|
-
// incremental redraws) are bogus here — reset to force a full repaint into
|
|
1108
|
-
// the blank band. Save/restore them around the draw so the band path is
|
|
1109
|
-
// transient: it must not corrupt the persistent incremental-redraw state
|
|
1110
|
-
// the main loop relies on if this container later transitions to canvas
|
|
1111
|
-
// mode (the values are also stale across the multiple bands a tall node
|
|
1112
|
-
// spans, so restoring the pre-band value keeps things consistent).
|
|
1113
|
-
int16_t savedLastTextW = __ui_nodes[c].lastTextWidth;
|
|
1114
|
-
uint16_t savedLastTextH = __ui_nodes[c].lastTextHeight;
|
|
1115
|
-
if (__ui_nodes[c].kind == NODE_PROGRESS || __ui_nodes[c].kind == NODE_RANGE) {
|
|
1116
|
-
__ui_nodes[c].lastTextWidth = -1;
|
|
1117
|
-
}
|
|
1118
|
-
__ui_nodes[c].lastTextHeight = 0;
|
|
1119
|
-
ui_draw_node_body(static_cast<int16_t>(c), &ctx);
|
|
1120
|
-
__ui_nodes[c].lastTextWidth = savedLastTextW;
|
|
1121
|
-
__ui_nodes[c].lastTextHeight = savedLastTextH;
|
|
1122
|
-
__ui_nodes[c].box.x = origBoxX;
|
|
1123
|
-
__ui_nodes[c].box.y = origBoxY;
|
|
1124
|
-
}
|
|
1125
|
-
ui_display_set_target(prevTarget);
|
|
1126
|
-
// Composite this band's scrollbar slice into the gutter. The track covers
|
|
1127
|
-
// every row; the thumb covers viewport-rows [sbThumbY, sbThumbY+sbThumbH).
|
|
1128
|
-
// Intersect that with this band's [bandTop, bandBot) range and fill the
|
|
1129
|
-
// overlapping canvas-local rows with the thumb color.
|
|
1130
|
-
if (sbVisible) {
|
|
1131
|
-
// Full gutter width (vw - contentW, normally 4px): child decorations
|
|
1132
|
-
// (outset shadows) poke past the content area and would survive as 1px
|
|
1133
|
-
// ticks in any uncovered edge column.
|
|
1134
|
-
display_canvasFillRect(band, contentW, 0, vw - contentW, thisH, sbTrackCol);
|
|
1135
|
-
int16_t thumbTop = static_cast<int16_t>(sbThumbY);
|
|
1136
|
-
int16_t thumbBot = static_cast<int16_t>(sbThumbY + sbThumbH);
|
|
1137
|
-
int16_t ovTop = thumbTop > bandTop ? thumbTop : bandTop;
|
|
1138
|
-
int16_t ovBot = thumbBot < bandBot ? thumbBot : bandBot;
|
|
1139
|
-
if (ovBot > ovTop) {
|
|
1140
|
-
display_canvasFillRect(band, contentW, static_cast<int16_t>(ovTop - bandTop), 3, static_cast<int16_t>(ovBot - ovTop), sbThumbCol);
|
|
1141
|
-
}
|
|
1142
|
-
} else {
|
|
1143
|
-
// No scrollbar (content fits) — clear the gutter to the scroll bg so the
|
|
1144
|
-
// previous screen's scrollbar pixels don't bleed through.
|
|
1145
|
-
display_canvasFillRect(band, contentW, 0, vw - contentW, thisH, scrollBg);
|
|
1146
|
-
}
|
|
1147
|
-
// Push the FULL viewport width in one transaction (single-write fast path:
|
|
1148
|
-
// w == stride). Each band — content + scrollbar slice — is fully rendered
|
|
1149
|
-
// before it touches the panel, eliminating both tearing and scrollbar flash.
|
|
1150
|
-
ui_push_canvas_rect(band, vox, static_cast<int16_t>(voy + bandTop), vw, thisH);
|
|
1151
|
-
}
|
|
1152
|
-
__ui_nodes[s].lastPaintedScrollY = __ui_nodes[s].scrollY;
|
|
1153
|
-
// The subtree was fully rendered this frame; the main loop's scroll-defer
|
|
1154
|
-
// path will skip these nodes (bufferedScrollDirectFull stays clear), and the
|
|
1155
|
-
// caller has already marked the owner non-dirty.
|
|
1156
|
-
for (uint16_t c = s + 1; c < subtreeEnd; c++) {
|
|
1157
|
-
__ui_nodes[c].dirty = 0;
|
|
1158
|
-
}
|
|
1159
|
-
return 1;
|
|
1160
|
-
}
|
|
1161
|
-
|
|
1162
|
-
// ── Whole-frame band composition (drawer slide frames) ────────────────────
|
|
1163
|
-
// Composes every visible active-screen node in draw order over a display
|
|
1164
|
-
// rect, one horizontal band at a time, pushing each band in a single
|
|
1165
|
-
// transaction. The drawer slide uses it over the union of the panel's old
|
|
1166
|
-
// and new paint rects: a mark-all-dirty slide step instead repainted the
|
|
1167
|
-
// whole screen with direct per-node clears (visible flashing on SPI TFTs,
|
|
1168
|
-
// and only ~3 steps fit inside the 180ms slide). Returns 0 when the band
|
|
1169
|
-
// canvas or draw order is unavailable (caller falls back to mark-all-dirty);
|
|
1170
|
-
// a rect fully outside the panel returns 1 (nothing to compose).
|
|
1171
|
-
static inline uint8_t ui_render_screen_bands(int16_t rx, int16_t ry, int16_t rw, int16_t rh) {
|
|
1172
|
-
if (rw <= 0 || rh <= 0) return 1;
|
|
1173
|
-
if (!ui_clip_rect_to_display_target(&rx, &ry, &rw, &rh)) return 1;
|
|
1174
|
-
if (!__ui_draw_order || !__ui_scroll_candidates) return 0;
|
|
1175
|
-
CuttlefishCanvas16* band = ui_band_canvas_for_width(rw);
|
|
1176
|
-
if (!band) return 0;
|
|
1177
|
-
int16_t bandH = display_canvasHeight(band);
|
|
1178
|
-
// Seed color: the active screen's background (the screen fill node paints
|
|
1179
|
-
// over it in draw order anyway; the seed keeps uncovered rows defined).
|
|
1180
|
-
UI_COLOR_T screenBg = static_cast<UI_COLOR_T>(0);
|
|
1181
|
-
if (__ui_active_screen_bg_node < __ui_node_count) {
|
|
1182
|
-
screenBg = __ui_nodes[__ui_active_screen_bg_node].hasBg
|
|
1183
|
-
? __ui_nodes[__ui_active_screen_bg_node].bg
|
|
1184
|
-
: __ui_nodes[__ui_active_screen_bg_node].clearColor;
|
|
1185
|
-
}
|
|
1186
|
-
// Draw-ordered candidate list once per composition (same shape as the
|
|
1187
|
-
// scroll band path); each band then only Y-culls against it.
|
|
1188
|
-
__ui_scroll_candidate_count = 0;
|
|
1189
|
-
for (uint16_t pass = 0; pass < __ui_node_count; pass++) {
|
|
1190
|
-
uint16_t c = __ui_draw_order[pass];
|
|
1191
|
-
if (__ui_nodes[c].screenId != __ui_active_screen) continue;
|
|
1192
|
-
if (!ui_is_effectively_visible(c)) continue;
|
|
1193
|
-
if (__ui_scroll_candidate_count >= __ui_node_count) break;
|
|
1194
|
-
UIScrollPaintCandidate& cand = __ui_scroll_candidates[__ui_scroll_candidate_count++];
|
|
1195
|
-
cand.node = c;
|
|
1196
|
-
cand.screenY = ui_draw_y_for_node(c);
|
|
1197
|
-
cand.faceH = __ui_nodes[c].box.h;
|
|
1198
|
-
// Scrolled children clip to their scroll viewport (CSS overflow): a
|
|
1199
|
-
// compose over a region touching the viewport edge must not paint their
|
|
1200
|
-
// faces at absolute positions beyond it. The initial whole-screen compose
|
|
1201
|
-
// left stale button rows below the fold, and composes over the header
|
|
1202
|
-
// region smeared scrolled content into the header — bars of previously
|
|
1203
|
-
// scrolled pixels that nothing repaints. The viewport rect is the scroll
|
|
1204
|
-
// ancestor's own draw position (its scrollY applies to children, not to
|
|
1205
|
-
// itself).
|
|
1206
|
-
int16_t san = ui_scroll_ancestor_for_node(c);
|
|
1207
|
-
if (san >= 0 && __ui_nodes[san].box.h > 0 && __ui_nodes[san].box.w > 0) {
|
|
1208
|
-
cand.clipTop = ui_draw_y_for_node(static_cast<uint16_t>(san));
|
|
1209
|
-
cand.clipBottom = static_cast<int16_t>(cand.clipTop + __ui_nodes[san].box.h);
|
|
1210
|
-
cand.clipLeft = ui_draw_x_for_node(static_cast<uint16_t>(san));
|
|
1211
|
-
cand.clipRight = static_cast<int16_t>(cand.clipLeft + __ui_nodes[san].box.w);
|
|
1212
|
-
} else {
|
|
1213
|
-
cand.clipTop = -32767;
|
|
1214
|
-
cand.clipBottom = 32767;
|
|
1215
|
-
cand.clipLeft = -32767;
|
|
1216
|
-
cand.clipRight = 32767;
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
CuttlefishDisplayTarget* prevTarget = ui_display_get_target();
|
|
1220
|
-
for (int16_t bandTop = 0; bandTop < rh; bandTop += bandH) {
|
|
1221
|
-
int16_t bandBot = static_cast<int16_t>(bandTop + bandH);
|
|
1222
|
-
if (bandBot > rh) bandBot = rh;
|
|
1223
|
-
int16_t thisH = static_cast<int16_t>(bandBot - bandTop);
|
|
1224
|
-
display_canvasFillRect(band, 0, 0, rw, thisH, screenBg);
|
|
1225
|
-
ui_display_set_target(band);
|
|
1226
|
-
for (uint16_t ci = 0; ci < __ui_scroll_candidate_count; ci++) {
|
|
1227
|
-
uint16_t c = __ui_scroll_candidates[ci].node;
|
|
1228
|
-
int16_t screenY = __ui_scroll_candidates[ci].screenY;
|
|
1229
|
-
int16_t faceH = __ui_scroll_candidates[ci].faceH;
|
|
1230
|
-
// Clip before the per-node work (AGENTS.md): skip nodes whose face
|
|
1231
|
-
// does not intersect this band's Y range.
|
|
1232
|
-
if (screenY + faceH <= ry + bandTop) continue;
|
|
1233
|
-
if (screenY >= ry + bandBot) continue;
|
|
1234
|
-
int16_t origBoxX = __ui_nodes[c].box.x;
|
|
1235
|
-
int16_t origBoxY = __ui_nodes[c].box.y;
|
|
1236
|
-
int16_t origBoxW = __ui_nodes[c].box.w;
|
|
1237
|
-
int16_t origBoxH = __ui_nodes[c].box.h;
|
|
1238
|
-
__ui_nodes[c].box.x = static_cast<int16_t>(origBoxX - rx);
|
|
1239
|
-
__ui_nodes[c].box.y = static_cast<int16_t>(origBoxY - ry - bandTop);
|
|
1240
|
-
// Clamp the face to the scroll viewport (band-local coords). The
|
|
1241
|
-
//
|
|
1242
|
-
//
|
|
1243
|
-
//
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
__ui_nodes[c].box.
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
int16_t
|
|
1281
|
-
int16_t
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
uint16_t
|
|
1293
|
-
|
|
1294
|
-
uint16_t
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
ctx
|
|
1319
|
-
ctx.
|
|
1320
|
-
ctx.
|
|
1321
|
-
ctx.
|
|
1322
|
-
ctx.
|
|
1323
|
-
ctx.
|
|
1324
|
-
ctx.
|
|
1325
|
-
ctx.
|
|
1326
|
-
ctx.
|
|
1327
|
-
ctx.
|
|
1328
|
-
ctx.
|
|
1329
|
-
ctx.
|
|
1330
|
-
ctx.
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
//
|
|
1334
|
-
//
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
__ui_nodes[c].
|
|
1343
|
-
|
|
1344
|
-
__ui_nodes[c].
|
|
1345
|
-
__ui_nodes[c].
|
|
1346
|
-
__ui_nodes[c].box.
|
|
1347
|
-
__ui_nodes[c].box.
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
}
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
//
|
|
1358
|
-
//
|
|
1359
|
-
//
|
|
1360
|
-
//
|
|
1361
|
-
//
|
|
1362
|
-
//
|
|
1363
|
-
//
|
|
1364
|
-
//
|
|
1365
|
-
//
|
|
1366
|
-
//
|
|
1367
|
-
//
|
|
1368
|
-
//
|
|
1369
|
-
//
|
|
1370
|
-
//
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
//
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
int16_t
|
|
1385
|
-
int16_t
|
|
1386
|
-
int16_t
|
|
1387
|
-
int16_t
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
uint16_t
|
|
1394
|
-
|
|
1395
|
-
uint16_t
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
int16_t
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
//
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
int16_t
|
|
1433
|
-
int16_t
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
//
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
ctx
|
|
1446
|
-
ctx.
|
|
1447
|
-
ctx.
|
|
1448
|
-
ctx.
|
|
1449
|
-
ctx.
|
|
1450
|
-
ctx.
|
|
1451
|
-
ctx.
|
|
1452
|
-
ctx.
|
|
1453
|
-
ctx.
|
|
1454
|
-
ctx.
|
|
1455
|
-
ctx.
|
|
1456
|
-
ctx.
|
|
1457
|
-
ctx.
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
//
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
__ui_nodes[i].
|
|
1473
|
-
__ui_nodes[i].
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
//
|
|
1485
|
-
//
|
|
1486
|
-
//
|
|
1487
|
-
//
|
|
1488
|
-
//
|
|
1489
|
-
//
|
|
1490
|
-
//
|
|
1491
|
-
//
|
|
1492
|
-
//
|
|
1493
|
-
//
|
|
1494
|
-
//
|
|
1495
|
-
//
|
|
1496
|
-
//
|
|
1497
|
-
//
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
int16_t
|
|
1504
|
-
int16_t
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
int16_t
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
//
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
int16_t
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
uint16_t
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
//
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
int16_t
|
|
1591
|
-
int16_t
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
__ui_nodes[i].
|
|
1613
|
-
__ui_nodes[i].
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
//
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
int16_t
|
|
1625
|
-
int16_t
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
//
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
uint16_t
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
if (
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
}
|
|
1
|
+
// Slice of the C++ runtime header.
|
|
2
|
+
// The per-node kind switch, extracted into a callable function so the
|
|
3
|
+
// strip/band renderer (ui_render_scroll_bands) can compose scroll-subtree
|
|
4
|
+
// nodes into a band canvas without duplicating ~700 lines of kind-specific
|
|
5
|
+
// draw code.
|
|
6
|
+
//
|
|
7
|
+
// The caller fills a UINodeDrawCtx with the computed draw state (drawY, text
|
|
8
|
+
// metrics, colors, text source) and invokes ui_draw_node_body. Both the main
|
|
9
|
+
// dirty-node draw loop (dirty-draw-phase.ts) and the band renderer set the ctx
|
|
10
|
+
// up the same way; the difference is only what __ui_gfx points at (display /
|
|
11
|
+
// framebuffer / scroll canvas / band canvas) and how the node's box coords are
|
|
12
|
+
// translated, both of which the caller handles before calling this.
|
|
13
|
+
//
|
|
14
|
+
// The NODE_LIST case is special: it manages its own canvas push, static
|
|
15
|
+
// decoration, coordinate restore, dirty-clear, and then returns 1 (a sentinel
|
|
16
|
+
// meaning "I fully handled this node — skip the caller's post-switch
|
|
17
|
+
// epilogue"). Every other case returns 0 and the caller runs its epilogue
|
|
18
|
+
// (outline, paint-canvas push, coordinate restore, dirty-clear, refresh rect).
|
|
19
|
+
// This return-sentinel preserves the original 'continue;' the list case used
|
|
20
|
+
// to short-circuit the rest of the per-node loop body.
|
|
21
|
+
//
|
|
22
|
+
// See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
|
|
23
|
+
export function emitNodeDrawBody(): string {
|
|
24
|
+
return `
|
|
25
|
+
// Computed draw state for one node, passed into ui_draw_node_body so it can be
|
|
26
|
+
// called from both the main dirty-node loop and the strip/band renderer.
|
|
27
|
+
struct UINodeDrawCtx {
|
|
28
|
+
int16_t drawY; // node's screen-space draw Y (scroll-adjusted)
|
|
29
|
+
UI_COLOR_T bColor; // border color (borderColor or fg, opacity-blended)
|
|
30
|
+
UI_COLOR_T fillBg; // fill background (bg, opacity-blended toward parent)
|
|
31
|
+
uint8_t ts; // text size
|
|
32
|
+
uint16_t textMaxW; // max text width (node box content width)
|
|
33
|
+
uint16_t tw; // laid-out text width
|
|
34
|
+
uint16_t th; // laid-out text height
|
|
35
|
+
uint16_t paintTextW; // paint-rect text width (tw + insets)
|
|
36
|
+
uint16_t paintTextH; // paint-rect text height (th + insets)
|
|
37
|
+
const char* displayText; // text source (binding buffer or flash literal)
|
|
38
|
+
// Call-site draw state the NODE_LIST case reads. These mirror the main draw
|
|
39
|
+
// loop's locals; the band renderer sets them to its own (target = band canvas,
|
|
40
|
+
// drawingBufferedScroll = 0, origBox = the node's untranslated box).
|
|
41
|
+
uint8_t drawingBufferedScroll; // 1 when drawing into the buffered scroll canvas
|
|
42
|
+
CuttlefishDisplayTarget* drawTarget; // __ui_draw_target (framebuffer or display)
|
|
43
|
+
int16_t origBoxX; // node's untranslated box.x (restored after draw)
|
|
44
|
+
int16_t origBoxY; // node's untranslated box.y (restored after draw)
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Returns 1 when the node fully handled its own canvas push, decoration,
|
|
48
|
+
// coordinate restore, and dirty-clear (NODE_LIST). Returns 0 otherwise, in
|
|
49
|
+
// which case the caller is responsible for the post-switch epilogue.
|
|
50
|
+
//
|
|
51
|
+
// The ctx parameter is typed const void* (cast back to UINodeDrawCtx* below):
|
|
52
|
+
// the emitter auto-inserts a forward declaration of every function near the
|
|
53
|
+
// top of the program, BEFORE this struct is defined, so a struct-typed
|
|
54
|
+
// parameter makes that generated prototype fail to compile
|
|
55
|
+
// ("'UINodeDrawCtx' does not name a type"). Primitive-only parameters keep
|
|
56
|
+
// the auto-generated prototype valid; call sites still pass &ctx, which
|
|
57
|
+
// converts implicitly to const void*.
|
|
58
|
+
static inline uint8_t ui_draw_node_body(int16_t i, const void* rawCtx) {
|
|
59
|
+
const UINodeDrawCtx* ctx = static_cast<const UINodeDrawCtx*>(rawCtx);
|
|
60
|
+
int16_t drawY = ctx->drawY;
|
|
61
|
+
UI_COLOR_T bColor = ctx->bColor;
|
|
62
|
+
UI_COLOR_T fillBg = ctx->fillBg;
|
|
63
|
+
uint8_t ts = ctx->ts;
|
|
64
|
+
uint16_t textMaxW = ctx->textMaxW;
|
|
65
|
+
uint16_t tw = ctx->tw;
|
|
66
|
+
uint16_t th = ctx->th;
|
|
67
|
+
uint16_t paintTextW = ctx->paintTextW;
|
|
68
|
+
uint16_t paintTextH = ctx->paintTextH;
|
|
69
|
+
const char* displayText = ctx->displayText;
|
|
70
|
+
uint8_t drawingBufferedScroll = ctx->drawingBufferedScroll;
|
|
71
|
+
CuttlefishDisplayTarget* __ui_draw_target = ctx->drawTarget;
|
|
72
|
+
int16_t origBoxX = ctx->origBoxX;
|
|
73
|
+
int16_t origBoxY = ctx->origBoxY;
|
|
74
|
+
switch (__ui_nodes[i].kind) {
|
|
75
|
+
case NODE_FILL:
|
|
76
|
+
if (__ui_nodes[i].gradientEnabled > 0) {
|
|
77
|
+
ui_draw_gradient_fill(i, drawY);
|
|
78
|
+
} else if (__ui_nodes[i].borderRadius > 0 && __ui_nodes[i].hasBg) {
|
|
79
|
+
int16_t fillW = ui_rotated_face_w(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
80
|
+
int16_t fillH = ui_rotated_face_h(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
81
|
+
ui_display_fill_round_rect(__ui_nodes[i].box.x, drawY, fillW, fillH, __ui_nodes[i].borderRadius, fillBg);
|
|
82
|
+
} else if (__ui_nodes[i].hasBg) {
|
|
83
|
+
int16_t fillW = ui_rotated_face_w(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
84
|
+
int16_t fillH = ui_rotated_face_h(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
85
|
+
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, fillW, fillH, fillBg);
|
|
86
|
+
}
|
|
87
|
+
ui_draw_shadow(i, drawY, 1);
|
|
88
|
+
if (__ui_nodes[i].borderStyle != 0) {
|
|
89
|
+
UI_COLOR_T bColor = __ui_nodes[i].borderColor ? __ui_nodes[i].borderColor : __ui_nodes[i].fg;
|
|
90
|
+
int16_t borderW = ui_rotated_face_w(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
91
|
+
int16_t borderH = ui_rotated_face_h(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
92
|
+
ui_draw_rect_outline(__ui_nodes[i].box.x, drawY, borderW, borderH,
|
|
93
|
+
__ui_nodes[i].borderRadius, __ui_nodes[i].borderStyle, __ui_nodes[i].borderWidth, bColor);
|
|
94
|
+
}
|
|
95
|
+
break;
|
|
96
|
+
case NODE_TEXT:
|
|
97
|
+
{
|
|
98
|
+
int16_t insetL = static_cast<int16_t>(__ui_nodes[i].borderWidth) + static_cast<int16_t>(__ui_nodes[i].paddingLeft);
|
|
99
|
+
int16_t insetR = static_cast<int16_t>(__ui_nodes[i].borderWidth) + static_cast<int16_t>(__ui_nodes[i].paddingRight);
|
|
100
|
+
int16_t insetT = static_cast<int16_t>(__ui_nodes[i].borderWidth) + static_cast<int16_t>(__ui_nodes[i].paddingTop);
|
|
101
|
+
int16_t textX = __ui_nodes[i].box.x + insetL;
|
|
102
|
+
int16_t textY = drawY + insetT;
|
|
103
|
+
int16_t textW = static_cast<int16_t>(__ui_nodes[i].box.w) - insetL - insetR;
|
|
104
|
+
if (textW < 1) textW = 1;
|
|
105
|
+
uint8_t textBoxPainted = 0;
|
|
106
|
+
if (__ui_nodes[i].gradientEnabled > 0) {
|
|
107
|
+
ui_draw_gradient_fill(i, drawY);
|
|
108
|
+
textBoxPainted = 1;
|
|
109
|
+
} else if (__ui_nodes[i].borderRadius > 0 && __ui_nodes[i].hasBg) {
|
|
110
|
+
ui_display_fill_round_rect(__ui_nodes[i].box.x, drawY,
|
|
111
|
+
__ui_nodes[i].box.w, __ui_nodes[i].box.h, __ui_nodes[i].borderRadius, fillBg);
|
|
112
|
+
textBoxPainted = 1;
|
|
113
|
+
} else if (__ui_nodes[i].hasBg) {
|
|
114
|
+
ui_display_fill_rect(__ui_nodes[i].box.x, drawY,
|
|
115
|
+
__ui_nodes[i].box.w, __ui_nodes[i].box.h, fillBg);
|
|
116
|
+
textBoxPainted = 1;
|
|
117
|
+
}
|
|
118
|
+
{
|
|
119
|
+
uint16_t clearW = __ui_nodes[i].box.w;
|
|
120
|
+
int16_t paintedTextW = static_cast<int16_t>(__ui_nodes[i].lastTextWidth) + insetL + insetR;
|
|
121
|
+
if (__ui_nodes[i].lastTextWidth > 0 && paintedTextW > static_cast<int16_t>(clearW)) {
|
|
122
|
+
clearW = static_cast<uint16_t>(paintedTextW);
|
|
123
|
+
}
|
|
124
|
+
uint16_t paddedTw = static_cast<uint16_t>(static_cast<int16_t>(tw) + insetL + insetR);
|
|
125
|
+
if (paddedTw > clearW) clearW = paddedTw;
|
|
126
|
+
// overflow:hidden/scroll: never clear past the node's own box. A nowrap
|
|
127
|
+
// line wider than its box would otherwise erase the parent's border.
|
|
128
|
+
if (__ui_nodes[i].scrollable) clearW = __ui_nodes[i].box.w;
|
|
129
|
+
uint16_t clearH = __ui_nodes[i].box.h;
|
|
130
|
+
int16_t paintedTextH = static_cast<int16_t>(__ui_nodes[i].lastTextHeight) + insetT + static_cast<int16_t>(__ui_nodes[i].paddingBottom) + static_cast<int16_t>(__ui_nodes[i].borderWidth);
|
|
131
|
+
if (__ui_nodes[i].lastTextHeight > 0 && paintedTextH > static_cast<int16_t>(clearH)) {
|
|
132
|
+
clearH = static_cast<uint16_t>(paintedTextH);
|
|
133
|
+
}
|
|
134
|
+
uint16_t paddedTh = static_cast<uint16_t>(static_cast<int16_t>(th) + insetT + static_cast<int16_t>(__ui_nodes[i].paddingBottom) + static_cast<int16_t>(__ui_nodes[i].borderWidth));
|
|
135
|
+
if (paddedTh > clearH) clearH = paddedTh;
|
|
136
|
+
// Dynamic transparent text still needs a clear, otherwise old glyph
|
|
137
|
+
// pixels accumulate when only this text node is dirty.
|
|
138
|
+
UI_COLOR_T clearCol = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor;
|
|
139
|
+
// Blend the clear toward the backdrop by opacity so a translucent text
|
|
140
|
+
// node (inherited from an opacity:<1 parent) doesn't repaint a solid
|
|
141
|
+
// block of its parent's fill around the glyphs.
|
|
142
|
+
if (__ui_nodes[i].opacity < 100) {
|
|
143
|
+
clearCol = ui_blend(clearCol, ui_parent_clear_color(i), __ui_nodes[i].opacity);
|
|
144
|
+
}
|
|
145
|
+
if (!textBoxPainted) {
|
|
146
|
+
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, clearW, clearH, clearCol);
|
|
147
|
+
}
|
|
148
|
+
__ui_nodes[i].lastTextWidth = tw;
|
|
149
|
+
__ui_nodes[i].lastTextHeight = th;
|
|
150
|
+
}
|
|
151
|
+
ui_draw_shadow(i, drawY, 1);
|
|
152
|
+
if (__ui_nodes[i].borderStyle != 0) {
|
|
153
|
+
ui_draw_node_border(i, __ui_nodes[i].box.x, drawY, bColor);
|
|
154
|
+
}
|
|
155
|
+
// Rich-text (inline runs): draw from precomputed geometry instead of the
|
|
156
|
+
// single-string wrapped path. Geometry is baked at transpile time; the
|
|
157
|
+
// runtime does not re-wrap.
|
|
158
|
+
if (__ui_nodes[i].runCount > 0) {
|
|
159
|
+
UI_COLOR_T richTextBg;
|
|
160
|
+
if (__ui_nodes[i].opacity < 100) {
|
|
161
|
+
uint16_t p = __ui_nodes[i].parent;
|
|
162
|
+
UI_COLOR_T source = (p != UI_NO_PARENT && __ui_nodes[p].hasBg) ? __ui_nodes[p].bg
|
|
163
|
+
: (__ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor);
|
|
164
|
+
richTextBg = ui_blend(source, __ui_nodes[i].clearColor, __ui_nodes[i].opacity);
|
|
165
|
+
} else {
|
|
166
|
+
richTextBg = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : ui_parent_clear_color(i);
|
|
167
|
+
}
|
|
168
|
+
if (__ui_nodes[i].textShadowCount > 0) {
|
|
169
|
+
UI_COLOR_T tsClear = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor;
|
|
170
|
+
uint32_t tsCol = ui_blend(__ui_nodes[i].textShadowColor, tsClear, __ui_nodes[i].textShadowAlpha);
|
|
171
|
+
// Shadow pass: draw the rich block in the shadow color at the offset.
|
|
172
|
+
// (Per-segment shadow color is approximated by drawing the whole
|
|
173
|
+
// block once in tsCol.)
|
|
174
|
+
ui_draw_rich_text(i,
|
|
175
|
+
textX + __ui_nodes[i].textShadowOffsetX,
|
|
176
|
+
textY + __ui_nodes[i].textShadowOffsetY,
|
|
177
|
+
tsClear, __ui_nodes[i].fontAntialias, 1, tsCol, static_cast<uint16_t>(textW));
|
|
178
|
+
}
|
|
179
|
+
ui_draw_rich_text(i, textX, textY, richTextBg, __ui_nodes[i].fontAntialias, 0, 0, static_cast<uint16_t>(textW));
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
{
|
|
183
|
+
// Text shadow: draw the text in the shadow color at the offset first.
|
|
184
|
+
UI_COLOR_T tsClear = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor;
|
|
185
|
+
if (__ui_nodes[i].textShadowCount > 0) {
|
|
186
|
+
uint32_t tsCol = ui_blend(__ui_nodes[i].textShadowColor, tsClear, __ui_nodes[i].textShadowAlpha);
|
|
187
|
+
ui_draw_wrapped_text(displayText,
|
|
188
|
+
textX + __ui_nodes[i].textShadowOffsetX,
|
|
189
|
+
textY + __ui_nodes[i].textShadowOffsetY,
|
|
190
|
+
static_cast<uint16_t>(textW), tsCol, tsCol, ts, __ui_nodes[i].fontAntialias,
|
|
191
|
+
__ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing, __ui_nodes[i].lineHeight,
|
|
192
|
+
__ui_nodes[i].whiteSpaceMode, __ui_nodes[i].textAlign, 0, __ui_nodes[i].textOverflow);
|
|
193
|
+
}
|
|
194
|
+
// Use the parent's clear color as the text background when the node
|
|
195
|
+
// has no own background. This makes Adafruit_GFX's opaque glyph-cell
|
|
196
|
+
// fill blend with the parent (instead of drawing solid fg blocks that
|
|
197
|
+
// overlap adjacent lines/elements). For AA text, fg != bg so the
|
|
198
|
+
// edge-detection path still runs correctly.
|
|
199
|
+
// Glyph-cell background. For a translucent text node sitting on a
|
|
200
|
+
// filled translucent parent, the glyph cells must match the parent's
|
|
201
|
+
// blended fill: blend565(parent.bg, backdrop, opacity). The node's own
|
|
202
|
+
// clearColor carries the backdrop (set by flatten), and opacity has
|
|
203
|
+
// already inherited from the parent. Use the parent's raw bg as the
|
|
204
|
+
// source so the glyph cells reproduce the parent's translucent fill.
|
|
205
|
+
UI_COLOR_T textBg;
|
|
206
|
+
if (__ui_nodes[i].opacity < 100) {
|
|
207
|
+
uint16_t p = __ui_nodes[i].parent;
|
|
208
|
+
UI_COLOR_T source = (p != UI_NO_PARENT && __ui_nodes[p].hasBg) ? __ui_nodes[p].bg
|
|
209
|
+
: (__ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor);
|
|
210
|
+
textBg = ui_blend(source, __ui_nodes[i].clearColor, __ui_nodes[i].opacity);
|
|
211
|
+
} else {
|
|
212
|
+
textBg = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : ui_parent_clear_color(i);
|
|
213
|
+
}
|
|
214
|
+
ui_draw_wrapped_text(displayText, textX, textY, static_cast<uint16_t>(textW),
|
|
215
|
+
__ui_nodes[i].fg, textBg, ts, __ui_nodes[i].fontAntialias,
|
|
216
|
+
__ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing, __ui_nodes[i].lineHeight,
|
|
217
|
+
__ui_nodes[i].whiteSpaceMode, __ui_nodes[i].textAlign, __ui_nodes[i].underline, __ui_nodes[i].textOverflow);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
break;
|
|
221
|
+
case NODE_BUTTON:
|
|
222
|
+
// HTML disabled: halve the drawn colors (web-like faded control).
|
|
223
|
+
if (__ui_nodes[i].disabled) {
|
|
224
|
+
fillBg = (fillBg >> 1) & UI_DIM_MASK;
|
|
225
|
+
bColor = (bColor >> 1) & UI_DIM_MASK;
|
|
226
|
+
}
|
|
227
|
+
if (__ui_nodes[i].borderRadius > 0 && __ui_nodes[i].hasBg)
|
|
228
|
+
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, fillBg);
|
|
229
|
+
else if (__ui_nodes[i].hasBg)
|
|
230
|
+
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, __ui_nodes[i].box.w, __ui_nodes[i].box.h, fillBg);
|
|
231
|
+
ui_draw_shadow(i, drawY, 1);
|
|
232
|
+
if (__ui_nodes[i].borderStyle != 0) {
|
|
233
|
+
ui_draw_node_border(i, __ui_nodes[i].box.x, drawY, bColor);
|
|
234
|
+
}
|
|
235
|
+
{
|
|
236
|
+
int16_t insetL = static_cast<int16_t>(__ui_nodes[i].borderWidth) + static_cast<int16_t>(__ui_nodes[i].paddingLeft);
|
|
237
|
+
int16_t insetR = static_cast<int16_t>(__ui_nodes[i].borderWidth) + static_cast<int16_t>(__ui_nodes[i].paddingRight);
|
|
238
|
+
int16_t insetT = static_cast<int16_t>(__ui_nodes[i].borderWidth) + static_cast<int16_t>(__ui_nodes[i].paddingTop);
|
|
239
|
+
int16_t insetB = static_cast<int16_t>(__ui_nodes[i].borderWidth) + static_cast<int16_t>(__ui_nodes[i].paddingBottom);
|
|
240
|
+
int16_t textX = __ui_nodes[i].box.x + insetL;
|
|
241
|
+
int16_t textY = drawY + insetT;
|
|
242
|
+
int16_t textW = static_cast<int16_t>(__ui_nodes[i].box.w) - insetL - insetR;
|
|
243
|
+
int16_t textH = static_cast<int16_t>(__ui_nodes[i].box.h) - insetT - insetB;
|
|
244
|
+
if (textW < 1) textW = 1;
|
|
245
|
+
if (textH < 1) textH = static_cast<int16_t>(th);
|
|
246
|
+
ui_draw_wrapped_text(displayText,
|
|
247
|
+
textX,
|
|
248
|
+
textY + (textH - static_cast<int16_t>(th)) / 2,
|
|
249
|
+
static_cast<uint16_t>(textW),
|
|
250
|
+
__ui_nodes[i].fg,
|
|
251
|
+
__ui_nodes[i].hasBg ? fillBg : ui_parent_clear_color(i),
|
|
252
|
+
ts, __ui_nodes[i].fontAntialias, __ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing,
|
|
253
|
+
__ui_nodes[i].lineHeight, __ui_nodes[i].whiteSpaceMode, __ui_nodes[i].textAlign, __ui_nodes[i].underline, __ui_nodes[i].textOverflow);
|
|
254
|
+
}
|
|
255
|
+
break;
|
|
256
|
+
case NODE_SELECT:
|
|
257
|
+
// <select>: a bordered, optionally rounded box with its current option
|
|
258
|
+
// text drawn VERTICALLY CENTERED (unlike NODE_TEXT, which top-aligns).
|
|
259
|
+
// Mirrors NODE_BUTTON's centering so the label sits mid-box when the
|
|
260
|
+
// control is enlarged for touch (min-height). The label is dynamic
|
|
261
|
+
// (auto-bound to the selected option's text), so clear the previous
|
|
262
|
+
// text rect before redrawing, like NODE_CHECK does.
|
|
263
|
+
{
|
|
264
|
+
uint16_t clearW = __ui_nodes[i].box.w;
|
|
265
|
+
if (__ui_nodes[i].lastTextWidth > 0 && __ui_nodes[i].lastTextWidth > static_cast<int16_t>(clearW)) {
|
|
266
|
+
clearW = static_cast<uint16_t>(__ui_nodes[i].lastTextWidth);
|
|
267
|
+
}
|
|
268
|
+
if (paintTextW > clearW) clearW = paintTextW;
|
|
269
|
+
uint16_t clearH = __ui_nodes[i].box.h;
|
|
270
|
+
if (__ui_nodes[i].lastTextHeight > 0 && __ui_nodes[i].lastTextHeight > static_cast<int16_t>(clearH)) {
|
|
271
|
+
clearH = static_cast<uint16_t>(__ui_nodes[i].lastTextHeight);
|
|
272
|
+
}
|
|
273
|
+
if (paintTextH > clearH) clearH = paintTextH;
|
|
274
|
+
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, clearW, clearH,
|
|
275
|
+
__ui_nodes[i].hasBg ? fillBg : ui_parent_clear_color(i));
|
|
276
|
+
__ui_nodes[i].lastTextWidth = paintTextW;
|
|
277
|
+
__ui_nodes[i].lastTextHeight = paintTextH;
|
|
278
|
+
}
|
|
279
|
+
if (__ui_nodes[i].borderRadius > 0 && __ui_nodes[i].hasBg)
|
|
280
|
+
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, fillBg);
|
|
281
|
+
else if (__ui_nodes[i].hasBg)
|
|
282
|
+
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, __ui_nodes[i].box.w, __ui_nodes[i].box.h, fillBg);
|
|
283
|
+
ui_draw_shadow(i, drawY, 1);
|
|
284
|
+
if (__ui_nodes[i].borderStyle != 0) {
|
|
285
|
+
ui_draw_node_border(i, __ui_nodes[i].box.x, drawY, bColor);
|
|
286
|
+
}
|
|
287
|
+
{
|
|
288
|
+
int16_t insetL = static_cast<int16_t>(__ui_nodes[i].borderWidth) + static_cast<int16_t>(__ui_nodes[i].paddingLeft);
|
|
289
|
+
int16_t insetR = static_cast<int16_t>(__ui_nodes[i].borderWidth) + static_cast<int16_t>(__ui_nodes[i].paddingRight);
|
|
290
|
+
int16_t insetT = static_cast<int16_t>(__ui_nodes[i].borderWidth) + static_cast<int16_t>(__ui_nodes[i].paddingTop);
|
|
291
|
+
int16_t insetB = static_cast<int16_t>(__ui_nodes[i].borderWidth) + static_cast<int16_t>(__ui_nodes[i].paddingBottom);
|
|
292
|
+
int16_t textX = __ui_nodes[i].box.x + insetL;
|
|
293
|
+
int16_t textY = drawY + insetT;
|
|
294
|
+
// The right end reserves 14px for the dropdown chevron — the label
|
|
295
|
+
// wraps against the reduced width, never under the chevron.
|
|
296
|
+
int16_t textW = static_cast<int16_t>(__ui_nodes[i].box.w) - insetL - insetR - 14;
|
|
297
|
+
int16_t textH = static_cast<int16_t>(__ui_nodes[i].box.h) - insetT - insetB;
|
|
298
|
+
if (textW < 1) textW = 1;
|
|
299
|
+
if (textH < 1) textH = static_cast<int16_t>(th);
|
|
300
|
+
ui_draw_wrapped_text(displayText,
|
|
301
|
+
textX,
|
|
302
|
+
textY + (textH - static_cast<int16_t>(th)) / 2,
|
|
303
|
+
static_cast<uint16_t>(textW),
|
|
304
|
+
__ui_nodes[i].fg,
|
|
305
|
+
__ui_nodes[i].hasBg ? fillBg : ui_parent_clear_color(i),
|
|
306
|
+
ts, __ui_nodes[i].fontAntialias, __ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing,
|
|
307
|
+
__ui_nodes[i].lineHeight, __ui_nodes[i].whiteSpaceMode, __ui_nodes[i].textAlign, __ui_nodes[i].underline, __ui_nodes[i].textOverflow);
|
|
308
|
+
// Dropdown chevron: a small solid v at the right end — the
|
|
309
|
+
// affordance that the control opens a menu of options.
|
|
310
|
+
{
|
|
311
|
+
int16_t chX = __ui_nodes[i].box.x + static_cast<int16_t>(__ui_nodes[i].box.w) - insetR - 11;
|
|
312
|
+
int16_t chY = drawY + (static_cast<int16_t>(__ui_nodes[i].box.h) - 5) / 2;
|
|
313
|
+
ui_display_fill_rect(chX, chY, 9, 1, __ui_nodes[i].fg);
|
|
314
|
+
ui_display_fill_rect(chX + 1, chY + 1, 7, 1, __ui_nodes[i].fg);
|
|
315
|
+
ui_display_fill_rect(chX + 2, chY + 2, 5, 1, __ui_nodes[i].fg);
|
|
316
|
+
ui_display_fill_rect(chX + 3, chY + 3, 3, 1, __ui_nodes[i].fg);
|
|
317
|
+
ui_display_fill_rect(chX + 4, chY + 4, 1, 1, __ui_nodes[i].fg);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
break;
|
|
321
|
+
case NODE_CHECK:
|
|
322
|
+
{
|
|
323
|
+
uint16_t clearW = __ui_nodes[i].box.w;
|
|
324
|
+
if (__ui_nodes[i].lastTextWidth > 0 && __ui_nodes[i].lastTextWidth > static_cast<int16_t>(clearW)) {
|
|
325
|
+
clearW = static_cast<uint16_t>(__ui_nodes[i].lastTextWidth);
|
|
326
|
+
}
|
|
327
|
+
if (paintTextW > clearW) clearW = paintTextW;
|
|
328
|
+
uint16_t clearH = __ui_nodes[i].box.h;
|
|
329
|
+
if (__ui_nodes[i].lastTextHeight > 0 && __ui_nodes[i].lastTextHeight > static_cast<int16_t>(clearH)) {
|
|
330
|
+
clearH = static_cast<uint16_t>(__ui_nodes[i].lastTextHeight);
|
|
331
|
+
}
|
|
332
|
+
if (paintTextH > clearH) clearH = paintTextH;
|
|
333
|
+
// border-radius > 0 (kit .switch pills): clear the full text rect to
|
|
334
|
+
// the backdrop, then paint the rounded box. Mirrors the preview's
|
|
335
|
+
// drawCheckNode pill path. The :checked pair (kit wires it to
|
|
336
|
+
// --primary/--primary-foreground) swaps the TRACK color when on.
|
|
337
|
+
if (__ui_nodes[i].borderRadius > 0 && __ui_nodes[i].hasBg) {
|
|
338
|
+
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, clearW, clearH,
|
|
339
|
+
ui_parent_clear_color(i));
|
|
340
|
+
ui_display_fill_round_rect(__ui_nodes[i].box.x, drawY,
|
|
341
|
+
__ui_nodes[i].box.w, __ui_nodes[i].box.h,
|
|
342
|
+
__ui_nodes[i].borderRadius,
|
|
343
|
+
(__ui_nodes[i].value && __ui_nodes[i].hasCheckedBg) ? __ui_nodes[i].checkedBg : fillBg);
|
|
344
|
+
} else {
|
|
345
|
+
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, clearW, clearH,
|
|
346
|
+
__ui_nodes[i].hasBg ? fillBg : ui_parent_clear_color(i));
|
|
347
|
+
}
|
|
348
|
+
__ui_nodes[i].lastTextWidth = paintTextW;
|
|
349
|
+
__ui_nodes[i].lastTextHeight = paintTextH;
|
|
350
|
+
}
|
|
351
|
+
{
|
|
352
|
+
// Pills inset the 16px indicator from the left edge so the knob
|
|
353
|
+
// doesn't touch the rounded end; plain checkboxes keep it flush.
|
|
354
|
+
// The knob SLIDES with state — left when off, right when on — the
|
|
355
|
+
// switch affordance (static jump; no travel animation).
|
|
356
|
+
int16_t knobSlide = __ui_nodes[i].borderRadius > 0
|
|
357
|
+
? static_cast<int16_t>(__ui_nodes[i].box.w) - 16 - 6
|
|
358
|
+
: 0;
|
|
359
|
+
if (knobSlide < 0) knobSlide = 0;
|
|
360
|
+
int16_t cbX = __ui_nodes[i].box.x
|
|
361
|
+
+ (__ui_nodes[i].borderRadius > 0 ? 3 : 0)
|
|
362
|
+
+ (__ui_nodes[i].value ? knobSlide : 0);
|
|
363
|
+
// Vertically center the 16px indicator within the box so a tall
|
|
364
|
+
// (touch-friendly) checkbox doesn't pin the indicator to the top.
|
|
365
|
+
// (box.h - 16) / 2 is 0 for the default 16px-tall box, so existing
|
|
366
|
+
// checkboxes render byte-identically.
|
|
367
|
+
int16_t cbY = drawY + (static_cast<int16_t>(__ui_nodes[i].box.h) - 16) / 2;
|
|
368
|
+
if (cbY < drawY) cbY = drawY;
|
|
369
|
+
if (__ui_nodes[i].value && __ui_nodes[i].borderRadius > 0) {
|
|
370
|
+
// Switch pill: the knob stays a knob — a solid circle when on, no
|
|
371
|
+
// checkbox square + checkmark. Same geometry as the radio dot.
|
|
372
|
+
// The knob carries the :checked color (primary-foreground).
|
|
373
|
+
ui_display_fill_circle(cbX + 8, cbY + 8, 7,
|
|
374
|
+
__ui_nodes[i].hasCheckedFg ? __ui_nodes[i].checkedFg : __ui_nodes[i].fg);
|
|
375
|
+
} else if (__ui_nodes[i].value) {
|
|
376
|
+
// Checked face carries the :checked background (primary), the
|
|
377
|
+
// checkmark its color (primary-foreground).
|
|
378
|
+
ui_display_fill_rect(cbX, cbY, 16, 16,
|
|
379
|
+
__ui_nodes[i].hasCheckedBg ? __ui_nodes[i].checkedBg : __ui_nodes[i].fg);
|
|
380
|
+
UI_COLOR_T inv = __ui_nodes[i].hasCheckedFg ? __ui_nodes[i].checkedFg
|
|
381
|
+
: (__ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor);
|
|
382
|
+
#ifdef UI_AA
|
|
383
|
+
{
|
|
384
|
+
// Draw the checkmark to a 16×16 AA canvas for smooth diagonals.
|
|
385
|
+
CuttlefishCanvas16* c = ui_aa_begin(16, 16, __ui_nodes[i].fg);
|
|
386
|
+
// First stroke: down-left (3,8 → 7,12)
|
|
387
|
+
if (c) {
|
|
388
|
+
ui_aa_line(c, 4.0f, 8.0f, 7.0f, 12.0f, inv);
|
|
389
|
+
ui_aa_line(c, 5.0f, 8.0f, 8.0f, 12.0f, inv);
|
|
390
|
+
// Second stroke: up-right (7,11 → 13,4)
|
|
391
|
+
ui_aa_line(c, 7.0f, 11.0f, 13.0f, 4.0f, inv);
|
|
392
|
+
ui_aa_line(c, 8.0f, 11.0f, 14.0f, 4.0f, inv);
|
|
393
|
+
ui_aa_push(c, cbX, cbY);
|
|
394
|
+
} else {
|
|
395
|
+
ui_display_draw_line(cbX + 3, cbY + 8, cbX + 7, cbY + 12, inv);
|
|
396
|
+
ui_display_draw_line(cbX + 4, cbY + 8, cbX + 8, cbY + 12, inv);
|
|
397
|
+
ui_display_draw_line(cbX + 7, cbY + 12, cbX + 13, cbY + 4, inv);
|
|
398
|
+
ui_display_draw_line(cbX + 8, cbY + 12, cbX + 14, cbY + 4, inv);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
#else
|
|
402
|
+
ui_display_draw_line(cbX + 3, cbY + 8, cbX + 7, cbY + 12, inv);
|
|
403
|
+
ui_display_draw_line(cbX + 4, cbY + 8, cbX + 8, cbY + 12, inv);
|
|
404
|
+
ui_display_draw_line(cbX + 3, cbY + 9, cbX + 7, cbY + 13, inv);
|
|
405
|
+
ui_display_draw_line(cbX + 7, cbY + 12, cbX + 13, cbY + 4, inv);
|
|
406
|
+
ui_display_draw_line(cbX + 8, cbY + 12, cbX + 14, cbY + 4, inv);
|
|
407
|
+
ui_display_draw_line(cbX + 7, cbY + 13, cbX + 13, cbY + 5, inv);
|
|
408
|
+
#endif
|
|
409
|
+
} else if (__ui_nodes[i].borderRadius > 0) {
|
|
410
|
+
// Off-state pill: hollow circular knob outline on the track.
|
|
411
|
+
ui_display_draw_circle(cbX + 8, cbY + 8, 7, __ui_nodes[i].fg);
|
|
412
|
+
} else {
|
|
413
|
+
ui_display_draw_rect(cbX, cbY, 16, 16, __ui_nodes[i].fg);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
// Center the label on the same baseline as the indicator: apply the
|
|
417
|
+
// indicator's vertical offset to the text origin too, so a tall box
|
|
418
|
+
// keeps the indicator + label aligned as a row rather than straddling
|
|
419
|
+
// the box top/bottom.
|
|
420
|
+
{
|
|
421
|
+
int16_t checkOff = (static_cast<int16_t>(__ui_nodes[i].box.h) - 16) / 2;
|
|
422
|
+
if (checkOff < 0) checkOff = 0;
|
|
423
|
+
ui_draw_wrapped_text(displayText, __ui_nodes[i].box.x + 22, drawY + checkOff, textMaxW,
|
|
424
|
+
__ui_nodes[i].fg, __ui_nodes[i].hasBg ? fillBg : ui_parent_clear_color(i),
|
|
425
|
+
ts, __ui_nodes[i].fontAntialias, __ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing,
|
|
426
|
+
__ui_nodes[i].lineHeight, __ui_nodes[i].whiteSpaceMode, 0, __ui_nodes[i].underline, __ui_nodes[i].textOverflow);
|
|
427
|
+
}
|
|
428
|
+
break;
|
|
429
|
+
case NODE_RADIO:
|
|
430
|
+
{
|
|
431
|
+
uint16_t clearW = __ui_nodes[i].box.w;
|
|
432
|
+
if (__ui_nodes[i].lastTextWidth > 0 && __ui_nodes[i].lastTextWidth > static_cast<int16_t>(clearW)) {
|
|
433
|
+
clearW = static_cast<uint16_t>(__ui_nodes[i].lastTextWidth);
|
|
434
|
+
}
|
|
435
|
+
if (paintTextW > clearW) clearW = paintTextW;
|
|
436
|
+
uint16_t clearH = __ui_nodes[i].box.h;
|
|
437
|
+
if (__ui_nodes[i].lastTextHeight > 0 && __ui_nodes[i].lastTextHeight > static_cast<int16_t>(clearH)) {
|
|
438
|
+
clearH = static_cast<uint16_t>(__ui_nodes[i].lastTextHeight);
|
|
439
|
+
}
|
|
440
|
+
if (paintTextH > clearH) clearH = paintTextH;
|
|
441
|
+
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, clearW, clearH,
|
|
442
|
+
__ui_nodes[i].hasBg ? fillBg : ui_parent_clear_color(i));
|
|
443
|
+
__ui_nodes[i].lastTextWidth = paintTextW;
|
|
444
|
+
__ui_nodes[i].lastTextHeight = paintTextH;
|
|
445
|
+
int16_t cbX = __ui_nodes[i].box.x;
|
|
446
|
+
// Vertically center the 16px indicator within the box (see NODE_CHECK).
|
|
447
|
+
int16_t cbY = drawY + (static_cast<int16_t>(__ui_nodes[i].box.h) - 16) / 2;
|
|
448
|
+
if (cbY < drawY) cbY = drawY;
|
|
449
|
+
#ifdef UI_AA
|
|
450
|
+
{
|
|
451
|
+
// Render the radio circle to a 16×16 AA canvas, then push.
|
|
452
|
+
UI_COLOR_T radioBg = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor;
|
|
453
|
+
// The selected ring carries the :checked background (primary).
|
|
454
|
+
UI_COLOR_T ringCol = (__ui_nodes[i].value && __ui_nodes[i].hasCheckedBg)
|
|
455
|
+
? __ui_nodes[i].checkedBg : __ui_nodes[i].fg;
|
|
456
|
+
CuttlefishCanvas16* c = ui_aa_begin(16, 16, radioBg);
|
|
457
|
+
if (c) {
|
|
458
|
+
if (__ui_nodes[i].value) {
|
|
459
|
+
ui_aa_fill_circle(c, 8, 8, 7.0f, ringCol);
|
|
460
|
+
ui_aa_fill_circle(c, 8, 8, 3.0f, radioBg);
|
|
461
|
+
} else {
|
|
462
|
+
ui_aa_circle(c, 8, 8, 7.0f, __ui_nodes[i].fg);
|
|
463
|
+
}
|
|
464
|
+
ui_aa_push(c, cbX, cbY);
|
|
465
|
+
} else if (__ui_nodes[i].value) {
|
|
466
|
+
ui_display_fill_circle(cbX + 8, cbY + 8, 7, ringCol);
|
|
467
|
+
ui_display_fill_circle(cbX + 8, cbY + 8, 3, radioBg);
|
|
468
|
+
} else {
|
|
469
|
+
ui_display_draw_circle(cbX + 8, cbY + 8, 7, __ui_nodes[i].fg);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
#else
|
|
473
|
+
if (__ui_nodes[i].value) {
|
|
474
|
+
ui_display_fill_circle(cbX + 8, cbY + 8, 7,
|
|
475
|
+
__ui_nodes[i].hasCheckedBg ? __ui_nodes[i].checkedBg : __ui_nodes[i].fg);
|
|
476
|
+
ui_display_fill_circle(cbX + 8, cbY + 8, 3, __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor);
|
|
477
|
+
} else {
|
|
478
|
+
ui_display_draw_circle(cbX + 8, cbY + 8, 7, __ui_nodes[i].fg);
|
|
479
|
+
}
|
|
480
|
+
#endif
|
|
481
|
+
}
|
|
482
|
+
// Center the label on the same baseline as the indicator (see NODE_CHECK).
|
|
483
|
+
{
|
|
484
|
+
int16_t radioOff = (static_cast<int16_t>(__ui_nodes[i].box.h) - 16) / 2;
|
|
485
|
+
if (radioOff < 0) radioOff = 0;
|
|
486
|
+
ui_draw_wrapped_text(displayText, __ui_nodes[i].box.x + 22, drawY + radioOff, textMaxW,
|
|
487
|
+
__ui_nodes[i].fg, __ui_nodes[i].hasBg ? fillBg : ui_parent_clear_color(i),
|
|
488
|
+
ts, __ui_nodes[i].fontAntialias, __ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing,
|
|
489
|
+
__ui_nodes[i].lineHeight, __ui_nodes[i].whiteSpaceMode, 0, __ui_nodes[i].underline, __ui_nodes[i].textOverflow);
|
|
490
|
+
}
|
|
491
|
+
break;
|
|
492
|
+
case NODE_PROGRESS:
|
|
493
|
+
// Progress bar: outline track + filled portion based on .value (0-100).
|
|
494
|
+
// Incremental redraw — only draws/clears the delta to avoid flashing.
|
|
495
|
+
{
|
|
496
|
+
int16_t bx = __ui_nodes[i].box.x;
|
|
497
|
+
int16_t by = drawY;
|
|
498
|
+
int16_t bw = __ui_nodes[i].box.w;
|
|
499
|
+
int16_t bh = __ui_nodes[i].box.h;
|
|
500
|
+
UI_COLOR_T bgCol = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor;
|
|
501
|
+
UI_COLOR_T fgCol = __ui_nodes[i].fg;
|
|
502
|
+
|
|
503
|
+
// On first draw (lastTextWidth < 0), draw everything.
|
|
504
|
+
// Otherwise incremental: only update the changed portion.
|
|
505
|
+
uint8_t pct = __ui_constrain(__ui_nodes[i].value, 0, 100);
|
|
506
|
+
int16_t fillW = (static_cast<int32_t>(bw - 2) * pct) / 100;
|
|
507
|
+
int16_t prevW = __ui_nodes[i].lastTextWidth; // reused as previous fill width
|
|
508
|
+
|
|
509
|
+
if (prevW < 0) {
|
|
510
|
+
// Full redraw: outline + background + fill
|
|
511
|
+
ui_display_draw_rect(bx, by, bw, bh, fgCol);
|
|
512
|
+
ui_display_fill_rect(bx + 1, by + 1, bw - 2, bh - 2, bgCol);
|
|
513
|
+
if (fillW > 0) {
|
|
514
|
+
ui_display_fill_rect(bx + 1, by + 1, fillW, bh - 2, fgCol);
|
|
515
|
+
}
|
|
516
|
+
} else if (fillW > prevW) {
|
|
517
|
+
// Value increased: draw new fill segment on top (no clear needed)
|
|
518
|
+
ui_display_fill_rect(bx + 1 + prevW, by + 1, fillW - prevW, bh - 2, fgCol);
|
|
519
|
+
} else if (fillW < prevW) {
|
|
520
|
+
// Value decreased: clear the removed portion
|
|
521
|
+
ui_display_fill_rect(bx + 1 + fillW, by + 1, prevW - fillW, bh - 2, bgCol);
|
|
522
|
+
}
|
|
523
|
+
// Remember current fill width for next incremental update
|
|
524
|
+
__ui_nodes[i].lastTextWidth = fillW;
|
|
525
|
+
}
|
|
526
|
+
break;
|
|
527
|
+
case NODE_RANGE:
|
|
528
|
+
// Range slider: horizontal track + draggable thumb.
|
|
529
|
+
// Incremental redraw (like NODE_PROGRESS): lastTextWidth holds the
|
|
530
|
+
// previous fill width. We erase the delta region between old and new
|
|
531
|
+
// thumb positions with the background, then redraw the track portion
|
|
532
|
+
// and the new thumb — so dragging backward doesn't leave ghost thumbs.
|
|
533
|
+
{
|
|
534
|
+
int16_t bx = __ui_nodes[i].box.x;
|
|
535
|
+
int16_t by = drawY;
|
|
536
|
+
int16_t bw = __ui_nodes[i].box.w;
|
|
537
|
+
int16_t bh = __ui_nodes[i].box.h;
|
|
538
|
+
UI_COLOR_T fgCol = __ui_nodes[i].fg;
|
|
539
|
+
UI_COLOR_T bgCol = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor;
|
|
540
|
+
UI_COLOR_T dimFg = (UI_COLOR_T)((fgCol >> 1) & UI_DIM_MASK);
|
|
541
|
+
|
|
542
|
+
int16_t trackY = by + bh / 2;
|
|
543
|
+
int16_t rMin = __ui_nodes[i].rangeMin;
|
|
544
|
+
int16_t rMax = __ui_nodes[i].rangeMax;
|
|
545
|
+
int16_t range = rMax - rMin;
|
|
546
|
+
if (range <= 0) range = 100;
|
|
547
|
+
int16_t pct = __ui_constrain(__ui_nodes[i].value, rMin, rMax) - rMin;
|
|
548
|
+
int16_t fillW = (static_cast<int32_t>(bw - 8) * pct) / range;
|
|
549
|
+
// lastTextWidth carries the previous fill width, or -1 if this node
|
|
550
|
+
// has never been drawn (fillW=0 at value=min is a valid thumb pos).
|
|
551
|
+
int16_t prevFillW = __ui_nodes[i].lastTextWidth;
|
|
552
|
+
int16_t newThumbX = bx + 4 + fillW - 3;
|
|
553
|
+
|
|
554
|
+
if (prevFillW < 0) {
|
|
555
|
+
// First draw: redraw the whole track + fill from scratch.
|
|
556
|
+
ui_display_draw_fast_hline(bx, trackY, bw, dimFg);
|
|
557
|
+
ui_display_draw_fast_hline(bx + 4, trackY, fillW, fgCol);
|
|
558
|
+
} else {
|
|
559
|
+
// Incremental: wipe the strip between the old and new thumb
|
|
560
|
+
// positions (whichever extends further on each side), then restore
|
|
561
|
+
// the track line. This is symmetric — old thumbs disappear whether
|
|
562
|
+
// the drag moves forward or backward.
|
|
563
|
+
int16_t prevThumbX = bx + 4 + prevFillW - 3;
|
|
564
|
+
int16_t left = prevThumbX < newThumbX ? prevThumbX : newThumbX;
|
|
565
|
+
int16_t right = prevThumbX + 6 > newThumbX + 6 ? prevThumbX + 6 : newThumbX + 6;
|
|
566
|
+
if (left < bx) left = bx;
|
|
567
|
+
if (right > bx + bw) right = bx + bw;
|
|
568
|
+
// Erase the thumb band (10px tall) to background.
|
|
569
|
+
ui_display_fill_rect(left, trackY - 5, right - left, 10, bgCol);
|
|
570
|
+
// Restore the track line over the wiped strip: bright up to the
|
|
571
|
+
// current fill end, dim beyond it.
|
|
572
|
+
int16_t fillEnd = bx + 4 + fillW;
|
|
573
|
+
if (right <= fillEnd) {
|
|
574
|
+
ui_display_draw_fast_hline(left, trackY, right - left, fgCol);
|
|
575
|
+
} else if (left >= fillEnd) {
|
|
576
|
+
ui_display_draw_fast_hline(left, trackY, right - left, dimFg);
|
|
577
|
+
} else {
|
|
578
|
+
ui_display_draw_fast_hline(left, trackY, fillEnd - left, fgCol);
|
|
579
|
+
ui_display_draw_fast_hline(fillEnd, trackY, right - fillEnd, dimFg);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Thumb: small filled rectangle at the current position.
|
|
584
|
+
if (newThumbX < bx + 1) newThumbX = bx + 1;
|
|
585
|
+
if (newThumbX > bx + bw - 7) newThumbX = bx + bw - 7;
|
|
586
|
+
ui_display_fill_rect(newThumbX, trackY - 5, 6, 10, fgCol);
|
|
587
|
+
|
|
588
|
+
// Remember current fill width for the next incremental update.
|
|
589
|
+
__ui_nodes[i].lastTextWidth = fillW;
|
|
590
|
+
}
|
|
591
|
+
break;
|
|
592
|
+
case NODE_INPUT:
|
|
593
|
+
// Input field: bordered rect + current text (or placeholder), clipped to box width.
|
|
594
|
+
{
|
|
595
|
+
int16_t bx = __ui_nodes[i].box.x;
|
|
596
|
+
int16_t by = drawY;
|
|
597
|
+
int16_t bw = __ui_nodes[i].box.w;
|
|
598
|
+
int16_t bh = __ui_nodes[i].box.h;
|
|
599
|
+
UI_COLOR_T fgCol = __ui_nodes[i].fg;
|
|
600
|
+
UI_COLOR_T bgCol = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor;
|
|
601
|
+
// HTML disabled: halve the drawn colors (web-like faded control).
|
|
602
|
+
if (__ui_nodes[i].disabled) {
|
|
603
|
+
fgCol = (fgCol >> 1) & UI_DIM_MASK;
|
|
604
|
+
bgCol = (bgCol >> 1) & UI_DIM_MASK;
|
|
605
|
+
}
|
|
606
|
+
if (__ui_nodes[i].borderRadius > 0) {
|
|
607
|
+
ui_display_fill_round_rect(bx, by, bw, bh, __ui_nodes[i].borderRadius, bgCol);
|
|
608
|
+
} else {
|
|
609
|
+
ui_display_fill_rect(bx, by, bw, bh, bgCol);
|
|
610
|
+
}
|
|
611
|
+
UI_COLOR_T inputBorder = __ui_nodes[i].borderColor ? __ui_nodes[i].borderColor : fgCol;
|
|
612
|
+
uint8_t inputBorderStyle = __ui_nodes[i].borderStyle ? __ui_nodes[i].borderStyle : 1;
|
|
613
|
+
uint8_t inputBorderWidth = __ui_nodes[i].borderWidth ? __ui_nodes[i].borderWidth : 1;
|
|
614
|
+
ui_draw_rect_outline(bx, by, bw, bh, __ui_nodes[i].borderRadius, inputBorderStyle, inputBorderWidth, inputBorder);
|
|
615
|
+
// Show typed text (textBuffer) in fg color, or placeholder (.text)
|
|
616
|
+
// dimmed gray when the buffer is empty.
|
|
617
|
+
UI_COLOR_T textCol = fgCol;
|
|
618
|
+
const char* disp = (__ui_nodes[i].textBuffer[0] != 0)
|
|
619
|
+
? __ui_nodes[i].textBuffer
|
|
620
|
+
: (__ui_nodes[i].text ? __ui_nodes[i].text : "");
|
|
621
|
+
#if defined(UI_HIDE_OSK)
|
|
622
|
+
// Desktop target: when this input is the active edit target, suppress
|
|
623
|
+
// the placeholder. The editing buffer is loaded from the (likely empty)
|
|
624
|
+
// textBuffer on focus, so without this the placeholder ("enter name")
|
|
625
|
+
// would render with the caret at its end. Hide it so the caret shows
|
|
626
|
+
// on a clean field at position 0 until the user types.
|
|
627
|
+
if (__ui_kb_visible && static_cast<int16_t>(__ui_kb_target) == static_cast<int16_t>(i) && __ui_nodes[i].textBuffer[0] == 0) {
|
|
628
|
+
disp = "";
|
|
629
|
+
}
|
|
630
|
+
#endif
|
|
631
|
+
if (__ui_nodes[i].textBuffer[0] == 0) {
|
|
632
|
+
#if UI_COLOR_DEPTH == 888
|
|
633
|
+
textCol = 0x848484;
|
|
634
|
+
#else
|
|
635
|
+
textCol = 0x8410;
|
|
636
|
+
#endif
|
|
637
|
+
}
|
|
638
|
+
// Note: the actual text draw is via ui_draw_text (which uses __ui_gfx).
|
|
639
|
+
// The setCursor/setTextColor/setTextSize below are legacy — ui_draw_text
|
|
640
|
+
// handles its own cursor/colors. Keep them on __ui_gfx for consistency
|
|
641
|
+
// (in case __ui_gfx is the scroll canvas, not __tc_display).
|
|
642
|
+
ui_display_set_cursor(bx + 4, by + (bh - ts * 8) / 2);
|
|
643
|
+
ui_display_set_text_color(textCol, bgCol);
|
|
644
|
+
ui_display_set_text_size(ts);
|
|
645
|
+
// Clip: at textSize ts, each char is ts*6px advance. Only print chars
|
|
646
|
+
// that fit within the box (bw - 8px margin), so text never overflows
|
|
647
|
+
// the border or wraps to the next line.
|
|
648
|
+
int16_t maxChars = (bw - 8) / (ts * 6);
|
|
649
|
+
if (maxChars < 0) maxChars = 0;
|
|
650
|
+
int16_t len = static_cast<int16_t>(strlen(disp));
|
|
651
|
+
if (len > maxChars) len = maxChars;
|
|
652
|
+
if (len > UI_TEXT_BUF) len = UI_TEXT_BUF;
|
|
653
|
+
char clipped[UI_TEXT_BUF + 1];
|
|
654
|
+
for (int16_t c = 0; c < len; c++) {
|
|
655
|
+
clipped[c] = disp[c];
|
|
656
|
+
}
|
|
657
|
+
clipped[len] = 0;
|
|
658
|
+
ui_draw_text(clipped, bx + 4, by + (bh - ui_text_height(ts, __ui_nodes[i].fontFace)) / 2,
|
|
659
|
+
textCol, bgCol, ts, __ui_nodes[i].fontAntialias, __ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing);
|
|
660
|
+
#if defined(UI_HIDE_OSK)
|
|
661
|
+
// Desktop target: with no OSK grid there's no focus indicator. Draw a
|
|
662
|
+
// blinking caret at the end of the typed text on the active edit target
|
|
663
|
+
// so the user sees which field they're editing. Blink ~3×/sec via the
|
|
664
|
+
// top bits of __ui_kb_blink (mask 0x20 toggles every 32 ticks ≈ 530ms).
|
|
665
|
+
if (__ui_kb_visible && static_cast<int16_t>(__ui_kb_target) == static_cast<int16_t>(i) && (__ui_kb_blink & 0x20)) {
|
|
666
|
+
uint16_t caretW = ui_text_width(clipped, ts, __ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing);
|
|
667
|
+
int16_t caretX = bx + 4 + static_cast<int16_t>(caretW);
|
|
668
|
+
int16_t caretYTop = by + (bh - ts * 8) / 2;
|
|
669
|
+
// fgCol (not textCol): the placeholder-dimming path sets textCol to
|
|
670
|
+
// gray, which would make the caret nearly invisible on a focused
|
|
671
|
+
// empty field. The caret should always be the input's foreground.
|
|
672
|
+
ui_display_fill_rect(caretX, caretYTop, static_cast<int16_t>(ts > 1 ? 2 : 1), static_cast<int16_t>(ts * 8), fgCol);
|
|
673
|
+
}
|
|
674
|
+
#endif
|
|
675
|
+
}
|
|
676
|
+
break;
|
|
677
|
+
case NODE_IMG:
|
|
678
|
+
{
|
|
679
|
+
UI_COLOR_T imgBg = __ui_nodes[i].hasBg ? fillBg : __ui_nodes[i].clearColor;
|
|
680
|
+
int16_t fillW = ui_rotated_face_w(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
681
|
+
int16_t fillH = ui_rotated_face_h(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
682
|
+
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, fillW, fillH, imgBg);
|
|
683
|
+
}
|
|
684
|
+
if (__ui_nodes[i].imgDataId < __ui_image_count) {
|
|
685
|
+
const UIImage* img = &__ui_images[__ui_nodes[i].imgDataId];
|
|
686
|
+
int16_t targetW = __ui_nodes[i].box.w;
|
|
687
|
+
int16_t targetH = __ui_nodes[i].box.h;
|
|
688
|
+
ui_draw_image_with_fit(img, __ui_nodes[i].box.x, drawY, __ui_nodes[i].rotateDeg, __ui_nodes[i].objectFit, targetW, targetH);
|
|
689
|
+
}
|
|
690
|
+
if (__ui_nodes[i].borderStyle != 0) {
|
|
691
|
+
int16_t borderW = ui_rotated_face_w(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
692
|
+
int16_t borderH = ui_rotated_face_h(i, __ui_nodes[i].box.w, __ui_nodes[i].box.h);
|
|
693
|
+
ui_draw_rect_outline(__ui_nodes[i].box.x, drawY, borderW, borderH,
|
|
694
|
+
__ui_nodes[i].borderRadius, __ui_nodes[i].borderStyle, __ui_nodes[i].borderWidth, bColor);
|
|
695
|
+
}
|
|
696
|
+
break;
|
|
697
|
+
case NODE_CANVAS: {
|
|
698
|
+
// Find this node's draw callback.
|
|
699
|
+
void (*__ui_canvas_fn)(CuttlefishCanvas16*) = nullptr;
|
|
700
|
+
for (uint16_t b = 0; b < __ui_canvas_binding_count; b++) {
|
|
701
|
+
if (__ui_canvas_bindings[b].node == i) { __ui_canvas_fn = __ui_canvas_bindings[b].fn; break; }
|
|
702
|
+
}
|
|
703
|
+
if (__ui_canvas_fn) {
|
|
704
|
+
int16_t __ui_cw = __ui_nodes[i].canvasW;
|
|
705
|
+
int16_t __ui_ch = __ui_nodes[i].canvasH;
|
|
706
|
+
if (__ui_cw > 0 && __ui_ch > 0) {
|
|
707
|
+
uint8_t __ui_canvas_drawn = 0;
|
|
708
|
+
if (ui_display_is_default_target()) {
|
|
709
|
+
// Cache a canvas sized to the buffer for direct hardware draws so
|
|
710
|
+
// the callback's many primitives push as one bitmap. When the
|
|
711
|
+
// active target is already a memory canvas (scroll/repair/full
|
|
712
|
+
// framebuffer), skip the nested allocation and draw directly below.
|
|
713
|
+
// A canvas can construct but fail its internal pixel-buffer malloc
|
|
714
|
+
// (non-null canvas, null buffer). Treat that as "no canvas" so a
|
|
715
|
+
// transient malloc failure self-heals instead of blanking the node.
|
|
716
|
+
if (!__ui_node_canvas || !display_canvasBuffer(__ui_node_canvas) ||
|
|
717
|
+
display_canvasWidth(__ui_node_canvas) != __ui_cw || display_canvasHeight(__ui_node_canvas) != __ui_ch) {
|
|
718
|
+
display_deleteCanvas(__ui_node_canvas);
|
|
719
|
+
__ui_node_canvas = display_createCanvas(__ui_cw, __ui_ch);
|
|
720
|
+
}
|
|
721
|
+
CuttlefishCanvas16* __ui_lc = __ui_node_canvas;
|
|
722
|
+
if (__ui_lc && display_canvasBuffer(__ui_lc)) {
|
|
723
|
+
display_canvasFillScreen(__ui_lc, __ui_nodes[i].clearColor);
|
|
724
|
+
CuttlefishDisplayTarget* __ui_prev_target = ui_display_get_target();
|
|
725
|
+
ui_display_set_target((CuttlefishDisplayTarget*)__ui_lc);
|
|
726
|
+
__ui_canvas_fn(__ui_lc);
|
|
727
|
+
ui_display_set_target(__ui_prev_target);
|
|
728
|
+
ui_draw_canvas_rect(__ui_lc, __ui_nodes[i].box.x, drawY, __ui_cw, __ui_ch);
|
|
729
|
+
__ui_canvas_drawn = 1;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (!__ui_canvas_drawn) {
|
|
733
|
+
UI_COLOR_T __ui_canvas_bg = __ui_nodes[i].hasBg ? fillBg : __ui_nodes[i].clearColor;
|
|
734
|
+
ui_display_fill_rect(__ui_nodes[i].box.x, drawY, __ui_nodes[i].box.w, __ui_nodes[i].box.h, __ui_canvas_bg);
|
|
735
|
+
int16_t __ui_prev_off_x = __ui_draw_off_x;
|
|
736
|
+
int16_t __ui_prev_off_y = __ui_draw_off_y;
|
|
737
|
+
int16_t __ui_prev_canvas_w = __ui_canvas_fallback_w;
|
|
738
|
+
int16_t __ui_prev_canvas_h = __ui_canvas_fallback_h;
|
|
739
|
+
__ui_canvas_fallback_w = __ui_cw;
|
|
740
|
+
__ui_canvas_fallback_h = __ui_ch;
|
|
741
|
+
__ui_draw_off_x = static_cast<int16_t>(__ui_prev_off_x + __ui_nodes[i].box.x);
|
|
742
|
+
__ui_draw_off_y = static_cast<int16_t>(__ui_prev_off_y + drawY);
|
|
743
|
+
__ui_canvas_fn(nullptr);
|
|
744
|
+
__ui_draw_off_x = __ui_prev_off_x;
|
|
745
|
+
__ui_draw_off_y = __ui_prev_off_y;
|
|
746
|
+
__ui_canvas_fallback_w = __ui_prev_canvas_w;
|
|
747
|
+
__ui_canvas_fallback_h = __ui_prev_canvas_h;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
break;
|
|
752
|
+
}
|
|
753
|
+
case NODE_LIST: {
|
|
754
|
+
// Virtualized list: state lives on the node now (listCountFn/listItemFn/
|
|
755
|
+
// listCount/scrollY/contentHeight/listItemHeight), not in a side table.
|
|
756
|
+
// Resolve lazily as a safety net for targets whose startup path runs
|
|
757
|
+
// before the generated binding table has been copied onto the node.
|
|
758
|
+
if (!__ui_nodes[i].listItemFn) {
|
|
759
|
+
for (uint16_t b = 0; b < __ui_list_binding_count; b++) {
|
|
760
|
+
if (__ui_list_bindings[b].node == static_cast<uint16_t>(i)) {
|
|
761
|
+
__ui_nodes[i].listCountFn = __ui_list_bindings[b].countFn;
|
|
762
|
+
__ui_nodes[i].listItemFn = __ui_list_bindings[b].itemFn;
|
|
763
|
+
__ui_nodes[i].listTapFn = __ui_list_bindings[b].tapFn;
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (!__ui_nodes[i].listItemFn) break;
|
|
769
|
+
int16_t bx = __ui_nodes[i].box.x;
|
|
770
|
+
int16_t by = drawY;
|
|
771
|
+
int16_t bw = __ui_nodes[i].box.w;
|
|
772
|
+
int16_t bh = __ui_nodes[i].box.h;
|
|
773
|
+
uint16_t ih = __ui_nodes[i].listItemHeight > 0 ? __ui_nodes[i].listItemHeight : 24;
|
|
774
|
+
uint16_t itemCount = __ui_nodes[i].listCount;
|
|
775
|
+
int16_t listScrollY = __ui_nodes[i].scrollY;
|
|
776
|
+
int16_t listContentH = __ui_nodes[i].contentHeight;
|
|
777
|
+
UI_COLOR_T clearCol = __ui_nodes[i].clearColor;
|
|
778
|
+
uint8_t listFullRepaint = 0;
|
|
779
|
+
// Render to a viewport-sized canvas so edge glyphs are naturally clipped.
|
|
780
|
+
// Treat a null buffer (failed internal malloc) as "no canvas" and retry —
|
|
781
|
+
// see __ui_node_canvas for the zombie-caching rationale.
|
|
782
|
+
// __ui_list_canvas is file-scoped so ui_release_canvas_state() can free it.
|
|
783
|
+
if (!__ui_list_canvas || !display_canvasBuffer(__ui_list_canvas) ||
|
|
784
|
+
display_canvasWidth(__ui_list_canvas) != bw || display_canvasHeight(__ui_list_canvas) != bh) {
|
|
785
|
+
display_deleteCanvas(__ui_list_canvas);
|
|
786
|
+
__ui_list_canvas_node = -1;
|
|
787
|
+
__ui_list_canvas = ui_create_canvas_best(bw, bh);
|
|
788
|
+
listFullRepaint = 1;
|
|
789
|
+
}
|
|
790
|
+
CuttlefishCanvas16* lc = __ui_list_canvas;
|
|
791
|
+
if (!lc || !display_canvasBuffer(lc)) {
|
|
792
|
+
// Viewport canvas won't allocate (no PSRAM / over SRAM budget on a
|
|
793
|
+
// full-width list). Fall back to the band renderer: composite the
|
|
794
|
+
// visible rows into the ~10KB band canvas strip-by-strip, pushing
|
|
795
|
+
// each band tear-free. Mirrors the generic scroll path's Mode B →
|
|
796
|
+
// ui_render_scroll_bands fallback. If even the band canvas fails,
|
|
797
|
+
// give up (render nothing) — same as the old break.
|
|
798
|
+
if (ui_render_list_bands(static_cast<uint16_t>(i))) {
|
|
799
|
+
return 1; // band path handled push + decoration + dirty-clear
|
|
800
|
+
}
|
|
801
|
+
// Last-resort visible fallback: if even the small band canvas cannot
|
|
802
|
+
// allocate, draw the fully visible rows in one target transaction
|
|
803
|
+
// rather than leaving a black/empty list. This path is only reached
|
|
804
|
+
// under extreme memory pressure; normal renders remain atomic canvas
|
|
805
|
+
// pushes.
|
|
806
|
+
if (ui_render_list_direct(static_cast<uint16_t>(i))) {
|
|
807
|
+
__ui_nodes[i].box.x = origBoxX;
|
|
808
|
+
__ui_nodes[i].box.y = origBoxY;
|
|
809
|
+
return 1;
|
|
810
|
+
}
|
|
811
|
+
break;
|
|
812
|
+
}
|
|
813
|
+
if (__ui_list_canvas_node != static_cast<int16_t>(i)) listFullRepaint = 1;
|
|
814
|
+
int16_t repaintY = 0;
|
|
815
|
+
int16_t repaintH = bh;
|
|
816
|
+
int16_t deltaY = listScrollY - __ui_nodes[i].lastPaintedScrollY;
|
|
817
|
+
int16_t absDelta = deltaY < 0 ? -deltaY : deltaY;
|
|
818
|
+
uint8_t canShiftList = (!listFullRepaint && deltaY != 0 && absDelta < bh);
|
|
819
|
+
if (canShiftList) {
|
|
820
|
+
ui_shift_container_canvas(lc, deltaY, clearCol, &repaintY, &repaintH);
|
|
821
|
+
} else {
|
|
822
|
+
display_canvasFillScreen(lc, clearCol);
|
|
823
|
+
repaintY = 0;
|
|
824
|
+
repaintH = bh;
|
|
825
|
+
}
|
|
826
|
+
// GFXcanvas text has no clipping. For shift-and-repair frames, draw row
|
|
827
|
+
// text into a strip-sized repair canvas first, then blit only that strip
|
|
828
|
+
// into the shifted list canvas. This prevents a 1-5px repair from
|
|
829
|
+
// repainting full glyphs across pixels that were already shifted.
|
|
830
|
+
CuttlefishCanvas16* listTextCanvas = lc;
|
|
831
|
+
int16_t listTextOffsetY = 0;
|
|
832
|
+
uint8_t drawingListRepair = 0;
|
|
833
|
+
if (canShiftList && repaintH > 0 && repaintH < bh) {
|
|
834
|
+
CuttlefishCanvas16* rc = ui_get_repair_canvas(bw, repaintH);
|
|
835
|
+
if (rc) {
|
|
836
|
+
display_canvasFillScreen(rc, clearCol);
|
|
837
|
+
listTextCanvas = rc;
|
|
838
|
+
listTextOffsetY = repaintY;
|
|
839
|
+
drawingListRepair = 1;
|
|
840
|
+
} else {
|
|
841
|
+
display_canvasFillScreen(lc, clearCol);
|
|
842
|
+
repaintY = 0;
|
|
843
|
+
repaintH = bh;
|
|
844
|
+
canShiftList = 0;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
// Compute visible range for the repainted strip. Previously-rendered
|
|
848
|
+
// pixels are shifted in-place; only the exposed band needs new rows.
|
|
849
|
+
uint16_t first = (listScrollY + repaintY) / ih;
|
|
850
|
+
uint16_t last = (listScrollY + repaintY + repaintH - 1) / ih + 1;
|
|
851
|
+
if (itemCount > 0 && last >= itemCount) last = itemCount - 1;
|
|
852
|
+
// Draw each visible item (canvas-local coords: 0,0 = viewport top).
|
|
853
|
+
char listBuf[UI_TEXT_BUF + 1];
|
|
854
|
+
CuttlefishDisplayTarget* listPrevTarget = ui_display_get_target();
|
|
855
|
+
if (itemCount > 0 && repaintH > 0) {
|
|
856
|
+
for (uint16_t idx = first; idx <= last; idx++) {
|
|
857
|
+
int16_t itemY = static_cast<int16_t>(idx * ih) - listScrollY - listTextOffsetY;
|
|
858
|
+
__ui_nodes[i].listItemFn(idx, listBuf, UI_TEXT_BUF + 1);
|
|
859
|
+
listBuf[UI_TEXT_BUF] = 0;
|
|
860
|
+
ui_display_set_target((CuttlefishDisplayTarget*)listTextCanvas);
|
|
861
|
+
int16_t __ui_saved_off_x2 = __ui_draw_off_x;
|
|
862
|
+
int16_t __ui_saved_off_y2 = __ui_draw_off_y;
|
|
863
|
+
__ui_draw_off_x = 0;
|
|
864
|
+
__ui_draw_off_y = 0;
|
|
865
|
+
// Keep classic list glyphs in the canvas pixel buffer. Native
|
|
866
|
+
// CuttlefishGFX text can disappear when copied from a canvas to
|
|
867
|
+
// the panel, while the explicit glyph path remains compositable.
|
|
868
|
+
ui_draw_list_text(listBuf, 4, itemY + static_cast<int16_t>(ih - 16) / 2,
|
|
869
|
+
__ui_nodes[i].fg, clearCol, 2, __ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing);
|
|
870
|
+
__ui_draw_off_x = __ui_saved_off_x2;
|
|
871
|
+
__ui_draw_off_y = __ui_saved_off_y2;
|
|
872
|
+
ui_display_set_target(listPrevTarget);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
if (drawingListRepair) {
|
|
876
|
+
CuttlefishDisplayTarget* prevTarget = ui_display_get_target();
|
|
877
|
+
ui_display_set_target((CuttlefishDisplayTarget*)lc);
|
|
878
|
+
ui_draw_canvas_rect(listTextCanvas, 0, repaintY, bw, repaintH);
|
|
879
|
+
ui_display_set_target(prevTarget);
|
|
880
|
+
}
|
|
881
|
+
// Scrollbar (canvas-local coords).
|
|
882
|
+
if (listContentH > bh) {
|
|
883
|
+
int16_t tx = bw - 4;
|
|
884
|
+
uint16_t thumbH = static_cast<uint32_t>(bh) * bh / listContentH;
|
|
885
|
+
if (thumbH < 8) thumbH = 8;
|
|
886
|
+
int16_t maxScroll = listContentH - bh;
|
|
887
|
+
uint16_t thumbY = maxScroll > 0 ? static_cast<uint32_t>(bh - thumbH) * listScrollY / maxScroll : 0;
|
|
888
|
+
UI_COLOR_T dimFg = (UI_COLOR_T)((__ui_nodes[i].fg >> 1) & UI_DIM_MASK);
|
|
889
|
+
display_canvasFillRect(lc, tx, 0, 3, bh, dimFg);
|
|
890
|
+
display_canvasFillRect(lc, tx, thumbY, 3, thumbH, __ui_nodes[i].fg);
|
|
891
|
+
}
|
|
892
|
+
// Outset shadows are static decoration. Redrawing the hard shadow
|
|
893
|
+
// directly to the panel before every small scroll-frame creates a
|
|
894
|
+
// visible shadow-then-content intermediate state on SPI TFTs. Keep it
|
|
895
|
+
// for full list repaints, but skip it for shift-and-repair scrolls.
|
|
896
|
+
if (!drawingBufferedScroll && !__ui_fb && !canShiftList) {
|
|
897
|
+
ui_draw_shadow(i, by, 0);
|
|
898
|
+
}
|
|
899
|
+
// Standalone lists push directly. Lists inside a buffered scroll
|
|
900
|
+
// container must composite into that scroll canvas; their box has
|
|
901
|
+
// already been translated to canvas-local coordinates.
|
|
902
|
+
if (drawingBufferedScroll) {
|
|
903
|
+
ui_draw_canvas_rect(lc, bx, by, bw, bh);
|
|
904
|
+
} else {
|
|
905
|
+
ui_push_canvas_rect(lc, bx, by, bw, bh);
|
|
906
|
+
}
|
|
907
|
+
// Draw static decoration after the scrollable pixels are composited.
|
|
908
|
+
// Keeping border rows out of __ui_list_canvas prevents the cached
|
|
909
|
+
// shift step from dragging top/bottom border pixels through the list.
|
|
910
|
+
ui_draw_shadow(i, by, 1);
|
|
911
|
+
if (__ui_nodes[i].borderStyle != 0) {
|
|
912
|
+
ui_draw_node_border(i, bx, by, bColor);
|
|
913
|
+
}
|
|
914
|
+
ui_draw_node_outline(i, bx, by);
|
|
915
|
+
__ui_list_canvas_node = static_cast<int16_t>(i);
|
|
916
|
+
__ui_nodes[i].lastPaintedScrollY = listScrollY;
|
|
917
|
+
__ui_nodes[i].dirty = 0;
|
|
918
|
+
if (__ui_fb) {
|
|
919
|
+
UIRect listPaintRect;
|
|
920
|
+
ui_node_paint_rect(i, origBoxX, origBoxY, origBoxX, origBoxY,
|
|
921
|
+
static_cast<uint16_t>(bw), static_cast<uint16_t>(bh), &listPaintRect);
|
|
922
|
+
ui_fb_add_rect(listPaintRect.x, listPaintRect.y, listPaintRect.w, listPaintRect.h);
|
|
923
|
+
}
|
|
924
|
+
ui_display_set_target(__ui_draw_target);
|
|
925
|
+
__ui_nodes[i].box.x = origBoxX;
|
|
926
|
+
__ui_nodes[i].box.y = origBoxY;
|
|
927
|
+
return 1; // list handled canvas push, decoration, and coordinate restore
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
return 0;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// ── Band renderer: tear-free scroll compositing without a viewport canvas ──
|
|
934
|
+
// When a scroll container's viewport canvas won't allocate (no PSRAM, tight
|
|
935
|
+
// SRAM, or a viewport larger than the budget), the previous fallback
|
|
936
|
+
// (direct-full) painted each child straight to the display — one SPI
|
|
937
|
+
// transaction per primitive, visible as tearing during fast drags. The band
|
|
938
|
+
// renderer instead composes the whole visible subtree into a short horizontal
|
|
939
|
+
// band canvas (vw × UI_STRIP_BAND_HEIGHT, e.g. 320×16 ≈ 10KB at RGB565),
|
|
940
|
+
// pushes that band in one SPI transaction, then moves to the next band. Each
|
|
941
|
+
// band completes before it is pushed, so the panel never shows a half-drawn
|
|
942
|
+
// frame — eliminating tearing while keeping memory bounded regardless of
|
|
943
|
+
// program size (the canvas height is fixed; only width tracks the viewport).
|
|
944
|
+
//
|
|
945
|
+
// Returns 1 when the subtree was rendered and pushed (the caller must skip the
|
|
946
|
+
// normal direct-full child repaint). Returns 0 when the band canvas could not
|
|
947
|
+
// be allocated (caller falls back to direct-full).
|
|
948
|
+
// Shared band-canvas alloc helper. The three band renderers reuse the
|
|
949
|
+
// persistent __ui_band_canvas, reallocating only when the width changes so
|
|
950
|
+
// display_canvasWidth == w == stride (ui_push_canvas_rect's single-write fast
|
|
951
|
+
// path requires w == stride — a sub-width push takes a row-by-row path some
|
|
952
|
+
// ST7796S drivers mishandle). Returns the canvas (with a valid buffer) or null.
|
|
953
|
+
static inline CuttlefishCanvas16* ui_band_canvas_for_width(int16_t w) {
|
|
954
|
+
if (!__ui_band_canvas || !display_canvasBuffer(__ui_band_canvas) ||
|
|
955
|
+
display_canvasWidth(__ui_band_canvas) != w) {
|
|
956
|
+
display_deleteCanvas(__ui_band_canvas);
|
|
957
|
+
__ui_band_canvas = ui_create_canvas_best(w, UI_STRIP_BAND_HEIGHT);
|
|
958
|
+
}
|
|
959
|
+
CuttlefishCanvas16* band = __ui_band_canvas;
|
|
960
|
+
return (band && display_canvasBuffer(band)) ? band : nullptr;
|
|
961
|
+
}
|
|
962
|
+
static inline uint8_t ui_render_scroll_bands(uint16_t s) {
|
|
963
|
+
if (s >= __ui_node_count) return 0;
|
|
964
|
+
int16_t vw = __ui_nodes[s].box.w;
|
|
965
|
+
int16_t vh = __ui_nodes[s].box.h;
|
|
966
|
+
if (vw <= 0 || vh <= 0) return 0;
|
|
967
|
+
int16_t vox = __ui_nodes[s].box.x;
|
|
968
|
+
int16_t voy = __ui_nodes[s].box.y;
|
|
969
|
+
UI_COLOR_T scrollBg = __ui_nodes[s].hasBg ? __ui_nodes[s].bg : __ui_nodes[s].clearColor;
|
|
970
|
+
// One persistent band canvas, reused across bands and frames, reallocated
|
|
971
|
+
// only when the viewport width changes (so stride == vw for the single-write
|
|
972
|
+
// push fast path). See ui_band_canvas_for_width.
|
|
973
|
+
CuttlefishCanvas16* band = ui_band_canvas_for_width(vw);
|
|
974
|
+
if (!band) return 0;
|
|
975
|
+
int16_t bandH = display_canvasHeight(band);
|
|
976
|
+
// Build a compact list of descendants that can intersect the viewport once per
|
|
977
|
+
// scroll repaint. The old nested band×subtree walk repeated visibility, screen,
|
|
978
|
+
// and Y culling for every band; the candidate list keeps that work O(subtree)
|
|
979
|
+
// and each band only visits rows that can actually contribute pixels.
|
|
980
|
+
uint16_t subtreeEnd = __ui_nodes[s].subtreeEnd;
|
|
981
|
+
if (subtreeEnd > __ui_node_count) subtreeEnd = __ui_node_count;
|
|
982
|
+
// The scrollbar occupies the rightmost 4px gutter (tx = vw - 4, 3px wide).
|
|
983
|
+
// Rather than draw it separately at frame end (which made each band push erase
|
|
984
|
+
// the previous frame's scrollbar slice-by-slice, flashing it during scroll),
|
|
985
|
+
// composite this band's scrollbar slice into the gutter of the band canvas
|
|
986
|
+
// itself. Each band then pushes the FULL viewport width in one SPI transaction
|
|
987
|
+
// (the single-write fast path that works on every driver — a sub-width push
|
|
988
|
+
// forces a row-by-row path that some ST7796S drivers mishandle, showing only
|
|
989
|
+
// the first row). The scrollbar is thus part of each band's atomic push: no
|
|
990
|
+
// separate erase/redraw cycle, no flash, and the fast path is preserved.
|
|
991
|
+
int16_t contentW = vw > 4 ? static_cast<int16_t>(vw - 4) : vw;
|
|
992
|
+
// Scrollbar geometry (document/viewport coords): track spans [0, vh); thumb
|
|
993
|
+
// spans [thumbY, thumbY + thumbH). Both are 3px wide at canvas-x contentW.
|
|
994
|
+
uint16_t sbThumbH = 0;
|
|
995
|
+
uint16_t sbThumbY = 0;
|
|
996
|
+
uint8_t sbVisible = 0;
|
|
997
|
+
UI_COLOR_T sbTrackCol = (UI_COLOR_T)((__ui_nodes[s].fg >> 1) & UI_DIM_MASK);
|
|
998
|
+
UI_COLOR_T sbThumbCol = __ui_nodes[s].fg;
|
|
999
|
+
if (__ui_nodes[s].contentHeight > vh) {
|
|
1000
|
+
sbVisible = 1;
|
|
1001
|
+
sbThumbH = static_cast<uint32_t>(vh) * vh / __ui_nodes[s].contentHeight;
|
|
1002
|
+
if (sbThumbH < 8) sbThumbH = 8;
|
|
1003
|
+
// Clamp to vh so the (vh - sbThumbH) subtraction below can't go negative on
|
|
1004
|
+
// a degenerate sub-8px viewport (which would cast to a huge uint32_t and
|
|
1005
|
+
// place the thumb off-screen). ui_draw_scrollbar_direct has the same min-8
|
|
1006
|
+
// clamp; this extra guard keeps the band path's slice math safe.
|
|
1007
|
+
if (sbThumbH > static_cast<uint16_t>(vh)) sbThumbH = static_cast<uint16_t>(vh);
|
|
1008
|
+
int16_t maxScroll = __ui_nodes[s].contentHeight - vh;
|
|
1009
|
+
sbThumbY = maxScroll > 0 ? static_cast<uint32_t>(vh - sbThumbH) * __ui_nodes[s].scrollY / maxScroll : 0;
|
|
1010
|
+
}
|
|
1011
|
+
CuttlefishDisplayTarget* prevTarget = ui_display_get_target();
|
|
1012
|
+
if (!__ui_scroll_candidates) return 0;
|
|
1013
|
+
__ui_scroll_candidate_count = 0;
|
|
1014
|
+
for (uint16_t c = s + 1; c < subtreeEnd && __ui_scroll_candidate_count < __ui_node_count; c++) {
|
|
1015
|
+
if (!ui_is_effectively_visible(c) || __ui_nodes[c].screenId != __ui_active_screen) continue;
|
|
1016
|
+
UIScrollPaintCandidate& candidate = __ui_scroll_candidates[__ui_scroll_candidate_count++];
|
|
1017
|
+
candidate.node = c;
|
|
1018
|
+
candidate.screenY = ui_draw_y_for_node(c);
|
|
1019
|
+
candidate.faceH = __ui_nodes[c].box.h;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// Top→bottom bands over the viewport.
|
|
1023
|
+
for (int16_t bandTop = 0; bandTop < vh; bandTop += bandH) {
|
|
1024
|
+
int16_t bandBot = bandTop + bandH;
|
|
1025
|
+
if (bandBot > vh) bandBot = vh;
|
|
1026
|
+
int16_t thisH = static_cast<int16_t>(bandBot - bandTop);
|
|
1027
|
+
display_canvasFillRect(band, 0, 0, contentW, bandH, scrollBg);
|
|
1028
|
+
ui_display_set_target(band);
|
|
1029
|
+
for (uint16_t candidateIdx = 0; candidateIdx < __ui_scroll_candidate_count; candidateIdx++) {
|
|
1030
|
+
uint16_t c = __ui_scroll_candidates[candidateIdx].node;
|
|
1031
|
+
int16_t screenY = __ui_scroll_candidates[candidateIdx].screenY;
|
|
1032
|
+
int16_t faceH = __ui_scroll_candidates[candidateIdx].faceH;
|
|
1033
|
+
// Cull nodes whose face does not intersect this band's Y range before
|
|
1034
|
+
// doing any expensive per-node work (AGENTS.md: clip before the inner
|
|
1035
|
+
// loop). A node fully above or below the band contributes no pixels.
|
|
1036
|
+
if (screenY + faceH <= voy + bandTop) continue;
|
|
1037
|
+
if (screenY >= voy + bandBot) continue;
|
|
1038
|
+
// Translate the node's box into band-local coordinates, mirroring the main
|
|
1039
|
+
// loop's drawingBufferedScroll setup: subtract the viewport origin and the
|
|
1040
|
+
// band's top row. ui_draw_y_for_node then re-derives the band-local Y
|
|
1041
|
+
// (document Y - scrollY - voy - bandTop) = (screenY - voy - bandTop). The
|
|
1042
|
+
// box.x translate below is read by ui_draw_x_for_node (which adds
|
|
1043
|
+
// transformOffsetX + pressedOffsetX), so it must be set before computing
|
|
1044
|
+
// drawX; the body needs box.x = drawX (including the pressed offset,
|
|
1045
|
+
// matching the main loop and ui_render_node_bands).
|
|
1046
|
+
int16_t origBoxX = __ui_nodes[c].box.x;
|
|
1047
|
+
int16_t origBoxY = __ui_nodes[c].box.y;
|
|
1048
|
+
__ui_nodes[c].box.x = static_cast<int16_t>(origBoxX - vox);
|
|
1049
|
+
__ui_nodes[c].box.y = static_cast<int16_t>(origBoxY - voy - bandTop);
|
|
1050
|
+
int16_t drawX = ui_draw_x_for_node(c);
|
|
1051
|
+
int16_t drawY = ui_draw_y_for_node(c);
|
|
1052
|
+
// Outset shadow at the rest position, under the face — parity with the
|
|
1053
|
+
// main dirty loop and ui_render_node_bands. This renderer omitted it,
|
|
1054
|
+
// so band-composited scroll viewports (viewport canvas won't allocate)
|
|
1055
|
+
// lost every shadow: theme --shadow-* tokens rendered in the preview
|
|
1056
|
+
// but not on device. Lists manage their own shadow elsewhere.
|
|
1057
|
+
if (__ui_nodes[c].kind != NODE_LIST) {
|
|
1058
|
+
__ui_nodes[c].box.x = ui_base_draw_x_for_node(c);
|
|
1059
|
+
ui_draw_shadow(c, ui_base_draw_y_for_node(c), 0);
|
|
1060
|
+
}
|
|
1061
|
+
__ui_nodes[c].box.x = drawX;
|
|
1062
|
+
uint8_t ts = __ui_nodes[c].textSize ? __ui_nodes[c].textSize : 2;
|
|
1063
|
+
uint16_t textMaxW = ui_node_text_max_width(c);
|
|
1064
|
+
uint16_t tw = 0;
|
|
1065
|
+
uint16_t th = 0;
|
|
1066
|
+
ui_node_text_layout_metrics(c, textMaxW, &tw, &th);
|
|
1067
|
+
uint16_t paintTextW = tw;
|
|
1068
|
+
uint16_t paintTextH = th;
|
|
1069
|
+
if (__ui_nodes[c].kind == NODE_TEXT || __ui_nodes[c].kind == NODE_SELECT) {
|
|
1070
|
+
uint16_t hInset = static_cast<uint16_t>(__ui_nodes[c].paddingLeft) + static_cast<uint16_t>(__ui_nodes[c].paddingRight) + static_cast<uint16_t>(__ui_nodes[c].borderWidth) * 2;
|
|
1071
|
+
uint16_t vInset = static_cast<uint16_t>(__ui_nodes[c].paddingTop) + static_cast<uint16_t>(__ui_nodes[c].paddingBottom) + static_cast<uint16_t>(__ui_nodes[c].borderWidth) * 2;
|
|
1072
|
+
paintTextW = static_cast<uint16_t>(tw + hInset);
|
|
1073
|
+
paintTextH = static_cast<uint16_t>(th + vInset);
|
|
1074
|
+
}
|
|
1075
|
+
if (__ui_nodes[c].kind == NODE_CHECK || __ui_nodes[c].kind == NODE_RADIO) {
|
|
1076
|
+
paintTextW = static_cast<uint16_t>(tw + 22);
|
|
1077
|
+
if (paintTextH < 16) paintTextH = 16;
|
|
1078
|
+
}
|
|
1079
|
+
const char* displayText = __ui_nodes[c].hasTextBinding
|
|
1080
|
+
? __ui_nodes[c].textBuffer
|
|
1081
|
+
: __ui_nodes[c].text;
|
|
1082
|
+
UI_COLOR_T bColor = __ui_nodes[c].borderColor ? __ui_nodes[c].borderColor : __ui_nodes[c].fg;
|
|
1083
|
+
UI_COLOR_T fillBg = __ui_nodes[c].bg;
|
|
1084
|
+
if (__ui_nodes[c].opacity < 100) {
|
|
1085
|
+
UI_COLOR_T backdrop = ui_parent_clear_color(c);
|
|
1086
|
+
bColor = ui_blend(bColor, backdrop, __ui_nodes[c].opacity);
|
|
1087
|
+
fillBg = ui_blend(__ui_nodes[c].bg, backdrop, __ui_nodes[c].opacity);
|
|
1088
|
+
}
|
|
1089
|
+
UINodeDrawCtx ctx;
|
|
1090
|
+
ctx.drawY = drawY;
|
|
1091
|
+
ctx.bColor = bColor;
|
|
1092
|
+
ctx.fillBg = fillBg;
|
|
1093
|
+
ctx.ts = ts;
|
|
1094
|
+
ctx.textMaxW = textMaxW;
|
|
1095
|
+
ctx.tw = tw;
|
|
1096
|
+
ctx.th = th;
|
|
1097
|
+
ctx.paintTextW = paintTextW;
|
|
1098
|
+
ctx.paintTextH = paintTextH;
|
|
1099
|
+
ctx.displayText = displayText;
|
|
1100
|
+
ctx.drawingBufferedScroll = 0;
|
|
1101
|
+
ctx.drawTarget = prevTarget;
|
|
1102
|
+
ctx.origBoxX = origBoxX;
|
|
1103
|
+
ctx.origBoxY = origBoxY;
|
|
1104
|
+
// The band draws onto a freshly cleared canvas every band, so the node's
|
|
1105
|
+
// incremental-paint caches (lastTextWidth/lastTextHeight, which carry
|
|
1106
|
+
// across frames for the main loop's NODE_TEXT/NODE_PROGRESS/NODE_RANGE
|
|
1107
|
+
// incremental redraws) are bogus here — reset to force a full repaint into
|
|
1108
|
+
// the blank band. Save/restore them around the draw so the band path is
|
|
1109
|
+
// transient: it must not corrupt the persistent incremental-redraw state
|
|
1110
|
+
// the main loop relies on if this container later transitions to canvas
|
|
1111
|
+
// mode (the values are also stale across the multiple bands a tall node
|
|
1112
|
+
// spans, so restoring the pre-band value keeps things consistent).
|
|
1113
|
+
int16_t savedLastTextW = __ui_nodes[c].lastTextWidth;
|
|
1114
|
+
uint16_t savedLastTextH = __ui_nodes[c].lastTextHeight;
|
|
1115
|
+
if (__ui_nodes[c].kind == NODE_PROGRESS || __ui_nodes[c].kind == NODE_RANGE) {
|
|
1116
|
+
__ui_nodes[c].lastTextWidth = -1;
|
|
1117
|
+
}
|
|
1118
|
+
__ui_nodes[c].lastTextHeight = 0;
|
|
1119
|
+
ui_draw_node_body(static_cast<int16_t>(c), &ctx);
|
|
1120
|
+
__ui_nodes[c].lastTextWidth = savedLastTextW;
|
|
1121
|
+
__ui_nodes[c].lastTextHeight = savedLastTextH;
|
|
1122
|
+
__ui_nodes[c].box.x = origBoxX;
|
|
1123
|
+
__ui_nodes[c].box.y = origBoxY;
|
|
1124
|
+
}
|
|
1125
|
+
ui_display_set_target(prevTarget);
|
|
1126
|
+
// Composite this band's scrollbar slice into the gutter. The track covers
|
|
1127
|
+
// every row; the thumb covers viewport-rows [sbThumbY, sbThumbY+sbThumbH).
|
|
1128
|
+
// Intersect that with this band's [bandTop, bandBot) range and fill the
|
|
1129
|
+
// overlapping canvas-local rows with the thumb color.
|
|
1130
|
+
if (sbVisible) {
|
|
1131
|
+
// Full gutter width (vw - contentW, normally 4px): child decorations
|
|
1132
|
+
// (outset shadows) poke past the content area and would survive as 1px
|
|
1133
|
+
// ticks in any uncovered edge column.
|
|
1134
|
+
display_canvasFillRect(band, contentW, 0, vw - contentW, thisH, sbTrackCol);
|
|
1135
|
+
int16_t thumbTop = static_cast<int16_t>(sbThumbY);
|
|
1136
|
+
int16_t thumbBot = static_cast<int16_t>(sbThumbY + sbThumbH);
|
|
1137
|
+
int16_t ovTop = thumbTop > bandTop ? thumbTop : bandTop;
|
|
1138
|
+
int16_t ovBot = thumbBot < bandBot ? thumbBot : bandBot;
|
|
1139
|
+
if (ovBot > ovTop) {
|
|
1140
|
+
display_canvasFillRect(band, contentW, static_cast<int16_t>(ovTop - bandTop), 3, static_cast<int16_t>(ovBot - ovTop), sbThumbCol);
|
|
1141
|
+
}
|
|
1142
|
+
} else {
|
|
1143
|
+
// No scrollbar (content fits) — clear the gutter to the scroll bg so the
|
|
1144
|
+
// previous screen's scrollbar pixels don't bleed through.
|
|
1145
|
+
display_canvasFillRect(band, contentW, 0, vw - contentW, thisH, scrollBg);
|
|
1146
|
+
}
|
|
1147
|
+
// Push the FULL viewport width in one transaction (single-write fast path:
|
|
1148
|
+
// w == stride). Each band — content + scrollbar slice — is fully rendered
|
|
1149
|
+
// before it touches the panel, eliminating both tearing and scrollbar flash.
|
|
1150
|
+
ui_push_canvas_rect(band, vox, static_cast<int16_t>(voy + bandTop), vw, thisH);
|
|
1151
|
+
}
|
|
1152
|
+
__ui_nodes[s].lastPaintedScrollY = __ui_nodes[s].scrollY;
|
|
1153
|
+
// The subtree was fully rendered this frame; the main loop's scroll-defer
|
|
1154
|
+
// path will skip these nodes (bufferedScrollDirectFull stays clear), and the
|
|
1155
|
+
// caller has already marked the owner non-dirty.
|
|
1156
|
+
for (uint16_t c = s + 1; c < subtreeEnd; c++) {
|
|
1157
|
+
__ui_nodes[c].dirty = 0;
|
|
1158
|
+
}
|
|
1159
|
+
return 1;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
// ── Whole-frame band composition (drawer slide frames) ────────────────────
|
|
1163
|
+
// Composes every visible active-screen node in draw order over a display
|
|
1164
|
+
// rect, one horizontal band at a time, pushing each band in a single
|
|
1165
|
+
// transaction. The drawer slide uses it over the union of the panel's old
|
|
1166
|
+
// and new paint rects: a mark-all-dirty slide step instead repainted the
|
|
1167
|
+
// whole screen with direct per-node clears (visible flashing on SPI TFTs,
|
|
1168
|
+
// and only ~3 steps fit inside the 180ms slide). Returns 0 when the band
|
|
1169
|
+
// canvas or draw order is unavailable (caller falls back to mark-all-dirty);
|
|
1170
|
+
// a rect fully outside the panel returns 1 (nothing to compose).
|
|
1171
|
+
static inline uint8_t ui_render_screen_bands(int16_t rx, int16_t ry, int16_t rw, int16_t rh) {
|
|
1172
|
+
if (rw <= 0 || rh <= 0) return 1;
|
|
1173
|
+
if (!ui_clip_rect_to_display_target(&rx, &ry, &rw, &rh)) return 1;
|
|
1174
|
+
if (!__ui_draw_order || !__ui_scroll_candidates) return 0;
|
|
1175
|
+
CuttlefishCanvas16* band = ui_band_canvas_for_width(rw);
|
|
1176
|
+
if (!band) return 0;
|
|
1177
|
+
int16_t bandH = display_canvasHeight(band);
|
|
1178
|
+
// Seed color: the active screen's background (the screen fill node paints
|
|
1179
|
+
// over it in draw order anyway; the seed keeps uncovered rows defined).
|
|
1180
|
+
UI_COLOR_T screenBg = static_cast<UI_COLOR_T>(0);
|
|
1181
|
+
if (__ui_active_screen_bg_node < __ui_node_count) {
|
|
1182
|
+
screenBg = __ui_nodes[__ui_active_screen_bg_node].hasBg
|
|
1183
|
+
? __ui_nodes[__ui_active_screen_bg_node].bg
|
|
1184
|
+
: __ui_nodes[__ui_active_screen_bg_node].clearColor;
|
|
1185
|
+
}
|
|
1186
|
+
// Draw-ordered candidate list once per composition (same shape as the
|
|
1187
|
+
// scroll band path); each band then only Y-culls against it.
|
|
1188
|
+
__ui_scroll_candidate_count = 0;
|
|
1189
|
+
for (uint16_t pass = 0; pass < __ui_node_count; pass++) {
|
|
1190
|
+
uint16_t c = __ui_draw_order[pass];
|
|
1191
|
+
if (__ui_nodes[c].screenId != __ui_active_screen) continue;
|
|
1192
|
+
if (!ui_is_effectively_visible(c)) continue;
|
|
1193
|
+
if (__ui_scroll_candidate_count >= __ui_node_count) break;
|
|
1194
|
+
UIScrollPaintCandidate& cand = __ui_scroll_candidates[__ui_scroll_candidate_count++];
|
|
1195
|
+
cand.node = c;
|
|
1196
|
+
cand.screenY = ui_draw_y_for_node(c);
|
|
1197
|
+
cand.faceH = __ui_nodes[c].box.h;
|
|
1198
|
+
// Scrolled children clip to their scroll viewport (CSS overflow): a
|
|
1199
|
+
// compose over a region touching the viewport edge must not paint their
|
|
1200
|
+
// faces at absolute positions beyond it. The initial whole-screen compose
|
|
1201
|
+
// left stale button rows below the fold, and composes over the header
|
|
1202
|
+
// region smeared scrolled content into the header — bars of previously
|
|
1203
|
+
// scrolled pixels that nothing repaints. The viewport rect is the scroll
|
|
1204
|
+
// ancestor's own draw position (its scrollY applies to children, not to
|
|
1205
|
+
// itself).
|
|
1206
|
+
int16_t san = ui_scroll_ancestor_for_node(c);
|
|
1207
|
+
if (san >= 0 && __ui_nodes[san].box.h > 0 && __ui_nodes[san].box.w > 0) {
|
|
1208
|
+
cand.clipTop = ui_draw_y_for_node(static_cast<uint16_t>(san));
|
|
1209
|
+
cand.clipBottom = static_cast<int16_t>(cand.clipTop + __ui_nodes[san].box.h);
|
|
1210
|
+
cand.clipLeft = ui_draw_x_for_node(static_cast<uint16_t>(san));
|
|
1211
|
+
cand.clipRight = static_cast<int16_t>(cand.clipLeft + __ui_nodes[san].box.w);
|
|
1212
|
+
} else {
|
|
1213
|
+
cand.clipTop = -32767;
|
|
1214
|
+
cand.clipBottom = 32767;
|
|
1215
|
+
cand.clipLeft = -32767;
|
|
1216
|
+
cand.clipRight = 32767;
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
CuttlefishDisplayTarget* prevTarget = ui_display_get_target();
|
|
1220
|
+
for (int16_t bandTop = 0; bandTop < rh; bandTop += bandH) {
|
|
1221
|
+
int16_t bandBot = static_cast<int16_t>(bandTop + bandH);
|
|
1222
|
+
if (bandBot > rh) bandBot = rh;
|
|
1223
|
+
int16_t thisH = static_cast<int16_t>(bandBot - bandTop);
|
|
1224
|
+
display_canvasFillRect(band, 0, 0, rw, thisH, screenBg);
|
|
1225
|
+
ui_display_set_target(band);
|
|
1226
|
+
for (uint16_t ci = 0; ci < __ui_scroll_candidate_count; ci++) {
|
|
1227
|
+
uint16_t c = __ui_scroll_candidates[ci].node;
|
|
1228
|
+
int16_t screenY = __ui_scroll_candidates[ci].screenY;
|
|
1229
|
+
int16_t faceH = __ui_scroll_candidates[ci].faceH;
|
|
1230
|
+
// Clip before the per-node work (AGENTS.md): skip nodes whose face
|
|
1231
|
+
// does not intersect this band's Y range.
|
|
1232
|
+
if (screenY + faceH <= ry + bandTop) continue;
|
|
1233
|
+
if (screenY >= ry + bandBot) continue;
|
|
1234
|
+
int16_t origBoxX = __ui_nodes[c].box.x;
|
|
1235
|
+
int16_t origBoxY = __ui_nodes[c].box.y;
|
|
1236
|
+
int16_t origBoxW = __ui_nodes[c].box.w;
|
|
1237
|
+
int16_t origBoxH = __ui_nodes[c].box.h;
|
|
1238
|
+
__ui_nodes[c].box.x = static_cast<int16_t>(origBoxX - rx);
|
|
1239
|
+
__ui_nodes[c].box.y = static_cast<int16_t>(origBoxY - ry - bandTop);
|
|
1240
|
+
// Clamp the face to the scroll viewport (band-local DRAW coords). The
|
|
1241
|
+
// clamp must operate on the node's DRAW position — box.y is
|
|
1242
|
+
// content-local and the ancestor scroll is subtracted later, inside
|
|
1243
|
+
// ui_draw_y_for_node, so comparing box.y against display-space clip
|
|
1244
|
+
// bounds passed scrolled content whose CONTENT coordinate numerically
|
|
1245
|
+
// fell inside the viewport: a button scrolled up past the viewport top
|
|
1246
|
+
// painted over the header, and the smearing stuck (nothing repaints
|
|
1247
|
+
// the header afterwards). Clamp the draw position and shift the box by
|
|
1248
|
+
// the delta; text wrapping and layout metrics shrink with the box.
|
|
1249
|
+
// Fully clipped faces drop out here.
|
|
1250
|
+
{
|
|
1251
|
+
// int32 math: the unclipped sentinels are ±32767 and the band-local
|
|
1252
|
+
// conversion subtracts region/band offsets — int16 arithmetic wraps
|
|
1253
|
+
// (a wrapped-positive clipTop clamped every face to garbage and
|
|
1254
|
+
// dropped the candidate: the main screen's header card vanished
|
|
1255
|
+
// into the dark screen background, reading as a black rect).
|
|
1256
|
+
int32_t clipTopL = static_cast<int32_t>(__ui_scroll_candidates[ci].clipTop) - ry - bandTop;
|
|
1257
|
+
int32_t clipBotL = static_cast<int32_t>(__ui_scroll_candidates[ci].clipBottom) - ry - bandTop;
|
|
1258
|
+
int32_t clipLeftL = static_cast<int32_t>(__ui_scroll_candidates[ci].clipLeft) - rx;
|
|
1259
|
+
int32_t clipRightL = static_cast<int32_t>(__ui_scroll_candidates[ci].clipRight) - rx;
|
|
1260
|
+
int16_t dispY0 = ui_draw_y_for_node(c);
|
|
1261
|
+
int16_t dispX0 = ui_draw_x_for_node(c);
|
|
1262
|
+
int32_t y0 = dispY0 < clipTopL ? clipTopL : dispY0;
|
|
1263
|
+
int32_t y1 = static_cast<int32_t>(dispY0) + __ui_nodes[c].box.h > clipBotL ? clipBotL : static_cast<int32_t>(dispY0) + __ui_nodes[c].box.h;
|
|
1264
|
+
int32_t x0 = dispX0 < clipLeftL ? clipLeftL : dispX0;
|
|
1265
|
+
int32_t x1 = static_cast<int32_t>(dispX0) + __ui_nodes[c].box.w > clipRightL ? clipRightL : static_cast<int32_t>(dispX0) + __ui_nodes[c].box.w;
|
|
1266
|
+
if (x1 - x0 <= 0 || y1 - y0 <= 0) {
|
|
1267
|
+
__ui_nodes[c].box.x = origBoxX;
|
|
1268
|
+
__ui_nodes[c].box.y = origBoxY;
|
|
1269
|
+
__ui_nodes[c].box.w = origBoxW;
|
|
1270
|
+
__ui_nodes[c].box.h = origBoxH;
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
// box.y/box.x already carry the band shift; add the clamp delta on
|
|
1274
|
+
// top (y0 - dispY0 is 0 when unclipped, preserving the shift).
|
|
1275
|
+
__ui_nodes[c].box.y = static_cast<int16_t>(__ui_nodes[c].box.y + (y0 - dispY0));
|
|
1276
|
+
__ui_nodes[c].box.h = static_cast<int16_t>(y1 - y0);
|
|
1277
|
+
__ui_nodes[c].box.x = static_cast<int16_t>(__ui_nodes[c].box.x + (x0 - dispX0));
|
|
1278
|
+
__ui_nodes[c].box.w = static_cast<int16_t>(x1 - x0);
|
|
1279
|
+
}
|
|
1280
|
+
int16_t baseDrawX = ui_base_draw_x_for_node(c);
|
|
1281
|
+
int16_t baseDrawY = ui_base_draw_y_for_node(c);
|
|
1282
|
+
int16_t drawX = ui_draw_x_for_node(c);
|
|
1283
|
+
int16_t drawY = ui_draw_y_for_node(c);
|
|
1284
|
+
// Outset shadow at the rest position, under the face (mirrors the
|
|
1285
|
+
// main loop; lists manage their own shadow elsewhere).
|
|
1286
|
+
if (__ui_nodes[c].kind != NODE_LIST) {
|
|
1287
|
+
__ui_nodes[c].box.x = baseDrawX;
|
|
1288
|
+
ui_draw_shadow(c, baseDrawY, 0);
|
|
1289
|
+
}
|
|
1290
|
+
__ui_nodes[c].box.x = drawX;
|
|
1291
|
+
uint8_t ts = __ui_nodes[c].textSize ? __ui_nodes[c].textSize : 2;
|
|
1292
|
+
uint16_t textMaxW = ui_node_text_max_width(c);
|
|
1293
|
+
uint16_t tw = 0;
|
|
1294
|
+
uint16_t th = 0;
|
|
1295
|
+
ui_node_text_layout_metrics(c, textMaxW, &tw, &th);
|
|
1296
|
+
uint16_t paintTextW = tw;
|
|
1297
|
+
uint16_t paintTextH = th;
|
|
1298
|
+
if (__ui_nodes[c].kind == NODE_TEXT || __ui_nodes[c].kind == NODE_SELECT) {
|
|
1299
|
+
uint16_t hInset = static_cast<uint16_t>(__ui_nodes[c].paddingLeft) + static_cast<uint16_t>(__ui_nodes[c].paddingRight) + static_cast<uint16_t>(__ui_nodes[c].borderWidth) * 2;
|
|
1300
|
+
uint16_t vInset = static_cast<uint16_t>(__ui_nodes[c].paddingTop) + static_cast<uint16_t>(__ui_nodes[c].paddingBottom) + static_cast<uint16_t>(__ui_nodes[c].borderWidth) * 2;
|
|
1301
|
+
paintTextW = static_cast<uint16_t>(tw + hInset);
|
|
1302
|
+
paintTextH = static_cast<uint16_t>(th + vInset);
|
|
1303
|
+
}
|
|
1304
|
+
if (__ui_nodes[c].kind == NODE_CHECK || __ui_nodes[c].kind == NODE_RADIO) {
|
|
1305
|
+
paintTextW = static_cast<uint16_t>(tw + 22);
|
|
1306
|
+
if (paintTextH < 16) paintTextH = 16;
|
|
1307
|
+
}
|
|
1308
|
+
const char* displayText = __ui_nodes[c].hasTextBinding
|
|
1309
|
+
? __ui_nodes[c].textBuffer
|
|
1310
|
+
: __ui_nodes[c].text;
|
|
1311
|
+
UI_COLOR_T bColor = __ui_nodes[c].borderColor ? __ui_nodes[c].borderColor : __ui_nodes[c].fg;
|
|
1312
|
+
UI_COLOR_T fillBg = __ui_nodes[c].bg;
|
|
1313
|
+
if (__ui_nodes[c].opacity < 100) {
|
|
1314
|
+
UI_COLOR_T backdrop = ui_parent_clear_color(c);
|
|
1315
|
+
bColor = ui_blend(bColor, backdrop, __ui_nodes[c].opacity);
|
|
1316
|
+
fillBg = ui_blend(__ui_nodes[c].bg, backdrop, __ui_nodes[c].opacity);
|
|
1317
|
+
}
|
|
1318
|
+
UINodeDrawCtx ctx;
|
|
1319
|
+
ctx.drawY = drawY;
|
|
1320
|
+
ctx.bColor = bColor;
|
|
1321
|
+
ctx.fillBg = fillBg;
|
|
1322
|
+
ctx.ts = ts;
|
|
1323
|
+
ctx.textMaxW = textMaxW;
|
|
1324
|
+
ctx.tw = tw;
|
|
1325
|
+
ctx.th = th;
|
|
1326
|
+
ctx.paintTextW = paintTextW;
|
|
1327
|
+
ctx.paintTextH = paintTextH;
|
|
1328
|
+
ctx.displayText = displayText;
|
|
1329
|
+
ctx.drawingBufferedScroll = 0;
|
|
1330
|
+
ctx.drawTarget = prevTarget;
|
|
1331
|
+
ctx.origBoxX = origBoxX;
|
|
1332
|
+
ctx.origBoxY = origBoxY;
|
|
1333
|
+
// Each band starts on a freshly seeded canvas, so the persistent
|
|
1334
|
+
// incremental-paint caches (lastTextWidth/Height) are invalid mid-band.
|
|
1335
|
+
// Save/restore so the band path is transient (same contract as
|
|
1336
|
+
// ui_render_scroll_bands / ui_render_node_bands).
|
|
1337
|
+
int16_t savedLastTextW = __ui_nodes[c].lastTextWidth;
|
|
1338
|
+
uint16_t savedLastTextH = __ui_nodes[c].lastTextHeight;
|
|
1339
|
+
if (__ui_nodes[c].kind == NODE_PROGRESS || __ui_nodes[c].kind == NODE_RANGE) {
|
|
1340
|
+
__ui_nodes[c].lastTextWidth = -1;
|
|
1341
|
+
}
|
|
1342
|
+
__ui_nodes[c].lastTextHeight = 0;
|
|
1343
|
+
ui_draw_node_body(static_cast<int16_t>(c), &ctx);
|
|
1344
|
+
__ui_nodes[c].lastTextWidth = savedLastTextW;
|
|
1345
|
+
__ui_nodes[c].lastTextHeight = savedLastTextH;
|
|
1346
|
+
__ui_nodes[c].box.x = origBoxX;
|
|
1347
|
+
__ui_nodes[c].box.y = origBoxY;
|
|
1348
|
+
__ui_nodes[c].box.w = origBoxW;
|
|
1349
|
+
__ui_nodes[c].box.h = origBoxH;
|
|
1350
|
+
}
|
|
1351
|
+
ui_display_set_target(prevTarget);
|
|
1352
|
+
ui_push_canvas_rect(band, rx, static_cast<int16_t>(ry + bandTop), rw, thisH);
|
|
1353
|
+
}
|
|
1354
|
+
return 1;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
// ── Per-node band renderer ────────────────────────────────────────────────
|
|
1358
|
+
// Tear-free single-node repaint when the full node-sized repair canvas won't
|
|
1359
|
+
// allocate (e.g. a full-width button on a no-PSRAM target: a 284×52 paint rect
|
|
1360
|
+
// is ~29KB, too big for internal SRAM, so ui_get_repair_canvas returns null and
|
|
1361
|
+
// the repaint would otherwise fall back to direct clear→redraw-to-SPI, which
|
|
1362
|
+
// flashes). Mirrors ui_render_scroll_bands but composites a SINGLE node over
|
|
1363
|
+
// its paint rect, reusing the persistent __ui_band_canvas (~10KB at
|
|
1364
|
+
// UI_STRIP_BAND_HEIGHT, proven to allocate on no-PSRAM targets). Each band is
|
|
1365
|
+
// seeded with the parent backdrop, the node's shadow+body+outline composited
|
|
1366
|
+
// into it, then pushed — one transaction per band — so nothing reaches the
|
|
1367
|
+
// panel until the band is complete (no tearing). Returns 1 if the band canvas
|
|
1368
|
+
// allocated and the node was rendered; 0 if even the band canvas can't
|
|
1369
|
+
// allocate (caller falls back to the direct-draw path).
|
|
1370
|
+
//
|
|
1371
|
+
// prX/prY/prW/prH = the node's paint rect in DISPLAY coords (the union of
|
|
1372
|
+
// rest-shadow + pressed-face + outline, as computed by ui_node_paint_rect).
|
|
1373
|
+
static inline uint8_t ui_render_node_bands(uint16_t i, int16_t prX, int16_t prY, int16_t prW, int16_t prH) {
|
|
1374
|
+
if (i >= __ui_node_count || prW <= 0 || prH <= 0) return 0;
|
|
1375
|
+
// Band canvas: reuse the persistent slot, reallocated to prW width (so stride
|
|
1376
|
+
// == prW for the single-write push fast path). See ui_band_canvas_for_width.
|
|
1377
|
+
CuttlefishCanvas16* band = ui_band_canvas_for_width(prW);
|
|
1378
|
+
if (!band) return 0;
|
|
1379
|
+
int16_t bandH = display_canvasHeight(band);
|
|
1380
|
+
|
|
1381
|
+
// Display-space draw coords for the node (untranslated — band-local offsets
|
|
1382
|
+
// are applied per band below). baseDraw excludes the pressed offset; draw
|
|
1383
|
+
// includes it (a pressed button's face sits 3px below its shadow).
|
|
1384
|
+
int16_t dispBaseDrawX = ui_base_draw_x_for_node(i);
|
|
1385
|
+
int16_t dispBaseDrawY = ui_base_draw_y_for_node(i);
|
|
1386
|
+
int16_t dispDrawX = ui_draw_x_for_node(i);
|
|
1387
|
+
int16_t dispDrawY = ui_draw_y_for_node(i);
|
|
1388
|
+
int16_t origBoxX = __ui_nodes[i].box.x;
|
|
1389
|
+
int16_t origBoxY = __ui_nodes[i].box.y;
|
|
1390
|
+
|
|
1391
|
+
// Per-kind draw metrics (same as the main loop / scroll band path).
|
|
1392
|
+
uint8_t ts = __ui_nodes[i].textSize ? __ui_nodes[i].textSize : 2;
|
|
1393
|
+
uint16_t textMaxW = ui_node_text_max_width(i);
|
|
1394
|
+
uint16_t tw = 0;
|
|
1395
|
+
uint16_t th = 0;
|
|
1396
|
+
ui_node_text_layout_metrics(i, textMaxW, &tw, &th);
|
|
1397
|
+
uint16_t paintTextW = tw;
|
|
1398
|
+
uint16_t paintTextH = th;
|
|
1399
|
+
if (__ui_nodes[i].kind == NODE_TEXT || __ui_nodes[i].kind == NODE_SELECT) {
|
|
1400
|
+
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;
|
|
1401
|
+
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;
|
|
1402
|
+
paintTextW = static_cast<uint16_t>(tw + hInset);
|
|
1403
|
+
paintTextH = static_cast<uint16_t>(th + vInset);
|
|
1404
|
+
}
|
|
1405
|
+
if (__ui_nodes[i].kind == NODE_CHECK || __ui_nodes[i].kind == NODE_RADIO) {
|
|
1406
|
+
paintTextW = static_cast<uint16_t>(tw + 22);
|
|
1407
|
+
if (paintTextH < 16) paintTextH = 16;
|
|
1408
|
+
}
|
|
1409
|
+
const char* displayText = __ui_nodes[i].hasTextBinding
|
|
1410
|
+
? __ui_nodes[i].textBuffer
|
|
1411
|
+
: __ui_nodes[i].text;
|
|
1412
|
+
UI_COLOR_T bColor = __ui_nodes[i].borderColor ? __ui_nodes[i].borderColor : __ui_nodes[i].fg;
|
|
1413
|
+
UI_COLOR_T fillBg = __ui_nodes[i].bg;
|
|
1414
|
+
if (__ui_nodes[i].opacity < 100) {
|
|
1415
|
+
UI_COLOR_T backdrop = ui_parent_clear_color(i);
|
|
1416
|
+
bColor = ui_blend(bColor, backdrop, __ui_nodes[i].opacity);
|
|
1417
|
+
fillBg = ui_blend(__ui_nodes[i].bg, backdrop, __ui_nodes[i].opacity);
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
CuttlefishDisplayTarget* prevTarget = ui_display_get_target();
|
|
1421
|
+
// Top→bottom bands over the paint rect.
|
|
1422
|
+
for (int16_t bandTop = 0; bandTop < prH; bandTop += bandH) {
|
|
1423
|
+
int16_t bandBot = bandTop + bandH;
|
|
1424
|
+
if (bandBot > prH) bandBot = prH;
|
|
1425
|
+
int16_t thisH = static_cast<int16_t>(bandBot - bandTop);
|
|
1426
|
+
// Seed the band with the parent backdrop at this band's Y slice. The seed
|
|
1427
|
+
// fills the whole band; bandTop shifts the parent fill rect so its rows
|
|
1428
|
+
// land in [0, thisH) of the canvas.
|
|
1429
|
+
ui_seed_paint_canvas_for_node(i, band, prX, prY + bandTop, bandTop);
|
|
1430
|
+
ui_display_set_target(band);
|
|
1431
|
+
// Band-local coords: subtract the paint-rect origin and the band's top row.
|
|
1432
|
+
int16_t baseDrawX = static_cast<int16_t>(dispBaseDrawX - prX);
|
|
1433
|
+
int16_t baseDrawY = static_cast<int16_t>(dispBaseDrawY - prY - bandTop);
|
|
1434
|
+
int16_t drawX = static_cast<int16_t>(dispDrawX - prX);
|
|
1435
|
+
int16_t drawY = static_cast<int16_t>(dispDrawY - prY - bandTop);
|
|
1436
|
+
// Outset shadow at the rest position (baseDraw), under the face. Mirrors
|
|
1437
|
+
// the main loop's pre-body shadow draw. Lists skip the outset shadow in
|
|
1438
|
+
// the main loop (their shadow is drawn elsewhere); match that here.
|
|
1439
|
+
uint8_t skipListOutsetShadow = (__ui_nodes[i].kind == NODE_LIST);
|
|
1440
|
+
if (!skipListOutsetShadow) {
|
|
1441
|
+
__ui_nodes[i].box.x = baseDrawX;
|
|
1442
|
+
ui_draw_shadow(i, baseDrawY, 0);
|
|
1443
|
+
}
|
|
1444
|
+
__ui_nodes[i].box.x = drawX;
|
|
1445
|
+
UINodeDrawCtx ctx;
|
|
1446
|
+
ctx.drawY = drawY;
|
|
1447
|
+
ctx.bColor = bColor;
|
|
1448
|
+
ctx.fillBg = fillBg;
|
|
1449
|
+
ctx.ts = ts;
|
|
1450
|
+
ctx.textMaxW = textMaxW;
|
|
1451
|
+
ctx.tw = tw;
|
|
1452
|
+
ctx.th = th;
|
|
1453
|
+
ctx.paintTextW = paintTextW;
|
|
1454
|
+
ctx.paintTextH = paintTextH;
|
|
1455
|
+
ctx.displayText = displayText;
|
|
1456
|
+
ctx.drawingBufferedScroll = 0;
|
|
1457
|
+
ctx.drawTarget = prevTarget;
|
|
1458
|
+
ctx.origBoxX = origBoxX;
|
|
1459
|
+
ctx.origBoxY = origBoxY;
|
|
1460
|
+
// Each band starts on a freshly seeded canvas, so the persistent
|
|
1461
|
+
// incremental-paint caches (lastTextWidth/Height) are invalid mid-band.
|
|
1462
|
+
// Save/restore so the band path is transient (see ui_render_scroll_bands).
|
|
1463
|
+
int16_t savedLastTextW = __ui_nodes[i].lastTextWidth;
|
|
1464
|
+
uint16_t savedLastTextH = __ui_nodes[i].lastTextHeight;
|
|
1465
|
+
if (__ui_nodes[i].kind == NODE_PROGRESS || __ui_nodes[i].kind == NODE_RANGE) {
|
|
1466
|
+
__ui_nodes[i].lastTextWidth = -1;
|
|
1467
|
+
}
|
|
1468
|
+
__ui_nodes[i].lastTextHeight = 0;
|
|
1469
|
+
if (!ui_draw_node_body(static_cast<int16_t>(i), &ctx)) {
|
|
1470
|
+
ui_draw_node_outline(i, __ui_nodes[i].box.x, drawY);
|
|
1471
|
+
}
|
|
1472
|
+
__ui_nodes[i].lastTextWidth = savedLastTextW;
|
|
1473
|
+
__ui_nodes[i].lastTextHeight = savedLastTextH;
|
|
1474
|
+
__ui_nodes[i].box.x = origBoxX;
|
|
1475
|
+
__ui_nodes[i].box.y = origBoxY;
|
|
1476
|
+
ui_display_set_target(prevTarget);
|
|
1477
|
+
// Push the full paint-rect width (== stride) in one transaction: each band
|
|
1478
|
+
// is complete before it touches the panel, eliminating the press flash.
|
|
1479
|
+
ui_push_canvas_rect(band, prX, static_cast<int16_t>(prY + bandTop), prW, thisH);
|
|
1480
|
+
}
|
|
1481
|
+
return 1;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
// ── List band renderer ────────────────────────────────────────────────────
|
|
1485
|
+
// Tear-free <list> rendering when the viewport-sized list canvas won't allocate
|
|
1486
|
+
// (a full-width list viewport is ~29KB+, too big for no-PSRAM SRAM, so the
|
|
1487
|
+
// shift-and-repair cache can't be built and the list would otherwise render
|
|
1488
|
+
// nothing). Mirrors ui_render_scroll_bands but composites the list's OWN
|
|
1489
|
+
// virtualized rows (via listItemFn) instead of a subtree walk. Reuses the
|
|
1490
|
+
// persistent __ui_band_canvas (~10KB at UI_STRIP_BAND_HEIGHT); each band is
|
|
1491
|
+
// fully rendered — content rows + scrollbar slice — before its single SPI
|
|
1492
|
+
// push, so the panel never shows a half-drawn frame. Returns 1 if the band
|
|
1493
|
+
// canvas allocated and the list was rendered+pushed+decorated; 0 if even the
|
|
1494
|
+
// band canvas can't allocate (caller renders nothing).
|
|
1495
|
+
//
|
|
1496
|
+
// Shift-and-repair is impossible without the viewport cache, so every repaint
|
|
1497
|
+
// re-renders all visible rows. For a typical ~10-20 row text list this is
|
|
1498
|
+
// affordable; lastPaintedScrollY is still updated so a later transition to
|
|
1499
|
+
// canvas mode (memory frees) starts correctly.
|
|
1500
|
+
static inline uint8_t ui_render_list_bands(uint16_t i) {
|
|
1501
|
+
if (i >= __ui_node_count) return 0;
|
|
1502
|
+
if (!__ui_nodes[i].listItemFn) return 0;
|
|
1503
|
+
int16_t bx = __ui_nodes[i].box.x;
|
|
1504
|
+
int16_t by = ui_draw_y_for_node(i);
|
|
1505
|
+
int16_t bw = __ui_nodes[i].box.w;
|
|
1506
|
+
int16_t bh = __ui_nodes[i].box.h;
|
|
1507
|
+
if (bw <= 0 || bh <= 0) return 0;
|
|
1508
|
+
uint16_t ih = __ui_nodes[i].listItemHeight > 0 ? __ui_nodes[i].listItemHeight : 24;
|
|
1509
|
+
uint16_t itemCount = __ui_nodes[i].listCount;
|
|
1510
|
+
int16_t listScrollY = __ui_nodes[i].scrollY;
|
|
1511
|
+
int16_t listContentH = __ui_nodes[i].contentHeight;
|
|
1512
|
+
UI_COLOR_T clearCol = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor;
|
|
1513
|
+
int16_t origBoxX = __ui_nodes[i].box.x;
|
|
1514
|
+
int16_t origBoxY = __ui_nodes[i].box.y;
|
|
1515
|
+
|
|
1516
|
+
// Reuse the persistent band canvas, reallocated to bw width (so stride == bw
|
|
1517
|
+
// for the single-write push fast path). See ui_band_canvas_for_width.
|
|
1518
|
+
CuttlefishCanvas16* band = ui_band_canvas_for_width(bw);
|
|
1519
|
+
if (!band) return 0;
|
|
1520
|
+
int16_t bandH = display_canvasHeight(band);
|
|
1521
|
+
|
|
1522
|
+
// Scrollbar geometry (viewport coords), same as the cached-canvas list path.
|
|
1523
|
+
// Track spans [0, bh); thumb spans [thumbY, thumbY + thumbH). 3px wide at
|
|
1524
|
+
// canvas-x (bw - 4). Composited per band so the full-width push is atomic.
|
|
1525
|
+
int16_t contentW = bw > 4 ? static_cast<int16_t>(bw - 4) : bw;
|
|
1526
|
+
uint8_t sbVisible = (listContentH > bh) ? 1 : 0;
|
|
1527
|
+
uint16_t sbThumbH = 0;
|
|
1528
|
+
uint16_t sbThumbY = 0;
|
|
1529
|
+
UI_COLOR_T sbTrackCol = (UI_COLOR_T)((__ui_nodes[i].fg >> 1) & UI_DIM_MASK);
|
|
1530
|
+
UI_COLOR_T sbThumbCol = __ui_nodes[i].fg;
|
|
1531
|
+
if (sbVisible) {
|
|
1532
|
+
sbThumbH = static_cast<uint32_t>(bh) * bh / listContentH;
|
|
1533
|
+
if (sbThumbH < 8) sbThumbH = 8;
|
|
1534
|
+
if (sbThumbH > static_cast<uint16_t>(bh)) sbThumbH = static_cast<uint16_t>(bh);
|
|
1535
|
+
int16_t maxScroll = listContentH - bh;
|
|
1536
|
+
sbThumbY = maxScroll > 0 ? static_cast<uint32_t>(bh - sbThumbH) * listScrollY / maxScroll : 0;
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
char listBuf[UI_TEXT_BUF + 1];
|
|
1540
|
+
CuttlefishDisplayTarget* listBandPrevTarget = ui_display_get_target();
|
|
1541
|
+
// Top→bottom bands over the list viewport.
|
|
1542
|
+
for (int16_t bandTop = 0; bandTop < bh; bandTop += bandH) {
|
|
1543
|
+
int16_t bandBot = bandTop + bandH;
|
|
1544
|
+
if (bandBot > bh) bandBot = bh;
|
|
1545
|
+
int16_t thisH = static_cast<int16_t>(bandBot - bandTop);
|
|
1546
|
+
// Seed content area with the list bg; the gutter is filled by the
|
|
1547
|
+
// scrollbar composite below (or cleared to bg when no scrollbar).
|
|
1548
|
+
display_canvasFillRect(band, 0, 0, contentW, bandH, clearCol);
|
|
1549
|
+
if (!sbVisible) {
|
|
1550
|
+
display_canvasFillRect(band, contentW, 0, bw - contentW, bandH, clearCol);
|
|
1551
|
+
}
|
|
1552
|
+
// Draw visible items whose row intersects this band (canvas-local Y).
|
|
1553
|
+
display_targetSetTextWrap((CuttlefishDisplayTarget*)band, false);
|
|
1554
|
+
if (itemCount > 0) {
|
|
1555
|
+
// First/last item index whose [idx*ih - listScrollY, +ih) overlaps the
|
|
1556
|
+
// band's viewport-Y range [bandTop, bandBot).
|
|
1557
|
+
uint16_t first = static_cast<uint16_t>((listScrollY + bandTop) / ih);
|
|
1558
|
+
int32_t lastSigned = (static_cast<int32_t>(listScrollY) + bandBot - 1) / ih;
|
|
1559
|
+
uint16_t last = lastSigned < 0 ? 0 : static_cast<uint16_t>(lastSigned);
|
|
1560
|
+
if (itemCount > 0 && last >= itemCount) last = itemCount - 1;
|
|
1561
|
+
for (uint16_t idx = first; idx <= last; idx++) {
|
|
1562
|
+
int16_t itemY = static_cast<int16_t>(idx * ih) - listScrollY - bandTop;
|
|
1563
|
+
// Cull rows fully outside this band (the first/last estimate can be
|
|
1564
|
+
// off by one at the edges).
|
|
1565
|
+
if (itemY + static_cast<int16_t>(ih) <= 0) continue;
|
|
1566
|
+
if (itemY >= thisH) continue;
|
|
1567
|
+
__ui_nodes[i].listItemFn(idx, listBuf, UI_TEXT_BUF + 1);
|
|
1568
|
+
listBuf[UI_TEXT_BUF] = 0;
|
|
1569
|
+
ui_display_set_target(band);
|
|
1570
|
+
int16_t __ui_saved_off_x = __ui_draw_off_x;
|
|
1571
|
+
int16_t __ui_saved_off_y = __ui_draw_off_y;
|
|
1572
|
+
__ui_draw_off_x = 0;
|
|
1573
|
+
__ui_draw_off_y = 0;
|
|
1574
|
+
// Keep classic list glyphs in the canvas pixel buffer. Native
|
|
1575
|
+
// CuttlefishGFX text can disappear when copied from a canvas to
|
|
1576
|
+
// the panel, while the explicit glyph path remains compositable.
|
|
1577
|
+
ui_draw_list_text(listBuf, 4, itemY + static_cast<int16_t>(ih - 16) / 2,
|
|
1578
|
+
__ui_nodes[i].fg, clearCol, 2, __ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing);
|
|
1579
|
+
__ui_draw_off_x = __ui_saved_off_x;
|
|
1580
|
+
__ui_draw_off_y = __ui_saved_off_y;
|
|
1581
|
+
ui_display_set_target(listBandPrevTarget);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
// Composite this band's scrollbar slice into the gutter (atomic full-width
|
|
1585
|
+
// push — no separate erase/redraw cycle, no scrollbar flash).
|
|
1586
|
+
if (sbVisible) {
|
|
1587
|
+
// Full gutter width (bw - contentW): child decorations poking past the
|
|
1588
|
+
// content area must not survive in an uncovered edge column.
|
|
1589
|
+
display_canvasFillRect(band, contentW, 0, bw - contentW, thisH, sbTrackCol);
|
|
1590
|
+
int16_t thumbTop = static_cast<int16_t>(sbThumbY);
|
|
1591
|
+
int16_t thumbBot = static_cast<int16_t>(sbThumbY + sbThumbH);
|
|
1592
|
+
int16_t ovTop = thumbTop > bandTop ? thumbTop : bandTop;
|
|
1593
|
+
int16_t ovBot = thumbBot < bandBot ? thumbBot : bandBot;
|
|
1594
|
+
if (ovBot > ovTop) {
|
|
1595
|
+
display_canvasFillRect(band, contentW, static_cast<int16_t>(ovTop - bandTop), 3, static_cast<int16_t>(ovBot - ovTop), sbThumbCol);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1598
|
+
ui_push_canvas_rect(band, bx, static_cast<int16_t>(by + bandTop), bw, thisH);
|
|
1599
|
+
}
|
|
1600
|
+
// Static decoration on top of the composited content (same order as the
|
|
1601
|
+
// cached-canvas list path: outset shadow, border, outline).
|
|
1602
|
+
if (!__ui_fb) {
|
|
1603
|
+
ui_draw_shadow(i, by, 0);
|
|
1604
|
+
}
|
|
1605
|
+
ui_draw_shadow(i, by, 1);
|
|
1606
|
+
if (__ui_nodes[i].borderStyle != 0) {
|
|
1607
|
+
UI_COLOR_T bColor = __ui_nodes[i].borderColor ? __ui_nodes[i].borderColor : __ui_nodes[i].fg;
|
|
1608
|
+
ui_draw_node_border(i, bx, by, bColor);
|
|
1609
|
+
}
|
|
1610
|
+
ui_draw_node_outline(i, bx, by);
|
|
1611
|
+
__ui_list_canvas_node = static_cast<int16_t>(i);
|
|
1612
|
+
__ui_nodes[i].lastPaintedScrollY = listScrollY;
|
|
1613
|
+
__ui_nodes[i].dirty = 0;
|
|
1614
|
+
__ui_nodes[i].box.x = origBoxX;
|
|
1615
|
+
__ui_nodes[i].box.y = origBoxY;
|
|
1616
|
+
return 1;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// Last-resort list renderer used only when neither the viewport canvas nor the
|
|
1620
|
+
// bounded band canvas can allocate. It keeps the list usable under severe heap
|
|
1621
|
+
// pressure; normal paths remain canvas-composited and tear-resistant.
|
|
1622
|
+
static inline uint8_t ui_render_list_direct(uint16_t i) {
|
|
1623
|
+
if (i >= __ui_node_count || !__ui_nodes[i].listItemFn) return 0;
|
|
1624
|
+
int16_t bx = __ui_nodes[i].box.x;
|
|
1625
|
+
int16_t by = ui_draw_y_for_node(i);
|
|
1626
|
+
int16_t bw = __ui_nodes[i].box.w;
|
|
1627
|
+
int16_t bh = __ui_nodes[i].box.h;
|
|
1628
|
+
if (bw <= 0 || bh <= 0) return 0;
|
|
1629
|
+
uint16_t ih = __ui_nodes[i].listItemHeight > 0 ? __ui_nodes[i].listItemHeight : 24;
|
|
1630
|
+
uint16_t count = __ui_nodes[i].listCount;
|
|
1631
|
+
int16_t scrollY = __ui_nodes[i].scrollY;
|
|
1632
|
+
UI_COLOR_T bg = __ui_nodes[i].hasBg ? __ui_nodes[i].bg : __ui_nodes[i].clearColor;
|
|
1633
|
+
CuttlefishDisplayTarget* target = ui_display_get_target();
|
|
1634
|
+
ui_display_set_target(target);
|
|
1635
|
+
// This fallback is used only when no framebuffer is active. Keep the fill,
|
|
1636
|
+
// native text, and scrollbar in one panel transaction so the visible fallback
|
|
1637
|
+
// remains coherent on SPI TFTs even though it does not use a canvas.
|
|
1638
|
+
if (!__ui_fb) display_startWrite();
|
|
1639
|
+
ui_display_fill_rect(bx, by, bw, bh, bg);
|
|
1640
|
+
char buf[UI_TEXT_BUF + 1];
|
|
1641
|
+
uint16_t first = static_cast<uint16_t>((scrollY + 15) / ih);
|
|
1642
|
+
int16_t lastY = static_cast<int16_t>(scrollY + bh - 16);
|
|
1643
|
+
uint16_t last = lastY >= 0 ? static_cast<uint16_t>(lastY / ih) : 0;
|
|
1644
|
+
if (count > 0 && last >= count) last = count - 1;
|
|
1645
|
+
if (count > 0 && first < count && first <= last) {
|
|
1646
|
+
for (uint16_t idx = first; idx <= last; idx++) {
|
|
1647
|
+
int16_t itemY = static_cast<int16_t>(idx * ih) - scrollY;
|
|
1648
|
+
__ui_nodes[i].listItemFn(idx, buf, UI_TEXT_BUF + 1);
|
|
1649
|
+
buf[UI_TEXT_BUF] = 0;
|
|
1650
|
+
int16_t savedX = __ui_draw_off_x;
|
|
1651
|
+
int16_t savedY = __ui_draw_off_y;
|
|
1652
|
+
__ui_draw_off_x = 0;
|
|
1653
|
+
__ui_draw_off_y = 0;
|
|
1654
|
+
ui_draw_list_text_direct(buf, bx + 4, by + itemY + static_cast<int16_t>(ih - 16) / 2,
|
|
1655
|
+
__ui_nodes[i].fg, bg, 2, __ui_nodes[i].fontFace, __ui_nodes[i].letterSpacing);
|
|
1656
|
+
__ui_draw_off_x = savedX;
|
|
1657
|
+
__ui_draw_off_y = savedY;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
if (__ui_nodes[i].contentHeight > bh) {
|
|
1661
|
+
int16_t tx = bx + bw - 4;
|
|
1662
|
+
uint16_t thumbH = static_cast<uint32_t>(bh) * bh / __ui_nodes[i].contentHeight;
|
|
1663
|
+
if (thumbH < 8) thumbH = 8;
|
|
1664
|
+
if (thumbH > static_cast<uint16_t>(bh)) thumbH = static_cast<uint16_t>(bh);
|
|
1665
|
+
int16_t maxScroll = __ui_nodes[i].contentHeight - bh;
|
|
1666
|
+
uint16_t thumbY = maxScroll > 0 ? static_cast<uint32_t>(bh - thumbH) * scrollY / maxScroll : 0;
|
|
1667
|
+
UI_COLOR_T dimFg = (UI_COLOR_T)((__ui_nodes[i].fg >> 1) & UI_DIM_MASK);
|
|
1668
|
+
ui_display_fill_rect(tx, by, 3, bh, dimFg);
|
|
1669
|
+
ui_display_fill_rect(tx, static_cast<int16_t>(by + thumbY), 3, thumbH, __ui_nodes[i].fg);
|
|
1670
|
+
}
|
|
1671
|
+
if (!__ui_fb) display_endWrite();
|
|
1672
|
+
ui_draw_shadow(i, by, 1);
|
|
1673
|
+
if (__ui_nodes[i].borderStyle != 0) {
|
|
1674
|
+
UI_COLOR_T border = __ui_nodes[i].borderColor ? __ui_nodes[i].borderColor : __ui_nodes[i].fg;
|
|
1675
|
+
ui_draw_node_border(i, bx, by, border);
|
|
1676
|
+
}
|
|
1677
|
+
ui_draw_node_outline(i, bx, by);
|
|
1678
|
+
__ui_nodes[i].lastPaintedScrollY = scrollY;
|
|
1679
|
+
__ui_nodes[i].dirty = 0;
|
|
1680
|
+
return 1;
|
|
1681
|
+
}
|
|
1682
|
+
`;
|
|
1683
|
+
}
|