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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +6 -0
  2. package/dist/engine-index.js +2 -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 +42 -69
  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.d.ts +1 -0
  16. package/dist/ui-engine/runtime-header/cuttlefish-gfx.js +563 -0
  17. package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +26 -10
  18. package/dist/ui-engine/runtime-header/display-shim.js +8 -8
  19. package/dist/ui-engine/runtime-header/forward-decls.js +52 -10
  20. package/dist/ui-engine/runtime-header/image-drawing.js +154 -84
  21. package/dist/ui-engine/runtime-header/init-press-input.js +20 -5
  22. package/dist/ui-engine/runtime-header/keyboard.js +62 -18
  23. package/dist/ui-engine/runtime-header/node-decoration.js +8 -9
  24. package/dist/ui-engine/runtime-header/node-draw-body.d.ts +1 -0
  25. package/dist/ui-engine/runtime-header/node-draw-body.js +1402 -0
  26. package/dist/ui-engine/runtime-header/paint-order-coords.js +48 -18
  27. package/dist/ui-engine/runtime-header/paint-rects-repair.js +38 -17
  28. package/dist/ui-engine/runtime-header/scroll-physics.js +15 -18
  29. package/dist/ui-engine/runtime-header/state-bindings-nav.js +23 -13
  30. package/dist/ui-engine/runtime-header/structs.js +12 -10
  31. package/dist/ui-engine/runtime-header/text-rendering.js +132 -71
  32. package/dist/ui-engine/runtime-header/tick/bindings-phase.js +29 -10
  33. package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +336 -749
  34. package/dist/ui-engine/runtime-header/tick/flush-phase.js +7 -3
  35. package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +19 -3
  36. package/dist/ui-engine/runtime-header/tick/transitions-phase.js +31 -24
  37. package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +46 -41
  38. package/dist/ui-engine/runtime-header/types-defines.js +23 -3
  39. package/dist/ui-engine/runtime-header.d.ts +10 -1
  40. package/dist/ui-engine/runtime-header.js +7 -1
  41. package/dist/ui-engine/transpile-ui.js +2 -2
  42. package/dist/ui-engine/ua-stylesheet.js +46 -3
  43. package/dist/ui-engine/ui-lowering.d.ts +1 -3
  44. package/dist/ui-engine/ui-lowering.js +18 -24
  45. package/dist/ui-engine/ui-registry.js +1 -6
  46. package/package.json +2 -2
  47. package/src/engine-index.ts +2 -2
  48. package/src/preview/host-ui-runtime.ts +56 -12
  49. package/src/ui-engine/font-assets.ts +13 -0
  50. package/src/ui-engine/layout-engine.ts +19 -6
  51. package/src/ui-engine/model.ts +10 -5
  52. package/src/ui-engine/runtime-header/antialiasing.ts +52 -32
  53. package/src/ui-engine/runtime-header/blend-bodies.ts +4 -4
  54. package/src/ui-engine/runtime-header/canvas-helpers.ts +42 -69
  55. package/src/ui-engine/runtime-header/canvas-scrollbar.ts +22 -10
  56. package/src/ui-engine/runtime-header/color-mono-refresh.ts +10 -10
  57. package/src/ui-engine/runtime-header/cuttlefish-gfx.ts +566 -0
  58. package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +26 -10
  59. package/src/ui-engine/runtime-header/display-shim.ts +8 -8
  60. package/src/ui-engine/runtime-header/forward-decls.ts +52 -10
  61. package/src/ui-engine/runtime-header/image-drawing.ts +154 -84
  62. package/src/ui-engine/runtime-header/init-press-input.ts +20 -5
  63. package/src/ui-engine/runtime-header/keyboard.ts +62 -18
  64. package/src/ui-engine/runtime-header/node-decoration.ts +8 -9
  65. package/src/ui-engine/runtime-header/node-draw-body.ts +1402 -0
  66. package/src/ui-engine/runtime-header/paint-order-coords.ts +48 -18
  67. package/src/ui-engine/runtime-header/paint-rects-repair.ts +38 -17
  68. package/src/ui-engine/runtime-header/scroll-physics.ts +15 -18
  69. package/src/ui-engine/runtime-header/state-bindings-nav.ts +23 -13
  70. package/src/ui-engine/runtime-header/structs.ts +12 -10
  71. package/src/ui-engine/runtime-header/text-rendering.ts +132 -71
  72. package/src/ui-engine/runtime-header/tick/bindings-phase.ts +29 -10
  73. package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +336 -749
  74. package/src/ui-engine/runtime-header/tick/flush-phase.ts +7 -3
  75. package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +19 -3
  76. package/src/ui-engine/runtime-header/tick/transitions-phase.ts +31 -24
  77. package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +46 -41
  78. package/src/ui-engine/runtime-header/types-defines.ts +23 -3
  79. package/src/ui-engine/runtime-header.ts +17 -1
  80. package/src/ui-engine/transpile-ui.ts +2 -2
  81. package/src/ui-engine/ua-stylesheet.ts +46 -3
  82. package/src/ui-engine/ui-lowering.ts +19 -26
  83. package/src/ui-engine/ui-registry.ts +1 -7
  84. package/dist/ui-engine/scroll-memory-diagnostics.d.ts +0 -16
  85. package/dist/ui-engine/scroll-memory-diagnostics.js +0 -58
  86. package/src/ui-engine/scroll-memory-diagnostics.ts +0 -79
@@ -40,12 +40,12 @@ static inline uint16_t ui_next_utf8_codepoint(const unsigned char** p) {
40
40
  return b0;
41
41
  }
