ObjectivelyMVC
Object oriented MVC framework for SDL3 and GNU C
Loading...
Searching...
No Matches
Text.c File Reference
#include <assert.h>
#include <math.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include "Colors.h"
#include "Text.h"
#include "Theme.h"

Go to the source code of this file.

Data Structures

struct  LayoutBuffer
 A growable string for MVC_LayoutText. More...
 

Macros

#define _Class   _Text
 
#define _GNU_SOURCE
 

Functions

Class * _Text (void)
 
static void append (LayoutBuffer *buffer, const char *chars, size_t length)
 
static void appendRuns (Text *self, TTF_Text *layout, const TextSpan *span, int surfaceWidth)
 Appends the runs for one TextSpan of layout: one color run per line the span touches, or a single icon run positioned by the span's first line.
 
static void appendSpan (TextSpan **spans, size_t *count, TextSpan span)
 
static void applyStyle (View *self, const Style *style)
 
static void applyTransform (Text *self)
 Rebuilds transformed from text and transform. Color escapes pass through without counting as letters, and icon escapes that resolve against the window's Theme are copied verbatim, since icon names are case sensitive.
 
static void awakeWithDictionary (View *self, const Dictionary *dictionary)
 
static void buildRuns (Text *self, const char *layoutText, const TextSpan *spans, size_t count, int wrapWidth, int surfaceWidth)
 Builds this Text's runs for its rendered texture: the texture is rendered from the layout string in white, so each color run is a region of it drawn in the color in effect for those characters, and each icon run is the placeholder region an icon is drawn over. The layout is SDL_ttf's own, so the regions are exact glyph clusters, including where wrapping broke the lines.
 
static void checkIcons (Text *self)
 Invalidates this Text if the icon atlas it was prepared against has changed: a Theme swap, attaching to a window, or an icon registered since. Sets needsLayout when it does, since the natural size may have changed.
 
static size_t colorEscapeLength (const char *chars)
 Length of the color escape at chars, or 0 if there is none: ^N selects a color, ^^ is a literal caret.
 
static void dealloc (Object *self)
 
static String * description (const Object *self)
 
static void didMoveToWindow (View *self, SDL_Window *window)
 
static const char * displayText (const Text *self)
 
static bool hasEscapes (const char *text)
 
static const char * iconPlaceholder (const Font *font)
 The placeholder an icon occupies in font's layout: a codepoint that is not a line-break opportunity, repeated until it is at least a line height wide. Resolved once per Font, on first use.
 
static ImageAtlas * iconsFor (const Text *self)
 
static View * init (View *self)
 
static void initialize (Class *clazz)
 
static Text * initWithText (Text *self, const char *text, Font *font)
 
static void invalidate (Text *self)
 Releases this Text's rendered texture and runs, and invalidates its cached size.
 
bool MVC_HasColorEscapes (const char *text)
 
size_t MVC_IconEscapeLength (const char *chars, const ImageAtlas *icons, AtlasImage **image)
 Parses an icon escape at the start of chars: :name:, where name is one or more of [A-Za-z0-9_-] and under 64 bytes, and is registered in icons. An unregistered name is not an escape, so 12:30:45 stays literal unless someone registers an icon called 30.
 
char * MVC_LayoutText (const Font *font, const char *text, SDL_Color color, const ImageAtlas *icons, TextSpan **spans, size_t *count)
 Resolves the escapes in text into the string to lay out and the spans to draw it as.
 
char * MVC_StripColorEscapes (const char *text)
 Strips color escape sequences from the given text: ^0 through ^9 are removed, and ^^ collapses to ^.
 
static SDL_Size naturalSize (const Text *self)
 
static void refreshFont (Text *self)
 Re-resolves this Text's Font at the current window's pixel density. Shared by didMoveToWindow and renderDeviceDidReset, rather than the latter dispatching through the public didMoveToWindow, which would re-trigger View's own attachment side effects.
 
static void render (View *self, Renderer *renderer)
 
static void renderDeviceDidReset (View *self)
 
static void renderDeviceWillReset (View *self)
 
static void resolveFont (Text *self, const FontAttributes *attributes)
 Resolves and applies the Font with the given attributes: through the window's Theme when attached, which supplies the window's pixel density, or at a density of 1.0 otherwise, to be re-resolved on attachment via View::didMoveToWindow. The cache's reference is retained to mirror the owned reference the fallback returns.
 
static void setFont (Text *self, Font *font)
 
static void setText (Text *self, const char *text)
 
static void setTextWithFormat (Text *self, const char *fmt,...)
 
static void setTransform (Text *self, TextTransform transform)
 
static SDL_Size sizeText (const Text *self, const char *text)
 
static SDL_Size sizeThatFits (const View *self)
 

Variables

SDL_Color TextEscapeColors []
 
const EnumName TextTransformNames []
 

Macro Definition Documentation

◆ _Class

#define _Class   _Text

Definition at line 37 of file Text.c.

◆ _GNU_SOURCE

#define _GNU_SOURCE

Definition at line 24 of file Text.c.

Function Documentation

◆ _Text()

Class * _Text ( void  )

Definition at line 988 of file Text.c.

988 {
989 static Class *clazz;
990 static Once once;
991
992 do_once(&once, {
993 clazz = _initialize(&(const ClassDef) {
994 .name = "Text",
995 .superclass = _View(),
996 .instanceSize = sizeof(Text),
997 .interfaceSize = sizeof(TextInterface),
998 .initialize = initialize,
999 });
1000 });
1001
1002 return clazz;
1003}
static void initialize(Class *clazz)
Definition Text.c:961
Class * _View(void)
Definition View.c:2336
Text rendered with TrueType fonts.
Definition Text.h:131

◆ append()

static void append ( LayoutBuffer *  buffer,
const char *  chars,
size_t  length 
)
static

Definition at line 150 of file Text.c.

150 {
151
152 if (buffer->length + length + 1 > buffer->capacity) {
153 buffer->capacity = max(buffer->capacity * 2, buffer->length + length + 1);
154 buffer->chars = realloc(buffer->chars, buffer->capacity);
155 assert(buffer->chars);
156 }
157
158 memcpy(buffer->chars + buffer->length, chars, length);
159 buffer->length += length;
160 buffer->chars[buffer->length] = '\0';
161}
size_t capacity
Definition Text.c:147
char * chars
Definition Text.c:145
size_t length
Definition Text.c:146

