ghostty/src/font/discovery.zig
2025-07-02 11:38:26 -06:00

1043 lines
38 KiB
Zig

const std = @import("std");
const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const assert = std.debug.assert;
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 = self.list.getValueAtIndex(macos.text.FontDescriptor, self.i);
const desc = desc: {
const original = self.list[self.i];
// For some reason simply copying the attributes and recreating
// the descriptor removes the charset restriction. This is tested.
const attrs = original.copyAttributes();
defer attrs.release();
break :desc try macos.text.FontDescriptor.createWithAttributes(@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();
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();
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.
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" {
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);
}
}