Lagrange [work/v1.6]

Text: Align monospace glyph sizing to pixel grid

=> 73c96402a21872dba8999caba8f4374a91f0d8e4

diff --git a/src/ui/text.c b/src/ui/text.c
index 1761a87c..a8be33b6 100644
--- a/src/ui/text.c
+++ b/src/ui/text.c
@@ -85,7 +85,7 @@ iDefineTypeConstructionArgs(Glyph, (iChar ch), ch)
 struct Impl_Font {
     iBlock *       data;
     stbtt_fontinfo font;
-    float          scale;
+    float          xScale, yScale;
     int            vertOffset; /* offset due to scaling */
     int            height;
     int            baseline;
@@ -100,21 +100,33 @@ struct Impl_Font {
 
 static iFont *font_Text_(enum iFontId id);
 
-static void init_Font(iFont *d, const iBlock *data, int height, float scale, enum iFontId symbolsFont) {
+static void init_Font(iFont *d, const iBlock *data, int height, float scale,
+                      enum iFontId symbolsFont, iBool isMonospaced) {
     init_Hash(&d->glyphs);
     d->data = NULL;
+    d->isMonospaced = isMonospaced;
     d->height = height;
     iZap(d->font);
     stbtt_InitFont(&d->font, constData_Block(data), 0);
-    d->scale = stbtt_ScaleForPixelHeight(&d->font, height) * scale;
+    d->xScale = d->yScale = stbtt_ScaleForPixelHeight(&d->font, height) * scale;
+    if (d->isMonospaced) {
+        /* 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. */
+        int adv;
+        stbtt_GetCodepointHMetrics(&d->font, 'M', &adv, NULL);
+        const float advance = (float) adv * d->xScale;
+        if (advance > 4) { /* not too tiny */
+            d->xScale *= floorf(advance) / advance;
+        }
+    }
     d->vertOffset = height * (1.0f - scale) / 2;
     int ascent;
     stbtt_GetFontVMetrics(&d->font, &ascent, NULL, NULL);
-    d->baseline     = ceil(ascent * d->scale);
+    d->baseline     = ceil(ascent * d->yScale);
     d->symbolsFont  = symbolsFont;
     d->japaneseFont = regularJapanese_FontId;
     d->koreanFont   = regularKorean_FontId;
-    d->isMonospaced = iFalse;
     memset(d->indexTable, 0xff, sizeof(d->indexTable));
 }
 
@@ -271,11 +283,12 @@ static void initFonts_Text_(iText *d) {
     };
     iForIndices(i, fontData) {
         iFont *font = &d->fonts[i];
-        init_Font(
-            font, fontData[i].ttf, fontData[i].size, fontData[i].scaling, fontData[i].symbolsFont);
-        if (fontData[i].ttf == &fontFiraMonoRegular_Embedded) {
-            font->isMonospaced = iTrue;
-        }
+        init_Font(font,
+                  fontData[i].ttf,
+                  fontData[i].size,
+                  fontData[i].scaling,
+                  fontData[i].symbolsFont,
+                  fontData[i].ttf == &fontFiraMonoRegular_Embedded);
         if (i == default_FontId || i == defaultMedium_FontId) {
             font->manualKernOnly = iTrue;
         }
@@ -423,7 +436,7 @@ static void freeBmp_(void *ptr) {
 static SDL_Surface *rasterizeGlyph_Font_(const iFont *d, uint32_t glyphIndex, float xShift) {
     int w, h;
     uint8_t *bmp = stbtt_GetGlyphBitmapSubpixel(
-        &d->font, d->scale, d->scale, xShift, 0.0f, glyphIndex, &w, &h, 0, 0);
+        &d->font, d->xScale, d->yScale, xShift, 0.0f, glyphIndex, &w, &h, 0, 0);
     collect_Garbage(bmp, freeBmp_); /* `bmp` must be freed afterwards. */
     SDL_Surface *surface8 =
         SDL_CreateRGBSurfaceWithFormatFrom(bmp, w, h, 8, w, SDL_PIXELFORMAT_INDEX8);
@@ -478,12 +491,12 @@ static void cache_Font_(iFont *d, iGlyph *glyph, int hoff) {
             int adv;
             const uint32_t gIndex = glyph->glyphIndex;
             stbtt_GetGlyphHMetrics(&d->font, gIndex, &adv, NULL);
-            glyph->advance = d->scale * adv;
+            glyph->advance = d->xScale * adv;
         }
         stbtt_GetGlyphBitmapBoxSubpixel(&d->font,
                                         glyph->glyphIndex,
-                                        d->scale,
-                                        d->scale,
+                                        d->xScale,
+                                        d->yScale,
                                         hoff * 0.5f,
                                         0.0f,
                                         &glyph->d[hoff].x,
@@ -734,11 +747,7 @@ static iRect run_Font_(iFont *d, enum iRunMode mode, iRangecc text, size_t maxLe
         }
         const iBool useMonoAdvance =
             monoAdvance > 0 && !isJapanese_FontId(fontId_Text_(glyph->font));
-        /* The -0.25f is to mitigate issues with box-drawing characters sometimes rounding
-           up to leave a hairline gap with the previous character. The purpose is to overlap
-           the glyphs slightly, since they are rendered antialiased and unhinted, which means
-           the right edge pixels may end up partially non-opaque. */
-        const float advance = (useMonoAdvance ? monoAdvance - 0.25f : glyph->advance);
+        const float advance = (useMonoAdvance ? monoAdvance : glyph->advance);
         if (!isMeasuring_(mode)) {
             if (useMonoAdvance && dst.w > advance && glyph->font != d) {
                 /* Glyphs from a different font may need recentering to look better. */
@@ -769,7 +778,7 @@ static iRect run_Font_(iFont *d, enum iRunMode mode, iRangecc text, size_t maxLe
             const char *peek = chPos;
             const iChar next = nextChar_(&peek, text.end);
             if (enableKerning_Text && !d->manualKernOnly && next) {
-                xpos += d->scale * stbtt_GetGlyphKernAdvance(&d->font, glyph->glyphIndex, next);
+                xpos += d->xScale * stbtt_GetGlyphKernAdvance(&d->font, glyph->glyphIndex, next);
             }
         }
 #endif
Proxy Information
Original URL
gemini://git.skyjake.fi/lagrange/work%2Fv1.6/cdiff/73c96402a21872dba8999caba8f4374a91f0d8e4
Status Code
Success (20)
Meta
text/gemini; charset=utf-8
Capsule Response Time
56.239211 milliseconds
Gemini-to-HTML Time
0.443592 milliseconds

This content has been proxied by September (ba2dc).