◆ appendRuns()

static void appendRuns ( Text *  self,
TTF_Text *  layout,
const TextSpan *  span,
int  surfaceWidth 
)
static

Appends the runs for one TextSpan of layout: one color run per line the span touches, or a single icon run positioned by the span's first line.

Definition at line 400 of file Text.c.

400 {
401
402 int count = 0;
403 TTF_SubString **substrings = TTF_GetTextSubStringsForRange(layout, span->offset, span->length, &count);
404 assert(substrings);
405
406 if (count == 0) {
407 SDL_free(substrings);
408 return;
409 }
410
411 TextRun *runs = realloc(self->runs, (self->runCount + count) * sizeof(TextRun));
412 assert(runs);
413 self->runs = runs;
414
415 if (span->icon) {
416 // The placeholder is one word, so it only splits when the wrap width is narrower than a
417 // line height; the icon stays where the escape is, in the first fragment, and is clamped
418 // to it when drawn
419 self->runs[self->runCount++] = (TextRun) { substrings[0]->rect, Colors.White, retain(span->icon) };
420 SDL_free(substrings);
421 return;
422 }
423
424 for (int i = 0; i < count; i++) {
425 SDL_Rect rect = substrings[i]->rect;
426
427 // A cluster's rect is its advance box; the last glyph on a line can ink past it, and the
428 // surface is sized to include that, so let the line's last run reach the surface's edge
429 if (substrings[i]->flags & TTF_SUBSTRING_LINE_END) {
430 rect.w = surfaceWidth - rect.x;
431 }
432
433 self->runs[self->runCount++] = (TextRun) { rect, span->color, NULL };
434 }
435
436 SDL_free(substrings);
437}
W3C Color constants.
Definition Colors.h:37
SDL_Color White
Definition Colors.h:185
TextRun * runs
The runs of texture, one textured quad each, when the text contains escapes; otherwise NULL,...
Definition Text.h:190
size_t runCount
The number of entries in runs.
Definition Text.h:196
One quad of a rendered TrueType Text: a region of its texture, in texels, drawn in color,...
Definition Text.h:118
int length
Definition Text.h:93
SDL_Color color
Definition Text.h:94
AtlasImage * icon
Definition Text.h:95
int offset
Definition Text.h:92

◆ appendSpan()

static void appendSpan ( TextSpan **  spans,
size_t *  count,
TextSpan  span 
)
static

Definition at line 207 of file Text.c.

207 {
208
209 if (spans == NULL) {
210 return;
211 }
212
213 TextSpan *grown = realloc(*spans, (*count + 1) * sizeof(TextSpan));
214 assert(grown);
215
216 *spans = grown;
217 (*spans)[(*count)++] = span;
218}
One span of a laid-out string, in bytes of the layout string MVC_LayoutText returns: either the chara...
Definition Text.h:91

◆ applyStyle()

static void applyStyle ( View *  self,
const Style *  style 
)
static
See also
View::applyStyle(View *, const Style *)

Definition at line 534 of file Text.c.

534 {
535
536 super(View, self, applyStyle, style);
537
538 Text *this = (Text *) self;
539
540 const Inlet colorInlets[] = MakeInlets(
541 MakeInlet("color", InletTypeColor, &this->color, NULL)
542 );
543
544 if ($(self, bind, colorInlets, style->attributes)) {
545 invalidate(this);
546 }
547
548 int transform = -1;
549
550 const Inlet transformInlets[] = MakeInlets(
551 MakeInlet("text-transform", InletTypeEnum, &transform, (ident) TextTransformNames)
552 );
553
554 $(self, bind, transformInlets, style->attributes);
555
556 $(this, setTransform, transform < 0 ? TextTransformNone : (TextTransform) transform);
557
558 char *fontFamily = NULL;
559 int fontSize = -1, fontStyle = -1;
560
561 const Inlet fontInlets[] = MakeInlets(
562 MakeInlet("font-family", InletTypeCharacters, &fontFamily, NULL),
563 MakeInlet("font-size", InletTypeInteger, &fontSize, NULL),
564 MakeInlet("font-style", InletTypeEnum, &fontStyle, (ident) FontStyleNames)
565 );
566
567 if ($(self, bind, fontInlets, style->attributes)) {
568
569 const FontAttributes attributes = { fontFamily, fontSize, fontStyle };
570 resolveFont(this, &attributes);
571
572 if (fontFamily) {
573 free(fontFamily);
574 }
575 }
576}
const EnumName FontStyleNames[]
Definition Font.c:42
static void invalidate(Text *self)
Releases this Text's rendered texture and runs, and invalidates its cached size.
Definition Text.c:279
static void applyStyle(View *self, const Style *style)
Definition Text.c:534
static void setTransform(Text *self, TextTransform transform)
Definition Text.c:944
const EnumName TextTransformNames[]
Definition Text.c:76
static void resolveFont(Text *self, const FontAttributes *attributes)
Resolves and applies the Font with the given attributes: through the window's Theme when attached,...
Definition Text.c:470
TextTransform
Case transforms a Text applies when drawing, leaving Text::text as set.
Definition Text.h:67
@ TextTransformNone
Definition Text.h:68
@ InletTypeEnum
Definition View+JSON.h:75
@ InletTypeCharacters
Definition View+JSON.h:52
@ InletTypeInteger
Definition View+JSON.h:89
@ InletTypeColor
Definition View+JSON.h:62
#define MakeInlets(...)
Creates a null-termianted array of Inlets.
Definition View+JSON.h:215
#define MakeInlet(name, type, dest, data)
Creates an Inlet with the specified parameters.
Definition View+JSON.h:210
The family, size and style attributes that identify a Font, independent of the pixel density at which...
Definition Font.h:64
Inlets enable inbound data binding of View attributes through JSON.
Definition View+JSON.h:149
Dictionary * attributes
Definition Style.h:59
Views are the fundamental building blocks of ObjectivelyMVC user interfaces.
Definition View.h:225

◆ applyTransform()

static void applyTransform ( Text *  self)
static

