mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-28 18:11:39 +00:00
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!
This commit is contained in:
committed by
GitHub
parent
791739de9c
commit
12c9482d48
@@ -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 "<unsupported internal encoding>";
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user