@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.
- package/README.md +1457 -1469
- package/dist/cli.js +22 -19
- package/dist/preview/host-ui-runtime.js +0 -1
- package/dist/ui-engine/model.js +7 -1
- package/dist/ui-engine/runtime-header/canvas-scrollbar.js +115 -115
- package/dist/ui-engine/runtime-header/dirty-scroll-mutators.js +182 -182
- package/dist/ui-engine/runtime-header/forward-decls.js +227 -216
- package/dist/ui-engine/runtime-header/init-press-input.js +138 -138
- package/dist/ui-engine/runtime-header/keyboard.js +676 -676
- package/dist/ui-engine/runtime-header/node-draw-body.js +1658 -1656
- package/dist/ui-engine/runtime-header/paint-order-coords.js +259 -259
- package/dist/ui-engine/runtime-header/paint-rects-repair.js +736 -736
- package/dist/ui-engine/runtime-header/scroll-physics.js +4 -4
- package/dist/ui-engine/runtime-header/structs.js +228 -228
- package/dist/ui-engine/runtime-header/text-rendering.js +888 -888
- package/dist/ui-engine/runtime-header/tick/bindings-phase.js +118 -118
- package/dist/ui-engine/runtime-header/tick/dirty-draw-phase.js +896 -896
- package/dist/ui-engine/runtime-header/tick/scroll-canvas-phase.js +24 -24
- package/dist/ui-engine/runtime-header/touch-keyboard-fwd.js +11 -11
- package/dist/ui-engine/runtime-header/types-defines.js +110 -110
- package/dist/ui-engine/shadcn-kit.d.ts +1 -1
- package/dist/ui-engine/shadcn-kit.js +4 -0
- package/dist/ui-engine/ui-lowering.d.ts +1 -1
- package/dist/ui-engine/ui-lowering.js +14 -5
- package/dist/ui-engine/ui-registry.js +9 -1
- package/dist/wizard/display-catalog.d.ts +0 -3
- package/dist/wizard/display-catalog.js +3 -12
- package/dist/wizard/index.d.ts +2 -0
- package/dist/wizard/index.js +1 -0
- package/dist/wizard/integration-wizard.d.ts +3 -0
- package/dist/wizard/integration-wizard.js +69 -20
- package/dist/wizard/package-writer.d.ts +20 -0
- package/dist/wizard/package-writer.js +79 -0
- package/package.json +4 -4
- package/src/cli.ts +90 -87
- package/src/preview/host-ui-runtime.ts +0 -1
- package/src/ui-engine/model.ts +6 -1
- package/src/ui-engine/runtime-header/canvas-scrollbar.ts +121 -121
- package/src/ui-engine/runtime-header/dirty-scroll-mutators.ts +188 -188
- package/src/ui-engine/runtime-header/forward-decls.ts +238 -227
- package/src/ui-engine/runtime-header/init-press-input.ts +144 -144
- package/src/ui-engine/runtime-header/keyboard.ts +689 -689
- package/src/ui-engine/runtime-header/node-draw-body.ts +1683 -1681
- package/src/ui-engine/runtime-header/paint-order-coords.ts +265 -265
- package/src/ui-engine/runtime-header/paint-rects-repair.ts +742 -742
- package/src/ui-engine/runtime-header/scroll-physics.ts +4 -4
- package/src/ui-engine/runtime-header/structs.ts +234 -234
- package/src/ui-engine/runtime-header/text-rendering.ts +894 -894
- package/src/ui-engine/runtime-header/tick/bindings-phase.ts +124 -124
- package/src/ui-engine/runtime-header/tick/dirty-draw-phase.ts +902 -902
- package/src/ui-engine/runtime-header/tick/scroll-canvas-phase.ts +30 -30
- package/src/ui-engine/runtime-header/touch-keyboard-fwd.ts +11 -11
- package/src/ui-engine/runtime-header/types-defines.ts +116 -116
- package/src/ui-engine/shadcn-kit.ts +4 -0
- package/src/ui-engine/ui-lowering.ts +12 -4
- package/src/ui-engine/ui-registry.ts +9 -1
- package/src/wizard/display-catalog.ts +261 -273
- package/src/wizard/index.ts +46 -38
- package/src/wizard/integration-wizard.ts +679 -619
- package/src/wizard/package-writer.ts +98 -0
|
@@ -1,894 +1,894 @@
|
|
|
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 = (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
|
-
// Draw a gradient fill for an element. Replaces solid fillRect/fillRoundRect.`;
|
|
894
|
-
}
|
|
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 = (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
|
+
// Draw a gradient fill for an element. Replaces solid fillRect/fillRoundRect.`;
|
|
894
|
+
}
|