Rebuilds transformed from text and transform. Color escapes pass through without counting as letters, and icon escapes that resolve against the window's Theme are copied verbatim, since icon names are case sensitive.

Definition at line 311 of file Text.c.

311 {
312
313 free(self->transformed);
314 self->transformed = NULL;
315
316 if (self->transform == TextTransformNone || self->text == NULL) {
317 return;
318 }
319
320 self->transformed = strdup(self->text);
321 assert(self->transformed);
322
323 const ImageAtlas *icons = iconsFor(self);
324
325 bool wordStart = true;
326 for (char *c = self->transformed; *c; c++) {
327
328 if (colorEscapeLength(c)) {
329 c++;
330 continue;
331 }
332
333 if (*c == ':') {
334 const size_t length = MVC_IconEscapeLength(c, icons, NULL);
335 if (length) {
336 c += length - 1;
337 continue;
338 }
339 }
340
341 const unsigned char uc = (unsigned char) *c;
342
343 if ((uc & 0x80u) == 0) {
344 switch (self->transform) {
346 *c = (char) SDL_toupper(uc);
347 break;
349 *c = (char) SDL_tolower(uc);
350 break;
352 *c = (char) (wordStart ? SDL_toupper(uc) : SDL_tolower(uc));
353 break;
354 default:
355 break;
356 }
357 }
358
359 wordStart = SDL_isspace((unsigned char) *c) != 0;
360 }
361}
static size_t colorEscapeLength(const char *chars)
Length of the color escape at chars, or 0 if there is none: ^N selects a color, ^^ is a literal caret...
Definition Text.c:58
size_t MVC_IconEscapeLength(const char *chars, const ImageAtlas *icons, AtlasImage **image)
Parses an icon escape at the start of chars: :name:, where name is one or more of [A-Za-z0-9_-] and u...
Definition Text.c:104
static ImageAtlas * iconsFor(const Text *self)
Definition Text.c:297
@ TextTransformUppercase
Definition Text.h:69
@ TextTransformCapitalize
Definition Text.h:71
@ TextTransformLowercase
Definition Text.h:70
static ImageAtlas * icons(Theme *self)
Definition Theme.c:144
Packs many Images into one sheet, backed by one Texture.
Definition ImageAtlas.h:49
TextTransform transform
The case transform applied when drawing: the text-transform style attribute, or textTransform in JSON...
Definition Text.h:218
char * text
The text.
Definition Text.h:203

◆ awakeWithDictionary()

static void awakeWithDictionary ( View *  self,
const Dictionary *  dictionary 
)
static
See also
View::awakeWithDictionary(View *, const Dictionary *)

Definition at line 581 of file Text.c.

581 {
582
583 super(View, self, awakeWithDictionary, dictionary);
584
585 Text *this = (Text *) self;
586
587 int transform = this->transform;
588
589 const Inlet inlets[] = MakeInlets(
590 MakeInlet("color", InletTypeColor, &this->color, NULL),
591 MakeInlet("lineWrap", InletTypeBool, &this->lineWrap, NULL),
592 MakeInlet("text", InletTypeCharacters, &this->text, NULL),
593 MakeInlet("textTransform", InletTypeEnum, &transform, (ident) TextTransformNames)
594 );
595
596 $(self, bind, inlets, dictionary);
597
598 this->transform = (TextTransform) transform;
599
600 applyTransform(this);
601 invalidate(this);
602
603 $(self, sizeToFit);
604}
static void awakeWithDictionary(View *self, const Dictionary *dictionary)
Definition Text.c:581
static void applyTransform(Text *self)
Rebuilds transformed from text and transform. Color escapes pass through without counting as letters,...
Definition Text.c:311
@ InletTypeBool
Definition View+JSON.h:46
static void sizeToFit(View *self)
Definition View.c:2018

◆ buildRuns()

static void buildRuns ( Text *  self,
const char *  layoutText,
const TextSpan *  spans,
size_t  count,
int  wrapWidth,
int  surfaceWidth 
)
static

Builds this Text's runs for its rendered texture: the texture is rendered from the layout string in white, so each color run is a region of it drawn in the color in effect for those characters, and each icon run is the placeholder region an icon is drawn over. The layout is SDL_ttf's own, so the regions are exact glyph clusters, including where wrapping broke the lines.

Remarks
A cluster's rect is its advance box: ink that overhangs a run boundary within a line (italics, tight kerning) is clipped to whichever run owns that column.

Definition at line 448 of file Text.c.

448 {
449
450 assert(self->runs == NULL);
451
452 TTF_Text *layout = TTF_CreateText(NULL, self->font->font, layoutText, 0);
453 assert(layout);
454
455 TTF_SetTextWrapWidth(layout, (int) (wrapWidth * self->font->pixelDensity));
456
457 for (size_t i = 0; i < count; i++) {
458 appendRuns(self, layout, &spans[i], surfaceWidth);
459 }
460
461 TTF_DestroyText(layout);
462}
static void appendRuns(Text *self, TTF_Text *layout, const TextSpan *span, int surfaceWidth)
Appends the runs for one TextSpan of layout: one color run per line the span touches,...
Definition Text.c:400
float pixelDensity
The display pixel density scale, greater than 1.0 on high-density displays.
Definition Font.h:134
TTF_Font * font
The backing font.
Definition Font.h:122
Font * font
The Font. A fixed-width Font carries a bitmap, and is drawn as one quad per glyph from its glyph shee...
Definition Text.h:157

◆ checkIcons()

static void checkIcons ( Text *  self)
static

Invalidates this Text if the icon atlas it was prepared against has changed: a Theme swap, attaching to a window, or an icon registered since. Sets needsLayout when it does, since the natural size may have changed.

Definition at line 368 of file Text.c.

368 {
369
370 const ImageAtlas *icons = iconsFor(self);
371 const unsigned generation = icons ? icons->generation : 0;
372
373 if (icons != self->icons.atlas || generation != self->icons.generation) {
374 invalidate(self);
375 self->icons.atlas = icons;
376 self->icons.generation = generation;
377 applyTransform(self);
378 $((View *) self, setNeedsLayout);
379 }
380}
static void setNeedsLayout(View *self)
Definition View.c:1865
unsigned generation
Incremented by every ImageAtlas::addImageWithName and ImageAtlas::compile, so that anything derived f...
Definition ImageAtlas.h:66
const ImageAtlas * atlas
Definition Text.h:181
unsigned generation
Definition Text.h:182

