@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
@@ -2,893 +2,893 @@
2
2
  // Fonts, UTF-8, metrics, wrap, layout cache, all text draw variants, rich link hit.
3
3
  // See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
4
4
  export function emitTextRendering() {
5
- return `
6
- static inline const UIFontFace* ui_font_face(uint8_t id) {
7
- if (id == 0) return nullptr;
8
- for (uint16_t i = 0; i < __ui_font_face_count; i++) {
9
- if (__ui_font_faces[i].id == id) return &__ui_font_faces[i];
10
- }
11
- return nullptr;
12
- }
13
-
14
- static inline const UIFontGlyph* ui_font_glyph(const UIFontFace* face, uint16_t codepoint) {
15
- if (!face) return nullptr;
16
- for (uint8_t i = 0; i < face->glyphCount; i++) {
17
- if (face->glyphs[i].codepoint == codepoint) return &face->glyphs[i];
18
- }
19
- return nullptr;
20
- }
21
-
22
- static inline uint8_t ui_font_alpha_at(const UIFontFace* face, const UIFontGlyph* glyph, uint16_t pixelIndex) {
23
- if (!face || !glyph || !face->alpha) return 0;
24
- uint16_t nibble = glyph->dataOffset + pixelIndex;
25
- // Bounds check to prevent reading past the alpha array
26
- if (nibble >> 1 >= 65535) return 0;
27
- #if defined(__AVR__)
28
- uint8_t byte = pgm_read_byte(&face->alpha[nibble >> 1]);
29
- #else
30
- uint8_t byte = face->alpha[nibble >> 1];
31
- #endif
32
- return (nibble & 1) ? (byte & 0x0F) : (byte >> 4);
33
- }
34
-
35
- static inline uint16_t ui_next_utf8_codepoint(const unsigned char** p) {
36
- const unsigned char* s = *p;
37
- uint8_t b0 = *s++;
38
- if (b0 < 0x80) {
39
- *p = s;
40
- return b0;
41
- }
42
- if ((b0 & 0xE0) == 0xC0 && (s[0] & 0xC0) == 0x80) {
43
- uint16_t cp = (static_cast<uint16_t>(b0 & 0x1F) << 6) | static_cast<uint16_t>(s[0] & 0x3F);
44
- *p = s + 1;
45
- return cp;
46
- }
47
- if ((b0 & 0xF0) == 0xE0 && (s[0] & 0xC0) == 0x80 && (s[1] & 0xC0) == 0x80) {
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
- *p = s + 2;
50
- return cp;
51
- }
52
- if ((b0 & 0xF8) == 0xF0 && (s[0] & 0xC0) == 0x80 && (s[1] & 0xC0) == 0x80 && (s[2] & 0xC0) == 0x80) {
53
- *p = s + 3;
54
- return '?';
55
- }
56
- *p = s;
57
- return '?';
58
- }
59
-
60
- static inline uint16_t ui_asset_text_width(const char* text, const UIFontFace* face) {
61
- if (!text || !face) return 0;
62
- uint16_t w = 0;
63
- const unsigned char* p = (const unsigned char*)text;
64
- while (*p) {
65
- uint16_t codepoint = ui_next_utf8_codepoint(&p);
66
- const UIFontGlyph* glyph = ui_font_glyph(face, codepoint);
67
- w += glyph ? glyph->advance : (face->lineHeight / 2);
68
- }
69
- return w;
70
- }
71
-
72
- static inline uint8_t ui_asset_text_height(const UIFontFace* face) {
73
- return face ? face->lineHeight : 0;
74
- }
75
-
76
- static inline uint16_t ui_text_width(const char* text, uint8_t ts, uint8_t fontFace, int8_t letterSpacing) {
77
- const UIFontFace* face = ui_font_face(fontFace);
78
- if (face) return ui_asset_text_width(text, face);
79
- if (!text) return 0;
80
- if (ts == 0) ts = 2;
81
- uint16_t w = 0;
82
- for (const char* p = text; *p; p++) w += ts * 6 + letterSpacing;
83
- return w;
84
- }
85
-
86
- static inline uint8_t ui_text_height(uint8_t ts, uint8_t fontFace) {
87
- const UIFontFace* face = ui_font_face(fontFace);
88
- if (face) return ui_asset_text_height(face);
89
- return (ts ? ts : 2) * 8;
90
- }
91
-
92
- static inline uint8_t ui_text_line_height(uint8_t ts, uint8_t fontFace, uint8_t lineHeight) {
93
- return lineHeight ? lineHeight : ui_text_height(ts, fontFace);
94
- }
95
-
96
- static inline uint8_t ui_is_text_space(char c) {
97
- return c == ' ' || c == '\\t' || c == '\\f' || c == '\\v';
98
- }
99
-
100
- static inline uint8_t ui_is_text_newline(char c) {
101
- return c == '\\n' || c == '\\r';
102
- }
103
-
104
- static inline const char* ui_after_text_newline(const char* p) {
105
- if (!p || !*p) return p;
106
- if (*p == '\\r' && p[1] == '\\n') return p + 2;
107
- return p + 1;
108
- }
109
-
110
- static inline const char* ui_skip_wrap_spaces(const char* p) {
111
- while (p && ui_is_text_space(*p)) p++;
112
- return p;
113
- }
114
-
115
- static inline uint16_t ui_next_utf8_codepoint_bounded(const unsigned char** p, const unsigned char* end) {
116
- const unsigned char* s = *p;
117
- if (!s || s >= end) return 0;
118
- uint8_t b0 = *s++;
119
- if (b0 < 0x80) {
120
- *p = s;
121
- return b0;
122
- }
123
- if ((b0 & 0xE0) == 0xC0 && s < end && (s[0] & 0xC0) == 0x80) {
124
- uint16_t cp = (static_cast<uint16_t>(b0 & 0x1F) << 6) | static_cast<uint16_t>(s[0] & 0x3F);
125
- *p = s + 1;
126
- return cp;
127
- }
128
- if ((b0 & 0xF0) == 0xE0 && s + 1 < end && (s[0] & 0xC0) == 0x80 && (s[1] & 0xC0) == 0x80) {
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
- *p = s + 2;
131
- return cp;
132
- }
133
- if ((b0 & 0xF8) == 0xF0 && s + 2 < end && (s[0] & 0xC0) == 0x80 && (s[1] & 0xC0) == 0x80 && (s[2] & 0xC0) == 0x80) {
134
- *p = s + 3;
135
- return '?';
136
- }
137
- *p = s;
138
- return '?';
139
- }
140
-
141
- static inline uint16_t ui_text_codepoint_advance(uint16_t codepoint, uint8_t ts, uint8_t fontFace, int8_t letterSpacing) {
142
- const UIFontFace* face = ui_font_face(fontFace);
143
- if (face) {
144
- const UIFontGlyph* glyph = ui_font_glyph(face, codepoint);
145
- return glyph ? glyph->advance : (face->lineHeight / 2);
146
- }
147
- int16_t adv = static_cast<int16_t>(ts ? ts : 2) * 6 + letterSpacing;
148
- return adv > 0 ? static_cast<uint16_t>(adv) : 1;
149
- }
150
-
151
- static inline uint16_t ui_text_span_width(const char* start, const char* end, uint8_t ts, uint8_t fontFace, int8_t letterSpacing) {
152
- if (!start || !end || end <= start) return 0;
153
- uint16_t w = 0;
154
- const unsigned char* p = (const unsigned char*)start;
155
- const unsigned char* limit = (const unsigned char*)end;
156
- while (p < limit && *p) {
157
- uint16_t codepoint = ui_next_utf8_codepoint_bounded(&p, limit);
158
- if (codepoint == 0) break;
159
- w += ui_text_codepoint_advance(codepoint, ts, fontFace, letterSpacing);
160
- }
161
- return w;
162
- }
163
-
164
- struct UITextLine {
165
- const char* start;
166
- const char* end;
167
- uint16_t width;
168
- };
169
-
170
- static inline uint8_t ui_text_next_line(const char** cursor, uint16_t maxWidth, uint8_t whiteSpaceMode, uint8_t ts, uint8_t fontFace, int8_t letterSpacing, UITextLine* out) {
171
- if (!cursor || !*cursor || !out) return 0;
172
- const char* p = *cursor;
173
- if (!*p) return 0;
174
- uint8_t hardNewlines = whiteSpaceMode == UI_WS_PRE || whiteSpaceMode == UI_WS_PRE_LINE;
175
- uint8_t canWrap = (whiteSpaceMode == UI_WS_NORMAL || whiteSpaceMode == UI_WS_PRE_LINE) && maxWidth > 0;
176
-
177
- if (whiteSpaceMode == UI_WS_NORMAL || whiteSpaceMode == UI_WS_PRE_LINE) {
178
- p = ui_skip_wrap_spaces(p);
179
- }
180
- if (!*p) {
181
- *cursor = p;
182
- return 0;
183
- }
184
- if (hardNewlines && ui_is_text_newline(*p)) {
185
- out->start = p;
186
- out->end = p;
187
- out->width = 0;
188
- *cursor = ui_after_text_newline(p);
189
- return 1;
190
- }
191
-
192
- const char* lineStart = p;
193
- if (!canWrap) {
194
- while (*p && !(hardNewlines && ui_is_text_newline(*p))) p++;
195
- out->start = lineStart;
196
- out->end = p;
197
- out->width = ui_text_span_width(lineStart, p, ts, fontFace, letterSpacing);
198
- *cursor = (hardNewlines && ui_is_text_newline(*p)) ? ui_after_text_newline(p) : p;
199
- return 1;
200
- }
201
-
202
- uint16_t width = 0;
203
- const char* lastBreakAfter = nullptr;
204
- const char* lastBreakEnd = lineStart;
205
- uint16_t lastBreakWidth = 0;
206
- const char* lastNonSpaceEnd = lineStart;
207
- uint16_t lastNonSpaceWidth = 0;
208
-
209
- // Pre-compute string end bounds to avoid repeated scans in the loop
210
- const unsigned char* textEnd = (const unsigned char*)p;
211
- while (*textEnd) textEnd++;
212
-
213
- while (*p && p < (const char*)textEnd) {
214
- if (hardNewlines && ui_is_text_newline(*p)) break;
215
- const char* charStart = p;
216
- const unsigned char* next = (const unsigned char*)p;
217
- uint16_t codepoint = ui_next_utf8_codepoint_bounded(&next, textEnd);
218
- if (codepoint == 0) break;
219
- const char* charEnd = (const char*)next;
220
- uint8_t isBreakSpace = ui_is_text_space(*charStart) || (!hardNewlines && ui_is_text_newline(*charStart));
221
- uint16_t adv = ui_text_codepoint_advance(codepoint, ts, fontFace, letterSpacing);
222
-
223
- if (width > 0 && width + adv > maxWidth) {
224
- if (lastBreakAfter && lastBreakAfter > lineStart) {
225
- out->start = lineStart;
226
- out->end = lastBreakEnd;
227
- out->width = lastBreakWidth;
228
- *cursor = lastBreakAfter;
229
- return 1;
230
- }
231
- out->start = lineStart;
232
- out->end = charStart;
233
- out->width = width;
234
- *cursor = charStart;
235
- return 1;
236
- }
237
-
238
- width += adv;
239
- p = charEnd;
240
- if (isBreakSpace) {
241
- lastBreakAfter = p;
242
- lastBreakEnd = lastNonSpaceEnd;
243
- lastBreakWidth = lastNonSpaceWidth;
244
- } else {
245
- lastNonSpaceEnd = p;
246
- lastNonSpaceWidth = width;
247
- }
248
- }
249
-
250
- out->start = lineStart;
251
- if (whiteSpaceMode == UI_WS_NORMAL || whiteSpaceMode == UI_WS_PRE_LINE) {
252
- out->end = lastNonSpaceEnd;
253
- out->width = lastNonSpaceWidth;
254
- } else {
255
- out->end = p;
256
- out->width = width;
257
- }
258
- *cursor = (hardNewlines && ui_is_text_newline(*p)) ? ui_after_text_newline(p) : p;
259
- return 1;
260
- }
261
-
262
- static inline void ui_text_layout_metrics(const char* text, uint16_t maxWidth, uint8_t whiteSpaceMode, uint8_t ts, uint8_t fontFace, int8_t letterSpacing, uint8_t lineHeight, uint16_t* outW, uint16_t* outH) {
263
- if (!text) text = "";
264
- uint16_t maxLineW = 0;
265
- uint16_t h = 0;
266
- uint8_t lh = ui_text_line_height(ts, fontFace, lineHeight);
267
- const char* cursor = text;
268
- UITextLine line;
269
- while (ui_text_next_line(&cursor, maxWidth, whiteSpaceMode, ts, fontFace, letterSpacing, &line)) {
270
- if (line.width > maxLineW) maxLineW = line.width;
271
- h += lh;
272
- }
273
- if (h == 0) h = lh;
274
- if (outW) *outW = maxLineW;
275
- if (outH) *outH = h;
276
- }
277
-
278
- static inline void ui_invalidate_text_layout_cache(uint16_t nodeIdx) {
279
- if (nodeIdx < __ui_node_count) __ui_nodes[nodeIdx].layoutCacheKey = 0;
280
- }
281
-
282
- static inline uint32_t ui_text_layout_cache_key(uint16_t nodeIdx, uint16_t textMaxW) {
283
- uint32_t key = textMaxW;
284
- key = key * 31u + __ui_nodes[nodeIdx].whiteSpaceMode;
285
- uint8_t ts = __ui_nodes[nodeIdx].textSize ? __ui_nodes[nodeIdx].textSize : 2;
286
- key = key * 31u + ts;
287
- key = key * 31u + __ui_nodes[nodeIdx].fontFace;
288
- key = key * 31u + static_cast<uint8_t>(__ui_nodes[nodeIdx].letterSpacing + 128);
289
- key = key * 31u + __ui_nodes[nodeIdx].lineHeight;
290
- if (__ui_nodes[nodeIdx].hasTextBinding) {
291
- const char* t = __ui_nodes[nodeIdx].textBuffer;
292
- while (t && *t) {
293
- key = key * 31u + static_cast<uint8_t>(*t);
294
- t++;
295
- }
296
- }
297
- return key | 1u;
298
- }
299
-
300
- static inline uint16_t ui_node_text_max_width(uint16_t nodeIdx) {
301
- if (nodeIdx >= __ui_node_count) return 0;
302
- uint16_t textMaxW = __ui_nodes[nodeIdx].box.w;
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
- } else if (__ui_nodes[nodeIdx].kind == NODE_CHECK || __ui_nodes[nodeIdx].kind == NODE_RADIO) {
308
- textMaxW = __ui_nodes[nodeIdx].box.w > 22 ? static_cast<uint16_t>(__ui_nodes[nodeIdx].box.w - 22) : 0;
309
- }
310
- return textMaxW;
311
- }
312
-
313
- static inline void ui_node_text_layout_metrics(uint16_t nodeIdx, uint16_t textMaxW, uint16_t* outW, uint16_t* outH) {
314
- if (nodeIdx >= __ui_node_count) {
315
- if (outW) *outW = 0;
316
- if (outH) *outH = 0;
317
- return;
318
- }
319
- uint32_t key = ui_text_layout_cache_key(nodeIdx, textMaxW);
320
- if (__ui_nodes[nodeIdx].layoutCacheKey == key) {
321
- if (outW) *outW = __ui_nodes[nodeIdx].layoutMetricsW;
322
- if (outH) *outH = __ui_nodes[nodeIdx].layoutMetricsH;
323
- return;
324
- }
325
- const char* displayText = __ui_nodes[nodeIdx].hasTextBinding
326
- ? __ui_nodes[nodeIdx].textBuffer
327
- : __ui_nodes[nodeIdx].text;
328
- uint8_t ts = __ui_nodes[nodeIdx].textSize ? __ui_nodes[nodeIdx].textSize : 2;
329
- uint16_t tw = 0;
330
- uint16_t th = 0;
331
- ui_text_layout_metrics(displayText, textMaxW, __ui_nodes[nodeIdx].whiteSpaceMode, ts,
332
- __ui_nodes[nodeIdx].fontFace, __ui_nodes[nodeIdx].letterSpacing, __ui_nodes[nodeIdx].lineHeight, &tw, &th);
333
- __ui_nodes[nodeIdx].layoutCacheKey = key;
334
- __ui_nodes[nodeIdx].layoutMetricsW = tw;
335
- __ui_nodes[nodeIdx].layoutMetricsH = th;
336
- if (outW) *outW = tw;
337
- if (outH) *outH = th;
338
- }
339
-
340
- static inline void ui_copy_text_span(const char* start, const char* end, char* out, uint8_t outSize) {
341
- if (!out || outSize == 0) return;
342
- uint8_t len = 0;
343
- while (start && end && start < end && *start && len + 1 < outSize) {
344
- out[len++] = *start++;
345
- }
346
- out[len] = 0;
347
- }
348
-
349
- static inline uint8_t ui_draw_asset_text(const char* text, int16_t x, int16_t y, UI_COLOR_T fg, UI_COLOR_T bg, uint8_t antialias, uint8_t fontFace) {
350
- const UIFontFace* face = ui_font_face(fontFace);
351
- if (!text || !face) return 0;
352
- int16_t cursor = x;
353
- int16_t baseline = y + face->baseline;
354
- int16_t targetLeft, targetTop, targetRight, targetBottom;
355
- ui_display_target_bounds(&targetLeft, &targetTop, &targetRight, &targetBottom);
356
- const unsigned char* p = (const unsigned char*)text;
357
- while (*p) {
358
- uint16_t codepoint = ui_next_utf8_codepoint(&p);
359
- const UIFontGlyph* glyph = ui_font_glyph(face, codepoint);
360
- if (!glyph) {
361
- cursor += face->lineHeight / 2;
362
- continue;
363
- }
364
- int16_t glyphX = cursor + glyph->xOffset;
365
- int16_t glyphY = baseline + glyph->yOffset;
366
- if (glyphX + static_cast<int16_t>(glyph->width) <= targetLeft || glyphX >= targetRight ||
367
- glyphY + static_cast<int16_t>(glyph->height) <= targetTop || glyphY >= targetBottom) {
368
- cursor += glyph->advance;
369
- continue;
370
- }
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
- for (int16_t gy = gyStart; gy < gyEnd; gy++) {
376
- for (int16_t gx = gxStart; gx < gxEnd; gx++) {
377
- uint16_t pixelIndex = static_cast<uint16_t>(gy) * glyph->width + static_cast<uint16_t>(gx);
378
- uint8_t alpha = ui_font_alpha_at(face, glyph, pixelIndex);
379
- if (alpha == 0) continue;
380
- int16_t dx = glyphX + gx;
381
- int16_t dy = glyphY + gy;
382
- if (antialias) {
383
- if (fg == bg) {
384
- // Transparent mode: can't blend (fg==bg → all alphas become fg).
385
- // Use a threshold so only high-coverage pixels draw, avoiding
386
- // the bumpy look from flattening sub-pixel coverage to solid.
387
- if (alpha >= 8) ui_display_draw_pixel(dx, dy, fg);
388
- } else {
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
- }
391
- } else if (alpha >= 8) {
392
- ui_display_draw_pixel(dx, dy, fg);
393
- }
394
- }
395
- }
396
- cursor += glyph->advance;
397
- }
398
- return 1;
399
- }
400
-
401
- static inline void ui_draw_bitmap_text(const char* text, int16_t x, int16_t y, UI_COLOR_T fg, UI_COLOR_T bg, uint8_t ts, int8_t letterSpacing) {
402
- if (!text) text = "";
403
- if (ts == 0) ts = 2;
404
- ui_display_set_text_color(fg, bg);
405
- ui_display_set_text_size(ts);
406
- ui_display_set_text_wrap(false);
407
- // Draw char-by-char to apply letterSpacing between glyphs.
408
- if (letterSpacing == 0) {
409
- ui_display_set_cursor(x, y);
410
- ui_display_print(text);
411
- } else {
412
- int16_t cx = x;
413
- char buf[2] = {0, 0};
414
- for (const char* p = text; *p; p++) {
415
- ui_display_set_cursor(cx, y);
416
- buf[0] = *p;
417
- ui_display_print(buf);
418
- cx += ts * 6 + letterSpacing;
419
- }
420
- }
421
- }
422
-
423
- #ifdef UI_AA
424
- static CuttlefishCanvas16* __ui_aa_canvas = nullptr;
425
- static CuttlefishCanvas16* __ui_text_src_canvas = nullptr;
426
- static CuttlefishCanvas16* __ui_text_dst_canvas = nullptr;
427
-
428
- static inline CuttlefishCanvas16* ui_text_canvas(CuttlefishCanvas16** slot, int16_t w, int16_t h) {
429
- if (w <= 0) w = 1;
430
- if (h <= 0) h = 1;
431
- if (!*slot || display_canvasWidth(*slot) < w || display_canvasHeight(*slot) < h) {
432
- display_deleteCanvas(*slot);
433
- *slot = display_createCanvas(w, h);
434
- }
435
- return *slot;
436
- }
437
-
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
- uint8_t count = 0;
440
- for (int8_t dy = -static_cast<int8_t>(radius); dy <= static_cast<int8_t>(radius); dy++) {
441
- int16_t yy = y + dy;
442
- if (yy < 0 || yy >= h) continue;
443
- for (int8_t dx = -static_cast<int8_t>(radius); dx <= static_cast<int8_t>(radius); dx++) {
444
- int16_t xx = x + dx;
445
- if (xx < 0 || xx >= w) continue;
446
- if (display_canvasGetPixel(src, xx, yy) == fg) count++;
447
- }
448
- }
449
- return count;
450
- }
451
-
452
- static inline uint8_t ui_text_aa_coverage(uint8_t neighbors, uint8_t outerNeighbors, uint8_t isFg, uint8_t ts) {
453
- if (isFg) {
454
- if (ts <= 1) return neighbors >= 4 ? 100 : 96;
455
- if (ts == 2) return neighbors >= 8 ? 100 : neighbors >= 5 ? 96 : 92;
456
- return neighbors >= 8 ? 100 : neighbors >= 6 ? 96 : neighbors >= 4 ? 90 : 84;
457
- }
458
- if (neighbors == 0) {
459
- if (ts >= 3 && outerNeighbors > 0) {
460
- uint8_t outer = outerNeighbors * 2;
461
- return outer > 14 ? 14 : outer;
462
- }
463
- return 0;
464
- }
465
- uint8_t step = ts <= 1 ? 4 : ts == 2 ? 6 : 8;
466
- uint8_t cap = ts <= 1 ? 18 : ts == 2 ? 28 : 38;
467
- uint8_t coverage = neighbors * step;
468
- return coverage > cap ? cap : coverage;
469
- }
470
-
471
- // ── Rasterized-glyph coverage cache (LVGL-style) ──────────────────────────
472
- // The classic-font AA pass neighbor-counts a 3x3 (5x5 for large sizes) kernel
473
- // per pixel of the whole line canvas on EVERY repaint — a 460px label at ts=2
474
- // costs ~70k canvas reads per frame. Classic 5x7 glyph cells are position-
475
- // independent (the 6ts cell's trailing gap column is blank and the sampling
476
- // radius never crosses it into the next glyph's pixels except the final gap
477
- // column's 1px outer halo, which lands on spacing), so per-(char, ts)
478
- // coverage is cacheable. Direct-mapped, color-independent (coverage is a
479
- // shape property; fg/bg enter only at blend time).
480
- #ifndef UI_AA_GLYPH_CACHE_SLOTS
481
- #if defined(__AVR__)
482
- #define UI_AA_GLYPH_CACHE_SLOTS 0
483
- #else
484
- #define UI_AA_GLYPH_CACHE_SLOTS 48
485
- #endif
486
- #endif
487
- #define UI_AA_GLYPH_CACHE_MAX_TS 3
488
- #if UI_AA_GLYPH_CACHE_SLOTS > 0
489
- struct UIGlyphCovEntry {
490
- uint8_t ch;
491
- uint8_t ts;
492
- uint8_t valid;
493
- uint8_t cov[(6 * UI_AA_GLYPH_CACHE_MAX_TS) * (8 * UI_AA_GLYPH_CACHE_MAX_TS + 1)];
494
- };
495
- static struct UIGlyphCovEntry __ui_glyph_cov[UI_AA_GLYPH_CACHE_SLOTS];
496
- static CuttlefishCanvas16* __ui_glyph_src_canvas = nullptr;
497
-
498
- // Compute (on miss) and return the cached coverage map for one glyph cell,
499
- // or nullptr when the size exceeds the cached range. The cell is
500
- // (6*ts) x (8*ts + 1) — the +1 bottom pad row matches the whole-line canvas
501
- // so bottom-edge coverage is identical.
502
- static const uint8_t* ui_aa_glyph_coverage(uint8_t ch, uint8_t ts) {
503
- if (ts == 0U || ts > UI_AA_GLYPH_CACHE_MAX_TS) return nullptr;
504
- uint16_t slot = static_cast<uint16_t>((static_cast<uint16_t>(ch) * 7U) + ts) % UI_AA_GLYPH_CACHE_SLOTS;
505
- struct UIGlyphCovEntry* e = &__ui_glyph_cov[slot];
506
- if (e->valid && e->ch == ch && e->ts == ts) return e->cov;
507
- int16_t cw = static_cast<int16_t>(6 * ts);
508
- int16_t chh = static_cast<int16_t>(8 * ts + 1);
509
- if (cw <= 0 || chh <= 0) return nullptr;
510
- if (!__ui_glyph_src_canvas || display_canvasWidth(__ui_glyph_src_canvas) < cw || display_canvasHeight(__ui_glyph_src_canvas) < chh) {
511
- display_deleteCanvas(__ui_glyph_src_canvas);
512
- __ui_glyph_src_canvas = display_createCanvas(cw, chh);
513
- }
514
- if (!__ui_glyph_src_canvas || !display_canvasBuffer(__ui_glyph_src_canvas)) return nullptr;
515
- // Rasterize in black-on-white: coverage is shape-only (neighbor counts
516
- // compare against the glyph color), independent of the draw-time colors.
517
- const UI_COLOR_T cellFg = 0x0000;
518
- const UI_COLOR_T cellBg = 0xFFFF;
519
- display_canvasFillRect(__ui_glyph_src_canvas, 0, 0, cw, chh, cellBg);
520
- display_targetSetCursor((CuttlefishDisplayTarget*)__ui_glyph_src_canvas, 0, 0);
521
- display_targetSetTextColorBg((CuttlefishDisplayTarget*)__ui_glyph_src_canvas, cellFg, cellBg);
522
- display_targetSetTextSize((CuttlefishDisplayTarget*)__ui_glyph_src_canvas, ts);
523
- display_targetSetTextWrap((CuttlefishDisplayTarget*)__ui_glyph_src_canvas, false);
524
- char buf[2] = { static_cast<char>(ch), 0 };
525
- display_targetPrint((CuttlefishDisplayTarget*)__ui_glyph_src_canvas, buf);
526
- uint16_t i = 0;
527
- for (int16_t yy = 0; yy < chh; yy++) {
528
- for (int16_t xx = 0; xx < cw; xx++) {
529
- UI_COLOR_T px = display_canvasGetPixel(__ui_glyph_src_canvas, xx, yy);
530
- uint8_t neighbors = ui_text_fg_neighbors(__ui_glyph_src_canvas, xx, yy, cw, chh, cellFg);
531
- uint8_t outerNeighbors = 0;
532
- if (ts >= 3U && px != cellFg && neighbors == 0U) {
533
- outerNeighbors = ui_text_fg_neighbors(__ui_glyph_src_canvas, xx, yy, cw, chh, cellFg, 2);
534
- }
535
- e->cov[i++] = ui_text_aa_coverage(neighbors, outerNeighbors, px == cellFg ? 1U : 0U, ts);
536
- }
537
- }
538
- e->ch = ch;
539
- e->ts = ts;
540
- e->valid = 1U;
541
- return e->cov;
542
- }
543
- #else
544
- static const uint8_t* ui_aa_glyph_coverage(uint8_t /*ch*/, uint8_t /*ts*/) { return nullptr; }
545
- #endif
546
-
547
- static inline void ui_draw_aa_text(const char* text, int16_t x, int16_t y, UI_COLOR_T fg, UI_COLOR_T bg, uint8_t ts) {
548
- if (!text || !*text) return;
549
- if (ts == 0) ts = 2;
550
- uint16_t w = ui_text_width(text, ts, 0, 0);
551
- // Add 1px bottom padding so the AA edge-detection sampling doesn't clip the
552
- // glyph bottoms (the neighbor-count at the last row rounds partial coverage
553
- // to 0 without this clearance).
554
- uint8_t h = ui_text_height(ts, 0) + 1;
555
- if (w == 0 || h == 0 || fg == bg) {
556
- ui_draw_bitmap_text(text, x, y, fg, bg, ts, 0);
557
- return;
558
- }
559
- int16_t clipX = x;
560
- int16_t clipY = y;
561
- int16_t clipW = static_cast<int16_t>(w);
562
- int16_t clipH = static_cast<int16_t>(h);
563
- if (!ui_clip_rect_to_display_target(&clipX, &clipY, &clipW, &clipH)) return;
564
- int16_t localX = static_cast<int16_t>(clipX - x);
565
- int16_t localY = static_cast<int16_t>(clipY - y);
566
-
567
- // Fast path (rasterized-glyph cache): when every glyph's coverage is
568
- // cached, blend per-cell straight into dst — no line-canvas rasterization,
569
- // no neighbor sampling. Identical coverage per pixel except the 1px outer
570
- // halo the whole-line kernel lands on the final gap column before a glyph
571
- // start (spacing-only; sub-perceptual).
572
- {
573
- const uint8_t* firstCov = ui_aa_glyph_coverage(static_cast<uint8_t>(text[0]), ts);
574
- if (firstCov != nullptr) {
575
- CuttlefishCanvas16* dst = ui_text_canvas(&__ui_text_dst_canvas, static_cast<int16_t>(w), static_cast<int16_t>(h));
576
- if (dst && display_canvasBuffer(dst)) {
577
- display_canvasFillRect(dst, localX, localY, clipW, clipH, bg);
578
- int16_t cellW = static_cast<int16_t>(6 * ts);
579
- int16_t cellH = static_cast<int16_t>(8 * ts + 1);
580
- int16_t cellX = 0;
581
- for (const char* p2 = text; *p2; p2++) {
582
- const uint8_t* cov = (p2 == text) ? firstCov : ui_aa_glyph_coverage(static_cast<uint8_t>(*p2), ts);
583
- if (!cov) break; // size fell out of the cached range mid-string
584
- int16_t sx0 = cellX < localX ? static_cast<int16_t>(localX - cellX) : 0;
585
- int16_t sy0 = localY;
586
- int16_t sx1 = static_cast<int16_t>(cellX + cellW < localX + clipW ? cellX + cellW : localX + clipW);
587
- int16_t sy1 = static_cast<int16_t>(localY + clipH < cellH ? localY + clipH : cellH);
588
- for (int16_t yy = sy0; yy < sy1; yy++) {
589
- for (int16_t xx = static_cast<int16_t>(cellX + sx0); xx < sx1; xx++) {
590
- uint8_t coverage = cov[static_cast<uint16_t>(yy) * static_cast<uint16_t>(cellW) + static_cast<uint16_t>(xx - cellX)];
591
- display_targetDrawPixel((CuttlefishDisplayTarget*)dst, xx, yy,
592
- coverage == 0U ? bg : ui_blend(fg, bg, coverage));
593
- }
594
- }
595
- cellX += cellW;
596
- if (cellX >= localX + clipW) break;
597
- }
598
- int16_t stride = display_canvasWidth(dst);
599
- UI_COLOR_T* pixels = display_canvasBuffer(dst);
600
- for (int16_t row = 0; row < clipH; row++) {
601
- ui_display_draw_rgb_bitmap(clipX, static_cast<int16_t>(clipY + row),
602
- pixels + static_cast<int32_t>(localY + row) * stride + localX, clipW, 1);
603
- }
604
- return;
605
- }
606
- }
607
- }
608
-
609
- // Use pre-allocated static canvases (no dynamic allocation)
610
- CuttlefishCanvas16* src = ui_text_canvas(&__ui_text_src_canvas, static_cast<int16_t>(w), static_cast<int16_t>(h));
611
- CuttlefishCanvas16* dst = ui_text_canvas(&__ui_text_dst_canvas, static_cast<int16_t>(w), static_cast<int16_t>(h));
612
- if (!src || !dst || !display_canvasBuffer(src) || !display_canvasBuffer(dst)) {
613
- ui_draw_bitmap_text(text, x, y, fg, bg, ts, 0);
614
- return;
615
- }
616
-
617
- display_canvasFillRect(src, 0, 0, w, h, bg);
618
- display_canvasFillRect(dst, localX, localY, clipW, clipH, bg);
619
- display_targetSetCursor((CuttlefishDisplayTarget*)src, 0, 0);
620
- display_targetSetTextColorBg((CuttlefishDisplayTarget*)src, fg, bg);
621
- display_targetSetTextSize((CuttlefishDisplayTarget*)src, ts);
622
- display_targetSetTextWrap((CuttlefishDisplayTarget*)src, false);
623
- display_targetPrint((CuttlefishDisplayTarget*)src, text);
624
-
625
- for (int16_t yy = localY; yy < localY + clipH; yy++) {
626
- for (int16_t xx = localX; xx < localX + clipW; xx++) {
627
- UI_COLOR_T px = display_canvasGetPixel(src, xx, yy);
628
- uint8_t neighbors = ui_text_fg_neighbors(src, xx, yy, static_cast<int16_t>(w), h, fg);
629
- uint8_t outerNeighbors = 0;
630
- if (ts >= 3 && px != fg && neighbors == 0) {
631
- outerNeighbors = ui_text_fg_neighbors(src, xx, yy, static_cast<int16_t>(w), h, fg, 2);
632
- }
633
- uint8_t coverage = ui_text_aa_coverage(neighbors, outerNeighbors, px == fg ? 1 : 0, ts);
634
- display_targetDrawPixel((CuttlefishDisplayTarget*)dst, xx, yy, coverage == 0 ? bg : ui_blend(fg, bg, coverage));
635
- }
636
- }
637
-
638
- int16_t stride = display_canvasWidth(dst);
639
- UI_COLOR_T* pixels = display_canvasBuffer(dst);
640
- for (int16_t row = 0; row < clipH; row++) {
641
- ui_display_draw_rgb_bitmap(clipX, static_cast<int16_t>(clipY + row),
642
- pixels + static_cast<int32_t>(localY + row) * stride + localX, clipW, 1);
643
- }
644
- }
645
-
646
- static inline void ui_draw_text(const char* text, int16_t x, int16_t y, UI_COLOR_T fg, UI_COLOR_T bg, uint8_t ts, uint8_t antialias, uint8_t fontFace, int8_t letterSpacing) {
647
- if (fontFace && ui_draw_asset_text(text, x, y, fg, bg, antialias, fontFace)) {
648
- return;
649
- }
650
- // Only use AA when fg != bg (opaque background). When fg == bg (transparent
651
- // mode), the AA source canvas fills entirely with fg — no edges to detect,
652
- // producing a solid rectangle instead of text.
653
- if (antialias && fg != bg && letterSpacing == 0) {
654
- ui_draw_aa_text(text, x, y, fg, bg, ts);
655
- return;
656
- }
657
- ui_draw_bitmap_text(text, x, y, fg, bg, ts, letterSpacing);
658
- }
659
- #else
660
- static inline void ui_draw_text(const char* text, int16_t x, int16_t y, UI_COLOR_T fg, UI_COLOR_T bg, uint8_t ts, uint8_t antialias, uint8_t fontFace, int8_t letterSpacing) {
661
- (void)antialias;
662
- if (fontFace && ui_draw_asset_text(text, x, y, fg, bg, antialias, fontFace)) {
663
- return;
664
- }
665
- ui_draw_bitmap_text(text, x, y, fg, bg, ts, letterSpacing);
666
- }
667
- #endif
668
-
669
- // Draw classic list rows without routing through CuttlefishGFX::print(). The
670
- // Zephyr native target uses CuttlefishGFX for both the panel and RGB565 canvas;
671
- // its virtual text path is reliable on the panel but can be lost when the
672
- // canvas is later copied to the panel. Writing the same 5x7 glyphs through the
673
- // normal pixel/fill shims keeps the row pixels in the canvas buffer and works
674
- // for both cached-list and band-list rendering. Other adapters retain the
675
- // existing ui_draw_text fallback because they provide their own canvas text
676
- // implementation.
677
- static inline void ui_draw_list_text(const char* text, int16_t x, int16_t y,
678
- UI_COLOR_T fg, UI_COLOR_T bg, uint8_t ts,
679
- uint8_t fontFace, int8_t letterSpacing) {
680
- #if defined(CUTTLEFISH_GFX_DEFINED)
681
- if (fontFace == 0) {
682
- if (!text) text = "";
683
- if (ts == 0) ts = 2;
684
- int16_t cx = x;
685
- for (const unsigned char* p = (const unsigned char*)text; *p; p++) {
686
- uint8_t ch = *p;
687
- // CuttlefishGFX's classic font is the full 256-glyph Adafruit table.
688
- for (uint8_t col = 0; col < 5; col++) {
689
- uint8_t bits = cuttlefish_glcdfont[static_cast<uint16_t>(ch) * 5u + col];
690
- for (uint8_t row = 0; row < 7; row++) {
691
- if (bits & static_cast<uint8_t>(1u << row)) {
692
- ui_display_fill_rect(static_cast<int16_t>(cx + static_cast<int16_t>(col) * ts),
693
- static_cast<int16_t>(y + static_cast<int16_t>(row) * ts), ts, ts, fg);
694
- }
695
- }
696
- }
697
- // Match the classic GFX advance: five columns plus one blank column.
698
- cx = static_cast<int16_t>(cx + static_cast<int16_t>(ts) * 6 + letterSpacing);
699
- }
700
- return;
701
- }
702
- #endif
703
- ui_draw_text(text, x, y, fg, bg, ts, 0, fontFace, letterSpacing);
704
- }
705
-
706
- // Direct-panel list fallback. CuttlefishGFX's bitmap glyph path above is
707
- // intentionally used for RAM canvases, but a few native panel adapters only
708
- // commit their text state correctly through print()/drawChar(). Use that
709
- // established path when the list is painted straight to the panel; the caller
710
- // keeps the whole list inside one write transaction, so this does not expose a
711
- // partially composed row set.
712
- static inline void ui_draw_list_text_direct(const char* text, int16_t x, int16_t y,
713
- UI_COLOR_T fg, UI_COLOR_T bg, uint8_t ts,
714
- uint8_t fontFace, int8_t letterSpacing) {
715
- if (fontFace == 0) {
716
- ui_draw_bitmap_text(text, x, y, fg, bg, ts, letterSpacing);
717
- } else {
718
- ui_draw_text(text, x, y, fg, bg, ts, 0, fontFace, letterSpacing);
719
- }
720
- }
721
-
722
- /** Truncate a NUL-terminated buffer in place to fit within maxWidth (px) and
723
- * append "...". Used by text-overflow: ellipsis. Adafruit_GFX has no ellipsis
724
- * glyph, so three ASCII dots approximate it. */
725
- static inline void ui_truncate_ellipsis(char* buf, uint8_t bufSize, uint16_t maxWidth,
726
- uint8_t ts, uint8_t fontFace, int8_t letterSpacing) {
727
- if (!buf || bufSize == 0) return;
728
- uint8_t len = static_cast<uint8_t>(strlen(buf));
729
- // Reserve space for the three trailing dots.
730
- uint16_t dotsW = static_cast<uint16_t>(3) * ui_text_codepoint_advance(static_cast<uint16_t>('.'), ts, fontFace, letterSpacing);
731
- int16_t budget = static_cast<int16_t>(maxWidth) - static_cast<int16_t>(dotsW);
732
- if (budget <= 0) { if (bufSize > 3) { buf[0]='.'; buf[1]='.'; buf[2]='.'; buf[3]=0; } return; }
733
- // Measure once, then remove one UTF-8 codepoint at a time from the end.
734
- // The previous prefix-- loop rescanned the whole prefix on every iteration
735
- // (O(n²)); this keeps truncation linear for long bound strings.
736
- uint8_t prefix = len;
737
- uint16_t prefixW = ui_text_span_width(buf, buf + prefix, ts, fontFace, letterSpacing);
738
- while (prefix > 0 && static_cast<int16_t>(prefixW) > budget) {
739
- uint8_t cut = static_cast<uint8_t>(prefix - 1);
740
- while (cut > 0 && (static_cast<uint8_t>(buf[cut]) & 0xC0) == 0x80) cut--;
741
- uint16_t removed = ui_text_span_width(buf + cut, buf + prefix, ts, fontFace, letterSpacing);
742
- prefixW = prefixW > removed ? static_cast<uint16_t>(prefixW - removed) : 0;
743
- prefix = cut;
744
- }
745
- if (prefix + 3 < bufSize) {
746
- buf[prefix] = '.'; buf[prefix+1] = '.'; buf[prefix+2] = '.'; buf[prefix+3] = 0;
747
- } else if (bufSize > 3) {
748
- buf[0]='.'; buf[1]='.'; buf[2]='.'; buf[3]=0;
749
- }
750
- }
751
-
752
- // text-overflow: clip — trim trailing chars until the prefix fits maxWidth,
753
- // then NUL-terminate. No trailing dots (contrast with ui_truncate_ellipsis).
754
- static inline void ui_truncate_clip(char* buf, uint8_t bufSize, uint16_t maxWidth,
755
- uint8_t ts, uint8_t fontFace, int8_t letterSpacing) {
756
- if (!buf || bufSize == 0) return;
757
- uint8_t len = static_cast<uint8_t>(strlen(buf));
758
- uint8_t prefix = len;
759
- uint16_t prefixW = ui_text_span_width(buf, buf + prefix, ts, fontFace, letterSpacing);
760
- while (prefix > 0 && static_cast<int16_t>(prefixW) > static_cast<int16_t>(maxWidth)) {
761
- uint8_t cut = static_cast<uint8_t>(prefix - 1);
762
- while (cut > 0 && (static_cast<uint8_t>(buf[cut]) & 0xC0) == 0x80) cut--;
763
- uint16_t removed = ui_text_span_width(buf + cut, buf + prefix, ts, fontFace, letterSpacing);
764
- prefixW = prefixW > removed ? static_cast<uint16_t>(prefixW - removed) : 0;
765
- prefix = cut;
766
- }
767
- if (prefix < bufSize) buf[prefix] = 0;
768
- }
769
-
770
- static inline void ui_draw_wrapped_text(const char* text, int16_t x, int16_t y, uint16_t maxWidth, UI_COLOR_T fg, UI_COLOR_T bg,
771
- uint8_t ts, uint8_t antialias, uint8_t fontFace, int8_t letterSpacing,
772
- uint8_t lineHeight, uint8_t whiteSpaceMode, uint8_t textAlign, uint8_t underline, uint8_t textOverflow) {
773
- if (!text) text = "";
774
- uint8_t lh = ui_text_line_height(ts, fontFace, lineHeight);
775
- const char* cursor = text;
776
- int16_t lineY = y;
777
- UITextLine line;
778
- char lineBuf[UI_TEXT_LINE_BUF];
779
- int16_t targetLeft, targetTop, targetRight, targetBottom;
780
- ui_display_target_bounds(&targetLeft, &targetTop, &targetRight, &targetBottom);
781
- while (ui_text_next_line(&cursor, maxWidth, whiteSpaceMode, ts, fontFace, letterSpacing, &line)) {
782
- int16_t lineBottom = static_cast<int16_t>(lineY + lh);
783
- if (lineBottom <= targetTop || lineY >= targetBottom) {
784
- lineY += lh;
785
- continue;
786
- }
787
- int16_t lineX = x;
788
- if (textAlign == 1) lineX = x + (static_cast<int16_t>(maxWidth) - static_cast<int16_t>(line.width)) / 2;
789
- else if (textAlign == 2) lineX = x + static_cast<int16_t>(maxWidth) - static_cast<int16_t>(line.width);
790
- if (lineX + static_cast<int16_t>(line.width) <= targetLeft || lineX >= targetRight) {
791
- lineY += lh;
792
- continue;
793
- }
794
- ui_copy_text_span(line.start, line.end, lineBuf, UI_TEXT_LINE_BUF);
795
- // text-overflow — only applies when the line is wider than maxWidth.
796
- // textOverflow==1 (ellipsis): trim the span and append "...".
797
- // textOverflow==0 (clip): trim the span to the edge, no dots.
798
- if (static_cast<int16_t>(line.width) > static_cast<int16_t>(maxWidth)) {
799
- if (textOverflow) ui_truncate_ellipsis(lineBuf, UI_TEXT_LINE_BUF, maxWidth, ts, fontFace, letterSpacing);
800
- else ui_truncate_clip(lineBuf, UI_TEXT_LINE_BUF, maxWidth, ts, fontFace, letterSpacing);
801
- }
802
- ui_draw_text(lineBuf, lineX, lineY, fg, bg, ts, antialias, fontFace, letterSpacing);
803
- // text-decoration (underline=bit0, strikethrough=bit1)
804
- if (underline & 1) ui_display_draw_fast_hline(lineX, lineY + ui_text_height(ts, fontFace) - 1, line.width, fg);
805
- if (underline & 2) ui_display_draw_fast_hline(lineX, lineY + ui_text_height(ts, fontFace) / 2, line.width, fg);
806
- lineY += lh;
807
- }
808
- }
809
-
810
- // Draw a rich-text node from its precomputed run/segment/line geometry. Does
811
- // NOT re-wrap — the geometry was baked at transpile time (runs are static-only,
812
- // so the text never changes at runtime). Iterate segments once, skip lines and
813
- // segments outside the active draw target, and compute each segment's line
814
- // origin from textAlign + line width. Mixed font sizes align on the line's
815
- // baseline (each segment's top = baseline − its own ascent).
816
- static inline void ui_draw_rich_text(uint16_t nodeIdx, int16_t x, int16_t y, UI_COLOR_T bg, uint8_t antialias,
817
- uint8_t useFgOverride = 0, UI_COLOR_T fgOverride = 0, uint16_t maxWidth = 0) {
818
- UINode* n = &__ui_nodes[nodeIdx];
819
- uint16_t alignWidth = maxWidth ? maxWidth : n->box.w;
820
- int16_t targetLeft = static_cast<int16_t>(-__ui_draw_off_x);
821
- int16_t targetTop = static_cast<int16_t>(-__ui_draw_off_y);
822
- int16_t targetRight = static_cast<int16_t>(ui_display_target_width() - __ui_draw_off_x);
823
- int16_t targetBottom = static_cast<int16_t>(ui_display_target_height() - __ui_draw_off_y);
824
- uint16_t richSegEnd = static_cast<uint16_t>(n->richSegStart + n->richSegCount);
825
- for (uint16_t si = n->richSegStart; si < richSegEnd; si++) {
826
- UIRichSeg* seg = &__ui_rich_segs[si];
827
- if (seg->line >= n->richLineCount) continue;
828
- UIRichLine* line = &__ui_rich_lines[n->richLineStart + seg->line];
829
- int16_t lineTop = y + line->y;
830
- int16_t lineBottom = static_cast<int16_t>(lineTop + static_cast<int16_t>(line->h));
831
- if (lineBottom <= targetTop || lineTop >= targetBottom) continue;
832
- int16_t lineX = x;
833
- if (n->textAlign == 1) lineX = x + (static_cast<int16_t>(alignWidth) - static_cast<int16_t>(line->w)) / 2;
834
- else if (n->textAlign == 2) lineX = x + static_cast<int16_t>(alignWidth) - static_cast<int16_t>(line->w);
835
- int16_t segX = static_cast<int16_t>(lineX + seg->x);
836
- int16_t segRight = static_cast<int16_t>(segX + static_cast<int16_t>(seg->w));
837
- if (segRight <= targetLeft || segX >= targetRight) continue;
838
- UIRichRun* run = &__ui_runs[n->runStart + seg->runIndex];
839
- // Baseline alignment: for asset fonts, the ascent is the font face's
840
- // baseline (baked per px size); for the bitmap font (fontFace=0), it's the
841
- // 5x8 glyph ascent (7px × textSize). Using the wrong ascent misaligns runs
842
- // vertically and makes mixed rich-text lines look garbled.
843
- int16_t ascent;
844
- if (run->fontFace) {
845
- const UIFontFace* face = ui_font_face(run->fontFace);
846
- ascent = face ? static_cast<int16_t>(face->baseline) : (7 * static_cast<int16_t>(run->textSize));
847
- } else {
848
- ascent = 7 * static_cast<int16_t>(run->textSize);
849
- }
850
- int16_t segY = y + line->baseline - ascent;
851
- UI_COLOR_T fg = useFgOverride ? fgOverride : run->fg;
852
- ui_draw_text(seg->text, segX, segY, fg, bg, run->textSize, antialias, run->fontFace, run->letterSpacing);
853
- if (run->underline & 1) ui_display_draw_fast_hline(segX, segY + 8 * run->textSize - 1, seg->w, fg);
854
- if (run->underline & 2) ui_display_draw_fast_hline(segX, segY + 4 * run->textSize, seg->w, fg);
855
- }
856
- }
857
-
858
- // Hit-test a tap point (in node-local coordinates) against a rich-text node's
859
- // link runs. Returns the link run's resolved screen index, or -1 if the point
860
- // doesn't land on a link segment. Mirrors the list-item subdivision precedent
861
- // but uses measured segment rects instead of fixed row heights.
862
- static inline int8_t ui_rich_link_hit(uint16_t nodeIdx, int16_t px, int16_t py) {
863
- UINode* n = &__ui_nodes[nodeIdx];
864
- int16_t insetL = static_cast<int16_t>(n->borderWidth) + static_cast<int16_t>(n->paddingLeft);
865
- int16_t insetR = static_cast<int16_t>(n->borderWidth) + static_cast<int16_t>(n->paddingRight);
866
- int16_t insetT = static_cast<int16_t>(n->borderWidth) + static_cast<int16_t>(n->paddingTop);
867
- int16_t localX = px - insetL;
868
- int16_t localY = py - insetT;
869
- uint16_t alignWidth = n->box.w > static_cast<uint16_t>(insetL + insetR)
870
- ? static_cast<uint16_t>(static_cast<int16_t>(n->box.w) - insetL - insetR)
871
- : 0;
872
- for (uint16_t si = n->richSegStart; si < n->richSegStart + n->richSegCount; si++) {
873
- UIRichSeg* seg = &__ui_rich_segs[si];
874
- UIRichRun* run = &__ui_runs[n->runStart + seg->runIndex];
875
- if (run->linkTarget < 0) continue;
876
- UIRichLine* line = &__ui_rich_lines[n->richLineStart + seg->line];
877
- // Segment x is relative to its line's left edge (pre-alignment). For the
878
- // hit-test, account for center/right alignment the same way draw does.
879
- int16_t originX = 0;
880
- if (n->textAlign == 1) originX = (static_cast<int16_t>(alignWidth) - static_cast<int16_t>(line->w)) / 2;
881
- else if (n->textAlign == 2) originX = static_cast<int16_t>(alignWidth) - static_cast<int16_t>(line->w);
882
- int16_t sx = originX + seg->x;
883
- int16_t sy = line->y;
884
- if (localX >= sx && localX < sx + static_cast<int16_t>(seg->w) && localY >= sy && localY < sy + static_cast<int16_t>(line->h)) {
885
- return run->linkTarget;
886
- }
887
- }
888
- return -1;
889
- }
890
-
891
- // Draw shadows for an element. Loops over up to 4 shadow specs. Outset shadows
892
- // are drawn behind the element; inset shadows are drawn over the element fill.
5
+ return `
6
+ static inline const UIFontFace* ui_font_face(uint8_t id) {
7
+ if (id == 0) return nullptr;
8
+ for (uint16_t i = 0; i < __ui_font_face_count; i++) {
9
+ if (__ui_font_faces[i].id == id) return &__ui_font_faces[i];
10
+ }
11
+ return nullptr;
12
+ }
13
+
14
+ static inline const UIFontGlyph* ui_font_glyph(const UIFontFace* face, uint16_t codepoint) {
15
+ if (!face) return nullptr;
16
+ for (uint8_t i = 0; i < face->glyphCount; i++) {
17
+ if (face->glyphs[i].codepoint == codepoint) return &face->glyphs[i];
18
+ }
19
+ return nullptr;
20
+ }
21
+
22
+ static inline uint8_t ui_font_alpha_at(const UIFontFace* face, const UIFontGlyph* glyph, uint16_t pixelIndex) {
23
+ if (!face || !glyph || !face->alpha) return 0;
24
+ uint16_t nibble = glyph->dataOffset + pixelIndex;
25
+ // Bounds check to prevent reading past the alpha array
26
+ if (nibble >> 1 >= 65535) return 0;
27
+ #if defined(__AVR__)
28
+ uint8_t byte = pgm_read_byte(&face->alpha[nibble >> 1]);
29
+ #else
30
+ uint8_t byte = face->alpha[nibble >> 1];
31
+ #endif
32
+ return (nibble & 1) ? (byte & 0x0F) : (byte >> 4);
33
+ }
34
+
35
+ static inline uint16_t ui_next_utf8_codepoint(const unsigned char** p) {
36
+ const unsigned char* s = *p;
37
+ uint8_t b0 = *s++;
38
+ if (b0 < 0x80) {
39
+ *p = s;
40
+ return b0;
41
+ }
42
+ if ((b0 & 0xE0) == 0xC0 && (s[0] & 0xC0) == 0x80) {
43
+ uint16_t cp = (static_cast<uint16_t>(b0 & 0x1F) << 6) | static_cast<uint16_t>(s[0] & 0x3F);
44
+ *p = s + 1;
45
+ return cp;
46
+ }
47
+ if ((b0 & 0xF0) == 0xE0 && (s[0] & 0xC0) == 0x80 && (s[1] & 0xC0) == 0x80) {
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
+ *p = s + 2;
50
+ return cp;
51
+ }
52
+ if ((b0 & 0xF8) == 0xF0 && (s[0] & 0xC0) == 0x80 && (s[1] & 0xC0) == 0x80 && (s[2] & 0xC0) == 0x80) {
53
+ *p = s + 3;
54
+ return '?';
55
+ }
56
+ *p = s;
57
+ return '?';
58
+ }
59
+
60
+ static inline uint16_t ui_asset_text_width(const char* text, const UIFontFace* face) {
61
+ if (!text || !face) return 0;
62
+ uint16_t w = 0;
63
+ const unsigned char* p = (const unsigned char*)text;
64
+ while (*p) {
65
+ uint16_t codepoint = ui_next_utf8_codepoint(&p);
66
+ const UIFontGlyph* glyph = ui_font_glyph(face, codepoint);
67
+ w += glyph ? glyph->advance : (face->lineHeight / 2);
68
+ }
69
+ return w;
70
+ }
71
+
72
+ static inline uint8_t ui_asset_text_height(const UIFontFace* face) {
73
+ return face ? face->lineHeight : 0;
74
+ }
75
+
76
+ static inline uint16_t ui_text_width(const char* text, uint8_t ts, uint8_t fontFace, int8_t letterSpacing) {
77
+ const UIFontFace* face = ui_font_face(fontFace);
78
+ if (face) return ui_asset_text_width(text, face);
79
+ if (!text) return 0;
80
+ if (ts == 0) ts = 2;
81
+ uint16_t w = 0;
82
+ for (const char* p = text; *p; p++) w += ts * 6 + letterSpacing;
83
+ return w;
84
+ }
85
+
86
+ static inline uint8_t ui_text_height(uint8_t ts, uint8_t fontFace) {
87
+ const UIFontFace* face = ui_font_face(fontFace);
88
+ if (face) return ui_asset_text_height(face);
89
+ return (ts ? ts : 2) * 8;
90
+ }
91
+
92
+ static inline uint8_t ui_text_line_height(uint8_t ts, uint8_t fontFace, uint8_t lineHeight) {
93
+ return lineHeight ? lineHeight : ui_text_height(ts, fontFace);
94
+ }
95
+
96
+ static inline uint8_t ui_is_text_space(char c) {
97
+ return c == ' ' || c == '\\t' || c == '\\f' || c == '\\v';
98
+ }
99
+
100
+ static inline uint8_t ui_is_text_newline(char c) {
101
+ return c == '\\n' || c == '\\r';
102
+ }
103
+
104
+ static inline const char* ui_after_text_newline(const char* p) {
105
+ if (!p || !*p) return p;
106
+ if (*p == '\\r' && p[1] == '\\n') return p + 2;
107
+ return p + 1;
108
+ }
109
+
110
+ static inline const char* ui_skip_wrap_spaces(const char* p) {
111
+ while (p && ui_is_text_space(*p)) p++;
112
+ return p;
113
+ }
114
+
115
+ static inline uint16_t ui_next_utf8_codepoint_bounded(const unsigned char** p, const unsigned char* end) {
116
+ const unsigned char* s = *p;
117
+ if (!s || s >= end) return 0;
118
+ uint8_t b0 = *s++;
119
+ if (b0 < 0x80) {
120
+ *p = s;
121
+ return b0;
122
+ }
123
+ if ((b0 & 0xE0) == 0xC0 && s < end && (s[0] & 0xC0) == 0x80) {
124
+ uint16_t cp = (static_cast<uint16_t>(b0 & 0x1F) << 6) | static_cast<uint16_t>(s[0] & 0x3F);
125
+ *p = s + 1;
126
+ return cp;
127
+ }
128
+ if ((b0 & 0xF0) == 0xE0 && s + 1 < end && (s[0] & 0xC0) == 0x80 && (s[1] & 0xC0) == 0x80) {
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
+ *p = s + 2;
131
+ return cp;
132
+ }
133
+ if ((b0 & 0xF8) == 0xF0 && s + 2 < end && (s[0] & 0xC0) == 0x80 && (s[1] & 0xC0) == 0x80 && (s[2] & 0xC0) == 0x80) {
134
+ *p = s + 3;
135
+ return '?';
136
+ }
137
+ *p = s;
138
+ return '?';
139
+ }
140
+
141
+ static inline uint16_t ui_text_codepoint_advance(uint16_t codepoint, uint8_t ts, uint8_t fontFace, int8_t letterSpacing) {
142
+ const UIFontFace* face = ui_font_face(fontFace);
143
+ if (face) {
144
+ const UIFontGlyph* glyph = ui_font_glyph(face, codepoint);
145
+ return glyph ? glyph->advance : (face->lineHeight / 2);
146
+ }
147
+ int16_t adv = static_cast<int16_t>(ts ? ts : 2) * 6 + letterSpacing;
148
+ return adv > 0 ? static_cast<uint16_t>(adv) : 1;
149
+ }
150
+
151
+ static inline uint16_t ui_text_span_width(const char* start, const char* end, uint8_t ts, uint8_t fontFace, int8_t letterSpacing) {
152
+ if (!start || !end || end <= start) return 0;
153
+ uint16_t w = 0;
154
+ const unsigned char* p = (const unsigned char*)start;
155
+ const unsigned char* limit = (const unsigned char*)end;
156
+ while (p < limit && *p) {
157
+ uint16_t codepoint = ui_next_utf8_codepoint_bounded(&p, limit);
158
+ if (codepoint == 0) break;
159
+ w += ui_text_codepoint_advance(codepoint, ts, fontFace, letterSpacing);
160
+ }
161
+ return w;
162
+ }
163
+
164
+ struct UITextLine {
165
+ const char* start;
166
+ const char* end;
167
+ uint16_t width;
168
+ };
169
+
170
+ static inline uint8_t ui_text_next_line(const char** cursor, uint16_t maxWidth, uint8_t whiteSpaceMode, uint8_t ts, uint8_t fontFace, int8_t letterSpacing, UITextLine* out) {
171
+ if (!cursor || !*cursor || !out) return 0;
172
+ const char* p = *cursor;
173
+ if (!*p) return 0;
174
+ uint8_t hardNewlines = whiteSpaceMode == UI_WS_PRE || whiteSpaceMode == UI_WS_PRE_LINE;
175
+ uint8_t canWrap = (whiteSpaceMode == UI_WS_NORMAL || whiteSpaceMode == UI_WS_PRE_LINE) && maxWidth > 0;
176
+
177
+ if (whiteSpaceMode == UI_WS_NORMAL || whiteSpaceMode == UI_WS_PRE_LINE) {
178
+ p = ui_skip_wrap_spaces(p);
179
+ }
180
+ if (!*p) {
181
+ *cursor = p;
182
+ return 0;
183
+ }
184
+ if (hardNewlines && ui_is_text_newline(*p)) {
185
+ out->start = p;
186
+ out->end = p;
187
+ out->width = 0;
188
+ *cursor = ui_after_text_newline(p);
189
+ return 1;
190
+ }
191
+
192
+ const char* lineStart = p;
193
+ if (!canWrap) {
194
+ while (*p && !(hardNewlines && ui_is_text_newline(*p))) p++;
195
+ out->start = lineStart;
196
+ out->end = p;
197
+ out->width = ui_text_span_width(lineStart, p, ts, fontFace, letterSpacing);
198
+ *cursor = (hardNewlines && ui_is_text_newline(*p)) ? ui_after_text_newline(p) : p;
199
+ return 1;
200
+ }
201
+
202
+ uint16_t width = 0;
203
+ const char* lastBreakAfter = nullptr;
204
+ const char* lastBreakEnd = lineStart;
205
+ uint16_t lastBreakWidth = 0;
206
+ const char* lastNonSpaceEnd = lineStart;
207
+ uint16_t lastNonSpaceWidth = 0;
208
+
209
+ // Pre-compute string end bounds to avoid repeated scans in the loop
210
+ const unsigned char* textEnd = (const unsigned char*)p;
211
+ while (*textEnd) textEnd++;
212
+
213
+ while (*p && p < (const char*)textEnd) {
214
+ if (hardNewlines && ui_is_text_newline(*p)) break;
215
+ const char* charStart = p;
216
+ const unsigned char* next = (const unsigned char*)p;
217
+ uint16_t codepoint = ui_next_utf8_codepoint_bounded(&next, textEnd);
218
+ if (codepoint == 0) break;
219
+ const char* charEnd = (const char*)next;
220
+ uint8_t isBreakSpace = ui_is_text_space(*charStart) || (!hardNewlines && ui_is_text_newline(*charStart));
221
+ uint16_t adv = ui_text_codepoint_advance(codepoint, ts, fontFace, letterSpacing);
222
+
223
+ if (width > 0 && width + adv > maxWidth) {
224
+ if (lastBreakAfter && lastBreakAfter > lineStart) {
225
+ out->start = lineStart;
226
+ out->end = lastBreakEnd;
227
+ out->width = lastBreakWidth;
228
+ *cursor = lastBreakAfter;
229
+ return 1;
230
+ }
231
+ out->start = lineStart;
232
+ out->end = charStart;
233
+ out->width = width;
234
+ *cursor = charStart;
235
+ return 1;
236
+ }
237
+
238
+ width += adv;
239
+ p = charEnd;
240
+ if (isBreakSpace) {
241
+ lastBreakAfter = p;
242
+ lastBreakEnd = lastNonSpaceEnd;
243
+ lastBreakWidth = lastNonSpaceWidth;
244
+ } else {
245
+ lastNonSpaceEnd = p;
246
+ lastNonSpaceWidth = width;
247
+ }
248
+ }
249
+
250
+ out->start = lineStart;
251
+ if (whiteSpaceMode == UI_WS_NORMAL || whiteSpaceMode == UI_WS_PRE_LINE) {
252
+ out->end = lastNonSpaceEnd;
253
+ out->width = lastNonSpaceWidth;
254
+ } else {
255
+ out->end = p;
256
+ out->width = width;
257
+ }
258
+ *cursor = (hardNewlines && ui_is_text_newline(*p)) ? ui_after_text_newline(p) : p;
259
+ return 1;
260
+ }
261
+
262
+ static inline void ui_text_layout_metrics(const char* text, uint16_t maxWidth, uint8_t whiteSpaceMode, uint8_t ts, uint8_t fontFace, int8_t letterSpacing, uint8_t lineHeight, uint16_t* outW, uint16_t* outH) {
263
+ if (!text) text = "";
264
+ uint16_t maxLineW = 0;
265
+ uint16_t h = 0;
266
+ uint8_t lh = ui_text_line_height(ts, fontFace, lineHeight);
267
+ const char* cursor = text;
268
+ UITextLine line;
269
+ while (ui_text_next_line(&cursor, maxWidth, whiteSpaceMode, ts, fontFace, letterSpacing, &line)) {
270
+ if (line.width > maxLineW) maxLineW = line.width;
271
+ h += lh;
272
+ }
273
+ if (h == 0) h = lh;
274
+ if (outW) *outW = maxLineW;
275
+ if (outH) *outH = h;
276
+ }
277
+
278
+ static inline void ui_invalidate_text_layout_cache(uint16_t nodeIdx) {
279
+ if (nodeIdx < __ui_node_count) __ui_nodes[nodeIdx].layoutCacheKey = 0;
280
+ }
281
+
282
+ static inline uint32_t ui_text_layout_cache_key(uint16_t nodeIdx, uint16_t textMaxW) {
283
+ uint32_t key = textMaxW;
284
+ key = key * 31u + __ui_nodes[nodeIdx].whiteSpaceMode;
285
+ uint8_t ts = __ui_nodes[nodeIdx].textSize ? __ui_nodes[nodeIdx].textSize : 2;
286
+ key = key * 31u + ts;
287
+ key = key * 31u + __ui_nodes[nodeIdx].fontFace;
288
+ key = key * 31u + static_cast<uint8_t>(__ui_nodes[nodeIdx].letterSpacing + 128);
289
+ key = key * 31u + __ui_nodes[nodeIdx].lineHeight;
290
+ if (__ui_nodes[nodeIdx].hasTextBinding) {
291
+ const char* t = __ui_nodes[nodeIdx].textBuffer;
292
+ while (t && *t) {
293
+ key = key * 31u + static_cast<uint8_t>(*t);
294
+ t++;
295
+ }
296
+ }
297
+ return key | 1u;
298
+ }
299
+
300
+ static inline uint16_t ui_node_text_max_width(uint16_t nodeIdx) {
301
+ if (nodeIdx >= __ui_node_count) return 0;
302
+ uint16_t textMaxW = __ui_nodes[nodeIdx].box.w;
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
+ } else if (__ui_nodes[nodeIdx].kind == NODE_CHECK || __ui_nodes[nodeIdx].kind == NODE_RADIO) {
308
+ textMaxW = __ui_nodes[nodeIdx].box.w > 22 ? static_cast<uint16_t>(__ui_nodes[nodeIdx].box.w - 22) : 0;
309
+ }
310
+ return textMaxW;
311
+ }
312
+
313
+ static inline void ui_node_text_layout_metrics(uint16_t nodeIdx, uint16_t textMaxW, uint16_t* outW, uint16_t* outH) {
314
+ if (nodeIdx >= __ui_node_count) {
315
+ if (outW) *outW = 0;
316
+ if (outH) *outH = 0;
317
+ return;
318
+ }
319
+ uint32_t key = ui_text_layout_cache_key(nodeIdx, textMaxW);
320
+ if (__ui_nodes[nodeIdx].layoutCacheKey == key) {
321
+ if (outW) *outW = __ui_nodes[nodeIdx].layoutMetricsW;
322
+ if (outH) *outH = __ui_nodes[nodeIdx].layoutMetricsH;
323
+ return;
324
+ }
325
+ const char* displayText = __ui_nodes[nodeIdx].hasTextBinding
326
+ ? __ui_nodes[nodeIdx].textBuffer
327
+ : __ui_nodes[nodeIdx].text;
328
+ uint8_t ts = __ui_nodes[nodeIdx].textSize ? __ui_nodes[nodeIdx].textSize : 2;
329
+ uint16_t tw = 0;
330
+ uint16_t th = 0;
331
+ ui_text_layout_metrics(displayText, textMaxW, __ui_nodes[nodeIdx].whiteSpaceMode, ts,
332
+ __ui_nodes[nodeIdx].fontFace, __ui_nodes[nodeIdx].letterSpacing, __ui_nodes[nodeIdx].lineHeight, &tw, &th);
333
+ __ui_nodes[nodeIdx].layoutCacheKey = key;
334
+ __ui_nodes[nodeIdx].layoutMetricsW = tw;
335
+ __ui_nodes[nodeIdx].layoutMetricsH = th;
336
+ if (outW) *outW = tw;
337
+ if (outH) *outH = th;
338
+ }
339
+
340
+ static inline void ui_copy_text_span(const char* start, const char* end, char* out, uint8_t outSize) {
341
+ if (!out || outSize == 0) return;
342
+ uint8_t len = 0;
343
+ while (start && end && start < end && *start && len + 1 < outSize) {
344
+ out[len++] = *start++;
345
+ }
346
+ out[len] = 0;
347
+ }
348
+
349
+ static inline uint8_t ui_draw_asset_text(const char* text, int16_t x, int16_t y, UI_COLOR_T fg, UI_COLOR_T bg, uint8_t antialias, uint8_t fontFace) {
350
+ const UIFontFace* face = ui_font_face(fontFace);
351
+ if (!text || !face) return 0;
352
+ int16_t cursor = x;
353
+ int16_t baseline = y + face->baseline;
354
+ int16_t targetLeft, targetTop, targetRight, targetBottom;
355
+ ui_display_target_bounds(&targetLeft, &targetTop, &targetRight, &targetBottom);
356
+ const unsigned char* p = (const unsigned char*)text;
357
+ while (*p) {
358
+ uint16_t codepoint = ui_next_utf8_codepoint(&p);
359
+ const UIFontGlyph* glyph = ui_font_glyph(face, codepoint);
360
+ if (!glyph) {
361
+ cursor += face->lineHeight / 2;
362
+ continue;
363
+ }
364
+ int16_t glyphX = cursor + glyph->xOffset;
365
+ int16_t glyphY = baseline + glyph->yOffset;
366
+ if (glyphX + static_cast<int16_t>(glyph->width) <= targetLeft || glyphX >= targetRight ||
367
+ glyphY + static_cast<int16_t>(glyph->height) <= targetTop || glyphY >= targetBottom) {
368
+ cursor += glyph->advance;
369
+ continue;
370
+ }
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
+ for (int16_t gy = gyStart; gy < gyEnd; gy++) {
376
+ for (int16_t gx = gxStart; gx < gxEnd; gx++) {
377
+ uint16_t pixelIndex = static_cast<uint16_t>(gy) * glyph->width + static_cast<uint16_t>(gx);
378
+ uint8_t alpha = ui_font_alpha_at(face, glyph, pixelIndex);
379
+ if (alpha == 0) continue;
380
+ int16_t dx = glyphX + gx;
381
+ int16_t dy = glyphY + gy;
382
+ if (antialias) {
383
+ if (fg == bg) {
384
+ // Transparent mode: can't blend (fg==bg → all alphas become fg).
385
+ // Use a threshold so only high-coverage pixels draw, avoiding
386
+ // the bumpy look from flattening sub-pixel coverage to solid.
387
+ if (alpha >= 8) ui_display_draw_pixel(dx, dy, fg);
388
+ } else {
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
+ }
391
+ } else if (alpha >= 8) {
392
+ ui_display_draw_pixel(dx, dy, fg);
393
+ }
394
+ }
395
+ }
396
+ cursor += glyph->advance;
397
+ }
398
+ return 1;
399
+ }
400
+
401
+ static inline void ui_draw_bitmap_text(const char* text, int16_t x, int16_t y, UI_COLOR_T fg, UI_COLOR_T bg, uint8_t ts, int8_t letterSpacing) {
402
+ if (!text) text = "";
403
+ if (ts == 0) ts = 2;
404
+ ui_display_set_text_color(fg, bg);
405
+ ui_display_set_text_size(ts);
406
+ ui_display_set_text_wrap(false);
407
+ // Draw char-by-char to apply letterSpacing between glyphs.
408
+ if (letterSpacing == 0) {
409
+ ui_display_set_cursor(x, y);
410
+ ui_display_print(text);
411
+ } else {
412
+ int16_t cx = x;
413
+ char buf[2] = {0, 0};
414
+ for (const char* p = text; *p; p++) {
415
+ ui_display_set_cursor(cx, y);
416
+ buf[0] = *p;
417
+ ui_display_print(buf);
418
+ cx += ts * 6 + letterSpacing;
419
+ }
420
+ }
421
+ }
422
+
423
+ #ifdef UI_AA
424
+ static CuttlefishCanvas16* __ui_aa_canvas = nullptr;
425
+ static CuttlefishCanvas16* __ui_text_src_canvas = nullptr;
426
+ static CuttlefishCanvas16* __ui_text_dst_canvas = nullptr;
427
+
428
+ static inline CuttlefishCanvas16* ui_text_canvas(CuttlefishCanvas16** slot, int16_t w, int16_t h) {
429
+ if (w <= 0) w = 1;
430
+ if (h <= 0) h = 1;
431
+ if (!*slot || display_canvasWidth(*slot) < w || display_canvasHeight(*slot) < h) {
432
+ display_deleteCanvas(*slot);
433
+ *slot = display_createCanvas(w, h);
434
+ }
435
+ return *slot;
436
+ }
437
+
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
+ uint8_t count = 0;
440
+ for (int8_t dy = -static_cast<int8_t>(radius); dy <= static_cast<int8_t>(radius); dy++) {
441
+ int16_t yy = y + dy;
442
+ if (yy < 0 || yy >= h) continue;
443
+ for (int8_t dx = -static_cast<int8_t>(radius); dx <= static_cast<int8_t>(radius); dx++) {
444
+ int16_t xx = x + dx;
445
+ if (xx < 0 || xx >= w) continue;
446
+ if (display_canvasGetPixel(src, xx, yy) == fg) count++;
447
+ }
448
+ }
449
+ return count;
450
+ }
451
+
452
+ static inline uint8_t ui_text_aa_coverage(uint8_t neighbors, uint8_t outerNeighbors, uint8_t isFg, uint8_t ts) {
453
+ if (isFg) {
454
+ if (ts <= 1) return neighbors >= 4 ? 100 : 96;
455
+ if (ts == 2) return neighbors >= 8 ? 100 : neighbors >= 5 ? 96 : 92;
456
+ return neighbors >= 8 ? 100 : neighbors >= 6 ? 96 : neighbors >= 4 ? 90 : 84;
457
+ }
458
+ if (neighbors == 0) {
459
+ if (ts >= 3 && outerNeighbors > 0) {
460
+ uint8_t outer = outerNeighbors * 2;
461
+ return outer > 14 ? 14 : outer;
462
+ }
463
+ return 0;
464
+ }
465
+ uint8_t step = ts <= 1 ? 4 : ts == 2 ? 6 : 8;
466
+ uint8_t cap = ts <= 1 ? 18 : ts == 2 ? 28 : 38;
467
+ uint8_t coverage = neighbors * step;
468
+ return coverage > cap ? cap : coverage;
469
+ }
470
+
471
+ // ── Rasterized-glyph coverage cache (LVGL-style) ──────────────────────────
472
+ // The classic-font AA pass neighbor-counts a 3x3 (5x5 for large sizes) kernel
473
+ // per pixel of the whole line canvas on EVERY repaint — a 460px label at ts=2
474
+ // costs ~70k canvas reads per frame. Classic 5x7 glyph cells are position-
475
+ // independent (the 6ts cell's trailing gap column is blank and the sampling
476
+ // radius never crosses it into the next glyph's pixels except the final gap
477
+ // column's 1px outer halo, which lands on spacing), so per-(char, ts)
478
+ // coverage is cacheable. Direct-mapped, color-independent (coverage is a
479
+ // shape property; fg/bg enter only at blend time).
480
+ #ifndef UI_AA_GLYPH_CACHE_SLOTS
481
+ #if defined(__AVR__)
482
+ #define UI_AA_GLYPH_CACHE_SLOTS 0
483
+ #else
484
+ #define UI_AA_GLYPH_CACHE_SLOTS 48
485
+ #endif
486
+ #endif
487
+ #define UI_AA_GLYPH_CACHE_MAX_TS 3
488
+ #if UI_AA_GLYPH_CACHE_SLOTS > 0
489
+ struct UIGlyphCovEntry {
490
+ uint8_t ch;
491
+ uint8_t ts;
492
+ uint8_t valid;
493
+ uint8_t cov[(6 * UI_AA_GLYPH_CACHE_MAX_TS) * (8 * UI_AA_GLYPH_CACHE_MAX_TS + 1)];
494
+ };
495
+ static struct UIGlyphCovEntry __ui_glyph_cov[UI_AA_GLYPH_CACHE_SLOTS];
496
+ static CuttlefishCanvas16* __ui_glyph_src_canvas = nullptr;
497
+
498
+ // Compute (on miss) and return the cached coverage map for one glyph cell,
499
+ // or nullptr when the size exceeds the cached range. The cell is
500
+ // (6*ts) x (8*ts + 1) — the +1 bottom pad row matches the whole-line canvas
501
+ // so bottom-edge coverage is identical.
502
+ static const uint8_t* ui_aa_glyph_coverage(uint8_t ch, uint8_t ts) {
503
+ if (ts == 0U || ts > UI_AA_GLYPH_CACHE_MAX_TS) return nullptr;
504
+ uint16_t slot = static_cast<uint16_t>((static_cast<uint16_t>(ch) * 7U) + ts) % UI_AA_GLYPH_CACHE_SLOTS;
505
+ struct UIGlyphCovEntry* e = &__ui_glyph_cov[slot];
506
+ if (e->valid && e->ch == ch && e->ts == ts) return e->cov;
507
+ int16_t cw = static_cast<int16_t>(6 * ts);
508
+ int16_t chh = static_cast<int16_t>(8 * ts + 1);
509
+ if (cw <= 0 || chh <= 0) return nullptr;
510
+ if (!__ui_glyph_src_canvas || display_canvasWidth(__ui_glyph_src_canvas) < cw || display_canvasHeight(__ui_glyph_src_canvas) < chh) {
511
+ display_deleteCanvas(__ui_glyph_src_canvas);
512
+ __ui_glyph_src_canvas = display_createCanvas(cw, chh);
513
+ }
514
+ if (!__ui_glyph_src_canvas || !display_canvasBuffer(__ui_glyph_src_canvas)) return nullptr;
515
+ // Rasterize in black-on-white: coverage is shape-only (neighbor counts
516
+ // compare against the glyph color), independent of the draw-time colors.
517
+ const UI_COLOR_T cellFg = 0x0000;
518
+ const UI_COLOR_T cellBg = 0xFFFF;
519
+ display_canvasFillRect(__ui_glyph_src_canvas, 0, 0, cw, chh, cellBg);
520
+ display_targetSetCursor((CuttlefishDisplayTarget*)__ui_glyph_src_canvas, 0, 0);
521
+ display_targetSetTextColorBg((CuttlefishDisplayTarget*)__ui_glyph_src_canvas, cellFg, cellBg);
522
+ display_targetSetTextSize((CuttlefishDisplayTarget*)__ui_glyph_src_canvas, ts);
523
+ display_targetSetTextWrap((CuttlefishDisplayTarget*)__ui_glyph_src_canvas, false);
524
+ char buf[2] = { static_cast<char>(ch), 0 };
525
+ display_targetPrint((CuttlefishDisplayTarget*)__ui_glyph_src_canvas, buf);
526
+ uint16_t i = 0;
527
+ for (int16_t yy = 0; yy < chh; yy++) {
528
+ for (int16_t xx = 0; xx < cw; xx++) {
529
+ UI_COLOR_T px = display_canvasGetPixel(__ui_glyph_src_canvas, xx, yy);
530
+ uint8_t neighbors = ui_text_fg_neighbors(__ui_glyph_src_canvas, xx, yy, cw, chh, cellFg);
531
+ uint8_t outerNeighbors = 0;
532
+ if (ts >= 3U && px != cellFg && neighbors == 0U) {
533
+ outerNeighbors = ui_text_fg_neighbors(__ui_glyph_src_canvas, xx, yy, cw, chh, cellFg, 2);
534
+ }
535
+ e->cov[i++] = ui_text_aa_coverage(neighbors, outerNeighbors, px == cellFg ? 1U : 0U, ts);
536
+ }
537
+ }
538
+ e->ch = ch;
539
+ e->ts = ts;
540
+ e->valid = 1U;
541
+ return e->cov;
542
+ }
543
+ #else
544
+ static const uint8_t* ui_aa_glyph_coverage(uint8_t /*ch*/, uint8_t /*ts*/) { return nullptr; }
545
+ #endif
546
+
547
+ static inline void ui_draw_aa_text(const char* text, int16_t x, int16_t y, UI_COLOR_T fg, UI_COLOR_T bg, uint8_t ts) {
548
+ if (!text || !*text) return;
549
+ if (ts == 0) ts = 2;
550
+ uint16_t w = ui_text_width(text, ts, 0, 0);
551
+ // Add 1px bottom padding so the AA edge-detection sampling doesn't clip the
552
+ // glyph bottoms (the neighbor-count at the last row rounds partial coverage
553
+ // to 0 without this clearance).
554
+ uint8_t h = ui_text_height(ts, 0) + 1;
555
+ if (w == 0 || h == 0 || fg == bg) {
556
+ ui_draw_bitmap_text(text, x, y, fg, bg, ts, 0);
557
+ return;
558
+ }
559
+ int16_t clipX = x;
560
+ int16_t clipY = y;
561
+ int16_t clipW = static_cast<int16_t>(w);
562
+ int16_t clipH = static_cast<int16_t>(h);
563
+ if (!ui_clip_rect_to_display_target(&clipX, &clipY, &clipW, &clipH)) return;
564
+ int16_t localX = static_cast<int16_t>(clipX - x);
565
+ int16_t localY = static_cast<int16_t>(clipY - y);
566
+
567
+ // Fast path (rasterized-glyph cache): when every glyph's coverage is
568
+ // cached, blend per-cell straight into dst — no line-canvas rasterization,
569
+ // no neighbor sampling. Identical coverage per pixel except the 1px outer
570
+ // halo the whole-line kernel lands on the final gap column before a glyph
571
+ // start (spacing-only; sub-perceptual).
572
+ {
573
+ const uint8_t* firstCov = ui_aa_glyph_coverage(static_cast<uint8_t>(text[0]), ts);
574
+ if (firstCov != nullptr) {
575
+ CuttlefishCanvas16* dst = ui_text_canvas(&__ui_text_dst_canvas, static_cast<int16_t>(w), static_cast<int16_t>(h));
576
+ if (dst && display_canvasBuffer(dst)) {
577
+ display_canvasFillRect(dst, localX, localY, clipW, clipH, bg);
578
+ int16_t cellW = static_cast<int16_t>(6 * ts);
579
+ int16_t cellH = static_cast<int16_t>(8 * ts + 1);
580
+ int16_t cellX = 0;
581
+ for (const char* p2 = text; *p2; p2++) {
582
+ const uint8_t* cov = (p2 == text) ? firstCov : ui_aa_glyph_coverage(static_cast<uint8_t>(*p2), ts);
583
+ if (!cov) break; // size fell out of the cached range mid-string
584
+ int16_t sx0 = cellX < localX ? static_cast<int16_t>(localX - cellX) : 0;
585
+ int16_t sy0 = localY;
586
+ int16_t sx1 = static_cast<int16_t>(cellX + cellW < localX + clipW ? cellX + cellW : localX + clipW);
587
+ int16_t sy1 = static_cast<int16_t>(localY + clipH < cellH ? localY + clipH : cellH);
588
+ for (int16_t yy = sy0; yy < sy1; yy++) {
589
+ for (int16_t xx = static_cast<int16_t>(cellX + sx0); xx < sx1; xx++) {
590
+ uint8_t coverage = cov[static_cast<uint16_t>(yy) * static_cast<uint16_t>(cellW) + static_cast<uint16_t>(xx - cellX)];
591
+ display_targetDrawPixel((CuttlefishDisplayTarget*)dst, xx, yy,
592
+ coverage == 0U ? bg : ui_blend(fg, bg, coverage));
593
+ }
594
+ }
595
+ cellX += cellW;
596
+ if (cellX >= localX + clipW) break;
597
+ }
598
+ int16_t stride = display_canvasWidth(dst);
599
+ UI_COLOR_T* pixels = display_canvasBuffer(dst);
600
+ for (int16_t row = 0; row < clipH; row++) {
601
+ ui_display_draw_rgb_bitmap(clipX, static_cast<int16_t>(clipY + row),
602
+ pixels + static_cast<int32_t>(localY + row) * stride + localX, clipW, 1);
603
+ }
604
+ return;
605
+ }
606
+ }
607
+ }
608
+
609
+ // Use pre-allocated static canvases (no dynamic allocation)
610
+ CuttlefishCanvas16* src = ui_text_canvas(&__ui_text_src_canvas, static_cast<int16_t>(w), static_cast<int16_t>(h));
611
+ CuttlefishCanvas16* dst = ui_text_canvas(&__ui_text_dst_canvas, static_cast<int16_t>(w), static_cast<int16_t>(h));
612
+ if (!src || !dst || !display_canvasBuffer(src) || !display_canvasBuffer(dst)) {
613
+ ui_draw_bitmap_text(text, x, y, fg, bg, ts, 0);
614
+ return;
615
+ }
616
+
617
+ display_canvasFillRect(src, 0, 0, w, h, bg);
618
+ display_canvasFillRect(dst, localX, localY, clipW, clipH, bg);
619
+ display_targetSetCursor((CuttlefishDisplayTarget*)src, 0, 0);
620
+ display_targetSetTextColorBg((CuttlefishDisplayTarget*)src, fg, bg);
621
+ display_targetSetTextSize((CuttlefishDisplayTarget*)src, ts);
622
+ display_targetSetTextWrap((CuttlefishDisplayTarget*)src, false);
623
+ display_targetPrint((CuttlefishDisplayTarget*)src, text);
624
+
625
+ for (int16_t yy = localY; yy < localY + clipH; yy++) {
626
+ for (int16_t xx = localX; xx < localX + clipW; xx++) {
627
+ UI_COLOR_T px = display_canvasGetPixel(src, xx, yy);
628
+ uint8_t neighbors = ui_text_fg_neighbors(src, xx, yy, static_cast<int16_t>(w), h, fg);
629
+ uint8_t outerNeighbors = 0;
630
+ if (ts >= 3 && px != fg && neighbors == 0) {
631
+ outerNeighbors = ui_text_fg_neighbors(src, xx, yy, static_cast<int16_t>(w), h, fg, 2);
632
+ }
633
+ uint8_t coverage = ui_text_aa_coverage(neighbors, outerNeighbors, px == fg ? 1 : 0, ts);
634
+ display_targetDrawPixel((CuttlefishDisplayTarget*)dst, xx, yy, coverage == 0 ? bg : ui_blend(fg, bg, coverage));
635
+ }
636
+ }
637
+
638
+ int16_t stride = display_canvasWidth(dst);
639
+ UI_COLOR_T* pixels = display_canvasBuffer(dst);
640
+ for (int16_t row = 0; row < clipH; row++) {
641
+ ui_display_draw_rgb_bitmap(clipX, static_cast<int16_t>(clipY + row),
642
+ pixels + static_cast<int32_t>(localY + row) * stride + localX, clipW, 1);
643
+ }
644
+ }
645
+
646
+ static inline void ui_draw_text(const char* text, int16_t x, int16_t y, UI_COLOR_T fg, UI_COLOR_T bg, uint8_t ts, uint8_t antialias, uint8_t fontFace, int8_t letterSpacing) {
647
+ if (fontFace && ui_draw_asset_text(text, x, y, fg, bg, antialias, fontFace)) {
648
+ return;
649
+ }
650
+ // Only use AA when fg != bg (opaque background). When fg == bg (transparent
651
+ // mode), the AA source canvas fills entirely with fg — no edges to detect,
652
+ // producing a solid rectangle instead of text.
653
+ if (antialias && fg != bg && letterSpacing == 0) {
654
+ ui_draw_aa_text(text, x, y, fg, bg, ts);
655
+ return;
656
+ }
657
+ ui_draw_bitmap_text(text, x, y, fg, bg, ts, letterSpacing);
658
+ }
659
+ #else
660
+ static inline void ui_draw_text(const char* text, int16_t x, int16_t y, UI_COLOR_T fg, UI_COLOR_T bg, uint8_t ts, uint8_t antialias, uint8_t fontFace, int8_t letterSpacing) {
661
+ (void)antialias;
662
+ if (fontFace && ui_draw_asset_text(text, x, y, fg, bg, antialias, fontFace)) {
663
+ return;
664
+ }
665
+ ui_draw_bitmap_text(text, x, y, fg, bg, ts, letterSpacing);
666
+ }
667
+ #endif
668
+
669
+ // Draw classic list rows without routing through CuttlefishGFX::print(). The
670
+ // Zephyr native target uses CuttlefishGFX for both the panel and RGB565 canvas;
671
+ // its virtual text path is reliable on the panel but can be lost when the
672
+ // canvas is later copied to the panel. Writing the same 5x7 glyphs through the
673
+ // normal pixel/fill shims keeps the row pixels in the canvas buffer and works
674
+ // for both cached-list and band-list rendering. Other adapters retain the
675
+ // existing ui_draw_text fallback because they provide their own canvas text
676
+ // implementation.
677
+ static inline void ui_draw_list_text(const char* text, int16_t x, int16_t y,
678
+ UI_COLOR_T fg, UI_COLOR_T bg, uint8_t ts,
679
+ uint8_t fontFace, int8_t letterSpacing) {
680
+ #if defined(CUTTLEFISH_GFX_DEFINED)
681
+ if (fontFace == 0) {
682
+ if (!text) text = "";
683
+ if (ts == 0) ts = 2;
684
+ int16_t cx = x;
685
+ for (const unsigned char* p = (const unsigned char*)text; *p; p++) {
686
+ uint8_t ch = *p;
687
+ // CuttlefishGFX's classic font is the full 256-glyph Adafruit table.
688
+ for (uint8_t col = 0; col < 5; col++) {
689
+ uint8_t bits = cuttlefish_glcdfont[static_cast<uint16_t>(ch) * 5u + col];
690
+ for (uint8_t row = 0; row < 7; row++) {
691
+ if (bits & static_cast<uint8_t>(1u << row)) {
692
+ ui_display_fill_rect(static_cast<int16_t>(cx + static_cast<int16_t>(col) * ts),
693
+ static_cast<int16_t>(y + static_cast<int16_t>(row) * ts), ts, ts, fg);
694
+ }
695
+ }
696
+ }
697
+ // Match the classic GFX advance: five columns plus one blank column.
698
+ cx = static_cast<int16_t>(cx + static_cast<int16_t>(ts) * 6 + letterSpacing);
699
+ }
700
+ return;
701
+ }
702
+ #endif
703
+ ui_draw_text(text, x, y, fg, bg, ts, 0, fontFace, letterSpacing);
704
+ }
705
+
706
+ // Direct-panel list fallback. CuttlefishGFX's bitmap glyph path above is
707
+ // intentionally used for RAM canvases, but a few native panel adapters only
708
+ // commit their text state correctly through print()/drawChar(). Use that
709
+ // established path when the list is painted straight to the panel; the caller
710
+ // keeps the whole list inside one write transaction, so this does not expose a
711
+ // partially composed row set.
712
+ static inline void ui_draw_list_text_direct(const char* text, int16_t x, int16_t y,
713
+ UI_COLOR_T fg, UI_COLOR_T bg, uint8_t ts,
714
+ uint8_t fontFace, int8_t letterSpacing) {
715
+ if (fontFace == 0) {
716
+ ui_draw_bitmap_text(text, x, y, fg, bg, ts, letterSpacing);
717
+ } else {
718
+ ui_draw_text(text, x, y, fg, bg, ts, 0, fontFace, letterSpacing);
719
+ }
720
+ }
721
+
722
+ /** Truncate a NUL-terminated buffer in place to fit within maxWidth (px) and
723
+ * append "...". Used by text-overflow: ellipsis. Adafruit_GFX has no ellipsis
724
+ * glyph, so three ASCII dots approximate it. */
725
+ static inline void ui_truncate_ellipsis(char* buf, uint8_t bufSize, uint16_t maxWidth,
726
+ uint8_t ts, uint8_t fontFace, int8_t letterSpacing) {
727
+ if (!buf || bufSize == 0) return;
728
+ uint8_t len = static_cast<uint8_t>(strlen(buf));
729
+ // Reserve space for the three trailing dots.
730
+ uint16_t dotsW = static_cast<uint16_t>(3) * ui_text_codepoint_advance(static_cast<uint16_t>('.'), ts, fontFace, letterSpacing);
731
+ int16_t budget = static_cast<int16_t>(maxWidth) - static_cast<int16_t>(dotsW);
732
+ if (budget <= 0) { if (bufSize > 3) { buf[0]='.'; buf[1]='.'; buf[2]='.'; buf[3]=0; } return; }
733
+ // Measure once, then remove one UTF-8 codepoint at a time from the end.
734
+ // The previous prefix-- loop rescanned the whole prefix on every iteration
735
+ // (O(n²)); this keeps truncation linear for long bound strings.
736
+ uint8_t prefix = len;
737
+ uint16_t prefixW = ui_text_span_width(buf, buf + prefix, ts, fontFace, letterSpacing);
738
+ while (prefix > 0 && static_cast<int16_t>(prefixW) > budget) {
739
+ uint8_t cut = static_cast<uint8_t>(prefix - 1);
740
+ while (cut > 0 && (static_cast<uint8_t>(buf[cut]) & 0xC0) == 0x80) cut--;
741
+ uint16_t removed = ui_text_span_width(buf + cut, buf + prefix, ts, fontFace, letterSpacing);
742
+ prefixW = prefixW > removed ? static_cast<uint16_t>(prefixW - removed) : 0;
743
+ prefix = cut;
744
+ }
745
+ if (prefix + 3 < bufSize) {
746
+ buf[prefix] = '.'; buf[prefix+1] = '.'; buf[prefix+2] = '.'; buf[prefix+3] = 0;
747
+ } else if (bufSize > 3) {
748
+ buf[0]='.'; buf[1]='.'; buf[2]='.'; buf[3]=0;
749
+ }
750
+ }
751
+
752
+ // text-overflow: clip — trim trailing chars until the prefix fits maxWidth,
753
+ // then NUL-terminate. No trailing dots (contrast with ui_truncate_ellipsis).
754
+ static inline void ui_truncate_clip(char* buf, uint8_t bufSize, uint16_t maxWidth,
755
+ uint8_t ts, uint8_t fontFace, int8_t letterSpacing) {
756
+ if (!buf || bufSize == 0) return;
757
+ uint8_t len = static_cast<uint8_t>(strlen(buf));
758
+ uint8_t prefix = len;
759
+ uint16_t prefixW = ui_text_span_width(buf, buf + prefix, ts, fontFace, letterSpacing);
760
+ while (prefix > 0 && static_cast<int16_t>(prefixW) > static_cast<int16_t>(maxWidth)) {
761
+ uint8_t cut = static_cast<uint8_t>(prefix - 1);
762
+ while (cut > 0 && (static_cast<uint8_t>(buf[cut]) & 0xC0) == 0x80) cut--;
763
+ uint16_t removed = ui_text_span_width(buf + cut, buf + prefix, ts, fontFace, letterSpacing);
764
+ prefixW = prefixW > removed ? static_cast<uint16_t>(prefixW - removed) : 0;
765
+ prefix = cut;
766
+ }
767
+ if (prefix < bufSize) buf[prefix] = 0;
768
+ }
769
+
770
+ static inline void ui_draw_wrapped_text(const char* text, int16_t x, int16_t y, uint16_t maxWidth, UI_COLOR_T fg, UI_COLOR_T bg,
771
+ uint8_t ts, uint8_t antialias, uint8_t fontFace, int8_t letterSpacing,
772
+ uint8_t lineHeight, uint8_t whiteSpaceMode, uint8_t textAlign, uint8_t underline, uint8_t textOverflow) {
773
+ if (!text) text = "";
774
+ uint8_t lh = ui_text_line_height(ts, fontFace, lineHeight);
775
+ const char* cursor = text;
776
+ int16_t lineY = y;
777
+ UITextLine line;
778
+ char lineBuf[UI_TEXT_LINE_BUF];
779
+ int16_t targetLeft, targetTop, targetRight, targetBottom;
780
+ ui_display_target_bounds(&targetLeft, &targetTop, &targetRight, &targetBottom);
781
+ while (ui_text_next_line(&cursor, maxWidth, whiteSpaceMode, ts, fontFace, letterSpacing, &line)) {
782
+ int16_t lineBottom = static_cast<int16_t>(lineY + lh);
783
+ if (lineBottom <= targetTop || lineY >= targetBottom) {
784
+ lineY += lh;
785
+ continue;
786
+ }
787
+ int16_t lineX = x;
788
+ if (textAlign == 1) lineX = x + (static_cast<int16_t>(maxWidth) - static_cast<int16_t>(line.width)) / 2;
789
+ else if (textAlign == 2) lineX = x + static_cast<int16_t>(maxWidth) - static_cast<int16_t>(line.width);
790
+ if (lineX + static_cast<int16_t>(line.width) <= targetLeft || lineX >= targetRight) {
791
+ lineY += lh;
792
+ continue;
793
+ }
794
+ ui_copy_text_span(line.start, line.end, lineBuf, UI_TEXT_LINE_BUF);
795
+ // text-overflow — only applies when the line is wider than maxWidth.
796
+ // textOverflow==1 (ellipsis): trim the span and append "...".
797
+ // textOverflow==0 (clip): trim the span to the edge, no dots.
798
+ if (static_cast<int16_t>(line.width) > static_cast<int16_t>(maxWidth)) {
799
+ if (textOverflow) ui_truncate_ellipsis(lineBuf, UI_TEXT_LINE_BUF, maxWidth, ts, fontFace, letterSpacing);
800
+ else ui_truncate_clip(lineBuf, UI_TEXT_LINE_BUF, maxWidth, ts, fontFace, letterSpacing);
801
+ }
802
+ ui_draw_text(lineBuf, lineX, lineY, fg, bg, ts, antialias, fontFace, letterSpacing);
803
+ // text-decoration (underline=bit0, strikethrough=bit1)
804
+ if (underline & 1) ui_display_draw_fast_hline(lineX, lineY + ui_text_height(ts, fontFace) - 1, line.width, fg);
805
+ if (underline & 2) ui_display_draw_fast_hline(lineX, lineY + ui_text_height(ts, fontFace) / 2, line.width, fg);
806
+ lineY += lh;
807
+ }
808
+ }
809
+
810
+ // Draw a rich-text node from its precomputed run/segment/line geometry. Does
811
+ // NOT re-wrap — the geometry was baked at transpile time (runs are static-only,
812
+ // so the text never changes at runtime). Iterate segments once, skip lines and
813
+ // segments outside the active draw target, and compute each segment's line
814
+ // origin from textAlign + line width. Mixed font sizes align on the line's
815
+ // baseline (each segment's top = baseline − its own ascent).
816
+ static inline void ui_draw_rich_text(uint16_t nodeIdx, int16_t x, int16_t y, UI_COLOR_T bg, uint8_t antialias,
817
+ uint8_t useFgOverride = 0, UI_COLOR_T fgOverride = 0, uint16_t maxWidth = 0) {
818
+ UINode* n = &__ui_nodes[nodeIdx];
819
+ uint16_t alignWidth = maxWidth ? maxWidth : n->box.w;
820
+ int16_t targetLeft = static_cast<int16_t>(-__ui_draw_off_x);
821
+ int16_t targetTop = static_cast<int16_t>(-__ui_draw_off_y);
822
+ int16_t targetRight = static_cast<int16_t>(ui_display_target_width() - __ui_draw_off_x);
823
+ int16_t targetBottom = static_cast<int16_t>(ui_display_target_height() - __ui_draw_off_y);
824
+ uint16_t richSegEnd = static_cast<uint16_t>(n->richSegStart + n->richSegCount);
825
+ for (uint16_t si = n->richSegStart; si < richSegEnd; si++) {
826
+ UIRichSeg* seg = &__ui_rich_segs[si];
827
+ if (seg->line >= n->richLineCount) continue;
828
+ UIRichLine* line = &__ui_rich_lines[n->richLineStart + seg->line];
829
+ int16_t lineTop = y + line->y;
830
+ int16_t lineBottom = static_cast<int16_t>(lineTop + static_cast<int16_t>(line->h));
831
+ if (lineBottom <= targetTop || lineTop >= targetBottom) continue;
832
+ int16_t lineX = x;
833
+ if (n->textAlign == 1) lineX = x + (static_cast<int16_t>(alignWidth) - static_cast<int16_t>(line->w)) / 2;
834
+ else if (n->textAlign == 2) lineX = x + static_cast<int16_t>(alignWidth) - static_cast<int16_t>(line->w);
835
+ int16_t segX = static_cast<int16_t>(lineX + seg->x);
836
+ int16_t segRight = static_cast<int16_t>(segX + static_cast<int16_t>(seg->w));
837
+ if (segRight <= targetLeft || segX >= targetRight) continue;
838
+ UIRichRun* run = &__ui_runs[n->runStart + seg->runIndex];
839
+ // Baseline alignment: for asset fonts, the ascent is the font face's
840
+ // baseline (baked per px size); for the bitmap font (fontFace=0), it's the
841
+ // 5x8 glyph ascent (7px × textSize). Using the wrong ascent misaligns runs
842
+ // vertically and makes mixed rich-text lines look garbled.
843
+ int16_t ascent;
844
+ if (run->fontFace) {
845
+ const UIFontFace* face = ui_font_face(run->fontFace);
846
+ ascent = face ? static_cast<int16_t>(face->baseline) : (7 * static_cast<int16_t>(run->textSize));
847
+ } else {
848
+ ascent = 7 * static_cast<int16_t>(run->textSize);
849
+ }
850
+ int16_t segY = y + line->baseline - ascent;
851
+ UI_COLOR_T fg = useFgOverride ? fgOverride : run->fg;
852
+ ui_draw_text(seg->text, segX, segY, fg, bg, run->textSize, antialias, run->fontFace, run->letterSpacing);
853
+ if (run->underline & 1) ui_display_draw_fast_hline(segX, segY + 8 * run->textSize - 1, seg->w, fg);
854
+ if (run->underline & 2) ui_display_draw_fast_hline(segX, segY + 4 * run->textSize, seg->w, fg);
855
+ }
856
+ }
857
+
858
+ // Hit-test a tap point (in node-local coordinates) against a rich-text node's
859
+ // link runs. Returns the link run's resolved screen index, or -1 if the point
860
+ // doesn't land on a link segment. Mirrors the list-item subdivision precedent
861
+ // but uses measured segment rects instead of fixed row heights.
862
+ static inline int8_t ui_rich_link_hit(uint16_t nodeIdx, int16_t px, int16_t py) {
863
+ UINode* n = &__ui_nodes[nodeIdx];
864
+ int16_t insetL = static_cast<int16_t>(n->borderWidth) + static_cast<int16_t>(n->paddingLeft);
865
+ int16_t insetR = static_cast<int16_t>(n->borderWidth) + static_cast<int16_t>(n->paddingRight);
866
+ int16_t insetT = static_cast<int16_t>(n->borderWidth) + static_cast<int16_t>(n->paddingTop);
867
+ int16_t localX = px - insetL;
868
+ int16_t localY = py - insetT;
869
+ uint16_t alignWidth = n->box.w > static_cast<uint16_t>(insetL + insetR)
870
+ ? static_cast<uint16_t>(static_cast<int16_t>(n->box.w) - insetL - insetR)
871
+ : 0;
872
+ for (uint16_t si = n->richSegStart; si < n->richSegStart + n->richSegCount; si++) {
873
+ UIRichSeg* seg = &__ui_rich_segs[si];
874
+ UIRichRun* run = &__ui_runs[n->runStart + seg->runIndex];
875
+ if (run->linkTarget < 0) continue;
876
+ UIRichLine* line = &__ui_rich_lines[n->richLineStart + seg->line];
877
+ // Segment x is relative to its line's left edge (pre-alignment). For the
878
+ // hit-test, account for center/right alignment the same way draw does.
879
+ int16_t originX = 0;
880
+ if (n->textAlign == 1) originX = (static_cast<int16_t>(alignWidth) - static_cast<int16_t>(line->w)) / 2;
881
+ else if (n->textAlign == 2) originX = static_cast<int16_t>(alignWidth) - static_cast<int16_t>(line->w);
882
+ int16_t sx = originX + seg->x;
883
+ int16_t sy = line->y;
884
+ if (localX >= sx && localX < sx + static_cast<int16_t>(seg->w) && localY >= sy && localY < sy + static_cast<int16_t>(line->h)) {
885
+ return run->linkTarget;
886
+ }
887
+ }
888
+ return -1;
889
+ }
890
+
891
+ // Draw shadows for an element. Loops over up to 4 shadow specs. Outset shadows
892
+ // are drawn behind the element; inset shadows are drawn over the element fill.
893
893
  // Draw a gradient fill for an element. Replaces solid fillRect/fillRoundRect.`;
894
894
  }