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, };