/* Copyright 2020-2022 Jaakko Keränen Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* Overview of types: - Text : top-level text renderer instance (one per window) - Font : a font's assets for rendering, e.g., metrics and cached glyphs - Glyph : hash node; a single cached glyph, with Rect in cache texture - AttributedText : text string to be drawn that is split into sub-runs by attributes (font, color) - AttributedRun : a run inside AttributedText - GlyphBuffer : HarfBuzz-shaped glyphs corresponding to an AttributedRun - FontRun : cached state (e.g., AttributedText, glyphs) needed for rendering a text string - FontRunArgs : set of arguments for constructing a FontRun - RunArgs : input arguments for `run_Font_` (the low-level text rendering routine) - RunLayer : arguments for processing the glyphs of a GlyphBuffer (layers: background, foreground) Optimization notes: - Caching FontRuns is quite effective, but there is still plenty of unnecessary iteration of glyphs during wrapping of long text. It could help if there is a direct mapping between wrapPosRange and a GlyphBuffer's glyph indices. */ #include "text.h" #include "app.h" #include "attributedtext.h" #include "color.h" #include "paint.h" #include "resources.h" #include "ui/metrics.h" #include "ui/window.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined (LAGRANGE_ENABLE_HARFBUZZ) # include #endif #if defined (LAGRANGE_ENABLE_FRIBIDI) # include #endif #if SDL_VERSION_ATLEAST(2, 0, 10) # define LAGRANGE_RASTER_DEPTH 8 # define LAGRANGE_RASTER_FORMAT SDL_PIXELFORMAT_INDEX8 #else # define LAGRANGE_RASTER_DEPTH 32 # define LAGRANGE_RASTER_FORMAT SDL_PIXELFORMAT_RGBA8888 #endif #define STB_TRUETYPE_IMPLEMENTATION #include "../stb_truetype.h" iDeclareType(Font) iDeclareType(Glyph) iDeclareTypeConstructionArgs(Glyph, iChar ch) enum iGlyphFlag { rasterized0_GlyphFlag = iBit(1), /* zero offset */ rasterized1_GlyphFlag = iBit(2), /* quarter pixel offset */ rasterized2_GlyphFlag = iBit(3), /* half-pixel offset */ rasterized3_GlyphFlag = iBit(4), /* three quarters offset */ }; int enableHalfPixelGlyphs_Text = iTrue; /* debug setting */ int enableKerning_Text = iTrue; /* note: looking up kern pairs is slow */ static int numOffsetSteps_Glyph_ = 4; /* subpixel offsets for glyphs */ static int rasterizedAll_GlyphFlag_ = 0xf; /* updated with numOffsetSteps_Glyph */ iLocalDef float offsetStep_Glyph_(void) { return 1.0f / (float) numOffsetSteps_Glyph_; } static int makeRasterizedAll_GlyphFlag_(int n) { int flag = rasterized0_GlyphFlag; if (n > 1) { flag |= rasterized1_GlyphFlag; } if (n > 2) { flag |= rasterized2_GlyphFlag; } if (n > 3) { flag |= rasterized3_GlyphFlag; } return flag; } struct Impl_Glyph { iHashNode node; int flags; iFont *font; /* may come from symbols/emoji */ float advance; /* scaled */ iRect rect[4]; /* zero and half pixel offset */ iInt2 d[4]; }; void init_Glyph(iGlyph *d, uint32_t glyphIndex) { d->node.key = glyphIndex; d->flags = 0; d->font = NULL; d->advance = 0.0f; iZap(d->rect); iZap(d->d); } void deinit_Glyph(iGlyph *d) { iUnused(d); } static uint32_t index_Glyph_(const iGlyph *d) { return d->node.key; } iLocalDef iBool isRasterized_Glyph_(const iGlyph *d, int hoff) { return (d->flags & (rasterized0_GlyphFlag << hoff)) != 0; } iLocalDef iBool isFullyRasterized_Glyph_(const iGlyph *d) { return (d->flags & rasterizedAll_GlyphFlag_) == rasterizedAll_GlyphFlag_; } iLocalDef void setRasterized_Glyph_(iGlyph *d, int hoff) { d->flags |= rasterized0_GlyphFlag << hoff; } iDefineTypeConstructionArgs(Glyph, (iChar ch), ch) /*-----------------------------------------------------------------------------------------------*/ static iGlyph *glyph_Font_(iFont *d, iChar ch); iDeclareType(GlyphTable) struct Impl_GlyphTable { iHash glyphs; /* key is glyph index in the font */ /* TODO: `glyphs` does not need to be a Hash. We could lazily allocate an array with glyphCount elements instead. */ uint32_t indexTable[128 - 32]; /* quick ASCII lookup */ }; static void clearGlyphs_GlyphTable_(iGlyphTable *d) { if (d) { iForEach(Hash, i, &d->glyphs) { delete_Glyph((iGlyph *) i.value); } clear_Hash(&d->glyphs); } } static void init_GlyphTable(iGlyphTable *d) { init_Hash(&d->glyphs); memset(d->indexTable, 0xff, sizeof(d->indexTable)); } static void deinit_GlyphTable(iGlyphTable *d) { clearGlyphs_GlyphTable_(d); deinit_Hash(&d->glyphs); } iDefineTypeConstruction(GlyphTable) /*-----------------------------------------------------------------------------------------------*/ struct Impl_Font { iBaseFont font; int vertOffset; /* offset due to glyph scaling */ float xScale, yScale; float emAdvance; iGlyphTable *table; }; static void init_Font(iFont *d, const iFontSpec *fontSpec, const iFontFile *fontFile, enum iFontSize sizeId, float height) { const int scaleType = scaleType_FontSpec(sizeId); d->font.spec = fontSpec; d->font.file = fontFile; d->font.height = (int) (height * fontSpec->heightScale[scaleType]); const float glyphScale = fontSpec->glyphScale[scaleType]; d->xScale = d->yScale = scaleForPixelHeight_FontFile(fontFile, d->font.height) * glyphScale; if (isMonospaced_Font(d)) { /* It is important that monospaced fonts align 1:1 with the pixel grid so that box-drawing characters don't have partially occupied edge pixels, leading to seams between adjacent glyphs. */ const float advance = (float) fontFile->emAdvance * d->xScale; if (advance > 4) { /* not too tiny */ d->xScale *= floorf(advance) / advance; } } d->emAdvance = fontFile->emAdvance * d->xScale; d->font.baseline = fontFile->ascent * d->yScale; d->vertOffset = d->font.height * (1.0f - glyphScale) / 2 * fontSpec->vertOffsetScale[scaleType]; d->table = NULL; } static void deinit_Font(iFont *d) { delete_GlyphTable(d->table); } static uint32_t glyphIndex_Font_(iFont *d, iChar ch) { /* TODO: Add a small cache of ~5 most recently found indices. */ const size_t entry = ch - 32; if (!d->table) { d->table = new_GlyphTable(); } iGlyphTable *table = d->table; if (entry < iElemCount(table->indexTable)) { if (table->indexTable[entry] == ~0u) { table->indexTable[entry] = findGlyphIndex_FontFile(d->font.file, ch); } return table->indexTable[entry]; } return findGlyphIndex_FontFile(d->font.file, ch); } /*----------------------------------------------------------------------------------------------*/ iDeclareType(CacheRow) struct Impl_CacheRow { int height; iInt2 pos; }; iDeclareType(PrioMapItem) struct Impl_PrioMapItem { int priority; uint32_t fontIndex; }; static int cmp_PrioMapItem_(const void *a, const void *b) { const iPrioMapItem *i = a, *j = b; return -iCmp(i->priority, j->priority); } iDeclareType(FontRunArgs) iDeclareType(FontRun) iDeclareTypeConstructionArgs(FontRun, const iFontRunArgs *args, const iRangecc text, uint32_t crc) iDeclareType(StbText) iDeclareTypeConstructionArgs(StbText, SDL_Renderer *render, float documentFontSizeFactor) struct Impl_StbText { iText base; iArray fonts; /* fonts currently selected for use (incl. all styles/sizes) */ int overrideFontId; /* always checked for glyphs first, regardless of which font is used */ iFontSpec iosevkaFallback; /* copy of Iosevka as a low-priority spec */ iArray fontPriorityOrder; SDL_Texture * cache; iInt2 cacheSize; int cacheRowAllocStep; int cacheBottom; iArray cacheRows; SDL_Palette * grayscale; SDL_Palette * blackAndWhite; /* unsmoothed glyph palette */ iBool missingGlyphs; /* true if a glyph couldn't be found */ iChar missingChars[20]; /* rotating buffer of the latest missing characters */ iFontRun * cachedFontRuns[16]; /* recently generated HarfBuzz glyph buffers */ }; iLocalDef iStbText *current_StbText_(void) { return (iStbText *) current_Text(); } iLocalDef iFont *font_Text_(enum iFontId id) { iAssert(current_StbText_()); return at_Array(¤t_StbText_()->fonts, id & mask_FontId); } static void setupFontVariants_StbText_(iStbText *d, const iFontSpec *spec, int baseId) { const float uiSize = fontSize_UI * (isMobile_Platform() ? 1.1f : 1.0f); const float textSize = fontSize_UI * d->base.contentFontSize; if (spec->flags & override_FontSpecFlag && d->overrideFontId < 0) { /* This is the highest priority override font. */ d->overrideFontId = baseId; } iAssert(current_StbText_() == d); pushBack_Array(&d->fontPriorityOrder, &(iPrioMapItem){ spec->priority, baseId }); for (enum iFontStyle style = 0; style < max_FontStyle; style++) { for (enum iFontSize sizeId = 0; sizeId < max_FontSize; sizeId++) { init_Font(font_Text_(FONT_ID(baseId, style, sizeId)), spec, spec->styles[style], sizeId, (sizeId < contentRegular_FontSize ? uiSize : textSize) * scale_FontSize(sizeId)); } } } iBaseFont *font_Text(enum iFontId id) { return (iBaseFont *) font_Text_(id); } static enum iFontId fontId_Text_(const iFont *font) { return (enum iFontId) (font - (const iFont *) constData_Array(¤t_StbText_()->fonts)); } enum iFontId fontId_Text(const void *font) { return fontId_Text_(font); } iLocalDef enum iFontSize sizeId_Text_(const iFont *d) { return fontId_Text_(d) % max_FontSize; } iLocalDef enum iFontStyle styleId_Text_(const iFont *d) { return style_FontId(fontId_Text_(d)); } static const iFontSpec *tryFindSpec_(enum iPrefsString ps, const char *fallback) { const iFontSpec *spec = findSpec_Fonts(cstr_String(&prefs_App()->strings[ps])); return spec ? spec : findSpec_Fonts(fallback); } static const iFont *findFontVariant_StbText_(const iStbText *d, const iFontSpec *spec) { for (size_t i = 0; i < size_Array(&d->fonts); i += maxVariants_Fonts) { const iFont *font = constAt_Array(&d->fonts, i); if (font->font.spec == spec) { return font; } } return NULL; } static void initFonts_StbText_(iStbText *d) { /* The `fonts` array has precomputed scaling factors and other parameters in all sizes and styles for each available font. Indices to `fonts` act as font runtime IDs. */ /* First the mandatory fonts. */ d->overrideFontId = -1; clear_Array(&d->fontPriorityOrder); resize_Array(&d->fonts, auxiliary_FontId); /* room for the built-ins */ setupFontVariants_StbText_(d, tryFindSpec_(uiFont_PrefsString, "default"), default_FontId); setupFontVariants_StbText_(d, tryFindSpec_(monospaceFont_PrefsString, "iosevka"), monospace_FontId); setupFontVariants_StbText_(d, tryFindSpec_(headingFont_PrefsString, "default"), documentHeading_FontId); setupFontVariants_StbText_(d, tryFindSpec_(bodyFont_PrefsString, "default"), documentBody_FontId); setupFontVariants_StbText_(d, tryFindSpec_(monospaceDocumentFont_PrefsString, "iosevka-body"), documentMonospace_FontId); /* Check if there are auxiliary fonts available and set those up, too. */ iConstForEach(PtrArray, s, listSpecsByPriority_Fonts()) { const iFontSpec *spec = s.ptr; // printf("spec '%s': prio=%d\n", cstr_String(&spec->name), spec->priority); if (spec->flags & (auxiliary_FontSpecFlag | user_FontSpecFlag)) { const int fontId = size_Array(&d->fonts); resize_Array(&d->fonts, fontId + maxVariants_Fonts); setupFontVariants_StbText_(d, spec, fontId); } } /* If Iosevka is not the monospace font, add it as a fallback variant because it has a good coverage of symbols. */ { const iFontSpec *iosevka = findSpec_Fonts("iosevka"); /* this is expected to be built-in */ if (iosevka && !findFontVariant_StbText_(d, iosevka)) { d->iosevkaFallback = *iosevka; d->iosevkaFallback.priority = 20; /* make it pretty important */ const int fontId = size_Array(&d->fonts); resize_Array(&d->fonts, fontId + maxVariants_Fonts); setupFontVariants_StbText_(d, &d->iosevkaFallback, fontId); } } sort_Array(&d->fontPriorityOrder, cmp_PrioMapItem_); #if !defined (NDEBUG) printf("[Text] %zu font variants ready\n", size_Array(&d->fonts)); #endif gap_Text = iRound(gap_UI * d->base.contentFontSize); } static void deinitFonts_StbText_(iStbText *d) { iForEach(Array, i, &d->fonts) { deinit_Font(i.value); } clear_Array(&d->fonts); } static int maxGlyphHeight_Text_(const iText *d) { /* Huge size is 2 * contentFontSize. */ return 4 * d->contentFontSize * fontSize_UI; } static void initCache_StbText_(iStbText *d) { init_Array(&d->cacheRows, sizeof(iCacheRow)); const int textSize = d->base.contentFontSize * fontSize_UI; iAssert(textSize > 0); numOffsetSteps_Glyph_ = get_Window()->pixelRatio < 2.0f ? 4 : get_Window()->pixelRatio < 2.5f ? 3 : 2; rasterizedAll_GlyphFlag_ = makeRasterizedAll_GlyphFlag_(numOffsetSteps_Glyph_); #if !defined (NDEBUG) printf("[Text] subpixel offsets: %d\n", numOffsetSteps_Glyph_); #endif const iInt2 cacheDims = init_I2(8 * numOffsetSteps_Glyph_, 40); d->cacheSize = mul_I2(cacheDims, init1_I2(iMax(textSize, fontSize_UI))); SDL_RendererInfo renderInfo; SDL_GetRendererInfo(d->base.render, &renderInfo); if (renderInfo.max_texture_height > 0 && d->cacheSize.y > renderInfo.max_texture_height) { d->cacheSize.y = renderInfo.max_texture_height; d->cacheSize.x = renderInfo.max_texture_width; } d->cacheRowAllocStep = iMax(2, textSize / 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 <= 5 * textSize + d->cacheRowAllocStep; h += d->cacheRowAllocStep) { pushBack_Array(&d->cacheRows, &(iCacheRow){ .height = 0 }); } d->cacheBottom = 0; SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "0"); d->cache = SDL_CreateTexture(d->base.render, SDL_PIXELFORMAT_RGBA4444, SDL_TEXTUREACCESS_STATIC | SDL_TEXTUREACCESS_TARGET, d->cacheSize.x, d->cacheSize.y); SDL_SetTextureBlendMode(d->cache, SDL_BLENDMODE_BLEND); } static void deinitCache_StbText_(iStbText *d) { deinit_Array(&d->cacheRows); SDL_DestroyTexture(d->cache); } void init_StbText(iStbText *d, SDL_Renderer *render, float documentFontSizeFactor) { init_Text(&d->base, render, documentFontSizeFactor); iText *oldActive = current_Text(); setCurrent_Text(&d->base); init_Array(&d->fonts, sizeof(iFont)); init_Array(&d->fontPriorityOrder, sizeof(iPrioMapItem)); d->missingGlyphs = iFalse; iZap(d->missingChars); iZap(d->cachedFontRuns); /* A grayscale palette for rasterized glyphs. */ { SDL_Color colors[256]; for (int i = 0; i < 256; ++i) { /* TODO: On dark backgrounds, applying a gamma curve of some sort might be helpful. */ colors[i] = (SDL_Color){ 255, 255, 255, 255 * powf(i / 255.0f, 1.0f) + 0.5f }; } d->grayscale = SDL_AllocPalette(256); SDL_SetPaletteColors(d->grayscale, colors, 0, 256); } /* Black-and-white palette for unsmoothed glyphs. */ { SDL_Color colors[256]; for (int i = 0; i < 256; ++i) { colors[i] = (SDL_Color){ 255, 255, 255, i < 100 ? 0 : 255 }; } d->blackAndWhite = SDL_AllocPalette(256); SDL_SetPaletteColors(d->blackAndWhite, colors, 0, 256); } initCache_StbText_(d); initFonts_StbText_(d); setCurrent_Text(oldActive); } void deinit_StbText(iStbText *d) { #if defined (LAGRANGE_ENABLE_HARFBUZZ) iForIndices(i, d->cachedFontRuns) { delete_FontRun(d->cachedFontRuns[i]); } #endif SDL_FreePalette(d->blackAndWhite); SDL_FreePalette(d->grayscale); deinitFonts_StbText_(d); deinitCache_StbText_(d); deinit_Array(&d->fontPriorityOrder); deinit_Array(&d->fonts); deinit_Text(&d->base); } iText *new_Text(SDL_Renderer *render, float documentFontSizeFactor) { iStbText *d = iMalloc(StbText); init_StbText(d, render, documentFontSizeFactor); return (iText *) d; } void delete_Text(iText *d) { deinit_StbText((iStbText *) d); free(d); } void setOpacity_Text(float opacity) { SDL_SetTextureAlphaMod(current_StbText_()->cache, iClamp(opacity, 0.0f, 1.0f) * 255 + 0.5f); } static void resetCache_StbText_(iStbText *d) { deinitCache_StbText_(d); iForEach(Array, i, &d->fonts) { clearGlyphs_GlyphTable_(((iFont *) i.value)->table); } initCache_StbText_(d); } void resetFonts_Text(iText *d) { iText *oldActive = current_Text(); iStbText *s = (iStbText *) d; setCurrent_Text(d); /* some routines rely on the global `activeText_` pointer */ deinitFonts_StbText_(s); deinitCache_StbText_(s); initCache_StbText_(s); initFonts_StbText_(s); setCurrent_Text(oldActive); } void resetFontCache_Text(iText *d) { iText *oldActive = current_Text(); setCurrent_Text(d); /* some routines rely on the global `activeText_` pointer */ resetCache_StbText_((iStbText *) d); setCurrent_Text(oldActive); } static SDL_Palette *glyphPalette_(void) { return prefs_App()->fontSmoothing ? current_StbText_()->grayscale : current_StbText_()->blackAndWhite; } static SDL_Surface *rasterizeGlyph_Font_(const iFont *d, uint32_t glyphIndex, float xShift) { int w, h; uint8_t *bmp = rasterizeGlyph_FontFile(d->font.file, d->xScale, d->yScale, xShift, glyphIndex, &w, &h); SDL_Surface *surface8 = SDL_CreateRGBSurfaceWithFormatFrom(bmp, w, h, 8, w, SDL_PIXELFORMAT_INDEX8); SDL_SetSurfaceBlendMode(surface8, SDL_BLENDMODE_NONE); SDL_SetSurfacePalette(surface8, glyphPalette_()); #if LAGRANGE_RASTER_DEPTH != 8 /* Convert to the cache format. */ SDL_Surface *surf = SDL_ConvertSurfaceFormat(surface8, LAGRANGE_RASTER_FORMAT, 0); SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_NONE); free(bmp); SDL_FreeSurface(surface8); return surf; #else return surface8; #endif } iLocalDef iCacheRow *cacheRow_StbText_(iStbText *d, int height) { return at_Array(&d->cacheRows, (height - 1) / d->cacheRowAllocStep); } static iInt2 assignCachePos_Text_(iStbText *d, iInt2 size) { iCacheRow *cur = cacheRow_StbText_(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 allocate_Font_(iFont *d, iGlyph *glyph, int hoff) { iRect *glRect = &glyph->rect[hoff]; int x0, y0, x1, y1; measureGlyph_FontFile(d->font.file, index_Glyph_(glyph), d->xScale, d->yScale, hoff * offsetStep_Glyph_(), &x0, &y0, &x1, &y1); glRect->size = init_I2(x1 - x0, y1 - y0); /* Determine placement in the glyph cache texture, advancing in rows. */ glRect->pos = assignCachePos_Text_(current_StbText_(), glRect->size); glyph->d[hoff] = init_I2(x0, y0); glyph->d[hoff].y += d->vertOffset; if (hoff == 0) { /* hoff>=1 uses same metrics as `glyph` */ glyph->advance = d->xScale * glyphAdvance_FontFile(d->font.file, index_Glyph_(glyph)); } } iLocalDef iFont *characterFont_Font_(iFont *d, iChar ch, uint32_t *glyphIndex) { if (isVariationSelector_Char(ch)) { return d; } const enum iFontStyle styleId = styleId_Text_(d); const enum iFontSize sizeId = sizeId_Text_(d); const iBool isMonospaced = isMonospaced_Font(d); iFont *overrideFont = NULL; if (ch != 0x20 && current_StbText_()->overrideFontId >= 0) { /* Override font is checked first. */ overrideFont = font_Text_(FONT_ID(current_StbText_()->overrideFontId, styleId, sizeId)); if (overrideFont != d && (*glyphIndex = glyphIndex_Font_(overrideFont, ch)) != 0) { return overrideFont; } } /* The font's own version of the glyph. */ if ((*glyphIndex = glyphIndex_Font_(d, ch)) != 0) { return d; } for (int preferMonospaced = isMonospaced ? 1 : 0; preferMonospaced >= 0; preferMonospaced--) { /* As a fallback, check all other available fonts of this size in priority order. */ iConstForEach(Array, i, ¤t_StbText_()->fontPriorityOrder) { iFont *font = font_Text_(FONT_ID(((const iPrioMapItem *) i.value)->fontIndex, styleId, sizeId)); if (font == d || font == overrideFont) { continue; /* already checked this one */ } if (preferMonospaced && !isMonospaced_Font(font)) { continue; } if ((*glyphIndex = glyphIndex_Font_(font, ch)) != 0) { #if 0 printf("using '%s' (pr:%d) for %lc (%x) => %d [missing in '%s']\n", cstr_String(&font->font.spec->id), font->font.spec->priority, (int) ch, ch, glyphIndex_Font_(font, ch), cstr_String(&d->font.spec->id)); #endif return font; } } } if (!*glyphIndex) { fprintf(stderr, "failed to find %08x (%lc)\n", ch, (int) ch); fflush(stderr); iStbText *tx = current_StbText_(); tx->missingGlyphs = iTrue; /* Remember a few of the latest missing characters. */ iBool gotIt = iFalse; for (size_t i = 0; i < iElemCount(tx->missingChars); i++) { if (tx->missingChars[i] == ch) { gotIt = iTrue; break; } } if (!gotIt) { memmove(tx->missingChars + 1, tx->missingChars, sizeof(tx->missingChars) - sizeof(tx->missingChars[0])); tx->missingChars[0] = ch; } } return d; } static iGlyph *glyphByIndex_Font_(iFont *d, uint32_t glyphIndex) { if (!d->table) { d->table = new_GlyphTable(); } iGlyph* glyph = NULL; void * node = value_Hash(&d->table->glyphs, glyphIndex); if (node) { glyph = node; } else { iStbText *tx = current_StbText_(); /* If the cache is running out of space, clear it and we'll recache what's needed currently. */ if (tx->cacheBottom > tx->cacheSize.y - maxGlyphHeight_Text_(&tx->base)) { #if !defined (NDEBUG) printf("[Text] glyph cache is full, clearing!\n"); fflush(stdout); #endif resetCache_StbText_(tx); } glyph = new_Glyph(glyphIndex); glyph->font = d; /* New glyphs are always allocated at least. This reserves a position in the cache and updates the glyph metrics. */ for (int offsetIndex = 0; offsetIndex < numOffsetSteps_Glyph_; offsetIndex++) { allocate_Font_(d, glyph, offsetIndex); } insert_Hash(&d->table->glyphs, &glyph->node); } return glyph; } static iGlyph *glyph_Font_(iFont *d, iChar ch) { /* The glyph may actually come from a different font; look up the right font. */ uint32_t glyphIndex = 0; iFont *font = characterFont_Font_(d, ch, &glyphIndex); return glyphByIndex_Font_(font, glyphIndex); } iBaseFont *characterFont_BaseFont(iBaseFont *d, iChar ch) { const iGlyph *glyph = glyph_Font_((iFont *) d, ch); if (index_Glyph_(glyph)) { return (iBaseFont *) glyph->font; } return NULL; } static iChar nextChar_(const char **chPos, const char *end) { if (*chPos == end) { return 0; } iChar ch; int len = decodeBytes_MultibyteChar(*chPos, end, &ch); if (len <= 0) { (*chPos)++; /* skip it */ return 0; } (*chPos) += len; return ch; } /*----------------------------------------------------------------------------------------------*/ iDeclareType(RasterGlyph) struct Impl_RasterGlyph { iGlyph *glyph; int hoff; iRect rect; }; static void cacheGlyphs_Font_(iFont *d, const uint32_t *glyphIndices, size_t numGlyphIndices) { /* TODO: Make this an object so it can be used sequentially without reallocating buffers. */ SDL_Surface *buf = NULL; const iInt2 bufSize = init_I2(iMin(1024, d->font.height * iMin(5 * numGlyphIndices, 20)), d->font.height * 4 / 3); int bufX = 0; iArray * rasters = NULL; SDL_Texture *oldTarget = NULL; iBool isTargetChanged = iFalse; iAssert(isExposed_Window(get_Window())); /* We'll flush the buffered rasters periodically until everything is cached. */ size_t index = 0; while (index < numGlyphIndices) { for (; index < numGlyphIndices; index++) { const uint32_t glyphIndex = glyphIndices[index]; const int lastCacheBottom = current_StbText_()->cacheBottom; iGlyph *glyph = glyphByIndex_Font_(d, glyphIndex); if (current_StbText_()->cacheBottom < lastCacheBottom) { /* The cache was reset due to running out of space. We need to restart from the beginning! */ bufX = 0; if (rasters) { clear_Array(rasters); } index = 0; break; } if (!isFullyRasterized_Glyph_(glyph)) { /* Need to cache this. */ if (buf == NULL) { rasters = new_Array(sizeof(iRasterGlyph)); buf = SDL_CreateRGBSurfaceWithFormat( 0, bufSize.x, bufSize.y, LAGRANGE_RASTER_DEPTH, LAGRANGE_RASTER_FORMAT); SDL_SetSurfaceBlendMode(buf, SDL_BLENDMODE_NONE); SDL_SetSurfacePalette(buf, glyphPalette_()); } SDL_Surface *surfaces[4] = { NULL, NULL, NULL, NULL }; for (int si = 0; si < numOffsetSteps_Glyph_; si++) { surfaces[si] = !isRasterized_Glyph_(glyph, si) ? rasterizeGlyph_Font_(glyph->font, index_Glyph_(glyph), si * offsetStep_Glyph_()) : NULL; } iBool outOfSpace = iFalse; iForIndices(i, surfaces) { if (surfaces[i]) { const int w = iMin(surfaces[i]->w, bufSize.x); const int h = surfaces[i]->h; iAssert(w <= bufSize.x); if (bufX + w <= bufSize.x) { SDL_BlitSurface(surfaces[i], NULL, buf, &(SDL_Rect){ bufX, 0, w, h }); pushBack_Array(rasters, &(iRasterGlyph){ glyph, i, init_Rect(bufX, 0, w, h) }); bufX += w; } else { outOfSpace = iTrue; break; } } } iForIndices(i, surfaces) { /* cleanup */ if (surfaces[i]) { if (surfaces[i]->flags & SDL_PREALLOC) { free(surfaces[i]->pixels); } SDL_FreeSurface(surfaces[i]); } } if (outOfSpace) { /* Redo this glyph. `index` does not get incremented. */ break; } } } /* Finished or the buffer is full, copy the glyphs to the cache texture. */ if (!isEmpty_Array(rasters)) { SDL_Renderer *render = current_Text()->render; SDL_Texture *bufTex = SDL_CreateTextureFromSurface(render, buf); SDL_SetTextureBlendMode(bufTex, SDL_BLENDMODE_NONE); if (!isTargetChanged) { isTargetChanged = iTrue; oldTarget = SDL_GetRenderTarget(render); SDL_SetRenderTarget(render, current_StbText_()->cache); } // printf("copying %zu rasters from %p\n", size_Array(rasters), bufTex); fflush(stdout); iConstForEach(Array, i, rasters) { const iRasterGlyph *rg = i.value; // iAssert(isEqual_I2(rg->rect.size, rg->glyph->rect[rg->hoff].size)); const iRect *glRect = &rg->glyph->rect[rg->hoff]; SDL_RenderCopy(render, bufTex, (const SDL_Rect *) &rg->rect, (const SDL_Rect *) glRect); setRasterized_Glyph_(rg->glyph, rg->hoff); // printf(" - %u (hoff %d)\n", index_Glyph_(rg->glyph), rg->hoff); } SDL_DestroyTexture(bufTex); /* Resume with an empty buffer. */ clear_Array(rasters); bufX = 0; } } if (rasters) { delete_Array(rasters); } if (buf) { SDL_FreeSurface(buf); } if (isTargetChanged) { SDL_SetRenderTarget(current_Text()->render, oldTarget); } } iLocalDef void cacheSingleGlyph_Font_(iFont *d, uint32_t glyphIndex) { cacheGlyphs_Font_(d, &glyphIndex, 1); } static void cacheTextGlyphs_Font_(iFont *d, const iRangecc text) { iArray glyphIndices; init_Array(&glyphIndices, sizeof(uint32_t)); iAttributedText attrText; init_AttributedText(&attrText, text, 0, d, none_ColorId, 0, d, none_ColorId, 0); /* We use AttributedText here so the font lookup matches the behavior during text drawing -- glyphs may be selected from a font that's different than `d`. */ const iChar *logicalText = constData_Array(&attrText.logical); iConstForEach(Array, i, &attrText.runs) { const iAttributedRun *run = i.value; if (run->flags.isLineBreak) { continue; } for (int pos = run->logical.start; pos < run->logical.end; pos++) { const iChar ch = logicalText[pos]; if (!isSpace_Char(ch) && !isControl_Char(ch)) { /* TODO: Use `run->font`; the glyph may be selected from a different font. */ const uint32_t glyphIndex = glyphIndex_Font_(d, ch); if (glyphIndex) { pushBack_Array(&glyphIndices, &glyphIndex); } } } } deinit_AttributedText(&attrText); /* TODO: Cache glyphs from ALL the fonts we encountered above. */ cacheGlyphs_Font_(d, constData_Array(&glyphIndices), size_Array(&glyphIndices)); deinit_Array(&glyphIndices); } void cache_Text(int fontId, iRangecc text) { cacheTextGlyphs_Font_(font_Text_(fontId), text); } float horizKern_Font_(iFont *d, uint32_t glyph1, uint32_t glyph2) { #if defined (LAGRANGE_ENABLE_KERNING) if (!enableKerning_Text || ~d->font.spec->flags & fixNunitoKerning_FontSpecFlag) { return 0.0f; } if (glyph1 && glyph2) { /* These indices will be quickly found from the lookup table. */ const uint32_t gi_h = glyphIndex_Font_(d, 'h'); const uint32_t gi_i = glyphIndex_Font_(d, 'i'); int kern = 0; /* Nunito needs some kerning fixes. */ if (glyph1 == glyphIndex_Font_(d, 'W') && (glyph2 == gi_h || glyph2 == gi_i)) { kern = -60; } else if (glyph1 == glyphIndex_Font_(d, 'T') && glyph2 == gi_h) { kern = -25; } else if (glyph1 == glyphIndex_Font_(d, 'V') && glyph2 == gi_i) { kern = -40; } return d->xScale * kern; } #endif return 0.0f; } static float nextTabStop_Font_(const iFont *d, float x) { const float stop = prefs_App()->tabWidth * d->emAdvance; return floorf(x / stop) * stop + stop; } #if defined (LAGRANGE_ENABLE_HARFBUZZ) iDeclareType(GlyphBuffer) struct Impl_GlyphBuffer { hb_buffer_t * hb; iFont * font; const iChar * logicalText; hb_glyph_info_t * glyphInfo; hb_glyph_position_t *glyphPos; unsigned int glyphCount; hb_script_t script; }; static void init_GlyphBuffer_(iGlyphBuffer *d, iFont *font, const iChar *logicalText) { d->hb = hb_buffer_create(); d->font = font; d->logicalText = logicalText; d->glyphInfo = NULL; d->glyphPos = NULL; d->glyphCount = 0; d->script = 0; } static void deinit_GlyphBuffer_(iGlyphBuffer *d) { hb_buffer_destroy(d->hb); } static void shape_GlyphBuffer_(iGlyphBuffer *d) { if (!d->glyphInfo) { hb_shape(d->font->font.file->hbFont, d->hb, NULL, 0); d->glyphInfo = hb_buffer_get_glyph_infos(d->hb, &d->glyphCount); d->glyphPos = hb_buffer_get_glyph_positions(d->hb, &d->glyphCount); } } static float advance_GlyphBuffer_(const iGlyphBuffer *d, iRangei wrapPosRange) { float x = 0.0f; for (unsigned int i = 0; i < d->glyphCount; i++) { const int logPos = d->glyphInfo[i].cluster; if (logPos < wrapPosRange.start || logPos >= wrapPosRange.end) { continue; } x += d->font->xScale * d->glyphPos[i].x_advance; if (d->logicalText[logPos] == '\t') { x = nextTabStop_Font_(d->font, x); } if (i + 1 < d->glyphCount) { x += horizKern_Font_(d->font, d->glyphInfo[i].codepoint, d->glyphInfo[i + 1].codepoint); } } return x; } static void evenMonospaceAdvances_GlyphBuffer_(iGlyphBuffer *d, iFont *baseFont) { const float monoAdvance = baseFont->emAdvance; for (unsigned int i = 0; i < d->glyphCount; ++i) { const hb_glyph_info_t *info = d->glyphInfo + i; if (d->glyphPos[i].x_advance > 0 && d->font != baseFont) { const iChar ch = d->logicalText[info->cluster]; if (ch == 0x20 || isPictograph_Char(ch) || isEmoji_Char(ch) || (ch >= 0x1fb00 && ch <= 0x1fbff /* legacy computing */)) { const float dw = d->font->xScale * d->glyphPos[i].x_advance - (isEmoji_Char(ch) ? 2 : 1) * monoAdvance; d->glyphPos[i].x_offset -= dw / 2 / d->font->xScale - 1; d->glyphPos[i].x_advance -= dw / d->font->xScale - 1; } } } } static void alignOtherFontsVertically_GlyphBuffer_(iGlyphBuffer *d, iFont *baseFont) { int offset = 0; if (d->font->font.height > baseFont->font.height) { /* Doesn't fit on the baseline, so move it up. */ offset = (d->font->font.height - baseFont->font.height) / 2; for (unsigned int i = 0; i < d->glyphCount; ++i) { d->glyphPos[i].y_offset += offset / d->font->yScale; } } } iLocalDef float justificationWeight_(iChar c) { if (c == '.' || c == '!' || c == '?' ||c == ';') { return 2.0f; } /* if (c == ',' || c == ':') { return 1.0f; } */ return 1.0f; } static void justify_GlyphBuffer_(iGlyphBuffer *buffers, size_t numBuffers, iRangei wrapPosRange, float *wrapAdvance, int available, iBool isLast) { iGlyphBuffer *begin = buffers; iGlyphBuffer *end = buffers + numBuffers; float outerSpace = available - *wrapAdvance; float totalInnerSpace = 0.0f; float numSpaces = 0; int numAdvancing = 0; const float maxSpaceExpansion = 0.14f; if (isLast || outerSpace <= 0) { return; } /* TODO: This could use a utility that handles the `wrapPosRange` character span inside a span of runs. */ #define CHECK_LOGPOS() \ if (logPos < wrapPosRange.start) continue; \ if (logPos >= wrapPosRange.end) break /* Find out if there are spaces to expand. */ for (iGlyphBuffer *buf = begin; buf != end; buf++) { for (size_t i = 0; i < buf->glyphCount; i++) { hb_glyph_info_t *info = &buf->glyphInfo[i]; hb_glyph_position_t *pos = &buf->glyphPos[i]; const int logPos = info->cluster; CHECK_LOGPOS(); if (pos->x_advance > 0) { numAdvancing++; } if (buf->logicalText[logPos] == 0x20) { totalInnerSpace += pos->x_advance * buf->font->xScale; float weight = justificationWeight_(buf->logicalText[iMax(0, logPos - 1)]); numSpaces += weight; } } } if (numSpaces >= 2 && totalInnerSpace > 0) { outerSpace = iMin(outerSpace, *wrapAdvance * maxSpaceExpansion); float adv = 0.0f; for (iGlyphBuffer *buf = begin; buf != end; buf++) { const float xScale = buf->font->xScale; for (size_t i = 0; i < buf->glyphCount; i++) { hb_glyph_info_t *info = &buf->glyphInfo[i]; hb_glyph_position_t *pos = &buf->glyphPos[i]; const int logPos = info->cluster; CHECK_LOGPOS(); if (buf->logicalText[logPos] == 0x20) { float weight = justificationWeight_(buf->logicalText[iMax(0, logPos - 1)]); pos->x_advance = (weight * (totalInnerSpace + outerSpace) / numSpaces) / xScale; } adv += pos->x_advance * xScale; } } *wrapAdvance = adv; } /* Finally expand all glyphs a little, if we must. */ if (numAdvancing > 1 && *wrapAdvance < available - 1.0f) { float expandable = *wrapAdvance; float outerSpace = available - expandable; for (iGlyphBuffer *buf = begin; buf != end; buf++) { if (buf->script) continue; const float xScale = buf->font->xScale; for (size_t i = 0; i < buf->glyphCount; i++) { hb_glyph_info_t *info = &buf->glyphInfo[i]; hb_glyph_position_t *pos = &buf->glyphPos[i]; const int logPos = info->cluster; CHECK_LOGPOS(); if (pos->x_advance > 0) { pos->x_advance += (outerSpace / (numAdvancing - 1)) / xScale; } } } *wrapAdvance = available; } } struct Impl_FontRunArgs { size_t maxLen; iFont *font; int color; int baseDir; iFont *baseFont; int baseFgColorId; iChar overrideChar; }; iLocalDef iBool equal_FontRunArgs(const iFontRunArgs *a, const iFontRunArgs *b) { return memcmp(a, b, sizeof(iFontRunArgs)) == 0; } struct Impl_FontRun { uint32_t textCrc32; iFontRunArgs args; iAttributedText attrText; iArray buffers; /* GlyphBuffers */ }; #if defined (LAGRANGE_ENABLE_HARFBUZZ) static const hb_script_t hbScripts_[max_Script] = { 0, HB_SCRIPT_ARABIC, HB_SCRIPT_BENGALI, HB_SCRIPT_DEVANAGARI, HB_SCRIPT_HAN, HB_SCRIPT_HIRAGANA, HB_SCRIPT_KATAKANA, HB_SCRIPT_ORIYA, HB_SCRIPT_TAMIL, }; #endif void init_FontRun(iFontRun *d, const iFontRunArgs *args, const iRangecc text, uint32_t crc) { d->textCrc32 = crc; d->args = *args; /* Split the text into a number of attributed runs that specify exactly which font is used and other attributes such as color. (HarfBuzz shaping is done with one specific font.) */ init_AttributedText(&d->attrText, text, args->maxLen, args->font, args->color, args->baseDir, args->baseFont, args->baseFgColorId, args->overrideChar); const iChar *logicalText = constData_Array(&d->attrText.logical); const iChar *visualText = constData_Array(&d->attrText.visual); const int * logToVis = constData_Array(&d->attrText.logicalToVisual); const int * visToLog = constData_Array(&d->attrText.visualToLogical); const size_t runCount = size_Array(&d->attrText.runs); init_Array(&d->buffers, sizeof(iGlyphBuffer)); resize_Array(&d->buffers, runCount); /* Prepare the HarfBuzz buffers. */ iConstForEach(Array, i, &d->attrText.runs) { const iAttributedRun *run = i.value; iGlyphBuffer *buf = at_Array(&d->buffers, index_ArrayConstIterator(&i)); init_GlyphBuffer_(buf, (iFont *) run->font, logicalText); /* Insert the text in visual order (LTR) in the HarfBuzz buffer for shaping. First we need to map the logical run to the corresponding visual run. */ int v[2] = { logToVis[run->logical.start], logToVis[run->logical.end - 1] }; if (v[0] > v[1]) { iSwap(int, v[0], v[1]); /* always LTR */ } for (int vis = v[0]; vis <= v[1]; vis++) { hb_buffer_add(buf->hb, visualText[vis], visToLog[vis]); } hb_buffer_set_content_type(buf->hb, HB_BUFFER_CONTENT_TYPE_UNICODE); hb_buffer_set_direction(buf->hb, HB_DIRECTION_LTR); /* visual */ const hb_script_t script = hbScripts_[run->flags.script]; if (script) { buf->script = script; hb_buffer_set_script(buf->hb, script); } shape_GlyphBuffer_(buf); /* this may take a little while */ } if (isMonospaced_Font(args->font)) { /* Fit borrowed glyphs into the expected monospacing. */ for (size_t runIndex = 0; runIndex < runCount; runIndex++) { evenMonospaceAdvances_GlyphBuffer_(at_Array(&d->buffers, runIndex), args->font); } } for (size_t runIndex = 0; runIndex < runCount; runIndex++) { alignOtherFontsVertically_GlyphBuffer_(at_Array(&d->buffers, runIndex), args->font); } } void deinit_FontRun(iFontRun *d) { iForEach(Array, b, &d->buffers) { deinit_GlyphBuffer_(b.value); } deinit_Array(&d->buffers); deinit_AttributedText(&d->attrText); } iLocalDef const iGlyphBuffer *buffer_FontRun(const iFontRun *d, size_t pos) { return constAt_Array(&d->buffers, pos); } iDefineTypeConstructionArgs(FontRun, (const iFontRunArgs *args, const iRangecc text, uint32_t crc), args, text, crc) iDeclareType(RunLayer) struct Impl_RunLayer { iFont *font; int mode; iInt2 orig; iRect bounds; const iFontRun *fontRun; const iArray *runOrder; iRangei wrapPosRange; float xCursor; float yCursor; float xCursorMax; }; enum iRunLayerType { background_RunLayerType = 0, foreground_RunLayerType = 1, }; void process_RunLayer_(iRunLayer *d, int layerIndex) { const iAttributedText *attrText = &d->fontRun->attrText; const iArray *buffers = &d->fontRun->buffers; const iChar *logicalText = constData_Array(&attrText->logical); /* TODO: Shouldn't the hit tests be done here? */ for (size_t logRunIndex = 0; logRunIndex < size_Array(d->runOrder); logRunIndex++) { const size_t runIndex = constValue_Array(d->runOrder, logRunIndex, size_t); const iAttributedRun *run = constAt_Array(&attrText->runs, runIndex); if (run->flags.isLineBreak) { d->xCursor = 0.0f; d->yCursor += d->font->font.height; continue; } const iColor fgClr = fgColor_AttributedRun(run); const iColor bgClr = bgColor_AttributedRun(run); iBool isBgFilled = iFalse; if (~d->mode & permanentColorFlag_RunMode) { isBgFilled = (bgClr.a != 0) || (d->mode & fillBackground_RunMode); } const iGlyphBuffer *buf = constAt_Array(buffers, runIndex); iAssert(run->font == (iBaseFont *) buf->font); /* Process all the glyphs. */ for (unsigned int i = 0; i < buf->glyphCount; i++) { const hb_glyph_info_t *info = &buf->glyphInfo[i]; const hb_codepoint_t glyphId = info->codepoint; const int logPos = info->cluster; if (logPos < d->wrapPosRange.start || logPos >= d->wrapPosRange.end) { continue; /* can't break because of RTL (?) */ } iFont *runFont = (iFont *) run->font; const float xOffset = runFont->xScale * buf->glyphPos[i].x_offset; float yOffset = runFont->yScale * buf->glyphPos[i].y_offset; const float xAdvance = runFont->xScale * buf->glyphPos[i].x_advance; const float yAdvance = runFont->yScale * buf->glyphPos[i].y_advance; const iGlyph *glyph = glyphByIndex_Font_(runFont, glyphId); const iChar ch = logicalText[logPos]; if (ch == '\t') { #if 0 if (mode & draw_RunMode) { /* Tab indicator. */ iColor tabColor = get_Color(uiTextAction_ColorId); SDL_SetRenderDrawColor(activeText_->render, tabColor.r, tabColor.g, tabColor.b, 255); const int pad = d->height / 6; SDL_RenderFillRect(activeText_->render, &(SDL_Rect){ orig.x + xCursor, orig.y + yCursor + d->height / 2 - pad / 2, pad, pad }); } #endif d->xCursor = nextTabStop_Font_(d->font, d->xCursor) - xAdvance; } const float xf = d->xCursor + xOffset; float subpixel = xf - (int) xf; if (subpixel < 0.0f) { subpixel = 1.0f + subpixel; } const int hoff = enableHalfPixelGlyphs_Text ? (int) (subpixel / offsetStep_Glyph_()) : 0; if (ch == 0x3001 || ch == 0x3002) { /* Ideographic Comma and Full Stop */ /* Vertical misalignment?? */ if (yOffset == 0.0f) { /* Move down to baseline. Why doesn't HarfBuzz do this? */ yOffset = glyph->d[hoff].y + glyph->rect[hoff].size.y + glyph->d[hoff].y / 4; } } /* Output position for the glyph. */ SDL_Rect dst = { d->orig.x + d->xCursor + xOffset + glyph->d[hoff].x, d->orig.y + d->yCursor - yOffset + glyph->font->font.baseline + glyph->d[hoff].y, glyph->rect[hoff].size.x, glyph->rect[hoff].size.y }; /* Align baselines of different fonts. */ if (run->font != attrText->baseFont && ~run->font->spec->flags & auxiliary_FontSpecFlag) { const int bl1 = ((iFont *) attrText->baseFont)->font.baseline + ((iFont *) attrText->baseFont)->vertOffset; const int bl2 = runFont->font.baseline + runFont->vertOffset; dst.y += bl1 - bl2; } /* Update the bounding box. */ if (layerIndex == background_RunLayerType) { if (d->mode & visualFlag_RunMode) { if (isEmpty_Rect(d->bounds)) { d->bounds = init_Rect(dst.x, dst.y, dst.w, dst.h); } else { d->bounds = union_Rect(d->bounds, init_Rect(dst.x, dst.y, dst.w, dst.h)); } } else { d->bounds.size.x = iMax(d->bounds.size.x, dst.x + dst.w - d->orig.x); d->bounds.size.y = iMax(d->bounds.size.y, d->yCursor + glyph->font->font.height); } } const iBool isSpace = (logicalText[logPos] == 0x20); if (d->mode & draw_RunMode && (isBgFilled || !isSpace)) { dst.x += origin_Paint.x; dst.y += origin_Paint.y; if (layerIndex == background_RunLayerType && isBgFilled) { /* TODO: Backgrounds of all glyphs should be cleared before drawing anything else. */ if (bgClr.a) { SDL_SetRenderDrawColor(current_Text()->render, bgClr.r, bgClr.g, bgClr.b, 255); const SDL_Rect bgRect = { origin_Paint.x + d->orig.x + d->xCursor, origin_Paint.y + d->orig.y + d->yCursor, (int) ceilf(subpixel + xAdvance), d->font->font.height, }; SDL_RenderFillRect(current_Text()->render, &bgRect); } else if (d->mode & fillBackground_RunMode) { /* Alpha blending looks much better if the RGB components don't change in the partially transparent pixels. */ SDL_SetRenderDrawColor(current_Text()->render, fgClr.r, fgClr.g, fgClr.b, 0); SDL_RenderFillRect(current_Text()->render, &dst); } } if (layerIndex == foreground_RunLayerType && !isSpace) { /* Draw the glyph. */ if (!isRasterized_Glyph_(glyph, hoff)) { cacheSingleGlyph_Font_(runFont, glyphId); /* may cause cache reset */ glyph = glyphByIndex_Font_(runFont, glyphId); iAssert(isRasterized_Glyph_(glyph, hoff)); } if (~d->mode & permanentColorFlag_RunMode) { SDL_SetTextureColorMod(current_StbText_()->cache, fgClr.r, fgClr.g, fgClr.b); } SDL_Rect src; memcpy(&src, &glyph->rect[hoff], sizeof(SDL_Rect)); SDL_RenderCopy(current_Text()->render, current_StbText_()->cache, &src, &dst); } #if 0 /* Show spaces and direction. */ if (isSpace) { const iColor debug = get_Color(run->flags.isRTL ? yellow_ColorId : red_ColorId); SDL_SetRenderDrawColor(text_.render, debug.r, debug.g, debug.b, 255); dst.w = xAdvance; dst.h = d->height / 2; dst.y -= d->height / 2; SDL_RenderFillRect(text_.render, &dst); } #endif } d->xCursor += xAdvance; d->yCursor += yAdvance; /* Additional kerning tweak. It would be better to use HarfBuzz font callbacks, but they don't seem to get called? */ if (i + 1 < buf->glyphCount) { d->xCursor += horizKern_Font_(runFont, glyphId, buf->glyphInfo[i + 1].codepoint); } d->xCursorMax = iMax(d->xCursorMax, d->xCursor); } } } static unsigned fontRunCacheHits_ = 0; static unsigned fontRunCacheTotal_ = 0; static iFontRun *makeOrFindCachedFontRun_StbText_(iStbText *d, const iFontRunArgs *runArgs, const iRangecc text, iBool *wasFound) { fontRunCacheTotal_++; #if 0 if (fontRunCacheTotal_ % 100 == 0) { printf("FONT RUN CACHE: %d/%d rate:%.1f%%\n", fontRunCacheHits_, fontRunCacheTotal_, (float) fontRunCacheHits_ / (float) fontRunCacheTotal_ * 100); fflush(stdout); } #endif const uint32_t crc = iCrc32(text.start, size_Range(&text)); iForIndices(i, d->cachedFontRuns) { if (d->cachedFontRuns[i] && d->cachedFontRuns[i]->textCrc32 == crc && equal_FontRunArgs(runArgs, &d->cachedFontRuns[i]->args)) { d->cachedFontRuns[i]->attrText.source = text; fontRunCacheHits_++; *wasFound = iTrue; return d->cachedFontRuns[i]; } } *wasFound = iFalse; delete_FontRun(d->cachedFontRuns[iElemCount(d->cachedFontRuns) - 1]); memmove(d->cachedFontRuns + 1, d->cachedFontRuns, sizeof(d->cachedFontRuns) - sizeof(d->cachedFontRuns[0])); d->cachedFontRuns[0] = new_FontRun(runArgs, text, crc); return d->cachedFontRuns[0]; } static void run_Font_(iFont *d, const iRunArgs *args) { const int mode = args->mode; const iInt2 orig = args->pos; iRect bounds = { orig, init_I2(0, d->font.height) }; float xCursor = 0.0f; float yCursor = 0.0f; float xCursorMax = 0.0f; const iBool isMonospaced = isMonospaced_Font(d); iWrapText *wrap = args->wrap; iFontRun *fontRun; iBool didFindCachedFontRun = iFalse; /* Set the default text foreground color. */ if (mode & draw_RunMode) { const iColor clr = get_Color(args->color); SDL_SetTextureColorMod(current_StbText_()->cache, clr.r, clr.g, clr.b); } iAssert(args->text.end >= args->text.start); /* We keep a small cache of recently shaped runs because preparing these can be expensive. Quite frequently the same text is quickly re-drawn and/or measured (e.g., InputWidget). */ fontRun = makeOrFindCachedFontRun_StbText_( current_StbText_(), &(iFontRunArgs){ args->maxLen, d, args->color, args->baseDir, current_Text()->baseFontId >= 0 ? font_Text_(current_Text()->baseFontId) : d, current_Text()->baseFgColorId, wrap ? wrap->overrideChar : 0 }, args->text, &didFindCachedFontRun); const iAttributedText *attrText = &fontRun->attrText; const size_t runCount = size_Array(&attrText->runs); const iChar *logicalText = constData_Array(&attrText->logical); if (wrap) { wrap->baseDir = attrText->isBaseRTL ? -1 : +1; /* TODO: Duplicated args? */ iAssert(equalRange_Rangecc(wrap->text, args->text)); /* Initialize the wrap range. */ wrap->wrapRange_ = args->text; wrap->hitAdvance_out = zero_I2(); wrap->hitChar_out = NULL; wrap->hitGlyphNormX_out = 0.0f; } iBool willAbortDueToWrap = iFalse; const size_t textLen = size_Array(&attrText->logical); iRanges wrapRuns = { 0, runCount }; iRangei wrapPosRange = { 0, textLen }; int wrapResumePos = textLen; /* logical position where next line resumes */ size_t wrapResumeRunIndex = runCount; /* index of run where next line resumes */ iTextAttrib attrib = { .fgColorId = args->color, .bgColorId = none_ColorId, .isBaseRTL = attrText->isBaseRTL }; iTextAttrib wrapAttrib = attrib; iTextAttrib lastAttrib = attrib; const int layoutBound = (wrap ? wrap->maxWidth : args->layoutBound); iBool isFirst = iTrue; const iBool checkHitPoint = wrap && !isEqual_I2(wrap->hitPoint, zero_I2()); const iBool checkHitChar = wrap && wrap->hitChar; iBool wasCharHit = iFalse; size_t numWrapLines = 0; while (!isEmpty_Range(&wrapRuns)) { if (isFirst) { isFirst = iFalse; } else { xCursor = 0; yCursor += d->font.height; } float wrapAdvance = 0.0f; /* First we need to figure out how much text fits on the current line. */ if (wrap) { float breakAdvance = -1.0f; size_t breakRunIndex = iInvalidPos; iAssert(wrapPosRange.end == textLen); /* Determine ends of wrapRuns and wrapVisRange. */ int safeBreakPos = -1; for (size_t runIndex = wrapRuns.start; runIndex < wrapRuns.end; runIndex++) { const iAttributedRun *run = constAt_Array(&attrText->runs, runIndex); /* Update the attributes. */ if (run->flags.isLineBreak) { if (checkHitChar && wrap->hitChar == sourcePtr_AttributedText(attrText, run->logical.start)) { wrap->hitAdvance_out = init_I2(wrapAdvance, yCursor); } wrapPosRange.end = run->logical.start; wrapResumePos = run->logical.end; wrapRuns.end = runIndex; wrapResumeRunIndex = runIndex + 1; break; } wrapResumeRunIndex = runCount; wrapResumePos = textLen; iGlyphBuffer *buf = at_Array(&fontRun->buffers, runIndex); iAssert(run->font == (iAnyFont *) buf->font); iChar prevCh[2] = { 0, 0 }; lastAttrib = run->attrib; // printf("checking run %zu...\n", runIndex); for (unsigned int ir = 0; ir < buf->glyphCount; ir++) { const int i = (run->attrib.isRTL ? buf->glyphCount - ir - 1 : ir); const hb_glyph_info_t *info = &buf->glyphInfo[i]; const hb_codepoint_t glyphId = info->codepoint; const int logPos = info->cluster; if (logPos < wrapPosRange.start || logPos >= wrapPosRange.end) { continue; } iFont * runFont = (iFont *) run->font; const iGlyph *glyph = glyphByIndex_Font_(runFont, glyphId); // const int glyphFlags = hb_glyph_info_get_glyph_flags(info); const float xOffset = runFont->xScale * buf->glyphPos[i].x_offset; const float xAdvance = runFont->xScale * buf->glyphPos[i].x_advance; const iChar ch = logicalText[logPos]; const enum iWrapTextMode wrapMode = isCJK_Script(run->flags.script) ? anyCharacter_WrapTextMode : args->wrap->mode; iAssert(xAdvance >= 0); if (wrapMode == word_WrapTextMode) { /* When word-wrapping, only consider certain places breakable. */ if (((prevCh[0] == '-' || prevCh[0] == '/' || prevCh[0] == '\\' || prevCh[0] == '?' || prevCh[0] == '!' || prevCh[0] == '&' || prevCh[0] == '+' || prevCh[0] == '_' || prevCh[0] == '@') && !isPunct_Char(ch)) || (isAlpha_Char(prevCh[1]) && prevCh[0] == '.' && isAlpha_Char(ch))) { safeBreakPos = logPos; breakAdvance = wrapAdvance; breakRunIndex = runIndex; // printf("sbp:%d breakAdv_A:%f\n", safeBreakPos, breakAdvance); // isSoftHyphenBreak = iFalse; } else if (isSpace_Char(ch)) { safeBreakPos = logPos; breakAdvance = wrapAdvance; breakRunIndex = runIndex; // printf("sbp:%d breakAdv_B:%f\n", safeBreakPos, breakAdvance); // isSoftHyphenBreak = iFalse; } prevCh[1] = prevCh[0]; prevCh[0] = ch; } else { safeBreakPos = logPos; breakAdvance = wrapAdvance; breakRunIndex = runIndex; wrapAttrib = run->attrib; } if (ch == '\t') { wrapAdvance = nextTabStop_Font_(d, wrapAdvance) - xAdvance; } /* Out of room? */ if (wrap->maxWidth > 0 && wrapAdvance + xOffset + glyph->d[0].x + glyph->rect[0].size.x > args->wrap->maxWidth) { // printf("out of room at lp:%d! safeBreakPos:%d (idx:%zu) breakAdv:%f\n", // logPos, safeBreakPos, // breakRunIndex, breakAdvance); if (safeBreakPos > wrapPosRange.start) { /* There's a usable break position ahead of the line start. */ wrapPosRange.end = safeBreakPos; } else { /* No usable safe break (either none found, or it equals the start of the current line, which would cause no forward progress). */ if (wrapMode == word_WrapTextMode && run->logical.start > wrapPosRange.start) { /* Don't have a word break position, so the whole run needs to be cut. */ wrapPosRange.end = run->logical.start; wrapResumePos = run->logical.start; wrapRuns.end = runIndex + 1; wrapResumeRunIndex = runIndex; break; } if (logPos == wrapPosRange.start) { /* The very first glyph on this line is too wide to fit. Force it onto the line anyway to ensure forward progress and prevent an infinite loop. */ wrapPosRange.end = logPos + 1; breakAdvance = wrapAdvance + xAdvance; } else { wrapPosRange.end = logPos; breakAdvance = wrapAdvance; } breakRunIndex = runIndex; } wrapResumePos = wrapPosRange.end; if (wrapMode != anyCharacter_WrapTextMode) { while (wrapResumePos < textLen && isSpace_Char(logicalText[wrapResumePos])) { wrapResumePos++; /* skip space */ } } wrapRuns.end = breakRunIndex + 1; /* still includes this run */ wrapResumeRunIndex = breakRunIndex; /* ...but continue from the same one */ // printf("-> wrapAdv:%f (breakAdv:%f)\n", wrapAdvance, breakAdvance); wrapAdvance = breakAdvance; // printf("wrapResumePos:%d\n", wrapResumePos); break; } wrapAdvance += xAdvance; /* Additional kerning tweak. It would be better to use HarfBuzz font callbacks, but they don't seem to get called? */ if (i + 1 < buf->glyphCount) { wrapAdvance += horizKern_Font_(buf->font, glyphId, buf->glyphInfo[i + 1].codepoint); } } // printf("...finished checking run %zu\n", runIndex); } } else { /* Not wrapped so everything fits! Calculate total advance without wrapping. */ for (size_t i = wrapRuns.start; i < wrapRuns.end; i++) { wrapAdvance += advance_GlyphBuffer_(buffer_FontRun(fontRun, i), wrapPosRange); } } /* Justification. */ if (args->justify && !didFindCachedFontRun && layoutBound && !isMonospaced) { /* NOTE: May modify a cached FontRun! */ justify_GlyphBuffer_(at_Array(&fontRun->buffers, wrapRuns.start), size_Range(&wrapRuns), wrapPosRange, &wrapAdvance, layoutBound, wrapRuns.start > 0 && wrapRuns.end == runCount /* last wrap? */); } /* Hit tests. */ if (checkHitPoint || checkHitChar) { iAssert(wrap); const iBool isHitPointOnThisLine = (checkHitPoint && wrap->hitPoint.y >= orig.y + yCursor && wrap->hitPoint.y < orig.y + yCursor + d->font.height); float hitAdvance = 0.0f; for (size_t i = wrapRuns.start; i < wrapRuns.end; i++) { const iGlyphBuffer *buf = buffer_FontRun(fontRun, i); for (size_t j = 0; j < buf->glyphCount; j++) { const int logPos = buf->glyphInfo[j].cluster; CHECK_LOGPOS(); const float xAdvance = buf->glyphPos[j].x_advance * buf->font->xScale; if (checkHitChar && !wasCharHit) { const char *sourceLoc = sourcePtr_AttributedText(attrText, logPos); if (sourceLoc <= wrap->hitChar) { wrap->hitAdvance_out = init_I2(hitAdvance, yCursor); } if (sourceLoc >= wrap->hitChar) { wasCharHit = iTrue; /* variation selectors etc. have matching cluster */ } } if (isHitPointOnThisLine) { if (wrap->hitPoint.x >= orig.x + hitAdvance && wrap->hitPoint.x < orig.x + hitAdvance + xAdvance) { wrap->hitChar_out = sourcePtr_AttributedText(attrText, logPos); wrap->hitGlyphNormX_out = (wrap->hitPoint.x - wrapAdvance) / xAdvance; } } hitAdvance += xAdvance; } } if (checkHitChar && !wasCharHit) { wrap->hitAdvance_out = init_I2(hitAdvance, yCursor); /* last end of line */ } if (isHitPointOnThisLine && !wrap->hitChar_out) { /* Check if the hit point is on the left side of this line. */ if (wrap->hitPoint.x < orig.x) { const iGlyphBuffer *buf = buffer_FontRun(fontRun, wrapRuns.start); if (buf->glyphCount > 0) { wrap->hitChar_out = sourcePtr_AttributedText(attrText, buf->glyphInfo[0].cluster); wrap->hitGlyphNormX_out = 0.0f; } } /* Maybe on the right side? */ else { if (wrapResumePos == textLen) { wrap->hitChar_out = sourcePtr_AttributedText(attrText, wrapResumePos); } else { const char *hit = sourcePtr_AttributedText(attrText, iMax(0, wrapResumePos - 1)); while (hit > args->text.start) { if (!isSpace_Char(hit[-1])) break; hit--; } wrap->hitChar_out = hit; } wrap->hitGlyphNormX_out = 0.0f; } } } iArray runOrder; init_Array(&runOrder, sizeof(size_t)); /* Reorder the run indices according to text direction. */ { size_t oppositeInsertIndex = iInvalidPos; for (size_t runIndex = wrapRuns.start; runIndex < wrapRuns.end; runIndex++) { const iAttributedRun *run = at_Array(&fontRun->attrText.runs, runIndex); if (!attrText->isBaseRTL) { /* left-to-right */ if (run->attrib.isRTL) { if (oppositeInsertIndex == iInvalidPos) { oppositeInsertIndex = size_Array(&runOrder); } insert_Array(&runOrder, oppositeInsertIndex, &runIndex); } else { pushBack_Array(&runOrder, &runIndex); oppositeInsertIndex = iInvalidPos; } } else { /* right-to-left */ if (!run->attrib.isRTL) { if (oppositeInsertIndex == iInvalidPos) { oppositeInsertIndex = 0; } insert_Array(&runOrder, oppositeInsertIndex++, &runIndex); } else { pushFront_Array(&runOrder, &runIndex); oppositeInsertIndex = iInvalidPos; } } } #if 0 printf("Run order: "); iConstForEach(Array, ro, &runOrder) { const size_t *idx = ro.value; printf("%zu {%s}\n", *idx, cstr_Rangecc(sourceRange_AttributedText_(&attrText, ((const iAttributedRun *) at_Array(&attrText.runs, *idx))->logical))); } printf("\n"); #endif } iAssert(size_Array(&runOrder) == size_Range(&wrapRuns)); /* Alignment. */ int origin = 0; iBool isRightAligned = attrText->isBaseRTL; if (isRightAligned) { if (layoutBound > 0) { origin = layoutBound - wrapAdvance; } } /* Make a callback for each wrapped line. */ if (wrap && wrap->wrapFunc && !notify_WrapText(args->wrap, sourcePtr_AttributedText(attrText, wrapResumePos), wrapAttrib, origin, iRound(wrapAdvance))) { willAbortDueToWrap = iTrue; } numWrapLines++; if (wrap && wrap->maxLines && numWrapLines == wrap->maxLines) { willAbortDueToWrap = iTrue; } wrapAttrib = lastAttrib; /* We have determined a possible wrap position and alignment for the work runs, so now we can process the glyphs. However, glyphs may sometimes overlap due to kerning, so all backgrounds must be drawn first, as a separate layer, before any foreground glyphs. Otherwise, there would be visible clipping. */ iRunLayer layer = { /* TODO: Could use this already above and not duplicate the variables here. */ .font = d, .mode = mode, .orig = orig, .bounds = bounds, .fontRun = fontRun, .runOrder = &runOrder, .wrapPosRange = wrapPosRange, .xCursorMax = xCursorMax, .yCursor = yCursor, }; for (int layerIndex = 0; layerIndex < 2; layerIndex++) { if (~mode & draw_RunMode && layerIndex == foreground_RunLayerType) { continue; /* just one layer for measurements */ } layer.xCursor = origin; layer.yCursor = yCursor; process_RunLayer_(&layer, layerIndex); } bounds = layer.bounds; xCursor = layer.xCursor; xCursorMax = layer.xCursorMax; yCursor = layer.yCursor; deinit_Array(&runOrder); if (willAbortDueToWrap) { break; } wrapRuns.start = wrapResumeRunIndex; wrapRuns.end = runCount; wrapPosRange.start = wrapResumePos; wrapPosRange.end = textLen; } if (endsWith_Rangecc(args->text, "\n")) { /* FIXME: This is a kludge, the wrap loop should handle this case, too. */ /* The last wrap is an empty newline wrap. */ xCursor = 0; yCursor += d->font.height; } if (args->metrics_out) { args->metrics_out->advance = init_I2(xCursor, yCursor); args->metrics_out->bounds = bounds; } } #else /* !defined (LAGRANGE_ENABLE_HARFBUZZ) */ /* The fallback method: an incomplete solution for simple scripts. */ # define run_Font_ runSimple_Font_ # include "text_simple.c" #endif /* defined (LAGRANGE_ENABLE_HARFBUZZ) */ void run_Font(iBaseFont *font, const iRunArgs *args) { return run_Font_((iFont *) font, args); } /*----------------------------------------------------------------------------------------------*/ iBool checkMissing_Text(void) { iStbText *d = current_StbText_(); const iBool missing = d->missingGlyphs; d->missingGlyphs = iFalse; return missing; } iChar missing_Text(size_t index) { iStbText *d = current_StbText_(); if (index >= iElemCount(d->missingChars)) { return 0; } return d->missingChars[index]; } void resetMissing_Text(iText *d) { ((iStbText *) d)->missingGlyphs = iFalse; iZap(((iStbText *) d)->missingChars); } SDL_Texture *glyphCache_Text(void) { return current_StbText_()->cache; }