From 12c9482d48790c3a2b3b01dbc349a6a06339ac14 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 1 Oct 2022 22:21:30 -0700 Subject: [PATCH] Mac Font Discovery with CoreText (#17) This implements font discovery so the `--font-family` flag works for macOS. Fonts are looked up using the Core Text API so any installed font on the Mac system can be used. We still use FreeType for rendering, and CoreText doesn't _quite_ give us all the information we need to build the exact face in FreeType. So a TODO after this is to now implement glyph _rendering_ using Core Text and Core Graphics. Until then, a couple fonts don't quite work (i.e. Monaco, a big one!) but many do! --- build.zig | 8 ++ pkg/macos/text/font.zig | 14 ++++ src/font/DeferredFace.zig | 112 +++++++++++++++++++++++++++ src/font/discovery.zig | 159 +++++++++++++++++++++++++++++++++++++- src/font/main.zig | 2 + 5 files changed, 293 insertions(+), 2 deletions(-) diff --git a/build.zig b/build.zig index f227ee657..57bae6254 100644 --- a/build.zig +++ b/build.zig @@ -17,6 +17,7 @@ const system_sdk = @import("vendor/mach/libs/glfw/system_sdk.zig"); // Build options, see the build options help for more info. var tracy: bool = false; +var enable_coretext: bool = false; var enable_fontconfig: bool = false; pub fn build(b: *std.build.Builder) !void { @@ -38,6 +39,12 @@ pub fn build(b: *std.build.Builder) !void { "Enable Tracy integration (default true in Debug on Linux)", ) orelse (mode == .Debug and target.isLinux()); + enable_coretext = b.option( + bool, + "coretext", + "Enable coretext for font discovery (default true on macOS)", + ) orelse target.isDarwin(); + enable_fontconfig = b.option( bool, "fontconfig", @@ -65,6 +72,7 @@ pub fn build(b: *std.build.Builder) !void { const exe = b.addExecutable("ghostty", "src/main.zig"); const exe_options = b.addOptions(); exe_options.addOption(bool, "tracy_enabled", tracy); + exe_options.addOption(bool, "coretext", enable_coretext); exe_options.addOption(bool, "fontconfig", enable_fontconfig); // Exe diff --git a/pkg/macos/text/font.zig b/pkg/macos/text/font.zig index 3a7db565a..0ffc37015 100644 --- a/pkg/macos/text/font.zig +++ b/pkg/macos/text/font.zig @@ -31,6 +31,20 @@ pub const Font = opaque { @intCast(c_long, chars.len), ); } + + pub fn copyAttribute(self: *Font, comptime attr: text.FontAttribute) attr.Value() { + return @intToPtr(attr.Value(), @ptrToInt(c.CTFontCopyAttribute( + @ptrCast(c.CTFontRef, self), + @ptrCast(c.CFStringRef, attr.key()), + ))); + } + + pub fn copyDisplayName(self: *Font) *foundation.String { + return @intToPtr( + *foundation.String, + @ptrToInt(c.CTFontCopyDisplayName(@ptrCast(c.CTFontRef, self))), + ); + } }; test { diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index b6bef7384..f2ff28203 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -9,6 +9,7 @@ const DeferredFace = @This(); const std = @import("std"); const assert = std.debug.assert; const fontconfig = @import("fontconfig"); +const macos = @import("macos"); const options = @import("main.zig").options; const Library = @import("main.zig").Library; const Face = @import("main.zig").Face; @@ -20,6 +21,9 @@ face: ?Face = null, /// Fontconfig fc: if (options.fontconfig) ?Fontconfig else void = if (options.fontconfig) null else {}, +/// CoreText +ct: if (options.coretext) ?CoreText else void = if (options.coretext) null else {}, + /// Fontconfig specific data. This is only present if building with fontconfig. pub const Fontconfig = struct { /// The pattern for this font. This must be the "render prepared" pattern. @@ -38,6 +42,17 @@ pub const Fontconfig = struct { } }; +/// CoreText specific data. This is only present when building with CoreText. +pub const CoreText = struct { + /// The initialized font + font: *macos.text.Font, + + pub fn deinit(self: *CoreText) void { + self.font.release(); + self.* = undefined; + } +}; + /// Initialize a deferred face that is already pre-loaded. The deferred face /// takes ownership over the loaded face, deinit will deinit the loaded face. pub fn initLoaded(face: Face) DeferredFace { @@ -47,6 +62,7 @@ pub fn initLoaded(face: Face) DeferredFace { pub fn deinit(self: *DeferredFace) void { if (self.face) |*face| face.deinit(); if (options.fontconfig) if (self.fc) |*fc| fc.deinit(); + if (options.coretext) if (self.ct) |*ct| ct.deinit(); self.* = undefined; } @@ -63,6 +79,13 @@ pub fn name(self: DeferredFace) ![:0]const u8 { return (try fc.pattern.get(.fullname, 0)).string; } + if (options.coretext) { + if (self.ct) |ct| { + const display_name = ct.font.copyDisplayName(); + return display_name.cstringPtr(.utf8) orelse ""; + } + } + return "TODO: built-in font names"; } @@ -80,6 +103,11 @@ pub fn load( return; } + if (options.coretext) { + try self.loadCoreText(lib, size); + return; + } + // Unreachable because we must be already loaded or have the // proper configuration for one of the other deferred mechanisms. unreachable; @@ -100,6 +128,45 @@ fn loadFontconfig( self.face = try Face.initFile(lib, filename, face_index, size); } +fn loadCoreText( + self: *DeferredFace, + lib: Library, + size: Face.DesiredSize, +) !void { + assert(self.face == null); + const ct = self.ct.?; + + // Get the URL for the font so we can get the filepath + const url = ct.font.copyAttribute(.url); + defer url.release(); + + // Get the path from the URL + const path = url.copyPath() orelse return error.FontHasNoFile; + defer path.release(); + + // URL decode the path + const blank = try macos.foundation.String.createWithBytes("", .utf8, false); + defer blank.release(); + const decoded = try macos.foundation.URL.createStringByReplacingPercentEscapes( + path, + blank, + ); + defer decoded.release(); + + // Decode into a c string. 1024 bytes should be enough for anybody. + var buf: [1024]u8 = undefined; + const path_slice = decoded.cstring(buf[0..1023], .utf8) orelse + return error.FontPathCantDecode; + + // Freetype requires null-terminated. We always leave space at + // the end for a zero so we set that up here. + buf[path_slice.len] = 0; + + // TODO: face index 0 is not correct long term and we should switch + // to using CoreText for rendering, too. + self.face = try Face.initFile(lib, buf[0..path_slice.len :0], 0, size); +} + /// Returns true if this face can satisfy the given codepoint and /// presentation. If presentation is null, then it just checks if the /// codepoint is present at all. @@ -136,6 +203,20 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool { } } + // If we are using coretext, we check the loaded CT font. + if (options.coretext) { + if (self.ct) |ct| { + // Turn UTF-32 into UTF-16 for CT API + var unichars: [2]u16 = undefined; + const pair = macos.foundation.stringGetSurrogatePairForLongCharacter(cp, &unichars); + const len: usize = if (pair) 2 else 1; + + // Get our glyphs + var glyphs = [2]macos.graphics.Glyph{ 0, 0 }; + return ct.font.getGlyphsForCharacters(unichars[0..len], glyphs[0..len]); + } + } + // This is unreachable because discovery mechanisms terminate, and // if we're not using a discovery mechanism, the face MUST be loaded. unreachable; @@ -186,3 +267,34 @@ test "fontconfig" { try testing.expect(def.hasCodepoint(' ', null)); try testing.expect(def.face.?.glyphIndex(' ') != null); } + +test "coretext" { + if (!options.coretext) return error.SkipZigTest; + + const discovery = @import("main.zig").discovery; + const testing = std.testing; + + // Load freetype + var lib = try Library.init(); + defer lib.deinit(); + + // Get a deferred face from fontconfig + var def = def: { + var fc = discovery.CoreText.init(); + var it = try fc.discover(.{ .family = "Monaco", .size = 12 }); + defer it.deinit(); + break :def (try it.next()).?; + }; + defer def.deinit(); + try testing.expect(!def.loaded()); + try testing.expect(def.hasCodepoint(' ', null)); + + // Verify we can get the name + const n = try def.name(); + try testing.expect(n.len > 0); + + // Load it and verify it works + try def.load(lib, .{ .points = 12 }); + try testing.expect(def.hasCodepoint(' ', null)); + try testing.expect(def.face.?.glyphIndex(' ') != null); +} diff --git a/src/font/discovery.zig b/src/font/discovery.zig index d3b325782..0022ab52d 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -1,13 +1,20 @@ const std = @import("std"); +const builtin = @import("builtin"); const assert = std.debug.assert; const fontconfig = @import("fontconfig"); +const macos = @import("macos"); const options = @import("main.zig").options; const DeferredFace = @import("main.zig").DeferredFace; const log = std.log.named(.discovery); /// Discover implementation for the compile options. -pub const Discover = if (options.fontconfig) Fontconfig else void; +pub const Discover = if (options.fontconfig) + Fontconfig +else if (options.coretext) + CoreText +else + void; /// 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 @@ -57,6 +64,70 @@ pub const Descriptor = struct { 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 is always set + const family = try macos.foundation.String.createWithBytes(self.family, .utf8, false); + defer family.release(); + attrs.setValue( + macos.text.FontAttribute.family_name.key(), + family, + ); + + // Set our size attribute if set + if (self.size > 0) { + const size32 = @intCast(i32, 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, + }; + const traits_cval = traits.cval(); + 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, + @ptrCast(*const i32, &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( + *macos.foundation.Dictionary, + attrs, + )); + } }; pub const Fontconfig = struct { @@ -134,7 +205,74 @@ pub const Fontconfig = struct { }; }; -test { +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, 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 + const desc_arr = try macos.foundation.Array.create( + macos.text.FontDescriptor, + &[_]*const macos.text.FontDescriptor{ct_desc}, + ); + defer desc_arr.release(); + + // Build our collection + const set = try macos.text.FontCollection.createWithFontDescriptors(desc_arr); + defer set.release(); + const list = set.createMatchingFontDescriptors(); + errdefer list.release(); + + return DiscoverIterator{ + .list = list, + .i = 0, + }; + } + + pub const DiscoverIterator = struct { + list: *macos.foundation.Array, + i: usize, + + pub fn deinit(self: *DiscoverIterator) void { + self.list.release(); + self.* = undefined; + } + + pub fn next(self: *DiscoverIterator) !?DeferredFace { + if (self.i >= self.list.getCount()) return null; + + // Create our font. We need a size to initialize it so we use size + // 12 but we will alter the size later. + const desc = self.list.getValueAtIndex(macos.text.FontDescriptor, self.i); + const font = try macos.text.Font.createWithFontDescriptor(desc, 12); + errdefer font.release(); + + // Increment after we return + defer self.i += 1; + + return DeferredFace{ + .face = null, + .ct = .{ .font = font }, + }; + } + }; +}; + +test "fontconfig" { if (!options.fontconfig) return error.SkipZigTest; const testing = std.testing; @@ -146,3 +284,20 @@ test { try testing.expect(!face.loaded()); } } + +test "core text" { + if (!options.coretext) return error.SkipZigTest; + + const testing = std.testing; + + var ct = CoreText.init(); + defer ct.deinit(); + var it = try ct.discover(.{ .family = "Monaco", .size = 12 }); + defer it.deinit(); + var count: usize = 0; + while (try it.next()) |face| { + count += 1; + try testing.expect(!face.loaded()); + } + try testing.expect(count > 0); +} diff --git a/src/font/main.zig b/src/font/main.zig index eaa947dfa..37c5e4f64 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -14,8 +14,10 @@ pub const Discover = discovery.Discover; /// Build options pub const options: struct { + coretext: bool = false, fontconfig: bool = false, } = .{ + .coretext = build_options.coretext, .fontconfig = build_options.fontconfig, };