From 1aa932f810a11617e9c9751c72b87aff8e55e281 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 25 Oct 2024 21:25:15 -0700 Subject: [PATCH] font/coretext: use CTFontCreateForString for final codepoint fallback Fixes #2499 We rely on CoreText's font discovery to find the best font for a fallback by using the character set attribute. It appears that for some codepoints, the character set attribute is not enough to find a font that supports the codepoint. In this case, we use CTFontCreateForString to find the font that CoreText would use. The one subtlety here is we need to ignore the last resort font, which just has replacement glyphs for all codepoints. We already had a function to do this for CJK characters (#1637) thankfully so we can just reuse that! This also fixes a bug where CTFontCreateForString range param expects the range length to be utf16 code units, not utf32. --- pkg/macos/text/font.zig | 4 +++ src/font/discovery.zig | 56 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/pkg/macos/text/font.zig b/pkg/macos/text/font.zig index 85f7de47e5..67a3030188 100644 --- a/pkg/macos/text/font.zig +++ b/pkg/macos/text/font.zig @@ -160,6 +160,10 @@ pub const Font = opaque { return @ptrFromInt(@intFromPtr(c.CTFontCopyDisplayName(@ptrCast(self)))); } + pub fn copyPostScriptName(self: *Font) *foundation.String { + return @ptrFromInt(@intFromPtr(c.CTFontCopyPostScriptName(@ptrCast(self)))); + } + pub fn getSymbolicTraits(self: *Font) text.FontSymbolicTraits { return @bitCast(c.CTFontGetSymbolicTraits(@ptrCast(self))); } diff --git a/src/font/discovery.zig b/src/font/discovery.zig index 6f43fcb7de..3aa16eebf1 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -424,7 +424,30 @@ pub const CoreText = struct { }; } - return try self.discover(alloc, desc); + const it = try self.discover(alloc, desc); + + // If our normal discovery doesn't find anything and we have a specific + // codepoint, then fallback to using CTFontCreateForString to find a + // matching font CoreText wants to use. See: + // https://github.com/ghostty-org/ghostty/issues/2499 + if (it.list.len == 0 and desc.codepoint > 0) codepoint: { + const ct_desc = try self.discoverCodepoint( + collection, + desc, + ) orelse break :codepoint; + + const list = try alloc.alloc(*macos.text.FontDescriptor, 1); + errdefer alloc.free(list); + list[0] = ct_desc; + + return DiscoverIterator{ + .alloc = alloc, + .list = list, + .i = 0, + }; + } + + return it; } /// Discover a font for a specific codepoint using the CoreText @@ -491,16 +514,45 @@ pub const CoreText = struct { ); defer str.release(); + // Get our range length for CTFontCreateForString. It looks like + // the range uses UTF-16 codepoints and not UTF-32 codepoints. + const range_len: usize = range_len: { + var unichars: [2]u16 = undefined; + const pair = macos.foundation.stringGetSurrogatePairForLongCharacter( + desc.codepoint, + &unichars, + ); + break :range_len if (pair) 2 else 1; + }; + // Get our font const font = original.font.createForString( str, - macos.foundation.Range.init(0, 1), + macos.foundation.Range.init(0, range_len), ) orelse return null; defer font.release(); + // Do not allow the last resort font to go through. This is the + // last font used by CoreText if it can't find anything else and + // only contains replacement characters. + last_resort: { + const name_str = font.copyPostScriptName(); + defer name_str.release(); + + // If the name doesn't fit in our buffer, then it can't + // be the last resort font so we break out. + var name_buf: [64]u8 = undefined; + const name: []const u8 = name_str.cstring(&name_buf, .utf8) orelse + break :last_resort; + + // If the name is "LastResort" then we don't want to use it. + if (std.mem.eql(u8, "LastResort", name)) return null; + } + // Get the descriptor return font.copyDescriptor(); } + fn copyMatchingDescriptors( alloc: Allocator, list: *macos.foundation.Array,