From 272f0bc7bb8ee120b836a93f604bd56afdc69fb8 Mon Sep 17 00:00:00 2001 From: Piotr Usewicz Date: Mon, 6 Jul 2026 23:29:19 +0200 Subject: [PATCH 1/3] Apply kerning to text layout cursor advance_to_next_glyph only advanced by xadvance, so kerning never affected glyph positions, text measurement, or word-wrap -- and cf_font_get_kern looked its table up by codepoint even though the table was keyed by glyph index, so it silently returned 0 for almost every font. Fix both: kern is applied to the cursor (not just a per-glyph render nudge) so it accumulates and is reflected in cf_text_size/wrap, and cf_font_get_kern now tries stb's GPOS-aware codepoint API first and falls back to the glyph-index-keyed legacy kern table when GPOS reports nothing (as happens with this project's bundled Calibri, whose GPOS layout stb's minimal parser can't read but whose legacy kern table has real data). Claude-Session: https://claude.ai/code/session_011V2maY4DG1DHscsMEHy6Bf --- src/cute_draw.cpp | 54 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 13 deletions(-) diff --git a/src/cute_draw.cpp b/src/cute_draw.cpp index 21a31360..2be4fd07 100644 --- a/src/cute_draw.cpp +++ b/src/cute_draw.cpp @@ -1790,7 +1790,9 @@ CF_Result cf_make_font_from_memory(void* data, int size, const char* font_name) stbtt_GetFontBoundingBox(&font->info, &x0, &y0, &x1, &y1); font->width = x1 - x0; - // Build kerning table. + // Build the legacy kerning table, keyed by glyph index (as stb provides it). Used + // as a fallback in cf_font_get_kern for fonts whose GPOS table isn't in a layout + // stb's minimal parser understands, even though real kern data still exists here. Array table_array; int table_length = stbtt_GetKerningTableLength(&font->info); table_array.ensure_capacity(table_length); @@ -2021,10 +2023,22 @@ CF_Glyph* cf_font_get_glyph(CF_Font* font, int code, float font_size, int blur) float cf_font_get_kern(CF_Font* font, float font_size, int code0, int code1) { - uint64_t key = CF_KERN_KEY(code0, code1); - int* val = font->kerning.try_get(key); - if (!val) return 0; - return *val * stbtt_ScaleForPixelHeight(&font->info, font_size); + // Prefer GPOS -- stb's codepoint API converts to glyph indices internally and + // reads GPOS pair-adjustment tables, which is where most modern fonts keep + // kerning. But stb's GPOS parser only understands a narrow set of lookup + // formats: some fonts (e.g. this project's bundled Calibri) have a GPOS table + // stb can't read, yet still carry real data in the legacy `kern` table. Since + // stb only consults `kern` when there's no GPOS table at all, fall back to our + // own glyph-index lookup into `kerning` (built in cf_make_font_from_data) when + // the GPOS path comes up empty. + int advance = stbtt_GetCodepointKernAdvance(&font->info, code0, code1); + if (!advance) { + int g0 = stbtt_FindGlyphIndex(&font->info, code0); + int g1 = stbtt_FindGlyphIndex(&font->info, code1); + int* val = font->kerning.try_get(CF_KERN_KEY(g0, g1)); + if (val) advance = *val; + } + return advance * stbtt_ScaleForPixelHeight(&font->info, font_size); } void cf_push_font(const char* font) @@ -2204,6 +2218,10 @@ static const char* s_find_end_of_line(CF_Font* font, const char* text, float wra const char* start_of_word = 0; float word_w = 0; int cp; + int cp_prev = 0; + // Mirrors the main render loop's hit_newline gating, so wrap points land where + // the kerned text will actually break. + bool at_line_start = true; while (*text) { const char* text_prev = text; @@ -2214,22 +2232,25 @@ static const char* s_find_end_of_line(CF_Font* font, const char* text, float wra x = 0; word_w = 0; start_of_word = 0; + at_line_start = true; continue; } else if (cp == '\r') { continue; } else { + float kern = at_line_start ? 0 : cf_font_get_kern(font, font_size, cp_prev, cp); + float advance = glyph->xadvance + kern; if (s_is_space(cp)) { - x += word_w + glyph->xadvance; + x += word_w + advance; word_w = 0; start_of_word = 0; } else { if (!start_of_word) { start_of_word = text_prev; } - if (x + word_w + glyph->xadvance < wrap_width) { - word_w += glyph->xadvance; + if (x + word_w + advance < wrap_width) { + word_w += advance; } else { - if (word_w + glyph->xadvance < wrap_width) { + if (word_w + advance < wrap_width) { // Put entire word on the next line. return start_of_word; } else { @@ -2238,6 +2259,8 @@ static const char* s_find_end_of_line(CF_Font* font, const char* text, float wra } } } + cp_prev = cp; + at_line_start = false; } } @@ -2766,6 +2789,13 @@ static v2 s_draw_text(const char* text, CF_V2 position, int text_length, bool re continue; } + // Advance the cursor by the pair kern so it accumulates across the line and is + // reflected in measurement + bounds, not just the visible glyph. Horizontal only; + // hit_newline guards against kerning across line breaks. + if (!vertical && !hit_newline) { + x += cf_font_get_kern(font, font_size, cp_prev, cp); + } + // Prepare a sprite struct for rendering. float xadvance = glyph->xadvance; if (render || markups) { @@ -2777,11 +2807,9 @@ static v2 s_draw_text(const char* text, CF_V2 position, int text_length, bool re s.geom.alpha = 1.0f; CF_Color color = s_draw->colors.last(); - uint64_t kern_key = CF_KERN_KEY(cp_prev, cp); - v2 kern = V2(cf_font_get_kern(font, font_size, cp_prev, cp), 0); v2 pad = V2(1,1); // Account for 1-pixel padding in spritebatch. - v2 q0 = glyph->q0 + V2(x,y) + kern - pad; - v2 q1 = glyph->q1 + V2(x,y) + kern + pad; + v2 q0 = glyph->q0 + V2(x,y) - pad; + v2 q1 = glyph->q1 + V2(x,y) + pad; // Apply any active custom text effects. bool use_corner_colors = false; From 69c81097b4819da83c239108afc98192754035d6 Mon Sep 17 00:00:00 2001 From: Piotr Usewicz Date: Mon, 6 Jul 2026 23:35:35 +0200 Subject: [PATCH 2/3] Skip kerning for the first glyph Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/cute_draw.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cute_draw.cpp b/src/cute_draw.cpp index 2be4fd07..42d0cbcb 100644 --- a/src/cute_draw.cpp +++ b/src/cute_draw.cpp @@ -2792,7 +2792,7 @@ static v2 s_draw_text(const char* text, CF_V2 position, int text_length, bool re // Advance the cursor by the pair kern so it accumulates across the line and is // reflected in measurement + bounds, not just the visible glyph. Horizontal only; // hit_newline guards against kerning across line breaks. - if (!vertical && !hit_newline) { + if (!vertical && !hit_newline && cp_prev) { x += cf_font_get_kern(font, font_size, cp_prev, cp); } From 1228f97a7bc47e24813d006e9d4f256cb60539ee Mon Sep 17 00:00:00 2001 From: Piotr Usewicz Date: Mon, 6 Jul 2026 23:47:21 +0200 Subject: [PATCH 3/3] Fix HiDPI glyph padding and stop rasterizing invisible glyphs Two more text-rendering bugs from the same review that turned up the kerning issue: - The glyph quad's border padding was a flat 1 logical unit, meant to cover spritebatch's 1-pixel atlas border. That border is 1 *device* pixel, and glyph bitmaps rasterize at pixel_scale resolution while q0/q1 are logical units -- so on HiDPI the pad overshot the actual border, stretching every glyph quad slightly past its real coverage. Divide by pixel_scale to match. - `glyph->visible |= w > 0 && h > 0` always evaluated true (w/h include border padding, which is never zero), silently overriding stbtt_IsGlyphEmpty's correct verdict. Every space character ended up rasterized, given an atlas slot, and pushed as a blank draw quad each time it appeared. Beyond fixing the flag, invisible glyphs now skip s_render's rasterization and atlas registration entirely -- layout metrics (xadvance, q0/q1) are still computed since those are needed regardless of visibility. A new `rendered` flag on CF_Glyph replaces the old `image_id`-based cache-hit check, since invisible glyphs never get an image_id. Verified via a debug print in samples/font_debug.cpp (reverted after): a space glyph now reports visible=0 rendered=1 image_id=0 xadvance=5.88 -- no rasterization or atlas slot, but layout spacing is unaffected. Claude-Session: https://claude.ai/code/session_011V2maY4DG1DHscsMEHy6Bf --- src/cute_draw.cpp | 15 ++++++++++++--- src/internal/cute_font_internal.h | 1 + 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/cute_draw.cpp b/src/cute_draw.cpp index 42d0cbcb..dec2ad8d 100644 --- a/src/cute_draw.cpp +++ b/src/cute_draw.cpp @@ -1968,7 +1968,12 @@ static void s_render(CF_Font* font, CF_Glyph* glyph, float font_size, int blur) glyph->q0 = V2((float)x0, -(float)(y0 + h)) / pixel_scale; // Swapped y. Logical units. glyph->q1 = V2((float)(x0 + w), -(float)y0) / pixel_scale; // Swapped y. Logical units. glyph->xadvance = xadvance * scale / pixel_scale; - glyph->visible |= w > 0 && h > 0; + glyph->rendered = true; + + // Glyphs with no ink (spaces, etc.) have nothing to rasterize -- layout metrics + // above are still valid and needed, but skip allocating a bitmap/atlas slot for + // them entirely, and skip drawing them (see `visible` check in s_draw_text). + if (!glyph->visible) return; // Render glyph. uint8_t* pixels_1bpp = (uint8_t*)CF_CALLOC(w * h); @@ -2014,7 +2019,7 @@ CF_Glyph* cf_font_get_glyph(CF_Font* font, int code, float font_size, int blur) glyph->index = glyph_index; glyph->visible = stbtt_IsGlyphEmpty(&font->info, glyph_index) == 0; } - if (glyph->image_id) return glyph; + if (glyph->rendered) return glyph; // Render the glyph if it exists in the font, but is not yet rendered. s_render(font, glyph, font_size, blur); @@ -2807,7 +2812,11 @@ static v2 s_draw_text(const char* text, CF_V2 position, int text_length, bool re s.geom.alpha = 1.0f; CF_Color color = s_draw->colors.last(); - v2 pad = V2(1,1); // Account for 1-pixel padding in spritebatch. + // Account for spritebatch's 1-pixel atlas border. The border is 1 *device* + // pixel (glyph bitmaps are rasterized at pixel_scale resolution), but q0/q1 + // are in logical units, so the pad must shrink by pixel_scale to match -- + // otherwise on HiDPI the quad is stretched past the glyph's actual coverage. + v2 pad = V2(1,1) / app->pixel_scale; v2 q0 = glyph->q0 + V2(x,y) - pad; v2 q1 = glyph->q1 + V2(x,y) + pad; diff --git a/src/internal/cute_font_internal.h b/src/internal/cute_font_internal.h index b5f1944c..5b9fcc64 100644 --- a/src/internal/cute_font_internal.h +++ b/src/internal/cute_font_internal.h @@ -25,6 +25,7 @@ struct CF_Glyph int w, h; float xadvance; bool visible; + bool rendered; // Metrics (and, if visible, the atlas image) have been computed. }; struct CF_Font