42
42
  if ((b0 & 0xE0) == 0xC0 && (s[0] & 0xC0) == 0x80) {
43
- uint16_t cp = ((uint16_t)(b0 & 0x1F) << 6) | (uint16_t)(s[0] & 0x3F);
43
+ uint16_t cp = (static_cast<uint16_t>(b0 & 0x1F) << 6) | static_cast<uint16_t>(s[0] & 0x3F);
44
44
  *p = s + 1;
45
45
  return cp;
46
46
  }
47
47
  if ((b0 & 0xF0) == 0xE0 && (s[0] & 0xC0) == 0x80 && (s[1] & 0xC0) == 0x80) {
48
- uint16_t cp = ((uint16_t)(b0 & 0x0F) << 12) | ((uint16_t)(s[0] & 0x3F) << 6) | (uint16_t)(s[1] & 0x3F);
48
+ uint16_t cp = (static_cast<uint16_t>(b0 & 0x0F) << 12) | (static_cast<uint16_t>(s[0] & 0x3F) << 6) | static_cast<uint16_t>(s[1] & 0x3F);
49
49
  *p = s + 2;
50
50
  return cp;
51
51
  }
@@ -121,12 +121,12 @@ static inline uint16_t ui_next_utf8_codepoint_bounded(const unsigned char** p, c
121
121
  return b0;
122
122
  }
123
123
  if ((b0 & 0xE0) == 0xC0 && s < end && (s[0] & 0xC0) == 0x80) {
124
- uint16_t cp = ((uint16_t)(b0 & 0x1F) << 6) | (uint16_t)(s[0] & 0x3F);
124
+ uint16_t cp = (static_cast<uint16_t>(b0 & 0x1F) << 6) | static_cast<uint16_t>(s[0] & 0x3F);
125
125
  *p = s + 1;
126
126
  return cp;
127
127
  }
128
128
  if ((b0 & 0xF0) == 0xE0 && s + 1 < end && (s[0] & 0xC0) == 0x80 && (s[1] & 0xC0) == 0x80) {
129
- uint16_t cp = ((uint16_t)(b0 & 0x0F) << 12) | ((uint16_t)(s[0] & 0x3F) << 6) | (uint16_t)(s[1] & 0x3F);
129
+ uint16_t cp = (static_cast<uint16_t>(b0 & 0x0F) << 12) | (static_cast<uint16_t>(s[0] & 0x3F) << 6) | static_cast<uint16_t>(s[1] & 0x3F);
130
130
  *p = s + 2;
131
131
  return cp;
132
132
  }
@@ -144,8 +144,8 @@ static inline uint16_t ui_text_codepoint_advance(uint16_t codepoint, uint8_t ts,
144
144
  const UIFontGlyph* glyph = ui_font_glyph(face, codepoint);
145
145
  return glyph ? glyph->advance : (face->lineHeight / 2);
146
146
  }
147
- int16_t adv = (int16_t)(ts ? ts : 2) * 6 + letterSpacing;
148
- return adv > 0 ? (uint16_t)adv : 1;
147
+ int16_t adv = static_cast<int16_t>(ts ? ts : 2) * 6 + letterSpacing;
148
+ return adv > 0 ? static_cast<uint16_t>(adv) : 1;
149
149
  }
150
150
 
