@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,689 +1,689 @@
|
|
|
1
|
-
// Slice of the C++ runtime header (original source lines 5434-5769).
|
|
2
|
-
// On-screen keyboard: add_key/insert/delete/compute_box/open/close/rect/touch/tick/tap/draw.
|
|
3
|
-
// See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
|
|
4
|
-
export function emitKeyboard(): string {
|
|
5
|
-
return `
|
|
6
|
-
// ── On-screen keyboard subsystem ───────────────────────────────────────────
|
|
7
|
-
// (UIKey struct, UI_KB_* defines, __ui_kb_box, __ui_kb_visible, __ui_kb_bs_held,
|
|
8
|
-
// __ui_last_touch_x/y are declared earlier near the touch state machine so
|
|
9
|
-
// the touch functions can reference them.)
|
|
10
|
-
|
|
11
|
-
// Populated by the per-keyboard loader function (emitted by the lowering).
|
|
12
|
-
// (__ui_kb_keys is forward-declared earlier near the touch state machine.)
|
|
13
|
-
static uint8_t __ui_kb_keyCount;
|
|
14
|
-
static uint8_t __ui_kb_rows;
|
|
15
|
-
static uint8_t __ui_kb_cols;
|
|
16
|
-
static char __ui_kb_buffer[UI_TEXT_BUF + 1];
|
|
17
|
-
static uint8_t __ui_kb_len;
|
|
18
|
-
static uint8_t __ui_kb_maxlen;
|
|
19
|
-
static uint8_t __ui_kb_shift;
|
|
20
|
-
// __ui_kb_visible, __ui_kb_bs_held, __ui_last_touch_x/y are forward-declared
|
|
21
|
-
// earlier (near the touch state machine) because ui_touch_up references them.
|
|
22
|
-
// __ui_kb_target is also forward-declared there (the UI_HIDE_OSK caret/blink
|
|
23
|
-
// paths in ui_tick and the input draw reference it before this point).
|
|
24
|
-
static uint32_t __ui_kb_bs_repeat; // last auto-repeat deletion time
|
|
25
|
-
// __ui_kb_dirty is forward-declared earlier (near the touch state machine).
|
|
26
|
-
static void (*__ui_kb_onchange)();
|
|
27
|
-
// Dispatch table: one loader per input node. Indexed by input position.
|
|
28
|
-
extern void (*__ui_kb_loaders[])();
|
|
29
|
-
extern const uint16_t __ui_kb_loader_count;
|
|
30
|
-
|
|
31
|
-
static inline void ui_kb_add_key(char ch, uint8_t special, UI_COLOR_T bg, UI_COLOR_T fg, UI_COLOR_T borderColor) {
|
|
32
|
-
if (__ui_kb_keyCount >= UI_KB_MAX) return;
|
|
33
|
-
__ui_kb_keys[__ui_kb_keyCount] = { ch, special };
|
|
34
|
-
__ui_kb_styles[__ui_kb_keyCount] = { bg, fg, borderColor };
|
|
35
|
-
__ui_kb_keyCount++;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Insert a character into the buffer (if space permits).
|
|
39
|
-
static inline void ui_kb_insert(char c) {
|
|
40
|
-
if (__ui_kb_maxlen > 0 && __ui_kb_len >= __ui_kb_maxlen) return;
|
|
41
|
-
if (__ui_kb_len >= UI_TEXT_BUF) return;
|
|
42
|
-
__ui_kb_buffer[__ui_kb_len++] = c;
|
|
43
|
-
__ui_kb_buffer[__ui_kb_len] = 0;
|
|
44
|
-
#if defined(UI_HIDE_OSK)
|
|
45
|
-
// Desktop target: the OSK grid isn't drawn, so the input field itself is the
|
|
46
|
-
// only place the in-progress text appears. Sync the buffer into the target
|
|
47
|
-
// node's textBuffer and mark it dirty so the field repaints on the next tick
|
|
48
|
-
// — without this, typing appears to do nothing until Enter commits at close.
|
|
49
|
-
if (__ui_kb_target >= 0) {
|
|
50
|
-
strncpy(__ui_nodes[__ui_kb_target].textBuffer, __ui_kb_buffer, UI_TEXT_BUF);
|
|
51
|
-
__ui_nodes[__ui_kb_target].textBuffer[UI_TEXT_BUF] = 0;
|
|
52
|
-
ui_mark_dirty(static_cast<uint16_t>(__ui_kb_target));
|
|
53
|
-
}
|
|
54
|
-
#endif
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Delete one character from the buffer.
|
|
58
|
-
static inline void ui_kb_delete() {
|
|
59
|
-
if (__ui_kb_len == 0) return;
|
|
60
|
-
__ui_kb_buffer[--__ui_kb_len] = 0;
|
|
61
|
-
#if defined(UI_HIDE_OSK)
|
|
62
|
-
if (__ui_kb_target >= 0) {
|
|
63
|
-
strncpy(__ui_nodes[__ui_kb_target].textBuffer, __ui_kb_buffer, UI_TEXT_BUF);
|
|
64
|
-
__ui_nodes[__ui_kb_target].textBuffer[UI_TEXT_BUF] = 0;
|
|
65
|
-
ui_mark_dirty(static_cast<uint16_t>(__ui_kb_target));
|
|
66
|
-
}
|
|
67
|
-
#endif
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// Compute the keyboard box on open from display dimensions + grid shape.
|
|
71
|
-
// Alpha (wide grid) docks to the bottom 75%; number (narrow grid) centers at 60%.
|
|
72
|
-
// Uses the display profile dimensions if available, else 320×240.
|
|
73
|
-
#ifndef __ui_display_w
|
|
74
|
-
#define __ui_display_w 320
|
|
75
|
-
#endif
|
|
76
|
-
#ifndef __ui_display_h
|
|
77
|
-
#define __ui_display_h 240
|
|
78
|
-
#endif
|
|
79
|
-
static inline void ui_kb_compute_box() {
|
|
80
|
-
uint8_t isNumber = (__ui_kb_cols <= 4);
|
|
81
|
-
uint16_t h = isNumber ? (__ui_display_h * 60 / 100) : (__ui_display_h * 75 / 100);
|
|
82
|
-
__ui_kb_box.w = isNumber ? (__ui_display_w * 50 / 100) : __ui_display_w;
|
|
83
|
-
__ui_kb_box.h = h;
|
|
84
|
-
__ui_kb_box.x = isNumber ? (__ui_display_w - __ui_kb_box.w) / 2 : 0;
|
|
85
|
-
__ui_kb_box.y = __ui_display_h - h;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Open the keyboard for an input node.
|
|
89
|
-
static inline void ui_kb_open(uint16_t nodeIdx, uint8_t inputPosition) {
|
|
90
|
-
__ui_kb_target = nodeIdx;
|
|
91
|
-
strncpy(__ui_kb_buffer, __ui_nodes[nodeIdx].textBuffer, UI_TEXT_BUF);
|
|
92
|
-
__ui_kb_buffer[UI_TEXT_BUF] = 0;
|
|
93
|
-
__ui_kb_len = strlen(__ui_kb_buffer);
|
|
94
|
-
uint16_t ml = __ui_nodes[nodeIdx].maxlen;
|
|
95
|
-
__ui_kb_maxlen = (ml > 0 && ml <= UI_TEXT_BUF) ? static_cast<uint8_t>(ml) : UI_TEXT_BUF;
|
|
96
|
-
__ui_kb_shift = 0;
|
|
97
|
-
__ui_kb_bs_held = 0;
|
|
98
|
-
// Load the key set via the dispatch table.
|
|
99
|
-
if (inputPosition < __ui_kb_loader_count) __ui_kb_loaders[inputPosition]();
|
|
100
|
-
__ui_kb_set_onchange();
|
|
101
|
-
ui_kb_compute_box();
|
|
102
|
-
__ui_kb_visible = 1;
|
|
103
|
-
__ui_kb_dirty = 1; // redraw on the first visible frame
|
|
104
|
-
// Do NOT pre-mark the tree dirty here. While the keyboard is visible the
|
|
105
|
-
// draw pass is skipped (its opaque background covers app nodes), so any
|
|
106
|
-
// dirty flags set now are never consumed/cleared — they survive until close,
|
|
107
|
-
// and the first post-close frame then repaints every dirty node = full-screen
|
|
108
|
-
// flash on SPI TFTs. The close path scopes the repaint to nodes whose paint
|
|
109
|
-
// rect intersects __ui_kb_box, which is the only region that needs restoring.
|
|
110
|
-
#if defined(UI_HIDE_OSK)
|
|
111
|
-
// Desktop target: the OSK isn't drawn, so the draw pass runs normally and the
|
|
112
|
-
// input field must repaint on focus to show the caret BEFORE the first
|
|
113
|
-
// keystroke. (The full-screen-flash concern above doesn't apply — there's no
|
|
114
|
-
// opaque overlay being skipped.) Seed the blink phase so the caret is ON for
|
|
115
|
-
// the first ~530ms (bit 0x20 set → visible), so focus feels immediate.
|
|
116
|
-
if (__ui_kb_target >= 0) ui_mark_dirty(static_cast<uint16_t>(__ui_kb_target));
|
|
117
|
-
__ui_kb_blink = 0x20;
|
|
118
|
-
#endif
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Close the keyboard: commit buffer back to the input node.
|
|
122
|
-
static inline void ui_kb_close() {
|
|
123
|
-
if (__ui_kb_target >= 0) {
|
|
124
|
-
strncpy(__ui_nodes[__ui_kb_target].textBuffer, __ui_kb_buffer, UI_TEXT_BUF);
|
|
125
|
-
__ui_nodes[__ui_kb_target].textBuffer[UI_TEXT_BUF] = 0;
|
|
126
|
-
if (__ui_kb_onchange) __ui_kb_onchange();
|
|
127
|
-
}
|
|
128
|
-
// Capture the keyboard box before clearing visibility — it identifies the
|
|
129
|
-
// screen region the opaque overlay covered and that now needs restoring.
|
|
130
|
-
UIRect kbBox = __ui_kb_box;
|
|
131
|
-
__ui_kb_visible = 0;
|
|
132
|
-
__ui_kb_target = -1;
|
|
133
|
-
__ui_kb_bs_held = 0;
|
|
134
|
-
// The <screen> root's paint rect spans the whole display, so it always
|
|
135
|
-
// intersects kbBox. Routing it through ui_mark_dirty would cascade via
|
|
136
|
-
// ui_mark_overlapping_higher_layers_dirty into marking every node on the
|
|
137
|
-
// active screen dirty (its rect overlaps everything), which is the
|
|
138
|
-
// full-screen flash this function exists to avoid. Repaint just the
|
|
139
|
-
// keyboard-box slice of its background directly instead.
|
|
140
|
-
int16_t screenRoot = -1;
|
|
141
|
-
for (uint16_t i = 0; i < __ui_node_count; i++) {
|
|
142
|
-
if (__ui_nodes[i].parent == UI_NO_PARENT && __ui_nodes[i].screenId == __ui_active_screen) {
|
|
143
|
-
screenRoot = static_cast<int16_t>(i);
|
|
144
|
-
break;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
if (screenRoot >= 0) {
|
|
148
|
-
UI_COLOR_T rootBg = (UI_COLOR_T)(__ui_nodes[screenRoot].hasBg
|
|
149
|
-
? __ui_nodes[screenRoot].bg
|
|
150
|
-
: __ui_nodes[screenRoot].clearColor);
|
|
151
|
-
ui_display_fill_rect(kbBox.x, kbBox.y, kbBox.w, kbBox.h, rootBg);
|
|
152
|
-
}
|
|
153
|
-
// Repaint everything else the keyboard overlay actually overwrote: nodes
|
|
154
|
-
// whose paint rect intersects the keyboard box, plus the edited input
|
|
155
|
-
// itself (its text just changed). ui_mark_dirty handles overlap repair +
|
|
156
|
-
// scroll clipping — safe here since these nodes are bounded in size, unlike
|
|
157
|
-
// the screen root.
|
|
158
|
-
for (uint16_t i = 0; i < __ui_node_count; i++) {
|
|
159
|
-
if (static_cast<int16_t>(i) == screenRoot) continue;
|
|
160
|
-
UIRect r;
|
|
161
|
-
ui_node_current_paint_rect(i, &r);
|
|
162
|
-
if (r.w > 0 && r.h > 0 &&
|
|
163
|
-
ui_rects_intersect(r.x, r.y, r.w, r.h, kbBox.x, kbBox.y, kbBox.w, kbBox.h)) {
|
|
164
|
-
ui_mark_dirty(i);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Compute a key's rect from its index, given the grid + box.
|
|
170
|
-
// The top UI_KB_TEXT_H pixels of the box are reserved for the preview text row;
|
|
171
|
-
// keys fill the area below it.
|
|
172
|
-
static inline void ui_kb_key_rect(uint8_t idx, UIRect* out) {
|
|
173
|
-
uint8_t col = idx % __ui_kb_cols;
|
|
174
|
-
uint8_t row = idx / __ui_kb_cols;
|
|
175
|
-
int16_t keysH = __ui_kb_box.h - UI_KB_TEXT_H; // key area height (below text row)
|
|
176
|
-
out->x = __ui_kb_box.x + static_cast<int16_t>(col) * __ui_kb_box.w / __ui_kb_cols;
|
|
177
|
-
out->y = __ui_kb_box.y + UI_KB_TEXT_H + static_cast<int16_t>(row) * keysH / __ui_kb_rows;
|
|
178
|
-
out->w = __ui_kb_box.w / __ui_kb_cols;
|
|
179
|
-
out->h = keysH / __ui_kb_rows;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// Handle a touch-down inside the keyboard box. tx,ty are display coords.
|
|
183
|
-
// Handle a touch-down inside the keyboard box. Only fires on the initial
|
|
184
|
-
// down edge (tracked by __ui_touch_state in the modal path), NOT every poll.
|
|
185
|
-
// Records WHICH key is under the finger — the same key is activated on release
|
|
186
|
-
// (ui_kb_handle_tap), avoiding mis-targeting from coordinate drift on release.
|
|
187
|
-
static inline void ui_kb_handle_touch(int16_t tx, int16_t ty) {
|
|
188
|
-
__ui_kb_pressed_key = -1;
|
|
189
|
-
for (uint8_t i = 0; i < __ui_kb_keyCount; i++) {
|
|
190
|
-
UIKey k = __ui_kb_keys[i];
|
|
191
|
-
if (k.special == 255) continue; // padding cell, skip
|
|
192
|
-
UIRect r;
|
|
193
|
-
ui_kb_key_rect(i, &r);
|
|
194
|
-
if (tx >= r.x && tx < r.x + r.w && ty >= r.y && ty < r.y + r.h) {
|
|
195
|
-
__ui_kb_pressed_key = static_cast<int8_t>(i); // remember for release
|
|
196
|
-
__ui_kb_repaint_key = static_cast<int8_t>(i);
|
|
197
|
-
__ui_kb_dirty = 2; // targeted: redraw just this key's highlight
|
|
198
|
-
// Backspace starts deleting immediately + arms auto-repeat.
|
|
199
|
-
if (k.special == 2) {
|
|
200
|
-
__ui_kb_bs_held = 1;
|
|
201
|
-
__ui_kb_bs_repeat =
|
|
202
|
-
ui_kb_delete();
|
|
203
|
-
}
|
|
204
|
-
return;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Called each frame while the keyboard is visible + a touch is held.
|
|
210
|
-
// Handles ⌫ auto-repeat.
|
|
211
|
-
static inline void ui_kb_tick(uint32_t now) {
|
|
212
|
-
if (!__ui_kb_bs_held) return;
|
|
213
|
-
if (now - __ui_kb_bs_repeat >= UI_KB_REPEAT_MS) {
|
|
214
|
-
ui_kb_delete();
|
|
215
|
-
__ui_kb_bs_repeat = now;
|
|
216
|
-
__ui_kb_repaint_key = __ui_kb_pressed_key; // ⌫ key stays highlighted
|
|
217
|
-
__ui_kb_dirty = 2; // targeted: text row + ⌫ key
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Handle a tap release. Activates the key that was under the finger on
|
|
222
|
-
// touch-down (__ui_kb_pressed_key) — NOT a fresh hit-test, which would
|
|
223
|
-
// mis-target due to coordinate drift on a resistive panel at release.
|
|
224
|
-
static inline void ui_kb_handle_tap(int16_t tx, int16_t ty) {
|
|
225
|
-
(void)tx; (void)ty; // key was recorded on touch-down; no re-hit-test
|
|
226
|
-
if (__ui_kb_pressed_key < 0) return;
|
|
227
|
-
UIKey k = __ui_kb_keys[__ui_kb_pressed_key];
|
|
228
|
-
int8_t releasedKey = __ui_kb_pressed_key;
|
|
229
|
-
__ui_kb_pressed_key = -1; // clear pressed state → highlight reverts
|
|
230
|
-
switch (k.special) {
|
|
231
|
-
case 0: { // char
|
|
232
|
-
char c = k.ch;
|
|
233
|
-
uint8_t wasShift = __ui_kb_shift;
|
|
234
|
-
if (wasShift && c >= 'a' && c <= 'z') c -= 32;
|
|
235
|
-
ui_kb_insert(c);
|
|
236
|
-
__ui_kb_shift = 0; // shift resets after one char
|
|
237
|
-
// Text row changes; if shift was active, repaint shift key too.
|
|
238
|
-
__ui_kb_repaint_key = wasShift ? -1 : releasedKey;
|
|
239
|
-
__ui_kb_dirty = 2;
|
|
240
|
-
break;
|
|
241
|
-
}
|
|
242
|
-
case 1: // shift toggle — all letter keys change case, full redraw
|
|
243
|
-
__ui_kb_shift = !__ui_kb_shift;
|
|
244
|
-
__ui_kb_dirty = 1;
|
|
245
|
-
break;
|
|
246
|
-
case 2: // backspace: handled on down + repeat; nothing on tap-up
|
|
247
|
-
__ui_kb_repaint_key = releasedKey; // revert highlight
|
|
248
|
-
__ui_kb_dirty = 2;
|
|
249
|
-
break;
|
|
250
|
-
case 3: // OK
|
|
251
|
-
ui_kb_close();
|
|
252
|
-
break;
|
|
253
|
-
case 4: { // page-swap — new key layout, full redraw
|
|
254
|
-
extern void __ui_kb_load_default_alpha();
|
|
255
|
-
extern void __ui_kb_load_default_number();
|
|
256
|
-
if (__ui_kb_cols <= 4) __ui_kb_load_default_alpha();
|
|
257
|
-
else __ui_kb_load_default_number();
|
|
258
|
-
ui_kb_compute_box();
|
|
259
|
-
__ui_kb_dirty = 1;
|
|
260
|
-
break;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Draw a single key by index. Shared by the full draw + targeted redraw.
|
|
266
|
-
static inline void ui_kb_draw_key(uint8_t i) {
|
|
267
|
-
UIKey k = __ui_kb_keys[i];
|
|
268
|
-
UIKeyStyle ks = __ui_kb_styles[i];
|
|
269
|
-
UIRect r;
|
|
270
|
-
ui_kb_key_rect(i, &r);
|
|
271
|
-
UI_COLOR_T bg = ks.bg;
|
|
272
|
-
UI_COLOR_T fg = ks.fg;
|
|
273
|
-
UI_COLOR_T border = ks.borderColor;
|
|
274
|
-
// Shift-active highlight: brighten the shift key's background — but only
|
|
275
|
-
// when not pressed, so the press inversion stays high-contrast.
|
|
276
|
-
if (k.special == 1 && __ui_kb_shift && static_cast<int8_t>(i) != __ui_kb_pressed_key) {
|
|
277
|
-
#if UI_COLOR_DEPTH == 888
|
|
278
|
-
bg = 0xBDEFFF;
|
|
279
|
-
#else
|
|
280
|
-
bg = 0xBDF7;
|
|
281
|
-
#endif
|
|
282
|
-
}
|
|
283
|
-
// Pressed key: invert colors for clear tap feedback.
|
|
284
|
-
if (static_cast<int8_t>(i) == __ui_kb_pressed_key) { UI_COLOR_T t = bg; bg = fg; fg = t; }
|
|
285
|
-
ui_display_fill_rect(r.x + 1, r.y + 1, r.w - 2, r.h - 2, bg);
|
|
286
|
-
ui_display_draw_rect(r.x + 1, r.y + 1, r.w - 2, r.h - 2, border);
|
|
287
|
-
ui_display_set_text_color(fg, bg);
|
|
288
|
-
ui_display_set_text_size(1);
|
|
289
|
-
// Derive the label string + its length for centering.
|
|
290
|
-
const char* labelStr;
|
|
291
|
-
char single[2];
|
|
292
|
-
switch (k.special) {
|
|
293
|
-
case 1: labelStr = "Aa"; break;
|
|
294
|
-
case 2: labelStr = "DEL"; break;
|
|
295
|
-
case 3: labelStr = "OK"; break;
|
|
296
|
-
case 4: labelStr = __ui_kb_cols <= 4 ? "ABC" : "123"; break;
|
|
297
|
-
default:
|
|
298
|
-
single[0] = (__ui_kb_shift && k.ch >= 'a' && k.ch <= 'z') ? static_cast<char>(k.ch - 32) : k.ch;
|
|
299
|
-
single[1] = 0;
|
|
300
|
-
labelStr = single;
|
|
301
|
-
break;
|
|
302
|
-
}
|
|
303
|
-
// Center: textW = len * 6px, textH = 8px. Position inside the key rect.
|
|
304
|
-
uint8_t len = strlen(labelStr);
|
|
305
|
-
int16_t textW = static_cast<int16_t>(len) * 6;
|
|
306
|
-
int16_t textH = 8;
|
|
307
|
-
int16_t cx = r.x + (r.w - textW) / 2;
|
|
308
|
-
int16_t cy = r.y + (r.h - textH) / 2;
|
|
309
|
-
if (cx < r.x + 1) cx = r.x + 1; // clamp if label wider than key
|
|
310
|
-
ui_display_set_cursor(cx, cy);
|
|
311
|
-
ui_display_print(labelStr);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
// Redraw only the text display row (top of keyboard box). Used when a char is
|
|
315
|
-
// inserted/deleted without changing key highlights.
|
|
316
|
-
static inline void ui_kb_draw_text_row() {
|
|
317
|
-
// Clear the text row area (top UI_KB_TEXT_H px of the keyboard box).
|
|
318
|
-
ui_display_fill_rect(__ui_kb_box.x, __ui_kb_box.y, __ui_kb_box.w, UI_KB_TEXT_H, __ui_kb_bg);
|
|
319
|
-
ui_display_set_cursor(__ui_kb_box.x + 4, __ui_kb_box.y + 4);
|
|
320
|
-
#if UI_COLOR_DEPTH == 888
|
|
321
|
-
ui_display_set_text_color(0xFFFFFF, 0x000000);
|
|
322
|
-
#else
|
|
323
|
-
ui_display_set_text_color(0xFFFF, 0x0000);
|
|
324
|
-
#endif
|
|
325
|
-
ui_display_set_text_size(2);
|
|
326
|
-
ui_display_print(__ui_kb_buffer);
|
|
327
|
-
ui_display_print("_"); // cursor
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// Draw the full keyboard overlay (background + text row + all keys).
|
|
331
|
-
// Renders into a dedicated offscreen canvas first, then pushes in a single
|
|
332
|
-
// SPI transaction — avoids the per-key/per-rect display writes that show up
|
|
333
|
-
// as visible row-by-row painting on SPI TFTs.
|
|
334
|
-
// (__ui_kb_canvas is forward-declared in the touch-keyboard-fwd slice.)
|
|
335
|
-
static inline void ui_kb_draw() {
|
|
336
|
-
int16_t kw = __ui_kb_box.w;
|
|
337
|
-
int16_t kh = __ui_kb_box.h;
|
|
338
|
-
if (kw <= 0 || kh <= 0) return;
|
|
339
|
-
// (Re)allocate the keyboard canvas to exact size (see repair-canvas comment
|
|
340
|
-
// — stride mismatch corrupts the push when reusing a differently-sized canvas).
|
|
341
|
-
// ui_create_canvas_best prefers PSRAM when available (a full-width keyboard
|
|
342
|
-
// canvas can be ~30KB, too big for no-PSRAM internal SRAM) and falls back to
|
|
343
|
-
// SRAM — same policy as the list/scroll viewport canvases.
|
|
344
|
-
if (!__ui_kb_canvas || !display_canvasBuffer(__ui_kb_canvas) ||
|
|
345
|
-
display_canvasWidth(__ui_kb_canvas) != kw ||
|
|
346
|
-
display_canvasHeight(__ui_kb_canvas) != kh) {
|
|
347
|
-
display_deleteCanvas(__ui_kb_canvas);
|
|
348
|
-
__ui_kb_canvas = ui_create_canvas_best(kw, kh);
|
|
349
|
-
}
|
|
350
|
-
if (!__ui_kb_canvas || !display_canvasBuffer(__ui_kb_canvas)) {
|
|
351
|
-
// Canvas alloc failed — fall back to direct draw (slow but correct).
|
|
352
|
-
ui_display_fill_rect(__ui_kb_box.x, __ui_kb_box.y, kw, kh, __ui_kb_bg);
|
|
353
|
-
ui_kb_draw_text_row();
|
|
354
|
-
for (uint8_t i = 0; i < __ui_kb_keyCount; i++) {
|
|
355
|
-
if (__ui_kb_keys[i].special == 255) continue;
|
|
356
|
-
ui_kb_draw_key(i);
|
|
357
|
-
}
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
// Redirect drawing into the canvas. All ui_display_* calls below go to RAM.
|
|
361
|
-
CuttlefishDisplayTarget* __kb_prev_target = ui_display_get_target();
|
|
362
|
-
ui_display_set_target(__ui_kb_canvas);
|
|
363
|
-
// Opaque background over the keyboard box (canvas-local 0,0).
|
|
364
|
-
ui_display_fill_rect(0, 0, kw, kh, __ui_kb_bg);
|
|
365
|
-
// Text row + keys: temporarily shift box origin to canvas-local so the
|
|
366
|
-
// existing draw_text_row / draw_key functions compute coords at (0,0).
|
|
367
|
-
int16_t saveBoxX = __ui_kb_box.x;
|
|
368
|
-
int16_t saveBoxY = __ui_kb_box.y;
|
|
369
|
-
__ui_kb_box.x = 0;
|
|
370
|
-
__ui_kb_box.y = 0;
|
|
371
|
-
ui_kb_draw_text_row();
|
|
372
|
-
for (uint8_t i = 0; i < __ui_kb_keyCount; i++) {
|
|
373
|
-
if (__ui_kb_keys[i].special == 255) continue;
|
|
374
|
-
ui_kb_draw_key(i);
|
|
375
|
-
}
|
|
376
|
-
__ui_kb_box.x = saveBoxX;
|
|
377
|
-
__ui_kb_box.y = saveBoxY;
|
|
378
|
-
// Restore display target and push the canvas in one transaction.
|
|
379
|
-
ui_display_set_target(__kb_prev_target);
|
|
380
|
-
display_startWrite();
|
|
381
|
-
display_setAddrWindow(saveBoxX, saveBoxY, kw, kh);
|
|
382
|
-
display_writePixels(display_canvasBuffer(__ui_kb_canvas), static_cast<uint32_t>(kw) * kh);
|
|
383
|
-
display_endWrite();
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
static inline void ui_select_menu_geom(uint16_t nodeIdx, UIRect* out, int16_t* rowH) {
|
|
387
|
-
const UINode* n = &__ui_nodes[nodeIdx];
|
|
388
|
-
int16_t rows = static_cast<int16_t>(n->optionCount);
|
|
389
|
-
if (rows < 1) rows = 1;
|
|
390
|
-
*rowH = 22;
|
|
391
|
-
// Anchor to the select itself: same left edge and width, so the modal
|
|
392
|
-
// reads as the control's own dropdown (never overhangs toward a scrollbar).
|
|
393
|
-
// Clamp to the screen for selects that extend past it.
|
|
394
|
-
int16_t x = n->box.x;
|
|
395
|
-
if (x < 0) x = 0;
|
|
396
|
-
int16_t w = n->box.w;
|
|
397
|
-
if (w < 120) w = 120;
|
|
398
|
-
if (x + w > static_cast<int16_t>(__ui_display_w)) w = static_cast<int16_t>(__ui_display_w) - x;
|
|
399
|
-
int16_t h = rows * (*rowH) + 8;
|
|
400
|
-
int16_t maxH = static_cast<int16_t>(__ui_display_h) * 3 / 5;
|
|
401
|
-
if (h > maxH) h = maxH;
|
|
402
|
-
out->x = x;
|
|
403
|
-
out->y = static_cast<int16_t>((static_cast<int16_t>(__ui_display_h) - h) / 2);
|
|
404
|
-
out->w = w;
|
|
405
|
-
out->h = h;
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
static inline void ui_select_menu_open(uint16_t nodeIdx) {
|
|
409
|
-
if (__ui_nodes[nodeIdx].kind != NODE_SELECT) return;
|
|
410
|
-
if (__ui_nodes[nodeIdx].optionCount < 1) return;
|
|
411
|
-
__ui_select_menu = static_cast<int16_t>(nodeIdx);
|
|
412
|
-
__ui_select_menu_dirty = 1;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
static inline void ui_select_menu_close(uint8_t repaint) {
|
|
416
|
-
if (__ui_select_menu < 0) return;
|
|
417
|
-
__ui_select_menu = -1;
|
|
418
|
-
__ui_select_menu_dirty = 0;
|
|
419
|
-
if (repaint) {
|
|
420
|
-
// Erase repaint: the whole active screen redraws beneath the vanished
|
|
421
|
-
// overlay (same contract as navigate's mark-all-dirty).
|
|
422
|
-
for (uint16_t i = 0; i < __ui_node_count; i++) {
|
|
423
|
-
__ui_nodes[i].dirty = 1;
|
|
424
|
-
__ui_nodes[i].lastTextHeight = 0;
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
static inline void ui_select_menu_draw() {
|
|
430
|
-
if (__ui_select_menu < 0) return;
|
|
431
|
-
uint16_t idx = static_cast<uint16_t>(__ui_select_menu);
|
|
432
|
-
const UINode* n = &__ui_nodes[idx];
|
|
433
|
-
UIRect g;
|
|
434
|
-
int16_t rowH;
|
|
435
|
-
ui_select_menu_geom(idx, &g, &rowH);
|
|
436
|
-
// Themed panel: the select's own radius/border/bg — the modal reads as
|
|
437
|
-
// the control's popover, matching the surrounding UI (device parity with
|
|
438
|
-
// the preview's drawSelectMenu theming).
|
|
439
|
-
int16_t radius = n->borderRadius > 0 ? n->borderRadius : 6;
|
|
440
|
-
if (radius > g.w / 2) radius = g.w / 2;
|
|
441
|
-
if (radius > g.h / 2) radius = g.h / 2;
|
|
442
|
-
UI_COLOR_T panel = n->hasBg ? n->bg : n->clearColor;
|
|
443
|
-
UI_COLOR_T borderCol = (n->borderStyle != 0 && n->borderColor != 0) ? n->borderColor : n->fg;
|
|
444
|
-
ui_display_fill_round_rect(g.x, g.y, g.w, g.h, radius, panel);
|
|
445
|
-
ui_display_draw_round_rect(g.x, g.y, g.w, g.h, radius, borderCol);
|
|
446
|
-
char buf[UI_TEXT_BUF + 1];
|
|
447
|
-
int16_t rowInset = radius / 2; if (rowInset < 2) rowInset = 2;
|
|
448
|
-
int16_t rowRadius = radius; if (rowRadius > 6) rowRadius = 6;
|
|
449
|
-
for (uint8_t r = 0; r < n->optionCount; r++) {
|
|
450
|
-
int16_t ry = g.y + 4 + static_cast<int16_t>(r) * rowH;
|
|
451
|
-
uint8_t current = (r == static_cast<uint8_t>(n->value));
|
|
452
|
-
// The selected row carries the :checked pair (the kit wires it to
|
|
453
|
-
// --accent/--accent-foreground, shadcn's SelectItem selected state).
|
|
454
|
-
UI_COLOR_T rowBg = n->hasCheckedBg ? static_cast<UI_COLOR_T>(n->checkedBg) : n->fg;
|
|
455
|
-
UI_COLOR_T rowFg = n->hasCheckedFg ? static_cast<UI_COLOR_T>(n->checkedFg) : panel;
|
|
456
|
-
if (current) {
|
|
457
|
-
ui_display_fill_round_rect(g.x + rowInset, ry, g.w - 2 * rowInset, rowH, rowRadius, rowBg);
|
|
458
|
-
}
|
|
459
|
-
buf[0] = 0;
|
|
460
|
-
if (n->optionTextFn) n->optionTextFn(r, buf, UI_TEXT_BUF + 1);
|
|
461
|
-
ui_draw_wrapped_text(buf,
|
|
462
|
-
g.x + 22, ry + (rowH - static_cast<int16_t>(8 * n->textSize)) / 2,
|
|
463
|
-
static_cast<uint16_t>(g.w - 30),
|
|
464
|
-
current ? rowFg : n->fg,
|
|
465
|
-
panel, n->textSize, n->fontAntialias, n->fontFace, n->letterSpacing,
|
|
466
|
-
n->lineHeight, n->whiteSpaceMode, 0, 0, 0);
|
|
467
|
-
if (current) {
|
|
468
|
-
int16_t cx = g.x + 7;
|
|
469
|
-
int16_t cy = ry + rowH / 2;
|
|
470
|
-
ui_display_draw_line(cx, cy, cx + 3, cy + 3, rowFg);
|
|
471
|
-
ui_display_draw_line(cx + 3, cy + 3, cx + 8, cy - 4, rowFg);
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
__ui_select_menu_dirty = 0;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
static inline void ui_select_menu_tap(int16_t tx, int16_t ty) {
|
|
478
|
-
if (__ui_select_menu < 0) return;
|
|
479
|
-
uint16_t idx = static_cast<uint16_t>(__ui_select_menu);
|
|
480
|
-
UIRect g;
|
|
481
|
-
int16_t rowH;
|
|
482
|
-
ui_select_menu_geom(idx, &g, &rowH);
|
|
483
|
-
if (tx >= g.x && tx < g.x + g.w && ty >= g.y && ty < g.y + g.h) {
|
|
484
|
-
int16_t row = (ty - g.y - 4) / rowH;
|
|
485
|
-
if (row >= 0 && row < static_cast<int16_t>(__ui_nodes[idx].optionCount)) {
|
|
486
|
-
__ui_nodes[idx].value = row;
|
|
487
|
-
ui_mark_dirty(idx);
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
ui_select_menu_close(1);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
// ── <drawer> implementation (device twin of the preview drawer) ──────────
|
|
494
|
-
|
|
495
|
-
static int8_t __ui_drawer_slot_of(uint16_t nodeIdx) {
|
|
496
|
-
for (uint8_t s = 0; s < __ui_drawer_slots; s++) {
|
|
497
|
-
if (__ui_drawer_idx[s] == static_cast<int16_t>(nodeIdx)) return static_cast<int8_t>(s);
|
|
498
|
-
}
|
|
499
|
-
return -1;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
static void ui_drawer_discover() {
|
|
503
|
-
__ui_drawer_slots = 0;
|
|
504
|
-
for (uint16_t i = 0; i < __ui_node_count && __ui_drawer_slots < UI_DRAWER_MAX; i++) {
|
|
505
|
-
if (__ui_nodes[i].drawerSide < 0) continue;
|
|
506
|
-
uint8_t s = __ui_drawer_slots++;
|
|
507
|
-
__ui_drawer_idx[s] = static_cast<int16_t>(i);
|
|
508
|
-
__ui_drawer_open[s] = 0;
|
|
509
|
-
__ui_drawer_progress[s] = 0;
|
|
510
|
-
// Seed closed: subtree offsets at full travel (never paints at rest).
|
|
511
|
-
ui_drawer_apply(s, 0);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
static void ui_drawer_apply(uint8_t slot, uint8_t progress) {
|
|
516
|
-
if (slot >= __ui_drawer_slots || __ui_drawer_idx[slot] < 0) return;
|
|
517
|
-
uint16_t di = static_cast<uint16_t>(__ui_drawer_idx[slot]);
|
|
518
|
-
UINode* d = &__ui_nodes[di];
|
|
519
|
-
int16_t travel = (d->drawerSide == 2 || d->drawerSide == 3)
|
|
520
|
-
? static_cast<int16_t>(d->box.w) : static_cast<int16_t>(d->box.h);
|
|
521
|
-
if (d->drawerSide == 4) travel = 0; // <dialog>: centered, no slide
|
|
522
|
-
else {
|
|
523
|
-
// Edge panels must slide FULLY off the display, not just their own
|
|
524
|
-
// height: a bottom panel anchored at the viewport bottom (bottom:0) is
|
|
525
|
-
// already AT the edge, so travel == box.h parks its top row exactly at
|
|
526
|
-
// the fold — a sliver stayed visible after close. The seeded closed
|
|
527
|
-
// state never showed that sliver before the first open (the initial
|
|
528
|
-
// paint's scroll-clip dropped it), and a closed panel can't be tapped
|
|
529
|
-
// open, so the post-close "peek" read as a rendering artifact, not an
|
|
530
|
-
// affordance — it is gone for drawers and toasts alike. Rest position =
|
|
531
|
-
// box minus ancestor scroll (transformOffset still holds the previous
|
|
532
|
-
// apply's slide at this point, so it cannot be used here).
|
|
533
|
-
int16_t restX = static_cast<int16_t>(d->box.x);
|
|
534
|
-
int16_t restY = static_cast<int16_t>(d->box.y);
|
|
535
|
-
uint16_t pa = d->parent;
|
|
536
|
-
while (pa != UI_NO_PARENT && pa < __ui_node_count) {
|
|
537
|
-
if (__ui_nodes[pa].scrollable) restY = static_cast<int16_t>(restY - __ui_nodes[pa].scrollY);
|
|
538
|
-
pa = __ui_nodes[pa].parent;
|
|
539
|
-
}
|
|
540
|
-
int16_t dl = 0, dt = 0, dr = 0, db = 0;
|
|
541
|
-
ui_display_target_bounds(&dl, &dt, &dr, &db);
|
|
542
|
-
int16_t need = d->drawerSide == 0 ? static_cast<int16_t>(db - restY)
|
|
543
|
-
: d->drawerSide == 1 ? static_cast<int16_t>(restY + d->box.h - dt)
|
|
544
|
-
: d->drawerSide == 2 ? static_cast<int16_t>(restX + d->box.w - dl)
|
|
545
|
-
: static_cast<int16_t>(dr - restX);
|
|
546
|
-
if (need > travel) travel = need;
|
|
547
|
-
}
|
|
548
|
-
int16_t off = static_cast<int16_t>((static_cast<int32_t>(travel) * (255 - progress)) / 255);
|
|
549
|
-
int16_t dx = d->drawerSide == 2 ? -off : d->drawerSide == 3 ? off : 0;
|
|
550
|
-
int16_t dy = d->drawerSide == 1 ? -off : d->drawerSide == 0 ? off : 0;
|
|
551
|
-
d->transformOffsetX = dx;
|
|
552
|
-
d->transformOffsetY = dy;
|
|
553
|
-
for (uint16_t i = di + 1; i < d->subtreeEnd; i++) {
|
|
554
|
-
// Descendants don't inherit transform offsets: their own box coords are
|
|
555
|
-
// layout-local, so shift their draw origin via transform offsets too.
|
|
556
|
-
__ui_nodes[i].transformOffsetX += dx - (__ui_drawer_last_dx[slot]);
|
|
557
|
-
__ui_nodes[i].transformOffsetY += dy - (__ui_drawer_last_dy[slot]);
|
|
558
|
-
}
|
|
559
|
-
__ui_drawer_last_dx[slot] = dx;
|
|
560
|
-
__ui_drawer_last_dy[slot] = dy;
|
|
561
|
-
__ui_drawer_progress[slot] = progress;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
static void ui_drawer_open(uint16_t nodeIdx) {
|
|
565
|
-
int8_t s = __ui_drawer_slot_of(nodeIdx);
|
|
566
|
-
if (s < 0) return;
|
|
567
|
-
__ui_drawer_open[s] = 1;
|
|
568
|
-
__ui_toast_elapsed[s] = 0; // <toast>: restart the auto-close window
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
static void ui_drawer_close(uint16_t nodeIdx) {
|
|
572
|
-
int8_t s = __ui_drawer_slot_of(nodeIdx);
|
|
573
|
-
if (s < 0) return;
|
|
574
|
-
__ui_drawer_open[s] = 0;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
static void ui_drawer_close_all() {
|
|
578
|
-
for (uint8_t s = 0; s < __ui_drawer_slots; s++) __ui_drawer_open[s] = 0;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
static void ui_drawer_tick(uint32_t deltaMs) {
|
|
582
|
-
uint8_t step = static_cast<uint8_t>((deltaMs * 255UL) / 180UL);
|
|
583
|
-
if (step == 0) step = 1;
|
|
584
|
-
for (uint8_t s = 0; s < __ui_drawer_slots; s++) {
|
|
585
|
-
if (__ui_drawer_idx[s] < 0) continue;
|
|
586
|
-
uint16_t tnode = static_cast<uint16_t>(__ui_drawer_idx[s]);
|
|
587
|
-
// <toast duration>: once fully open, count up and auto-close. A reopen
|
|
588
|
-
// (ui_drawer_open) restarts the window; manual close just wins.
|
|
589
|
-
if (__ui_nodes[tnode].toastDuration > 0 && __ui_drawer_open[s] &&
|
|
590
|
-
__ui_drawer_progress[s] == 255) {
|
|
591
|
-
__ui_toast_elapsed[s] += deltaMs;
|
|
592
|
-
if (__ui_toast_elapsed[s] >= __ui_nodes[tnode].toastDuration) {
|
|
593
|
-
__ui_drawer_open[s] = 0;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
uint8_t target = __ui_drawer_open[s] ? 255 : 0;
|
|
597
|
-
uint8_t p = __ui_drawer_progress[s];
|
|
598
|
-
if (p == target) continue;
|
|
599
|
-
// Compose each slide frame as the UNION of the drawer's old and new
|
|
600
|
-
// SUBTREE paint rects in tear-free bands (ui_render_screen_bands). The
|
|
601
|
-
// previous contract — mark the whole active screen dirty per slide step —
|
|
602
|
-
// repainted the entire screen with direct per-node clears on no-fb
|
|
603
|
-
// targets: visible flashing, and only ~3 steps fit inside the 180ms
|
|
604
|
-
// slide. The banded union is a fraction of the SPI traffic and never
|
|
605
|
-
// exposes an intermediate state. Falls back to the whole-screen dirty
|
|
606
|
-
// repaint when the band canvas can't allocate or a retained framebuffer
|
|
607
|
-
// composes the frame in RAM (its dirty-union push is already tear-free).
|
|
608
|
-
//
|
|
609
|
-
// SUBTREE extent, not the root's paint rect: a <dialog> root whose
|
|
610
|
-
// children are absolutely positioned lays out at h=0 — the root-rect
|
|
611
|
-
// union degenerated to an empty band (a no-op "composed" success), so
|
|
612
|
-
// NOTHING repainted on open or close on hardware. The preview already
|
|
613
|
-
// used its currentSubtreePaintRect here. Side-4 dialogs also snap
|
|
614
|
-
// instead of animating: travel is 0, so intermediate progress values
|
|
615
|
-
// are 180ms of identical frames — the snap composes ONE band frame for
|
|
616
|
-
// the open state and ONE for the erase, so modal toggles are tear-free
|
|
617
|
-
// on no-fb SPI targets too (mark-all's per-node clears flashed the
|
|
618
|
-
// whole screen).
|
|
619
|
-
uint16_t di = static_cast<uint16_t>(__ui_drawer_idx[s]);
|
|
620
|
-
uint8_t next = target > p ? (p + step > target ? target : p + step)
|
|
621
|
-
: (p < step || p - step < target ? target : p - step);
|
|
622
|
-
if (__ui_nodes[di].drawerSide == 4) next = target;
|
|
623
|
-
UIRect oldRect;
|
|
624
|
-
uint8_t hasOld = ui_subtree_current_paint_rect(di, &oldRect);
|
|
625
|
-
ui_drawer_apply(s, next);
|
|
626
|
-
UIRect newRect;
|
|
627
|
-
uint8_t hasNew = ui_subtree_current_paint_rect(di, &newRect);
|
|
628
|
-
// A failed rect query leaves x/y UNWRITTEN — unioning it against the
|
|
629
|
-
// good side mixed stack garbage into the band region (huge regions clipped
|
|
630
|
-
// to full-screen, or empty ones that "composed" nothing). When one side
|
|
631
|
-
// has no on-screen extent (a dialog snapping open from fully-closed, or
|
|
632
|
-
// erasing to fully-closed), the union is just the side that exists.
|
|
633
|
-
if (!hasOld && !hasNew) continue;
|
|
634
|
-
if (!hasOld) oldRect = newRect;
|
|
635
|
-
if (!hasNew) newRect = oldRect;
|
|
636
|
-
uint8_t composed = 0;
|
|
637
|
-
if (!ui_get_framebuffer()) {
|
|
638
|
-
int16_t ux0 = oldRect.x < newRect.x ? oldRect.x : newRect.x;
|
|
639
|
-
int16_t uy0 = oldRect.y < newRect.y ? oldRect.y : newRect.y;
|
|
640
|
-
int16_t ux1 = oldRect.x + oldRect.w > newRect.x + newRect.w ? static_cast<int16_t>(oldRect.x + oldRect.w) : static_cast<int16_t>(newRect.x + newRect.w);
|
|
641
|
-
int16_t uy1 = oldRect.y + oldRect.h > newRect.y + newRect.h ? static_cast<int16_t>(oldRect.y + oldRect.h) : static_cast<int16_t>(newRect.y + newRect.h);
|
|
642
|
-
composed = ui_render_screen_bands(ux0, uy0, static_cast<int16_t>(ux1 - ux0), static_cast<int16_t>(uy1 - uy0));
|
|
643
|
-
if (composed) {
|
|
644
|
-
// The drawer crossed a scroll viewport that caches its pixels; the
|
|
645
|
-
// canvas holds pre-slide content where the drawer now sits. Invalidate
|
|
646
|
-
// it so the next scroll recomposites instead of shifting stale pixels.
|
|
647
|
-
ui_invalidate_scroll_canvas_for_node(di);
|
|
648
|
-
// The composed bands are the AUTHORITATIVE frame for this region —
|
|
649
|
-
// every node fully inside it is already painted in correct stacking
|
|
650
|
-
// order. Clear those dirty flags or the dirty pass repaints them
|
|
651
|
-
// through the per-node ladder: a viewport-sized overlay (the dialog
|
|
652
|
-
// scrim) is too big for the repair canvas and falls to the DIRECT
|
|
653
|
-
// ladder — one flat full-screen fill that erased the just-composed
|
|
654
|
-
// frame, then the card/title/buttons re-popped band by band. That
|
|
655
|
-
// erase + re-layer sequence was the modal-toggle flash on hardware.
|
|
656
|
-
// Bindings run after the drawer tick, so any dirty flag they raise
|
|
657
|
-
// this frame still repaints normally.
|
|
658
|
-
for (uint16_t q = 0; q < __ui_node_count; q++) {
|
|
659
|
-
if (__ui_nodes[q].screenId != __ui_active_screen) continue;
|
|
660
|
-
if (!__ui_nodes[q].dirty) continue;
|
|
661
|
-
UIRect pr;
|
|
662
|
-
ui_node_current_paint_rect(q, &pr);
|
|
663
|
-
if (pr.w <= 0 || pr.h <= 0) continue;
|
|
664
|
-
if (pr.x >= ux0 && pr.y >= uy0 &&
|
|
665
|
-
static_cast<int16_t>(pr.x + pr.w) <= static_cast<int16_t>(ux1) &&
|
|
666
|
-
static_cast<int16_t>(pr.y + pr.h) <= static_cast<int16_t>(uy1)) {
|
|
667
|
-
__ui_nodes[q].dirty = 0;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
if (!composed) {
|
|
673
|
-
for (uint16_t i = 0; i < __ui_node_count; i++) {
|
|
674
|
-
if (__ui_nodes[i].screenId != __ui_active_screen) continue;
|
|
675
|
-
__ui_nodes[i].dirty = 1;
|
|
676
|
-
__ui_nodes[i].lastTextHeight = 0;
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
}
|
|
681
|
-
`;
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// ── Modal <select> option list ────────────────────────────────────────────
|
|
685
|
-
// Device twin of the preview's select modal: tapping a select opens a
|
|
686
|
-
// centered list of its options (capped to ~60% of the panel height); tapping
|
|
687
|
-
// a row sets the value and closes; tapping outside dismisses. The overlay is
|
|
688
|
-
// stamped after the dirty pass every frame while open; closing marks the
|
|
689
|
-
// whole tree dirty for the erase repaint.
|
|
1
|
+
// Slice of the C++ runtime header (original source lines 5434-5769).
|
|
2
|
+
// On-screen keyboard: add_key/insert/delete/compute_box/open/close/rect/touch/tick/tap/draw.
|
|
3
|
+
// See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
|
|
4
|
+
export function emitKeyboard(): string {
|
|
5
|
+
return `
|
|
6
|
+
// ── On-screen keyboard subsystem ───────────────────────────────────────────
|
|
7
|
+
// (UIKey struct, UI_KB_* defines, __ui_kb_box, __ui_kb_visible, __ui_kb_bs_held,
|
|
8
|
+
// __ui_last_touch_x/y are declared earlier near the touch state machine so
|
|
9
|
+
// the touch functions can reference them.)
|
|
10
|
+
|
|
11
|
+
// Populated by the per-keyboard loader function (emitted by the lowering).
|
|
12
|
+
// (__ui_kb_keys is forward-declared earlier near the touch state machine.)
|
|
13
|
+
static uint8_t __ui_kb_keyCount;
|
|
14
|
+
static uint8_t __ui_kb_rows;
|
|
15
|
+
static uint8_t __ui_kb_cols;
|
|
16
|
+
static char __ui_kb_buffer[UI_TEXT_BUF + 1];
|
|
17
|
+
static uint8_t __ui_kb_len;
|
|
18
|
+
static uint8_t __ui_kb_maxlen;
|
|
19
|
+
static uint8_t __ui_kb_shift;
|
|
20
|
+
// __ui_kb_visible, __ui_kb_bs_held, __ui_last_touch_x/y are forward-declared
|
|
21
|
+
// earlier (near the touch state machine) because ui_touch_up references them.
|
|
22
|
+
// __ui_kb_target is also forward-declared there (the UI_HIDE_OSK caret/blink
|
|
23
|
+
// paths in ui_tick and the input draw reference it before this point).
|
|
24
|
+
static uint32_t __ui_kb_bs_repeat; // last auto-repeat deletion time
|
|
25
|
+
// __ui_kb_dirty is forward-declared earlier (near the touch state machine).
|
|
26
|
+
static void (*__ui_kb_onchange)();
|
|
27
|
+
// Dispatch table: one loader per input node. Indexed by input position.
|
|
28
|
+
extern void (*__ui_kb_loaders[])();
|
|
29
|
+
extern const uint16_t __ui_kb_loader_count;
|
|
30
|
+
|
|
31
|
+
static inline void ui_kb_add_key(char ch, uint8_t special, UI_COLOR_T bg, UI_COLOR_T fg, UI_COLOR_T borderColor) {
|
|
32
|
+
if (__ui_kb_keyCount >= UI_KB_MAX) return;
|
|
33
|
+
__ui_kb_keys[__ui_kb_keyCount] = { ch, special };
|
|
34
|
+
__ui_kb_styles[__ui_kb_keyCount] = { bg, fg, borderColor };
|
|
35
|
+
__ui_kb_keyCount++;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Insert a character into the buffer (if space permits).
|
|
39
|
+
static inline void ui_kb_insert(char c) {
|
|
40
|
+
if (__ui_kb_maxlen > 0 && __ui_kb_len >= __ui_kb_maxlen) return;
|
|
41
|
+
if (__ui_kb_len >= UI_TEXT_BUF) return;
|
|
42
|
+
__ui_kb_buffer[__ui_kb_len++] = c;
|
|
43
|
+
__ui_kb_buffer[__ui_kb_len] = 0;
|
|
44
|
+
#if defined(UI_HIDE_OSK)
|
|
45
|
+
// Desktop target: the OSK grid isn't drawn, so the input field itself is the
|
|
46
|
+
// only place the in-progress text appears. Sync the buffer into the target
|
|
47
|
+
// node's textBuffer and mark it dirty so the field repaints on the next tick
|
|
48
|
+
// — without this, typing appears to do nothing until Enter commits at close.
|
|
49
|
+
if (__ui_kb_target >= 0) {
|
|
50
|
+
strncpy(__ui_nodes[__ui_kb_target].textBuffer, __ui_kb_buffer, UI_TEXT_BUF);
|
|
51
|
+
__ui_nodes[__ui_kb_target].textBuffer[UI_TEXT_BUF] = 0;
|
|
52
|
+
ui_mark_dirty(static_cast<uint16_t>(__ui_kb_target));
|
|
53
|
+
}
|
|
54
|
+
#endif
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Delete one character from the buffer.
|
|
58
|
+
static inline void ui_kb_delete() {
|
|
59
|
+
if (__ui_kb_len == 0) return;
|
|
60
|
+
__ui_kb_buffer[--__ui_kb_len] = 0;
|
|
61
|
+
#if defined(UI_HIDE_OSK)
|
|
62
|
+
if (__ui_kb_target >= 0) {
|
|
63
|
+
strncpy(__ui_nodes[__ui_kb_target].textBuffer, __ui_kb_buffer, UI_TEXT_BUF);
|
|
64
|
+
__ui_nodes[__ui_kb_target].textBuffer[UI_TEXT_BUF] = 0;
|
|
65
|
+
ui_mark_dirty(static_cast<uint16_t>(__ui_kb_target));
|
|
66
|
+
}
|
|
67
|
+
#endif
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Compute the keyboard box on open from display dimensions + grid shape.
|
|
71
|
+
// Alpha (wide grid) docks to the bottom 75%; number (narrow grid) centers at 60%.
|
|
72
|
+
// Uses the display profile dimensions if available, else 320×240.
|
|
73
|
+
#ifndef __ui_display_w
|
|
74
|
+
#define __ui_display_w 320
|
|
75
|
+
#endif
|
|
76
|
+
#ifndef __ui_display_h
|
|
77
|
+
#define __ui_display_h 240
|
|
78
|
+
#endif
|
|
79
|
+
static inline void ui_kb_compute_box() {
|
|
80
|
+
uint8_t isNumber = (__ui_kb_cols <= 4);
|
|
81
|
+
uint16_t h = isNumber ? (__ui_display_h * 60 / 100) : (__ui_display_h * 75 / 100);
|
|
82
|
+
__ui_kb_box.w = isNumber ? (__ui_display_w * 50 / 100) : __ui_display_w;
|
|
83
|
+
__ui_kb_box.h = h;
|
|
84
|
+
__ui_kb_box.x = isNumber ? (__ui_display_w - __ui_kb_box.w) / 2 : 0;
|
|
85
|
+
__ui_kb_box.y = __ui_display_h - h;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Open the keyboard for an input node.
|
|
89
|
+
static inline void ui_kb_open(uint16_t nodeIdx, uint8_t inputPosition) {
|
|
90
|
+
__ui_kb_target = nodeIdx;
|
|
91
|
+
strncpy(__ui_kb_buffer, __ui_nodes[nodeIdx].textBuffer, UI_TEXT_BUF);
|
|
92
|
+
__ui_kb_buffer[UI_TEXT_BUF] = 0;
|
|
93
|
+
__ui_kb_len = strlen(__ui_kb_buffer);
|
|
94
|
+
uint16_t ml = __ui_nodes[nodeIdx].maxlen;
|
|
95
|
+
__ui_kb_maxlen = (ml > 0 && ml <= UI_TEXT_BUF) ? static_cast<uint8_t>(ml) : UI_TEXT_BUF;
|
|
96
|
+
__ui_kb_shift = 0;
|
|
97
|
+
__ui_kb_bs_held = 0;
|
|
98
|
+
// Load the key set via the dispatch table.
|
|
99
|
+
if (inputPosition < __ui_kb_loader_count) __ui_kb_loaders[inputPosition]();
|
|
100
|
+
__ui_kb_set_onchange();
|
|
101
|
+
ui_kb_compute_box();
|
|
102
|
+
__ui_kb_visible = 1;
|
|
103
|
+
__ui_kb_dirty = 1; // redraw on the first visible frame
|
|
104
|
+
// Do NOT pre-mark the tree dirty here. While the keyboard is visible the
|
|
105
|
+
// draw pass is skipped (its opaque background covers app nodes), so any
|
|
106
|
+
// dirty flags set now are never consumed/cleared — they survive until close,
|
|
107
|
+
// and the first post-close frame then repaints every dirty node = full-screen
|
|
108
|
+
// flash on SPI TFTs. The close path scopes the repaint to nodes whose paint
|
|
109
|
+
// rect intersects __ui_kb_box, which is the only region that needs restoring.
|
|
110
|
+
#if defined(UI_HIDE_OSK)
|
|
111
|
+
// Desktop target: the OSK isn't drawn, so the draw pass runs normally and the
|
|
112
|
+
// input field must repaint on focus to show the caret BEFORE the first
|
|
113
|
+
// keystroke. (The full-screen-flash concern above doesn't apply — there's no
|
|
114
|
+
// opaque overlay being skipped.) Seed the blink phase so the caret is ON for
|
|
115
|
+
// the first ~530ms (bit 0x20 set → visible), so focus feels immediate.
|
|
116
|
+
if (__ui_kb_target >= 0) ui_mark_dirty(static_cast<uint16_t>(__ui_kb_target));
|
|
117
|
+
__ui_kb_blink = 0x20;
|
|
118
|
+
#endif
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Close the keyboard: commit buffer back to the input node.
|
|
122
|
+
static inline void ui_kb_close() {
|
|
123
|
+
if (__ui_kb_target >= 0) {
|
|
124
|
+
strncpy(__ui_nodes[__ui_kb_target].textBuffer, __ui_kb_buffer, UI_TEXT_BUF);
|
|
125
|
+
__ui_nodes[__ui_kb_target].textBuffer[UI_TEXT_BUF] = 0;
|
|
126
|
+
if (__ui_kb_onchange) __ui_kb_onchange();
|
|
127
|
+
}
|
|
128
|
+
// Capture the keyboard box before clearing visibility — it identifies the
|
|
129
|
+
// screen region the opaque overlay covered and that now needs restoring.
|
|
130
|
+
UIRect kbBox = __ui_kb_box;
|
|
131
|
+
__ui_kb_visible = 0;
|
|
132
|
+
__ui_kb_target = -1;
|
|
133
|
+
__ui_kb_bs_held = 0;
|
|
134
|
+
// The <screen> root's paint rect spans the whole display, so it always
|
|
135
|
+
// intersects kbBox. Routing it through ui_mark_dirty would cascade via
|
|
136
|
+
// ui_mark_overlapping_higher_layers_dirty into marking every node on the
|
|
137
|
+
// active screen dirty (its rect overlaps everything), which is the
|
|
138
|
+
// full-screen flash this function exists to avoid. Repaint just the
|
|
139
|
+
// keyboard-box slice of its background directly instead.
|
|
140
|
+
int16_t screenRoot = -1;
|
|
141
|
+
for (uint16_t i = 0; i < __ui_node_count; i++) {
|
|
142
|
+
if (__ui_nodes[i].parent == UI_NO_PARENT && __ui_nodes[i].screenId == __ui_active_screen) {
|
|
143
|
+
screenRoot = static_cast<int16_t>(i);
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (screenRoot >= 0) {
|
|
148
|
+
UI_COLOR_T rootBg = (UI_COLOR_T)(__ui_nodes[screenRoot].hasBg
|
|
149
|
+
? __ui_nodes[screenRoot].bg
|
|
150
|
+
: __ui_nodes[screenRoot].clearColor);
|
|
151
|
+
ui_display_fill_rect(kbBox.x, kbBox.y, kbBox.w, kbBox.h, rootBg);
|
|
152
|
+
}
|
|
153
|
+
// Repaint everything else the keyboard overlay actually overwrote: nodes
|
|
154
|
+
// whose paint rect intersects the keyboard box, plus the edited input
|
|
155
|
+
// itself (its text just changed). ui_mark_dirty handles overlap repair +
|
|
156
|
+
// scroll clipping — safe here since these nodes are bounded in size, unlike
|
|
157
|
+
// the screen root.
|
|
158
|
+
for (uint16_t i = 0; i < __ui_node_count; i++) {
|
|
159
|
+
if (static_cast<int16_t>(i) == screenRoot) continue;
|
|
160
|
+
UIRect r;
|
|
161
|
+
ui_node_current_paint_rect(i, &r);
|
|
162
|
+
if (r.w > 0 && r.h > 0 &&
|
|
163
|
+
ui_rects_intersect(r.x, r.y, r.w, r.h, kbBox.x, kbBox.y, kbBox.w, kbBox.h)) {
|
|
164
|
+
ui_mark_dirty(i);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Compute a key's rect from its index, given the grid + box.
|
|
170
|
+
// The top UI_KB_TEXT_H pixels of the box are reserved for the preview text row;
|
|
171
|
+
// keys fill the area below it.
|
|
172
|
+
static inline void ui_kb_key_rect(uint8_t idx, UIRect* out) {
|
|
173
|
+
uint8_t col = idx % __ui_kb_cols;
|
|
174
|
+
uint8_t row = idx / __ui_kb_cols;
|
|
175
|
+
int16_t keysH = __ui_kb_box.h - UI_KB_TEXT_H; // key area height (below text row)
|
|
176
|
+
out->x = __ui_kb_box.x + static_cast<int16_t>(col) * __ui_kb_box.w / __ui_kb_cols;
|
|
177
|
+
out->y = __ui_kb_box.y + UI_KB_TEXT_H + static_cast<int16_t>(row) * keysH / __ui_kb_rows;
|
|
178
|
+
out->w = __ui_kb_box.w / __ui_kb_cols;
|
|
179
|
+
out->h = keysH / __ui_kb_rows;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Handle a touch-down inside the keyboard box. tx,ty are display coords.
|
|
183
|
+
// Handle a touch-down inside the keyboard box. Only fires on the initial
|
|
184
|
+
// down edge (tracked by __ui_touch_state in the modal path), NOT every poll.
|
|
185
|
+
// Records WHICH key is under the finger — the same key is activated on release
|
|
186
|
+
// (ui_kb_handle_tap), avoiding mis-targeting from coordinate drift on release.
|
|
187
|
+
static inline void ui_kb_handle_touch(int16_t tx, int16_t ty) {
|
|
188
|
+
__ui_kb_pressed_key = -1;
|
|
189
|
+
for (uint8_t i = 0; i < __ui_kb_keyCount; i++) {
|
|
190
|
+
UIKey k = __ui_kb_keys[i];
|
|
191
|
+
if (k.special == 255) continue; // padding cell, skip
|
|
192
|
+
UIRect r;
|
|
193
|
+
ui_kb_key_rect(i, &r);
|
|
194
|
+
if (tx >= r.x && tx < r.x + r.w && ty >= r.y && ty < r.y + r.h) {
|
|
195
|
+
__ui_kb_pressed_key = static_cast<int8_t>(i); // remember for release
|
|
196
|
+
__ui_kb_repaint_key = static_cast<int8_t>(i);
|
|
197
|
+
__ui_kb_dirty = 2; // targeted: redraw just this key's highlight
|
|
198
|
+
// Backspace starts deleting immediately + arms auto-repeat.
|
|
199
|
+
if (k.special == 2) {
|
|
200
|
+
__ui_kb_bs_held = 1;
|
|
201
|
+
__ui_kb_bs_repeat = __tc_now_ms();
|
|
202
|
+
ui_kb_delete();
|
|
203
|
+
}
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Called each frame while the keyboard is visible + a touch is held.
|
|
210
|
+
// Handles ⌫ auto-repeat.
|
|
211
|
+
static inline void ui_kb_tick(uint32_t now) {
|
|
212
|
+
if (!__ui_kb_bs_held) return;
|
|
213
|
+
if (now - __ui_kb_bs_repeat >= UI_KB_REPEAT_MS) {
|
|
214
|
+
ui_kb_delete();
|
|
215
|
+
__ui_kb_bs_repeat = now;
|
|
216
|
+
__ui_kb_repaint_key = __ui_kb_pressed_key; // ⌫ key stays highlighted
|
|
217
|
+
__ui_kb_dirty = 2; // targeted: text row + ⌫ key
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Handle a tap release. Activates the key that was under the finger on
|
|
222
|
+
// touch-down (__ui_kb_pressed_key) — NOT a fresh hit-test, which would
|
|
223
|
+
// mis-target due to coordinate drift on a resistive panel at release.
|
|
224
|
+
static inline void ui_kb_handle_tap(int16_t tx, int16_t ty) {
|
|
225
|
+
(void)tx; (void)ty; // key was recorded on touch-down; no re-hit-test
|
|
226
|
+
if (__ui_kb_pressed_key < 0) return;
|
|
227
|
+
UIKey k = __ui_kb_keys[__ui_kb_pressed_key];
|
|
228
|
+
int8_t releasedKey = __ui_kb_pressed_key;
|
|
229
|
+
__ui_kb_pressed_key = -1; // clear pressed state → highlight reverts
|
|
230
|
+
switch (k.special) {
|
|
231
|
+
case 0: { // char
|
|
232
|
+
char c = k.ch;
|
|
233
|
+
uint8_t wasShift = __ui_kb_shift;
|
|
234
|
+
if (wasShift && c >= 'a' && c <= 'z') c -= 32;
|
|
235
|
+
ui_kb_insert(c);
|
|
236
|
+
__ui_kb_shift = 0; // shift resets after one char
|
|
237
|
+
// Text row changes; if shift was active, repaint shift key too.
|
|
238
|
+
__ui_kb_repaint_key = wasShift ? -1 : releasedKey;
|
|
239
|
+
__ui_kb_dirty = 2;
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
case 1: // shift toggle — all letter keys change case, full redraw
|
|
243
|
+
__ui_kb_shift = !__ui_kb_shift;
|
|
244
|
+
__ui_kb_dirty = 1;
|
|
245
|
+
break;
|
|
246
|
+
case 2: // backspace: handled on down + repeat; nothing on tap-up
|
|
247
|
+
__ui_kb_repaint_key = releasedKey; // revert highlight
|
|
248
|
+
__ui_kb_dirty = 2;
|
|
249
|
+
break;
|
|
250
|
+
case 3: // OK
|
|
251
|
+
ui_kb_close();
|
|
252
|
+
break;
|
|
253
|
+
case 4: { // page-swap — new key layout, full redraw
|
|
254
|
+
extern void __ui_kb_load_default_alpha();
|
|
255
|
+
extern void __ui_kb_load_default_number();
|
|
256
|
+
if (__ui_kb_cols <= 4) __ui_kb_load_default_alpha();
|
|
257
|
+
else __ui_kb_load_default_number();
|
|
258
|
+
ui_kb_compute_box();
|
|
259
|
+
__ui_kb_dirty = 1;
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Draw a single key by index. Shared by the full draw + targeted redraw.
|
|
266
|
+
static inline void ui_kb_draw_key(uint8_t i) {
|
|
267
|
+
UIKey k = __ui_kb_keys[i];
|
|
268
|
+
UIKeyStyle ks = __ui_kb_styles[i];
|
|
269
|
+
UIRect r;
|
|
270
|
+
ui_kb_key_rect(i, &r);
|
|
271
|
+
UI_COLOR_T bg = ks.bg;
|
|
272
|
+
UI_COLOR_T fg = ks.fg;
|
|
273
|
+
UI_COLOR_T border = ks.borderColor;
|
|
274
|
+
// Shift-active highlight: brighten the shift key's background — but only
|
|
275
|
+
// when not pressed, so the press inversion stays high-contrast.
|
|
276
|
+
if (k.special == 1 && __ui_kb_shift && static_cast<int8_t>(i) != __ui_kb_pressed_key) {
|
|
277
|
+
#if UI_COLOR_DEPTH == 888
|
|
278
|
+
bg = 0xBDEFFF;
|
|
279
|
+
#else
|
|
280
|
+
bg = 0xBDF7;
|
|
281
|
+
#endif
|
|
282
|
+
}
|
|
283
|
+
// Pressed key: invert colors for clear tap feedback.
|
|
284
|
+
if (static_cast<int8_t>(i) == __ui_kb_pressed_key) { UI_COLOR_T t = bg; bg = fg; fg = t; }
|
|
285
|
+
ui_display_fill_rect(r.x + 1, r.y + 1, r.w - 2, r.h - 2, bg);
|
|
286
|
+
ui_display_draw_rect(r.x + 1, r.y + 1, r.w - 2, r.h - 2, border);
|
|
287
|
+
ui_display_set_text_color(fg, bg);
|
|
288
|
+
ui_display_set_text_size(1);
|
|
289
|
+
// Derive the label string + its length for centering.
|
|
290
|
+
const char* labelStr;
|
|
291
|
+
char single[2];
|
|
292
|
+
switch (k.special) {
|
|
293
|
+
case 1: labelStr = "Aa"; break;
|
|
294
|
+
case 2: labelStr = "DEL"; break;
|
|
295
|
+
case 3: labelStr = "OK"; break;
|
|
296
|
+
case 4: labelStr = __ui_kb_cols <= 4 ? "ABC" : "123"; break;
|
|
297
|
+
default:
|
|
298
|
+
single[0] = (__ui_kb_shift && k.ch >= 'a' && k.ch <= 'z') ? static_cast<char>(k.ch - 32) : k.ch;
|
|
299
|
+
single[1] = 0;
|
|
300
|
+
labelStr = single;
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
// Center: textW = len * 6px, textH = 8px. Position inside the key rect.
|
|
304
|
+
uint8_t len = strlen(labelStr);
|
|
305
|
+
int16_t textW = static_cast<int16_t>(len) * 6;
|
|
306
|
+
int16_t textH = 8;
|
|
307
|
+
int16_t cx = r.x + (r.w - textW) / 2;
|
|
308
|
+
int16_t cy = r.y + (r.h - textH) / 2;
|
|
309
|
+
if (cx < r.x + 1) cx = r.x + 1; // clamp if label wider than key
|
|
310
|
+
ui_display_set_cursor(cx, cy);
|
|
311
|
+
ui_display_print(labelStr);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Redraw only the text display row (top of keyboard box). Used when a char is
|
|
315
|
+
// inserted/deleted without changing key highlights.
|
|
316
|
+
static inline void ui_kb_draw_text_row() {
|
|
317
|
+
// Clear the text row area (top UI_KB_TEXT_H px of the keyboard box).
|
|
318
|
+
ui_display_fill_rect(__ui_kb_box.x, __ui_kb_box.y, __ui_kb_box.w, UI_KB_TEXT_H, __ui_kb_bg);
|
|
319
|
+
ui_display_set_cursor(__ui_kb_box.x + 4, __ui_kb_box.y + 4);
|
|
320
|
+
#if UI_COLOR_DEPTH == 888
|
|
321
|
+
ui_display_set_text_color(0xFFFFFF, 0x000000);
|
|
322
|
+
#else
|
|
323
|
+
ui_display_set_text_color(0xFFFF, 0x0000);
|
|
324
|
+
#endif
|
|
325
|
+
ui_display_set_text_size(2);
|
|
326
|
+
ui_display_print(__ui_kb_buffer);
|
|
327
|
+
ui_display_print("_"); // cursor
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Draw the full keyboard overlay (background + text row + all keys).
|
|
331
|
+
// Renders into a dedicated offscreen canvas first, then pushes in a single
|
|
332
|
+
// SPI transaction — avoids the per-key/per-rect display writes that show up
|
|
333
|
+
// as visible row-by-row painting on SPI TFTs.
|
|
334
|
+
// (__ui_kb_canvas is forward-declared in the touch-keyboard-fwd slice.)
|
|
335
|
+
static inline void ui_kb_draw() {
|
|
336
|
+
int16_t kw = __ui_kb_box.w;
|
|
337
|
+
int16_t kh = __ui_kb_box.h;
|
|
338
|
+
if (kw <= 0 || kh <= 0) return;
|
|
339
|
+
// (Re)allocate the keyboard canvas to exact size (see repair-canvas comment
|
|
340
|
+
// — stride mismatch corrupts the push when reusing a differently-sized canvas).
|
|
341
|
+
// ui_create_canvas_best prefers PSRAM when available (a full-width keyboard
|
|
342
|
+
// canvas can be ~30KB, too big for no-PSRAM internal SRAM) and falls back to
|
|
343
|
+
// SRAM — same policy as the list/scroll viewport canvases.
|
|
344
|
+
if (!__ui_kb_canvas || !display_canvasBuffer(__ui_kb_canvas) ||
|
|
345
|
+
display_canvasWidth(__ui_kb_canvas) != kw ||
|
|
346
|
+
display_canvasHeight(__ui_kb_canvas) != kh) {
|
|
347
|
+
display_deleteCanvas(__ui_kb_canvas);
|
|
348
|
+
__ui_kb_canvas = ui_create_canvas_best(kw, kh);
|
|
349
|
+
}
|
|
350
|
+
if (!__ui_kb_canvas || !display_canvasBuffer(__ui_kb_canvas)) {
|
|
351
|
+
// Canvas alloc failed — fall back to direct draw (slow but correct).
|
|
352
|
+
ui_display_fill_rect(__ui_kb_box.x, __ui_kb_box.y, kw, kh, __ui_kb_bg);
|
|
353
|
+
ui_kb_draw_text_row();
|
|
354
|
+
for (uint8_t i = 0; i < __ui_kb_keyCount; i++) {
|
|
355
|
+
if (__ui_kb_keys[i].special == 255) continue;
|
|
356
|
+
ui_kb_draw_key(i);
|
|
357
|
+
}
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
// Redirect drawing into the canvas. All ui_display_* calls below go to RAM.
|
|
361
|
+
CuttlefishDisplayTarget* __kb_prev_target = ui_display_get_target();
|
|
362
|
+
ui_display_set_target(__ui_kb_canvas);
|
|
363
|
+
// Opaque background over the keyboard box (canvas-local 0,0).
|
|
364
|
+
ui_display_fill_rect(0, 0, kw, kh, __ui_kb_bg);
|
|
365
|
+
// Text row + keys: temporarily shift box origin to canvas-local so the
|
|
366
|
+
// existing draw_text_row / draw_key functions compute coords at (0,0).
|
|
367
|
+
int16_t saveBoxX = __ui_kb_box.x;
|
|
368
|
+
int16_t saveBoxY = __ui_kb_box.y;
|
|
369
|
+
__ui_kb_box.x = 0;
|
|
370
|
+
__ui_kb_box.y = 0;
|
|
371
|
+
ui_kb_draw_text_row();
|
|
372
|
+
for (uint8_t i = 0; i < __ui_kb_keyCount; i++) {
|
|
373
|
+
if (__ui_kb_keys[i].special == 255) continue;
|
|
374
|
+
ui_kb_draw_key(i);
|
|
375
|
+
}
|
|
376
|
+
__ui_kb_box.x = saveBoxX;
|
|
377
|
+
__ui_kb_box.y = saveBoxY;
|
|
378
|
+
// Restore display target and push the canvas in one transaction.
|
|
379
|
+
ui_display_set_target(__kb_prev_target);
|
|
380
|
+
display_startWrite();
|
|
381
|
+
display_setAddrWindow(saveBoxX, saveBoxY, kw, kh);
|
|
382
|
+
display_writePixels(display_canvasBuffer(__ui_kb_canvas), static_cast<uint32_t>(kw) * kh);
|
|
383
|
+
display_endWrite();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
static inline void ui_select_menu_geom(uint16_t nodeIdx, UIRect* out, int16_t* rowH) {
|
|
387
|
+
const UINode* n = &__ui_nodes[nodeIdx];
|
|
388
|
+
int16_t rows = static_cast<int16_t>(n->optionCount);
|
|
389
|
+
if (rows < 1) rows = 1;
|
|
390
|
+
*rowH = 22;
|
|
391
|
+
// Anchor to the select itself: same left edge and width, so the modal
|
|
392
|
+
// reads as the control's own dropdown (never overhangs toward a scrollbar).
|
|
393
|
+
// Clamp to the screen for selects that extend past it.
|
|
394
|
+
int16_t x = n->box.x;
|
|
395
|
+
if (x < 0) x = 0;
|
|
396
|
+
int16_t w = n->box.w;
|
|
397
|
+
if (w < 120) w = 120;
|
|
398
|
+
if (x + w > static_cast<int16_t>(__ui_display_w)) w = static_cast<int16_t>(__ui_display_w) - x;
|
|
399
|
+
int16_t h = rows * (*rowH) + 8;
|
|
400
|
+
int16_t maxH = static_cast<int16_t>(__ui_display_h) * 3 / 5;
|
|
401
|
+
if (h > maxH) h = maxH;
|
|
402
|
+
out->x = x;
|
|
403
|
+
out->y = static_cast<int16_t>((static_cast<int16_t>(__ui_display_h) - h) / 2);
|
|
404
|
+
out->w = w;
|
|
405
|
+
out->h = h;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
static inline void ui_select_menu_open(uint16_t nodeIdx) {
|
|
409
|
+
if (__ui_nodes[nodeIdx].kind != NODE_SELECT) return;
|
|
410
|
+
if (__ui_nodes[nodeIdx].optionCount < 1) return;
|
|
411
|
+
__ui_select_menu = static_cast<int16_t>(nodeIdx);
|
|
412
|
+
__ui_select_menu_dirty = 1;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
static inline void ui_select_menu_close(uint8_t repaint) {
|
|
416
|
+
if (__ui_select_menu < 0) return;
|
|
417
|
+
__ui_select_menu = -1;
|
|
418
|
+
__ui_select_menu_dirty = 0;
|
|
419
|
+
if (repaint) {
|
|
420
|
+
// Erase repaint: the whole active screen redraws beneath the vanished
|
|
421
|
+
// overlay (same contract as navigate's mark-all-dirty).
|
|
422
|
+
for (uint16_t i = 0; i < __ui_node_count; i++) {
|
|
423
|
+
__ui_nodes[i].dirty = 1;
|
|
424
|
+
__ui_nodes[i].lastTextHeight = 0;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
static inline void ui_select_menu_draw() {
|
|
430
|
+
if (__ui_select_menu < 0) return;
|
|
431
|
+
uint16_t idx = static_cast<uint16_t>(__ui_select_menu);
|
|
432
|
+
const UINode* n = &__ui_nodes[idx];
|
|
433
|
+
UIRect g;
|
|
434
|
+
int16_t rowH;
|
|
435
|
+
ui_select_menu_geom(idx, &g, &rowH);
|
|
436
|
+
// Themed panel: the select's own radius/border/bg — the modal reads as
|
|
437
|
+
// the control's popover, matching the surrounding UI (device parity with
|
|
438
|
+
// the preview's drawSelectMenu theming).
|
|
439
|
+
int16_t radius = n->borderRadius > 0 ? n->borderRadius : 6;
|
|
440
|
+
if (radius > g.w / 2) radius = g.w / 2;
|
|
441
|
+
if (radius > g.h / 2) radius = g.h / 2;
|
|
442
|
+
UI_COLOR_T panel = n->hasBg ? n->bg : n->clearColor;
|
|
443
|
+
UI_COLOR_T borderCol = (n->borderStyle != 0 && n->borderColor != 0) ? n->borderColor : n->fg;
|
|
444
|
+
ui_display_fill_round_rect(g.x, g.y, g.w, g.h, radius, panel);
|
|
445
|
+
ui_display_draw_round_rect(g.x, g.y, g.w, g.h, radius, borderCol);
|
|
446
|
+
char buf[UI_TEXT_BUF + 1];
|
|
447
|
+
int16_t rowInset = radius / 2; if (rowInset < 2) rowInset = 2;
|
|
448
|
+
int16_t rowRadius = radius; if (rowRadius > 6) rowRadius = 6;
|
|
449
|
+
for (uint8_t r = 0; r < n->optionCount; r++) {
|
|
450
|
+
int16_t ry = g.y + 4 + static_cast<int16_t>(r) * rowH;
|
|
451
|
+
uint8_t current = (r == static_cast<uint8_t>(n->value));
|
|
452
|
+
// The selected row carries the :checked pair (the kit wires it to
|
|
453
|
+
// --accent/--accent-foreground, shadcn's SelectItem selected state).
|
|
454
|
+
UI_COLOR_T rowBg = n->hasCheckedBg ? static_cast<UI_COLOR_T>(n->checkedBg) : n->fg;
|
|
455
|
+
UI_COLOR_T rowFg = n->hasCheckedFg ? static_cast<UI_COLOR_T>(n->checkedFg) : panel;
|
|
456
|
+
if (current) {
|
|
457
|
+
ui_display_fill_round_rect(g.x + rowInset, ry, g.w - 2 * rowInset, rowH, rowRadius, rowBg);
|
|
458
|
+
}
|
|
459
|
+
buf[0] = 0;
|
|
460
|
+
if (n->optionTextFn) n->optionTextFn(r, buf, UI_TEXT_BUF + 1);
|
|
461
|
+
ui_draw_wrapped_text(buf,
|
|
462
|
+
g.x + 22, ry + (rowH - static_cast<int16_t>(8 * n->textSize)) / 2,
|
|
463
|
+
static_cast<uint16_t>(g.w - 30),
|
|
464
|
+
current ? rowFg : n->fg,
|
|
465
|
+
panel, n->textSize, n->fontAntialias, n->fontFace, n->letterSpacing,
|
|
466
|
+
n->lineHeight, n->whiteSpaceMode, 0, 0, 0);
|
|
467
|
+
if (current) {
|
|
468
|
+
int16_t cx = g.x + 7;
|
|
469
|
+
int16_t cy = ry + rowH / 2;
|
|
470
|
+
ui_display_draw_line(cx, cy, cx + 3, cy + 3, rowFg);
|
|
471
|
+
ui_display_draw_line(cx + 3, cy + 3, cx + 8, cy - 4, rowFg);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
__ui_select_menu_dirty = 0;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
static inline void ui_select_menu_tap(int16_t tx, int16_t ty) {
|
|
478
|
+
if (__ui_select_menu < 0) return;
|
|
479
|
+
uint16_t idx = static_cast<uint16_t>(__ui_select_menu);
|
|
480
|
+
UIRect g;
|
|
481
|
+
int16_t rowH;
|
|
482
|
+
ui_select_menu_geom(idx, &g, &rowH);
|
|
483
|
+
if (tx >= g.x && tx < g.x + g.w && ty >= g.y && ty < g.y + g.h) {
|
|
484
|
+
int16_t row = (ty - g.y - 4) / rowH;
|
|
485
|
+
if (row >= 0 && row < static_cast<int16_t>(__ui_nodes[idx].optionCount)) {
|
|
486
|
+
__ui_nodes[idx].value = row;
|
|
487
|
+
ui_mark_dirty(idx);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
ui_select_menu_close(1);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ── <drawer> implementation (device twin of the preview drawer) ──────────
|
|
494
|
+
|
|
495
|
+
static int8_t __ui_drawer_slot_of(uint16_t nodeIdx) {
|
|
496
|
+
for (uint8_t s = 0; s < __ui_drawer_slots; s++) {
|
|
497
|
+
if (__ui_drawer_idx[s] == static_cast<int16_t>(nodeIdx)) return static_cast<int8_t>(s);
|
|
498
|
+
}
|
|
499
|
+
return -1;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
static void ui_drawer_discover() {
|
|
503
|
+
__ui_drawer_slots = 0;
|
|
504
|
+
for (uint16_t i = 0; i < __ui_node_count && __ui_drawer_slots < UI_DRAWER_MAX; i++) {
|
|
505
|
+
if (__ui_nodes[i].drawerSide < 0) continue;
|
|
506
|
+
uint8_t s = __ui_drawer_slots++;
|
|
507
|
+
__ui_drawer_idx[s] = static_cast<int16_t>(i);
|
|
508
|
+
__ui_drawer_open[s] = 0;
|
|
509
|
+
__ui_drawer_progress[s] = 0;
|
|
510
|
+
// Seed closed: subtree offsets at full travel (never paints at rest).
|
|
511
|
+
ui_drawer_apply(s, 0);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
static void ui_drawer_apply(uint8_t slot, uint8_t progress) {
|
|
516
|
+
if (slot >= __ui_drawer_slots || __ui_drawer_idx[slot] < 0) return;
|
|
517
|
+
uint16_t di = static_cast<uint16_t>(__ui_drawer_idx[slot]);
|
|
518
|
+
UINode* d = &__ui_nodes[di];
|
|
519
|
+
int16_t travel = (d->drawerSide == 2 || d->drawerSide == 3)
|
|
520
|
+
? static_cast<int16_t>(d->box.w) : static_cast<int16_t>(d->box.h);
|
|
521
|
+
if (d->drawerSide == 4) travel = 0; // <dialog>: centered, no slide
|
|
522
|
+
else {
|
|
523
|
+
// Edge panels must slide FULLY off the display, not just their own
|
|
524
|
+
// height: a bottom panel anchored at the viewport bottom (bottom:0) is
|
|
525
|
+
// already AT the edge, so travel == box.h parks its top row exactly at
|
|
526
|
+
// the fold — a sliver stayed visible after close. The seeded closed
|
|
527
|
+
// state never showed that sliver before the first open (the initial
|
|
528
|
+
// paint's scroll-clip dropped it), and a closed panel can't be tapped
|
|
529
|
+
// open, so the post-close "peek" read as a rendering artifact, not an
|
|
530
|
+
// affordance — it is gone for drawers and toasts alike. Rest position =
|
|
531
|
+
// box minus ancestor scroll (transformOffset still holds the previous
|
|
532
|
+
// apply's slide at this point, so it cannot be used here).
|
|
533
|
+
int16_t restX = static_cast<int16_t>(d->box.x);
|
|
534
|
+
int16_t restY = static_cast<int16_t>(d->box.y);
|
|
535
|
+
uint16_t pa = d->parent;
|
|
536
|
+
while (pa != UI_NO_PARENT && pa < __ui_node_count) {
|
|
537
|
+
if (__ui_nodes[pa].scrollable) restY = static_cast<int16_t>(restY - __ui_nodes[pa].scrollY);
|
|
538
|
+
pa = __ui_nodes[pa].parent;
|
|
539
|
+
}
|
|
540
|
+
int16_t dl = 0, dt = 0, dr = 0, db = 0;
|
|
541
|
+
ui_display_target_bounds(&dl, &dt, &dr, &db);
|
|
542
|
+
int16_t need = d->drawerSide == 0 ? static_cast<int16_t>(db - restY)
|
|
543
|
+
: d->drawerSide == 1 ? static_cast<int16_t>(restY + d->box.h - dt)
|
|
544
|
+
: d->drawerSide == 2 ? static_cast<int16_t>(restX + d->box.w - dl)
|
|
545
|
+
: static_cast<int16_t>(dr - restX);
|
|
546
|
+
if (need > travel) travel = need;
|
|
547
|
+
}
|
|
548
|
+
int16_t off = static_cast<int16_t>((static_cast<int32_t>(travel) * (255 - progress)) / 255);
|
|
549
|
+
int16_t dx = d->drawerSide == 2 ? -off : d->drawerSide == 3 ? off : 0;
|
|
550
|
+
int16_t dy = d->drawerSide == 1 ? -off : d->drawerSide == 0 ? off : 0;
|
|
551
|
+
d->transformOffsetX = dx;
|
|
552
|
+
d->transformOffsetY = dy;
|
|
553
|
+
for (uint16_t i = di + 1; i < d->subtreeEnd; i++) {
|
|
554
|
+
// Descendants don't inherit transform offsets: their own box coords are
|
|
555
|
+
// layout-local, so shift their draw origin via transform offsets too.
|
|
556
|
+
__ui_nodes[i].transformOffsetX += dx - (__ui_drawer_last_dx[slot]);
|
|
557
|
+
__ui_nodes[i].transformOffsetY += dy - (__ui_drawer_last_dy[slot]);
|
|
558
|
+
}
|
|
559
|
+
__ui_drawer_last_dx[slot] = dx;
|
|
560
|
+
__ui_drawer_last_dy[slot] = dy;
|
|
561
|
+
__ui_drawer_progress[slot] = progress;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
static void ui_drawer_open(uint16_t nodeIdx) {
|
|
565
|
+
int8_t s = __ui_drawer_slot_of(nodeIdx);
|
|
566
|
+
if (s < 0) return;
|
|
567
|
+
__ui_drawer_open[s] = 1;
|
|
568
|
+
__ui_toast_elapsed[s] = 0; // <toast>: restart the auto-close window
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
static void ui_drawer_close(uint16_t nodeIdx) {
|
|
572
|
+
int8_t s = __ui_drawer_slot_of(nodeIdx);
|
|
573
|
+
if (s < 0) return;
|
|
574
|
+
__ui_drawer_open[s] = 0;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
static void ui_drawer_close_all() {
|
|
578
|
+
for (uint8_t s = 0; s < __ui_drawer_slots; s++) __ui_drawer_open[s] = 0;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
static void ui_drawer_tick(uint32_t deltaMs) {
|
|
582
|
+
uint8_t step = static_cast<uint8_t>((deltaMs * 255UL) / 180UL);
|
|
583
|
+
if (step == 0) step = 1;
|
|
584
|
+
for (uint8_t s = 0; s < __ui_drawer_slots; s++) {
|
|
585
|
+
if (__ui_drawer_idx[s] < 0) continue;
|
|
586
|
+
uint16_t tnode = static_cast<uint16_t>(__ui_drawer_idx[s]);
|
|
587
|
+
// <toast duration>: once fully open, count up and auto-close. A reopen
|
|
588
|
+
// (ui_drawer_open) restarts the window; manual close just wins.
|
|
589
|
+
if (__ui_nodes[tnode].toastDuration > 0 && __ui_drawer_open[s] &&
|
|
590
|
+
__ui_drawer_progress[s] == 255) {
|
|
591
|
+
__ui_toast_elapsed[s] += deltaMs;
|
|
592
|
+
if (__ui_toast_elapsed[s] >= __ui_nodes[tnode].toastDuration) {
|
|
593
|
+
__ui_drawer_open[s] = 0;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
uint8_t target = __ui_drawer_open[s] ? 255 : 0;
|
|
597
|
+
uint8_t p = __ui_drawer_progress[s];
|
|
598
|
+
if (p == target) continue;
|
|
599
|
+
// Compose each slide frame as the UNION of the drawer's old and new
|
|
600
|
+
// SUBTREE paint rects in tear-free bands (ui_render_screen_bands). The
|
|
601
|
+
// previous contract — mark the whole active screen dirty per slide step —
|
|
602
|
+
// repainted the entire screen with direct per-node clears on no-fb
|
|
603
|
+
// targets: visible flashing, and only ~3 steps fit inside the 180ms
|
|
604
|
+
// slide. The banded union is a fraction of the SPI traffic and never
|
|
605
|
+
// exposes an intermediate state. Falls back to the whole-screen dirty
|
|
606
|
+
// repaint when the band canvas can't allocate or a retained framebuffer
|
|
607
|
+
// composes the frame in RAM (its dirty-union push is already tear-free).
|
|
608
|
+
//
|
|
609
|
+
// SUBTREE extent, not the root's paint rect: a <dialog> root whose
|
|
610
|
+
// children are absolutely positioned lays out at h=0 — the root-rect
|
|
611
|
+
// union degenerated to an empty band (a no-op "composed" success), so
|
|
612
|
+
// NOTHING repainted on open or close on hardware. The preview already
|
|
613
|
+
// used its currentSubtreePaintRect here. Side-4 dialogs also snap
|
|
614
|
+
// instead of animating: travel is 0, so intermediate progress values
|
|
615
|
+
// are 180ms of identical frames — the snap composes ONE band frame for
|
|
616
|
+
// the open state and ONE for the erase, so modal toggles are tear-free
|
|
617
|
+
// on no-fb SPI targets too (mark-all's per-node clears flashed the
|
|
618
|
+
// whole screen).
|
|
619
|
+
uint16_t di = static_cast<uint16_t>(__ui_drawer_idx[s]);
|
|
620
|
+
uint8_t next = target > p ? (p + step > target ? target : p + step)
|
|
621
|
+
: (p < step || p - step < target ? target : p - step);
|
|
622
|
+
if (__ui_nodes[di].drawerSide == 4) next = target;
|
|
623
|
+
UIRect oldRect;
|
|
624
|
+
uint8_t hasOld = ui_subtree_current_paint_rect(di, &oldRect);
|
|
625
|
+
ui_drawer_apply(s, next);
|
|
626
|
+
UIRect newRect;
|
|
627
|
+
uint8_t hasNew = ui_subtree_current_paint_rect(di, &newRect);
|
|
628
|
+
// A failed rect query leaves x/y UNWRITTEN — unioning it against the
|
|
629
|
+
// good side mixed stack garbage into the band region (huge regions clipped
|
|
630
|
+
// to full-screen, or empty ones that "composed" nothing). When one side
|
|
631
|
+
// has no on-screen extent (a dialog snapping open from fully-closed, or
|
|
632
|
+
// erasing to fully-closed), the union is just the side that exists.
|
|
633
|
+
if (!hasOld && !hasNew) continue;
|
|
634
|
+
if (!hasOld) oldRect = newRect;
|
|
635
|
+
if (!hasNew) newRect = oldRect;
|
|
636
|
+
uint8_t composed = 0;
|
|
637
|
+
if (!ui_get_framebuffer()) {
|
|
638
|
+
int16_t ux0 = oldRect.x < newRect.x ? oldRect.x : newRect.x;
|
|
639
|
+
int16_t uy0 = oldRect.y < newRect.y ? oldRect.y : newRect.y;
|
|
640
|
+
int16_t ux1 = oldRect.x + oldRect.w > newRect.x + newRect.w ? static_cast<int16_t>(oldRect.x + oldRect.w) : static_cast<int16_t>(newRect.x + newRect.w);
|
|
641
|
+
int16_t uy1 = oldRect.y + oldRect.h > newRect.y + newRect.h ? static_cast<int16_t>(oldRect.y + oldRect.h) : static_cast<int16_t>(newRect.y + newRect.h);
|
|
642
|
+
composed = ui_render_screen_bands(ux0, uy0, static_cast<int16_t>(ux1 - ux0), static_cast<int16_t>(uy1 - uy0));
|
|
643
|
+
if (composed) {
|
|
644
|
+
// The drawer crossed a scroll viewport that caches its pixels; the
|
|
645
|
+
// canvas holds pre-slide content where the drawer now sits. Invalidate
|
|
646
|
+
// it so the next scroll recomposites instead of shifting stale pixels.
|
|
647
|
+
ui_invalidate_scroll_canvas_for_node(di);
|
|
648
|
+
// The composed bands are the AUTHORITATIVE frame for this region —
|
|
649
|
+
// every node fully inside it is already painted in correct stacking
|
|
650
|
+
// order. Clear those dirty flags or the dirty pass repaints them
|
|
651
|
+
// through the per-node ladder: a viewport-sized overlay (the dialog
|
|
652
|
+
// scrim) is too big for the repair canvas and falls to the DIRECT
|
|
653
|
+
// ladder — one flat full-screen fill that erased the just-composed
|
|
654
|
+
// frame, then the card/title/buttons re-popped band by band. That
|
|
655
|
+
// erase + re-layer sequence was the modal-toggle flash on hardware.
|
|
656
|
+
// Bindings run after the drawer tick, so any dirty flag they raise
|
|
657
|
+
// this frame still repaints normally.
|
|
658
|
+
for (uint16_t q = 0; q < __ui_node_count; q++) {
|
|
659
|
+
if (__ui_nodes[q].screenId != __ui_active_screen) continue;
|
|
660
|
+
if (!__ui_nodes[q].dirty) continue;
|
|
661
|
+
UIRect pr;
|
|
662
|
+
ui_node_current_paint_rect(q, &pr);
|
|
663
|
+
if (pr.w <= 0 || pr.h <= 0) continue;
|
|
664
|
+
if (pr.x >= ux0 && pr.y >= uy0 &&
|
|
665
|
+
static_cast<int16_t>(pr.x + pr.w) <= static_cast<int16_t>(ux1) &&
|
|
666
|
+
static_cast<int16_t>(pr.y + pr.h) <= static_cast<int16_t>(uy1)) {
|
|
667
|
+
__ui_nodes[q].dirty = 0;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
if (!composed) {
|
|
673
|
+
for (uint16_t i = 0; i < __ui_node_count; i++) {
|
|
674
|
+
if (__ui_nodes[i].screenId != __ui_active_screen) continue;
|
|
675
|
+
__ui_nodes[i].dirty = 1;
|
|
676
|
+
__ui_nodes[i].lastTextHeight = 0;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
`;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// ── Modal <select> option list ────────────────────────────────────────────
|
|
685
|
+
// Device twin of the preview's select modal: tapping a select opens a
|
|
686
|
+
// centered list of its options (capped to ~60% of the panel height); tapping
|
|
687
|
+
// a row sets the value and closes; tapping outside dismisses. The overlay is
|
|
688
|
+
// stamped after the dirty pass every frame while open; closing marks the
|
|
689
|
+
// whole tree dirty for the erase repaint.
|