aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/font/discovery.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/font/discovery.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/font/discovery.zig')
-rw-r--r--vendor/libghostty-vt/src/font/discovery.zig1049
1 files changed, 1049 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/font/discovery.zig b/vendor/libghostty-vt/src/font/discovery.zig
new file mode 100644
index 00000000..c419d36a
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/discovery.zig
@@ -0,0 +1,1049 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const assert = @import("../quirks.zig").inlineAssert;
+const fontconfig = @import("fontconfig");
+const macos = @import("macos");
+const opentype = @import("opentype.zig");
+const options = @import("main.zig").options;
+const Collection = @import("main.zig").Collection;
+const DeferredFace = @import("main.zig").DeferredFace;
+const Variation = @import("main.zig").face.Variation;
+
+const log = std.log.scoped(.discovery);
+
+/// Discover implementation for the compile options.
+pub const Discover = switch (options.backend) {
+ .freetype => void, // no discovery
+ .fontconfig_freetype => Fontconfig,
+ .web_canvas => void, // no discovery
+ .coretext,
+ .coretext_freetype,
+ .coretext_harfbuzz,
+ .coretext_noshape,
+ => CoreText,
+};
+
+/// Descriptor is used to search for fonts. The only required field
+/// is "family". The rest are ignored unless they're set to a non-zero
+/// value.
+pub const Descriptor = struct {
+ /// Font family to search for. This can be a fully qualified font
+ /// name such as "Fira Code", "monospace", "serif", etc. Memory is
+ /// owned by the caller and should be freed when this descriptor
+ /// is no longer in use. The discovery structs will never store the
+ /// descriptor.
+ ///
+ /// On systems that use fontconfig (Linux), this can be a full
+ /// fontconfig pattern, such as "Fira Code-14:bold".
+ family: ?[:0]const u8 = null,
+
+ /// Specific font style to search for. This will filter the style
+ /// string the font advertises. The "bold/italic" booleans later in this
+ /// struct filter by the style trait the font has, not the string, so
+ /// these can be used in conjunction or not.
+ style: ?[:0]const u8 = null,
+
+ /// A codepoint that this font must be able to render.
+ codepoint: u32 = 0,
+
+ /// Font size in points that the font should support. For conversion
+ /// to pixels, we will use 72 DPI for Mac and 96 DPI for everything else.
+ /// (If pixel conversion is necessary, i.e. emoji fonts)
+ size: f32 = 0,
+
+ /// True if we want to search specifically for a font that supports
+ /// specific styles.
+ bold: bool = false,
+ italic: bool = false,
+ monospace: bool = false,
+
+ /// Variation axes to apply to the font. This also impacts searching
+ /// for fonts since fonts with the ability to set these variations
+ /// will be preferred, but not guaranteed.
+ variations: []const Variation = &.{},
+
+ /// Hash the descriptor with the given hasher.
+ pub fn hash(self: Descriptor, hasher: anytype) void {
+ const autoHash = std.hash.autoHash;
+ const autoHashStrat = std.hash.autoHashStrat;
+ autoHashStrat(hasher, self.family, .Deep);
+ autoHashStrat(hasher, self.style, .Deep);
+ autoHash(hasher, self.codepoint);
+ autoHash(hasher, @as(u32, @bitCast(self.size)));
+ autoHash(hasher, self.bold);
+ autoHash(hasher, self.italic);
+ autoHash(hasher, self.monospace);
+ autoHash(hasher, self.variations.len);
+ for (self.variations) |variation| {
+ autoHash(hasher, variation.id);
+
+ // This is not correct, but we don't currently depend on the
+ // hash value being different based on decimal values of variations.
+ autoHash(hasher, @as(i64, @intFromFloat(variation.value)));
+ }
+ }
+
+ /// Returns a hash code that can be used to uniquely identify this
+ /// action.
+ pub fn hashcode(self: Descriptor) u64 {
+ var hasher = std.hash.Wyhash.init(0);
+ self.hash(&hasher);
+ return hasher.final();
+ }
+
+ /// Deep copy of the struct. The given allocator is expected to
+ /// be an arena allocator of some sort since the descriptor
+ /// itself doesn't support fine-grained deallocation of fields.
+ pub fn clone(self: *const Descriptor, alloc: Allocator) !Descriptor {
+ // We can't do any errdefer cleanup in here. As documented we
+ // expect the allocator to be an arena so any errors should be
+ // cleaned up somewhere else.
+
+ var copy = self.*;
+ copy.family = if (self.family) |src| try alloc.dupeZ(u8, src) else null;
+ copy.style = if (self.style) |src| try alloc.dupeZ(u8, src) else null;
+ copy.variations = try alloc.dupe(Variation, self.variations);
+ return copy;
+ }
+
+ /// Convert to Fontconfig pattern to use for lookup. The pattern does
+ /// not have defaults filled/substituted (Fontconfig thing) so callers
+ /// must still do this.
+ pub fn toFcPattern(self: Descriptor) *fontconfig.Pattern {
+ const pat = fontconfig.Pattern.create();
+ if (self.family) |family| {
+ assert(pat.add(.family, .{ .string = family }, false));
+ }
+ if (self.style) |style| {
+ assert(pat.add(.style, .{ .string = style }, false));
+ }
+ if (self.codepoint > 0) {
+ const cs = fontconfig.CharSet.create();
+ defer cs.destroy();
+ assert(cs.addChar(self.codepoint));
+ assert(pat.add(.charset, .{ .char_set = cs }, false));
+ }
+ if (self.size > 0) assert(pat.add(
+ .size,
+ .{ .integer = @intFromFloat(@round(self.size)) },
+ false,
+ ));
+ if (self.bold) assert(pat.add(
+ .weight,
+ .{ .integer = @intFromEnum(fontconfig.Weight.bold) },
+ false,
+ ));
+ if (self.italic) assert(pat.add(
+ .slant,
+ .{ .integer = @intFromEnum(fontconfig.Slant.italic) },
+ false,
+ ));
+
+ // For fontconfig, we always add monospace in the pattern. Since
+ // fontconfig sorts by closeness to the pattern, this doesn't fully
+ // exclude non-monospace but helps prefer it.
+ assert(pat.add(
+ .spacing,
+ .{ .integer = @intFromEnum(fontconfig.Spacing.mono) },
+ false,
+ ));
+
+ return pat;
+ }
+
+ /// Convert to Core Text font descriptor to use for lookup or
+ /// conversion to a specific font.
+ pub fn toCoreTextDescriptor(self: Descriptor) !*macos.text.FontDescriptor {
+ const attrs = try macos.foundation.MutableDictionary.create(0);
+ defer attrs.release();
+
+ // Family
+ if (self.family) |family_bytes| {
+ const family = try macos.foundation.String.createWithBytes(family_bytes, .utf8, false);
+ defer family.release();
+ attrs.setValue(
+ macos.text.FontAttribute.family_name.key(),
+ family,
+ );
+ }
+
+ // Style
+ if (self.style) |style_bytes| {
+ const style = try macos.foundation.String.createWithBytes(style_bytes, .utf8, false);
+ defer style.release();
+ attrs.setValue(
+ macos.text.FontAttribute.style_name.key(),
+ style,
+ );
+ }
+
+ // Codepoint support
+ if (self.codepoint > 0) {
+ const cs = try macos.foundation.CharacterSet.createWithCharactersInRange(.{
+ .location = self.codepoint,
+ .length = 1,
+ });
+ defer cs.release();
+ attrs.setValue(
+ macos.text.FontAttribute.character_set.key(),
+ cs,
+ );
+ }
+
+ // Set our size attribute if set
+ if (self.size > 0) {
+ const size32: i32 = @intFromFloat(@round(self.size));
+ const size = try macos.foundation.Number.create(
+ .sint32,
+ &size32,
+ );
+ defer size.release();
+ attrs.setValue(
+ macos.text.FontAttribute.size.key(),
+ size,
+ );
+ }
+
+ // Build our traits. If we set any, then we store it in the attributes
+ // otherwise we do nothing. We determine this by setting up the packed
+ // struct, converting to an int, and checking if it is non-zero.
+ const traits: macos.text.FontSymbolicTraits = .{
+ .bold = self.bold,
+ .italic = self.italic,
+ .monospace = self.monospace,
+ };
+ const traits_cval: u32 = @bitCast(traits);
+ if (traits_cval > 0) {
+ // Setting traits is a pain. We have to create a nested dictionary
+ // of the symbolic traits value, and set that in our attributes.
+ const traits_num = try macos.foundation.Number.create(
+ .sint32,
+ @as(*const i32, @ptrCast(&traits_cval)),
+ );
+ defer traits_num.release();
+
+ const traits_dict = try macos.foundation.MutableDictionary.create(0);
+ defer traits_dict.release();
+ traits_dict.setValue(
+ macos.text.FontTraitKey.symbolic.key(),
+ traits_num,
+ );
+
+ attrs.setValue(
+ macos.text.FontAttribute.traits.key(),
+ traits_dict,
+ );
+ }
+
+ return try macos.text.FontDescriptor.createWithAttributes(@ptrCast(attrs));
+ }
+};
+
+pub const Fontconfig = struct {
+ fc_config: *fontconfig.Config,
+
+ pub fn init() Fontconfig {
+ // safe to call multiple times and concurrently
+ _ = fontconfig.init();
+ return .{ .fc_config = fontconfig.initLoadConfigAndFonts() };
+ }
+
+ pub fn deinit(self: *Fontconfig) void {
+ self.fc_config.destroy();
+ }
+
+ /// Discover fonts from a descriptor. This returns an iterator that can
+ /// be used to build up the deferred fonts.
+ pub fn discover(
+ self: *const Fontconfig,
+ alloc: Allocator,
+ desc: Descriptor,
+ ) !DiscoverIterator {
+ _ = alloc;
+
+ // Build our pattern that we'll search for
+ const pat = desc.toFcPattern();
+ errdefer pat.destroy();
+ assert(self.fc_config.substituteWithPat(pat, .pattern));
+ pat.defaultSubstitute();
+
+ // Search
+ const res = self.fc_config.fontSort(pat, false, null);
+ if (res.result != .match) return error.FontConfigFailed;
+ errdefer res.fs.destroy();
+
+ return .{
+ .config = self.fc_config,
+ .pattern = pat,
+ .set = res.fs,
+ .fonts = res.fs.fonts(),
+ .variations = desc.variations,
+ .i = 0,
+ };
+ }
+
+ pub fn discoverFallback(
+ self: *const Fontconfig,
+ alloc: Allocator,
+ collection: *Collection,
+ desc: Descriptor,
+ ) !DiscoverIterator {
+ _ = collection;
+ return try self.discover(alloc, desc);
+ }
+
+ pub const DiscoverIterator = struct {
+ config: *fontconfig.Config,
+ pattern: *fontconfig.Pattern,
+ set: *fontconfig.FontSet,
+ fonts: []*fontconfig.Pattern,
+ variations: []const Variation,
+ i: usize,
+
+ pub fn deinit(self: *DiscoverIterator) void {
+ self.set.destroy();
+ self.pattern.destroy();
+ self.* = undefined;
+ }
+
+ pub fn next(self: *DiscoverIterator) fontconfig.Error!?DeferredFace {
+ if (self.i >= self.fonts.len) return null;
+
+ // Get the copied pattern from our fontset that has the
+ // attributes configured for rendering.
+ const font_pattern = try self.config.fontRenderPrepare(
+ self.pattern,
+ self.fonts[self.i],
+ );
+ errdefer font_pattern.destroy();
+
+ // Increment after we return
+ defer self.i += 1;
+
+ return DeferredFace{
+ .fc = .{
+ .pattern = font_pattern,
+ .charset = (try font_pattern.get(.charset, 0)).char_set,
+ .langset = (try font_pattern.get(.lang, 0)).lang_set,
+ .variations = self.variations,
+ },
+ };
+ }
+ };
+};
+
+pub const CoreText = struct {
+ pub fn init() CoreText {
+ // Required for the "interface" but does nothing for CoreText.
+ return .{};
+ }
+
+ pub fn deinit(self: *CoreText) void {
+ _ = self;
+ }
+
+ /// Discover fonts from a descriptor. This returns an iterator that can
+ /// be used to build up the deferred fonts.
+ pub fn discover(self: *const CoreText, alloc: Allocator, desc: Descriptor) !DiscoverIterator {
+ _ = self;
+
+ // Build our pattern that we'll search for
+ const ct_desc = try desc.toCoreTextDescriptor();
+ defer ct_desc.release();
+
+ // Our descriptors have to be in an array
+ var ct_desc_arr = [_]*const macos.text.FontDescriptor{ct_desc};
+ const desc_arr = try macos.foundation.Array.create(macos.text.FontDescriptor, &ct_desc_arr);
+ defer desc_arr.release();
+
+ // Build our collection
+ const set = try macos.text.FontCollection.createWithFontDescriptors(desc_arr);
+ defer set.release();
+ const list = set.createMatchingFontDescriptors();
+ defer list.release();
+
+ // Sort our descriptors
+ const zig_list = try copyMatchingDescriptors(alloc, list);
+ errdefer alloc.free(zig_list);
+ sortMatchingDescriptors(&desc, zig_list);
+
+ return DiscoverIterator{
+ .alloc = alloc,
+ .list = zig_list,
+ .variations = desc.variations,
+ .i = 0,
+ };
+ }
+
+ pub fn discoverFallback(
+ self: *const CoreText,
+ alloc: Allocator,
+ collection: *Collection,
+ desc: Descriptor,
+ ) !DiscoverIterator {
+ // If we have a codepoint within the CJK unified ideographs block
+ // then we fallback to macOS to find a font that supports it because
+ // there isn't a better way manually with CoreText that I can find that
+ // properly takes into account system locale.
+ //
+ // References:
+ // - http://unicode.org/charts/PDF/U4E00.pdf
+ // - https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/platform/fonts/LocaleInFonts.md#unified-han-ideographs
+ if (desc.codepoint >= 0x4E00 and
+ desc.codepoint <= 0x9FFF)
+ han: {
+ const han = try self.discoverCodepoint(
+ collection,
+ desc,
+ ) orelse break :han;
+
+ // This is silly but our discover iterator needs a slice so
+ // we allocate here. This isn't a performance bottleneck but
+ // this is something we can optimize very easily...
+ const list = try alloc.alloc(*macos.text.FontDescriptor, 1);
+ errdefer alloc.free(list);
+ list[0] = han;
+
+ return DiscoverIterator{
+ .alloc = alloc,
+ .list = list,
+ .variations = desc.variations,
+ .i = 0,
+ };
+ }
+
+ 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,
+ .variations = desc.variations,
+ .i = 0,
+ };
+ }
+
+ return it;
+ }
+
+ /// Discover a font for a specific codepoint using the CoreText
+ /// CTFontCreateForString API.
+ fn discoverCodepoint(
+ self: *const CoreText,
+ collection: *Collection,
+ desc: Descriptor,
+ ) !?*macos.text.FontDescriptor {
+ _ = self;
+
+ if (comptime options.backend.hasFreetype()) {
+ // If we have freetype, we can't use CoreText to find a font
+ // that supports a specific codepoint because we need to
+ // have a CoreText font to be able to do so.
+ return null;
+ }
+
+ assert(desc.codepoint > 0);
+
+ // Get our original font. This is dependent on the requested style
+ // from the descriptor.
+ const original = original: {
+ // In all the styles below, we try to match it but if we don't
+ // we always fall back to some other option. The order matters
+ // here.
+
+ if (desc.bold and desc.italic) {
+ const entries = collection.faces.get(.bold_italic);
+ if (entries.count() > 0) {
+ break :original try collection.getFace(.{ .style = .bold_italic });
+ }
+ }
+
+ if (desc.bold) {
+ const entries = collection.faces.get(.bold);
+ if (entries.count() > 0) {
+ break :original try collection.getFace(.{ .style = .bold });
+ }
+ }
+
+ if (desc.italic) {
+ const entries = collection.faces.get(.italic);
+ if (entries.count() > 0) {
+ break :original try collection.getFace(.{ .style = .italic });
+ }
+ }
+
+ break :original try collection.getFace(.{ .style = .regular });
+ };
+
+ // We need it in utf8 format
+ var buf: [4]u8 = undefined;
+ const len = try std.unicode.utf8Encode(
+ @intCast(desc.codepoint),
+ &buf,
+ );
+
+ // We need a CFString
+ const str = try macos.foundation.String.createWithBytes(
+ buf[0..len],
+ .utf8,
+ false,
+ );
+ 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, 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,
+ ) ![]*macos.text.FontDescriptor {
+ var result = try alloc.alloc(*macos.text.FontDescriptor, list.getCount());
+ errdefer alloc.free(result);
+ for (0..result.len) |i| {
+ result[i] = list.getValueAtIndex(macos.text.FontDescriptor, i);
+
+ // We need to retain because once the list is freed it will
+ // release all its members.
+ result[i].retain();
+ }
+ return result;
+ }
+
+ fn sortMatchingDescriptors(
+ desc: *const Descriptor,
+ list: []*macos.text.FontDescriptor,
+ ) void {
+ std.mem.sortUnstable(*macos.text.FontDescriptor, list, desc, struct {
+ fn lessThan(
+ desc_inner: *const Descriptor,
+ lhs: *macos.text.FontDescriptor,
+ rhs: *macos.text.FontDescriptor,
+ ) bool {
+ const lhs_score: Score = .score(desc_inner, lhs);
+ const rhs_score: Score = .score(desc_inner, rhs);
+ // Higher score is "less" (earlier)
+ return lhs_score.int() > rhs_score.int();
+ }
+ }.lessThan);
+ }
+
+ /// We represent our sorting score as a packed struct so that we
+ /// can compare scores numerically but build scores symbolically.
+ ///
+ /// Note that packed structs store their fields from least to most
+ /// significant, so the fields here are defined in increasing order
+ /// of precedence.
+ const Score = packed struct {
+ const Backing = @typeInfo(@This()).@"struct".backing_integer.?;
+
+ /// Number of glyphs in the font, if two fonts have identical
+ /// scores otherwise then we prefer the one with more glyphs.
+ ///
+ /// (Number of glyphs clamped at u16 intmax)
+ glyph_count: u16 = 0,
+ /// A fuzzy match on the style string, less important than
+ /// an exact match, and less important than trait matches.
+ fuzzy_style: u8 = 0,
+ /// Whether the bold-ness of the font matches the descriptor.
+ /// This is less important than italic because a font that's italic
+ /// when it shouldn't be or not italic when it should be is a bigger
+ /// problem (subjectively) than being the wrong weight.
+ bold: bool = false,
+ /// Whether the italic-ness of the font matches the descriptor.
+ /// This is less important than an exact match on the style string
+ /// because we want users to be allowed to override trait matching
+ /// for the bold/italic/bold italic styles if they want.
+ italic: bool = false,
+ /// An exact (case-insensitive) match on the style string.
+ exact_style: bool = false,
+ /// Whether the font is monospace, this is more important than any of
+ /// the other fields unless we're looking for a specific codepoint,
+ /// in which case that is the most important thing.
+ monospace: bool = false,
+ /// If we're looking for a codepoint, whether this font has it.
+ codepoint: bool = false,
+
+ pub fn int(self: Score) Backing {
+ return @bitCast(self);
+ }
+
+ fn score(desc: *const Descriptor, ct_desc: *const macos.text.FontDescriptor) Score {
+ var self: Score = .{};
+
+ // We always load the font if we can since some things can only be
+ // inspected on the font itself. Fonts that can't be loaded score
+ // 0 automatically because we don't want a font we can't load.
+ const font: *macos.text.Font = macos.text.Font.createWithFontDescriptor(
+ ct_desc,
+ 12,
+ ) catch return self;
+ defer font.release();
+
+ // We prefer fonts with more glyphs, all else being equal.
+ {
+ const Type = @TypeOf(self.glyph_count);
+ self.glyph_count = std.math.cast(
+ Type,
+ font.getGlyphCount(),
+ ) orelse std.math.maxInt(Type);
+ }
+
+ // If we're searching for a codepoint, then we
+ // prioritize fonts that have that codepoint.
+ if (desc.codepoint > 0) {
+ // Turn UTF-32 into UTF-16 for CT API
+ var unichars: [2]u16 = undefined;
+ const pair = macos.foundation.stringGetSurrogatePairForLongCharacter(
+ desc.codepoint,
+ &unichars,
+ );
+ const len: usize = if (pair) 2 else 1;
+
+ // Get our glyphs
+ var glyphs = [2]macos.graphics.Glyph{ 0, 0 };
+ self.codepoint = font.getGlyphsForCharacters(
+ unichars[0..len],
+ glyphs[0..len],
+ );
+ }
+
+ // Get our symbolic traits for the descriptor so we can
+ // compare boolean attributes like bold, monospace, etc.
+ const symbolic_traits: macos.text.FontSymbolicTraits = traits: {
+ const traits = ct_desc.copyAttribute(.traits) orelse break :traits .{};
+ defer traits.release();
+
+ const key = macos.text.FontTraitKey.symbolic.key();
+ const symbolic = traits.getValue(macos.foundation.Number, key) orelse
+ break :traits .{};
+
+ break :traits macos.text.FontSymbolicTraits.init(symbolic);
+ };
+
+ self.monospace = symbolic_traits.monospace;
+
+ // We try to derived data from the font itself, which is generally
+ // more reliable than only using the symbolic traits for this.
+ const is_bold: bool, const is_italic: bool = derived: {
+ // We start with initial guesses based on the symbolic traits,
+ // but refine these with more information if we can get it.
+ var is_italic = symbolic_traits.italic;
+ var is_bold = symbolic_traits.bold;
+
+ // Read the 'head' table out of the font data if it's available.
+ if (head: {
+ const tag = macos.text.FontTableTag.init("head");
+ const data = font.copyTable(tag) orelse break :head null;
+ defer data.release();
+ const ptr = data.getPointer();
+ const len = data.getLength();
+ break :head opentype.Head.init(ptr[0..len]) catch |err| {
+ log.warn("error parsing head table: {}", .{err});
+ break :head null;
+ };
+ }) |head_| {
+ const head: opentype.Head = head_;
+ is_bold = is_bold or (head.macStyle & 1 == 1);
+ is_italic = is_italic or (head.macStyle & 2 == 2);
+ }
+
+ // Read the 'OS/2' table out of the font data if it's available.
+ if (os2: {
+ const tag = macos.text.FontTableTag.init("OS/2");
+ const data = font.copyTable(tag) orelse break :os2 null;
+ defer data.release();
+ const ptr = data.getPointer();
+ const len = data.getLength();
+ break :os2 opentype.OS2.init(ptr[0..len]) catch |err| {
+ log.warn("error parsing OS/2 table: {}", .{err});
+ break :os2 null;
+ };
+ }) |os2| {
+ is_bold = is_bold or os2.fsSelection.bold;
+ is_italic = is_italic or os2.fsSelection.italic;
+ }
+
+ // Check if we have variation axes in our descriptor, if we
+ // do then we can derive weight italic-ness or both from them.
+ if (font.copyAttribute(.variation_axes)) |axes| variations: {
+ defer axes.release();
+
+ // Copy the variation values for this instance of the font.
+ // if there are none then we just break out immediately.
+ const values: *macos.foundation.Dictionary =
+ font.copyAttribute(.variation) orelse break :variations;
+ defer values.release();
+
+ var buf: [1024]u8 = undefined;
+
+ // If we see the 'ital' value then we ignore 'slnt'.
+ var ital_seen = false;
+
+ const len = axes.getCount();
+ for (0..len) |i| {
+ const dict = axes.getValueAtIndex(macos.foundation.Dictionary, i);
+ const Key = macos.text.FontVariationAxisKey;
+ const cf_id = dict.getValue(Key.identifier.Value(), Key.identifier.key()).?;
+ const cf_name = dict.getValue(Key.name.Value(), Key.name.key()).?;
+ const cf_def = dict.getValue(Key.default_value.Value(), Key.default_value.key()).?;
+
+ const name_str = cf_name.cstring(&buf, .utf8) orelse "";
+
+ // Default value
+ var def: f64 = 0;
+ _ = cf_def.getValue(.double, &def);
+ // Value in this font
+ var val: f64 = def;
+ if (values.getValue(
+ macos.foundation.Number,
+ cf_id,
+ )) |cf_val| _ = cf_val.getValue(.double, &val);
+
+ if (std.mem.eql(u8, "wght", name_str)) {
+ // Somewhat subjective threshold, we consider fonts
+ // bold if they have a 'wght' set greater than 600.
+ is_bold = val > 600;
+ continue;
+ }
+ if (std.mem.eql(u8, "ital", name_str)) {
+ is_italic = val > 0.5;
+ ital_seen = true;
+ continue;
+ }
+ if (!ital_seen and std.mem.eql(u8, "slnt", name_str)) {
+ // Arbitrary threshold of anything more than a 5
+ // degree clockwise slant is considered italic.
+ is_italic = val <= -5.0;
+ continue;
+ }
+ }
+ }
+
+ break :derived .{ is_bold, is_italic };
+ };
+
+ self.bold = desc.bold == is_bold;
+ self.italic = desc.italic == is_italic;
+
+ // Get the style string from the font.
+ var style_str_buf: [128]u8 = undefined;
+ const style_str: []const u8 = style_str: {
+ const style = ct_desc.copyAttribute(.style_name) orelse
+ break :style_str "";
+ defer style.release();
+
+ break :style_str style.cstring(&style_str_buf, .utf8) orelse "";
+ };
+
+ // The first string in this slice will be used for the exact match,
+ // and for the fuzzy match, all matching substrings will increase
+ // the rank.
+ const desired_styles: []const [:0]const u8 = desired: {
+ if (desc.style) |s| break :desired &.{s};
+
+ // If we don't have an explicitly desired style name, we base
+ // it on the bold and italic properties, this isn't ideal since
+ // fonts may use style names other than these, but it helps in
+ // some edge cases.
+ if (desc.bold) {
+ if (desc.italic) break :desired &.{ "bold italic", "bold", "italic", "oblique" };
+ break :desired &.{ "bold", "upright" };
+ } else if (desc.italic) {
+ break :desired &.{ "italic", "regular", "oblique" };
+ }
+ break :desired &.{ "regular", "upright" };
+ };
+
+ self.exact_style = std.ascii.eqlIgnoreCase(
+ style_str,
+ desired_styles[0],
+ );
+ // Our "fuzzy match" score is 0 if the desired style isn't present
+ // in the string, otherwise we give higher priority for styles that
+ // have fewer characters not in the desired_styles list.
+ const fuzzy_type = @TypeOf(self.fuzzy_style);
+ self.fuzzy_style = @intCast(style_str.len);
+ for (desired_styles) |s| {
+ if (std.ascii.indexOfIgnoreCase(style_str, s) != null) {
+ self.fuzzy_style -|= @intCast(s.len);
+ }
+ }
+ self.fuzzy_style = std.math.maxInt(fuzzy_type) -| self.fuzzy_style;
+
+ return self;
+ }
+ };
+
+ pub const DiscoverIterator = struct {
+ alloc: Allocator,
+ list: []const *macos.text.FontDescriptor,
+ variations: []const Variation,
+ i: usize,
+
+ pub fn deinit(self: *DiscoverIterator) void {
+ for (self.list) |desc| {
+ desc.release();
+ }
+ self.alloc.free(self.list);
+ self.* = undefined;
+ }
+
+ pub fn next(self: *DiscoverIterator) !?DeferredFace {
+ if (self.i >= self.list.len) return null;
+
+ // Get our descriptor. We need to remove the character set
+ // limitation because we may have used that to filter but we
+ // don't want it anymore because it'll restrict the characters
+ // available.
+ const desc = desc: {
+ // We create a copy, overwriting the character set attribute.
+ const attrs = try macos.foundation.MutableDictionary.create(0);
+ defer attrs.release();
+
+ attrs.setValue(
+ macos.text.FontAttribute.character_set.key(),
+ macos.c.kCFNull,
+ );
+
+ break :desc try macos.text.FontDescriptor.createCopyWithAttributes(
+ self.list[self.i],
+ @ptrCast(attrs),
+ );
+ };
+ defer desc.release();
+
+ // Create our font. We need a size to initialize it so we use size
+ // 12 but we will alter the size later.
+ const font = try macos.text.Font.createWithFontDescriptor(desc, 12);
+ errdefer font.release();
+
+ // Increment after we return
+ defer self.i += 1;
+
+ return DeferredFace{
+ .ct = .{
+ .font = font,
+ .variations = self.variations,
+ },
+ };
+ }
+ };
+};
+
+test "descriptor hash" {
+ const testing = std.testing;
+
+ var d: Descriptor = .{};
+ try testing.expect(d.hashcode() != 0);
+}
+
+test "descriptor hash family names" {
+ const testing = std.testing;
+
+ var d1: Descriptor = .{ .family = "A" };
+ var d2: Descriptor = .{ .family = "B" };
+ try testing.expect(d1.hashcode() != d2.hashcode());
+}
+
+test "fontconfig" {
+ if (options.backend != .fontconfig_freetype) return error.SkipZigTest;
+
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var fc = Fontconfig.init();
+ defer fc.deinit();
+ var it = try fc.discover(alloc, .{ .family = "monospace", .size = 12 });
+ defer it.deinit();
+}
+
+test "fontconfig codepoint" {
+ if (options.backend != .fontconfig_freetype) return error.SkipZigTest;
+
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var fc = Fontconfig.init();
+ defer fc.deinit();
+ var it = try fc.discover(alloc, .{ .codepoint = 'A', .size = 12 });
+ defer it.deinit();
+
+ // The first result should have the codepoint. Later ones may not
+ // because fontconfig returns all fonts sorted.
+ var face = (try it.next()).?;
+ defer face.deinit();
+ try testing.expect(face.hasCodepoint('A', null));
+
+ // Should have other codepoints too
+ try testing.expect(face.hasCodepoint('B', null));
+}
+
+test "coretext" {
+ if (options.backend != .coretext and options.backend != .coretext_freetype)
+ return error.SkipZigTest;
+
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var ct = CoreText.init();
+ defer ct.deinit();
+ var it = try ct.discover(alloc, .{ .family = "Monaco", .size = 12 });
+ defer it.deinit();
+ var count: usize = 0;
+ while (try it.next()) |_| {
+ count += 1;
+ }
+ try testing.expect(count > 0);
+}
+
+test "coretext codepoint" {
+ if (options.backend != .coretext and options.backend != .coretext_freetype)
+ return error.SkipZigTest;
+
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var ct = CoreText.init();
+ defer ct.deinit();
+ var it = try ct.discover(alloc, .{ .codepoint = 'A', .size = 12 });
+ defer it.deinit();
+
+ // The first result should have the codepoint. Later ones may not
+ // because fontconfig returns all fonts sorted.
+ const face = (try it.next()).?;
+ try testing.expect(face.hasCodepoint('A', null));
+
+ // Should have other codepoints too
+ try testing.expect(face.hasCodepoint('B', null));
+}
+
+test "coretext sorting" {
+ if (options.backend != .coretext and options.backend != .coretext_freetype)
+ return error.SkipZigTest;
+
+ // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!//
+ // FIXME: Disabled for now because SF Pro is not available in CI
+ // The solution likely involves directly testing that the
+ // `sortMatchingDescriptors` function sorts a bundled test
+ // font correctly, instead of relying on the system fonts.
+ if (true) return error.SkipZigTest;
+ // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!//
+
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var ct = CoreText.init();
+ defer ct.deinit();
+
+ // We try to get a Regular, Italic, Bold, & Bold Italic version of SF Pro,
+ // which should be installed on all Macs, and has many styles which makes
+ // it a good test, since there will be many results for each discovery.
+
+ // Regular
+ {
+ var it = try ct.discover(alloc, .{
+ .family = "SF Pro",
+ .size = 12,
+ });
+ defer it.deinit();
+ const res = (try it.next()).?;
+ var buf: [1024]u8 = undefined;
+ const name = try res.name(&buf);
+ try testing.expectEqualStrings("SF Pro Regular", name);
+ }
+
+ // Regular Italic
+ //
+ // NOTE: This makes sure that we don't accidentally prefer "Thin Italic",
+ // which we previously did, because it has a shorter name.
+ {
+ var it = try ct.discover(alloc, .{
+ .family = "SF Pro",
+ .size = 12,
+ .italic = true,
+ });
+ defer it.deinit();
+ const res = (try it.next()).?;
+ var buf: [1024]u8 = undefined;
+ const name = try res.name(&buf);
+ try testing.expectEqualStrings("SF Pro Regular Italic", name);
+ }
+
+ // Bold
+ {
+ var it = try ct.discover(alloc, .{
+ .family = "SF Pro",
+ .size = 12,
+ .bold = true,
+ });
+ defer it.deinit();
+ const res = (try it.next()).?;
+ var buf: [1024]u8 = undefined;
+ const name = try res.name(&buf);
+ try testing.expectEqualStrings("SF Pro Bold", name);
+ }
+
+ // Bold Italic
+ {
+ var it = try ct.discover(alloc, .{
+ .family = "SF Pro",
+ .size = 12,
+ .bold = true,
+ .italic = true,
+ });
+ defer it.deinit();
+ const res = (try it.next()).?;
+ var buf: [1024]u8 = undefined;
+ const name = try res.name(&buf);
+ try testing.expectEqualStrings("SF Pro Bold Italic", name);
+ }
+}