@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.
Files changed (60) hide show
  1. package/README.md +1457 -1469
  2. package/dist/cli.js +22 -19
  3. package/dist/preview/host-ui-runtime.js +0 -1
  4. package/dist/ui-engine/model.js +7 -1
  5. package/dist/ui-engine/runtime-header/canvas-scrollbar.js +115 -115
  6. package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +182 -182
  7. package/dist/ui-engine/runtime-header/forward-decls.js +227 -216
  8. package/dist/ui-engine/runtime-header/init-press-input.js +138 -138
  9. package/dist/ui-engine/runtime-header/keyboard.js +676 -676
  10. package/dist/ui-engine/runtime-header/node-draw-body.js +1658 -1656
  11. package/dist/ui-engine/runtime-header/paint-order-coords.js +259 -259
  12. package/dist/ui-engine/runtime-header/paint-rects-repair.js +736 -736
  13. package/dist/ui-engine/runtime-header/scroll-physics.js +4 -4
  14. package/dist/ui-engine/runtime-header/structs.js +228 -228
  15. package/dist/ui-engine/runtime-header/text-rendering.js +888 -888
  16. package/dist/ui-engine/runtime-header/tick/bindings-phase.js +118 -118
  17. package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +896 -896
  18. package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +24 -24
  19. package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +11 -11
  20. package/dist/ui-engine/runtime-header/types-defines.js +110 -110
  21. package/dist/ui-engine/shadcn-kit.d.ts +1 -1
  22. package/dist/ui-engine/shadcn-kit.js +4 -0
  23. package/dist/ui-engine/ui-lowering.d.ts +1 -1
  24. package/dist/ui-engine/ui-lowering.js +14 -5
  25. package/dist/ui-engine/ui-registry.js +9 -1
  26. package/dist/wizard/display-catalog.d.ts +0 -3
  27. package/dist/wizard/display-catalog.js +3 -12
  28. package/dist/wizard/index.d.ts +2 -0
  29. package/dist/wizard/index.js +1 -0
  30. package/dist/wizard/integration-wizard.d.ts +3 -0
  31. package/dist/wizard/integration-wizard.js +69 -20
  32. package/dist/wizard/package-writer.d.ts +20 -0
  33. package/dist/wizard/package-writer.js +79 -0
  34. package/package.json +4 -4
  35. package/src/cli.ts +90 -87
  36. package/src/preview/host-ui-runtime.ts +0 -1
  37. package/src/ui-engine/model.ts +6 -1
  38. package/src/ui-engine/runtime-header/canvas-scrollbar.ts +121 -121
  39. package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +188 -188
  40. package/src/ui-engine/runtime-header/forward-decls.ts +238 -227
  41. package/src/ui-engine/runtime-header/init-press-input.ts +144 -144
  42. package/src/ui-engine/runtime-header/keyboard.ts +689 -689
  43. package/src/ui-engine/runtime-header/node-draw-body.ts +1683 -1681
  44. package/src/ui-engine/runtime-header/paint-order-coords.ts +265 -265
  45. package/src/ui-engine/runtime-header/paint-rects-repair.ts +742 -742
  46. package/src/ui-engine/runtime-header/scroll-physics.ts +4 -4
  47. package/src/ui-engine/runtime-header/structs.ts +234 -234
  48. package/src/ui-engine/runtime-header/text-rendering.ts +894 -894
  49. package/src/ui-engine/runtime-header/tick/bindings-phase.ts +124 -124
  50. package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +902 -902
  51. package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +30 -30
  52. package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +11 -11
  53. package/src/ui-engine/runtime-header/types-defines.ts +116 -116
  54. package/src/ui-engine/shadcn-kit.ts +4 -0
  55. package/src/ui-engine/ui-lowering.ts +12 -4
  56. package/src/ui-engine/ui-registry.ts +9 -1
  57. package/src/wizard/display-catalog.ts +261 -273
  58. package/src/wizard/index.ts +46 -38
  59. package/src/wizard/integration-wizard.ts +679 -619
  60. package/src/wizard/package-writer.ts +98 -0