◆ colorEscapeLength()

static size_t colorEscapeLength ( const char *  chars)
static

Length of the color escape at chars, or 0 if there is none: ^N selects a color, ^^ is a literal caret.

Definition at line 58 of file Text.c.

58 {
59 if (chars[0] == '^' && ((chars[1] >= '0' && chars[1] <= '9') || chars[1] == '^')) {
60 return 2;
61 }
62 return 0;
63}

◆ dealloc()

static void dealloc ( Object *  self)
static
See also
Object::dealloc(Object *)

Definition at line 495 of file Text.c.

495 {
496
497 Text *this = (Text *) self;
498
499 invalidate(this);
500
501 release(this->font);
502
503 free(this->text);
504 free(this->transformed);
505
506 super(Object, self, dealloc);
507}
static void dealloc(Object *self)
Definition Text.c:495
static Font * font(Theme *self, const FontAttributes *attributes, float pixelDensity)
Definition Theme.c:108

◆ description()

static String * description ( const Object *  self)
static
See also
Object::description(const Object *)

Definition at line 512 of file Text.c.

512 {
513
514 View *this = (View *) self;
515 const SDL_Rect bounds = $(this, bounds);
516
517 String *classNames = $((Object *) this->classNames, description);
518 String *description = str("%s@%p \"%s\" %s [%d, %d, %d, %d]",
519 this->identifier ?: classnameof(self),
520 self,
521 displayText((Text *) self) ?: "",
522 classNames->chars,
523 bounds.x, bounds.y, bounds.w, bounds.h);
524
525 release(classNames);
526 return description;
527}
static String * description(const Object *self)
Definition Text.c:512
static const char * displayText(const Text *self)
Definition Text.c:385
static SDL_Rect bounds(const View *self)
Definition View.c:552

◆ didMoveToWindow()

static void didMoveToWindow ( View *  self,
SDL_Window *  window 
)
static
See also
View::didMoveToWindow(View *, SDL_Window *)

Definition at line 624 of file Text.c.

624 {
625
626 super(View, self, didMoveToWindow, window);
627
628 // Nothing to resolve or size when detaching, and View::dealloc detaches after Text::dealloc has
629 // already released the Font, so touching it here would be a use-after-free
630 if (window) {
631
632 refreshFont((Text *) self);
633
634 // Icons resolve through the window's Theme; sizing re-prepares against it (see checkIcons)
635 $(self, sizeToFit);
636 }
637}
static void didMoveToWindow(View *self, SDL_Window *window)
Definition Text.c:624
static void refreshFont(Text *self)
Re-resolves this Text's Font at the current window's pixel density. Shared by didMoveToWindow and ren...
Definition Text.c:611

◆ displayText()

static const char * displayText ( const Text *  self)
static
Returns
The string this Text draws and measures: transformed when a transform is set.

Definition at line 385 of file Text.c.

385 {
386 return self->transformed ?: self->text;
387}

◆ hasEscapes()

static bool hasEscapes ( const char *  text)
static
Returns
True if text contains anything MVC_LayoutText might resolve: a caret or a colon.

Definition at line 392 of file Text.c.

392 {
393 return MVC_HasColorEscapes(text) || strchr(text, ':') != NULL;
394}
bool MVC_HasColorEscapes(const char *text)
Definition Text.c:65

◆ iconPlaceholder()

static const char * iconPlaceholder ( const Font *  font)
static

The placeholder an icon occupies in font's layout: a codepoint that is not a line-break opportunity, repeated until it is at least a line height wide. Resolved once per Font, on first use.

Measured with TTF_GetStringSize, the same engine that lays out and renders the text, rather than glyph metrics: Coda has no U+00A0, and shaping substitutes the space glyph for it at the space's advance, where the glyph metrics would report .notdef's.

Definition at line 171 of file Text.c.

171 {
172
173 if (font->iconPlaceholder) {
174 return font->iconPlaceholder;
175 }
176
177 static const Uint32 candidates[] = { 0x00A0, 0x2007, '_' };
178
179 Uint32 codepoint = candidates[lengthof(candidates) - 1];
180 for (size_t i = 0; i < lengthof(candidates); i++) {
181 if (TTF_FontHasGlyph(font->font, candidates[i])) {
182 codepoint = candidates[i];
183 break;
184 }
185 }
186
187 char utf8[5] = { 0 };
188 SDL_UCS4ToUTF8(codepoint, utf8);
189
190 const int lineHeight = TTF_GetFontHeight(font->font);
191
192 LayoutBuffer placeholder = { 0 };
193
194 for (int i = 0, width = 0; i < 16 && width < lineHeight; i++) {
195 append(&placeholder, utf8, strlen(utf8));
196 if (!TTF_GetStringSize(font->font, placeholder.chars, 0, &width, NULL)) {
197 break;
198 }
199 }
200
201 // A lazily resolved cache on an otherwise immutable Font, like its bitmap's Texture
202 ((Font *) font)->iconPlaceholder = placeholder.chars;
203
204 return font->iconPlaceholder;
205}
static void append(LayoutBuffer *buffer, const char *chars, size_t length)
Definition Text.c:150
TrueType fonts.
Definition Font.h:88
A growable string for MVC_LayoutText.
Definition Text.c:144

◆ iconsFor()

static ImageAtlas * iconsFor ( const Text *  self)
static
Returns
The icon ImageAtlas of this Text's window's Theme, or NULL when detached.

Definition at line 297 of file Text.c.

297 {
298
299 const View *view = (View *) self;
300
301 Theme *theme = view->window ? $$(Theme, theme, view->window) : NULL;
302
303 return theme ? $(theme, icons) : NULL;
304}
static Theme * theme(SDL_Window *window)
Definition Theme.c:295
The Theme type.
Definition Theme.h:54
SDL_Window * window
The window.
Definition View.h:480

◆ init()

static View * init ( View *  self)
static
See also
View::init(View *)

Definition at line 642 of file Text.c.

642 {
643 return (View *) $((Text *) self, initWithText, NULL, NULL);
644}
static Text * initWithText(Text *self, const char *text, Font *font)
Definition Text.c:808

