@typecad/ui 0.1.0-alpha.1

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 (183) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1443 -0
  3. package/dist/engine-index.d.ts +7 -0
  4. package/dist/engine-index.js +41 -0
  5. package/dist/index.d.ts +165 -0
  6. package/dist/index.js +42 -0
  7. package/dist/preview/build-program.d.ts +7 -0
  8. package/dist/preview/build-program.js +665 -0
  9. package/dist/preview/host-gfx.d.ts +90 -0
  10. package/dist/preview/host-gfx.js +552 -0
  11. package/dist/preview/host-ui-runtime.d.ts +241 -0
  12. package/dist/preview/host-ui-runtime.js +3236 -0
  13. package/dist/preview/types.d.ts +83 -0
  14. package/dist/preview/types.js +1 -0
  15. package/dist/types.d.ts +108 -0
  16. package/dist/types.js +10 -0
  17. package/dist/ui-engine/block-layout.d.ts +8 -0
  18. package/dist/ui-engine/block-layout.js +95 -0
  19. package/dist/ui-engine/color.d.ts +49 -0
  20. package/dist/ui-engine/color.js +273 -0
  21. package/dist/ui-engine/css-parser.d.ts +155 -0
  22. package/dist/ui-engine/css-parser.js +1058 -0
  23. package/dist/ui-engine/default-keyboards.d.ts +3 -0
  24. package/dist/ui-engine/default-keyboards.js +53 -0
  25. package/dist/ui-engine/easing.d.ts +12 -0
  26. package/dist/ui-engine/easing.js +76 -0
  27. package/dist/ui-engine/font-assets.d.ts +50 -0
  28. package/dist/ui-engine/font-assets.js +431 -0
  29. package/dist/ui-engine/html-parser.d.ts +116 -0
  30. package/dist/ui-engine/html-parser.js +284 -0
  31. package/dist/ui-engine/image-assets.d.ts +21 -0
  32. package/dist/ui-engine/image-assets.js +125 -0
  33. package/dist/ui-engine/inline-parser.d.ts +30 -0
  34. package/dist/ui-engine/inline-parser.js +66 -0
  35. package/dist/ui-engine/keyframes.d.ts +3 -0
  36. package/dist/ui-engine/keyframes.js +39 -0
  37. package/dist/ui-engine/layout-engine.d.ts +35 -0
  38. package/dist/ui-engine/layout-engine.js +203 -0
  39. package/dist/ui-engine/model.d.ts +239 -0
  40. package/dist/ui-engine/model.js +987 -0
  41. package/dist/ui-engine/rich-layout.d.ts +51 -0
  42. package/dist/ui-engine/rich-layout.js +220 -0
  43. package/dist/ui-engine/run-types.d.ts +13 -0
  44. package/dist/ui-engine/run-types.js +1 -0
  45. package/dist/ui-engine/runtime-header/antialiasing.d.ts +1 -0
  46. package/dist/ui-engine/runtime-header/antialiasing.js +141 -0
  47. package/dist/ui-engine/runtime-header/blend-bodies.d.ts +1 -0
  48. package/dist/ui-engine/runtime-header/blend-bodies.js +39 -0
  49. package/dist/ui-engine/runtime-header/canvas-helpers.d.ts +1 -0
  50. package/dist/ui-engine/runtime-header/canvas-helpers.js +155 -0
  51. package/dist/ui-engine/runtime-header/canvas-scrollbar.d.ts +1 -0
  52. package/dist/ui-engine/runtime-header/canvas-scrollbar.js +105 -0
  53. package/dist/ui-engine/runtime-header/color-mono-refresh.d.ts +1 -0
  54. package/dist/ui-engine/runtime-header/color-mono-refresh.js +154 -0
  55. package/dist/ui-engine/runtime-header/dirty-scroll-mutators.d.ts +1 -0
  56. package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +169 -0
  57. package/dist/ui-engine/runtime-header/display-shim.d.ts +1 -0
  58. package/dist/ui-engine/runtime-header/display-shim.js +104 -0
  59. package/dist/ui-engine/runtime-header/forward-decls.d.ts +1 -0
  60. package/dist/ui-engine/runtime-header/forward-decls.js +137 -0
  61. package/dist/ui-engine/runtime-header/guard-close.d.ts +1 -0
  62. package/dist/ui-engine/runtime-header/guard-close.js +8 -0
  63. package/dist/ui-engine/runtime-header/image-drawing.d.ts +1 -0
  64. package/dist/ui-engine/runtime-header/image-drawing.js +300 -0
  65. package/dist/ui-engine/runtime-header/init-press-input.d.ts +1 -0
  66. package/dist/ui-engine/runtime-header/init-press-input.js +126 -0
  67. package/dist/ui-engine/runtime-header/keyboard.d.ts +1 -0
  68. package/dist/ui-engine/runtime-header/keyboard.js +342 -0
  69. package/dist/ui-engine/runtime-header/node-decoration.d.ts +1 -0
  70. package/dist/ui-engine/runtime-header/node-decoration.js +212 -0
  71. package/dist/ui-engine/runtime-header/paint-order-coords.d.ts +1 -0
  72. package/dist/ui-engine/runtime-header/paint-order-coords.js +190 -0
  73. package/dist/ui-engine/runtime-header/paint-rects-repair.d.ts +1 -0
  74. package/dist/ui-engine/runtime-header/paint-rects-repair.js +553 -0
  75. package/dist/ui-engine/runtime-header/scroll-physics.d.ts +1 -0
  76. package/dist/ui-engine/runtime-header/scroll-physics.js +161 -0
  77. package/dist/ui-engine/runtime-header/state-bindings-nav.d.ts +1 -0
  78. package/dist/ui-engine/runtime-header/state-bindings-nav.js +176 -0
  79. package/dist/ui-engine/runtime-header/structs.d.ts +1 -0
  80. package/dist/ui-engine/runtime-header/structs.js +220 -0
  81. package/dist/ui-engine/runtime-header/text-rendering.d.ts +1 -0
  82. package/dist/ui-engine/runtime-header/text-rendering.js +715 -0
  83. package/dist/ui-engine/runtime-header/tick/bindings-phase.d.ts +1 -0
  84. package/dist/ui-engine/runtime-header/tick/bindings-phase.js +103 -0
  85. package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.d.ts +1 -0
  86. package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +1051 -0
  87. package/dist/ui-engine/runtime-header/tick/flush-phase.d.ts +1 -0
  88. package/dist/ui-engine/runtime-header/tick/flush-phase.js +17 -0
  89. package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.d.ts +1 -0
  90. package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +15 -0
  91. package/dist/ui-engine/runtime-header/tick/transitions-phase.d.ts +1 -0
  92. package/dist/ui-engine/runtime-header/tick/transitions-phase.js +213 -0
  93. package/dist/ui-engine/runtime-header/tick-motion-helpers.d.ts +1 -0
  94. package/dist/ui-engine/runtime-header/tick-motion-helpers.js +26 -0
  95. package/dist/ui-engine/runtime-header/tick.d.ts +1 -0
  96. package/dist/ui-engine/runtime-header/tick.js +17 -0
  97. package/dist/ui-engine/runtime-header/touch-keyboard-fwd.d.ts +1 -0
  98. package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +406 -0
  99. package/dist/ui-engine/runtime-header/types-defines.d.ts +1 -0
  100. package/dist/ui-engine/runtime-header/types-defines.js +79 -0
  101. package/dist/ui-engine/runtime-header.d.ts +1 -0
  102. package/dist/ui-engine/runtime-header.js +69 -0
  103. package/dist/ui-engine/scroll-memory-diagnostics.d.ts +16 -0
  104. package/dist/ui-engine/scroll-memory-diagnostics.js +58 -0
  105. package/dist/ui-engine/select-engine.d.ts +4 -0
  106. package/dist/ui-engine/select-engine.js +19 -0
  107. package/dist/ui-engine/style-resolver.d.ts +77 -0
  108. package/dist/ui-engine/style-resolver.js +341 -0
  109. package/dist/ui-engine/text-layout.d.ts +19 -0
  110. package/dist/ui-engine/text-layout.js +92 -0
  111. package/dist/ui-engine/transpile-ui.d.ts +11 -0
  112. package/dist/ui-engine/transpile-ui.js +33 -0
  113. package/dist/ui-engine/ua-stylesheet.d.ts +3 -0
  114. package/dist/ui-engine/ua-stylesheet.js +56 -0
  115. package/dist/ui-engine/ui-file-splitter.d.ts +14 -0
  116. package/dist/ui-engine/ui-file-splitter.js +47 -0
  117. package/dist/ui-engine/ui-lowering.d.ts +33 -0
  118. package/dist/ui-engine/ui-lowering.js +390 -0
  119. package/dist/ui-engine/ui-registry.d.ts +81 -0
  120. package/dist/ui-engine/ui-registry.js +429 -0
  121. package/dist/ui-engine/yoga-layout.d.ts +12 -0
  122. package/dist/ui-engine/yoga-layout.js +314 -0
  123. package/package.json +86 -0
  124. package/src/engine-index.ts +51 -0
  125. package/src/index.ts +232 -0
  126. package/src/preview/build-program.ts +734 -0
  127. package/src/preview/host-gfx.ts +580 -0
  128. package/src/preview/host-ui-runtime.ts +3245 -0
  129. package/src/preview/types.ts +96 -0
  130. package/src/types.ts +127 -0
  131. package/src/ui-engine/block-layout.ts +104 -0
  132. package/src/ui-engine/color.ts +263 -0
  133. package/src/ui-engine/css-parser.ts +1003 -0
  134. package/src/ui-engine/default-keyboards.ts +58 -0
  135. package/src/ui-engine/easing.ts +53 -0
  136. package/src/ui-engine/font-assets.ts +512 -0
  137. package/src/ui-engine/html-parser.ts +397 -0
  138. package/src/ui-engine/image-assets.ts +170 -0
  139. package/src/ui-engine/inline-parser.ts +78 -0
  140. package/src/ui-engine/keyframes.ts +47 -0
  141. package/src/ui-engine/layout-engine.ts +228 -0
  142. package/src/ui-engine/model.ts +1225 -0
  143. package/src/ui-engine/rich-layout.ts +257 -0
  144. package/src/ui-engine/run-types.ts +14 -0
  145. package/src/ui-engine/runtime-header/antialiasing.ts +141 -0
  146. package/src/ui-engine/runtime-header/blend-bodies.ts +39 -0
  147. package/src/ui-engine/runtime-header/canvas-helpers.ts +155 -0
  148. package/src/ui-engine/runtime-header/canvas-scrollbar.ts +105 -0
  149. package/src/ui-engine/runtime-header/color-mono-refresh.ts +154 -0
  150. package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +169 -0
  151. package/src/ui-engine/runtime-header/display-shim.ts +104 -0
  152. package/src/ui-engine/runtime-header/forward-decls.ts +137 -0
  153. package/src/ui-engine/runtime-header/guard-close.ts +8 -0
  154. package/src/ui-engine/runtime-header/image-drawing.ts +300 -0
  155. package/src/ui-engine/runtime-header/init-press-input.ts +126 -0
  156. package/src/ui-engine/runtime-header/keyboard.ts +342 -0
  157. package/src/ui-engine/runtime-header/node-decoration.ts +212 -0
  158. package/src/ui-engine/runtime-header/paint-order-coords.ts +190 -0
  159. package/src/ui-engine/runtime-header/paint-rects-repair.ts +553 -0
  160. package/src/ui-engine/runtime-header/scroll-physics.ts +161 -0
  161. package/src/ui-engine/runtime-header/state-bindings-nav.ts +176 -0
  162. package/src/ui-engine/runtime-header/structs.ts +220 -0
  163. package/src/ui-engine/runtime-header/text-rendering.ts +715 -0
  164. package/src/ui-engine/runtime-header/tick/bindings-phase.ts +103 -0
  165. package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +1051 -0
  166. package/src/ui-engine/runtime-header/tick/flush-phase.ts +17 -0
  167. package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +15 -0
  168. package/src/ui-engine/runtime-header/tick/transitions-phase.ts +213 -0
  169. package/src/ui-engine/runtime-header/tick-motion-helpers.ts +26 -0
  170. package/src/ui-engine/runtime-header/tick.ts +18 -0
  171. package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +406 -0
  172. package/src/ui-engine/runtime-header/types-defines.ts +79 -0
  173. package/src/ui-engine/runtime-header.ts +71 -0
  174. package/src/ui-engine/scroll-memory-diagnostics.ts +79 -0
  175. package/src/ui-engine/select-engine.ts +21 -0
  176. package/src/ui-engine/style-resolver.ts +416 -0
  177. package/src/ui-engine/text-layout.ts +114 -0
  178. package/src/ui-engine/transpile-ui.ts +45 -0
  179. package/src/ui-engine/ua-stylesheet.ts +60 -0
  180. package/src/ui-engine/ui-file-splitter.ts +59 -0
  181. package/src/ui-engine/ui-lowering.ts +459 -0
  182. package/src/ui-engine/ui-registry.ts +530 -0
  183. package/src/ui-engine/yoga-layout.ts +309 -0
