#include "text.h" #include "color.h" #include "metrics.h" #include "embedded.h" #include "app.h" #define STB_TRUETYPE_IMPLEMENTATION #include "../stb_truetype.h" #include #include #include #include #include #include #include #include #include #include iDeclareType(Font) iDeclareType(Glyph) iDeclareTypeConstructionArgs(Glyph, iChar ch) struct Impl_Glyph { iHashNode node; const iFont *font; /* may come from symbols/emoji */ iRect rect[2]; /* zero and half pixel offset */ iInt2 d[2]; float advance; /* scaled */ }; void init_Glyph(iGlyph *d, iChar ch) { d->node.key = ch; d->font = NULL; d->rect[0] = zero_Rect(); d->rect[1] = zero_Rect(); d->advance = 0.0f; } void deinit_Glyph(iGlyph *d) { iUnused(d); } iChar char_Glyph(const iGlyph *d) { return d->node.key; } iDefineTypeConstructionArgs(Glyph, (iChar ch), ch) /*-----------------------------------------------------------------------------------------------*/ struct Impl_Font { iBlock * data; stbtt_fontinfo font; float scale; int height; int baseline; iHash glyphs; iBool enableKerning; enum iFontId symbolsFont; /* font to use for symbols */ }; static iFont *font_Text_(enum iFontId id); static void init_Font(iFont *d, const iBlock *data, int height, enum iFontId symbolsFont) { init_Hash(&d->glyphs); d->data = NULL; d->height = height; iZap(d->font); stbtt_InitFont(&d->font, constData_Block(data), 0); d->scale = stbtt_ScaleForPixelHeight(&d->font, height); int ascent; stbtt_GetFontVMetrics(&d->font, &ascent, NULL, NULL); d->baseline = (int) ascent * d->scale; d->symbolsFont = symbolsFont; d->enableKerning = iTrue; } static void deinit_Font(iFont *d) { iForEach(Hash, i, &d->glyphs) { delete_Glyph((iGlyph *) i.value); } deinit_Hash(&d->glyphs); delete_Block(d->data); } iDeclareType(Text) iDeclareType(CacheRow) struct Impl_CacheRow { int height; iInt2 pos; }; struct Impl_Text { iFont fonts[max_FontId]; SDL_Renderer *render; SDL_Texture * cache; iInt2 cacheSize; int cacheRowAllocStep; int cacheBottom; iArray cacheRows; SDL_Palette * grayscale; iRegExp * ansiEscape; }; static iText text_; void init_Text(SDL_Renderer *render) { iText *d = &text_; init_Array(&d->cacheRows, sizeof(iCacheRow)); d->ansiEscape = new_RegExp("\\[([0-9;]+)m", 0); d->render = render; /* A grayscale palette for rasterized glyphs. */ { SDL_Color colors[256]; for (int i = 0; i < 256; ++i) { colors[i] = (SDL_Color){ 255, 255, 255, i }; } d->grayscale = SDL_AllocPalette(256); SDL_SetPaletteColors(d->grayscale, colors, 0, 256); } /* Initialize the glyph cache. */ { d->cacheSize = init_I2(fontSize_UI * 16, fontSize_UI * 64); d->cacheRowAllocStep = fontSize_UI / 6; /* Allocate initial (empty) rows. These will be assigned actual locations in the cache once at least one glyph is stored. */ for (int h = d->cacheRowAllocStep; h <= 2 * fontSize_UI; h += d->cacheRowAllocStep) { pushBack_Array(&d->cacheRows, &(iCacheRow){ .height = 0 }); } d->cacheBottom = 0; d->cache = SDL_CreateTexture(render, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET, d->cacheSize.x, d->cacheSize.y); SDL_SetTextureBlendMode(d->cache, SDL_BLENDMODE_BLEND); } /* Load the fonts. */ { const struct { const iBlock *ttf; int size; int symbolsFont; } fontData[max_FontId] = { { &fontSourceSansProRegular_Embedded, fontSize_UI, symbols_FontId }, { &fontFiraSansRegular_Embedded, fontSize_UI, symbols_FontId }, { &fontFiraMonoRegular_Embedded, fontSize_UI * 0.866f, smallSymbols_FontId }, { &fontFiraMonoRegular_Embedded, fontSize_UI * 0.666f, smallSymbols_FontId }, { &fontFiraSansRegular_Embedded, fontSize_UI * 1.333f, mediumSymbols_FontId }, { &fontFiraSansItalic_Embedded, fontSize_UI, symbols_FontId }, { &fontFiraSansBold_Embedded, fontSize_UI, symbols_FontId }, { &fontFiraSansBold_Embedded, fontSize_UI * 1.333f, mediumSymbols_FontId }, { &fontFiraSansBold_Embedded, fontSize_UI * 1.666f, largeSymbols_FontId }, { &fontFiraSansBold_Embedded, fontSize_UI * 2.000f, hugeSymbols_FontId }, { &fontSymbola_Embedded, fontSize_UI, symbols_FontId }, { &fontSymbola_Embedded, fontSize_UI * 1.333f, mediumSymbols_FontId }, { &fontSymbola_Embedded, fontSize_UI * 1.666f, largeSymbols_FontId }, { &fontSymbola_Embedded, fontSize_UI * 2.000f, hugeSymbols_FontId }, { &fontSymbola_Embedded, fontSize_UI * 0.866f, smallSymbols_FontId }, { &fontNotoEmojiRegular_Embedded, fontSize_UI, symbols_FontId }, { &fontNotoEmojiRegular_Embedded, fontSize_UI * 1.333f, mediumSymbols_FontId }, { &fontNotoEmojiRegular_Embedded, fontSize_UI * 1.666f, largeSymbols_FontId }, { &fontNotoEmojiRegular_Embedded, fontSize_UI * 2.000f, hugeSymbols_FontId }, { &fontNotoEmojiRegular_Embedded, fontSize_UI * 0.866f, smallSymbols_FontId }, }; iForIndices(i, fontData) { iFont *font = &d->fonts[i]; init_Font(font, fontData[i].ttf, fontData[i].size, fontData[i].symbolsFont); if (fontData[i].ttf == &fontFiraMonoRegular_Embedded) { font->enableKerning = iFalse; } } } } void deinit_Text(void) { iText *d = &text_; SDL_FreePalette(d->grayscale); iForIndices(i, d->fonts) { deinit_Font(&d->fonts[i]); } deinit_Array(&d->cacheRows); SDL_DestroyTexture(d->cache); d->render = NULL; iRelease(d->ansiEscape); } iFont *font_Text_(enum iFontId id) { return &text_.fonts[id]; } static SDL_Surface *rasterizeGlyph_Font_(const iFont *d, iChar ch, float xShift) { int w, h; uint8_t *bmp = stbtt_GetCodepointBitmapSubpixel( &d->font, d->scale, d->scale, xShift, 0.0f, ch, &w, &h, 0, 0); /* Note: `bmp` must be freed afterwards. */ SDL_Surface *surface = SDL_CreateRGBSurfaceWithFormatFrom(bmp, w, h, 8, w, SDL_PIXELFORMAT_INDEX8); SDL_SetSurfacePalette(surface, text_.grayscale); return surface; } iLocalDef SDL_Rect sdlRect_(const iRect rect) { return (SDL_Rect){ rect.pos.x, rect.pos.y, rect.size.x, rect.size.y }; } iLocalDef iCacheRow *cacheRow_Text_(iText *d, int height) { return at_Array(&d->cacheRows, (height - 1) / d->cacheRowAllocStep); } static iInt2 assignCachePos_Text_(iText *d, iInt2 size) { iCacheRow *cur = cacheRow_Text_(d, size.y); if (cur->height == 0) { /* Begin a new row height. */ cur->height = (1 + (size.y - 1) / d->cacheRowAllocStep) * d->cacheRowAllocStep; cur->pos.y = d->cacheBottom; d->cacheBottom = cur->pos.y + cur->height; } iAssert(cur->height >= size.y); /* TODO: Automatically enlarge the cache if running out of space? Maybe make it paged, but beware of texture swapping too often inside a text string. */ if (cur->pos.x + size.x > d->cacheSize.x) { /* Does not fit on this row, advance to a new location in the cache. */ cur->pos.y = d->cacheBottom; cur->pos.x = 0; d->cacheBottom += cur->height; iAssert(d->cacheBottom <= d->cacheSize.y); } const iInt2 assigned = cur->pos; cur->pos.x += size.x; return assigned; } static void cache_Font_(iFont *d, iGlyph *glyph, int hoff) { iText *txt = &text_; SDL_Renderer *render = txt->render; SDL_Texture *tex = NULL; SDL_Surface *surface = NULL; const iChar ch = char_Glyph(glyph); iRect *glRect = &glyph->rect[hoff]; /* Rasterize the glyph using stbtt. */ { surface = rasterizeGlyph_Font_(d, ch, hoff * 0.5f); if (hoff == 0) { int adv, lsb; stbtt_GetCodepointHMetrics(&d->font, ch, &adv, &lsb); glyph->advance = d->scale * adv; } stbtt_GetCodepointBitmapBoxSubpixel(&d->font, ch, d->scale, d->scale, hoff * 0.5f, 0.0f, &glyph->d[hoff].x, &glyph->d[hoff].y, NULL, NULL); tex = SDL_CreateTextureFromSurface(render, surface); glRect->size = init_I2(surface->w, surface->h); } /* Determine placement in the glyph cache texture, advancing in rows. */ glRect->pos = assignCachePos_Text_(txt, glRect->size); SDL_SetRenderTarget(render, txt->cache); const SDL_Rect dstRect = sdlRect_(*glRect); SDL_RenderCopy(render, tex, &(SDL_Rect){ 0, 0, dstRect.w, dstRect.h }, &dstRect); SDL_SetRenderTarget(render, NULL); if (tex) { SDL_DestroyTexture(tex); iAssert(surface); stbtt_FreeBitmap(surface->pixels, NULL); SDL_FreeSurface(surface); } } iLocalDef iFont *characterFont_Font_(iFont *d, iChar ch) { if (stbtt_FindGlyphIndex(&d->font, ch) != 0) { return d; } /* Not defined in current font, try Noto Emoji (for selected characters). */ if ((ch >= 0x1f300 && ch < 0x1f600) || (ch >= 0x1f680 && ch <= 0x1f6c5)) { iFont *emoji = font_Text_(d->symbolsFont + fromSymbolsToEmojiOffset_FontId); if (emoji != d && stbtt_FindGlyphIndex(&emoji->font, ch)) { return emoji; } } /* Fall back to Symbola for anything else. */ return font_Text_(d->symbolsFont); } static const iGlyph *glyph_Font_(iFont *d, iChar ch) { /* It may actually come from a different font. */ iFont *font = characterFont_Font_(d, ch); const void *node = value_Hash(&font->glyphs, ch); if (node) { return node; } iGlyph *glyph = new_Glyph(ch); glyph->font = font; cache_Font_(font, glyph, 0); cache_Font_(font, glyph, 1); /* half-pixel offset */ insert_Hash(&font->glyphs, &glyph->node); return glyph; } enum iRunMode { measure_RunMode, measureNoWrap_RunMode, draw_RunMode, drawPermanentColor_RunMode }; static iChar nextChar_(const char **chPos, const char *end) { if (*chPos == end) { return 0; } iChar ch; int len = decodeBytes_MultibyteChar(*chPos, end - *chPos, &ch); if (len <= 0) { (*chPos)++; /* skip it */ return 0; } (*chPos) += len; return ch; } int enableHalfPixelGlyphs_Text = iTrue; iLocalDef iBool isWrapBoundary_(iChar a, iChar b) { if (b == '/' || b == '-' || b == ',' || b == ';' || b == ':') { return iTrue; } return !isSpace_Char(a) && isSpace_Char(b); } static iInt2 run_Font_(iFont *d, enum iRunMode mode, iRangecc text, size_t maxLen, iInt2 pos, int xposLimit, const char **continueFrom_out, int *runAdvance_out) { iInt2 size = zero_I2(); const iInt2 orig = pos; float xpos = pos.x; float xposMax = xpos; iAssert(xposLimit == 0 || mode == measure_RunMode || mode == measureNoWrap_RunMode); const char *lastWordEnd = text.start; if (continueFrom_out) { *continueFrom_out = text.end; } iChar prevCh = 0; for (const char *chPos = text.start; chPos != text.end; ) { iAssert(chPos < text.end); if (*chPos == 0x1b) { /* ANSI escape. */ chPos++; iRegExpMatch m; if (match_RegExp(text_.ansiEscape, chPos, text.end - chPos, &m)) { if (mode == draw_RunMode) { /* Change the color. */ const iColor clr = ansi_Color(capturedRange_RegExpMatch(&m, 1), gray75_ColorId); SDL_SetTextureColorMod(text_.cache, clr.r, clr.g, clr.b); } chPos = end_RegExpMatch(&m); continue; } } iChar ch = nextChar_(&chPos, text.end); /* Special instructions. */ { if (ch == '\n') { xpos = pos.x; pos.y += d->height; prevCh = ch; continue; } if (ch == '\r') { const iChar esc = nextChar_(&chPos, text.end); if (mode == draw_RunMode) { const iColor clr = get_Color(esc - '0'); SDL_SetTextureColorMod(text_.cache, clr.r, clr.g, clr.b); } prevCh = 0; continue; } } const iGlyph *glyph = glyph_Font_(d, ch); int x1 = xpos; const int hoff = enableHalfPixelGlyphs_Text ? (xpos - x1 > 0.5f ? 1 : 0) : 0; int x2 = x1 + glyph->rect[hoff].size.x; /* Out of the allotted space? */ if (xposLimit > 0 && x2 > xposLimit) { *continueFrom_out = lastWordEnd; break; } size.x = iMax(size.x, x2 - orig.x); size.y = iMax(size.y, pos.y + glyph->font->height - orig.y); if (mode != measure_RunMode && mode != measureNoWrap_RunMode) { SDL_Rect dst = { x1 + glyph->d[hoff].x, pos.y + glyph->font->baseline + glyph->d[hoff].y, glyph->rect[hoff].size.x, glyph->rect[hoff].size.y }; SDL_RenderCopy(text_.render, text_.cache, (const SDL_Rect *) &glyph->rect[hoff], &dst); } xpos += glyph->advance; xposMax = iMax(xposMax, xpos); if (mode == measureNoWrap_RunMode || isWrapBoundary_(prevCh, ch)) { lastWordEnd = chPos; } /* Check the next character. */ if (d->enableKerning && glyph->font == d) { /* TODO: No need to decode the next char twice; check this on the next iteration. */ const char *peek = chPos; const iChar next = nextChar_(&peek, text.end); if (ch == '/' && next == '/') { /* Manual kerning for double-slash. */ xpos -= glyph->rect[hoff].size.x * 0.5f; } else if (next) { xpos += d->scale * stbtt_GetCodepointKernAdvance(&d->font, ch, next); } } prevCh = ch; if (--maxLen == 0) { break; } } if (runAdvance_out) { *runAdvance_out = xposMax - orig.x; } return size; } int lineHeight_Text(int fontId) { return text_.fonts[fontId].height; } iInt2 measureRange_Text(int fontId, iRangecc text) { if (isEmpty_Range(&text)) { return init_I2(0, lineHeight_Text(fontId)); } return run_Font_(&text_.fonts[fontId], measure_RunMode, text, iInvalidSize, zero_I2(), 0, NULL, NULL); } iInt2 measure_Text(int fontId, const char *text) { return measureRange_Text(fontId, range_CStr(text)); } iInt2 advanceRange_Text(int fontId, iRangecc text) { int advance; const int height = run_Font_(&text_.fonts[fontId], measure_RunMode, text, iInvalidSize, zero_I2(), 0, NULL, &advance) .y; return init_I2(advance, height); } iInt2 tryAdvance_Text(int fontId, iRangecc text, int width, const char **endPos) { int advance; const int height = run_Font_(&text_.fonts[fontId], measure_RunMode, text, iInvalidSize, zero_I2(), width, endPos, &advance) .y; return init_I2(advance, height); } iInt2 tryAdvanceNoWrap_Text(int fontId, iRangecc text, int width, const char **endPos) { int advance; const int height = run_Font_(&text_.fonts[fontId], measureNoWrap_RunMode, text, iInvalidSize, zero_I2(), width, endPos, &advance) .y; return init_I2(advance, height); } iInt2 advance_Text(int fontId, const char *text) { return advanceRange_Text(fontId, range_CStr(text)); } iInt2 advanceN_Text(int fontId, const char *text, size_t n) { if (n == 0) { return init_I2(0, lineHeight_Text(fontId)); } int advance; run_Font_( &text_.fonts[fontId], measure_RunMode, range_CStr(text), n, zero_I2(), 0, NULL, &advance); return init_I2(advance, lineHeight_Text(fontId)); } static void draw_Text_(int fontId, iInt2 pos, int color, iRangecc text) { iText *d = &text_; const iColor clr = get_Color(color & mask_ColorId); SDL_SetTextureColorMod(d->cache, clr.r, clr.g, clr.b); run_Font_(&d->fonts[fontId], color & permanent_ColorId ? drawPermanentColor_RunMode : draw_RunMode, text, iInvalidSize, pos, 0, NULL, NULL); } void drawAlign_Text(int fontId, iInt2 pos, int color, enum iAlignment align, const char *text, ...) { iBlock chars; init_Block(&chars, 0); { va_list args; va_start(args, text); vprintf_Block(&chars, text, args); va_end(args); } if (align == center_Alignment) { pos.x -= measure_Text(fontId, cstr_Block(&chars)).x / 2; } else if (align == right_Alignment) { pos.x -= measure_Text(fontId, cstr_Block(&chars)).x; } draw_Text_(fontId, pos, color, (iRangecc){ constBegin_Block(&chars), constEnd_Block(&chars) }); deinit_Block(&chars); } void draw_Text(int fontId, iInt2 pos, int color, const char *text, ...) { iBlock chars; init_Block(&chars, 0); { va_list args; va_start(args, text); vprintf_Block(&chars, text, args); va_end(args); } draw_Text_(fontId, pos, color, (iRangecc){ constBegin_Block(&chars), constEnd_Block(&chars) }); deinit_Block(&chars); } void drawString_Text(int fontId, iInt2 pos, int color, const iString *text) { draw_Text_(fontId, pos, color, range_String(text)); } void drawCentered_Text(int fontId, iRect rect, int color, const char *format, ...) { iBlock chars; init_Block(&chars, 0); { va_list args; va_start(args, format); vprintf_Block(&chars, format, args); va_end(args); } const char *text = cstr_Block(&chars); iInt2 textSize = advance_Text(fontId, text); draw_Text_(fontId, sub_I2(mid_Rect(rect), divi_I2(textSize, 2)), color, (iRangecc){ constBegin_Block(&chars), constEnd_Block(&chars) }); deinit_Block(&chars); } SDL_Texture *glyphCache_Text(void) { return text_.cache; } /*-----------------------------------------------------------------------------------------------*/ iDefineTypeConstructionArgs(TextBuf, (int font, const char *text), font, text) void init_TextBuf(iTextBuf *d, int font, const char *text) { SDL_Renderer *render = text_.render; d->size = advance_Text(font, text); d->texture = SDL_CreateTexture(render, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET, d->size.x, d->size.y); SDL_SetTextureBlendMode(d->texture, SDL_BLENDMODE_BLEND); SDL_SetRenderTarget(render, d->texture); draw_Text_(font, zero_I2(), white_ColorId, range_CStr(text)); SDL_SetRenderTarget(render, NULL); } void deinit_TextBuf(iTextBuf *d) { SDL_DestroyTexture(d->texture); } void draw_TextBuf(const iTextBuf *d, iInt2 pos, int color) { const iColor clr = get_Color(color); SDL_SetTextureColorMod(d->texture, clr.r, clr.g, clr.b); SDL_RenderCopy(text_.render, d->texture, &(SDL_Rect){ 0, 0, d->size.x, d->size.y }, &(SDL_Rect){ pos.x, pos.y, d->size.x, d->size.y }); }