◆ initialize()

static void initialize ( Class *  clazz)
static
See also
Class::initialize(Class *)

Definition at line 961 of file Text.c.

961 {
962
963 ((ObjectInterface *) clazz->interface)->dealloc = dealloc;
964 ((ObjectInterface *) clazz->interface)->description = description;
965
966 ((ViewInterface *) clazz->interface)->applyStyle = applyStyle;
967 ((ViewInterface *) clazz->interface)->awakeWithDictionary = awakeWithDictionary;
968 ((ViewInterface *) clazz->interface)->didMoveToWindow = didMoveToWindow;
969 ((ViewInterface *) clazz->interface)->init = init;
970 ((ViewInterface *) clazz->interface)->render = render;
971 ((ViewInterface *) clazz->interface)->renderDeviceDidReset = renderDeviceDidReset;
972 ((ViewInterface *) clazz->interface)->renderDeviceWillReset = renderDeviceWillReset;
973 ((ViewInterface *) clazz->interface)->sizeThatFits = sizeThatFits;
974
975 ((TextInterface *) clazz->interface)->initWithText = initWithText;
976 ((TextInterface *) clazz->interface)->naturalSize = naturalSize;
977 ((TextInterface *) clazz->interface)->setFont = setFont;
978 ((TextInterface *) clazz->interface)->setText = setText;
979 ((TextInterface *) clazz->interface)->setTextWithFormat = setTextWithFormat;
980 ((TextInterface *) clazz->interface)->setTransform = setTransform;
981 ((TextInterface *) clazz->interface)->sizeText = sizeText;
982}
static SDL_Size sizeText(const Text *self, const char *text)
Definition Text.c:853
static void setFont(Text *self, Font *font)
Definition Text.c:880
static void setTextWithFormat(Text *self, const char *fmt,...)
Definition Text.c:925
static View * init(View *self)
Definition Text.c:642
static void setText(Text *self, const char *text)
Definition Text.c:899
static SDL_Size sizeThatFits(const View *self)
Definition Text.c:798
static SDL_Size naturalSize(const Text *self)
Definition Text.c:823
static void render(View *self, Renderer *renderer)
Definition Text.c:649
static void renderDeviceWillReset(View *self)
Definition Text.c:788
static void renderDeviceDidReset(View *self)
Definition Text.c:776
SDL_Size naturalSize(const Text *self)
Resolves the rendered size of this Text.
Definition Text.c:823
void render(View *self, Renderer *renderer)
Renders this View using the given renderer.
Definition Control.c:181

◆ initWithText()

static Text * initWithText ( Text *  self,
const char *  text,
Font *  font 
)
static

Definition at line 808 of file Text.c.

808 {
809
810 self = (Text *) super(View, self, initWithFrame, NULL);
811 if (self) {
812 $(self, setFont, font);
813 $(self, setText, text);
814 }
815
816 return self;
817}
static Box * initWithFrame(Box *self, const SDL_Rect *frame)
Definition Box.c:92

◆ invalidate()

static void invalidate ( Text *  self)
static

Releases this Text's rendered texture and runs, and invalidates its cached size.

Definition at line 279 of file Text.c.

279 {
280
281 self->texture = release(self->texture);
282
283 for (size_t i = 0; i < self->runCount; i++) {
284 release(self->runs[i].icon);
285 }
286
287 free(self->runs);
288 self->runs = NULL;
289 self->runCount = 0;
290
291 self->naturalSizeCache.isValid = false;
292}
Texture * texture
The rendered GPU texture.
Definition Text.h:209
bool isValid
Definition Text.h:172
AtlasImage * icon
Definition Text.h:121

◆ MVC_HasColorEscapes()

bool MVC_HasColorEscapes ( const char *  text)
Parameters
textThe text, or NULL.
Returns
True if text contains a color escape (^0 through ^9, or ^^).

Definition at line 65 of file Text.c.

65 {
66
67 for (const char *p = text ? strchr(text, '^') : NULL; p; p = strchr(p + 1, '^')) {
68 if (colorEscapeLength(p)) {
69 return true;
70 }
71 }
72
73 return false;
74}

◆ MVC_IconEscapeLength()

size_t MVC_IconEscapeLength ( const char *  chars,
const ImageAtlas *  icons,
AtlasImage **  image 
)

Parses an icon escape at the start of chars: :name:, where name is one or more of [A-Za-z0-9_-] and under 64 bytes, and is registered in icons. An unregistered name is not an escape, so 12:30:45 stays literal unless someone registers an icon called 30.

Parameters
charsThe text, positioned at a candidate :.
iconsThe icon ImageAtlas, or NULL for none.
imageIf not NULL, receives the icon, or NULL.
Returns
The byte length of the escape, or 0 if chars does not start with one.

Definition at line 104 of file Text.c.

104 {
105
106 if (image) {
107 *image = NULL;
108 }
109
110 if (chars == NULL || icons == NULL || chars[0] != ':') {
111 return 0;
112 }
113
114 const char *q = chars + 1;
115 while (SDL_isalnum((unsigned char) *q) || *q == '_' || *q == '-') {
116 q++;
117 }
118
119 char name[64];
120 const size_t len = q - chars - 1;
121
122 if (*q != ':' || len == 0 || len >= sizeof(name)) {
123 return 0;
124 }
125
126 memcpy(name, chars + 1, len);
127 name[len] = '\0';
128
130 if (icon == NULL) {
131 return 0;
132 }
133
134 if (image) {
135 *image = icon;
136 }
137
138 return len + 2;
139}
static String * name(const Font *self)
Definition Font.c:303
static AtlasImage * imageWithName(const ImageAtlas *self, const char *name)
Definition ImageAtlas.c:112
static AtlasImage * icon(Theme *self, const char *name)
Definition Theme.c:175
static Image * image(Theme *self, const char *name, float pixelDensity)
Definition Theme.c:82
An Image packed into an ImageAtlas.
Definition AtlasImage.h:41

◆ MVC_LayoutText()

char * MVC_LayoutText ( const Font *  font,
const char *  text,
SDL_Color  color,
const ImageAtlas *  icons,
TextSpan **  spans,
size_t *  count 
)

Resolves the escapes in text into the string to lay out and the spans to draw it as.

