fix: honor CJK-capable font-family before fallback injection (#2241)
This commit is contained in:
parent
27fa3873be
commit
97fee253b5
2 changed files with 173 additions and 10 deletions
|
|
@ -4,6 +4,7 @@ import AppKit
|
|||
import Metal
|
||||
import QuartzCore
|
||||
import Combine
|
||||
import CoreText
|
||||
import Darwin
|
||||
import Sentry
|
||||
import Bonsplit
|
||||
|
|
@ -1365,15 +1366,12 @@ class GhosttyApp {
|
|||
/// monospace attributes (e.g. AB_appare from Adobe CC, or LingWai) can be
|
||||
/// selected depending on what is installed. This injects a sensible
|
||||
/// default based on the system's preferred languages without overriding
|
||||
/// user-managed fallback chains.
|
||||
/// user-managed fallback chains or configured fonts that already cover
|
||||
/// the affected CJK ranges.
|
||||
///
|
||||
/// See: https://github.com/manaflow-ai/cmux/pull/1017
|
||||
private func loadCJKFontFallbackIfNeeded(_ config: ghostty_config_t) {
|
||||
let userFontConfig = Self.userFontConfigSummary()
|
||||
if userFontConfig.containsCodepointMap { return }
|
||||
if userFontConfig.hasExplicitFontFamilyFallbackChain { return }
|
||||
|
||||
guard let mappings = Self.cjkFontMappings() else { return }
|
||||
guard let mappings = Self.autoInjectedCJKFontMappings() else { return }
|
||||
|
||||
let lines = mappings.map { range, font in
|
||||
"font-codepoint-map = \(range)=\(font)"
|
||||
|
|
@ -1409,6 +1407,21 @@ class GhosttyApp {
|
|||
"U+30A0-U+30FF", // Katakana
|
||||
]
|
||||
|
||||
/// Representative scalars used to detect whether the configured primary
|
||||
/// font already covers the ranges cmux would otherwise auto-map.
|
||||
private static let cjkCoverageSampleCharactersByRange: [String: [UniChar]] = [
|
||||
"U+3000-U+303F": [0x3001, 0x300C],
|
||||
"U+4E00-U+9FFF": [0x4E00, 0x65E5, 0x6C34],
|
||||
"U+F900-U+FAFF": [0xF900],
|
||||
"U+FF00-U+FFEF": [0xFF10, 0xFF21],
|
||||
"U+3400-U+4DBF": [0x3400],
|
||||
"U+1100-U+11FF": [0x1100, 0x1161],
|
||||
"U+3130-U+318F": [0x3131, 0x314F],
|
||||
"U+3040-U+309F": [0x3042, 0x3093],
|
||||
"U+30A0-U+30FF": [0x30A2, 0x30F3],
|
||||
"U+AC00-U+D7AF": [0xAC00, 0xD55C],
|
||||
]
|
||||
|
||||
private struct UserFontConfigSummary {
|
||||
var containsCodepointMap = false
|
||||
var effectiveFontFamilies: [String] = []
|
||||
|
|
@ -1485,6 +1498,38 @@ class GhosttyApp {
|
|||
return mappings.isEmpty ? nil : mappings
|
||||
}
|
||||
|
||||
/// Returns only the CJK mappings cmux should auto-inject after respecting
|
||||
/// explicit user overrides and the glyph coverage of the configured
|
||||
/// primary font family.
|
||||
static func autoInjectedCJKFontMappings(
|
||||
preferredLanguages: [String] = Locale.preferredLanguages,
|
||||
configPaths: [String] = loadedCJKScanPaths(),
|
||||
rangeCoverageProbe: ((String, String) -> Bool)? = nil
|
||||
) -> [(String, String)]? {
|
||||
guard var mappings = cjkFontMappings(preferredLanguages: preferredLanguages) else { return nil }
|
||||
|
||||
let summary = userFontConfigSummary(configPaths: configPaths)
|
||||
if summary.containsCodepointMap || summary.hasExplicitFontFamilyFallbackChain {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let configuredFontFamily = summary.effectiveFontFamilies.first else {
|
||||
return mappings
|
||||
}
|
||||
|
||||
if let rangeCoverageProbe {
|
||||
mappings.removeAll { range, _ in
|
||||
rangeCoverageProbe(configuredFontFamily, range)
|
||||
}
|
||||
} else if let configuredFont = configuredCTFont(named: configuredFontFamily) {
|
||||
mappings.removeAll { range, _ in
|
||||
fontContainsGlyphs(configuredFont, forRange: range)
|
||||
}
|
||||
}
|
||||
|
||||
return mappings.isEmpty ? nil : mappings
|
||||
}
|
||||
|
||||
/// Checks whether the user's Ghostty config files already contain
|
||||
/// a `font-codepoint-map` entry covering CJK ranges. Also checks
|
||||
/// application-support config paths that cmux may load at runtime.
|
||||
|
|
@ -1502,11 +1547,58 @@ class GhosttyApp {
|
|||
|
||||
static func shouldInjectCJKFontFallback(
|
||||
preferredLanguages: [String] = Locale.preferredLanguages,
|
||||
configPaths: [String] = loadedCJKScanPaths()
|
||||
configPaths: [String] = loadedCJKScanPaths(),
|
||||
rangeCoverageProbe: ((String, String) -> Bool)? = nil
|
||||
) -> Bool {
|
||||
guard cjkFontMappings(preferredLanguages: preferredLanguages) != nil else { return false }
|
||||
let summary = userFontConfigSummary(configPaths: configPaths)
|
||||
return !summary.containsCodepointMap && !summary.hasExplicitFontFamilyFallbackChain
|
||||
autoInjectedCJKFontMappings(
|
||||
preferredLanguages: preferredLanguages,
|
||||
configPaths: configPaths,
|
||||
rangeCoverageProbe: rangeCoverageProbe
|
||||
) != nil
|
||||
}
|
||||
|
||||
private static func configuredCTFont(
|
||||
named name: String,
|
||||
size: CGFloat = 12
|
||||
) -> CTFont? {
|
||||
let trimmed = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return nil }
|
||||
|
||||
let font = CTFontCreateWithName(trimmed as CFString, size, nil)
|
||||
let normalizedRequestedName = normalizedFontName(trimmed)
|
||||
let resolvedNames = [
|
||||
kCTFontFamilyNameKey,
|
||||
kCTFontFullNameKey,
|
||||
kCTFontPostScriptNameKey,
|
||||
].compactMap { CTFontCopyName(font, $0) as String? }
|
||||
|
||||
guard resolvedNames.contains(where: { normalizedFontName($0) == normalizedRequestedName }) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return font
|
||||
}
|
||||
|
||||
private static func fontContainsGlyphs(
|
||||
_ font: CTFont,
|
||||
forRange range: String
|
||||
) -> Bool {
|
||||
guard let characters = cjkCoverageSampleCharactersByRange[range] else {
|
||||
return false
|
||||
}
|
||||
|
||||
var glyphs = Array(repeating: CGGlyph(), count: characters.count)
|
||||
let hasGlyphs = CTFontGetGlyphsForCharacters(font, characters, &glyphs, characters.count)
|
||||
return hasGlyphs && !glyphs.contains(0)
|
||||
}
|
||||
|
||||
private static func normalizedFontName(_ name: String) -> String {
|
||||
name
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.split(whereSeparator: \.isWhitespace)
|
||||
.joined(separator: " ")
|
||||
.folding(options: [.diacriticInsensitive, .widthInsensitive], locale: Locale(identifier: "en_US_POSIX"))
|
||||
.lowercased()
|
||||
}
|
||||
|
||||
private static func userFontConfigSummary(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue