libgui 2.01
libgui is a GUI library for the GameCube, Wii, and Wii U
 
Loading...
Searching...
No Matches
GuiTextRenderer.cpp
1/****************************************************************************
2 * libgui
3 * Daryl Borth 2026
4 * GuiTextRenderer.cpp
5 ***************************************************************************/
6
7#include "GuiTextRenderer.h"
8#include <cmath>
9#include <cstdlib>
10#include <cstring>
11#include <cstdint>
12#include <cwchar>
13#include FT_MODULE_H
14#include FT_SYSTEM_H
15
16GuiTextRenderer* fontSystem;
17
18GlyphData GuiTextRenderer::missingGlyph;
19uint32_t GuiTextRenderer::metricsGeneration = 0;
20
21// FT_Init_FreeType() registers every font driver and renderer FreeType was
22// built with, and because those are reached through function-pointer tables the
23// linker has to keep all of them (about 600 KB) even though only one font is
24// ever loaded. Register just the modules that font needs instead, using the
25// same module classes FreeType's own ftinit.c does. They are declared as
26// opaque data because their real types live in FreeType's internal headers;
27// all that is needed is their address.
28//
29// This is enough for a TrueType font that has hinting instructions and a
30// Unicode cmap, which is what the bundled font is. A font without instructions
31// would also want autofit_module_class (otherwise it renders unhinted), and one
32// without a Unicode cmap would want psnames_module_class.
33extern "C" {
34 extern const unsigned char tt_driver_class[];
35 extern const unsigned char sfnt_module_class[];
36 extern const unsigned char ft_smooth_renderer_class[];
37}
38
39namespace {
40 void* ftAlloc(FT_Memory, long size) { return std::malloc(size); }
41 void ftFree(FT_Memory, void* block) { std::free(block); }
42 void* ftRealloc(FT_Memory, long, long newSize, void* block) { return std::realloc(block, newSize); }
43
44 // Must outlive the library, so it is static
45 FT_MemoryRec_ ftMemory = { nullptr, ftAlloc, ftFree, ftRealloc };
46
47 FT_Library createFreeTypeLibrary()
48 {
49 FT_Library library = nullptr;
50 if (FT_New_Library(&ftMemory, &library) != 0)
51 return nullptr;
52
53 if (FT_Add_Module(library, reinterpret_cast<const FT_Module_Class*>(tt_driver_class)) != 0 ||
54 FT_Add_Module(library, reinterpret_cast<const FT_Module_Class*>(sfnt_module_class)) != 0 ||
55 FT_Add_Module(library, reinterpret_cast<const FT_Module_Class*>(ft_smooth_renderer_class)) != 0)
56 {
57 FT_Done_Library(library);
58 return nullptr;
59 }
60 return library;
61 }
62
63 // Converts an already->>6-shifted FreeType value (in physical/rasterized
64 // pixels) back down to the design-pixel units
65 inline int16_t ToDesignPixels(int v, float uiScale)
66 {
67 return uiScale == 1.0f ? (int16_t)v : (int16_t)std::lround(v / uiScale);
68 }
69}
70
71GuiTextRenderer::GuiTextRenderer(const uint8_t* fontBuffer, FT_Long bufferSize, GlyphRenderer* glyphRenderer, float uiScale_)
72 : currentPixelSize(0), renderer(glyphRenderer), current(nullptr), uiScale(uiScale_)
73{
74 ++metricsGeneration;
75 ftLibrary = createFreeTypeLibrary();
76 FT_New_Memory_Face(ftLibrary, (FT_Byte*)fontBuffer, bufferSize, 0, &ftFace);
77 ftKerningEnabled = FT_HAS_KERNING(ftFace);
78}
79
80GuiTextRenderer::~GuiTextRenderer() {
81 unloadFont();
82 if (ftFace) FT_Done_Face(ftFace);
83 if (ftLibrary) FT_Done_Library(ftLibrary);
84}
85
86void GuiTextRenderer::unloadFont() {
87 for (auto& sizePair : fontData) {
88 for (auto& charPair : sizePair.second.charMap) {
89 if (charPair.second.texture) {
90 renderer->destroyTexture(charPair.second.texture); // Delegate to injected renderer
91 charPair.second.texture = nullptr;
92 }
93 }
94 }
95 fontData.clear();
96 current = nullptr;
97 ++metricsGeneration;
98}
99
100void GuiTextRenderer::setPixelSize(int16_t pixelSize) {
101 if (currentPixelSize != pixelSize) {
102 currentPixelSize = pixelSize;
103 current = nullptr;
104 // currentPixelSize (and every size callers pass in/read back) stays
105 // in design pixels - FreeType itself is asked to rasterize at
106 // uiScale x that.
107 FT_Set_Pixel_Sizes(ftFace, 0, (FT_UInt)std::lround(currentPixelSize * uiScale));
108 }
109}
110
111// --- Caching Engine ---
112
113GlyphData* GuiTextRenderer::cacheGlyphData(wchar_t charCode, int16_t pixelSize) {
114 ftData* data = &fontData[pixelSize];
115
116 // Initialize metrics on first run for this size
117 if (!data->metricsReady) {
118 data->metricsReady = true;
119 setPixelSize(pixelSize);
120 data->align.ascender = ToDesignPixels(ftFace->size->metrics.ascender >> 6, uiScale);
121 data->align.descender = ToDesignPixels(ftFace->size->metrics.descender >> 6, uiScale);
122 data->align.max = 0;
123 data->align.min = 0;
124 }
125
126 const bool inFastRange = (uint32_t)charCode < 256;
127
128 auto it = data->charMap.find(charCode);
129 if (it != data->charMap.end()) {
130 if (inFastRange) data->fast[charCode] = &it->second;
131 return &it->second; // Already cached
132 }
133
134 FT_UInt gIndex = FT_Get_Char_Index(ftFace, (FT_ULong)charCode);
135 bool loaded = gIndex != 0 && (FT_Load_Glyph(ftFace, gIndex, FT_LOAD_DEFAULT | FT_LOAD_RENDER | FT_LOAD_NO_BITMAP) == 0 ||
136 FT_Load_Glyph(ftFace, gIndex, FT_LOAD_DEFAULT | FT_LOAD_RENDER) == 0);
137 if (loaded) {
138 if (ftFace->glyph->format == FT_GLYPH_FORMAT_BITMAP && ftFace->glyph->bitmap.pixel_mode == FT_PIXEL_MODE_GRAY) {
139 FT_Bitmap* glyphBitmap = &ftFace->glyph->bitmap;
140
141 GlyphData& charData = data->charMap[charCode];
142 charData.renderOffsetX = ToDesignPixels(ftFace->glyph->bitmap_left, uiScale);
143 charData.glyphAdvanceX = (uint16_t)ToDesignPixels(ftFace->glyph->advance.x >> 6, uiScale);
144 charData.glyphAdvanceY = (uint16_t)ToDesignPixels(ftFace->glyph->advance.y >> 6, uiScale);
145 charData.glyphIndex = (uint32_t)gIndex;
146
147 // Physical (rasterized-at-uiScale) bitmap dims - what the
148 // texture is actually created/loaded at.
149 charData.textureWidth = glyphBitmap->width;
150 charData.textureHeight = glyphBitmap->rows;
151 // Design-pixel dims the quad is drawn at, so text lays out and
152 // sizes identically to uiScale 1.0 while the texture is crisper.
153 charData.drawWidth = (uint16_t)ToDesignPixels(glyphBitmap->width, uiScale);
154 charData.drawHeight = (uint16_t)ToDesignPixels(glyphBitmap->rows, uiScale);
155
156 charData.renderOffsetY = ToDesignPixels(ftFace->glyph->bitmap_top, uiScale);
157 charData.renderOffsetMax = charData.renderOffsetY;
158 charData.renderOffsetMin = ToDesignPixels((int)glyphBitmap->rows - ftFace->glyph->bitmap_top, uiScale);
159
160 // Delegate Texture creation and data loading to the active backend
161 charData.texture = renderer->createTexture(charData.textureWidth, charData.textureHeight);
162 if (charData.texture) {
163 renderer->loadTextureData(charData.texture, glyphBitmap);
164 }
165
166 if (inFastRange) data->fast[charCode] = &charData;
167 return &charData;
168 }
169 }
170 if (inFastRange) data->fast[charCode] = &missingGlyph;
171 return nullptr;
172}
173
174GlyphData* GuiTextRenderer::glyph(wchar_t charCode) {
175 if (!current) current = &fontData[currentPixelSize];
176
177 if ((uint32_t)charCode < 256) {
178 GlyphData* g = current->fast[charCode];
179 if (g) return g == &missingGlyph ? nullptr : g;
180 }
181 return cacheGlyphData(charCode, currentPixelSize);
182}
183
184// --- Layout & Styling Engine ---
185
186int16_t GuiTextRenderer::getStyleOffsetWidth(uint16_t width, uint32_t format) {
187 if (format & GUI_TEXT_JUSTIFY_LEFT) return 0;
188 else if (format & GUI_TEXT_JUSTIFY_CENTER) return -(width >> 1);
189 else if (format & GUI_TEXT_JUSTIFY_RIGHT) return -width;
190 return 0;
191}
192
193int16_t GuiTextRenderer::getStyleOffsetHeight(FontOffset* offset, uint32_t format) {
194 switch (format & GUI_TEXT_ALIGN_MASK) {
195 case GUI_TEXT_ALIGN_TOP: return offset->ascender;
196 case GUI_TEXT_ALIGN_BOTTOM: return offset->descender;
197 case GUI_TEXT_ALIGN_BASELINE: return 0;
198 case GUI_TEXT_ALIGN_GLYPH_TOP: return offset->max;
199 case GUI_TEXT_ALIGN_GLYPH_MIDDLE: return (offset->max + offset->min + 1) >> 1;
200 case GUI_TEXT_ALIGN_GLYPH_BOTTOM: return offset->min;
201 case GUI_TEXT_ALIGN_MIDDLE:
202 default: return (offset->ascender + offset->descender + 1) >> 1;
203 }
204}
205
206void GuiTextRenderer::getOffset(const wchar_t* text, FontOffset* offset) {
207 int16_t strMax = 0, strMin = 9999;
208
209 int i = 0;
210 while (text[i]) {
211 GlyphData* glyphData = glyph(text[i]);
212 if (glyphData) {
213 strMax = glyphData->renderOffsetMax > strMax ? glyphData->renderOffsetMax : strMax;
214 strMin = glyphData->renderOffsetMin < strMin ? glyphData->renderOffsetMin : strMin;
215 }
216 ++i;
217 }
218
219 offset->ascender = ToDesignPixels(ftFace->size->metrics.ascender >> 6, uiScale);
220 offset->descender = ToDesignPixels(ftFace->size->metrics.descender >> 6, uiScale);
221 offset->max = strMax;
222 offset->min = strMin;
223}
224
225// --- Metrics ---
226
227uint16_t GuiTextRenderer::getWidth(const wchar_t* text) {
228 uint16_t strWidth = 0;
229 FT_Vector pairDelta;
230 GlyphData* prev = nullptr;
231
232 for (int i = 0; text[i]; ++i) {
233 GlyphData* glyphData = glyph(text[i]);
234 if (glyphData) {
235 if (ftKerningEnabled && prev) {
236 FT_Get_Kerning(ftFace, prev->glyphIndex, glyphData->glyphIndex, FT_KERNING_DEFAULT, &pairDelta);
237 strWidth += ToDesignPixels(pairDelta.x >> 6, uiScale);
238 }
239 strWidth += glyphData->glyphAdvanceX;
240 }
241 prev = glyphData;
242 }
243 return strWidth;
244}
245
246int GuiTextRenderer::fitCount(const wchar_t* text, int maxWidth) {
247 uint16_t strWidth = 0;
248 FT_Vector pairDelta;
249 GlyphData* prev = nullptr;
250 int best = 0;
251
252 for (int i = 0; text[i]; ++i) {
253 GlyphData* glyphData = glyph(text[i]);
254 if (glyphData) {
255 if (ftKerningEnabled && prev) {
256 FT_Get_Kerning(ftFace, prev->glyphIndex, glyphData->glyphIndex, FT_KERNING_DEFAULT, &pairDelta);
257 strWidth += ToDesignPixels(pairDelta.x >> 6, uiScale);
258 }
259 strWidth += glyphData->glyphAdvanceX;
260 }
261 prev = glyphData;
262 if ((int)strWidth <= maxWidth) best = i + 1;
263 }
264 return best;
265}
266
267void GuiTextRenderer::scrollWindow(const wchar_t* text, uint32_t textlen, uint32_t pos, int maxWidth, wchar_t* out) {
268 uint32_t dynlen = textlen > pos ? textlen - pos : 0;
269 const bool spaced = dynlen + 2 < textlen;
270 if (spaced) dynlen += 2;
271
272 // the longest line that could be wanted: the tail, then as much of the start as would still leave
273 // the line shorter than the text
274 const uint32_t extra = textlen > dynlen + 1 ? textlen - 1 - dynlen : 0;
275 const uint32_t total = dynlen + extra;
276
277 lineScratch.resize(total + 1);
278 wchar_t* line = lineScratch.data();
279 uint32_t n = 0;
280 for (uint32_t i = pos; i < textlen; ++i) line[n++] = text[i];
281 if (spaced) { line[n++] = L' '; line[n++] = L' '; }
282 for (uint32_t i = 0; i < extra; ++i) line[n++] = text[i];
283 line[n] = 0;
284
285 // the width of every start of the line, in one pass
286 widthScratch.resize(total + 1);
287 uint16_t* w = widthScratch.data();
288 uint16_t strWidth = 0;
289 FT_Vector pairDelta;
290 GlyphData* prev = nullptr;
291 w[0] = 0;
292 for (uint32_t i = 0; i < total; ++i) {
293 GlyphData* glyphData = glyph(line[i]);
294 if (glyphData) {
295 if (ftKerningEnabled && prev) {
296 FT_Get_Kerning(ftFace, prev->glyphIndex, glyphData->glyphIndex, FT_KERNING_DEFAULT, &pairDelta);
297 strWidth += ToDesignPixels(pairDelta.x >> 6, uiScale);
298 }
299 strWidth += glyphData->glyphAdvanceX;
300 }
301 prev = glyphData;
302 w[i + 1] = strWidth;
303 }
304
305 uint32_t len;
306 if ((int)w[dynlen] > maxWidth) {
307 // too wide: shorten until it fits
308 len = dynlen;
309 while (len > 0 && (int)w[len] > maxWidth) --len;
310 } else {
311 // room to spare: take on characters from the start of the text until it is full
312 len = dynlen;
313 while ((int)w[len] < maxWidth && len + 1 < textlen) ++len;
314 // the last character added is dropped, and one more if it overfilled the line
315 const uint32_t drop = (int)w[len] > maxWidth ? 2 : 1;
316 len = len > drop ? len - drop : 0;
317 }
318 for (uint32_t i = 0; i < len; ++i) out[i] = line[i];
319 out[len] = 0;
320}
321
322uint16_t GuiTextRenderer::getHeight(const wchar_t* text) {
323 FontOffset offset;
324 getOffset(text, &offset);
325 return offset.max - offset.min;
326}
327
328// --- Drawing Pipeline ---
329
330uint16_t GuiTextRenderer::drawText(int16_t x, int16_t y, const wchar_t* text, PixelColor color, uint32_t renderFlags) {
331 if (!text) return 0;
332
333 uint16_t x_pos = x, printed = 0;
334 int16_t x_offset = 0, y_offset = 0;
335 FT_Vector pairDelta;
336 FontOffset offset;
337
338 // Layout Alignment Calculations. Left-justified text needs no width, and only the glyph-based
339 // vertical alignments need the extents of the text itself (the others use the font's own metrics),
340 // so the text is measured only when the answer is used.
341 if ((renderFlags & GUI_TEXT_JUSTIFY_MASK) && !(renderFlags & GUI_TEXT_JUSTIFY_LEFT)) {
342 x_offset = getStyleOffsetWidth(getWidth(text), renderFlags);
343 }
344 if (renderFlags & GUI_TEXT_ALIGN_MASK) {
345 const uint32_t vertical = renderFlags & GUI_TEXT_ALIGN_MASK;
346 if (vertical == GUI_TEXT_ALIGN_GLYPH_TOP || vertical == GUI_TEXT_ALIGN_GLYPH_MIDDLE || vertical == GUI_TEXT_ALIGN_GLYPH_BOTTOM) {
347 getOffset(text, &offset);
348 } else {
349 offset.ascender = ToDesignPixels(ftFace->size->metrics.ascender >> 6, uiScale);
350 offset.descender = ToDesignPixels(ftFace->size->metrics.descender >> 6, uiScale);
351 offset.max = 0;
352 offset.min = 0;
353 }
354 y_offset = getStyleOffsetHeight(&offset, renderFlags);
355 }
356
357 GlyphData* prev = nullptr;
358 renderer->beginText();
359 for (int i = 0; text[i]; ++i) {
360 GlyphData* glyphData = glyph(text[i]);
361
362 if (glyphData) {
363 // Kerning adjustments
364 if (ftKerningEnabled && prev) {
365 FT_Get_Kerning(ftFace, prev->glyphIndex, glyphData->glyphIndex, FT_KERNING_DEFAULT, &pairDelta);
366 x_pos += ToDesignPixels(pairDelta.x >> 6, uiScale);
367 }
368
369 // Draw current glyph via generic renderer
370 int16_t screenX = x_pos + glyphData->renderOffsetX + x_offset;
371 int16_t screenY = y - glyphData->renderOffsetY + y_offset;
372 renderer->drawQuad(glyphData->texture, screenX, screenY, glyphData->drawWidth, glyphData->drawHeight, color);
373
374 x_pos += glyphData->glyphAdvanceX;
375 ++printed;
376 }
377 prev = glyphData;
378 }
379 renderer->endText();
380
381 // Process additional layout features (Underlines/Strikethrough)
382 if (renderFlags & GUI_TEXT_STYLE_MASK) {
383 getOffset(text, &offset);
384 drawTextFeature(x + x_offset, y + y_offset, getWidth(text), &offset, renderFlags, color);
385 }
386
387 return printed;
388}
389
390void GuiTextRenderer::drawTextFeature(int16_t x, int16_t y, uint16_t width, FontOffset* offsetData, uint32_t format, const PixelColor& color) {
391 uint16_t featureHeight = currentPixelSize >> 4 > 0 ? currentPixelSize >> 4 : 1;
392
393 if (format & GUI_TEXT_STYLE_UNDERLINE) {
394 renderer->drawFeature(x, y + 1, width, featureHeight, color);
395 }
396 if (format & GUI_TEXT_STYLE_STRIKE) {
397 renderer->drawFeature(x, y - (offsetData->max >> 1), width, featureHeight, color);
398 }
399}
400
401// --- UTF-8 Wrapper Overloads ---
402
403wchar_t* GuiTextRenderer::charToWideChar(const char* strChar) {
404 if (!strChar) return nullptr;
405
406 const unsigned char* s = reinterpret_cast<const unsigned char*>(strChar);
407 wchar_t* strWChar = new wchar_t[strlen(strChar) + 1]; // never longer than the byte count
408 size_t out = 0;
409
410 while (*s) {
411 uint32_t cp = *s;
412 int extra = 0;
413 uint32_t minCp = 0;
414
415 if (cp < 0x80) {
416 // ASCII
417 }
418 else if (cp >= 0xC2 && cp <= 0xDF) { extra = 1; cp &= 0x1F; minCp = 0x80; }
419 else if (cp >= 0xE0 && cp <= 0xEF) { extra = 2; cp &= 0x0F; minCp = 0x800; }
420 else if (cp >= 0xF0 && cp <= 0xF4) { extra = 3; cp &= 0x07; minCp = 0x10000; }
421 // else: stray continuation byte / invalid lead - handled below as Latin-1
422
423 if (extra) {
424 int n = 0;
425 uint32_t v = cp;
426 while (n < extra && (s[1 + n] & 0xC0) == 0x80) {
427 v = (v << 6) | (s[1 + n] & 0x3F);
428 ++n;
429 }
430
431 bool valid = (n == extra) && v >= minCp && v <= 0x10FFFF && !(v >= 0xD800 && v <= 0xDFFF);
432 if (valid && v > (uint32_t)WCHAR_MAX) valid = false; // can't represent in a 16-bit wchar_t
433 if (valid) {
434 strWChar[out++] = (wchar_t)v;
435 s += 1 + extra;
436 continue;
437 }
438 cp = *s; // invalid sequence: keep the lead byte as Latin-1
439 }
440
441 strWChar[out++] = (wchar_t)cp;
442 ++s;
443 }
444
445 strWChar[out] = L'\0';
446 return strWChar;
447}
448
449uint16_t GuiTextRenderer::drawText(int16_t x, int16_t y, const char* text, PixelColor color, uint32_t renderFlags) {
450 wchar_t* wText = charToWideChar(text);
451 uint16_t result = drawText(x, y, wText, color, renderFlags);
452 delete[] wText;
453 return result;
454}
455
456uint16_t GuiTextRenderer::getWidth(const char* text) {
457 wchar_t* wText = charToWideChar(text);
458 uint16_t result = getWidth(wText);
459 delete[] wText;
460 return result;
461}
462
463uint16_t GuiTextRenderer::getHeight(const char* text) {
464 wchar_t* wText = charToWideChar(text);
465 uint16_t result = getHeight(wText);
466 delete[] wText;
467 return result;
468}
469
470// FreeType's sfnt module uses brotli decoder (~175 KB). WOFF2 support is not needed - so we override it here
471// The signature and the return value come from brotli/decode.h
472// (BROTLI_DECODER_RESULT_ERROR is 0)
473extern "C" int BrotliDecoderDecompress(size_t encodedSize, const uint8_t* encodedBuffer, size_t* decodedSize, uint8_t* decodedBuffer)
474{
475 (void)encodedSize;
476 (void)encodedBuffer;
477 (void)decodedSize;
478 (void)decodedBuffer;
479 return 0;
480}
Draws glyph textures and solid rectangles on behalf of GuiTextRenderer.
virtual void drawFeature(int16_t screenX, int16_t screenY, uint16_t width, uint16_t height, const PixelColor &color)=0
Draws a solid-color rectangle used for text decoration (underline/strike-through).
FreeType2-based glyph shaping/caching.
void setPixelSize(int16_t pixelSize)
Selects the pixel size subsequent drawText()/getWidth()/getHeight() calls use.
uint16_t getHeight(const wchar_t *text)
static wchar_t * charToWideChar(const char *p)
uint16_t drawText(int16_t x, int16_t y, const wchar_t *text, PixelColor color=black, uint32_t renderFlags=0)
Draws text at (x, y) using the current pixel size.
int fitCount(const wchar_t *text, int maxWidth)
How many leading characters of text fit in maxWidth (the largest n for which the first n characters m...
void scrollWindow(const wchar_t *text, uint32_t textlen, uint32_t pos, int maxWidth, wchar_t *out)
The part of a horizontally scrolling line that is shown at one scroll position: the text from pos on,...
void getOffset(const wchar_t *text, FontOffset *offset)
uint16_t getWidth(const wchar_t *text)
GuiTextRenderer(const uint8_t *fontBuffer, FT_Long bufferSize, GlyphRenderer *glyphRenderer, float uiScale=1.0f)
Per-pixel-size font metrics used for text-block alignment/positioning.
int16_t max
Height above the baseline of the tallest glyph in the measured string (see GuiTextRenderer::getOffset...
int16_t min
Depth below the baseline of the lowest glyph in the measured string (see GuiTextRenderer::getOffset()...
int16_t descender
Font descender for the current size (negative: distance below the baseline), in design pixels.
int16_t ascender
Font ascender for the current size (distance above the baseline), in design pixels.
Cached per-glyph metrics and rasterized texture, keyed by (pixel size, char code) in GuiTextRenderer:...
int16_t renderOffsetX
Horizontal offset from the pen position to the bitmap's left edge.
uint16_t glyphAdvanceX
Horizontal pen advance after this glyph.
uint16_t textureWidth
Pixel dims of the rasterized bitmap/texture, at uiScale - always >= drawWidth/drawHeight,...
uint16_t drawWidth
Design-pixel dims to actually draw the quad at (textureWidth/Height divided back down by uiScale) - k...
int16_t renderOffsetMax
Height of the glyph bitmap above the baseline (feeds FontOffset::max)
uint32_t glyphIndex
FreeType glyph index.
int16_t renderOffsetY
Height of the bitmap's top edge above the baseline.
int16_t renderOffsetMin
Depth of the glyph bitmap below the baseline (feeds FontOffset::min)
uint16_t glyphAdvanceY
Vertical pen advance after this glyph.
uint16_t drawHeight
Height counterpart of drawWidth.
uint16_t textureHeight
Height counterpart of textureWidth.
void * texture
Abstracted texture pointer.
8-bit-per-channel RGBA color used by the video interfaces.
Definition Mesh.h:16