^N selects a color for what follows, ^^ becomes ^, and a registered :icon: becomes a run of placeholder characters at least a line height wide, for the icon to be drawn over. The placeholder is never a line-break opportunity, so an icon stays with its word. With no icons, and spans NULL, the result is that of MVC_StripColorEscapes.

Parameters
fontThe Font the text will be laid out in.
textThe text.
colorThe color in effect at the start of text.
iconsThe icon ImageAtlas, or NULL for none.
spansIf not NULL, receives the spans, which the caller MUST free.
countIf not NULL, receives the number of spans; meaningful only with spans.
Returns
The layout string, which the caller MUST free.

Definition at line 220 of file Text.c.

220 {
221
222 assert(font);
223 assert(text);
224
225 LayoutBuffer layout = { 0 };
226 append(&layout, "", 0);
227
228 size_t n = 0;
229 if (spans) {
230 *spans = NULL;
231 }
232
233 size_t start = 0;
234
235 for (const char *p = text; ; ) {
236
237 const bool end = *p == '\0';
238 const bool escape = !end && p[0] == '^' && p[1] >= '0' && p[1] <= '9';
239
240 AtlasImage *icon = NULL;
241 const size_t iconLength = (!end && *p == ':') ? MVC_IconEscapeLength(p, icons, &icon) : 0;
242
243 if (end || escape || iconLength) {
244 if (layout.length > start) {
245 appendSpan(spans, &n, (TextSpan) { (int) start, (int) (layout.length - start), color, NULL });
246 }
247 if (end) {
248 break;
249 }
250 if (escape) {
251 color = TextEscapeColors[p[1] - '0'];
252 p += 2;
253 } else {
254 const char *placeholder = iconPlaceholder(font);
255 appendSpan(spans, &n, (TextSpan) { (int) layout.length, (int) strlen(placeholder), Colors.White, icon });
256 append(&layout, placeholder, strlen(placeholder));
257 p += iconLength;
258 }
259 start = layout.length;
260 } else if (p[0] == '^' && p[1] == '^') {
261 append(&layout, "^", 1);
262 p += 2;
263 } else {
264 append(&layout, p, 1);
265 p++;
266 }
267 }
268
269 if (count) {
270 *count = n;
271 }
272
273 return layout.chars;
274}
static void appendSpan(TextSpan **spans, size_t *count, TextSpan span)
Definition Text.c:207
SDL_Color TextEscapeColors[]
Definition Text.c:41
static const char * iconPlaceholder(const Font *font)
The placeholder an icon occupies in font's layout: a codepoint that is not a line-break opportunity,...
Definition Text.c:171

◆ MVC_StripColorEscapes()

char * MVC_StripColorEscapes ( const char *  text)

Strips color escape sequences from the given text: ^0 through ^9 are removed, and ^^ collapses to ^.

Parameters
textThe text.
Returns
A newly allocated copy of text without escape sequences. The caller must free it.

Definition at line 83 of file Text.c.

83 {
84
85 assert(text);
86
87 char *stripped = malloc(strlen(text) + 1);
88 size_t idx = 0;
89
90 for (const char *p = text; *p; p++) {
91 if (p[0] == '^' && p[1] >= '0' && p[1] <= '9') {
92 p++;
93 } else if (p[0] == '^' && p[1] == '^') {
94 stripped[idx++] = *p++;
95 } else {
96 stripped[idx++] = *p;
97 }
98 }
99
100 stripped[idx] = '\0';
101 return stripped;
102}

◆ naturalSize()

static SDL_Size naturalSize ( const Text *  self)
static

Definition at line 823 of file Text.c.

823 {
824
825 Font *font = self->font;
826 if (font == NULL) {
827 return MakeSize(0, 0);
828 }
829
830 // Nominally const, but a changed icon atlas invalidates the texture and runs here. Safe only
831 // because layout precedes drawing within a frame and the Renderer keeps unretained Textures.
832 Text *this = (Text *) self;
833
834 checkIcons(this);
835
836 if (self->naturalSizeCache.isValid && font->pixelDensity == self->naturalSizeCache.pixelDensity) {
837 return self->naturalSizeCache.size;
838 }
839
840 const SDL_Size size = $(self, sizeText, displayText(self) ?: "");
841
842 this->naturalSizeCache.size = size;
843 this->naturalSizeCache.pixelDensity = font->pixelDensity;
844 this->naturalSizeCache.isValid = true;
845
846 return size;
847}
static SDL_Size size(const Image *self)
Definition Image.c:416
static void checkIcons(Text *self)
Invalidates this Text if the icon atlas it was prepared against has changed: a Theme swap,...
Definition Text.c:368
float pixelDensity
Definition Text.h:171
SDL_Size size
Definition Text.h:170

◆ refreshFont()

static void refreshFont ( Text *  self)
static

Re-resolves this Text's Font at the current window's pixel density. Shared by didMoveToWindow and renderDeviceDidReset, rather than the latter dispatching through the public didMoveToWindow, which would re-trigger View's own attachment side effects.

Definition at line 611 of file Text.c.

611 {
612
613 const View *view = (View *) self;
614
615 if (view->window && self->font) {
616 const FontAttributes attributes = { self->font->family, self->font->size, self->font->style };
617 resolveFont(self, &attributes);
618 }
619}
char * family
The family name.
Definition Font.h:117
int size
The point size.
Definition Font.h:144
int style
The style.
Definition Font.h:149

◆ render()

static void render ( View *  self,
Renderer *  renderer 
)
static
See also
View::render(View *, Renderer *)

Definition at line 649 of file Text.c.

