ObjectivelyMVC
Object oriented MVC framework for SDL3 and GNU C
Loading...
Searching...
No Matches
Text.c
Go to the documentation of this file.
1/*
2 * ObjectivelyMVC: Object oriented MVC framework for SDL3 and C.
3 * Copyright (C) 2014 Jay Dolan <jay@jaydolan.com>
4 *
5 * This software is provided 'as-is', without any express or implied
6 * warranty. In no event will the authors be held liable for any damages
7 * arising from the use of this software.
8 *
9 * Permission is granted to anyone to use this software for any purpose,
10 * including commercial applications, and to alter it and redistribute it
11 * freely, subject to the following restrictions:
12 *
13 * 1. The origin of this software must not be misrepresented; you must not
14 * claim that you wrote the original software. If you use this software
15 * in a product, an acknowledgment in the product documentation would be
16 * appreciated but is not required.
17 *
18 * 2. Altered source versions must be plainly marked as such, and must not be
19 * misrepresented as being the original software.
20 *
21 * 3. This notice may not be removed or altered from any source distribution.
22 */
23
24#define _GNU_SOURCE
25
26#include <assert.h>
27#include <math.h>
28#include <stdarg.h>
29#include <stdlib.h>
30#include <stdio.h>
31#include <string.h>
32
33#include "Colors.h"
34#include "Text.h"
35#include "Theme.h"
36
37#define _Class _Text
38
39#pragma mark - Color Escape Sequences
40
41SDL_Color TextEscapeColors[] = {
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};
53
58static size_t colorEscapeLength(const char *chars) {
59 if (chars[0] == '^' && ((chars[1] >= '0' && chars[1] <= '9') || chars[1] == '^')) {
60 return 2;
61 }
62 return 0;
63}
64
65bool MVC_HasColorEscapes(const char *text) {
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}
75
76const EnumName TextTransformNames[] = MakeEnumNames(
77 MakeEnumAlias(TextTransformNone, none),
78 MakeEnumAlias(TextTransformUppercase, uppercase),
79 MakeEnumAlias(TextTransformLowercase, lowercase),
80 MakeEnumAlias(TextTransformCapitalize, capitalize)
81);
82
83char *MVC_StripColorEscapes(const char *text) {
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}
103
104size_t MVC_IconEscapeLength(const char *chars, const ImageAtlas *icons, AtlasImage **image) {
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}
140
144typedef struct {
145 char *chars;
146 size_t length;
147 size_t capacity;
149
150static void append(LayoutBuffer *buffer, const char *chars, size_t length) {
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}
162
171static const char *iconPlaceholder(const Font *font) {
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}
206
207static void appendSpan(TextSpan **spans, size_t *count, TextSpan span) {
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}
219
220char *MVC_LayoutText(const Font *font, const char *text, SDL_Color color, const ImageAtlas *icons, TextSpan **spans, size_t *count) {
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}
275
279static void invalidate(Text *self) {
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}
293
297static ImageAtlas *iconsFor(const Text *self) {
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}
305
311static void applyTransform(Text *self) {
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}
362
368static void checkIcons(Text *self) {
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}
381
385static const char *displayText(const Text *self) {
386 return self->transformed ?: self->text;
387}
388
392static bool hasEscapes(const char *text) {
393 return MVC_HasColorEscapes(text) || strchr(text, ':') != NULL;
394}
395
400static void appendRuns(Text *self, TTF_Text *layout, const TextSpan *span, int surfaceWidth) {
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}
438
448static void buildRuns(Text *self, const char *layoutText, const TextSpan *spans, size_t count, int wrapWidth, int surfaceWidth) {
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}
463
470static void resolveFont(Text *self, const FontAttributes *attributes) {
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}
489
490#pragma mark - ObjectInterface
491
495static void dealloc(Object *self) {
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}
508
512static String *description(const Object *self) {
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}
528
529#pragma mark - View
530
534static void applyStyle(View *self, const Style *style) {
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}
577
581static void awakeWithDictionary(View *self, const Dictionary *dictionary) {
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}
605
611static void refreshFont(Text *self) {
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}
620
624static void didMoveToWindow(View *self, SDL_Window *window) {
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}
638
642static View *init(View *self) {
643 return (View *) $((Text *) self, initWithText, NULL, NULL);
644}
645
649static void render(View *self, Renderer *renderer) {
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}
772
776static void renderDeviceDidReset(View *self) {
777
778 refreshFont((Text *) self);
779
780 $(self, sizeToFit);
781
782 super(View, self, renderDeviceDidReset);
783}
784
788static void renderDeviceWillReset(View *self) {
789
790 invalidate((Text *) self);
791
792 super(View, self, renderDeviceWillReset);
793}
794
798static SDL_Size sizeThatFits(const View *self) {
799 return $((Text *) self, naturalSize);
800}
801
802#pragma mark - Text
803
808static Text *initWithText(Text *self, const char *text, Font *font) {
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}
818
823static SDL_Size naturalSize(const Text *self) {
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}
848
853static SDL_Size sizeText(const Text *self, const char *text) {
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}
875
880static void setFont(Text *self, Font *font) {
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}
894
899static void setText(Text *self, const char *text) {
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}
917
925static void setTextWithFormat(Text *self, const char *fmt, ...) {
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}
939
944static void setTransform(Text *self, TextTransform transform) {
945
946 if (transform != self->transform) {
947 self->transform = transform;
948
949 applyTransform(self);
950 invalidate(self);
951
952 $((View *) self, sizeToFit);
953 }
954}
955
956#pragma mark - Class lifecycle
957
961static void initialize(Class *clazz) {
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}
983
988Class *_Text(void) {
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}
1004
1005#undef _Class
static Box * initWithFrame(Box *self, const SDL_Rect *frame)
Definition Box.c:92
W3C Color constants.
static void sizeCharacters(const Font *self, const char *chars, int *w, int *h)
Definition Font.c:352
static SDL_Surface * renderCharacters(const Font *self, const char *chars, SDL_Color color, int wrapWidth)
Definition Font.c:330
static Font * fontWithAttributes(const FontAttributes *attributes, float pixelDensity)
Definition Font.c:199
const EnumName FontStyleNames[]
Definition Font.c:42
static String * name(const Font *self)
Definition Font.c:303
static Font * defaultFont(void)
Definition Font.c:159
static SDL_Size size(const Image *self)
Definition Image.c:416
static AtlasImage * imageWithName(const ImageAtlas *self, const char *name)
Definition ImageAtlas.c:112
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
bool MVC_HasColorEscapes(const char *text)
Definition Text.c:65
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
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
Class * _Text(void)
Definition Text.c:988
static SDL_Size sizeText(const Text *self, const char *text)
Definition Text.c:853
char * MVC_StripColorEscapes(const char *text)
Strips color escape sequences from the given text: ^0 through ^9 are removed, and ^^ collapses to ^.
Definition Text.c:83
static void appendSpan(TextSpan **spans, size_t *count, TextSpan span)
Definition Text.c:207
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
SDL_Color TextEscapeColors[]
Definition Text.c:41
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
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
static void awakeWithDictionary(View *self, const Dictionary *dictionary)
Definition Text.c:581
static void invalidate(Text *self)
Releases this Text's rendered texture and runs, and invalidates its cached size.
Definition Text.c:279
static void setText(Text *self, const char *text)
Definition Text.c:899
static void applyStyle(View *self, const Style *style)
Definition Text.c:534
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 SDL_Size sizeThatFits(const View *self)
Definition Text.c:798
static String * description(const Object *self)
Definition Text.c:512
static SDL_Size naturalSize(const Text *self)
Definition Text.c:823
static void render(View *self, Renderer *renderer)
Definition Text.c:649
static void dealloc(Object *self)
Definition Text.c:495
static Text * initWithText(Text *self, const char *text, Font *font)
Definition Text.c:808
static void applyTransform(Text *self)
Rebuilds transformed from text and transform. Color escapes pass through without counting as letters,...
Definition Text.c:311
static void initialize(Class *clazz)
Definition Text.c:961
static void renderDeviceWillReset(View *self)
Definition Text.c:788
static bool hasEscapes(const char *text)
Definition Text.c:392
static void append(LayoutBuffer *buffer, const char *chars, size_t length)
Definition Text.c:150
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 void renderDeviceDidReset(View *self)
Definition Text.c:776
static void setTransform(Text *self, TextTransform transform)
Definition Text.c:944
const EnumName TextTransformNames[]
Definition Text.c:76
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
static ImageAtlas * iconsFor(const Text *self)
Definition Text.c:297
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 const char * displayText(const Text *self)
Definition Text.c:385
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
Text rendered with TrueType fonts.
TextTransform
Case transforms a Text applies when drawing, leaving Text::text as set.
Definition Text.h:67
@ TextTransformUppercase
Definition Text.h:69
@ TextTransformCapitalize
Definition Text.h:71
@ TextTransformLowercase
Definition Text.h:70
@ TextTransformNone
Definition Text.h:68
static AtlasImage * icon(Theme *self, const char *name)
Definition Theme.c:175
static ImageAtlas * icons(Theme *self)
Definition Theme.c:144
static Font * font(Theme *self, const FontAttributes *attributes, float pixelDensity)
Definition Theme.c:108
static Theme * theme(SDL_Window *window)
Definition Theme.c:295
static Image * image(Theme *self, const char *name, float pixelDensity)
Definition Theme.c:82
The Theme type.
@ InletTypeEnum
Definition View+JSON.h:75
@ InletTypeCharacters
Definition View+JSON.h:52
@ InletTypeBool
Definition View+JSON.h:46
@ 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
Class * _View(void)
Definition View.c:2336
static void setNeedsLayout(View *self)
Definition View.c:1865
static SDL_Rect renderFrame(const View *self)
Definition View.c:1629
static void sizeToFit(View *self)
Definition View.c:2018
static SDL_Rect bounds(const View *self)
Definition View.c:552
An Image packed into an ImageAtlas.
Definition AtlasImage.h:41
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
W3C Color constants.
Definition Colors.h:37
SDL_Color White
Definition Colors.h:185
The family, size and style attributes that identify a Font, independent of the pixel density at which...
Definition Font.h:64
SDL_Surface * surface
The glyph sheet: a grid of FONT_BITMAP_COLUMNS by FONT_BITMAP_COLUMNS cells of cellSize,...
TrueType fonts.
Definition Font.h:88
float pixelDensity
The display pixel density scale, greater than 1.0 on high-density displays.
Definition Font.h:134
char * family
The family name.
Definition Font.h:117
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
int size
The point size.
Definition Font.h:144
TTF_Font * font
The backing font.
Definition Font.h:122
int style
The style.
Definition Font.h:149
Packs many Images into one sheet, backed by one Texture.
Definition ImageAtlas.h:49
unsigned generation
Incremented by every ImageAtlas::addImageWithName and ImageAtlas::compile, so that anything derived f...
Definition ImageAtlas.h:66
Inlets enable inbound data binding of View attributes through JSON.
Definition View+JSON.h:149
A growable string for MVC_LayoutText.
Definition Text.c:144
size_t capacity
Definition Text.c:147
char * chars
Definition Text.c:145
size_t length
Definition Text.c:146
Renderer extends Object with ObjectivelyMVC's UI rendering layer.
Definition Renderer.h:75
RenderDevice * device
The backing RenderDevice.
Definition Renderer.h:97
The Style type.
Definition Style.h:43
Dictionary * attributes
Definition Style.h:59
Text rendered with TrueType fonts.
Definition Text.h:131
TextTransform transform
The case transform applied when drawing: the text-transform style attribute, or textTransform in JSON...
Definition Text.h:218
Texture * texture
The rendered GPU texture.
Definition Text.h:209
float pixelDensity
Definition Text.h:171
const ImageAtlas * atlas
Definition Text.h:181
TextRun * runs
The runs of texture, one textured quad each, when the text contains escapes; otherwise NULL,...
Definition Text.h:190
SDL_Size naturalSize(const Text *self)
Resolves the rendered size of this Text.
Definition Text.c:823
SDL_Size size
Definition Text.h:170
SDL_Color color
The text color.
Definition Text.h:148
bool isValid
Definition Text.h:172
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
size_t runCount
The number of entries in runs.
Definition Text.h:196
char * text
The text.
Definition Text.h:203
unsigned generation
Definition Text.h:182
One quad of a rendered TrueType Text: a region of its texture, in texels, drawn in color,...
Definition Text.h:118
AtlasImage * icon
Definition Text.h:121
SDL_Color color
Definition Text.h:120
SDL_Rect src
Definition Text.h:119
One span of a laid-out string, in bytes of the layout string MVC_LayoutText returns: either the chara...
Definition Text.h:91
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
The Theme type.
Definition Theme.h:54
Views are the fundamental building blocks of ObjectivelyMVC user interfaces.
Definition View.h:225
SDL_Window * window
The window.
Definition View.h:480
void render(View *self, Renderer *renderer)
Renders this View using the given renderer.
Definition Control.c:181