151
151
  static inline uint16_t ui_text_span_width(const char* start, const char* end, uint8_t ts, uint8_t fontFace, int8_t letterSpacing) {
@@ -285,12 +285,12 @@ static inline uint32_t ui_text_layout_cache_key(uint16_t nodeIdx, uint16_t textM
285
285
  uint8_t ts = __ui_nodes[nodeIdx].textSize ? __ui_nodes[nodeIdx].textSize : 2;
286
286
  key = key * 31u + ts;
287
287
  key = key * 31u + __ui_nodes[nodeIdx].fontFace;
288
- key = key * 31u + (uint8_t)(__ui_nodes[nodeIdx].letterSpacing + 128);
288
+ key = key * 31u + static_cast<uint8_t>(__ui_nodes[nodeIdx].letterSpacing + 128);
289
289
  key = key * 31u + __ui_nodes[nodeIdx].lineHeight;
290
290
  if (__ui_nodes[nodeIdx].hasTextBinding) {
291
291
  const char* t = __ui_nodes[nodeIdx].textBuffer;
292
292
  while (t && *t) {
293
- key = key * 31u + (uint8_t)*t;
293
+ key = key * 31u + static_cast<uint8_t>(*t);
294
294
  t++;
295
295
  }
296
296
  }
@@ -300,12 +300,12 @@ static inline uint32_t ui_text_layout_cache_key(uint16_t nodeIdx, uint16_t textM
300
300
  static inline uint16_t ui_node_text_max_width(uint16_t nodeIdx) {
301
301
  if (nodeIdx >= __ui_node_count) return 0;
302
302
  uint16_t textMaxW = __ui_nodes[nodeIdx].box.w;
303
- if (__ui_nodes[nodeIdx].kind == NODE_TEXT || __ui_nodes[nodeIdx].kind == NODE_BUTTON) {
304
- uint16_t hInset = (uint16_t)__ui_nodes[nodeIdx].paddingLeft + (uint16_t)__ui_nodes[nodeIdx].paddingRight +
305
- (uint16_t)__ui_nodes[nodeIdx].borderWidth * 2;
306
- textMaxW = __ui_nodes[nodeIdx].box.w > hInset ? (uint16_t)(__ui_nodes[nodeIdx].box.w - hInset) : 0;
303
+ if (__ui_nodes[nodeIdx].kind == NODE_TEXT || __ui_nodes[nodeIdx].kind == NODE_BUTTON || __ui_nodes[nodeIdx].kind == NODE_SELECT) {
304
+ uint16_t hInset = static_cast<uint16_t>(__ui_nodes[nodeIdx].paddingLeft) + static_cast<uint16_t>(__ui_nodes[nodeIdx].paddingRight) +
305
+ static_cast<uint16_t>(__ui_nodes[nodeIdx].borderWidth) * 2;
306
+ textMaxW = __ui_nodes[nodeIdx].box.w > hInset ? static_cast<uint16_t>(__ui_nodes[nodeIdx].box.w - hInset) : 0;
307
307
  } else if (__ui_nodes[nodeIdx].kind == NODE_CHECK || __ui_nodes[nodeIdx].kind == NODE_RADIO) {
308
- textMaxW = __ui_nodes[nodeIdx].box.w > 22 ? (uint16_t)(__ui_nodes[nodeIdx].box.w - 22) : 0;
308
+ textMaxW = __ui_nodes[nodeIdx].box.w > 22 ? static_cast<uint16_t>(__ui_nodes[nodeIdx].box.w - 22) : 0;
309
309
  }
310
310
  return textMaxW;
311
311
  }
@@ -363,18 +363,18 @@ static inline uint8_t ui_draw_asset_text(const char* text, int16_t x, int16_t y,
363
363
  }
364
364
  int16_t glyphX = cursor + glyph->xOffset;
365
365
  int16_t glyphY = baseline + glyph->yOffset;
366
- if (glyphX + (int16_t)glyph->width <= targetLeft || glyphX >= targetRight ||
367
- glyphY + (int16_t)glyph->height <= targetTop || glyphY >= targetBottom) {
366
+ if (glyphX + static_cast<int16_t>(glyph->width) <= targetLeft || glyphX >= targetRight ||
367
+ glyphY + static_cast<int16_t>(glyph->height) <= targetTop || glyphY >= targetBottom) {
368
368
  cursor += glyph->advance;
369
369
  continue;
370
370
  }
371
- int16_t gxStart = glyphX < targetLeft ? (int16_t)(targetLeft - glyphX) : 0;
372
- int16_t gyStart = glyphY < targetTop ? (int16_t)(targetTop - glyphY) : 0;
373
- int16_t gxEnd = glyphX + (int16_t)glyph->width > targetRight ? (int16_t)(targetRight - glyphX) : glyph->width;
374
- int16_t gyEnd = glyphY + (int16_t)glyph->height > targetBottom ? (int16_t)(targetBottom - glyphY) : glyph->height;
371
+ int16_t gxStart = glyphX < targetLeft ? static_cast<int16_t>(targetLeft - glyphX) : 0;
372
+ int16_t gyStart = glyphY < targetTop ? static_cast<int16_t>(targetTop - glyphY) : 0;
373
+ int16_t gxEnd = glyphX + static_cast<int16_t>(glyph->width) > targetRight ? static_cast<int16_t>(targetRight - glyphX) : glyph->width;
374
+ int16_t gyEnd = glyphY + static_cast<int16_t>(glyph->height) > targetBottom ? static_cast<int16_t>(targetBottom - glyphY) : glyph->height;
375
375
  for (int16_t gy = gyStart; gy < gyEnd; gy++) {
376
376
  for (int16_t gx = gxStart; gx < gxEnd; gx++) {
377
- uint16_t pixelIndex = (uint16_t)gy * glyph->width + (uint16_t)gx;
377
+ uint16_t pixelIndex = static_cast<uint16_t>(gy) * glyph->width + static_cast<uint16_t>(gx);
378
378
  uint8_t alpha = ui_font_alpha_at(face, glyph, pixelIndex);
379
379
  if (alpha == 0) continue;
380
380
  int16_t dx = glyphX + gx;
@@ -386,7 +386,7 @@ static inline uint8_t ui_draw_asset_text(const char* text, int16_t x, int16_t y,
386
386
  // the bumpy look from flattening sub-pixel coverage to solid.
387
387
  if (alpha >= 8) ui_display_draw_pixel(dx, dy, fg);
388
388
  } else {
389
- ui_display_draw_pixel(dx, dy, alpha >= 15 ? fg : ui_blend(fg, bg, (uint8_t)((uint16_t)alpha * 100 / 15)));
389
+ ui_display_draw_pixel(dx, dy, alpha >= 15 ? fg : ui_blend(fg, bg, static_cast<uint8_t>(static_cast<uint16_t>(alpha) * 100 / 15)));
390
390
  }
391
391
  } else if (alpha >= 8) {
392
392
  ui_display_draw_pixel(dx, dy, fg);
@@ -437,10 +437,10 @@ static inline void ui_draw_bitmap_text(const char* text, int16_t x, int16_t y, U
437
437
 
438
438
  static inline uint8_t ui_text_fg_neighbors(CuttlefishCanvas16* src, int16_t x, int16_t y, int16_t w, int16_t h, UI_COLOR_T fg, uint8_t radius = 1) {
439
439
  uint8_t count = 0;
440
- for (int8_t dy = -(int8_t)radius; dy <= (int8_t)radius; dy++) {
440
+ for (int8_t dy = -static_cast<int8_t>(radius); dy <= static_cast<int8_t>(radius); dy++) {
441
441
  int16_t yy = y + dy;
442
442
  if (yy < 0 || yy >= h) continue;
443
- for (int8_t dx = -(int8_t)radius; dx <= (int8_t)radius; dx++) {
443
+ for (int8_t dx = -static_cast<int8_t>(radius); dx <= static_cast<int8_t>(radius); dx++) {
444
444
  int16_t xx = x + dx;
445
445
  if (xx < 0 || xx >= w) continue;
446
446
  if (display_canvasGetPixel(src, xx, yy) == fg) count++;
@@ -482,15 +482,15 @@ static inline void ui_draw_aa_text(const char* text, int16_t x, int16_t y, UI_CO
482
482
  }
483
483
  int16_t clipX = x;
484
484
  int16_t clipY = y;
485
- int16_t clipW = (int16_t)w;
486
- int16_t clipH = (int16_t)h;
485
+ int16_t clipW = static_cast<int16_t>(w);
486
+ int16_t clipH = static_cast<int16_t>(h);
487
487
  if (!ui_clip_rect_to_display_target(&clipX, &clipY, &clipW, &clipH)) return;
488
- int16_t localX = (int16_t)(clipX - x);
489
- int16_t localY = (int16_t)(clipY - y);
488
+ int16_t localX = static_cast<int16_t>(clipX - x);
489
+ int16_t localY = static_cast<int16_t>(clipY - y);
490
490
 
491
491
  // Use pre-allocated static canvases (no dynamic allocation)
492
- CuttlefishCanvas16* src = ui_text_canvas(&__ui_text_src_canvas, (int16_t)w, (int16_t)h);
493
- CuttlefishCanvas16* dst = ui_text_canvas(&__ui_text_dst_canvas, (int16_t)w, (int16_t)h);
492
+ CuttlefishCanvas16* src = ui_text_canvas(&__ui_text_src_canvas, static_cast<int16_t>(w), static_cast<int16_t>(h));
493
+ CuttlefishCanvas16* dst = ui_text_canvas(&__ui_text_dst_canvas, static_cast<int16_t>(w), static_cast<int16_t>(h));
494
494
  if (!src || !dst || !display_canvasBuffer(src) || !display_canvasBuffer(dst)) {
495
495
  ui_draw_bitmap_text(text, x, y, fg, bg, ts, 0);
496
496
  return;
@@ -507,10 +507,10 @@ static inline void ui_draw_aa_text(const char* text, int16_t x, int16_t y, UI_CO
507
507
  for (int16_t yy = localY; yy < localY + clipH; yy++) {
508
508
  for (int16_t xx = localX; xx < localX + clipW; xx++) {
509
509
  UI_COLOR_T px = display_canvasGetPixel(src, xx, yy);
510
- uint8_t neighbors = ui_text_fg_neighbors(src, xx, yy, (int16_t)w, h, fg);
510
+ uint8_t neighbors = ui_text_fg_neighbors(src, xx, yy, static_cast<int16_t>(w), h, fg);
511
511
  uint8_t outerNeighbors = 0;
512
512
  if (ts >= 3 && px != fg && neighbors == 0) {
513
- outerNeighbors = ui_text_fg_neighbors(src, xx, yy, (int16_t)w, h, fg, 2);
513
+ outerNeighbors = ui_text_fg_neighbors(src, xx, yy, static_cast<int16_t>(w), h, fg, 2);
514
514
  }
515
515
  uint8_t coverage = ui_text_aa_coverage(neighbors, outerNeighbors, px == fg ? 1 : 0, ts);
516
516
  display_targetDrawPixel((CuttlefishDisplayTarget*)dst, xx, yy, coverage == 0 ? bg : ui_blend(fg, bg, coverage));
@@ -520,8 +520,8 @@ static inline void ui_draw_aa_text(const char* text, int16_t x, int16_t y, UI_CO
520
520
  int16_t stride = display_canvasWidth(dst);
521
521
  UI_COLOR_T* pixels = display_canvasBuffer(dst);
522
522
  for (int16_t row = 0; row < clipH; row++) {
523
- ui_display_draw_rgb_bitmap(clipX, (int16_t)(clipY + row),
524
- pixels + (int32_t)(localY + row) * stride + localX, clipW, 1);
523
+ ui_display_draw_rgb_bitmap(clipX, static_cast<int16_t>(clipY + row),
524
+ pixels + static_cast<int32_t>(localY + row) * stride + localX, clipW, 1);
525
525
  }
526
526
  }
527
527
 
@@ -548,23 +548,81 @@ static inline void ui_draw_text(const char* text, int16_t x, int16_t y, UI_COLOR
548
548
  }
549
549
  #endif
550
550
 
551
+ // Draw classic list rows without routing through CuttlefishGFX::print(). The
552
+ // Zephyr native target uses CuttlefishGFX for both the panel and RGB565 canvas;
553
+ // its virtual text path is reliable on the panel but can be lost when the
554
+ // canvas is later copied to the panel. Writing the same 5x7 glyphs through the
555
+ // normal pixel/fill shims keeps the row pixels in the canvas buffer and works
556
+ // for both cached-list and band-list rendering. Other adapters retain the
557
+ // existing ui_draw_text fallback because they provide their own canvas text
558
+ // implementation.
559
+ static inline void ui_draw_list_text(const char* text, int16_t x, int16_t y,
560
+ UI_COLOR_T fg, UI_COLOR_T bg, uint8_t ts,
561
+ uint8_t fontFace, int8_t letterSpacing) {
562
+ #if defined(CUTTLEFISH_GFX_DEFINED)
563
+ if (fontFace == 0) {
564
+ if (!text) text = "";
565
+ if (ts == 0) ts = 2;
566
+ int16_t cx = x;
567
+ for (const unsigned char* p = (const unsigned char*)text; *p; p++) {
568
+ uint8_t ch = *p;
569
+ // CuttlefishGFX's classic font is the full 256-glyph Adafruit table.
570
+ for (uint8_t col = 0; col < 5; col++) {
571
+ uint8_t bits = cuttlefish_glcdfont[static_cast<uint16_t>(ch) * 5u + col];
572
+ for (uint8_t row = 0; row < 7; row++) {
573
+ if (bits & static_cast<uint8_t>(1u << row)) {
574
+ ui_display_fill_rect(static_cast<int16_t>(cx + static_cast<int16_t>(col) * ts),
575
+ static_cast<int16_t>(y + static_cast<int16_t>(row) * ts), ts, ts, fg);
576
+ }
577
+ }
578
+ }
579
+ // Match the classic GFX advance: five columns plus one blank column.
580
+ cx = static_cast<int16_t>(cx + static_cast<int16_t>(ts) * 6 + letterSpacing);
581
+ }
582
+ return;
583
+ }
584
+ #endif
585
+ ui_draw_text(text, x, y, fg, bg, ts, 0, fontFace, letterSpacing);
586
+ }
587
+
588
+ // Direct-panel list fallback. CuttlefishGFX's bitmap glyph path above is
589
+ // intentionally used for RAM canvases, but a few native panel adapters only
590
+ // commit their text state correctly through print()/drawChar(). Use that
591
+ // established path when the list is painted straight to the panel; the caller
592
+ // keeps the whole list inside one write transaction, so this does not expose a
593
+ // partially composed row set.
594
+ static inline void ui_draw_list_text_direct(const char* text, int16_t x, int16_t y,
595
+ UI_COLOR_T fg, UI_COLOR_T bg, uint8_t ts,
596
+ uint8_t fontFace, int8_t letterSpacing) {
597
+ if (fontFace == 0) {
598
+ ui_draw_bitmap_text(text, x, y, fg, bg, ts, letterSpacing);
599
+ } else {
600
+ ui_draw_text(text, x, y, fg, bg, ts, 0, fontFace, letterSpacing);
601
+ }
602
+ }
603
+
551
604
  /** Truncate a NUL-terminated buffer in place to fit within maxWidth (px) and
552
605
  * append "...". Used by text-overflow: ellipsis. Adafruit_GFX has no ellipsis
553
606
  * glyph, so three ASCII dots approximate it. */
554
607
  static inline void ui_truncate_ellipsis(char* buf, uint8_t bufSize, uint16_t maxWidth,
555
608
  uint8_t ts, uint8_t fontFace, int8_t letterSpacing) {
556
609
  if (!buf || bufSize == 0) return;
557
- uint8_t len = (uint8_t)strlen(buf);
610
+ uint8_t len = static_cast<uint8_t>(strlen(buf));
558
611
  // Reserve space for the three trailing dots.
559
- uint16_t dotsW = (uint16_t)3 * ui_text_codepoint_advance((uint16_t)'.', ts, fontFace, letterSpacing);
560
- int16_t budget = (int16_t)maxWidth - (int16_t)dotsW;
612
+ uint16_t dotsW = static_cast<uint16_t>(3) * ui_text_codepoint_advance(static_cast<uint16_t>('.'), ts, fontFace, letterSpacing);
613
+ int16_t budget = static_cast<int16_t>(maxWidth) - static_cast<int16_t>(dotsW);
561
614
  if (budget <= 0) { if (bufSize > 3) { buf[0]='.'; buf[1]='.'; buf[2]='.'; buf[3]=0; } return; }
562
- // Trim trailing chars until the prefix fits the budget.
615
+ // Measure once, then remove one UTF-8 codepoint at a time from the end.
616
+ // The previous prefix-- loop rescanned the whole prefix on every iteration
617
+ // (O(n²)); this keeps truncation linear for long bound strings.
563
618
  uint8_t prefix = len;
564
- while (prefix > 0) {
565
- uint16_t w = ui_text_span_width(buf, buf + prefix, ts, fontFace, letterSpacing);
566
- if ((int16_t)w <= budget) break;
567
- prefix--;
619
+ uint16_t prefixW = ui_text_span_width(buf, buf + prefix, ts, fontFace, letterSpacing);
620
+ while (prefix > 0 && static_cast<int16_t>(prefixW) > budget) {
621
+ uint8_t cut = static_cast<uint8_t>(prefix - 1);
622
+ while (cut > 0 && (static_cast<uint8_t>(buf[cut]) & 0xC0) == 0x80) cut--;
623
+ uint16_t removed = ui_text_span_width(buf + cut, buf + prefix, ts, fontFace, letterSpacing);
624
+ prefixW = prefixW > removed ? static_cast<uint16_t>(prefixW - removed) : 0;
625
+ prefix = cut;
568
626
  }
569
627
  if (prefix + 3 < bufSize) {
570
628
  buf[prefix] = '.'; buf[prefix+1] = '.'; buf[prefix+2] = '.'; buf[prefix+3] = 0;
@@ -578,12 +636,15 @@ static inline void ui_truncate_ellipsis(char* buf, uint8_t bufSize, uint16_t max
578
636
  static inline void ui_truncate_clip(char* buf, uint8_t bufSize, uint16_t maxWidth,
579
637
  uint8_t ts, uint8_t fontFace, int8_t letterSpacing) {
580
638
  if (!buf || bufSize == 0) return;
581
- uint8_t len = (uint8_t)strlen(buf);
639
+ uint8_t len = static_cast<uint8_t>(strlen(buf));
582
640
  uint8_t prefix = len;
583
- while (prefix > 0) {
584
- uint16_t w = ui_text_span_width(buf, buf + prefix, ts, fontFace, letterSpacing);
585
- if ((int16_t)w <= (int16_t)maxWidth) break;
586
- prefix--;
641
+ uint16_t prefixW = ui_text_span_width(buf, buf + prefix, ts, fontFace, letterSpacing);
642
+ while (prefix > 0 && static_cast<int16_t>(prefixW) > static_cast<int16_t>(maxWidth)) {
643
+ uint8_t cut = static_cast<uint8_t>(prefix - 1);
644
+ while (cut > 0 && (static_cast<uint8_t>(buf[cut]) & 0xC0) == 0x80) cut--;
645
+ uint16_t removed = ui_text_span_width(buf + cut, buf + prefix, ts, fontFace, letterSpacing);
646
+ prefixW = prefixW > removed ? static_cast<uint16_t>(prefixW - removed) : 0;
647
+ prefix = cut;
587
648
  }
588
649
  if (prefix < bufSize) buf[prefix] = 0;
589
650
  }
@@ -600,15 +661,15 @@ static inline void ui_draw_wrapped_text(const char* text, int16_t x, int16_t y,
600
661
  int16_t targetLeft, targetTop, targetRight, targetBottom;
601
662
  ui_display_target_bounds(&targetLeft, &targetTop, &targetRight, &targetBottom);
602
663
  while (ui_text_next_line(&cursor, maxWidth, whiteSpaceMode, ts, fontFace, letterSpacing, &line)) {
603
- int16_t lineBottom = (int16_t)(lineY + lh);
664
+ int16_t lineBottom = static_cast<int16_t>(lineY + lh);
604
665
  if (lineBottom <= targetTop || lineY >= targetBottom) {
605
666
  lineY += lh;
606
667
  continue;
607
668
  }
608
669
  int16_t lineX = x;
609
- if (textAlign == 1) lineX = x + ((int16_t)maxWidth - (int16_t)line.width) / 2;
610
- else if (textAlign == 2) lineX = x + (int16_t)maxWidth - (int16_t)line.width;
611
- if (lineX + (int16_t)line.width <= targetLeft || lineX >= targetRight) {
670
+ if (textAlign == 1) lineX = x + (static_cast<int16_t>(maxWidth) - static_cast<int16_t>(line.width)) / 2;
671
+ else if (textAlign == 2) lineX = x + static_cast<int16_t>(maxWidth) - static_cast<int16_t>(line.width);
672
+ if (lineX + static_cast<int16_t>(line.width) <= targetLeft || lineX >= targetRight) {
612
673
  lineY += lh;
613
674
  continue;
614
675
  }
@@ -616,7 +677,7 @@ static inline void ui_draw_wrapped_text(const char* text, int16_t x, int16_t y,
616
677
  // text-overflow — only applies when the line is wider than maxWidth.
617
678
  // textOverflow==1 (ellipsis): trim the span and append "...".
618
679
  // textOverflow==0 (clip): trim the span to the edge, no dots.
619
- if ((int16_t)line.width > (int16_t)maxWidth) {
680
+ if (static_cast<int16_t>(line.width) > static_cast<int16_t>(maxWidth)) {
620
681
  if (textOverflow) ui_truncate_ellipsis(lineBuf, UI_TEXT_LINE_BUF, maxWidth, ts, fontFace, letterSpacing);
621
682
  else ui_truncate_clip(lineBuf, UI_TEXT_LINE_BUF, maxWidth, ts, fontFace, letterSpacing);
622
683
  }
@@ -638,23 +699,23 @@ static inline void ui_draw_rich_text(uint16_t nodeIdx, int16_t x, int16_t y, UI_
638
699
  uint8_t useFgOverride = 0, UI_COLOR_T fgOverride = 0, uint16_t maxWidth = 0) {
639
700
  UINode* n = &__ui_nodes[nodeIdx];
640
701
  uint16_t alignWidth = maxWidth ? maxWidth : n->box.w;
641
- int16_t targetLeft = (int16_t)(-__ui_draw_off_x);
642
- int16_t targetTop = (int16_t)(-__ui_draw_off_y);
643
- int16_t targetRight = (int16_t)(ui_display_target_width() - __ui_draw_off_x);
644
- int16_t targetBottom = (int16_t)(ui_display_target_height() - __ui_draw_off_y);
645
- uint16_t richSegEnd = (uint16_t)(n->richSegStart + n->richSegCount);
702
+ int16_t targetLeft = static_cast<int16_t>(-__ui_draw_off_x);
703
+ int16_t targetTop = static_cast<int16_t>(-__ui_draw_off_y);
704
+ int16_t targetRight = static_cast<int16_t>(ui_display_target_width() - __ui_draw_off_x);
705
+ int16_t targetBottom = static_cast<int16_t>(ui_display_target_height() - __ui_draw_off_y);
706
+ uint16_t richSegEnd = static_cast<uint16_t>(n->richSegStart + n->richSegCount);
646
707
  for (uint16_t si = n->richSegStart; si < richSegEnd; si++) {
647
708
  UIRichSeg* seg = &__ui_rich_segs[si];
648
709
  if (seg->line >= n->richLineCount) continue;
649
710
  UIRichLine* line = &__ui_rich_lines[n->richLineStart + seg->line];
650
711
  int16_t lineTop = y + line->y;
651
- int16_t lineBottom = (int16_t)(lineTop + (int16_t)line->h);
712
+ int16_t lineBottom = static_cast<int16_t>(lineTop + static_cast<int16_t>(line->h));
652
713
  if (lineBottom <= targetTop || lineTop >= targetBottom) continue;
653
714
  int16_t lineX = x;
654
- if (n->textAlign == 1) lineX = x + ((int16_t)alignWidth - (int16_t)line->w) / 2;
655
- else if (n->textAlign == 2) lineX = x + (int16_t)alignWidth - (int16_t)line->w;
656
- int16_t segX = (int16_t)(lineX + seg->x);
657
- int16_t segRight = (int16_t)(segX + (int16_t)seg->w);
715
+ if (n->textAlign == 1) lineX = x + (static_cast<int16_t>(alignWidth) - static_cast<int16_t>(line->w)) / 2;
716
+ else if (n->textAlign == 2) lineX = x + static_cast<int16_t>(alignWidth) - static_cast<int16_t>(line->w);
717
+ int16_t segX = static_cast<int16_t>(lineX + seg->x);
718
+ int16_t segRight = static_cast<int16_t>(segX + static_cast<int16_t>(seg->w));
658
719
  if (segRight <= targetLeft || segX >= targetRight) continue;
659
720
  UIRichRun* run = &__ui_runs[n->runStart + seg->runIndex];
660
721
  // Baseline alignment: for asset fonts, the ascent is the font face's
@@ -664,9 +725,9 @@ static inline void ui_draw_rich_text(uint16_t nodeIdx, int16_t x, int16_t y, UI_
664
725
  int16_t ascent;
665
726
  if (run->fontFace) {
666
727
  const UIFontFace* face = ui_font_face(run->fontFace);
667
- ascent = face ? (int16_t)face->baseline : (7 * (int16_t)run->textSize);
728
+ ascent = face ? static_cast<int16_t>(face->baseline) : (7 * static_cast<int16_t>(run->textSize));
668
729
  } else {
669
- ascent = 7 * (int16_t)run->textSize;
730
+ ascent = 7 * static_cast<int16_t>(run->textSize);
670
731
  }
671
732
  int16_t segY = y + line->baseline - ascent;
672
733
  UI_COLOR_T fg = useFgOverride ? fgOverride : run->fg;
@@ -682,13 +743,13 @@ static inline void ui_draw_rich_text(uint16_t nodeIdx, int16_t x, int16_t y, UI_
682
743
  // but uses measured segment rects instead of fixed row heights.
683
744
  static inline int8_t ui_rich_link_hit(uint16_t nodeIdx, int16_t px, int16_t py) {
684
745
  UINode* n = &__ui_nodes[nodeIdx];
685
- int16_t insetL = (int16_t)n->borderWidth + (int16_t)n->paddingLeft;
686
- int16_t insetR = (int16_t)n->borderWidth + (int16_t)n->paddingRight;
687
- int16_t insetT = (int16_t)n->borderWidth + (int16_t)n->paddingTop;
746
+ int16_t insetL = static_cast<int16_t>(n->borderWidth) + static_cast<int16_t>(n->paddingLeft);
747
+ int16_t insetR = static_cast<int16_t>(n->borderWidth) + static_cast<int16_t>(n->paddingRight);
748
+ int16_t insetT = static_cast<int16_t>(n->borderWidth) + static_cast<int16_t>(n->paddingTop);
688
749
  int16_t localX = px - insetL;
689
750
  int16_t localY = py - insetT;
690
- uint16_t alignWidth = n->box.w > (uint16_t)(insetL + insetR)
691
- ? (uint16_t)((int16_t)n->box.w - insetL - insetR)
751
+ uint16_t alignWidth = n->box.w > static_cast<uint16_t>(insetL + insetR)
752
+ ? static_cast<uint16_t>(static_cast<int16_t>(n->box.w) - insetL - insetR)
692
753
  : 0;
693
754
  for (uint16_t si = n->richSegStart; si < n->richSegStart + n->richSegCount; si++) {
694
755
  UIRichSeg* seg = &__ui_rich_segs[si];
@@ -698,11 +759,11 @@ static inline int8_t ui_rich_link_hit(uint16_t nodeIdx, int16_t px, int16_t py)
698
759
  // Segment x is relative to its line's left edge (pre-alignment). For the
699
760
  // hit-test, account for center/right alignment the same way draw does.
700
761
  int16_t originX = 0;
701
- if (n->textAlign == 1) originX = ((int16_t)alignWidth - (int16_t)line->w) / 2;
702
- else if (n->textAlign == 2) originX = (int16_t)alignWidth - (int16_t)line->w;
762
+ if (n->textAlign == 1) originX = (static_cast<int16_t>(alignWidth) - static_cast<int16_t>(line->w)) / 2;
763
+ else if (n->textAlign == 2) originX = static_cast<int16_t>(alignWidth) - static_cast<int16_t>(line->w);
703
764
  int16_t sx = originX + seg->x;
704
765
  int16_t sy = line->y;
705
- if (localX >= sx && localX < sx + (int16_t)seg->w && localY >= sy && localY < sy + (int16_t)line->h) {
766
+ if (localX >= sx && localX < sx + static_cast<int16_t>(seg->w) && localY >= sy && localY < sy + static_cast<int16_t>(line->h)) {
706
767
  return run->linkTarget;
707
768
  }
708
769
  }
@@ -6,6 +6,9 @@ export function emitTickBindingsPhase() {
6
6
  static inline void ui_tick(uint16_t deltaMs) {
7
7
  // Touch poll — runs if touch is configured (defined by the emit layer)
8
8
  ui_poll_touch();
9
+ // Allocate the retained framebuffer before transitions run. Geometry repair
10
+ // must clear old animation footprints into RAM, not directly onto the panel.
11
+ (void)ui_get_framebuffer();
9
12
  // ⓪' Poll GPIO inputs
10
13
  ui_poll_inputs();
11
14
  #if defined(UI_HIDE_OSK)
@@ -15,21 +18,27 @@ static inline void ui_tick(uint16_t deltaMs) {
15
18
  // keeps cycling even when no other state changes (otherwise the caret
16
19
  // freezes once the typed-text dirty clears).
17
20
  if (__ui_kb_visible && __ui_kb_target >= 0 && (__ui_kb_blink & 0x10)) {
18
- ui_mark_dirty((uint16_t)__ui_kb_target);
21
+ ui_mark_dirty(static_cast<uint16_t>(__ui_kb_target));
19
22
  }
20
23
  #endif
21
24
  // ⓪ Evaluate bindings: call each binding's fn, compare to the node's
22
25
  // current property value, mark dirty if changed.
23
26
  for (uint16_t i = 0; i < __ui_binding_count; i++) {
24
27
  if (__ui_bindings[i].prop == PROP_TEXT && __ui_bindings[i].textFn) {
25
- // Text binding: fill the node's buffer, compare content, mark dirty if changed.
28
+ // Text callbacks are necessarily evaluated to detect changes (the callback
29
+ // may format a signal or read external state), but avoid a stack-sized old
30
+ // copy: compare the generated value against the node buffer after writing.
31
+ // The callback contract is fill-style and must be deterministic for the same
32
+ // inputs; the temporary buffer keeps a change from destroying the old text.
26
33
  uint16_t n = __ui_bindings[i].node;
27
- char oldBuf[UI_TEXT_BUF + 1];
28
- strncpy(oldBuf, __ui_nodes[n].textBuffer, UI_TEXT_BUF);
29
- oldBuf[UI_TEXT_BUF] = '\\0';
30
- __ui_bindings[i].textFn(__ui_nodes[n].textBuffer, UI_TEXT_BUF + 1);
31
- __ui_nodes[n].textBuffer[UI_TEXT_BUF] = '\\0';
32
- if (strcmp(oldBuf, __ui_nodes[n].textBuffer) != 0) {
34
+ if (n >= __ui_node_count) continue;
35
+ char nextBuf[UI_TEXT_BUF + 1];
36
+ nextBuf[0] = '\\0';
37
+ __ui_bindings[i].textFn(nextBuf, UI_TEXT_BUF + 1);
38
+ nextBuf[UI_TEXT_BUF] = '\\0';
39
+ if (strcmp(nextBuf, __ui_nodes[n].textBuffer) != 0) {
40
+ strncpy(__ui_nodes[n].textBuffer, nextBuf, UI_TEXT_BUF);
41
+ __ui_nodes[n].textBuffer[UI_TEXT_BUF] = '\\0';
33
42
  ui_invalidate_text_layout_cache(n);
34
43
  ui_mark_dirty(n);
35
44
  }
@@ -46,13 +55,18 @@ static inline void ui_tick(uint16_t deltaMs) {
46
55
  if (__ui_bindings[i].prop == PROP_VALUE) {
47
56
  // Numeric value binding: drive a progress/range node's value live.
48
57
  uint16_t n = __ui_bindings[i].node;
49
- int16_t v = (int16_t)__ui_bindings[i].fn();
58
+ int16_t v = static_cast<int16_t>(__ui_bindings[i].fn());
50
59
  if (v != __ui_nodes[n].value) {
51
60
  __ui_nodes[n].value = v;
52
61
  ui_mark_dirty(n);
53
62
  }
54
63
  continue;
55
64
  }
65
+ // Cache numeric binding results. The first sample establishes the baseline;
66
+ // subsequent identical values return immediately without touching node state.
67
+ if (__ui_bindings[i].initialized && newVal == __ui_bindings[i].lastValue) continue;
68
+ __ui_bindings[i].lastValue = newVal;
69
+ __ui_bindings[i].initialized = 1;
56
70
  uint32_t* target = (__ui_bindings[i].prop == PROP_BG) ? &__ui_nodes[__ui_bindings[i].node].bg
57
71
  : (__ui_bindings[i].prop == PROP_FG) ? &__ui_nodes[__ui_bindings[i].node].fg
58
72
  : (__ui_bindings[i].prop == PROP_BORDER_COLOR) ? &__ui_nodes[__ui_bindings[i].node].borderColor
@@ -75,10 +89,14 @@ static inline void ui_tick(uint16_t deltaMs) {
75
89
  for (uint16_t i = 0; i < __ui_node_count; i++) {
76
90
  if (__ui_nodes[i].virtualized && __ui_nodes[i].listCountFn) {
77
91
  uint16_t ih = __ui_nodes[i].listItemHeight > 0 ? __ui_nodes[i].listItemHeight : 24;
92
+ // Keep the callback result as the list's invalidation token. The callback
93
+ // is still evaluated because it is the public list-count source, but an
94
+ // unchanged token avoids layout/dirty work and preserves the retained
95
+ // viewport canvas.
78
96
  uint16_t newCount = __ui_nodes[i].listCountFn();
79
97
  if (newCount != __ui_nodes[i].listCount) {
80
98
  __ui_nodes[i].listCount = newCount;
81
- __ui_nodes[i].contentHeight = (int16_t)((uint32_t)newCount * ih);
99
+ __ui_nodes[i].contentHeight = static_cast<int16_t>(static_cast<uint32_t>(newCount) * ih);
82
100
  __ui_nodes[i].lastPaintedScrollY = __ui_nodes[i].scrollY - (__ui_nodes[i].box.h > 0 ? __ui_nodes[i].box.h : 1);
83
101
  ui_mark_dirty(i);
84
102
  }
@@ -94,6 +112,7 @@ static inline void ui_tick(uint16_t deltaMs) {
94
112
  for (uint16_t i = 0; i < __ui_input_binding_count; i++) {
95
113
  if (!__ui_input_bindings[i].cb) continue;
96
114
  uint16_t n = __ui_input_bindings[i].node;
115
+ if (n >= __ui_node_count) continue;
97
116
  const char* cur = __ui_nodes[n].textBuffer;
98
117
  if (strcmp(cur, __ui_input_bindings[i].lastSeen) != 0) {
99
118
  strncpy(__ui_input_bindings[i].lastSeen, cur, UI_TEXT_BUF);