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
36static inline float view_pixel_density(SDL_Window *window) {
37 return window ? SDL_GetWindowPixelDensity(window) : 1.0f;
38}
39
40#define _Class _Text
41
42#pragma mark - Color Escape Sequences
43
44SDL_Color TextEscapeColors[] = {
45 { 0x00, 0x00, 0x00, 0xFF }, // ^0 Black
46 { 0xFF, 0x00, 0x00, 0xFF }, // ^1 Red
47 { 0x00, 0xFF, 0x00, 0xFF }, // ^2 Green
48 { 0xFF, 0xFF, 0x00, 0xFF }, // ^3 Yellow
49 { 0x00, 0x00, 0xFF, 0xFF }, // ^4 Blue
50 { 0xFF, 0x00, 0xFF, 0xFF }, // ^5 Magenta
51 { 0x00, 0xFF, 0xFF, 0xFF }, // ^6 Cyan
52 { 0xFF, 0xFF, 0xFF, 0xFF }, // ^7 White
53 { 0xFF, 0x80, 0x00, 0xFF }, // ^8 Orange
54 { 0x80, 0x80, 0x80, 0xFF } // ^9 Grey
55};
56
60static char *stripColors(const char *text) {
61
62 assert(text);
63
64 char *stripped = malloc(strlen(text) + 1);
65 int idx = 0;
66
67 for (const char *p = text; *p; p++) {
68 if (p[0] == '^' && p[1] >= '0' && p[1] <= '9') {
69 p++;
70 } else {
71 stripped[idx++] = *p;
72 }
73 }
74
75 stripped[idx] = '\0';
76 return stripped;
77}
78
82typedef struct {
83 SDL_Rect rect;
84 SDL_Color color;
85} CharInfo;
86
90static void colorize(SDL_Surface *surface, const CharInfo *info, float scale) {
91
92 const float fx = floorf(info->rect.x * scale);
93 const float fy = floorf(info->rect.y * scale);
94 const float fw = ceilf(info->rect.w * scale);
95 const float fh = ceilf(info->rect.h * scale);
96
97 int x = (int) fx - 1;
98 int y = (int) fy - 1;
99 int w = (int) fw + 2;
100 int h = (int) fh + 2;
101
102 if (w <= 0 || h <= 0) {
103 return;
104 }
105
106 if (x < 0) { w += x; x = 0; }
107 if (y < 0) { h += y; y = 0; }
108
109 uint32_t *pixels = (uint32_t *) surface->pixels;
110 const int pitch = surface->pitch / 4;
111 const SDL_Color color = info->color;
112
113 for (int row = y; row < y + h && row < surface->h; row++) {
114 for (int col = x; col < x + w && col < surface->w; col++) {
115 SDL_Color *c = (SDL_Color *) &pixels[row * pitch + col];
116 c->r = (int) (c->r * color.r / 255.0f);
117 c->g = (int) (c->g * color.g / 255.0f);
118 c->b = (int) (c->b * color.b / 255.0f);
119 }
120 }
121}
122
130static CharInfo *buildCharInfo(const Font *font, const char *text,
131 SDL_Color defaultColor, int wrapWidth, int *outCount) {
132
133 if (!text || !*text) {
134 *outCount = 0;
135 return NULL;
136 }
137
138 char *stripped = stripColors(text);
139 if (!stripped || !*stripped) {
140 free(stripped);
141 *outCount = 0;
142 return NULL;
143 }
144
145 const size_t strippedLen = strlen(stripped);
146 CharInfo *chars = malloc(sizeof(CharInfo) * strippedLen);
147
148 const int scaledWrapWidth = wrapWidth ? (int) (wrapWidth * font->scale) : 0;
149
150 int lineHeight;
151 $(font, sizeCharacters, "A", NULL, &lineHeight);
152
153 SDL_Color currentColor = defaultColor;
154 int charIdx = 0;
155 int lineStartByte = 0;
156 int prevH = 0;
157
158 for (const char *p = text; *p; p++) {
159 if (p[0] == '^' && p[1] >= '0' && p[1] <= '9') {
160 currentColor = TextEscapeColors[p[1] - '0'];
161 p++;
162 continue;
163 }
164
165 int currH;
166 if (scaledWrapWidth) {
167 TTF_GetStringSizeWrapped(font->font, stripped, charIdx + 1, scaledWrapWidth, NULL, &currH);
168 } else {
169 TTF_GetStringSize(font->font, stripped, charIdx + 1, NULL, &currH);
170 }
171
172 if (currH > prevH) {
173 lineStartByte = charIdx;
174 prevH = currH;
175 }
176
177 int lineX = 0;
178 if (charIdx > lineStartByte) {
179 TTF_GetStringSize(font->font, stripped + lineStartByte, charIdx - lineStartByte, &lineX, NULL);
180 }
181
182 int charW;
183 TTF_GetStringSize(font->font, stripped + charIdx, 1, &charW, NULL);
184
185 chars[charIdx].rect.x = (int) (lineX / font->scale);
186 chars[charIdx].rect.y = (int) (currH / font->scale) - lineHeight;
187 chars[charIdx].rect.w = (int) (charW / font->scale);
188 chars[charIdx].rect.h = lineHeight;
189 chars[charIdx].color = currentColor;
190
191 charIdx++;
192 }
193
194 free(stripped);
195 *outCount = charIdx;
196 return chars;
197}
198
202static SDL_Surface *renderWithColorEscapes(const Text *self, int wrapWidth) {
203
204 char *stripped = stripColors(self->text);
205 SDL_Surface *surface = $(self->font, renderCharacters, stripped, self->color, wrapWidth);
206 free(stripped);
207
208 if (!surface) {
209 return NULL;
210 }
211
212 int charCount = 0;
213 CharInfo *charInfo = buildCharInfo(self->font, self->text, self->color, wrapWidth, &charCount);
214
215 if (charInfo) {
216 SDL_LockSurface(surface);
217 for (int i = 0; i < charCount; i++) {
218 colorize(surface, &charInfo[i], self->font->scale);
219 }
220 SDL_UnlockSurface(surface);
221 free(charInfo);
222 }
223
224 return surface;
225}
226
230static void sizeWithColorEscapes(const Text *self, int *w, int *h) {
231
232 char *stripped = stripColors(self->text ?: "");
233 $(self->font, sizeCharacters, stripped, w, h);
234 free(stripped);
235}
236
237#pragma mark - ObjectInterface
238
242static void dealloc(Object *self) {
243
244 Text *this = (Text *) self;
245
246 release(this->font);
247
248 free(this->text);
249
250 this->texture = release(this->texture);
251
252 super(Object, self, dealloc);
253}
254
258static String *description(const Object *self) {
259
260 View *this = (View *) self;
261 const SDL_Rect bounds = $(this, bounds);
262
263 String *classNames = $((Object *) this->classNames, description);
264 String *description = str("%s@%p \"%s\" %s [%d, %d, %d, %d]",
265 this->identifier ?: classnameof(self),
266 self,
267 ((Text *) self)->text,
268 classNames->chars,
269 bounds.x, bounds.y, bounds.w, bounds.h);
270
271 release(classNames);
272 return description;
273}
274
275#pragma mark - View
276
280static void applyStyle(View *self, const Style *style) {
281
282 super(View, self, applyStyle, style);
283
284 Text *this = (Text *) self;
285
286 const Inlet colorInlets[] = MakeInlets(
287 MakeInlet("color", InletTypeColor, &this->color, NULL)
288 );
289
290 if ($(self, bind, colorInlets, style->attributes)) {
291 this->texture = release(this->texture);
292 this->textureSize = MakeSize(0, 0);
293 }
294
295 char *fontFamily = NULL;
296 int fontSize = -1, fontStyle = -1;
297
298 const Inlet fontInlets[] = MakeInlets(
299 MakeInlet("font-family", InletTypeCharacters, &fontFamily, NULL),
300 MakeInlet("font-size", InletTypeInteger, &fontSize, NULL),
301 MakeInlet("font-style", InletTypeEnum, &fontStyle, (ident) FontStyleNames)
302 );
303
304 if ($(self, bind, fontInlets, style->attributes)) {
305
306 Font *font = $$(Font, cachedFont, fontFamily , fontSize, fontStyle);
307 assert(font);
308
309 $(this, setFont, font);
310
311 if (fontFamily) {
312 free(fontFamily);
313 }
314 }
315}
316
320static void awakeWithDictionary(View *self, const Dictionary *dictionary) {
321
322 super(View, self, awakeWithDictionary, dictionary);
323
324 Text *this = (Text *) self;
325
326 const Inlet inlets[] = MakeInlets(
327 MakeInlet("color", InletTypeColor, &this->color, NULL),
328 MakeInlet("colorEscapes", InletTypeBool, &this->colorEscapes, NULL),
329 MakeInlet("font", InletTypeFont, &this->font, NULL),
330 MakeInlet("lineWrap", InletTypeBool, &this->lineWrap, NULL),
331 MakeInlet("text", InletTypeCharacters, &this->text, NULL)
332 );
333
334 $(self, bind, inlets, dictionary);
335
336 $(self, sizeToFit);
337}
338
342static View *init(View *self) {
343 return (View *) $((Text *) self, initWithText, NULL, NULL);
344}
345
349static void render(View *self, Renderer *renderer) {
350
351 super(View, self, render, renderer);
352
353 Text *this = (Text *) self;
354
355 assert(this->font);
356
357 const float scale = view_pixel_density(self->window);
358
359 if (this->font->scale != scale) {
360 this->font->scale = scale;
361 $(this->font, renderDeviceDidReset);
362 this->texture = release(this->texture);
363 this->textureSize = MakeSize(0, 0);
364 }
365
366 if (this->text) {
367
368 const SDL_Rect frame = $(self, renderFrame);
369
370 if (this->texture == NULL) {
371 SDL_Surface *surface;
372
373 if (this->colorEscapes) {
374 surface = renderWithColorEscapes(this, this->lineWrap ? frame.w : 0);
375 } else {
376 surface = $(this->font, renderCharacters,
377 this->text,
378 this->color,
379 this->lineWrap ? frame.w : 0);
380 }
381
382 assert(surface);
383
384 const float textureWidth = roundf(surface->w / scale);
385 const float textureHeight = roundf(surface->h / scale);
386
387 this->textureSize.w = (int) textureWidth;
388 this->textureSize.h = (int) textureHeight;
389
390 SDL_Surface *upload = surface;
391 SDL_Surface *converted = NULL;
392
393 if (SDL_BYTESPERPIXEL(surface->format) == 1) {
394 converted = SDL_CreateSurface(surface->w, surface->h, SDL_PIXELFORMAT_RGBA32);
395 assert(converted);
396 const SDL_PixelFormatDetails *details = SDL_GetPixelFormatDetails(SDL_PIXELFORMAT_RGBA32);
397 assert(details);
398 const Uint8 *src = (const Uint8 *) surface->pixels;
399 Uint32 *dst = (Uint32 *) converted->pixels;
400 for (int y = 0; y < surface->h; y++) {
401 for (int x = 0; x < surface->w; x++) {
402 const Uint8 a = src[y * surface->pitch + x];
403 dst[y * surface->w + x] = SDL_MapRGBA(details, NULL, 255, 255, 255, a);
404 }
405 }
406 upload = converted;
407 } else if (surface->format != SDL_PIXELFORMAT_RGBA32) {
408 converted = SDL_ConvertSurface(surface, SDL_PIXELFORMAT_RGBA32);
409 assert(converted);
410 upload = converted;
411 }
412
413 const SDL_GPUTextureCreateInfo texInfo = {
414 .type = SDL_GPU_TEXTURETYPE_2D,
415 .format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM,
416 .usage = SDL_GPU_TEXTUREUSAGE_SAMPLER,
417 .width = (Uint32) upload->w,
418 .height = (Uint32) upload->h,
419 .layer_count_or_depth = 1,
420 .num_levels = 1,
421 };
422
423 this->texture = $(renderer->device, createTexture, &texInfo, upload->pixels);
424
425 if (converted) {
426 SDL_DestroySurface(converted);
427 }
428
429 SDL_DestroySurface(surface);
430 }
431
432 assert(this->texture);
433
434 const SDL_Rect draw_rect = { frame.x, frame.y, this->textureSize.w, this->textureSize.h };
435 $(renderer, drawTexture, this->texture, &draw_rect, &Colors.White);
436 }
437}
438
442static void renderDeviceDidReset(View *self) {
443
444 Text *this = (Text *) self;
445
446 this->font->scale = view_pixel_density(self->window);
447 $(this->font, renderDeviceDidReset);
448
449 super(View, self, renderDeviceDidReset);
450}
451
455static void renderDeviceWillReset(View *self) {
456
457 Text *this = (Text *) self;
458
459 this->texture = release(this->texture);
460 this->textureSize = MakeSize(0, 0);
461
462 super(View, self, renderDeviceWillReset);
463}
464
468static SDL_Size sizeThatFits(const View *self) {
469 return $((Text *) self, naturalSize);
470}
471
472#pragma mark - Text
473
478static Text *initWithText(Text *self, const char *text, Font *font) {
479
480 self = (Text *) super(View, self, initWithFrame, NULL);
481 if (self) {
482 $(self, setFont, font);
483 $(self, setText, text);
484 }
485
486 return self;
487}
488
493static SDL_Size naturalSize(const Text *self) {
494
495 SDL_Size size = MakeSize(0, 0);
496
497 if (self->font) {
498 const char *text = self->text ?: "";
499
500 if (self->colorEscapes) {
501 sizeWithColorEscapes(self, &size.w, &size.h);
502 } else {
503 $(self->font, sizeCharacters, text, &size.w, &size.h);
504 }
505 }
506
507 return size;
508}
509
514static void setFont(Text *self, Font *font) {
515
516 font = font ?: $$(Font, defaultFont);
517
518 if (font != self->font) {
519
520 release(self->font);
521 self->font = retain(font);
522
523 self->texture = release(self->texture);
524 self->textureSize = MakeSize(0, 0);
525
526 $((View *) self, sizeToFit);
527 }
528}
529
534static void setText(Text *self, const char *text) {
535
536 if (strcmp(self->text ?: "", text ?: "")) {
537
538 free(self->text);
539
540 if (text && strlen(text)) {
541 self->text = strdup(text);
542 } else {
543 self->text = NULL;
544 }
545
546 self->texture = release(self->texture);
547 self->textureSize = MakeSize(0, 0);
548
549 $((View *) self, sizeToFit);
550 }
551}
552
560static void setTextWithFormat(Text *self, const char *fmt, ...) {
561
562 va_list args;
563 va_start(args, fmt);
564
565 char *text;
566 const int len = vasprintf(&text, fmt, args);
567 if (len >= 0) {
568 $(self, setText, text);
569 }
570
571 free(text);
572 va_end(args);
573}
574
575#pragma mark - Class lifecycle
576
580static void initialize(Class *clazz) {
581
582 ((ObjectInterface *) clazz->interface)->dealloc = dealloc;
583 ((ObjectInterface *) clazz->interface)->description = description;
584
585 ((ViewInterface *) clazz->interface)->applyStyle = applyStyle;
586 ((ViewInterface *) clazz->interface)->awakeWithDictionary = awakeWithDictionary;
587 ((ViewInterface *) clazz->interface)->init = init;
588 ((ViewInterface *) clazz->interface)->render = render;
589 ((ViewInterface *) clazz->interface)->renderDeviceDidReset = renderDeviceDidReset;
590 ((ViewInterface *) clazz->interface)->renderDeviceWillReset = renderDeviceWillReset;
591 ((ViewInterface *) clazz->interface)->sizeThatFits = sizeThatFits;
592
593 ((TextInterface *) clazz->interface)->initWithText = initWithText;
594 ((TextInterface *) clazz->interface)->naturalSize = naturalSize;
595 ((TextInterface *) clazz->interface)->setFont = setFont;
596 ((TextInterface *) clazz->interface)->setText = setText;
597 ((TextInterface *) clazz->interface)->setTextWithFormat = setTextWithFormat;
598}
599
604Class *_Text(void) {
605 static Class *clazz;
606 static Once once;
607
608 do_once(&once, {
609 clazz = _initialize(&(const ClassDef) {
610 .name = "Text",
611 .superclass = _View(),
612 .instanceSize = sizeof(Text),
613 .interfaceOffset = offsetof(Text, interface),
614 .interfaceSize = sizeof(TextInterface),
616 });
617 });
618
619 return clazz;
620}
621
622#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:274
static SDL_Surface * renderCharacters(const Font *self, const char *chars, SDL_Color color, int wrapWidth)
Definition Font.c:224
static Font * cachedFont(const char *family, int size, int style)
Definition Font.c:127
const EnumName FontStyleNames[]
Definition Font.c:41
static Font * defaultFont(void)
Definition Font.c:178
static SDL_Size size(const Image *self)
Definition Image.c:181
static void drawTexture(const Renderer *self, Texture *texture, const SDL_Rect *rect, const SDL_Color *color)
Definition Renderer.c:223
static SDL_Surface * renderWithColorEscapes(const Text *self, int wrapWidth)
Renders text with color escape sequences applied to an SDL_Surface.
Definition Text.c:202
Class * _Text(void)
Definition Text.c:604
static void setFont(Text *self, Font *font)
Definition Text.c:514
static void setTextWithFormat(Text *self, const char *fmt,...)
Definition Text.c:560
static View * init(View *self)
Definition Text.c:342
SDL_Color TextEscapeColors[]
Definition Text.c:44
static void sizeWithColorEscapes(const Text *self, int *w, int *h)
Resolves the rendered size of this Text's content, stripping color escapes.
Definition Text.c:230
static CharInfo * buildCharInfo(const Font *font, const char *text, SDL_Color defaultColor, int wrapWidth, int *outCount)
Builds a CharInfo table for text containing color escape sequences.
Definition Text.c:130
static void awakeWithDictionary(View *self, const Dictionary *dictionary)
Definition Text.c:320
static void setText(Text *self, const char *text)
Definition Text.c:534
static void applyStyle(View *self, const Style *style)
Definition Text.c:280
static SDL_Size sizeThatFits(const View *self)
Definition Text.c:468
static String * description(const Object *self)
Definition Text.c:258
static SDL_Size naturalSize(const Text *self)
Definition Text.c:493
static char * stripColors(const char *text)
Strips color escape sequences from text, returning a newly allocated string.
Definition Text.c:60
static void colorize(SDL_Surface *surface, const CharInfo *info, float scale)
Colorizes a character's rect on a locked SDL_Surface.
Definition Text.c:90
static void render(View *self, Renderer *renderer)
Definition Text.c:349
static float view_pixel_density(SDL_Window *window)
Definition Text.c:36
static void dealloc(Object *self)
Definition Text.c:242
static Text * initWithText(Text *self, const char *text, Font *font)
Definition Text.c:478
static void initialize(Class *clazz)
Definition Text.c:580
static void renderDeviceWillReset(View *self)
Definition Text.c:455
static void renderDeviceDidReset(View *self)
Definition Text.c:442
Text rendered with TrueType fonts.
@ InletTypeEnum
Definition View+JSON.h:75
@ InletTypeCharacters
Definition View+JSON.h:52
@ InletTypeBool
Definition View+JSON.h:46
@ InletTypeFont
Definition View+JSON.h:85
@ InletTypeInteger
Definition View+JSON.h:95
@ InletTypeColor
Definition View+JSON.h:62
#define MakeInlets(...)
Creates a null-termianted array of Inlets.
Definition View+JSON.h:221
#define MakeInlet(name, type, dest, data)
Creates an Inlet with the specified parameters.
Definition View+JSON.h:216
Class * _View(void)
Definition View.c:2056
static SDL_Rect renderFrame(const View *self)
Definition View.c:1444
static void sizeToFit(View *self)
Definition View.c:1756
static SDL_Rect bounds(const View *self)
Definition View.c:492
Character position and color for one visible character in a color-escaped string.
Definition Text.c:82
SDL_Rect rect
Definition Text.c:83
SDL_Color color
Definition Text.c:84
W3C Color constants.
Definition Colors.h:37
SDL_Color White
Definition Colors.h:185
TrueType fonts.
Definition Font.h:63
float scale
The display pixel density scale (e.g. 2.0 on Retina). Updated by callers via the scale field before i...
Definition Font.h:100
TTF_Font * font
The backing font.
Definition Font.h:89
Inlets enable inbound data binding of View attributes through JSON.
Definition View+JSON.h:155
Renderer extends Object with ObjectivelyMVC's UI rendering layer.
Definition Renderer.h:70
RenderDevice * device
The backing RenderDevice.
Definition Renderer.h:92
The Style type.
Definition Style.h:43
Dictionary * attributes
Definition Style.h:59
Text rendered with TrueType fonts.
Definition Text.h:69
SDL_Size textureSize
The logical draw dimensions of the texture (surface size / pixel density).
Definition Text.h:126
Texture * texture
The rendered GPU texture.
Definition Text.h:119
bool colorEscapes
If true, render text with color escape sequence support (^0-^7).
Definition Text.h:101
SDL_Size naturalSize(const Text *self)
Resolves the rendered size of this Text.
Definition Text.c:493
SDL_Color color
The text color.
Definition Text.h:85
Font * font
The Font.
Definition Text.h:92
char * text
The text.
Definition Text.h:113
Views are the fundamental building blocks of ObjectivelyMVC user interfaces.
Definition View.h:134
SDL_Window * window
The window.
Definition View.h:277
void render(View *self, Renderer *renderer)
Renders this View using the given renderer.
Definition Control.c:179