649 {
650
651 super(View, self, render, renderer);
652
653 Text *this = (Text *) self;
654
655 assert(this->font);
656
657 const float scale = this->font->pixelDensity;
658
659 if (this->text) {
660
661 checkIcons(this);
662
663 const char *text = displayText(this);
664
665 const SDL_Rect frame = $(self, renderFrame);
666
667 const int wrapWidth = this->lineWrap ? frame.w : 0;
668
669 if (this->font->bitmap.surface) {
670 $(this->font, renderBitmapCharacters, renderer, text, this->color, wrapWidth,
671 &(const SDL_Point) { frame.x, frame.y }, this->icons.atlas);
672 return;
673 }
674
675 if (this->texture == NULL) {
676 SDL_Surface *surface = NULL;
677
678 if (hasEscapes(text)) {
679 TextSpan *spans = NULL;
680 size_t count = 0;
681
682 char *layout = MVC_LayoutText(this->font, text, this->color, this->icons.atlas, &spans, &count);
683
684 // A colon or caret that resolved to nothing -- "Health: 100", a URL -- is plain text,
685 // and takes the single-quad path rather than a run per line
686 bool plain = true;
687 for (size_t i = 0; i < count && plain; i++) {
688 plain = spans[i].icon == NULL && SDL_memcmp(&spans[i].color, &this->color, sizeof(SDL_Color)) == 0;
689 }
690
691 // Text that is nothing but escapes has nothing to draw
692 if (*layout && plain) {
693 surface = $(this->font, renderCharacters, layout, this->color, wrapWidth);
694 } else if (*layout) {
695 surface = $(this->font, renderCharacters, layout, Colors.White, wrapWidth);
696 assert(surface);
697 buildRuns(this, layout, spans, count, wrapWidth, surface->w);
698 }
699
700 free(spans);
701 free(layout);
702
703 if (surface == NULL) {
704 return;
705 }
706 } else {
707 surface = $(this->font, renderCharacters, text, this->color, wrapWidth);
708 }
709
710 assert(surface);
711
712 const SDL_GPUTextureCreateInfo texInfo = {
713 .type = SDL_GPU_TEXTURETYPE_2D,
714 .format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM,
715 .usage = SDL_GPU_TEXTUREUSAGE_SAMPLER,
716 .width = (Uint32) surface->w,
717 .height = (Uint32) surface->h,
718 .layer_count_or_depth = 1,
719 .num_levels = 1,
720 };
721
722 this->texture = $(renderer->device, createTexture, &texInfo, surface->pixels);
723
724 SDL_DestroySurface(surface);
725 }
726
727 assert(this->texture);
728
729 // The destination size must be the texture's exact native size divided by scale, not that
730 // rounded to an integer: rounding it first, then having the renderer's projection
731 // multiply back by scale to reach physical pixels, lands on a physical width that differs
732 // from the texture's actual resolution -- stretching it by that (sub-)pixel remainder. Since
733 // the remainder depends on the string's own pixel width, this stretch changes with every
734 // keystroke, visibly shifting every glyph in the string, not just the one that was typed.
735
736 if (this->runs) {
737 const int lineHeight = TTF_GetFontHeight(this->font->font);
738
739 for (size_t i = 0; i < this->runCount; i++) {
740 const TextRun *run = &this->runs[i];
741
742 if (run->icon) {
743 // Drawn from the icon atlas, not this Text's texture, so an icon splits the Renderer's
744 // merged draw; centered in its placeholder, which is at least a line height wide
745 if (run->icon->atlas) {
746 const int side = min(lineHeight, run->src.w);
747 const SDL_FRect dest = {
748 frame.x + (run->src.x + (run->src.w - side) / 2) / scale, frame.y + run->src.y / scale,
749 side / scale, side / scale
750 };
751 Texture *texture = $(run->icon->atlas, texture, renderer->device);
752 $(renderer, drawTextureRegion, texture, &run->icon->rect, &dest, &Colors.White);
753 }
754 continue;
755 }
756
757 const SDL_FRect dest = {
758 frame.x + run->src.x / scale, frame.y + run->src.y / scale,
759 run->src.w / scale, run->src.h / scale
760 };
761 $(renderer, drawTextureRegion, this->texture, &run->src, &dest, &run->color);
762 }
763 } else {
764 const SDL_FRect dest = {
765 (float) frame.x, (float) frame.y,
766 this->texture->size.w / scale, this->texture->size.h / scale
767 };
768 $(renderer, drawTexture, this->texture, &dest, &Colors.White);
769 }
770 }
771}
static SDL_Surface * renderCharacters(const Font *self, const char *chars, SDL_Color color, int wrapWidth)
Definition Font.c:330
static Texture * texture(ImageAtlas *self, RenderDevice *device)
Definition ImageAtlas.c:289
static void drawTextureRegion(const Renderer *self, Texture *texture, const SDL_Rect *src, const SDL_FRect *dest, const SDL_Color *color)
Definition Renderer.c:497
static void drawTexture(const Renderer *self, Texture *texture, const SDL_FRect *dest, const SDL_Color *color)
Definition Renderer.c:489
static void buildRuns(Text *self, const char *layoutText, const TextSpan *spans, size_t count, int wrapWidth, int surfaceWidth)
Builds this Text's runs for its rendered texture: the texture is rendered from the layout string in w...
Definition Text.c:448
static bool hasEscapes(const char *text)
Definition Text.c:392
char * MVC_LayoutText(const Font *font, const char *text, SDL_Color color, const ImageAtlas *icons, TextSpan **spans, size_t *count)
Resolves the escapes in text into the string to lay out and the spans to draw it as.
Definition Text.c:220
static SDL_Rect renderFrame(const View *self)
Definition View.c:1629
SDL_Rect rect
This AtlasImage's rectangle within the atlas sheet, in texels.
Definition AtlasImage.h:66
ImageAtlas * atlas
The ImageAtlas this AtlasImage belongs to, or NULL once that atlas has been released,...
Definition AtlasImage.h:59
SDL_Surface * surface
The glyph sheet: a grid of FONT_BITMAP_COLUMNS by FONT_BITMAP_COLUMNS cells of cellSize,...
FontBitmap bitmap
The glyphs of a fixed-width Font, baked into one sheet; bitmap.surface is NULL for a proportional Fon...
Definition Font.h:107
RenderDevice * device
The backing RenderDevice.
Definition Renderer.h:97
SDL_Color color
Definition Text.h:120
SDL_Rect src
Definition Text.h:119

◆ renderDeviceDidReset()

static void renderDeviceDidReset ( View *  self)
static
See also
View::renderDeviceDidReset(View *)

Definition at line 776 of file Text.c.

776 {
777
778 refreshFont((Text *) self);
779
780 $(self, sizeToFit);
781
782 super(View, self, renderDeviceDidReset);
783}

◆ renderDeviceWillReset()

static void renderDeviceWillReset ( View *  self)
static
See also
View::renderDeviceWillReset(View *)

