39#pragma mark - Color Escape Sequences
42 { 0x00, 0x00, 0x00, 0xFF },
43 { 0xFF, 0x00, 0x00, 0xFF },
44 { 0x00, 0xFF, 0x00, 0xFF },
45 { 0xFF, 0xFF, 0x00, 0xFF },
46 { 0x00, 0x00, 0xFF, 0xFF },
47 { 0xFF, 0x00, 0xFF, 0xFF },
48 { 0x00, 0xFF, 0xFF, 0xFF },
49 { 0xFF, 0xFF, 0xFF, 0xFF },
50 { 0xFF, 0x80, 0x00, 0xFF },
51 { 0x80, 0x80, 0x80, 0xFF }
59 if (chars[0] ==
'^' && ((chars[1] >=
'0' && chars[1] <=
'9') || chars[1] ==
'^')) {
67 for (
const char *p = text ? strchr(text,
'^') : NULL; p; p = strchr(p + 1,
'^')) {
87 char *stripped = malloc(strlen(text) + 1);
90 for (
const char *p = text; *p; p++) {
91 if (p[0] ==
'^' && p[1] >=
'0' && p[1] <=
'9') {
93 }
else if (p[0] ==
'^' && p[1] ==
'^') {
94 stripped[idx++] = *p++;
100 stripped[idx] =
'\0';
110 if (chars == NULL ||
icons == NULL || chars[0] !=
':') {
114 const char *q = chars + 1;
115 while (SDL_isalnum((
unsigned char) *q) || *q ==
'_' || *q ==
'-') {
120 const size_t len = q - chars - 1;
122 if (*q !=
':' || len == 0 || len >=
sizeof(
name)) {
126 memcpy(
name, chars + 1, len);
155 assert(buffer->
chars);
158 memcpy(buffer->
chars + buffer->
length, chars, length);
173 if (
font->iconPlaceholder) {
174 return font->iconPlaceholder;
177 static const Uint32 candidates[] = { 0x00A0, 0x2007,
'_' };
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];
187 char utf8[5] = { 0 };
188 SDL_UCS4ToUTF8(codepoint, utf8);
190 const int lineHeight = TTF_GetFontHeight(
font->
font);
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)) {
204 return font->iconPlaceholder;
217 (*spans)[(*count)++] = span;
235 for (
const char *p = text; ; ) {
237 const bool end = *p ==
'\0';
238 const bool escape = !end && p[0] ==
'^' && p[1] >=
'0' && p[1] <=
'9';
243 if (end || escape || iconLength) {
244 if (layout.
length > start) {
256 append(&layout, placeholder, strlen(placeholder));
260 }
else if (p[0] ==
'^' && p[1] ==
'^') {
283 for (
size_t i = 0; i < self->
runCount; i++) {
291 self->naturalSizeCache.
isValid =
false;
313 free(self->transformed);
314 self->transformed = NULL;
320 self->transformed = strdup(self->
text);
321 assert(self->transformed);
325 bool wordStart =
true;
326 for (
char *c = self->transformed; *c; c++) {
341 const unsigned char uc = (
unsigned char) *c;
343 if ((uc & 0x80u) == 0) {
346 *c = (char) SDL_toupper(uc);
349 *c = (char) SDL_tolower(uc);
352 *c = (char) (wordStart ? SDL_toupper(uc) : SDL_tolower(uc));
359 wordStart = SDL_isspace((
unsigned char) *c) != 0;
386 return self->transformed ?: self->
text;
403 TTF_SubString **substrings = TTF_GetTextSubStringsForRange(layout, span->
offset, span->
length, &count);
407 SDL_free(substrings);
420 SDL_free(substrings);
424 for (
int i = 0; i < count; i++) {
425 SDL_Rect rect = substrings[i]->rect;
429 if (substrings[i]->flags & TTF_SUBSTRING_LINE_END) {
430 rect.w = surfaceWidth - rect.x;
436 SDL_free(substrings);
448static void buildRuns(
Text *self,
const char *layoutText,
const TextSpan *spans,
size_t count,
int wrapWidth,
int surfaceWidth) {
450 assert(self->
runs == NULL);
452 TTF_Text *layout = TTF_CreateText(NULL, self->
font->
font, layoutText, 0);
457 for (
size_t i = 0; i < count; i++) {
458 appendRuns(self, layout, &spans[i], surfaceWidth);
461 TTF_DestroyText(layout);
478 const float pixelDensity = SDL_GetWindowPixelDensity(view->
window);
490#pragma mark - ObjectInterface
504 free(this->transformed);
517 String *classNames = $((Object *) this->classNames,
description);
518 String *
description = str(
"%s@%p \"%s\" %s [%d, %d, %d, %d]",
519 this->identifier ?: classnameof(self),
544 if ($(self, bind, colorInlets, style->
attributes)) {
554 $(self, bind, transformInlets, style->
attributes);
558 char *fontFamily = NULL;
559 int fontSize = -1, fontStyle = -1;
567 if ($(self, bind, fontInlets, style->
attributes)) {
569 const FontAttributes attributes = { fontFamily, fontSize, fontStyle };
587 int transform = this->transform;
596 $(self, bind, inlets, dictionary);
667 const int wrapWidth = this->lineWrap ? frame.w : 0;
670 $(this->
font, renderBitmapCharacters, renderer, text, this->color, wrapWidth,
671 &(
const SDL_Point) { frame.x, frame.y }, this->
icons.atlas);
676 SDL_Surface *surface = NULL;
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;
692 if (*layout && plain) {
694 }
else if (*layout) {
697 buildRuns(
this, layout, spans, count, wrapWidth, surface->w);
703 if (surface == NULL) {
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,
722 this->
texture = $(renderer->
device, createTexture, &texInfo, surface->pixels);
724 SDL_DestroySurface(surface);
737 const int lineHeight = TTF_GetFontHeight(this->
font->
font);
739 for (
size_t i = 0; i < this->runCount; i++) {
740 const TextRun *run = &this->runs[i];
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
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
764 const SDL_FRect dest = {
765 (float) frame.x, (
float) frame.y,
827 return MakeSize(0, 0);
837 return self->naturalSizeCache.
size;
842 this->naturalSizeCache.size =
size;
844 this->naturalSizeCache.isValid =
true;
855 SDL_Size
size = MakeSize(0, 0);
857 if (self->
font == NULL || text == NULL) {
901 if (strcmp(self->
text ?:
"", text ?:
"")) {
905 if (text && strlen(text)) {
906 self->
text = strdup(text);
931 const int len = vasprintf(&text, fmt, args);
956#pragma mark - Class lifecycle
963 ((ObjectInterface *) clazz->interface)->dealloc =
dealloc;
964 ((ObjectInterface *) clazz->interface)->description =
description;
966 ((ViewInterface *) clazz->interface)->applyStyle =
applyStyle;
968 ((ViewInterface *) clazz->interface)->didMoveToWindow =
didMoveToWindow;
969 ((ViewInterface *) clazz->interface)->init =
init;
973 ((ViewInterface *) clazz->interface)->sizeThatFits =
sizeThatFits;
975 ((TextInterface *) clazz->interface)->initWithText =
initWithText;
977 ((TextInterface *) clazz->interface)->setFont =
setFont;
978 ((TextInterface *) clazz->interface)->setText =
setText;
980 ((TextInterface *) clazz->interface)->setTransform =
setTransform;
981 ((TextInterface *) clazz->interface)->sizeText =
sizeText;
993 clazz = _initialize(&(
const ClassDef) {
995 .superclass =
_View(),
996 .instanceSize =
sizeof(
Text),
997 .interfaceSize =
sizeof(TextInterface),
static Box * initWithFrame(Box *self, const SDL_Rect *frame)
static void sizeCharacters(const Font *self, const char *chars, int *w, int *h)
static SDL_Surface * renderCharacters(const Font *self, const char *chars, SDL_Color color, int wrapWidth)
static Font * fontWithAttributes(const FontAttributes *attributes, float pixelDensity)
const EnumName FontStyleNames[]
static String * name(const Font *self)
static Font * defaultFont(void)
static SDL_Size size(const Image *self)
static AtlasImage * imageWithName(const ImageAtlas *self, const char *name)
static Texture * texture(ImageAtlas *self, RenderDevice *device)
static void drawTextureRegion(const Renderer *self, Texture *texture, const SDL_Rect *src, const SDL_FRect *dest, const SDL_Color *color)
static void drawTexture(const Renderer *self, Texture *texture, const SDL_FRect *dest, const SDL_Color *color)
bool MVC_HasColorEscapes(const char *text)
static void checkIcons(Text *self)
Invalidates this Text if the icon atlas it was prepared against has changed: a Theme swap,...
static void didMoveToWindow(View *self, SDL_Window *window)
static void refreshFont(Text *self)
Re-resolves this Text's Font at the current window's pixel density. Shared by didMoveToWindow and ren...
static SDL_Size sizeText(const Text *self, const char *text)
char * MVC_StripColorEscapes(const char *text)
Strips color escape sequences from the given text: ^0 through ^9 are removed, and ^^ collapses to ^.
static void appendSpan(TextSpan **spans, size_t *count, TextSpan span)
static void setFont(Text *self, Font *font)
static void setTextWithFormat(Text *self, const char *fmt,...)
static View * init(View *self)
SDL_Color TextEscapeColors[]
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 const char * iconPlaceholder(const Font *font)
The placeholder an icon occupies in font's layout: a codepoint that is not a line-break opportunity,...
static void awakeWithDictionary(View *self, const Dictionary *dictionary)
static void invalidate(Text *self)
Releases this Text's rendered texture and runs, and invalidates its cached size.
static void setText(Text *self, const char *text)
static void applyStyle(View *self, const Style *style)
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...
static SDL_Size sizeThatFits(const View *self)
static String * description(const Object *self)
static SDL_Size naturalSize(const Text *self)
static void render(View *self, Renderer *renderer)
static void dealloc(Object *self)
static Text * initWithText(Text *self, const char *text, Font *font)
static void applyTransform(Text *self)
Rebuilds transformed from text and transform. Color escapes pass through without counting as letters,...
static void initialize(Class *clazz)
static void renderDeviceWillReset(View *self)
static bool hasEscapes(const char *text)
static void append(LayoutBuffer *buffer, const char *chars, size_t length)
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...
static void renderDeviceDidReset(View *self)
static void setTransform(Text *self, TextTransform transform)
const EnumName TextTransformNames[]
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,...
static ImageAtlas * iconsFor(const Text *self)
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.
static const char * displayText(const Text *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,...
Text rendered with TrueType fonts.
TextTransform
Case transforms a Text applies when drawing, leaving Text::text as set.
@ TextTransformCapitalize
static AtlasImage * icon(Theme *self, const char *name)
static ImageAtlas * icons(Theme *self)
static Font * font(Theme *self, const FontAttributes *attributes, float pixelDensity)
static Theme * theme(SDL_Window *window)
static Image * image(Theme *self, const char *name, float pixelDensity)
#define MakeInlets(...)
Creates a null-termianted array of Inlets.
#define MakeInlet(name, type, dest, data)
Creates an Inlet with the specified parameters.
static void setNeedsLayout(View *self)
static SDL_Rect renderFrame(const View *self)
static void sizeToFit(View *self)
static SDL_Rect bounds(const View *self)
An Image packed into an ImageAtlas.
SDL_Rect rect
This AtlasImage's rectangle within the atlas sheet, in texels.
ImageAtlas * atlas
The ImageAtlas this AtlasImage belongs to, or NULL once that atlas has been released,...
The family, size and style attributes that identify a Font, independent of the pixel density at which...
SDL_Surface * surface
The glyph sheet: a grid of FONT_BITMAP_COLUMNS by FONT_BITMAP_COLUMNS cells of cellSize,...
float pixelDensity
The display pixel density scale, greater than 1.0 on high-density displays.
char * family
The family name.
FontBitmap bitmap
The glyphs of a fixed-width Font, baked into one sheet; bitmap.surface is NULL for a proportional Fon...
TTF_Font * font
The backing font.
Packs many Images into one sheet, backed by one Texture.
unsigned generation
Incremented by every ImageAtlas::addImageWithName and ImageAtlas::compile, so that anything derived f...
Inlets enable inbound data binding of View attributes through JSON.
A growable string for MVC_LayoutText.
Renderer extends Object with ObjectivelyMVC's UI rendering layer.
RenderDevice * device
The backing RenderDevice.
Text rendered with TrueType fonts.
TextTransform transform
The case transform applied when drawing: the text-transform style attribute, or textTransform in JSON...
Texture * texture
The rendered GPU texture.
TextRun * runs
The runs of texture, one textured quad each, when the text contains escapes; otherwise NULL,...
SDL_Size naturalSize(const Text *self)
Resolves the rendered size of this Text.
SDL_Color color
The text color.
Font * font
The Font. A fixed-width Font carries a bitmap, and is drawn as one quad per glyph from its glyph shee...
size_t runCount
The number of entries in runs.
One quad of a rendered TrueType Text: a region of its texture, in texels, drawn in color,...
One span of a laid-out string, in bytes of the layout string MVC_LayoutText returns: either the chara...
Views are the fundamental building blocks of ObjectivelyMVC user interfaces.
SDL_Window * window
The window.
void render(View *self, Renderer *renderer)
Renders this View using the given renderer.