aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/font
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-05-07 18:29:21 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-05-07 18:29:21 +0300
commitdcfe11a7588fcd42471159453810abc9b7720a57 (patch)
treec32a77266028e73ca2382b96c96f8438a5fcd621 /vendor/libghostty-vt/src/font
parent2dfed860103c4e4505ac7f25def9fe37da1e1e6a (diff)
build: update libghostty-vt vendor
vendor ghostty 063ac3e and remove the musl libc++ link workaround. expose the new selection formatter and hyperlink uri apis for follow-up rendering work. add manual artifact validation for static linux builds.
Diffstat (limited to 'vendor/libghostty-vt/src/font')
-rw-r--r--vendor/libghostty-vt/src/font/DeferredFace.zig71
-rw-r--r--vendor/libghostty-vt/src/font/SharedGridSet.zig2
-rw-r--r--vendor/libghostty-vt/src/font/backend.zig18
-rw-r--r--vendor/libghostty-vt/src/font/discovery.zig314
-rw-r--r--vendor/libghostty-vt/src/font/face.zig1
-rw-r--r--vendor/libghostty-vt/src/font/library.zig1
-rw-r--r--vendor/libghostty-vt/src/font/opentype.zig2
-rw-r--r--vendor/libghostty-vt/src/font/opentype/glyf.zig585
-rw-r--r--vendor/libghostty-vt/src/font/shape.zig1
-rw-r--r--vendor/libghostty-vt/src/font/shaper/coretext.zig4
-rw-r--r--vendor/libghostty-vt/src/font/shaper/harfbuzz.zig4
11 files changed, 985 insertions, 18 deletions
diff --git a/vendor/libghostty-vt/src/font/DeferredFace.zig b/vendor/libghostty-vt/src/font/DeferredFace.zig
index e818cca3..425f3c28 100644
--- a/vendor/libghostty-vt/src/font/DeferredFace.zig
+++ b/vendor/libghostty-vt/src/font/DeferredFace.zig
@@ -26,6 +26,10 @@ fc: if (options.backend == .fontconfig_freetype) ?Fontconfig else void =
ct: if (font.Discover == font.discovery.CoreText) ?CoreText else void =
if (font.Discover == font.discovery.CoreText) null else {},
+/// Windows (FreeType directory scan)
+win: if (options.backend == .freetype_windows) ?Windows else void =
+ if (options.backend == .freetype_windows) null else {},
+
/// Canvas
wc: if (options.backend == .web_canvas) ?WebCanvas else void =
if (options.backend == .web_canvas) null else {},
@@ -51,6 +55,42 @@ pub const Fontconfig = struct {
}
};
+/// Windows specific data. Only present with the freetype_windows backend.
+///
+/// Unlike Fontconfig/CoreText which carry lightweight descriptor handles,
+/// the Windows backend has no external descriptor service — the "deferred"
+/// metadata is the FreeType face itself. We keep a pre-loaded face (loaded
+/// at discovery time) to answer `hasCodepoint` cheaply without re-opening
+/// the file on every query, and remember the path so `load()` can open a
+/// fresh face at the caller's requested size/options.
+pub const Windows = struct {
+ /// Path to the font file. Owned here.
+ path: [:0]const u8,
+
+ /// Face index within the file (for .ttc collections).
+ face_index: i32,
+
+ /// Variations to apply on load.
+ variations: []const font.face.Variation,
+
+ /// Pre-loaded face used for cheap metadata queries (glyphIndex,
+ /// hasColor). The size it was opened at is irrelevant for these
+ /// queries since the CMap is size-independent. Deinit'd with us.
+ peek: Face,
+
+ /// Whether the face presents as emoji (has color glyphs) or text.
+ presentation: Presentation,
+
+ /// Allocator that owns `path`.
+ alloc: Allocator,
+
+ pub fn deinit(self: *Windows) void {
+ self.peek.deinit();
+ self.alloc.free(self.path);
+ self.* = undefined;
+ }
+};
+
/// CoreText specific data. This is only present when building with CoreText.
pub const CoreText = struct {
/// The initialized font
@@ -88,6 +128,7 @@ pub fn deinit(self: *DeferredFace) void {
switch (options.backend) {
.fontconfig_freetype => if (self.fc) |*fc| fc.deinit(),
.freetype => {},
+ .freetype_windows => if (self.win) |*w| w.deinit(),
.web_canvas => if (self.wc) |*wc| wc.deinit(),
.coretext,
.coretext_freetype,
@@ -103,6 +144,8 @@ pub fn familyName(self: DeferredFace, buf: []u8) ![]const u8 {
switch (options.backend) {
.freetype => {},
+ .freetype_windows => if (self.win) |w| return try w.peek.name(buf),
+
.fontconfig_freetype => if (self.fc) |fc|
return (try fc.pattern.get(.family, 0)).string,
@@ -131,6 +174,8 @@ pub fn name(self: DeferredFace, buf: []u8) ![]const u8 {
switch (options.backend) {
.freetype => {},
+ .freetype_windows => if (self.win) |w| return try w.peek.name(buf),
+
.fontconfig_freetype => if (self.fc) |fc|
return (try fc.pattern.get(.fullname, 0)).string,
@@ -164,6 +209,7 @@ pub fn load(
) !Face {
return switch (options.backend) {
.fontconfig_freetype => try self.loadFontconfig(lib, opts),
+ .freetype_windows => try self.loadWindows(lib, opts),
.coretext, .coretext_harfbuzz, .coretext_noshape => try self.loadCoreText(lib, opts),
.coretext_freetype => try self.loadCoreTextFreetype(lib, opts),
.web_canvas => try self.loadWebCanvas(opts),
@@ -191,6 +237,19 @@ fn loadFontconfig(
return face;
}
+fn loadWindows(
+ self: *DeferredFace,
+ lib: Library,
+ opts: font.face.Options,
+) !Face {
+ const w = self.win.?;
+
+ var face = try Face.initFile(lib, w.path, w.face_index, opts);
+ errdefer face.deinit();
+ try face.setVariations(w.variations, opts);
+ return face;
+}
+
fn loadCoreText(
self: *DeferredFace,
lib: Library,
@@ -287,6 +346,14 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool {
}
},
+ .freetype_windows => {
+ // Use the pre-loaded peek face for a cheap CMap lookup.
+ if (self.win) |w| {
+ if (p) |desired| if (w.presentation != desired) return false;
+ return w.peek.glyphIndex(cp) != null;
+ }
+ },
+
.coretext,
.coretext_freetype,
.coretext_harfbuzz,
@@ -411,7 +478,7 @@ test "fontconfig" {
// Get a deferred face from fontconfig
var def = def: {
- var fc = discovery.Fontconfig.init();
+ var fc = discovery.Fontconfig.init(lib);
defer fc.deinit();
var it = try fc.discover(alloc, .{ .family = "monospace", .size = 12 });
defer it.deinit();
@@ -443,7 +510,7 @@ test "coretext" {
// Get a deferred face from fontconfig
var def = def: {
- var fc = discovery.CoreText.init();
+ var fc = discovery.CoreText.init(lib);
var it = try fc.discover(alloc, .{ .family = "Monaco", .size = 12 });
defer it.deinit();
break :def (try it.next()).?;
diff --git a/vendor/libghostty-vt/src/font/SharedGridSet.zig b/vendor/libghostty-vt/src/font/SharedGridSet.zig
index b832139b..9d8148bd 100644
--- a/vendor/libghostty-vt/src/font/SharedGridSet.zig
+++ b/vendor/libghostty-vt/src/font/SharedGridSet.zig
@@ -442,7 +442,7 @@ fn discover(self: *SharedGridSet) !?*Discover {
// If we initialized, use it
if (self.font_discover) |*v| return v;
- self.font_discover = .init();
+ self.font_discover = .init(self.font_lib);
return &self.font_discover.?;
}
diff --git a/vendor/libghostty-vt/src/font/backend.zig b/vendor/libghostty-vt/src/font/backend.zig
index d24edec7..867421de 100644
--- a/vendor/libghostty-vt/src/font/backend.zig
+++ b/vendor/libghostty-vt/src/font/backend.zig
@@ -6,6 +6,12 @@ pub const Backend = enum {
/// FreeType for font rendering with no font discovery enabled.
freetype,
+ /// FreeType for font rendering with a built-in Windows font directory
+ /// scanner (C:\Windows\Fonts + %LOCALAPPDATA%\Microsoft\Windows\Fonts).
+ /// Used when DirectWrite is not available; matches by family_name and
+ /// SFNT name table without any external index.
+ freetype_windows,
+
/// Fontconfig for font discovery and FreeType for font rendering.
fontconfig_freetype,
@@ -42,10 +48,10 @@ pub const Backend = enum {
if (target.os.tag == .windows) {
// Avoid fontconfig on Windows because its libxml2 dependency
- // may not unpack due to symlinks. Use plain freetype for now
- // which means no font discovery. Full solution would likely use
- // DirectWrite which has its own discovery API.
- return .freetype;
+ // may not unpack due to symlinks. Use the FreeType-based
+ // Windows font-directory scanner for discovery. A future
+ // DirectWrite backend can replace this if needed.
+ return .freetype_windows;
}
// macOS also supports "coretext_freetype" but there is no scenario
@@ -60,6 +66,7 @@ pub const Backend = enum {
pub fn hasFreetype(self: Backend) bool {
return switch (self) {
.freetype,
+ .freetype_windows,
.fontconfig_freetype,
.coretext_freetype,
=> true,
@@ -81,6 +88,7 @@ pub const Backend = enum {
=> true,
.freetype,
+ .freetype_windows,
.fontconfig_freetype,
.web_canvas,
=> false,
@@ -92,6 +100,7 @@ pub const Backend = enum {
.fontconfig_freetype => true,
.freetype,
+ .freetype_windows,
.coretext,
.coretext_freetype,
.coretext_harfbuzz,
@@ -104,6 +113,7 @@ pub const Backend = enum {
pub fn hasHarfbuzz(self: Backend) bool {
return switch (self) {
.freetype,
+ .freetype_windows,
.fontconfig_freetype,
.coretext_freetype,
.coretext_harfbuzz,
diff --git a/vendor/libghostty-vt/src/font/discovery.zig b/vendor/libghostty-vt/src/font/discovery.zig
index c419d36a..b945aa01 100644
--- a/vendor/libghostty-vt/src/font/discovery.zig
+++ b/vendor/libghostty-vt/src/font/discovery.zig
@@ -1,4 +1,5 @@
const std = @import("std");
+const builtin = @import("builtin");
const Allocator = std.mem.Allocator;
const assert = @import("../quirks.zig").inlineAssert;
const fontconfig = @import("fontconfig");
@@ -7,6 +8,9 @@ const opentype = @import("opentype.zig");
const options = @import("main.zig").options;
const Collection = @import("main.zig").Collection;
const DeferredFace = @import("main.zig").DeferredFace;
+const Face = @import("main.zig").Face;
+const Library = @import("main.zig").Library;
+const Presentation = @import("main.zig").Presentation;
const Variation = @import("main.zig").face.Variation;
const log = std.log.scoped(.discovery);
@@ -14,6 +18,7 @@ const log = std.log.scoped(.discovery);
/// Discover implementation for the compile options.
pub const Discover = switch (options.backend) {
.freetype => void, // no discovery
+ .freetype_windows => Windows,
.fontconfig_freetype => Fontconfig,
.web_canvas => void, // no discovery
.coretext,
@@ -242,7 +247,8 @@ pub const Descriptor = struct {
pub const Fontconfig = struct {
fc_config: *fontconfig.Config,
- pub fn init() Fontconfig {
+ pub fn init(lib: Library) Fontconfig {
+ _ = lib;
// safe to call multiple times and concurrently
_ = fontconfig.init();
return .{ .fc_config = fontconfig.initLoadConfigAndFonts() };
@@ -333,7 +339,8 @@ pub const Fontconfig = struct {
};
pub const CoreText = struct {
- pub fn init() CoreText {
+ pub fn init(lib: Library) CoreText {
+ _ = lib;
// Required for the "interface" but does nothing for CoreText.
return .{};
}
@@ -879,6 +886,263 @@ pub const CoreText = struct {
};
};
+/// Windows font discovery. Enumerates font files in the system and
+/// per-user font directories and matches them to a descriptor via
+/// FreeType's family_name field (with a fallback to the SFNT name
+/// table when family_name is missing).
+///
+/// No external service is used; each discover() call walks the
+/// directories, opening candidate files with FreeType only as needed.
+/// For typical Windows installations (~300 fonts) a name query is in
+/// the tens of milliseconds. A codepoint fallback query may be
+/// noticeably slower because every candidate has to be opened to
+/// probe its CMap.
+pub const Windows = struct {
+ lib: Library,
+
+ pub fn init(lib: Library) Windows {
+ return .{ .lib = lib };
+ }
+
+ pub fn deinit(self: *Windows) void {
+ _ = self;
+ }
+
+ pub fn discover(
+ self: *const Windows,
+ alloc: Allocator,
+ desc: Descriptor,
+ ) !DiscoverIterator {
+ return .{
+ .alloc = alloc,
+ .lib = self.lib,
+ .desc = desc,
+ .variations = desc.variations,
+ .state = .system,
+ .dir = null,
+ .iter = null,
+ .system_path = null,
+ .user_path = null,
+ };
+ }
+
+ pub fn discoverFallback(
+ self: *const Windows,
+ alloc: Allocator,
+ collection: *Collection,
+ desc: Descriptor,
+ ) !DiscoverIterator {
+ _ = collection;
+ return self.discover(alloc, desc);
+ }
+
+ pub const DiscoverIterator = struct {
+ alloc: Allocator,
+ lib: Library,
+ desc: Descriptor,
+ variations: []const Variation,
+ state: State,
+ dir: ?std.fs.Dir,
+ iter: ?std.fs.Dir.Iterator,
+ system_path: ?[:0]const u8,
+ user_path: ?[:0]const u8,
+
+ const State = enum { system, user, done };
+
+ pub fn deinit(self: *DiscoverIterator) void {
+ if (self.dir) |*d| d.close();
+ if (self.system_path) |p| self.alloc.free(p);
+ if (self.user_path) |p| self.alloc.free(p);
+ self.* = undefined;
+ }
+
+ pub fn next(self: *DiscoverIterator) !?DeferredFace {
+ while (true) {
+ // Ensure we have a directory iterator for the current state.
+ if (self.iter == null) {
+ switch (self.state) {
+ .system => {
+ const path = self.systemFontsPath() orelse {
+ self.state = .user;
+ continue;
+ };
+ self.system_path = path;
+ self.dir = std.fs.openDirAbsoluteZ(
+ path,
+ .{ .iterate = true },
+ ) catch {
+ self.state = .user;
+ continue;
+ };
+ self.iter = self.dir.?.iterate();
+ },
+ .user => {
+ const path = self.userFontsPath() orelse {
+ self.state = .done;
+ continue;
+ };
+ self.user_path = path;
+ self.dir = std.fs.openDirAbsoluteZ(
+ path,
+ .{ .iterate = true },
+ ) catch {
+ self.state = .done;
+ continue;
+ };
+ self.iter = self.dir.?.iterate();
+ },
+ .done => return null,
+ }
+ }
+
+ const entry = (self.iter.?.next() catch null) orelse {
+ // Finished this directory; advance state.
+ if (self.dir) |*d| d.close();
+ self.dir = null;
+ self.iter = null;
+ self.state = switch (self.state) {
+ .system => .user,
+ .user => .done,
+ .done => .done,
+ };
+ continue;
+ };
+
+ if (entry.kind != .file) continue;
+ if (!isFontFile(entry.name)) continue;
+
+ if (try self.tryMatch(entry.name)) |face| return face;
+ }
+ }
+
+ /// Build the system fonts directory from %SYSTEMROOT%. Returns null
+ /// if SYSTEMROOT is unset, which shouldn't happen on a healthy
+ /// Windows install but we just skip the directory rather than
+ /// falling back to a hardcoded drive letter.
+ fn systemFontsPath(self: *DiscoverIterator) ?[:0]const u8 {
+ const systemroot = std.process.getEnvVarOwned(
+ self.alloc,
+ "SYSTEMROOT",
+ ) catch return null;
+ defer self.alloc.free(systemroot);
+ return std.fmt.allocPrintSentinel(
+ self.alloc,
+ "{s}\\Fonts",
+ .{systemroot},
+ 0,
+ ) catch null;
+ }
+
+ fn userFontsPath(self: *DiscoverIterator) ?[:0]const u8 {
+ const local_appdata = std.process.getEnvVarOwned(
+ self.alloc,
+ "LOCALAPPDATA",
+ ) catch return null;
+ defer self.alloc.free(local_appdata);
+ return std.fmt.allocPrintSentinel(
+ self.alloc,
+ "{s}\\Microsoft\\Windows\\Fonts",
+ .{local_appdata},
+ 0,
+ ) catch null;
+ }
+
+ fn tryMatch(
+ self: *DiscoverIterator,
+ name: []const u8,
+ ) !?DeferredFace {
+ const dir_path = switch (self.state) {
+ .system => self.system_path.?,
+ .user => self.user_path.?,
+ .done => return null,
+ };
+
+ var path_buf: [std.fs.max_path_bytes]u8 = undefined;
+ const full_path = std.fmt.bufPrintZ(
+ &path_buf,
+ "{s}\\{s}",
+ .{ dir_path, name },
+ ) catch return null;
+
+ const is_ttc = std.ascii.endsWithIgnoreCase(name, ".ttc");
+ const max_faces: i32 = if (is_ttc) 16 else 1;
+
+ // Probe each face in the file.
+ var face_index: i32 = 0;
+ while (face_index < max_faces) : (face_index += 1) {
+ var face = Face.initFile(
+ self.lib,
+ full_path,
+ face_index,
+ .{ .size = .{ .points = 12 } },
+ ) catch break;
+
+ if (self.matches(&face)) {
+ return try self.makeDeferred(face, full_path, face_index);
+ }
+
+ face.deinit();
+ }
+
+ return null;
+ }
+
+ /// Check whether the given face matches the descriptor.
+ fn matches(self: *const DiscoverIterator, face: *Face) bool {
+ if (self.desc.family) |family| {
+ if (!familyMatches(face, family)) return false;
+ }
+ if (self.desc.codepoint != 0) {
+ if (face.glyphIndex(self.desc.codepoint) == null) return false;
+ }
+ return true;
+ }
+
+ fn makeDeferred(
+ self: *DiscoverIterator,
+ face: Face,
+ full_path: []const u8,
+ face_index: i32,
+ ) !DeferredFace {
+ const path_owned = try self.alloc.dupeZ(u8, full_path);
+ errdefer self.alloc.free(path_owned);
+
+ const presentation: Presentation =
+ if (face.hasColor()) .emoji else .text;
+
+ return DeferredFace{
+ .win = .{
+ .path = path_owned,
+ .face_index = face_index,
+ .variations = self.variations,
+ .peek = face,
+ .presentation = presentation,
+ .alloc = self.alloc,
+ },
+ };
+ }
+ };
+
+ fn isFontFile(name: []const u8) bool {
+ return std.ascii.endsWithIgnoreCase(name, ".ttf") or
+ std.ascii.endsWithIgnoreCase(name, ".ttc") or
+ std.ascii.endsWithIgnoreCase(name, ".otf");
+ }
+
+ /// Compare a face's family against a requested family name. Checks
+ /// FreeType's family_name first, then falls back to the SFNT name
+ /// table entry.
+ fn familyMatches(face: *Face, family: [:0]const u8) bool {
+ const ft_family: ?[*:0]const u8 = face.face.handle.*.family_name;
+ if (ft_family) |f| {
+ if (std.ascii.eqlIgnoreCase(std.mem.span(f), family)) return true;
+ }
+ var buf: [256]u8 = undefined;
+ const sfnt = face.name(&buf) catch "";
+ return sfnt.len > 0 and std.ascii.eqlIgnoreCase(sfnt, family);
+ }
+};
+
test "descriptor hash" {
const testing = std.testing;
@@ -900,7 +1164,10 @@ test "fontconfig" {
const testing = std.testing;
const alloc = testing.allocator;
- var fc = Fontconfig.init();
+ var lib = try Library.init(alloc);
+ defer lib.deinit();
+
+ var fc = Fontconfig.init(lib);
defer fc.deinit();
var it = try fc.discover(alloc, .{ .family = "monospace", .size = 12 });
defer it.deinit();
@@ -912,7 +1179,10 @@ test "fontconfig codepoint" {
const testing = std.testing;
const alloc = testing.allocator;
- var fc = Fontconfig.init();
+ var lib = try Library.init(alloc);
+ defer lib.deinit();
+
+ var fc = Fontconfig.init(lib);
defer fc.deinit();
var it = try fc.discover(alloc, .{ .codepoint = 'A', .size = 12 });
defer it.deinit();
@@ -934,7 +1204,10 @@ test "coretext" {
const testing = std.testing;
const alloc = testing.allocator;
- var ct = CoreText.init();
+ var lib = try Library.init(alloc);
+ defer lib.deinit();
+
+ var ct = CoreText.init(lib);
defer ct.deinit();
var it = try ct.discover(alloc, .{ .family = "Monaco", .size = 12 });
defer it.deinit();
@@ -952,7 +1225,10 @@ test "coretext codepoint" {
const testing = std.testing;
const alloc = testing.allocator;
- var ct = CoreText.init();
+ var lib = try Library.init(alloc);
+ defer lib.deinit();
+
+ var ct = CoreText.init(lib);
defer ct.deinit();
var it = try ct.discover(alloc, .{ .codepoint = 'A', .size = 12 });
defer it.deinit();
@@ -981,7 +1257,10 @@ test "coretext sorting" {
const testing = std.testing;
const alloc = testing.allocator;
- var ct = CoreText.init();
+ var lib = try Library.init(alloc);
+ defer lib.deinit();
+
+ var ct = CoreText.init(lib);
defer ct.deinit();
// We try to get a Regular, Italic, Bold, & Bold Italic version of SF Pro,
@@ -1047,3 +1326,24 @@ test "coretext sorting" {
try testing.expectEqualStrings("SF Pro Bold Italic", name);
}
}
+
+test "windows" {
+ if (options.backend != .freetype_windows) return error.SkipZigTest;
+
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var lib = try Library.init(alloc);
+ defer lib.deinit();
+
+ var win = Windows.init(lib);
+ defer win.deinit();
+
+ // Arial ships on every stock Windows install.
+ var it = try win.discover(alloc, .{ .family = "Arial", .size = 12 });
+ defer it.deinit();
+
+ var face = (try it.next()) orelse return error.TestFontNotFound;
+ defer face.deinit();
+ try testing.expect(face.hasCodepoint('A', null));
+}
diff --git a/vendor/libghostty-vt/src/font/face.zig b/vendor/libghostty-vt/src/font/face.zig
index a1312c45..d77253ad 100644
--- a/vendor/libghostty-vt/src/font/face.zig
+++ b/vendor/libghostty-vt/src/font/face.zig
@@ -11,6 +11,7 @@ pub const web_canvas = @import("face/web_canvas.zig");
/// Face implementation for the compile options.
pub const Face = switch (options.backend) {
.freetype,
+ .freetype_windows,
.fontconfig_freetype,
.coretext_freetype,
=> freetype.Face,
diff --git a/vendor/libghostty-vt/src/font/library.zig b/vendor/libghostty-vt/src/font/library.zig
index dce6dbd5..56946343 100644
--- a/vendor/libghostty-vt/src/font/library.zig
+++ b/vendor/libghostty-vt/src/font/library.zig
@@ -10,6 +10,7 @@ const font = @import("main.zig");
pub const Library = switch (options.backend) {
// Freetype requires a state library
.freetype,
+ .freetype_windows,
.fontconfig_freetype,
.coretext_freetype,
=> FreetypeLibrary,
diff --git a/vendor/libghostty-vt/src/font/opentype.zig b/vendor/libghostty-vt/src/font/opentype.zig
index dd02efeb..0cba9225 100644
--- a/vendor/libghostty-vt/src/font/opentype.zig
+++ b/vendor/libghostty-vt/src/font/opentype.zig
@@ -5,12 +5,14 @@ const os2 = @import("opentype/os2.zig");
const post = @import("opentype/post.zig");
const hhea = @import("opentype/hhea.zig");
const head = @import("opentype/head.zig");
+const glyf = @import("opentype/glyf.zig");
pub const SVG = svg.SVG;
pub const OS2 = os2.OS2;
pub const Post = post.Post;
pub const Hhea = hhea.Hhea;
pub const Head = head.Head;
+pub const Glyf = glyf.Glyf;
test {
@import("std").testing.refAllDecls(@This());
diff --git a/vendor/libghostty-vt/src/font/opentype/glyf.zig b/vendor/libghostty-vt/src/font/opentype/glyf.zig
new file mode 100644
index 00000000..7392729a
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/opentype/glyf.zig
@@ -0,0 +1,585 @@
+const std = @import("std");
+const sfnt = @import("sfnt.zig");
+
+/// Glyph Data Table
+///
+/// This takes a little bit of a different form than other tables that we
+/// have parsers for. Due to the fact that this table contains arrays of
+/// arbitrary length, we store a pointer (slice) to the underlying data,
+/// and then have functions for getting and interpreting specific parts.
+///
+/// References:
+/// - https://learn.microsoft.com/en-us/typography/opentype/spec/glyf
+///
+/// Field names are in camelCase to match names in spec.
+pub const Glyf = struct {
+ data: []const u8,
+
+ /// https://learn.microsoft.com/en-us/typography/opentype/spec/glyf#table-organization
+ pub const Entry = struct {
+ header: Header,
+
+ /// We store a reference to the original bytes so that we can
+ /// validate or iterate the contours or components of the glyph.
+ ///
+ /// This data starts immediately after the header.
+ data: []const u8,
+
+ /// The header that's always present at
+ /// the start of any glyph in the table.
+ ///
+ /// Depending on the number of contours, the data that
+ /// comes afterwards must be interpreted differently.
+ ///
+ /// References:
+ /// - https://learn.microsoft.com/en-us/typography/opentype/spec/glyf#glyph-headers
+ pub const Header = extern struct {
+ /// If the number of contours is greater than
+ /// or equal to zero, this is a simple glyph.
+ ///
+ /// If negative, this is a composite glyph — the
+ /// value -1 should be used for composite glyphs.
+ numberOfContours: sfnt.int16 align(1),
+
+ /// Minimum x for coordinate data.
+ xMin: sfnt.int16 align(1),
+
+ /// Minimum y for coordinate data.
+ yMin: sfnt.int16 align(1),
+
+ /// Maximum x for coordinate data.
+ xMax: sfnt.int16 align(1),
+
+ /// Maximum y for coordinate data.
+ yMax: sfnt.int16 align(1),
+ };
+
+ /// The bit flags that describe the point data in simple glyph entries.
+ ///
+ /// Doc strings for each field are copied with minimal modification
+ /// from the opentype spec. Field names are altered to be clearer and
+ /// more succinct, and mentions of those field names in doc strings
+ /// have been similarly modified to match the ones in the struct.
+ ///
+ /// The relationship between the <x|y>_short and <x|y>_repeat_or_sign
+ /// fields is important, and poorly explained in prose, so instead of
+ /// that, here's a table that should make it easier to understand.
+ ///
+ /// x_short > | false | true |
+ /// x_repeat_or_sign V |------------------|------------------|
+ /// | The x-coordinate | The x-coordinate |
+ /// false | of this point is | of this point is |
+ /// | a signed 16-bit | an unsigned byte |
+ /// | value added to | value treated as |
+ /// | the *Coordinates | negative, added |
+ /// | array. | to the array of |
+ /// | | xCoordinates. |
+ /// -------------------|------------------|------------------|
+ /// | The x-coordinate | The x-coordinate |
+ /// true | of this point is | of this point is |
+ /// | the same as the | an unsigned byte |
+ /// | previous point; | value treated as |
+ /// | nothing added to | positive, added |
+ /// | the xCoordinates | to the array of |
+ /// | array. | xCoordinates. |
+ /// -------------------|------------------|------------------|
+ ///
+ /// References:
+ /// - https://learn.microsoft.com/en-us/typography/opentype/spec/glyf#simple-glyph-description
+ pub const SimpleFlags = packed struct(u8) {
+ /// If set, the point is on the curve; otherwise, it is off the curve.
+ on_curve: bool,
+
+ /// If set, the corresponding x-coordinate is 1 byte long,
+ /// and the sign is determined by the x_repeat_or_sign flag.
+ ///
+ /// If not set, its interpretation depends on the x_repeat_or_sign
+ /// flag: If that other flag is set, the x-coordinate is the same
+ /// as the previous x-coordinate, and no element is added to the
+ /// xCoordinates array. If both flags are not set, the corresponding
+ /// element in the xCoordinates array is two bytes and interpreted
+ /// as a signed integer.
+ ///
+ /// See the description of the x_repeat_or_sign flag for additional
+ /// information.
+ x_short: bool,
+
+ /// If set, the corresponding y-coordinate is 1 byte long,
+ /// and the sign is determined by the y_repeat_or_sign flag.
+ ///
+ /// If not set, its interpretation depends on the y_repeat_or_sign
+ /// flag: If that other flag is set, the y-coordinate is the same
+ /// as the previous y-coordinate, and no element is added to the
+ /// yCoordinates array. If both flags are not set, the corresponding
+ /// element in the yCoordinates array is two bytes and interpreted
+ /// as a signed integer.
+ ///
+ /// See the description of the y_repeat_or_sign flag for additional
+ /// information.
+ y_short: bool,
+
+ /// If set, the next byte (read as unsigned) specifies the number
+ /// of additional times this flag byte is to be repeated in the
+ /// logical flags array — that is, the number of additional logical
+ /// flag entries inserted after this entry. (In the expanded logical
+ /// array, this bit is ignored.) In this way, the number of flags
+ /// listed can be smaller than the number of points in the glyph
+ /// description.
+ repeat: bool,
+
+ /// This flag has two meanings, depending on how the x_short flag
+ /// is set. If x_short is set, this bit describes the sign of the
+ /// value, with 1 equaling positive and 0 negative. If x_short is
+ /// not set and this bit is set, then the current x-coordinate is
+ /// the same as the previous x-coordinate. If x_short is not set
+ /// and this bit is also not set, the current x-coordinate is a
+ /// signed 16-bit delta vector.
+ x_repeat_or_sign: bool,
+
+ /// This flag has two meanings, depending on how the y_short flag
+ /// is set. If y_short is set, this bit describes the sign of the
+ /// value, with 1 equaling positive and 0 negative. If y_short is
+ /// not set and this bit is set, then the current y-coordinate is
+ /// the same as the previous y-coordinate. If y_short is not set
+ /// and this bit is also not set, the current y-coordinate is a
+ /// signed 16-bit delta vector.
+ y_repeat_or_sign: bool,
+
+ /// If set, contours in the glyph description could overlap.
+ ///
+ /// Use of this flag is not required — that is, contours may
+ /// overlap without having this flag set. When used, it must
+ /// be set on the first flag byte for the glyph.
+ overlap: bool,
+
+ /// Bit 7 is reserved: set to zero.
+ reserved: bool,
+
+ /// Determine the size (in bytes) of the corresponding
+ /// value in the `xCoordinates` array for this flagset.
+ ///
+ /// See doc comments on the struct for an explanation.
+ pub inline fn xBytes(self: SimpleFlags) u2 {
+ return if (self.x_short)
+ // short, 1 byte
+ 1
+ else if (self.x_repeat_or_sign)
+ // repeat, 0 bytes
+ 0
+ else
+ // otherwise, 16-bit, 2 bytes.
+ 2;
+ }
+
+ /// Determine the size (in bytes) of the corresponding
+ /// value in the `yCoordinates` array for this flagset.
+ ///
+ /// See doc comments on the struct for an explanation.
+ pub inline fn yBytes(self: SimpleFlags) u2 {
+ return if (self.y_short)
+ // short, 1 byte
+ 1
+ else if (self.y_repeat_or_sign)
+ // repeat, 0 bytes
+ 0
+ else
+ // otherwise, 16-bit, 2 bytes.
+ 2;
+ }
+ };
+
+ pub const Type = enum {
+ /// A glyph made of standard contours.
+ simple,
+ /// A glyph made of references to other glyphs.
+ composite,
+ };
+
+ /// Initialize an entry from the provided data.
+ ///
+ /// This DOES NOT COPY the data, it only stores a pointer to it.
+ ///
+ /// The lifetime of this struct, then, is the same as the
+ /// lifetime of the data that is used to initialize it.
+ pub fn init(data: []const u8) error{EndOfStream}!Entry {
+ var fbs = std.io.fixedBufferStream(data);
+ const reader = fbs.reader();
+ const header = try reader.readStructEndian(Header, .big);
+ return .{ .header = header, .data = data[fbs.pos..] };
+ }
+
+ /// Identifies what type (simple or composite) of entry this is.
+ pub fn entryType(self: Entry) Type {
+ return if (self.header.numberOfContours >= 0)
+ .simple
+ else
+ .composite;
+ }
+
+ /// Errors that can be returned from `Entry.size()`.
+ pub const SizeError = error{
+ /// The entry's data wasn't large enough, ran
+ /// out of bytes before we were done reading.
+ EndOfStream,
+
+ /// The entry contains hinting instructions,
+ /// which we don't currently support.
+ InstructionsNotSupported,
+
+ /// The entry is a composite glyph,
+ /// which we don't currently support.
+ CompositeNotSupported,
+
+ /// The elements of the end points array
+ /// must strictly monotonically increase.
+ ///
+ /// This error means the provided entry violated that.
+ EndPointsOutOfOrder,
+
+ /// This entry defines points past the index determined
+ /// by the final element of the endPtsOfContours array.
+ TooManyPoints,
+ };
+
+ /// Determines the size (in bytes) of this entry.
+ ///
+ /// If the entry is valid, returns the number of bytes
+ /// taken up by this entry, including its header.
+ ///
+ /// NOTE: Currently produces errors when given composite glyphs
+ /// or any glyphs that have hinting instructions included.
+ pub fn size(self: Entry) SizeError!usize {
+ var fbs = std.io.fixedBufferStream(self.data);
+ const reader = fbs.reader();
+ switch (self.entryType()) {
+ // https://learn.microsoft.com/en-us/typography/opentype/spec/glyf#simple-glyph-description
+ .simple => {
+ const num_contours: usize = @intCast(self.header.numberOfContours);
+
+ // From the spec:
+ //
+ // > If a glyph has zero contours, no additional glyph
+ // > data beyond the header is required. A glyph with
+ // > zero contours may have additional data, however;
+ // > in particular, it may have instructions that
+ // > operate on phantom points.
+ //
+ // If our number of contours is 0, and there's less than
+ // two bytes in the remaining data, then we just return
+ // the size of the header as our size. The reason for
+ // two bytes is because that's the minimum size of the
+ // extra data, since `instructionLength` is 16 bits.
+ if (num_contours == 0 and self.data.len < 2) {
+ return @sizeOf(Header);
+ }
+
+ // uint16 endPtsOfContours[numberOfContours]
+ //
+ // Array of point indices for the last point
+ // of each contour, in increasing numeric order.
+ var max_point_index: isize = -1;
+ for (0..num_contours) |_| {
+ const index = try reader.readInt(sfnt.uint16, .big);
+ // The endpoints are supposed to monotonically increase.
+ if (index <= max_point_index) return error.EndPointsOutOfOrder;
+ max_point_index = index;
+ }
+
+ // uint16 instructionLength
+ //
+ // Total number of bytes for instructions.
+ //
+ // If instructionLength is zero, no instructions
+ // are present for this glyph, and this field is
+ // followed directly by the flags field.
+ const instructions_length = try reader.readInt(sfnt.uint16, .big);
+
+ // Since we don't have code that validates instruction
+ // byte code, we just reject all glyphs that contain any.
+ //
+ // In the future we could change this to just ignore the
+ // instructions, or even validate them, but for now this
+ // is fine, since we only need this function at all to
+ // validate glyf entries from the glyph protocol, which
+ // explicitly forbids instructions anyway.
+ if (instructions_length > 0) return error.InstructionsNotSupported;
+
+ // uint8 flags[variable]
+ //
+ // Array of flag elements.
+ //
+ // ---
+ //
+ // We do additional accounting here to figure out how many
+ // bytes the next two fields (the [x|y]Coordinates arrays)
+ // should take, so that we can just try to throw out that
+ // many bytes in order to validate them. This is because
+ // the length of each one depends on the flags.
+ //
+ // We're using `i` here to count the number of logical
+ // entries we have, which should reach the number of
+ // points defined by the final endpoint (from earlier).
+ var i: usize = 0;
+ var x_coords_len: usize = 0;
+ var y_coords_len: usize = 0;
+ while (i <= max_point_index) : (i += 1) {
+ const flag: SimpleFlags = @bitCast(try reader.readByte());
+
+ // Determine how many bytes the x and y coordinates will
+ // be represented with in the corresponding arrays, add
+ // them to our tallies.
+ x_coords_len += flag.xBytes();
+ y_coords_len += flag.yBytes();
+
+ // 0x08 REPEAT_FLAG
+ // Bit 3: If set, the next byte (read as unsigned)
+ // specifies the number of additional times this flag
+ // byte is to be repeated in the logical flags array
+ // — that is, the number of additional logical flag
+ // entries inserted after this entry.
+ if (flag.repeat) {
+ // The flag is repeated a certain number of times,
+ // which means that the point count is increased by
+ // that count, and the x_coords_len and y_coords_len
+ // must be increased by the correct number of bytes
+ // as well.
+ const repeat_count: usize = try reader.readByte();
+ i += repeat_count;
+ x_coords_len += repeat_count * flag.xBytes();
+ y_coords_len += repeat_count * flag.yBytes();
+
+ // If the repeat count pushes our logical point
+ // number beyond the max point index which we
+ // figured out earlier from the end points, then
+ // there's an issue with this entry, error out.
+ if (i > max_point_index) return error.TooManyPoints;
+ }
+ }
+
+ // uint8 or int16 xCoordinates[variable]
+ //
+ // Contour point x-coordinates.
+ //
+ // ---
+ //
+ // We determined the length of this section (in bytes)
+ // above while processing the flags, so that we can just
+ // skip that many bytes to validate this field.
+ try reader.skipBytes(x_coords_len, .{});
+
+ // uint8 or int16 yCoordinates[variable]
+ //
+ // Contour point y-coordinates.
+ //
+ // ---
+ //
+ // We determined the length of this section (in bytes)
+ // above while processing the flags, so that we can just
+ // skip that many bytes to validate this field.
+ try reader.skipBytes(y_coords_len, .{});
+ },
+
+ .composite => {
+ // We don't have code for validating composite glyphs,
+ // mainly because we don't need it, since we only use
+ // this function for the glyph protocol which explicitly
+ // forbids composite glyphs anyway.
+ //
+ // So we return false for composite glyphs.
+ return error.CompositeNotSupported;
+ },
+ }
+
+ // No issues found, the glyf entry is valid, return its length.
+ return @sizeOf(Header) + fbs.pos;
+ }
+ };
+
+ /// Initialize the table from the provided data.
+ ///
+ /// This DOES NOT COPY the data, it only stores a pointer to it.
+ ///
+ /// The lifetime of this struct, then, is the same as the
+ /// lifetime of the data that is used to initialize it.
+ pub fn init(data: []const u8) Glyf {
+ return .{ .data = data };
+ }
+
+ /// Retrieve the entry at the provided offset.
+ pub fn entry(self: Glyf, index: usize) error{EndOfStream}!Entry {
+ return try Entry.init(self.data[index..]);
+ }
+};
+
+/// TESTING ONLY
+///
+/// Retrieves the glyf at the provided index from the provided font.
+///
+/// Returns it in a tuple with the expected length based on the loca table, and the entry.
+pub fn getGlyph(font: sfnt.SFNT, index: usize) !struct { usize, Glyf.Entry } {
+ comptime if (!@import("builtin").is_test)
+ @compileError("This function is for testing only! It doesn't check bounds or anything!");
+
+ const glyf = Glyf.init(font.getTable("glyf").?);
+ const head = try @import("head.zig").Head.init(font.getTable("head").?);
+ const loca = font.getTable("loca").?;
+
+ const start_offset = switch (head.indexToLocFormat) {
+ 0 => @as(usize, std.mem.bigToNative(
+ u16,
+ std.mem.bytesAsSlice(u16, loca)[index],
+ )) * 2,
+ 1 => @as(usize, std.mem.bigToNative(
+ u32,
+ std.mem.bytesAsSlice(u32, loca)[index],
+ )),
+ else => unreachable,
+ };
+
+ const end_offset = switch (head.indexToLocFormat) {
+ 0 => @as(usize, std.mem.bigToNative(
+ u16,
+ std.mem.bytesAsSlice(u16, loca)[index + 1],
+ )) * 2,
+ 1 => @as(usize, std.mem.bigToNative(
+ u32,
+ std.mem.bytesAsSlice(u32, loca)[index + 1],
+ )),
+ else => unreachable,
+ };
+
+ return .{ end_offset - start_offset, try glyf.entry(start_offset) };
+}
+
+test "glyf" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ // Cozette because it doesn't have any hinting.
+ const test_font = @import("../embedded.zig").cozette;
+
+ const font = try sfnt.SFNT.init(test_font, alloc);
+ defer font.deinit(alloc);
+
+ // Cozette doesn't actually include a glyph for notdef,
+ // but does include a glyph for `\0` (nul), at index 1.
+ const len_nul, const glyph_nul = try getGlyph(font, 1);
+ try testing.expect(glyph_nul.entryType() == .simple);
+ // It is legal for there to be extra data between two entries, just
+ // as long as the next entry starts after the previous one ends, so
+ // it's okay for the parsed size of the entry to be less than the size
+ // determined from the difference between subsequent loca offsets.
+ try testing.expect(len_nul >= try glyph_nul.size());
+
+ // Glyph "A" is at index 66.
+ const len_A, const glyph_A = try getGlyph(font, 66);
+ try testing.expect(glyph_A.entryType() == .simple);
+ try testing.expect(len_A >= try glyph_A.size());
+
+ // Glyph "Ĩ" is at index 265.
+ const len_Itilde, const glyph_Itilde = try getGlyph(font, 265);
+ try testing.expect(glyph_Itilde.entryType() == .simple);
+ try testing.expect(len_Itilde >= try glyph_Itilde.size());
+}
+
+test "glyf: reject glyphs with instructions and composite glyphs" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const test_font = @import("../embedded.zig").jetbrains_mono;
+
+ const font = try sfnt.SFNT.init(test_font, alloc);
+ defer font.deinit(alloc);
+
+ const len_notdef, const glyph_notdef = try getGlyph(font, 0);
+ try testing.expectEqual(100, len_notdef);
+ try testing.expect(glyph_notdef.entryType() == .simple);
+ try testing.expectError(
+ Glyf.Entry.SizeError.InstructionsNotSupported,
+ glyph_notdef.size(),
+ );
+
+ // Glyph "Á" is at index 2.
+ const len_Aacute, const glyph_Aacute = try getGlyph(font, 2);
+ try testing.expectEqual(24, len_Aacute);
+ try testing.expect(glyph_Aacute.entryType() == .composite);
+ try testing.expectError(
+ Glyf.Entry.SizeError.CompositeNotSupported,
+ glyph_Aacute.size(),
+ );
+}
+
+test "glyf: reject truncated" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ // Cozette because it doesn't have any hinting.
+ const test_font = @import("../embedded.zig").cozette;
+
+ const font = try sfnt.SFNT.init(test_font, alloc);
+ defer font.deinit(alloc);
+
+ _, var glyph_nul = try getGlyph(font, 1);
+ try testing.expect(glyph_nul.entryType() == .simple);
+ // Mess with the entry's data slice, truncating
+ // it before the full length (which is 228 bytes).
+ glyph_nul.data = glyph_nul.data[0 .. 227 - @sizeOf(Glyf.Entry.Header)];
+ try testing.expectError(Glyf.Entry.SizeError.EndOfStream, glyph_nul.size());
+}
+
+test "glyf: reject endpoints out of order" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ // Cozette because it doesn't have any hinting.
+ //
+ // Also we copy it with the allocator so we can mess with it.
+ const test_font = try alloc.dupe(u8, @import("../embedded.zig").cozette[0..]);
+ defer alloc.free(test_font);
+
+ const font = try sfnt.SFNT.init(test_font, alloc);
+ defer font.deinit(alloc);
+
+ _, var glyph_nul = try getGlyph(font, 1);
+ try testing.expect(glyph_nul.entryType() == .simple);
+ // Mess with the entry's data, insert a 0 in the middle of the endpoints.
+ //
+ // Because we know the underlying data is something we
+ // copied, we can just const cast it back to mutable lol.
+ std.mem.bytesAsSlice(u16, @as([]u8, @constCast(glyph_nul.data)))[3] = 0;
+ try testing.expectError(Glyf.Entry.SizeError.EndPointsOutOfOrder, glyph_nul.size());
+}
+
+test "glyf: reject too many points" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ // Cozette because it doesn't have any hinting.
+ //
+ // Also we copy it with the allocator so we can mess with it.
+ const test_font = try alloc.dupe(u8, @import("../embedded.zig").cozette[0..]);
+ defer alloc.free(test_font);
+
+ const font = try sfnt.SFNT.init(test_font, alloc);
+ defer font.deinit(alloc);
+
+ _, var glyph_nul = try getGlyph(font, 1);
+ try testing.expect(glyph_nul.entryType() == .simple);
+ // Mess with the entry's data, make the final two bytes of the flags
+ // array be a large number repeat to exceed the correct points count.
+ //
+ // Because we know the underlying data is something we
+ // copied, we can just const cast it back to mutable lol.
+ @as([]u8, @constCast(glyph_nul.data))[107] |= 0x08;
+ @as([]u8, @constCast(glyph_nul.data))[108] = 0xFF;
+ try testing.expectError(Glyf.Entry.SizeError.TooManyPoints, glyph_nul.size());
+}
+
+test "glyf: zero-contour glyph can be header-only" {
+ const testing = std.testing;
+
+ const header: Glyf.Entry.Header = .{
+ .numberOfContours = 0,
+ .xMin = 0,
+ .yMin = 0,
+ .xMax = 0,
+ .yMax = 0,
+ };
+ const glyph = try Glyf.Entry.init(std.mem.asBytes(&header));
+ try testing.expectEqual(@sizeOf(Glyf.Entry.Header), try glyph.size());
+}
diff --git a/vendor/libghostty-vt/src/font/shape.zig b/vendor/libghostty-vt/src/font/shape.zig
index 864c0f01..bc19c1f2 100644
--- a/vendor/libghostty-vt/src/font/shape.zig
+++ b/vendor/libghostty-vt/src/font/shape.zig
@@ -19,6 +19,7 @@ pub const default_features = feature.default_features;
/// Shaper implementation for our compile options.
pub const Shaper = switch (options.backend) {
.freetype,
+ .freetype_windows,
.fontconfig_freetype,
.coretext_freetype,
.coretext_harfbuzz,
diff --git a/vendor/libghostty-vt/src/font/shaper/coretext.zig b/vendor/libghostty-vt/src/font/shaper/coretext.zig
index ff7c6d9d..3f69af6d 100644
--- a/vendor/libghostty-vt/src/font/shaper/coretext.zig
+++ b/vendor/libghostty-vt/src/font/shaper/coretext.zig
@@ -2585,7 +2585,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
});
} else {
// On CoreText we want to load Apple Emoji, we should have it.
- var disco = font.Discover.init();
+ var disco = font.Discover.init(lib);
defer disco.deinit();
var disco_it = try disco.discover(alloc, .{
.family = "Apple Color Emoji",
@@ -2640,7 +2640,7 @@ fn testShaperWithDiscoveredFont(alloc: Allocator, font_req: [:0]const u8) !TestS
// Discover and add our font to the collection.
{
- var disco = font.Discover.init();
+ var disco = font.Discover.init(lib);
defer disco.deinit();
var disco_it = try disco.discover(alloc, .{
.family = font_req,
diff --git a/vendor/libghostty-vt/src/font/shaper/harfbuzz.zig b/vendor/libghostty-vt/src/font/shaper/harfbuzz.zig
index 9c78d9de..a400ecaf 100644
--- a/vendor/libghostty-vt/src/font/shaper/harfbuzz.zig
+++ b/vendor/libghostty-vt/src/font/shaper/harfbuzz.zig
@@ -2071,7 +2071,7 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
});
} else {
// On CoreText we want to load Apple Emoji, we should have it.
- var disco = font.Discover.init();
+ var disco = font.Discover.init(lib);
defer disco.deinit();
var disco_it = try disco.discover(alloc, .{
.family = "Apple Color Emoji",
@@ -2126,7 +2126,7 @@ fn testShaperWithDiscoveredFont(alloc: Allocator, font_req: [:0]const u8) !TestS
// Discover and add our font to the collection.
{
- var disco = font.Discover.init();
+ var disco = font.Discover.init(lib);
defer disco.deinit();
var disco_it = try disco.discover(alloc, .{
.family = font_req,