@@ -1,265 +1,265 @@
1
- // Slice of the C++ runtime header (original source lines 1711-1894).
2
- // rects_intersect, visibility, draw order, owner table, screen bg, coordinate helpers, shadow extents, expand_rect.
3
- // See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
4
- export function emitPaintOrderCoords(): string {
5
- return `
6
- static inline uint8_t ui_rects_intersect(int16_t ax, int16_t ay, int16_t aw, int16_t ah,
7
- int16_t bx, int16_t by, int16_t bw, int16_t bh) {
8
- return ax + aw > bx && ax < bx + bw && ay + ah > by && ay < by + bh;
9
- }
10
-
11
- static inline uint8_t ui_is_effectively_visible(uint16_t nodeIdx) {
12
- if (nodeIdx >= __ui_node_count) return 0;
13
- if (!__ui_nodes[nodeIdx].visible) return 0;
14
- uint16_t p = __ui_nodes[nodeIdx].parent;
15
- while (p != UI_NO_PARENT && p < __ui_node_count) {
16
- if (!__ui_nodes[p].visible) return 0;
17
- p = __ui_nodes[p].parent;
18
- }
19
- // Centered overlay panels (<dialog>): offsets cannot hide the panel or its
20
- // subtree (travel is 0 by design), so the closed state gates visibility
21
- // directly — the root via its own slot, descendants via a side-4 ancestor
22
- // walk. Without the ancestor walk, dialog children (scrim, card, buttons)
23
- // have drawerSide -1 and no offsets: they painted while the dialog was
24
- // closed (an "Are you sure?" card on screen entry, buttons that close an
25
- // already-closed slot and appear dead). Edge drawers (<drawer>, <toast
26
- // side>) keep offset-only hiding — the bottom "peek" of an edge-anchored
27
- // panel must stay visible.
28
- if (__ui_nodes[nodeIdx].drawerSide == 4) {
29
- int8_t centerSlot = __ui_drawer_slot_of(nodeIdx);
30
- if (centerSlot < 0 || (!__ui_drawer_open[centerSlot] && __ui_drawer_progress[centerSlot] == 0)) return 0;
31
- } else {
32
- uint16_t a = __ui_nodes[nodeIdx].parent;
33
- while (a != UI_NO_PARENT && a < __ui_node_count) {
34
- if (__ui_nodes[a].drawerSide == 4) {
35
- int8_t centerSlot = __ui_drawer_slot_of(a);
36
- if (centerSlot < 0 || (!__ui_drawer_open[centerSlot] && __ui_drawer_progress[centerSlot] == 0)) return 0;
37
- break;
38
- }
39
- a = __ui_nodes[a].parent;
40
- }
41
- }
42
- // No closed-drawer gating here for edge drawers: they hide by transform
43
- // offsets alone (seeded at full travel by ui_drawer_discover), so the
44
- // bottom "peek" of a bottom-anchored panel stays visible.
45
- return 1;
46
- }
47
-
48
- // Effective stacking z, CSS-style: a non-zero zIndex raises the node AND its
49
- // subtree — a z-raised panel forms a stacking context, so its descendants
50
- // stack WITH it (above lower-z siblings), never independently UNDER it. A
51
- // flat (zIndex, index) sort painted a dialog's scrim (z30) and card (z31)
52
- // over the card's own z0 children (title/description/buttons): the band
53
- // compositor erased the content it had just drawn (only the topmost child
54
- // survived), and hit-testing saw the card as "topmost" so its buttons could
55
- // never receive taps. 0 = "auto" (no context) — keep walking ancestors.
56
- static inline int16_t ui_stacking_z(uint16_t nodeIdx) {
57
- uint16_t n = nodeIdx;
58
- while (n != UI_NO_PARENT && n < __ui_node_count) {
59
- if (__ui_nodes[n].zIndex != 0) return __ui_nodes[n].zIndex;
60
- n = __ui_nodes[n].parent;
61
- }
62
- return 0;
63
- }
64
-
65
- static inline uint8_t ui_node_draws_before(uint16_t a, uint16_t b) {
66
- int16_t za = ui_stacking_z(a);
67
- int16_t zb = ui_stacking_z(b);
68
- if (za != zb) return za < zb;
69
- // Same stacking level: source order. Sibling stacking contexts occupy
70
- // disjoint index ranges, so this also keeps a context's subtree contiguous.
71
- return a < b;
72
- }
73
-
74
- // Sort node indices once at startup so the dirty draw pass is O(N) instead of
75
- // re-scanning all nodes for every dirty repaint (O(D·N)).
76
- static inline void ui_build_draw_order() {
77
- if (__ui_draw_order || __ui_node_count == 0) return;
78
- __ui_draw_order = new (std::nothrow) uint16_t[__ui_node_count];
79
- if (!__ui_draw_order) return;
80
- for (uint16_t i = 0; i < __ui_node_count; i++) __ui_draw_order[i] = i;
81
- for (uint16_t a = 1; a < __ui_node_count; a++) {
82
- uint16_t key = __ui_draw_order[a];
83
- int16_t j = static_cast<int16_t>(a) - 1;
84
- while (j >= 0 && ui_node_draws_before(key, __ui_draw_order[j])) {
85
- __ui_draw_order[j + 1] = __ui_draw_order[j];
86
- j--;
87
- }
88
- __ui_draw_order[j + 1] = key;
89
- }
90
- }
91
-
92
- // Index scroll containers once so touch/wheel ownership doesn't scan unrelated
93
- // nodes each frame.
94
- static inline void ui_build_scroll_owner_table() {
95
- if (__ui_scroll_owners || __ui_node_count == 0) return;
96
- uint16_t count = 0;
97
- for (uint16_t i = 0; i < __ui_node_count; i++) {
98
- if (__ui_nodes[i].scrollable) count++;
99
- }
100
- __ui_scroll_owner_count = count;
101
- if (!count) return;
102
- __ui_scroll_owners = new (std::nothrow) uint16_t[count];
103
- if (!__ui_scroll_owners) {
104
- __ui_scroll_owner_count = 0;
105
- return;
106
- }
107
- uint16_t w = 0;
108
- for (uint16_t i = 0; i < __ui_node_count; i++) {
109
- if (__ui_nodes[i].scrollable) {
110
- __ui_scroll_owners[w++] = i;
111
- }
112
- }
113
- }
114
-
115
- static inline void ui_refresh_active_screen_bg_node() {
116
- __ui_active_screen_bg_node = 0xFFFF;
117
- for (uint16_t i = 0; i < __ui_node_count; i++) {
118
- if (__ui_nodes[i].screenId == __ui_active_screen && __ui_nodes[i].kind == NODE_FILL) {
119
- __ui_active_screen_bg_node = i;
120
- return;
121
- }
122
- }
123
- }
124
-
125
- static inline uint8_t ui_scroll_subtree_has_dirty(uint16_t scrollNode) {
126
- if (scrollNode >= __ui_node_count) return 0;
127
- uint16_t end = __ui_nodes[scrollNode].subtreeEnd;
128
- if (end > __ui_node_count) end = __ui_node_count;
129
- for (uint16_t c = scrollNode + 1; c < end; c++) {
130
- if (__ui_nodes[c].dirty &&
131
- ui_is_effectively_visible(c) &&
132
- __ui_nodes[c].screenId == __ui_active_screen) {
133
- return 1;
134
- }
135
- }
136
- return 0;
137
- }
138
-
139
- static inline uint8_t ui_is_ancestor_of(uint16_t candidate, uint16_t nodeIdx) {
140
- uint16_t p = __ui_nodes[nodeIdx].parent;
141
- while (p != UI_NO_PARENT && p < __ui_node_count) {
142
- if (p == candidate) return 1;
143
- p = __ui_nodes[p].parent;
144
- }
145
- return 0;
146
- }
147
-
148
- static inline int16_t ui_pressed_offset_x_for_node(uint16_t nodeIdx) {
149
- return __ui_nodes[nodeIdx].value > 0 ? __ui_nodes[nodeIdx].pressedOffsetX : 0;
150
- }
151
-
152
- static inline int16_t ui_pressed_offset_y_for_node(uint16_t nodeIdx) {
153
- return __ui_nodes[nodeIdx].value > 0 ? __ui_nodes[nodeIdx].pressedOffsetY : 0;
154
- }
155
-
156
- static inline int16_t ui_base_draw_x_for_node(uint16_t nodeIdx) {
157
- return __ui_nodes[nodeIdx].box.x + __ui_nodes[nodeIdx].transformOffsetX;
158
- }
159
-
160
- static inline int16_t ui_draw_x_for_node(uint16_t nodeIdx) {
161
- return ui_base_draw_x_for_node(nodeIdx) + ui_pressed_offset_x_for_node(nodeIdx);
162
- }
163
-
164
- static inline int16_t ui_base_draw_y_for_node(uint16_t nodeIdx) {
165
- int16_t y = __ui_nodes[nodeIdx].box.y + __ui_nodes[nodeIdx].transformOffsetY;
166
- uint16_t p = __ui_nodes[nodeIdx].parent;
167
- while (p != UI_NO_PARENT && p < __ui_node_count) {
168
- if (__ui_nodes[p].scrollable) y -= __ui_nodes[p].scrollY;
169
- p = __ui_nodes[p].parent;
170
- }
171
- return y;
172
- }
173
-
174
- static inline int16_t ui_draw_y_for_node(uint16_t nodeIdx) {
175
- return ui_base_draw_y_for_node(nodeIdx) + ui_pressed_offset_y_for_node(nodeIdx);
176
- }
177
-
178
- // Find the scrollable container (list or generic scroll view) whose box contains
179
- // a point, on the active screen, with content overflowing the viewport. Mirrors
180
- // the touch path's scroll-scan (ui_touch_down) so the wheel handler finds the
181
- // SAME owner a drag would — robust against the cursor resting on a non-child
182
- // node (text, a sibling, padding) where the hit-test + ancestor-walk approach
183
- // misses. Returns the topmost such node by draw order, or -1 if none.
184
- static inline int16_t ui_scroll_node_at(int16_t tx, int16_t ty) {
185
- int16_t bestScroll = -1;
186
- if (__ui_scroll_owners) {
187
- for (uint16_t oi = 0; oi < __ui_scroll_owner_count; oi++) {
188
- uint16_t i = __ui_scroll_owners[oi];
189
- if (!ui_is_effectively_visible(i)) continue;
190
- if (__ui_nodes[i].screenId != __ui_active_screen) continue;
191
- if (__ui_nodes[i].contentHeight <= __ui_nodes[i].box.h) continue;
192
- int16_t drawX = ui_draw_x_for_node(i);
193
- int16_t drawY = ui_draw_y_for_node(i);
194
- if (tx >= drawX && tx < drawX + __ui_nodes[i].box.w &&
195
- ty >= drawY && ty < drawY + __ui_nodes[i].box.h &&
196
- (bestScroll < 0 || ui_node_draws_before(static_cast<uint16_t>(bestScroll), i))) {
197
- bestScroll = static_cast<int16_t>(i);
198
- }
199
- }
200
- } else {
201
- for (uint16_t i = 0; i < __ui_node_count; i++) {
202
- if (!__ui_nodes[i].scrollable || !ui_is_effectively_visible(i)) continue;
203
- if (__ui_nodes[i].screenId != __ui_active_screen) continue;
204
- if (__ui_nodes[i].contentHeight <= __ui_nodes[i].box.h) continue;
205
- int16_t drawX = ui_draw_x_for_node(i);
206
- int16_t drawY = ui_draw_y_for_node(i);
207
- if (tx >= drawX && tx < drawX + __ui_nodes[i].box.w &&
208
- ty >= drawY && ty < drawY + __ui_nodes[i].box.h &&
209
- (bestScroll < 0 || ui_node_draws_before(static_cast<uint16_t>(bestScroll), i))) {
210
- bestScroll = static_cast<int16_t>(i);
211
- }
212
- }
213
- }
214
- return bestScroll;
215
- }
216
-
217
- // Resolve the actual backdrop for a node by walking painted ancestors. A direct
218
- // parent may be transparent; using its clearColor there can blend against the
219
- // wrong layer when siblings or an opaque grandparent are behind the node.
220
- static inline UI_COLOR_T ui_parent_clear_color(uint16_t nodeIdx) {
221
- uint16_t p = __ui_nodes[nodeIdx].parent;
222
- while (p != UI_NO_PARENT && p < __ui_node_count) {
223
- if (__ui_nodes[p].hasBg) {
224
- UI_COLOR_T backdrop = __ui_nodes[p].bg;
225
- if (__ui_nodes[p].opacity < 100) {
226
- uint16_t pp = __ui_nodes[p].parent;
227
- UI_COLOR_T under = __ui_nodes[p].clearColor;
228
- if (pp != UI_NO_PARENT && pp < __ui_node_count) under = ui_parent_clear_color(p);
229
- backdrop = ui_blend(backdrop, under, __ui_nodes[p].opacity);
230
- }
231
- return backdrop;
232
- }
233
- p = __ui_nodes[p].parent;
234
- }
235
- return __ui_nodes[nodeIdx].clearColor;
236
- }
237
-
238
- static inline void ui_shadow_extents(uint16_t nodeIdx, int16_t* left, int16_t* top, int16_t* right, int16_t* bottom) {
239
- *left = 0; *top = 0; *right = 0; *bottom = 0;
240
- for (uint8_t s = 0; s < __ui_nodes[nodeIdx].shadowCount && s < 4; s++) {
241
- if (__ui_nodes[nodeIdx].shadowInset[s]) continue;
242
- int16_t blur = __ui_nodes[nodeIdx].shadowBlur[s];
243
- if (blur == 0) blur = 1;
244
- int16_t ox = __ui_nodes[nodeIdx].shadowOffsetX[s];
245
- int16_t oy = __ui_nodes[nodeIdx].shadowOffsetY[s];
246
- int16_t l = blur - ox;
247
- int16_t t = blur - oy;
248
- int16_t r = blur + ox;
249
- int16_t b = blur + oy;
250
- if (l > *left) *left = l;
251
- if (t > *top) *top = t;
252
- if (r > *right) *right = r;
253
- if (b > *bottom) *bottom = b;
254
- }
255
- }
256
-
257
- static inline void ui_expand_rect(int16_t* x0, int16_t* y0, int16_t* x1, int16_t* y1,
258
- int16_t rx0, int16_t ry0, int16_t rx1, int16_t ry1) {
259
- if (rx0 < *x0) *x0 = rx0;
260
- if (ry0 < *y0) *y0 = ry0;
261
- if (rx1 > *x1) *x1 = rx1;
262
- if (ry1 > *y1) *y1 = ry1;
263
- }
264
- `;
265
- }
1
+ // Slice of the C++ runtime header (original source lines 1711-1894).
2
+ // rects_intersect, visibility, draw order, owner table, screen bg, coordinate helpers, shadow extents, expand_rect.
3
+ // See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
4
+ export function emitPaintOrderCoords(): string {
5
+ return `
6
+ static inline uint8_t ui_rects_intersect(int16_t ax, int16_t ay, int16_t aw, int16_t ah,
7
+ int16_t bx, int16_t by, int16_t bw, int16_t bh) {
8
+ return ax + aw > bx && ax < bx + bw && ay + ah > by && ay < by + bh;
9
+ }
10
+
11
+ static inline uint8_t ui_is_effectively_visible(uint16_t nodeIdx) {
12
+ if (nodeIdx >= __ui_node_count) return 0;
13
+ if (!__ui_nodes[nodeIdx].visible) return 0;
14
+ uint16_t p = __ui_nodes[nodeIdx].parent;
15
+ while (p != UI_NO_PARENT && p < __ui_node_count) {
16
+ if (!__ui_nodes[p].visible) return 0;
17
+ p = __ui_nodes[p].parent;
18
+ }
19
+ // Centered overlay panels (<dialog>): offsets cannot hide the panel or its
20
+ // subtree (travel is 0 by design), so the closed state gates visibility
21
+ // directly — the root via its own slot, descendants via a side-4 ancestor
22
+ // walk. Without the ancestor walk, dialog children (scrim, card, buttons)
23
+ // have drawerSide -1 and no offsets: they painted while the dialog was
24
+ // closed (an "Are you sure?" card on screen entry, buttons that close an
25
+ // already-closed slot and appear dead). Edge drawers (<drawer>, <toast
26
+ // side>) keep offset-only hiding — the bottom "peek" of an edge-anchored
27
+ // panel must stay visible.
28
+ if (__ui_nodes[nodeIdx].drawerSide == 4) {
29
+ int8_t centerSlot = __ui_drawer_slot_of(nodeIdx);
30
+ if (centerSlot < 0 || (!__ui_drawer_open[centerSlot] && __ui_drawer_progress[centerSlot] == 0)) return 0;
31
+ } else {
32
+ uint16_t a = __ui_nodes[nodeIdx].parent;
33
+ while (a != UI_NO_PARENT && a < __ui_node_count) {
34
+ if (__ui_nodes[a].drawerSide == 4) {
35
+ int8_t centerSlot = __ui_drawer_slot_of(a);
36
+ if (centerSlot < 0 || (!__ui_drawer_open[centerSlot] && __ui_drawer_progress[centerSlot] == 0)) return 0;
37
+ break;
38
+ }
39
+ a = __ui_nodes[a].parent;
40
+ }
41
+ }
42
+ // No closed-drawer gating here for edge drawers: they hide by transform
43
+ // offsets alone (seeded at full travel by ui_drawer_discover), so the
44
+ // bottom "peek" of a bottom-anchored panel stays visible.
45
+ return 1;
46
+ }
47
+
48
+ // Effective stacking z, CSS-style: a non-zero zIndex raises the node AND its
49
+ // subtree — a z-raised panel forms a stacking context, so its descendants
50
+ // stack WITH it (above lower-z siblings), never independently UNDER it. A
51
+ // flat (zIndex, index) sort painted a dialog's scrim (z30) and card (z31)
52
+ // over the card's own z0 children (title/description/buttons): the band
53
+ // compositor erased the content it had just drawn (only the topmost child
54
+ // survived), and hit-testing saw the card as "topmost" so its buttons could
55
+ // never receive taps. 0 = "auto" (no context) — keep walking ancestors.
56
+ static inline int16_t ui_stacking_z(uint16_t nodeIdx) {
57
+ uint16_t n = nodeIdx;
58
+ while (n != UI_NO_PARENT && n < __ui_node_count) {
59
+ if (__ui_nodes[n].zIndex != 0) return __ui_nodes[n].zIndex;
60
+ n = __ui_nodes[n].parent;
61
+ }
62
+ return 0;
63
+ }
64
+
65
+ static inline uint8_t ui_node_draws_before(uint16_t a, uint16_t b) {
66
+ int16_t za = ui_stacking_z(a);
67
+ int16_t zb = ui_stacking_z(b);
68
+ if (za != zb) return za < zb;
69
+ // Same stacking level: source order. Sibling stacking contexts occupy
70
+ // disjoint index ranges, so this also keeps a context's subtree contiguous.
71
+ return a < b;
72
+ }
73
+
74
+ // Sort node indices once at startup so the dirty draw pass is O(N) instead of
75
+ // re-scanning all nodes for every dirty repaint (O(D·N)).
76
+ static inline void ui_build_draw_order() {
77
+ if (__ui_draw_order || __ui_node_count == 0) return;
78
+ __ui_draw_order = new (std::nothrow) uint16_t[__ui_node_count];
79
+ if (!__ui_draw_order) return;
80
+ for (uint16_t i = 0; i < __ui_node_count; i++) __ui_draw_order[i] = i;
81
+ for (uint16_t a = 1; a < __ui_node_count; a++) {
82
+ uint16_t key = __ui_draw_order[a];
83
+ int16_t j = static_cast<int16_t>(a) - 1;
84
+ while (j >= 0 && ui_node_draws_before(key, __ui_draw_order[j])) {
85
+ __ui_draw_order[j + 1] = __ui_draw_order[j];
86
+ j--;
87
+ }
88
+ __ui_draw_order[j + 1] = key;
89
+ }
90
+ }
91
+
92
+ // Index scroll containers once so touch/wheel ownership doesn't scan unrelated
93
+ // nodes each frame.
94
+ static inline void ui_build_scroll_owner_table() {
95
+ if (__ui_scroll_owners || __ui_node_count == 0) return;
96
+ uint16_t count = 0;
97
+ for (uint16_t i = 0; i < __ui_node_count; i++) {
98
+ if (__ui_nodes[i].scrollable) count++;
99
+ }
100
+ __ui_scroll_owner_count = count;
101
+ if (!count) return;
102
+ __ui_scroll_owners = new (std::nothrow) uint16_t[count];
103
+ if (!__ui_scroll_owners) {
104
+ __ui_scroll_owner_count = 0;
105
+ return;
106
+ }
107
+ uint16_t w = 0;
108
+ for (uint16_t i = 0; i < __ui_node_count; i++) {
109
+ if (__ui_nodes[i].scrollable) {
110
+ __ui_scroll_owners[w++] = i;
111
+ }
112
+ }
113
+ }
114
+
115
+ static inline void ui_refresh_active_screen_bg_node() {
116
+ __ui_active_screen_bg_node = 0xFFFF;
117
+ for (uint16_t i = 0; i < __ui_node_count; i++) {
118
+ if (__ui_nodes[i].screenId == __ui_active_screen && __ui_nodes[i].kind == NODE_FILL) {
119
+ __ui_active_screen_bg_node = i;
120
+ return;
121
+ }
122
+ }
123
+ }
124
+
125
+ static inline uint8_t ui_scroll_subtree_has_dirty(uint16_t scrollNode) {
126
+ if (scrollNode >= __ui_node_count) return 0;
127
+ uint16_t end = __ui_nodes[scrollNode].subtreeEnd;
128
+ if (end > __ui_node_count) end = __ui_node_count;
129
+ for (uint16_t c = scrollNode + 1; c < end; c++) {
130
+ if (__ui_nodes[c].dirty &&
131
+ ui_is_effectively_visible(c) &&
132
+ __ui_nodes[c].screenId == __ui_active_screen) {
133
+ return 1;
134
+ }
135
+ }
136
+ return 0;
137
+ }
138
+
139
+ static inline uint8_t ui_is_ancestor_of(uint16_t candidate, uint16_t nodeIdx) {
140
+ uint16_t p = __ui_nodes[nodeIdx].parent;
141
+ while (p != UI_NO_PARENT && p < __ui_node_count) {
142
+ if (p == candidate) return 1;
143
+ p = __ui_nodes[p].parent;
144
+ }
145
+ return 0;
146
+ }
147
+
148
+ static inline int16_t ui_pressed_offset_x_for_node(uint16_t nodeIdx) {
149
+ return __ui_nodes[nodeIdx].value > 0 ? __ui_nodes[nodeIdx].pressedOffsetX : 0;
150
+ }
151
+
152
+ static inline int16_t ui_pressed_offset_y_for_node(uint16_t nodeIdx) {
153
+ return __ui_nodes[nodeIdx].value > 0 ? __ui_nodes[nodeIdx].pressedOffsetY : 0;
154
+ }
155
+
156
+ static inline int16_t ui_base_draw_x_for_node(uint16_t nodeIdx) {
157
+ return __ui_nodes[nodeIdx].box.x + __ui_nodes[nodeIdx].transformOffsetX;
158
+ }
159
+
160
+ static inline int16_t ui_draw_x_for_node(uint16_t nodeIdx) {
161
+ return ui_base_draw_x_for_node(nodeIdx) + ui_pressed_offset_x_for_node(nodeIdx);
162
+ }
163
+
164
+ static inline int16_t ui_base_draw_y_for_node(uint16_t nodeIdx) {
165
+ int16_t y = __ui_nodes[nodeIdx].box.y + __ui_nodes[nodeIdx].transformOffsetY;
166
+ uint16_t p = __ui_nodes[nodeIdx].parent;
167
+ while (p != UI_NO_PARENT && p < __ui_node_count) {
168
+ if (__ui_nodes[p].scrollable) y -= __ui_nodes[p].scrollY;
169
+ p = __ui_nodes[p].parent;
170
+ }
171
+ return y;
172
+ }
173
+
174
+ static inline int16_t ui_draw_y_for_node(uint16_t nodeIdx) {
175
+ return ui_base_draw_y_for_node(nodeIdx) + ui_pressed_offset_y_for_node(nodeIdx);
176
+ }
177
+
178
+ // Find the scrollable container (list or generic scroll view) whose box contains
179
+ // a point, on the active screen, with content overflowing the viewport. Mirrors
180
+ // the touch path's scroll-scan (ui_touch_down) so the wheel handler finds the
181
+ // SAME owner a drag would — robust against the cursor resting on a non-child
182
+ // node (text, a sibling, padding) where the hit-test + ancestor-walk approach
183
+ // misses. Returns the topmost such node by draw order, or -1 if none.
184
+ static inline int16_t ui_scroll_node_at(int16_t tx, int16_t ty) {
185
+ int16_t bestScroll = -1;
186
+ if (__ui_scroll_owners) {
187
+ for (uint16_t oi = 0; oi < __ui_scroll_owner_count; oi++) {
188
+ uint16_t i = __ui_scroll_owners[oi];
189
+ if (!ui_is_effectively_visible(i)) continue;
190
+ if (__ui_nodes[i].screenId != __ui_active_screen) continue;
191
+ if (__ui_nodes[i].contentHeight <= __ui_nodes[i].box.h) continue;
192
+ int16_t drawX = ui_draw_x_for_node(i);
193
+ int16_t drawY = ui_draw_y_for_node(i);
194
+ if (tx >= drawX && tx < drawX + __ui_nodes[i].box.w &&
195
+ ty >= drawY && ty < drawY + __ui_nodes[i].box.h &&
196
+ (bestScroll < 0 || ui_node_draws_before(static_cast<uint16_t>(bestScroll), i))) {
197
+ bestScroll = static_cast<int16_t>(i);
198
+ }
199
+ }
200
+ } else {
201
+ for (uint16_t i = 0; i < __ui_node_count; i++) {
202
+ if (!__ui_nodes[i].scrollable || !ui_is_effectively_visible(i)) continue;
203
+ if (__ui_nodes[i].screenId != __ui_active_screen) continue;
204
+ if (__ui_nodes[i].contentHeight <= __ui_nodes[i].box.h) continue;
205
+ int16_t drawX = ui_draw_x_for_node(i);
206
+ int16_t drawY = ui_draw_y_for_node(i);
207
+ if (tx >= drawX && tx < drawX + __ui_nodes[i].box.w &&
208
+ ty >= drawY && ty < drawY + __ui_nodes[i].box.h &&
209
+ (bestScroll < 0 || ui_node_draws_before(static_cast<uint16_t>(bestScroll), i))) {
210
+ bestScroll = static_cast<int16_t>(i);
211
+ }
212
+ }
213
+ }
214
+ return bestScroll;
215
+ }
216
+
217
+ // Resolve the actual backdrop for a node by walking painted ancestors. A direct
218
+ // parent may be transparent; using its clearColor there can blend against the
219
+ // wrong layer when siblings or an opaque grandparent are behind the node.
220
+ static inline UI_COLOR_T ui_parent_clear_color(uint16_t nodeIdx) {
221
+ uint16_t p = __ui_nodes[nodeIdx].parent;
222
+ while (p != UI_NO_PARENT && p < __ui_node_count) {
223
+ if (__ui_nodes[p].hasBg) {
224
+ UI_COLOR_T backdrop = __ui_nodes[p].bg;
225
+ if (__ui_nodes[p].opacity < 100) {
226
+ uint16_t pp = __ui_nodes[p].parent;
227
+ UI_COLOR_T under = __ui_nodes[p].clearColor;
228
+ if (pp != UI_NO_PARENT && pp < __ui_node_count) under = ui_parent_clear_color(p);
229
+ backdrop = ui_blend(backdrop, under, __ui_nodes[p].opacity);
230
+ }
231
+ return backdrop;
232
+ }
233
+ p = __ui_nodes[p].parent;
234
+ }
235
+ return __ui_nodes[nodeIdx].clearColor;
236
+ }
237
+
238
+ static inline void ui_shadow_extents(uint16_t nodeIdx, int16_t* left, int16_t* top, int16_t* right, int16_t* bottom) {
239
+ *left = 0; *top = 0; *right = 0; *bottom = 0;
240
+ for (uint8_t s = 0; s < __ui_nodes[nodeIdx].shadowCount && s < 4; s++) {
241
+ if (__ui_nodes[nodeIdx].shadowInset[s]) continue;
242
+ int16_t blur = __ui_nodes[nodeIdx].shadowBlur[s];
243
+ if (blur == 0) blur = 1;
244
+ int16_t ox = __ui_nodes[nodeIdx].shadowOffsetX[s];
245
+ int16_t oy = __ui_nodes[nodeIdx].shadowOffsetY[s];
246
+ int16_t l = blur - ox;
247
+ int16_t t = blur - oy;
248
+ int16_t r = blur + ox;
249
+ int16_t b = blur + oy;
250
+ if (l > *left) *left = l;
251
+ if (t > *top) *top = t;
252
+ if (r > *right) *right = r;
253
+ if (b > *bottom) *bottom = b;
254
+ }
255
+ }
256
+
257
+ static inline void ui_expand_rect(int16_t* x0, int16_t* y0, int16_t* x1, int16_t* y1,
258
+ int16_t rx0, int16_t ry0, int16_t rx1, int16_t ry1) {
259
+ if (rx0 < *x0) *x0 = rx0;
260
+ if (ry0 < *y0) *y0 = ry0;
261
+ if (rx1 > *x1) *x1 = rx1;
262
+ if (ry1 > *y1) *y1 = ry1;
263
+ }
264
+ `;
265
+ }