From 59fbd6c46a51bd385abb713e2cff8610296bfb5c Mon Sep 17 00:00:00 2001
From: Sergey Fedorov <barracuda@macos-powerpc.org>
Date: Tue, 21 Jul 2026 14:15:22 +0000
Subject: [PATCH 03/13] fix(vendor): bold/italic glyph spacing and family
 resolution on 10.6
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Squashed from 2 original commits (upstream-history split into vendor/
frontend layers, 2026-07-21).

- vtrasterizer/TextRenderer.cpp: floor (not round-to-nearest) the shaped
  glyph advance to whole cells. A bold or fallback face whose glyphs are not
  perfectly cell-matched reports a per-glyph advance a little over one cell;
  nearest-rounding pushed some glyphs to two cells and others to one, causing
  ragged bold-label spacing ("Ke rnel"). Flooring trims only the overshoot; a
  real N-cell glyph (wide CJK, ligature) still reports an exact N-cell
  multiple and is unaffected. Contour is LTR, so advances are non-negative
  and floor-truncation is safe.
- text_shaper/coretext_locator.mm: on 10.6, CTFontCreateWithFontDescriptor
  given a family name plus a weight/slant traits dictionary returns a
  DIFFERENT family (e.g. Lucida Grande) when the requested weight is not an
  exact trait match, rather than the family's closest face — a 10.6 CoreText
  matching quirk. Fix: after resolving, verify the font's family equals the
  requested family; if not, re-resolve via NSFontManager
  fontWithFamily:traits:weight:size:, which reliably returns the family's own
  bold/italic member.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LzTmVgP2ruMz987VJ78k77
(cherry picked from commit cf2e336df5fd4112c2baf4b01063cae6e4adfa46)
---
 src/text_shaper/CoreTextLocator.mm | 39 ++++++++++++++++++++++++++++++
 src/vtrasterizer/TextRenderer.cpp  | 12 ++++++++-
 2 files changed, 50 insertions(+), 1 deletion(-)

diff --git a/src/text_shaper/CoreTextLocator.mm b/src/text_shaper/CoreTextLocator.mm
index 901202a4..263c4767 100644
--- a/src/text_shaper/CoreTextLocator.mm
+++ b/src/text_shaper/CoreTextLocator.mm
@@ -162,6 +162,45 @@ namespace text
 
         CTFontRef font = CTFontCreateWithFontDescriptor(descriptor, 12.0, NULL);
 
+        // On 10.6, CTFontCreateWithFontDescriptor with a family name PLUS a weight/slant traits dict
+        // is unreliable: when the requested weight is not an exact match it can return a completely
+        // different family (e.g. the system default Lucida Grande) instead of the family's closest
+        // face. That is exactly the "bold text falls back to Lucida" bug. Verify the resolved font is
+        // still the requested family; if not, resolve the family + bold/italic traits via NSFontManager
+        // (which reliably returns the family's own bold/italic member) and use that instead.
+        if (font)
+        {
+            CFStringRef resolvedFamily = CTFontCopyFamilyName(font);
+            bool const familyMatches =
+                resolvedFamily && CFStringCompare(resolvedFamily, familyName, 0) == kCFCompareEqualTo;
+            if (resolvedFamily)
+                CFRelease(resolvedFamily);
+
+            if (!familyMatches)
+            {
+                NSFontTraitMask mask = 0;
+                if (description.weight == FontWeight::Bold
+                    || description.weight == FontWeight::ExtraBold
+                    || description.weight == FontWeight::Black
+                    || description.weight == FontWeight::ExtraBlack
+                    || description.weight == FontWeight::DemiBold)
+                    mask |= NSBoldFontMask;
+                if (description.slant == FontSlant::Italic)
+                    mask |= NSItalicFontMask;
+
+                NSString* nsFamily = [NSString stringWithUTF8String:description.familyName.c_str()];
+                NSFont* nsFont = [_d->fm fontWithFamily:nsFamily
+                                                 traits:mask
+                                                 weight:(mask & NSBoldFontMask) ? 9 : 5
+                                                   size:12.0];
+                if (nsFont)
+                {
+                    CFRelease(font);
+                    font = CTFontCreateWithName((CFStringRef) [nsFont fontName], 12.0, NULL);
+                }
+            }
+        }
+
         if (font)
         {
             char path[PATH_MAX];
diff --git a/src/vtrasterizer/TextRenderer.cpp b/src/vtrasterizer/TextRenderer.cpp
index 80fe93f8..25e4ea71 100644
--- a/src/vtrasterizer/TextRenderer.cpp
+++ b/src/vtrasterizer/TextRenderer.cpp
@@ -717,7 +717,17 @@ void TextRenderer::renderTextGroup(std::u32string_view codepoints,
         // let the pen step by the exact cell delta -- zero for a combining mark, N for a ligature spanning
         // N cells -- with no rounding at all. That awaits TextClusterGrouper's east-asian-width fixme,
         // since clusters presently count cells appended rather than columns occupied.
-        pen.x += advanceToCells(glyphPosition.advance.x, cellWidth) * cellWidth * advanceScale;
+        //
+        // Until then, snap the advance DOWN to whole cells rather than to the nearest: a bold/fallback
+        // face whose glyphs are not perfectly cell-matched reports an advance a little over one cell
+        // (e.g. ~1.3), and nearest-rounding pushes some of those to two cells, producing ragged
+        // "Ke rnel"/"Di splay" spacing. A real N-cell glyph (wide CJK, ligature) reports an advance at
+        // an exact N-cell multiple, so flooring keeps it at N; only the sub-multiple overshoot of a
+        // mismatched face is trimmed. A non-zero advance always steps at least one cell.
+        auto const advCells = glyphPosition.advance.x == 0
+                                  ? 0
+                                  : std::max(1, glyphPosition.advance.x / std::max(1, cellWidth));
+        pen.x += advCells * cellWidth * advanceScale;
     }
 }
 
