Skip to content

Commit

Permalink
font/coretext: use CTFontCreateForString for final codepoint fallback
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mitchellh committed Oct 26, 2024
1 parent 734c8ce commit 1aa932f
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 2 deletions.
4 changes: 4 additions & 0 deletions pkg/macos/text/font.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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)));
}
Expand Down
56 changes: 54 additions & 2 deletions src/font/discovery.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down

0 comments on commit 1aa932f

Please sign in to comment.