@typecad/ui 1.0.0-alpha.7 → 1.0.0-alpha.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +6 -0
  2. package/dist/engine-index.js +0 -2
  3. package/dist/preview/host-ui-runtime.d.ts +1 -0
  4. package/dist/preview/host-ui-runtime.js +56 -11
  5. package/dist/ui-engine/font-assets.d.ts +8 -0
  6. package/dist/ui-engine/font-assets.js +13 -0
  7. package/dist/ui-engine/layout-engine.js +19 -6
  8. package/dist/ui-engine/model.d.ts +1 -1
  9. package/dist/ui-engine/model.js +10 -4
  10. package/dist/ui-engine/runtime-header/antialiasing.js +52 -32
  11. package/dist/ui-engine/runtime-header/blend-bodies.js +4 -4
  12. package/dist/ui-engine/runtime-header/canvas-helpers.js +32 -65
  13. package/dist/ui-engine/runtime-header/canvas-scrollbar.js +22 -10
  14. package/dist/ui-engine/runtime-header/color-mono-refresh.js +10 -10
  15. package/dist/ui-engine/runtime-header/cuttlefish-gfx.js +59 -29
  16. package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +26 -10
  17. package/dist/ui-engine/runtime-header/display-shim.js +8 -8
  18. package/dist/ui-engine/runtime-header/forward-decls.js +48 -6
  19. package/dist/ui-engine/runtime-header/image-drawing.js +154 -84
  20. package/dist/ui-engine/runtime-header/init-press-input.js +20 -5
  21. package/dist/ui-engine/runtime-header/keyboard.js +19 -16
  22. package/dist/ui-engine/runtime-header/node-decoration.js +8 -8
  23. package/dist/ui-engine/runtime-header/node-draw-body.d.ts +1 -0
  24. package/dist/ui-engine/runtime-header/node-draw-body.js +1402 -0
  25. package/dist/ui-engine/runtime-header/paint-order-coords.js +48 -18
  26. package/dist/ui-engine/runtime-header/paint-rects-repair.js +38 -17
  27. package/dist/ui-engine/runtime-header/scroll-physics.js +15 -14
  28. package/dist/ui-engine/runtime-header/state-bindings-nav.js +23 -13
  29. package/dist/ui-engine/runtime-header/structs.js +12 -10
  30. package/dist/ui-engine/runtime-header/text-rendering.js +132 -71
  31. package/dist/ui-engine/runtime-header/tick/bindings-phase.js +29 -10
  32. package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +299 -747
  33. package/dist/ui-engine/runtime-header/tick/flush-phase.js +7 -3
  34. package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +19 -3
  35. package/dist/ui-engine/runtime-header/tick/transitions-phase.js +31 -24
  36. package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +42 -40
  37. package/dist/ui-engine/runtime-header/types-defines.js +23 -3
  38. package/dist/ui-engine/runtime-header.js +2 -0
  39. package/dist/ui-engine/transpile-ui.js +2 -2
  40. package/dist/ui-engine/ua-stylesheet.js +46 -3
  41. package/dist/ui-engine/ui-lowering.d.ts +1 -3
  42. package/dist/ui-engine/ui-lowering.js +4 -23
  43. package/dist/ui-engine/ui-registry.js +1 -7
  44. package/package.json +2 -2
  45. package/src/engine-index.ts +0 -2
  46. package/src/preview/host-ui-runtime.ts +56 -12
  47. package/src/ui-engine/font-assets.ts +13 -0
  48. package/src/ui-engine/layout-engine.ts +19 -6
  49. package/src/ui-engine/model.ts +10 -5
  50. package/src/ui-engine/runtime-header/antialiasing.ts +52 -32
  51. package/src/ui-engine/runtime-header/blend-bodies.ts +4 -4
  52. package/src/ui-engine/runtime-header/canvas-helpers.ts +32 -65
  53. package/src/ui-engine/runtime-header/canvas-scrollbar.ts +22 -10
  54. package/src/ui-engine/runtime-header/color-mono-refresh.ts +10 -10
  55. package/src/ui-engine/runtime-header/cuttlefish-gfx.ts +59 -29
  56. package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +26 -10
  57. package/src/ui-engine/runtime-header/display-shim.ts +8 -8
  58. package/src/ui-engine/runtime-header/forward-decls.ts +48 -6
  59. package/src/ui-engine/runtime-header/image-drawing.ts +154 -84
  60. package/src/ui-engine/runtime-header/init-press-input.ts +20 -5
  61. package/src/ui-engine/runtime-header/keyboard.ts +19 -16
  62. package/src/ui-engine/runtime-header/node-decoration.ts +8 -8
  63. package/src/ui-engine/runtime-header/node-draw-body.ts +1402 -0
  64. package/src/ui-engine/runtime-header/paint-order-coords.ts +48 -18
  65. package/src/ui-engine/runtime-header/paint-rects-repair.ts +38 -17
  66. package/src/ui-engine/runtime-header/scroll-physics.ts +15 -14
  67. package/src/ui-engine/runtime-header/state-bindings-nav.ts +23 -13
  68. package/src/ui-engine/runtime-header/structs.ts +12 -10
  69. package/src/ui-engine/runtime-header/text-rendering.ts +132 -71
  70. package/src/ui-engine/runtime-header/tick/bindings-phase.ts +29 -10
  71. package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +299 -747
  72. package/src/ui-engine/runtime-header/tick/flush-phase.ts +7 -3
  73. package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +19 -3
  74. package/src/ui-engine/runtime-header/tick/transitions-phase.ts +31 -24
  75. package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +42 -40
  76. package/src/ui-engine/runtime-header/types-defines.ts +23 -3
  77. package/src/ui-engine/runtime-header.ts +2 -0
  78. package/src/ui-engine/transpile-ui.ts +2 -2
  79. package/src/ui-engine/ua-stylesheet.ts +46 -3
  80. package/src/ui-engine/ui-lowering.ts +3 -25
  81. package/src/ui-engine/ui-registry.ts +1 -8
  82. package/dist/ui-engine/scroll-memory-diagnostics.d.ts +0 -16
  83. package/dist/ui-engine/scroll-memory-diagnostics.js +0 -58
  84. package/src/ui-engine/scroll-memory-diagnostics.ts +0 -79