Definition at line 788 of file Text.c.

788 {
789
790 invalidate((Text *) self);
791
792 super(View, self, renderDeviceWillReset);
793}

◆ resolveFont()

static void resolveFont ( Text *  self,
const FontAttributes *  attributes 
)
static

Resolves and applies the Font with the given attributes: through the window's Theme when attached, which supplies the window's pixel density, or at a density of 1.0 otherwise, to be re-resolved on attachment via View::didMoveToWindow. The cache's reference is retained to mirror the owned reference the fallback returns.

Definition at line 470 of file Text.c.

470 {
471
472 View *view = (View *) self;
473
474 Theme *theme = view->window ? $$(Theme, theme, view->window) : NULL;
475
476 Font *font;
477 if (theme) {
478 const float pixelDensity = SDL_GetWindowPixelDensity(view->window);
479 font = retain($(theme, font, attributes, pixelDensity));
480 } else {
481 font = $$(Font, fontWithAttributes, attributes, 1.f);
482 }
483
484 assert(font);
485
486 $(self, setFont, font);
487 release(font);
488}
static Font * fontWithAttributes(const FontAttributes *attributes, float pixelDensity)
Definition Font.c:199

◆ setFont()

static void setFont ( Text *  self,
Font *  font 
)
static

Definition at line 880 of file Text.c.

880 {
881
882 font = font ?: $$(Font, defaultFont);
883
884 if (font != self->font) {
885
886 release(self->font);
887 self->font = retain(font);
888
889 invalidate(self);
890
891 $((View *) self, sizeToFit);
892 }
893}
static Font * defaultFont(void)
Definition Font.c:159

◆ setText()

static void setText ( Text *  self,
const char *  text 
)
static

Definition at line 899 of file Text.c.

899 {
900
901 if (strcmp(self->text ?: "", text ?: "")) {
902
903 free(self->text);
904
905 if (text && strlen(text)) {
906 self->text = strdup(text);
907 } else {
908 self->text = NULL;
909 }
910
911 applyTransform(self);
912 invalidate(self);
913
914 $((View *) self, sizeToFit);
915 }
916}

◆ setTextWithFormat()

static void setTextWithFormat ( Text *  self,
const char *  fmt,
  ... 
)
static

Definition at line 925 of file Text.c.

925 {
926
927 va_list args;
928 va_start(args, fmt);
929
930 char *text;
931 const int len = vasprintf(&text, fmt, args);
932 if (len >= 0) {
933 $(self, setText, text);
934 }
935
936 free(text);
937 va_end(args);
938}

◆ setTransform()

static void setTransform ( Text *  self,
TextTransform  transform 
)
static

Definition at line 944 of file Text.c.

944 {
945
946 if (transform != self->transform) {
947 self->transform = transform;
948
949 applyTransform(self);
950 invalidate(self);
951
952 $((View *) self, sizeToFit);
953 }
954}

◆ sizeText()

static SDL_Size sizeText ( const Text *  self,
const char *  text 
)
static

Definition at line 853 of file Text.c.

853 {
854
855 SDL_Size size = MakeSize(0, 0);
856
857 if (self->font == NULL || text == NULL) {
858 return size;
859 }
860
861 const ImageAtlas *icons = iconsFor(self);
862
863 if (self->font->bitmap.surface) {
864 $(self->font, sizeBitmapCharacters, text, 0, icons, &size.w, &size.h);
865 } else if (hasEscapes(text)) {
866 char *layout = MVC_LayoutText(self->font, text, self->color, icons, NULL, NULL);
867 $(self->font, sizeCharacters, layout, &size.w, &size.h);
868 free(layout);
869 } else {
870 $(self->font, sizeCharacters, text, &size.w, &size.h);
871 }
872
873 return size;
874}
static void sizeCharacters(const Font *self, const char *chars, int *w, int *h)
Definition Font.c:352
SDL_Color color
The text color.
Definition Text.h:148

◆ sizeThatFits()

static SDL_Size sizeThatFits ( const View *  self)
static
See also
View::sizeThatFits(View *)

Definition at line 798 of file Text.c.

798 {
799 return $((Text *) self, naturalSize);
800}

Variable Documentation

◆ TextEscapeColors

SDL_Color TextEscapeColors[]
Initial value:
= {
{ 0x00, 0x00, 0x00, 0xFF },
{ 0xFF, 0x00, 0x00, 0xFF },
{ 0x00, 0xFF, 0x00, 0xFF },
{ 0xFF, 0xFF, 0x00, 0xFF },
{ 0x00, 0x00, 0xFF, 0xFF },
{ 0xFF, 0x00, 0xFF, 0xFF },
{ 0x00, 0xFF, 0xFF, 0xFF },
{ 0xFF, 0xFF, 0xFF, 0xFF },
{ 0xFF, 0x80, 0x00, 0xFF },
{ 0x80, 0x80, 0x80, 0xFF }
}

Definition at line 41 of file Text.c.

41 {
42 { 0x00, 0x00, 0x00, 0xFF }, // ^0 Black
43 { 0xFF, 0x00, 0x00, 0xFF }, // ^1 Red
44 { 0x00, 0xFF, 0x00, 0xFF }, // ^2 Green
45 { 0xFF, 0xFF, 0x00, 0xFF }, // ^3 Yellow
46 { 0x00, 0x00, 0xFF, 0xFF }, // ^4 Blue
47 { 0xFF, 0x00, 0xFF, 0xFF }, // ^5 Magenta
48 { 0x00, 0xFF, 0xFF, 0xFF }, // ^6 Cyan
49 { 0xFF, 0xFF, 0xFF, 0xFF }, // ^7 White
50 { 0xFF, 0x80, 0x00, 0xFF }, // ^8 Orange
51 { 0x80, 0x80, 0x80, 0xFF } // ^9 Grey
52};

◆ TextTransformNames

const EnumName TextTransformNames[]
Initial value:
= MakeEnumNames(
MakeEnumAlias(TextTransformNone, none),
MakeEnumAlias(TextTransformUppercase, uppercase),
MakeEnumAlias(TextTransformLowercase, lowercase),
MakeEnumAlias(TextTransformCapitalize, capitalize)
)

Definition at line 76 of file Text.c.