@@ -0,0 +1,715 @@
1
+ // Slice of the C++ runtime header (original source lines 2995-3703).
2
+ // Fonts, UTF-8, metrics, wrap, layout cache, all text draw variants, rich link hit.
3
+ // See docs/superpowers/specs/2026-07-12-split-runtime-header-design.md.
4
+ export function emitTextRendering(): string {
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 = ((uint16_t)(b0 & 0x1F) << 6) | (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 = ((uint16_t)(b0 & 0x0F) << 12) | ((uint16_t)(s[0] & 0x3F) << 6) | (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 = ((uint16_t)(b0 & 0x1F) << 6) | (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 = ((uint16_t)(b0 & 0x0F) << 12) | ((uint16_t)(s[0] & 0x3F) << 6) | (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 = (int16_t)(ts ? ts : 2) * 6 + letterSpacing;
148
+ return adv > 0 ? (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 + (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 + (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) {
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;
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;
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 + (int16_t)glyph->width <= targetLeft || glyphX >= targetRight ||
367
+ glyphY + (int16_t)glyph->height <= targetTop || glyphY >= targetBottom) {
368
+ cursor += glyph->advance;
369
+ continue;
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;
375
+ for (int16_t gy = gyStart; gy < gyEnd; gy++) {
376
+ for (int16_t gx = gxStart; gx < gxEnd; gx++) {
377
+ uint16_t pixelIndex = (uint16_t)gy * glyph->width + (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, (uint8_t)((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 = -(int8_t)radius; dy <= (int8_t)radius; dy++) {
441
+ int16_t yy = y + dy;
442
+ if (yy < 0 || yy >= h) continue;
443
+ for (int8_t dx = -(int8_t)radius; dx <= (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
+ 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) {
472
+ if (!text || !*text) return;
473
+ if (ts == 0) ts = 2;
474
+ uint16_t w = ui_text_width(text, ts, 0, 0);
475
+ // Add 1px bottom padding so the AA edge-detection sampling doesn't clip the
476
+ // glyph bottoms (the neighbor-count at the last row rounds partial coverage
477
+ // to 0 without this clearance).
478
+ uint8_t h = ui_text_height(ts, 0) + 1;
479
+ if (w == 0 || h == 0 || fg == bg) {
480
+ ui_draw_bitmap_text(text, x, y, fg, bg, ts, 0);
481
+ return;
482
+ }
483
+ int16_t clipX = x;
484
+ int16_t clipY = y;
485
+ int16_t clipW = (int16_t)w;
486
+ int16_t clipH = (int16_t)h;
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);
490
+
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);
494
+ if (!src || !dst || !display_canvasBuffer(src) || !display_canvasBuffer(dst)) {
495
+ ui_draw_bitmap_text(text, x, y, fg, bg, ts, 0);
496
+ return;
497
+ }
498
+
499
+ display_canvasFillRect(src, 0, 0, w, h, bg);
500
+ display_canvasFillRect(dst, localX, localY, clipW, clipH, bg);
501
+ display_targetSetCursor((CuttlefishDisplayTarget*)src, 0, 0);
502
+ display_targetSetTextColorBg((CuttlefishDisplayTarget*)src, fg, bg);
503
+ display_targetSetTextSize((CuttlefishDisplayTarget*)src, ts);
504
+ display_targetSetTextWrap((CuttlefishDisplayTarget*)src, false);
505
+ display_targetPrint((CuttlefishDisplayTarget*)src, text);
506
+
507
+ for (int16_t yy = localY; yy < localY + clipH; yy++) {
508
+ for (int16_t xx = localX; xx < localX + clipW; xx++) {
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);
511
+ uint8_t outerNeighbors = 0;
512
+ if (ts >= 3 && px != fg && neighbors == 0) {
513
+ outerNeighbors = ui_text_fg_neighbors(src, xx, yy, (int16_t)w, h, fg, 2);
514
+ }
515
+ uint8_t coverage = ui_text_aa_coverage(neighbors, outerNeighbors, px == fg ? 1 : 0, ts);
516
+ display_targetDrawPixel((CuttlefishDisplayTarget*)dst, xx, yy, coverage == 0 ? bg : ui_blend(fg, bg, coverage));
517
+ }
518
+ }
519
+
520
+ int16_t stride = display_canvasWidth(dst);
521
+ UI_COLOR_T* pixels = display_canvasBuffer(dst);
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);
525
+ }
526
+ }
527
+
528
+ 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) {
529
+ if (fontFace && ui_draw_asset_text(text, x, y, fg, bg, antialias, fontFace)) {
530
+ return;
531
+ }
532
+ // Only use AA when fg != bg (opaque background). When fg == bg (transparent
533
+ // mode), the AA source canvas fills entirely with fg — no edges to detect,
534
+ // producing a solid rectangle instead of text.
535
+ if (antialias && fg != bg && letterSpacing == 0) {
536
+ ui_draw_aa_text(text, x, y, fg, bg, ts);
537
+ return;
538
+ }
539
+ ui_draw_bitmap_text(text, x, y, fg, bg, ts, letterSpacing);
540
+ }
541
+ #else
542
+ 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) {
543
+ (void)antialias;
544
+ if (fontFace && ui_draw_asset_text(text, x, y, fg, bg, antialias, fontFace)) {
545
+ return;
546
+ }
547
+ ui_draw_bitmap_text(text, x, y, fg, bg, ts, letterSpacing);
548
+ }
549
+ #endif
550
+
551
+ /** Truncate a NUL-terminated buffer in place to fit within maxWidth (px) and
552
+ * append "...". Used by text-overflow: ellipsis. Adafruit_GFX has no ellipsis
553
+ * glyph, so three ASCII dots approximate it. */
554
+ static inline void ui_truncate_ellipsis(char* buf, uint8_t bufSize, uint16_t maxWidth,
555
+ uint8_t ts, uint8_t fontFace, int8_t letterSpacing) {
556
+ if (!buf || bufSize == 0) return;
557
+ uint8_t len = (uint8_t)strlen(buf);
558
+ // 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;
561
+ 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.
563
+ 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--;
568
+ }
569
+ if (prefix + 3 < bufSize) {
570
+ buf[prefix] = '.'; buf[prefix+1] = '.'; buf[prefix+2] = '.'; buf[prefix+3] = 0;
571
+ } else if (bufSize > 3) {
572
+ buf[0]='.'; buf[1]='.'; buf[2]='.'; buf[3]=0;
573
+ }
574
+ }
575
+
576
+ // text-overflow: clip — trim trailing chars until the prefix fits maxWidth,
577
+ // then NUL-terminate. No trailing dots (contrast with ui_truncate_ellipsis).
578
+ static inline void ui_truncate_clip(char* buf, uint8_t bufSize, uint16_t maxWidth,
579
+ uint8_t ts, uint8_t fontFace, int8_t letterSpacing) {
580
+ if (!buf || bufSize == 0) return;
581
+ uint8_t len = (uint8_t)strlen(buf);
582
+ 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--;
587
+ }
588
+ if (prefix < bufSize) buf[prefix] = 0;
589
+ }
590
+
591
+ 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,
592
+ uint8_t ts, uint8_t antialias, uint8_t fontFace, int8_t letterSpacing,
593
+ uint8_t lineHeight, uint8_t whiteSpaceMode, uint8_t textAlign, uint8_t underline, uint8_t textOverflow) {
594
+ if (!text) text = "";
595
+ uint8_t lh = ui_text_line_height(ts, fontFace, lineHeight);
596
+ const char* cursor = text;
597
+ int16_t lineY = y;
598
+ UITextLine line;
599
+ char lineBuf[UI_TEXT_LINE_BUF];
600
+ int16_t targetLeft, targetTop, targetRight, targetBottom;
601
+ ui_display_target_bounds(&targetLeft, &targetTop, &targetRight, &targetBottom);
602
+ while (ui_text_next_line(&cursor, maxWidth, whiteSpaceMode, ts, fontFace, letterSpacing, &line)) {
603
+ int16_t lineBottom = (int16_t)(lineY + lh);
604
+ if (lineBottom <= targetTop || lineY >= targetBottom) {
605
+ lineY += lh;
606
+ continue;
607
+ }
608
+ 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) {
612
+ lineY += lh;
613
+ continue;
614
+ }
615
+ ui_copy_text_span(line.start, line.end, lineBuf, UI_TEXT_LINE_BUF);
616
+ // text-overflow — only applies when the line is wider than maxWidth.
617
+ // textOverflow==1 (ellipsis): trim the span and append "...".
618
+ // textOverflow==0 (clip): trim the span to the edge, no dots.
619
+ if ((int16_t)line.width > (int16_t)maxWidth) {
620
+ if (textOverflow) ui_truncate_ellipsis(lineBuf, UI_TEXT_LINE_BUF, maxWidth, ts, fontFace, letterSpacing);
621
+ else ui_truncate_clip(lineBuf, UI_TEXT_LINE_BUF, maxWidth, ts, fontFace, letterSpacing);
622
+ }
623
+ ui_draw_text(lineBuf, lineX, lineY, fg, bg, ts, antialias, fontFace, letterSpacing);
624
+ // text-decoration (underline=bit0, strikethrough=bit1)
625
+ if (underline & 1) ui_display_draw_fast_hline(lineX, lineY + ui_text_height(ts, fontFace) - 1, line.width, fg);
626
+ if (underline & 2) ui_display_draw_fast_hline(lineX, lineY + ui_text_height(ts, fontFace) / 2, line.width, fg);
627
+ lineY += lh;
628
+ }
629
+ }
630
+
631
+ // Draw a rich-text node from its precomputed run/segment/line geometry. Does
632
+ // NOT re-wrap — the geometry was baked at transpile time (runs are static-only,
633
+ // so the text never changes at runtime). Iterate segments once, skip lines and
634
+ // segments outside the active draw target, and compute each segment's line
635
+ // origin from textAlign + line width. Mixed font sizes align on the line's
636
+ // baseline (each segment's top = baseline − its own ascent).
637
+ static inline void ui_draw_rich_text(uint16_t nodeIdx, int16_t x, int16_t y, UI_COLOR_T bg, uint8_t antialias,
638
+ uint8_t useFgOverride = 0, UI_COLOR_T fgOverride = 0, uint16_t maxWidth = 0) {
639
+ UINode* n = &__ui_nodes[nodeIdx];
640
+ 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);
646
+ for (uint16_t si = n->richSegStart; si < richSegEnd; si++) {
647
+ UIRichSeg* seg = &__ui_rich_segs[si];
648
+ if (seg->line >= n->richLineCount) continue;
649
+ UIRichLine* line = &__ui_rich_lines[n->richLineStart + seg->line];
650
+ int16_t lineTop = y + line->y;
651
+ int16_t lineBottom = (int16_t)(lineTop + (int16_t)line->h);
652
+ if (lineBottom <= targetTop || lineTop >= targetBottom) continue;
653
+ 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);
658
+ if (segRight <= targetLeft || segX >= targetRight) continue;
659
+ UIRichRun* run = &__ui_runs[n->runStart + seg->runIndex];
660
+ // Baseline alignment: for asset fonts, the ascent is the font face's
661
+ // baseline (baked per px size); for the bitmap font (fontFace=0), it's the
662
+ // 5x8 glyph ascent (7px × textSize). Using the wrong ascent misaligns runs
663
+ // vertically and makes mixed rich-text lines look garbled.
664
+ int16_t ascent;
665
+ if (run->fontFace) {
666
+ const UIFontFace* face = ui_font_face(run->fontFace);
667
+ ascent = face ? (int16_t)face->baseline : (7 * (int16_t)run->textSize);
668
+ } else {
669
+ ascent = 7 * (int16_t)run->textSize;
670
+ }
671
+ int16_t segY = y + line->baseline - ascent;
672
+ UI_COLOR_T fg = useFgOverride ? fgOverride : run->fg;
673
+ ui_draw_text(seg->text, segX, segY, fg, bg, run->textSize, antialias, run->fontFace, run->letterSpacing);
674
+ if (run->underline & 1) ui_display_draw_fast_hline(segX, segY + 8 * run->textSize - 1, seg->w, fg);
675
+ if (run->underline & 2) ui_display_draw_fast_hline(segX, segY + 4 * run->textSize, seg->w, fg);
676
+ }
677
+ }
678
+
679
+ // Hit-test a tap point (in node-local coordinates) against a rich-text node's
680
+ // link runs. Returns the link run's resolved screen index, or -1 if the point
681
+ // doesn't land on a link segment. Mirrors the list-item subdivision precedent
682
+ // but uses measured segment rects instead of fixed row heights.
683
+ static inline int8_t ui_rich_link_hit(uint16_t nodeIdx, int16_t px, int16_t py) {
684
+ 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;
688
+ int16_t localX = px - insetL;
689
+ 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)
692
+ : 0;
693
+ for (uint16_t si = n->richSegStart; si < n->richSegStart + n->richSegCount; si++) {
694
+ UIRichSeg* seg = &__ui_rich_segs[si];
695
+ UIRichRun* run = &__ui_runs[n->runStart + seg->runIndex];
696
+ if (run->linkTarget < 0) continue;
697
+ UIRichLine* line = &__ui_rich_lines[n->richLineStart + seg->line];
698
+ // Segment x is relative to its line's left edge (pre-alignment). For the
699
+ // hit-test, account for center/right alignment the same way draw does.
700
+ 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;
703
+ int16_t sx = originX + seg->x;
704
+ int16_t sy = line->y;
705
+ if (localX >= sx && localX < sx + (int16_t)seg->w && localY >= sy && localY < sy + (int16_t)line->h) {
706
+ return run->linkTarget;
707
+ }
708
+ }
709
+ return -1;
710
+ }
711
+
712
+ // Draw shadows for an element. Loops over up to 4 shadow specs. Outset shadows
713
+ // are drawn behind the element; inset shadows are drawn over the element fill.
714
+ // Draw a gradient fill for an element. Replaces solid fillRect/fillRoundRect.`;
715
+ }