@@ -1,6 +1,12 @@
1
1
  // Slice of the C++ runtime header (original source lines 622-752).
2
2
  // navigate, canvas/scroll/text/paint/clip/AA/image forward declarations, persistent canvas globals.
3
3
  // See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
4
+ //
5
+ // EMIT BOUNDARY: This file is the structural head of the UI runtime header
6
+ // surface (C) — every partial emitter in this directory produces bytes that are
7
+ // assembled into the C++ header inlined into user sketches. The emitted bytes
8
+ // are covered by the TypeCAD Runtime Exception (see RUNTIME_EXCEPTION.md at the
9
+ // repository root) and are not subject to the license of this tool source.
4
10
  export function emitForwardDecls() {
5
11
  return `
6
12
  // Early forward declaration: ui_navigate (below) calls ui_release_canvas_state
@@ -9,9 +15,17 @@ export function emitForwardDecls() {
9
15
  static inline void ui_release_canvas_state();
10
16
  static inline void ui_set_pressed(uint16_t nodeIdx, uint8_t pressed);
11
17
  static inline void ui_refresh_active_screen_bg_node();
18
+ static inline CuttlefishCanvas16* ui_get_framebuffer();
19
+ // Set when the next frame must be composed from the active screen background;
20
+ // declared before ui_navigate because navigation can request that composition.
21
+ static uint8_t __ui_fb_needs_compose = 1;
12
22
 
13
23
  // Navigate to a screen by index. Marks the new screen's nodes dirty, releases
14
- // persistent canvas state, and optionally clears the display.
24
+ // persistent canvas state, and requests a complete off-screen composition when
25
+ // the full-frame path is available. The physical panel is never cleared here on
26
+ // that path: navigation must not expose an intermediate frame while the next
27
+ // frame is built. Without a full-frame buffer, the fallback remains best-effort;
28
+ // no-TE TFT hardware cannot provide a strict tear-free guarantee.
15
29
  static inline void ui_navigate(uint8_t screenIdx) {
16
30
  if (screenIdx >= __ui_screen_count || screenIdx == __ui_active_screen) return;
17
31
  __ui_active_screen = screenIdx;
@@ -23,7 +37,7 @@ static inline void ui_navigate(uint8_t screenIdx) {
23
37
  // and the button would stay stuck in its :pressed color.
24
38
  if (__ui_touch_node >= 0 && __ui_touch_node < __ui_node_count &&
25
39
  __ui_nodes[__ui_touch_node].kind == NODE_BUTTON && __ui_nodes[__ui_touch_node].value != 0) {
26
- ui_set_pressed((uint16_t)__ui_touch_node, 0);
40
+ ui_set_pressed(static_cast<uint16_t>(__ui_touch_node), 0);
27
41
  }
28
42
  __ui_scroll_node = -1;
29
43
  __ui_touch_node = -1;
@@ -34,17 +48,34 @@ static inline void ui_navigate(uint8_t screenIdx) {
34
48
  // resident and fragments the heap, so the new screen's buffer can't get a
35
49
  // contiguous block (the "works first, then blanks until reset" symptom).
36
50
  ui_release_canvas_state();
37
- // Clear the entire display so old screen content doesn't show. On deferred-
38
- // refresh panels (e-ink) a full clear flashes, so skip it — the all-nodes-
39
- // dirty marking below drives a full repaint via partial refresh instead.
51
+ // A framebuffer-capable target keeps the old frame visible until the new one
52
+ // is complete. Targets without enough memory retain the legacy clear fallback
53
+ // so stale pixels from the old screen cannot remain; this constrained path is
54
+ // best-effort because the panel has no TE/vblank synchronization.
55
+ if (ui_get_framebuffer()) {
56
+ __ui_fb_needs_compose = 1;
57
+ } else {
40
58
  #ifndef UI_REFRESH_DEFERRED
41
- display_fillScreen(0x0000);
59
+ UI_COLOR_T navBg = (UI_COLOR_T)0;
60
+ if (__ui_active_screen_bg_node < __ui_node_count) {
61
+ navBg = __ui_nodes[__ui_active_screen_bg_node].hasBg
62
+ ? __ui_nodes[__ui_active_screen_bg_node].bg
63
+ : __ui_nodes[__ui_active_screen_bg_node].clearColor;
64
+ }
65
+ display_fillScreen(navBg);
42
66
  #endif
67
+ }
43
68
  // Mark all nodes dirty so the new screen fully redraws.
44
69
  for (uint16_t i = 0; i < __ui_node_count; i++) {
45
70
  __ui_nodes[i].dirty = 1;
46
71
  __ui_nodes[i].lastTextHeight = 0;
47
72
  __ui_nodes[i].layoutCacheKey = 0;
73
+ if (__ui_nodes[i].scrollable) {
74
+ // The next screen needs a complete scroll-canvas seed; subsequent local
75
+ // child updates can use retained-canvas repairs.
76
+ __ui_nodes[i].lastPaintedScrollY = static_cast<int16_t>(
77
+ __ui_nodes[i].scrollY - (__ui_nodes[i].box.h > 0 ? __ui_nodes[i].box.h : 1));
78
+ }
48
79
  if (__ui_nodes[i].kind == NODE_PROGRESS || __ui_nodes[i].kind == NODE_RANGE) __ui_nodes[i].lastTextWidth = -1;
49
80
  else __ui_nodes[i].lastTextWidth = 0;
50
81
  }
@@ -77,6 +108,7 @@ static inline uint8_t ui_subtree_current_paint_rect(uint16_t nodeIdx, UIRect* ou
77
108
  static inline void ui_mark_overlapping_higher_layers_dirty(uint16_t nodeIdx);
78
109
  static inline void ui_mark_overlapping_higher_layers_dirty_for_rect(uint16_t nodeIdx, const UIRect* r);
79
110
  static inline void ui_mark_scroll_view_dirty(uint16_t scrollNode);
111
+ static inline UI_COLOR_T ui_parent_clear_color(uint16_t nodeIdx);
80
112
  static inline void ui_release_canvas_state();
81
113
  static inline void ui_set_visible(uint16_t nodeIdx, uint8_t visible);
82
114
  static inline void ui_invalidate_scroll_canvas_for_node(uint16_t nodeIdx);
@@ -101,6 +133,11 @@ static inline void ui_aa_fill_circle(CuttlefishCanvas16* c, int16_t cx, int16_t
101
133
  static inline uint8_t ui_repair_current_node_paint_with_parent(uint16_t nodeIdx, UIRect* r);
102
134
  static inline void ui_clear_node_paint_rect(uint16_t nodeIdx, const UIRect* paintRect);
103
135
  static inline uint8_t ui_try_repair_geometry_fill(uint16_t nodeIdx, const UIRect* oldRect);
136
+ // Band renderers (defined in node-draw-body; forward-declared so the NODE_LIST
137
+ // case can fall back to ui_render_list_bands before its definition site).
138
+ static inline uint8_t ui_render_node_bands(uint16_t nodeIdx, int16_t prX, int16_t prY, int16_t prW, int16_t prH);
139
+ static inline uint8_t ui_render_list_bands(uint16_t i);
140
+ static inline uint8_t ui_render_list_direct(uint16_t i);
104
141
  static inline void ui_draw_node_border(uint16_t i, int16_t drawX, int16_t drawY, UI_COLOR_T color);
105
142
  static inline void ui_draw_node_outline(uint16_t i, int16_t drawX, int16_t drawY);
106
143
  static inline void ui_draw_gradient_fill(uint16_t i, int16_t drawY);
@@ -114,6 +151,10 @@ static inline void ui_draw_scaled_image(const UIImage* img, int16_t x, int16_t y
114
151
  int16_t drawW, int16_t drawH);
115
152
 
116
153
  static CuttlefishDisplayTarget* __ui_gfx = display_defaultTarget();
154
+ // Full-frame composition is required for the first frame and after navigation.
155
+ // Once composed, subsequent dirty updates can safely modify the retained
156
+ // framebuffer without rebuilding unchanged nodes. The flags are declared
157
+ // before ui_navigate above because navigation sets the compose request.
117
158
  // Persistent canvas slots, all freed on screen change (ui_navigate) so each
118
159
  // screen starts with a clean heap. Without this, the first screen's canvas
119
160
  // buffer stays resident and fragments the heap, so a later screen's buffer
@@ -125,6 +166,7 @@ static CuttlefishCanvas16* __ui_list_canvas = nullptr; // virtualized <lis
125
166
  static int16_t __ui_list_canvas_node = -1; // node currently represented by __ui_list_canvas
126
167
  static CuttlefishCanvas16* __ui_node_canvas = nullptr; // <canvas> element offscreen
127
168
  static CuttlefishCanvas16* __ui_repair_canvas = nullptr; // buffered-paint / exposed-strip
169
+ static CuttlefishCanvas16* __ui_band_canvas = nullptr; // band renderer (no-PSRAM scroll)
128
170
  static CuttlefishCanvas16* __ui_kb_canvas = nullptr; // on-screen keyboard overlay
129
171
  static int16_t __ui_canvas_fallback_w = 0; // dimensions for direct <canvas> fallback
130
172
  static int16_t __ui_canvas_fallback_h = 0;
@@ -3,12 +3,54 @@
3
3
  // See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
4
4
  export function emitImageDrawing() {
5
5
  return `
6
- // ── Full-screen framebuffer (opt-in PSRAM perf experiment) ───────────────────
7
- // Disabled by default: pushing a full 320×240 frame for a tiny dirty button can
8
- // look like a global brightness flash on SPI TFTs. Enable only when a target
9
- // has enough PSRAM and a full-frame repaint is preferable to partial updates.
6
+ // ── Full-screen framebuffer (opt-in PSRAM composition path) ──────────────────
7
+ // Composes a complete frame off-screen so navigation and dirty-node updates do
8
+ // not expose intermediate clears/primitive writes. The panel still receives a
9
+ // sequential SPI transfer; without a TE/vblank signal this is a strong tearing
10
+ // reduction, not a strict hardware-level tear-free guarantee.
10
11
  static CuttlefishCanvas16* __ui_fb = nullptr;
11
12
  static uint8_t __ui_fb_tried = 0; // 0 = not yet attempted, 1 = alloc attempted
13
+ // Dirty bounds for the retained framebuffer. A local touch update should not
14
+ // retransmit the entire panel: the full-frame burst is long enough to look like
15
+ // a brightness dip on SPI TFTs even though the framebuffer itself is coherent.
16
+ static uint8_t __ui_fb_dirty = 0;
17
+ static int16_t __ui_fb_dirty_x0 = 0;
18
+ static int16_t __ui_fb_dirty_y0 = 0;
19
+ static int16_t __ui_fb_dirty_x1 = 0;
20
+ static int16_t __ui_fb_dirty_y1 = 0;
21
+
22
+ static inline void ui_fb_begin_frame() {
23
+ __ui_fb_dirty = 0;
24
+ __ui_fb_dirty_x0 = display_width();
25
+ __ui_fb_dirty_y0 = display_height();
26
+ __ui_fb_dirty_x1 = 0;
27
+ __ui_fb_dirty_y1 = 0;
28
+ }
29
+
30
+ static inline void ui_fb_add_rect(int16_t x, int16_t y, int16_t w, int16_t h) {
31
+ if (w <= 0 || h <= 0) return;
32
+ int16_t x0 = x < 0 ? 0 : x;
33
+ int16_t y0 = y < 0 ? 0 : y;
34
+ int16_t x1 = x + w;
35
+ int16_t y1 = y + h;
36
+ int16_t dw = display_width();
37
+ int16_t dh = display_height();
38
+ if (x1 > dw) x1 = dw;
39
+ if (y1 > dh) y1 = dh;
40
+ if (x0 >= x1 || y0 >= y1) return;
41
+ if (!__ui_fb_dirty) {
42
+ __ui_fb_dirty_x0 = x0;
43
+ __ui_fb_dirty_y0 = y0;
44
+ __ui_fb_dirty_x1 = x1;
45
+ __ui_fb_dirty_y1 = y1;
46
+ __ui_fb_dirty = 1;
47
+ return;
48
+ }
49
+ if (x0 < __ui_fb_dirty_x0) __ui_fb_dirty_x0 = x0;
50
+ if (y0 < __ui_fb_dirty_y0) __ui_fb_dirty_y0 = y0;
51
+ if (x1 > __ui_fb_dirty_x1) __ui_fb_dirty_x1 = x1;
52
+ if (y1 > __ui_fb_dirty_y1) __ui_fb_dirty_y1 = y1;
53
+ }
12
54
 
13
55
  // Get the framebuffer, allocating once (in PSRAM when available). Returns null
14
56
  // when PSRAM is absent or the allocation failed — callers fall back to direct.
@@ -31,14 +73,42 @@ static inline CuttlefishCanvas16* ui_get_framebuffer() {
31
73
  return __ui_fb;
32
74
  }
33
75
 
34
- // Push the entire framebuffer to the display in one bulk SPI transaction.
76
+ // Push only the changed framebuffer bounds. Rows are sent separately when the
77
+ // dirty union is narrower than the framebuffer because the source canvas has a
78
+ // wider stride than the panel window. Each row is complete before it reaches
79
+ // the panel, and the display adapter can synchronize each window to scanline.
35
80
  static inline void ui_push_framebuffer() {
36
- if (!__ui_fb || !display_canvasBuffer(__ui_fb)) return;
37
- int16_t w = display_canvasWidth(__ui_fb);
38
- int16_t h = display_canvasHeight(__ui_fb);
81
+ if (!__ui_fb || !display_canvasBuffer(__ui_fb) || !__ui_fb_dirty) return;
82
+ int16_t fw = display_canvasWidth(__ui_fb);
83
+ int16_t fh = display_canvasHeight(__ui_fb);
84
+ int16_t x = __ui_fb_dirty_x0;
85
+ int16_t y = __ui_fb_dirty_y0;
86
+ int16_t w = static_cast<int16_t>(__ui_fb_dirty_x1 - x);
87
+ int16_t h = static_cast<int16_t>(__ui_fb_dirty_y1 - y);
88
+ if (x < 0 || y < 0 || w <= 0 || h <= 0 || x >= fw || y >= fh) return;
89
+ if (x + w > fw) w = static_cast<int16_t>(fw - x);
90
+ if (y + h > fh) h = static_cast<int16_t>(fh - y);
91
+ // Use the panel's native bulk-pixel path, not drawRGBBitmap(). The native
92
+ // CuttlefishGFX implementation of drawRGBBitmap is intentionally generic and
93
+ // emits one writePixel transaction per pixel; using it for a retained-frame
94
+ // flush turns a small local update into hundreds of visible SPI flashes.
95
+ UI_COLOR_T* pixels = display_canvasBuffer(__ui_fb);
39
96
  display_startWrite();
40
- display_setAddrWindow(0, 0, w, h);
41
- display_writePixels(display_canvasBuffer(__ui_fb), (uint32_t)w * h);
97
+ if (x == 0 && w == fw) {
98
+ // Full-width rows are contiguous and can use one bulk write.
99
+ display_setAddrWindow(x, y, w, h);
100
+ display_writePixels(pixels + static_cast<int32_t>(y) * fw,
101
+ static_cast<uint32_t>(w) * h);
102
+ } else {
103
+ // Narrow dirty bounds require one row at a time because the source stride
104
+ // is fw while each destination window is only w pixels wide.
105
+ for (int16_t row = 0; row < h; row++) {
106
+ display_setAddrWindow(x, static_cast<int16_t>(y + row), w, 1);
107
+ display_writePixels(
108
+ pixels + static_cast<int32_t>(y + row) * fw + x,
109
+ static_cast<uint32_t>(w));
110
+ }
111
+ }
42
112
  display_endWrite();
43
113
  }
44
114
 
@@ -82,8 +152,8 @@ static inline void ui_draw_image_with_fit(const UIImage* img, int16_t x, int16_t
82
152
  offX = 0;
83
153
  offY = 0;
84
154
  } else if (fitMode == 2 || fitMode == 3 || fitMode == 4) {
85
- int32_t scaleX = ((int32_t)targetW * 1000) / srcW;
86
- int32_t scaleY = ((int32_t)targetH * 1000) / srcH;
155
+ int32_t scaleX = (static_cast<int32_t>(targetW) * 1000) / srcW;
156
+ int32_t scaleY = (static_cast<int32_t>(targetH) * 1000) / srcH;
87
157
  if (scaleX < 1) scaleX = 1;
88
158
  if (scaleY < 1) scaleY = 1;
89
159
  int32_t scale = scaleX;
@@ -95,8 +165,8 @@ static inline void ui_draw_image_with_fit(const UIImage* img, int16_t x, int16_t
95
165
  if (scaleY < scaleX) scale = scaleY;
96
166
  if (scale > 1000) scale = 1000;
97
167
  }
98
- drawW = (int16_t)((int32_t)srcW * scale / 1000);
99
- drawH = (int16_t)((int32_t)srcH * scale / 1000);
168
+ drawW = static_cast<int16_t>(static_cast<int32_t>(srcW) * scale / 1000);
169
+ drawH = static_cast<int16_t>(static_cast<int32_t>(srcH) * scale / 1000);
100
170
  if (drawW < 1) drawW = 1;
101
171
  if (drawH < 1) drawH = 1;
102
172
  if (fitMode == 3) {
@@ -115,40 +185,40 @@ static inline void ui_draw_image_with_fit(const UIImage* img, int16_t x, int16_t
115
185
  if (!ui_clip_rect_to_display_target(&clipX, &clipY, &clipW, &clipH)) return;
116
186
  int16_t txStart = 0, txEnd = targetW, tyStart = 0, tyEnd = targetH;
117
187
  if (q == 0) {
118
- txStart = ui_clamp_i16((int16_t)(clipX - x), 0, targetW);
119
- txEnd = ui_clamp_i16((int16_t)(clipX + clipW - x), 0, targetW);
120
- tyStart = ui_clamp_i16((int16_t)(clipY - y), 0, targetH);
121
- tyEnd = ui_clamp_i16((int16_t)(clipY + clipH - y), 0, targetH);
188
+ txStart = ui_clamp_i16(static_cast<int16_t>(clipX - x), 0, targetW);
189
+ txEnd = ui_clamp_i16(static_cast<int16_t>(clipX + clipW - x), 0, targetW);
190
+ tyStart = ui_clamp_i16(static_cast<int16_t>(clipY - y), 0, targetH);
191
+ tyEnd = ui_clamp_i16(static_cast<int16_t>(clipY + clipH - y), 0, targetH);
122
192
  } else if (q == 1) {
123
- txStart = ui_clamp_i16((int16_t)(clipY - y), 0, targetW);
124
- txEnd = ui_clamp_i16((int16_t)(clipY + clipH - y), 0, targetW);
125
- tyStart = ui_clamp_i16((int16_t)(x + targetH - (clipX + clipW)), 0, targetH);
126
- tyEnd = ui_clamp_i16((int16_t)(x + targetH - clipX), 0, targetH);
193
+ txStart = ui_clamp_i16(static_cast<int16_t>(clipY - y), 0, targetW);
194
+ txEnd = ui_clamp_i16(static_cast<int16_t>(clipY + clipH - y), 0, targetW);
195
+ tyStart = ui_clamp_i16(static_cast<int16_t>(x + targetH - (clipX + clipW)), 0, targetH);
196
+ tyEnd = ui_clamp_i16(static_cast<int16_t>(x + targetH - clipX), 0, targetH);
127
197
  } else if (q == 2) {
128
- txStart = ui_clamp_i16((int16_t)(x + targetW - (clipX + clipW)), 0, targetW);
129
- txEnd = ui_clamp_i16((int16_t)(x + targetW - clipX), 0, targetW);
130
- tyStart = ui_clamp_i16((int16_t)(y + targetH - (clipY + clipH)), 0, targetH);
131
- tyEnd = ui_clamp_i16((int16_t)(y + targetH - clipY), 0, targetH);
198
+ txStart = ui_clamp_i16(static_cast<int16_t>(x + targetW - (clipX + clipW)), 0, targetW);
199
+ txEnd = ui_clamp_i16(static_cast<int16_t>(x + targetW - clipX), 0, targetW);
200
+ tyStart = ui_clamp_i16(static_cast<int16_t>(y + targetH - (clipY + clipH)), 0, targetH);
201
+ tyEnd = ui_clamp_i16(static_cast<int16_t>(y + targetH - clipY), 0, targetH);
132
202
  } else {
133
- txStart = ui_clamp_i16((int16_t)(y + targetW - (clipY + clipH)), 0, targetW);
134
- txEnd = ui_clamp_i16((int16_t)(y + targetW - clipY), 0, targetW);
135
- tyStart = ui_clamp_i16((int16_t)(clipX - x), 0, targetH);
136
- tyEnd = ui_clamp_i16((int16_t)(clipX + clipW - x), 0, targetH);
203
+ txStart = ui_clamp_i16(static_cast<int16_t>(y + targetW - (clipY + clipH)), 0, targetW);
204
+ txEnd = ui_clamp_i16(static_cast<int16_t>(y + targetW - clipY), 0, targetW);
205
+ tyStart = ui_clamp_i16(static_cast<int16_t>(clipX - x), 0, targetH);
206
+ tyEnd = ui_clamp_i16(static_cast<int16_t>(clipX + clipW - x), 0, targetH);
137
207
  }
138
208
  if (txStart >= txEnd || tyStart >= tyEnd) return;
139
209
  for (int16_t ty = tyStart; ty < tyEnd; ty++) {
140
210
  int16_t localY = ty - offY;
141
211
  if (localY < 0 || localY >= drawH) continue;
142
- int16_t srcY = ((int32_t)localY * srcH) / drawH;
212
+ int16_t srcY = (static_cast<int32_t>(localY) * srcH) / drawH;
143
213
  if (srcY < 0) srcY = 0;
144
214
  if (srcY >= srcH) srcY = srcH - 1;
145
215
  for (int16_t tx = txStart; tx < txEnd; tx++) {
146
216
  int16_t localX = tx - offX;
147
217
  if (localX < 0 || localX >= drawW) continue;
148
- int16_t srcX = ((int32_t)localX * srcW) / drawW;
218
+ int16_t srcX = (static_cast<int32_t>(localX) * srcW) / drawW;
149
219
  if (srcX < 0) srcX = 0;
150
220
  if (srcX >= srcW) srcX = srcW - 1;
151
- UI_COLOR_T color = img->data[(int32_t)srcY * srcW + srcX];
221
+ UI_COLOR_T color = img->data[static_cast<int32_t>(srcY) * srcW + srcX];
152
222
  int16_t dx = tx;
153
223
  int16_t dy = ty;
154
224
  if (q == 1) {
@@ -177,43 +247,43 @@ static inline void ui_draw_scaled_image(const UIImage* img, int16_t x, int16_t y
177
247
  if (!ui_clip_rect_to_display_target(&clipX, &clipY, &clipW, &clipH)) return;
178
248
  int16_t dxStart = 0, dxEnd = drawW, dyStart = 0, dyEnd = drawH;
179
249
  if (q == 0) {
180
- dxStart = ui_clamp_i16((int16_t)(clipX - x), 0, drawW);
181
- dxEnd = ui_clamp_i16((int16_t)(clipX + clipW - x), 0, drawW);
182
- dyStart = ui_clamp_i16((int16_t)(clipY - y), 0, drawH);
183
- dyEnd = ui_clamp_i16((int16_t)(clipY + clipH - y), 0, drawH);
250
+ dxStart = ui_clamp_i16(static_cast<int16_t>(clipX - x), 0, drawW);
251
+ dxEnd = ui_clamp_i16(static_cast<int16_t>(clipX + clipW - x), 0, drawW);
252
+ dyStart = ui_clamp_i16(static_cast<int16_t>(clipY - y), 0, drawH);
253
+ dyEnd = ui_clamp_i16(static_cast<int16_t>(clipY + clipH - y), 0, drawH);
184
254
  } else if (q == 1) {
185
- dxStart = ui_clamp_i16((int16_t)(clipY - y), 0, drawW);
186
- dxEnd = ui_clamp_i16((int16_t)(clipY + clipH - y), 0, drawW);
187
- dyStart = ui_clamp_i16((int16_t)(x + drawH - (clipX + clipW)), 0, drawH);
188
- dyEnd = ui_clamp_i16((int16_t)(x + drawH - clipX), 0, drawH);
255
+ dxStart = ui_clamp_i16(static_cast<int16_t>(clipY - y), 0, drawW);
256
+ dxEnd = ui_clamp_i16(static_cast<int16_t>(clipY + clipH - y), 0, drawW);
257
+ dyStart = ui_clamp_i16(static_cast<int16_t>(x + drawH - (clipX + clipW)), 0, drawH);
258
+ dyEnd = ui_clamp_i16(static_cast<int16_t>(x + drawH - clipX), 0, drawH);
189
259
  } else if (q == 2) {
190
- dxStart = ui_clamp_i16((int16_t)(x + drawW - (clipX + clipW)), 0, drawW);
191
- dxEnd = ui_clamp_i16((int16_t)(x + drawW - clipX), 0, drawW);
192
- dyStart = ui_clamp_i16((int16_t)(y + drawH - (clipY + clipH)), 0, drawH);
193
- dyEnd = ui_clamp_i16((int16_t)(y + drawH - clipY), 0, drawH);
260
+ dxStart = ui_clamp_i16(static_cast<int16_t>(x + drawW - (clipX + clipW)), 0, drawW);
261
+ dxEnd = ui_clamp_i16(static_cast<int16_t>(x + drawW - clipX), 0, drawW);
262
+ dyStart = ui_clamp_i16(static_cast<int16_t>(y + drawH - (clipY + clipH)), 0, drawH);
263
+ dyEnd = ui_clamp_i16(static_cast<int16_t>(y + drawH - clipY), 0, drawH);
194
264
  } else {
195
- dxStart = ui_clamp_i16((int16_t)(y + drawW - (clipY + clipH)), 0, drawW);
196
- dxEnd = ui_clamp_i16((int16_t)(y + drawW - clipY), 0, drawW);
197
- dyStart = ui_clamp_i16((int16_t)(clipX - x), 0, drawH);
198
- dyEnd = ui_clamp_i16((int16_t)(clipX + clipW - x), 0, drawH);
265
+ dxStart = ui_clamp_i16(static_cast<int16_t>(y + drawW - (clipY + clipH)), 0, drawW);
266
+ dxEnd = ui_clamp_i16(static_cast<int16_t>(y + drawW - clipY), 0, drawW);
267
+ dyStart = ui_clamp_i16(static_cast<int16_t>(clipX - x), 0, drawH);
268
+ dyEnd = ui_clamp_i16(static_cast<int16_t>(clipX + clipW - x), 0, drawH);
199
269
  }
200
270
  if (dxStart >= dxEnd || dyStart >= dyEnd) return;
201
271
  if (q == 0) {
202
272
  // Simple case: no rotation, draw with scaling
203
273
  if (drawW == img->w && drawH == img->h) {
204
- int16_t rows = (int16_t)(dyEnd - dyStart);
205
- int16_t cols = (int16_t)(dxEnd - dxStart);
274
+ int16_t rows = static_cast<int16_t>(dyEnd - dyStart);
275
+ int16_t cols = static_cast<int16_t>(dxEnd - dxStart);
206
276
  for (int16_t row = 0; row < rows; row++) {
207
- ui_display_draw_rgb_bitmap((int16_t)(x + dxStart), (int16_t)(y + dyStart + row),
208
- img->data + (int32_t)(dyStart + row) * img->w + dxStart, cols, 1);
277
+ ui_display_draw_rgb_bitmap(static_cast<int16_t>(x + dxStart), static_cast<int16_t>(y + dyStart + row),
278
+ img->data + static_cast<int32_t>(dyStart + row) * img->w + dxStart, cols, 1);
209
279
  }
210
280
  } else {
211
281
  // Scale using nearest-neighbor
212
282
  for (int16_t dy = dyStart; dy < dyEnd; dy++) {
213
- int16_t srcY = ((int32_t)dy * img->h) / drawH;
283
+ int16_t srcY = (static_cast<int32_t>(dy) * img->h) / drawH;
214
284
  for (int16_t dx = dxStart; dx < dxEnd; dx++) {
215
- int16_t srcX = ((int32_t)dx * img->w) / drawW;
216
- UI_COLOR_T color = img->data[(int32_t)srcY * img->w + srcX];
285
+ int16_t srcX = (static_cast<int32_t>(dx) * img->w) / drawW;
286
+ UI_COLOR_T color = img->data[static_cast<int32_t>(srcY) * img->w + srcX];
217
287
  ui_display_draw_pixel(x + dx, y + dy, color);
218
288
  }
219
289
  }
@@ -221,10 +291,10 @@ static inline void ui_draw_scaled_image(const UIImage* img, int16_t x, int16_t y
221
291
  return;
222
292
  }
223
293
  for (int16_t dy = dyStart; dy < dyEnd; dy++) {
224
- int16_t srcY = ((int32_t)dy * img->h) / drawH;
294
+ int16_t srcY = (static_cast<int32_t>(dy) * img->h) / drawH;
225
295
  for (int16_t dx = dxStart; dx < dxEnd; dx++) {
226
- int16_t srcX = ((int32_t)dx * img->w) / drawW;
227
- UI_COLOR_T color = img->data[(int32_t)srcY * img->w + srcX];
296
+ int16_t srcX = (static_cast<int32_t>(dx) * img->w) / drawW;
297
+ UI_COLOR_T color = img->data[static_cast<int32_t>(srcY) * img->w + srcX];
228
298
  int16_t rdx = 0, rdy = 0;
229
299
  if (q == 1) {
230
300
  rdx = drawH - 1 - dy;
@@ -249,48 +319,48 @@ static inline void ui_draw_image_rotated(const UIImage* img, int16_t x, int16_t
249
319
  int16_t clipH = (q == 1 || q == 3) ? img->w : img->h;
250
320
  if (!ui_clip_rect_to_display_target(&clipX, &clipY, &clipW, &clipH)) return;
251
321
  if (q == 0) {
252
- int16_t sx0 = ui_clamp_i16((int16_t)(clipX - x), 0, (int16_t)img->w);
253
- int16_t sy0 = ui_clamp_i16((int16_t)(clipY - y), 0, (int16_t)img->h);
254
- int16_t sw = ui_clamp_i16(clipW, 0, (int16_t)(img->w - sx0));
255
- int16_t sh = ui_clamp_i16(clipH, 0, (int16_t)(img->h - sy0));
322
+ int16_t sx0 = ui_clamp_i16(static_cast<int16_t>(clipX - x), 0, static_cast<int16_t>(img->w));
323
+ int16_t sy0 = ui_clamp_i16(static_cast<int16_t>(clipY - y), 0, static_cast<int16_t>(img->h));
324
+ int16_t sw = ui_clamp_i16(clipW, 0, static_cast<int16_t>(img->w - sx0));
325
+ int16_t sh = ui_clamp_i16(clipH, 0, static_cast<int16_t>(img->h - sy0));
256
326
  for (int16_t row = 0; row < sh; row++) {
257
- ui_display_draw_rgb_bitmap((int16_t)(x + sx0), (int16_t)(y + sy0 + row),
258
- img->data + (int32_t)(sy0 + row) * img->w + sx0, sw, 1);
327
+ ui_display_draw_rgb_bitmap(static_cast<int16_t>(x + sx0), static_cast<int16_t>(y + sy0 + row),
328
+ img->data + static_cast<int32_t>(sy0 + row) * img->w + sx0, sw, 1);
259
329
  }
260
330
  return;
261
331
  }
262
332
  uint16_t sxStart = 0, sxEnd = img->w, syStart = 0, syEnd = img->h;
263
333
  if (q == 1) {
264
- sxStart = (uint16_t)ui_clamp_i16((int16_t)(clipY - y), 0, (int16_t)img->w);
265
- sxEnd = (uint16_t)ui_clamp_i16((int16_t)(clipY + clipH - y), 0, (int16_t)img->w);
266
- syStart = (uint16_t)ui_clamp_i16((int16_t)(x + img->h - (clipX + clipW)), 0, (int16_t)img->h);
267
- syEnd = (uint16_t)ui_clamp_i16((int16_t)(x + img->h - clipX), 0, (int16_t)img->h);
334
+ sxStart = static_cast<uint16_t>(ui_clamp_i16(static_cast<int16_t>(clipY - y), 0, static_cast<int16_t>(img->w)));
335
+ sxEnd = static_cast<uint16_t>(ui_clamp_i16(static_cast<int16_t>(clipY + clipH - y), 0, static_cast<int16_t>(img->w)));
336
+ syStart = static_cast<uint16_t>(ui_clamp_i16(static_cast<int16_t>(x + img->h - (clipX + clipW)), 0, static_cast<int16_t>(img->h)));
337
+ syEnd = static_cast<uint16_t>(ui_clamp_i16(static_cast<int16_t>(x + img->h - clipX), 0, static_cast<int16_t>(img->h)));
268
338
  } else if (q == 2) {
269
- sxStart = (uint16_t)ui_clamp_i16((int16_t)(x + img->w - (clipX + clipW)), 0, (int16_t)img->w);
270
- sxEnd = (uint16_t)ui_clamp_i16((int16_t)(x + img->w - clipX), 0, (int16_t)img->w);
271
- syStart = (uint16_t)ui_clamp_i16((int16_t)(y + img->h - (clipY + clipH)), 0, (int16_t)img->h);
272
- syEnd = (uint16_t)ui_clamp_i16((int16_t)(y + img->h - clipY), 0, (int16_t)img->h);
339
+ sxStart = static_cast<uint16_t>(ui_clamp_i16(static_cast<int16_t>(x + img->w - (clipX + clipW)), 0, static_cast<int16_t>(img->w)));
340
+ sxEnd = static_cast<uint16_t>(ui_clamp_i16(static_cast<int16_t>(x + img->w - clipX), 0, static_cast<int16_t>(img->w)));
341
+ syStart = static_cast<uint16_t>(ui_clamp_i16(static_cast<int16_t>(y + img->h - (clipY + clipH)), 0, static_cast<int16_t>(img->h)));
342
+ syEnd = static_cast<uint16_t>(ui_clamp_i16(static_cast<int16_t>(y + img->h - clipY), 0, static_cast<int16_t>(img->h)));
273
343
  } else {
274
- sxStart = (uint16_t)ui_clamp_i16((int16_t)(y + img->w - (clipY + clipH)), 0, (int16_t)img->w);
275
- sxEnd = (uint16_t)ui_clamp_i16((int16_t)(y + img->w - clipY), 0, (int16_t)img->w);
276
- syStart = (uint16_t)ui_clamp_i16((int16_t)(clipX - x), 0, (int16_t)img->h);
277
- syEnd = (uint16_t)ui_clamp_i16((int16_t)(clipX + clipW - x), 0, (int16_t)img->h);
344
+ sxStart = static_cast<uint16_t>(ui_clamp_i16(static_cast<int16_t>(y + img->w - (clipY + clipH)), 0, static_cast<int16_t>(img->w)));
345
+ sxEnd = static_cast<uint16_t>(ui_clamp_i16(static_cast<int16_t>(y + img->w - clipY), 0, static_cast<int16_t>(img->w)));
346
+ syStart = static_cast<uint16_t>(ui_clamp_i16(static_cast<int16_t>(clipX - x), 0, static_cast<int16_t>(img->h)));
347
+ syEnd = static_cast<uint16_t>(ui_clamp_i16(static_cast<int16_t>(clipX + clipW - x), 0, static_cast<int16_t>(img->h)));
278
348
  }
279
349
  if (sxStart >= sxEnd || syStart >= syEnd) return;
280
350
  for (uint16_t sy = syStart; sy < syEnd; sy++) {
281
351
  for (uint16_t sx = sxStart; sx < sxEnd; sx++) {
282
- UI_COLOR_T color = img->data[(uint32_t)sy * img->w + sx];
352
+ UI_COLOR_T color = img->data[static_cast<uint32_t>(sy) * img->w + sx];
283
353
  int16_t dx = 0;
284
354
  int16_t dy = 0;
285
355
  if (q == 1) {
286
- dx = (int16_t)(img->h - 1 - sy);
287
- dy = (int16_t)sx;
356
+ dx = static_cast<int16_t>(img->h - 1 - sy);
357
+ dy = static_cast<int16_t>(sx);
288
358
  } else if (q == 2) {
289
- dx = (int16_t)(img->w - 1 - sx);
290
- dy = (int16_t)(img->h - 1 - sy);
359
+ dx = static_cast<int16_t>(img->w - 1 - sx);
360
+ dy = static_cast<int16_t>(img->h - 1 - sy);
291
361
  } else {
292
- dx = (int16_t)sy;
293
- dy = (int16_t)(img->w - 1 - sx);
362
+ dx = static_cast<int16_t>(sy);
363
+ dy = static_cast<int16_t>(img->w - 1 - sx);
294
364
  }
295
365
  ui_display_draw_pixel(x + dx, y + dy, color);
296
366
  }
@@ -13,15 +13,22 @@ static inline void ui_init(void) {
13
13
  // containers start locked until the scroll-container loop proves their
14
14
  // canvas fits.
15
15
  if (!__ui_scroll_canvas_ok && __ui_node_count > 0) {
16
- __ui_scroll_canvas_ok = (uint8_t*)calloc(__ui_node_count, sizeof(uint8_t));
16
+ __ui_scroll_canvas_ok = new (std::nothrow) uint8_t[__ui_node_count]();
17
17
  }
18
- if (!__ui_scroll_mem_warned && __ui_node_count > 0) {
19
- __ui_scroll_mem_warned = (uint8_t*)calloc(__ui_node_count, sizeof(uint8_t));
18
+ if (!__ui_scroll_render_locked && __ui_node_count > 0) {
19
+ __ui_scroll_render_locked = new (std::nothrow) uint8_t[__ui_node_count]();
20
20
  }
21
21
  for (uint16_t i = 0; i < __ui_node_count; i++) {
22
22
  __ui_nodes[i].dirty = 1;
23
23
  __ui_nodes[i].lastTextHeight = 0;
24
24
  __ui_nodes[i].layoutCacheKey = 0;
25
+ if (__ui_nodes[i].scrollable) {
26
+ // Force the first scroll-canvas composition to seed every pixel. After
27
+ // that, a dirty child can be repaired in place without repainting the
28
+ // entire viewport.
29
+ __ui_nodes[i].lastPaintedScrollY = static_cast<int16_t>(
30
+ __ui_nodes[i].scrollY - (__ui_nodes[i].box.h > 0 ? __ui_nodes[i].box.h : 1));
31
+ }
25
32
  if (__ui_nodes[i].kind == NODE_PROGRESS || __ui_nodes[i].kind == NODE_RANGE) {
26
33
  __ui_nodes[i].lastTextWidth = -1;
27
34
  }
@@ -32,6 +39,11 @@ static inline void ui_init(void) {
32
39
  __ui_nodes[n].hasTextBinding = 1;
33
40
  strncpy(__ui_nodes[n].textBuffer, __ui_nodes[n].text ? __ui_nodes[n].text : "", UI_TEXT_BUF);
34
41
  __ui_nodes[n].textBuffer[UI_TEXT_BUF] = '\\0';
42
+ } else if (__ui_bindings[i].fn) {
43
+ // Prime numeric binding caches once at setup; the first tick can then skip
44
+ // redundant assignments and dirty propagation when values are unchanged.
45
+ __ui_bindings[i].lastValue = __ui_bindings[i].fn();
46
+ __ui_bindings[i].initialized = 1;
35
47
  }
36
48
  }
37
49
  // Seed virtualized-list runtime state. The fn pointers can't be baked into
@@ -52,7 +64,7 @@ static inline void ui_init(void) {
52
64
  uint16_t ih = __ui_nodes[i].listItemHeight > 0 ? __ui_nodes[i].listItemHeight : 24;
53
65
  uint16_t cnt = __ui_nodes[i].listCountFn();
54
66
  __ui_nodes[i].listCount = cnt;
55
- __ui_nodes[i].contentHeight = (int16_t)((uint32_t)cnt * ih);
67
+ __ui_nodes[i].contentHeight = static_cast<int16_t>(static_cast<uint32_t>(cnt) * ih);
56
68
  __ui_nodes[i].scrollY = 0;
57
69
  __ui_nodes[i].overscrollPx = 0;
58
70
  __ui_nodes[i].settling = 0;
@@ -61,6 +73,9 @@ static inline void ui_init(void) {
61
73
  ui_build_draw_order();
62
74
  ui_build_scroll_owner_table();
63
75
  ui_refresh_active_screen_bg_node();
76
+ if (!__ui_scroll_candidates && __ui_node_count > 0) {
77
+ __ui_scroll_candidates = new (std::nothrow) UIScrollPaintCandidate[__ui_node_count];
78
+ }
64
79
  }
65
80
 
66
81
  // Debounce: ignore press/release events within 50ms of the last edge.
@@ -99,7 +114,7 @@ static inline void ui_on_release(uint16_t nodeIdx) {
99
114
  }
100
115
 
101
116
  // Pin-watch callback type: void fn(void)
102
- typedef void (*PinWatchCallback)(void);
117
+ using PinWatchCallback = void (*)(void);
103
118
 
104
119
  struct UIPinWatch {
105
120
  uint8_t pin;