From 61fce4d0a4a117c4433be0fff4b4e7681f33bdf1 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Thu, 23 Apr 2026 13:32:49 +0900 Subject: [PATCH 1/5] font: add Windows font discovery backend Adds a FreeType-based Discover implementation for Windows that walks the system (C:\Windows\Fonts) and per-user (%LOCALAPPDATA%\Microsoft\Windows\Fonts) font directories, matching descriptors via family_name / SFNT name table and optionally codepoint presence. Wired up as a new .freetype_windows backend which Backend.default() now returns on Windows. Existing freetype-only paths are untouched. With this in place, standard code paths -- +list-fonts, SharedGridSet font-family lookup, CodepointResolver fallback -- work on Windows without any os.tag == .windows branches in the caller. Co-authored-by: Claude --- src/cli/list_fonts.zig | 15 ++- src/font/DeferredFace.zig | 67 +++++++++++ src/font/SharedGridSet.zig | 5 +- src/font/backend.zig | 18 ++- src/font/discovery.zig | 238 +++++++++++++++++++++++++++++++++++++ src/font/face.zig | 1 + src/font/library.zig | 1 + src/font/shape.zig | 1 + 8 files changed, 339 insertions(+), 7 deletions(-) diff --git a/src/cli/list_fonts.zig b/src/cli/list_fonts.zig index 396c4e8a6..26f684431 100644 --- a/src/cli/list_fonts.zig +++ b/src/cli/list_fonts.zig @@ -4,6 +4,7 @@ const ArenaAllocator = std.heap.ArenaAllocator; const Action = @import("ghostty.zig").Action; const args = @import("args.zig"); const font = @import("../font/main.zig"); +const discovery = @import("../font/discovery.zig"); const log = std.log.scoped(.list_fonts); @@ -100,8 +101,18 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { var families: std.ArrayList([]const u8) = .empty; var map: std.StringHashMap(std.ArrayListUnmanaged([]const u8)) = .init(alloc); - // Look up all available fonts - var disco = font.Discover.init(); + // Look up all available fonts. The Windows backend needs a FreeType + // library handle so it can open candidate font files while scanning + // the system/user font directories. + var font_lib = if (comptime font.Discover == discovery.Windows) + try font.Library.init(alloc) + else {}; + defer if (comptime font.Discover == discovery.Windows) font_lib.deinit(); + + var disco = if (comptime font.Discover == discovery.Windows) + font.Discover.init(font_lib) + else + font.Discover.init(); defer disco.deinit(); var disco_it = try disco.discover(alloc, .{ .family = config.family, diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index e818cca30..4bca46bc6 100644 --- a/src/font/DeferredFace.zig +++ b/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, diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index b832139b3..c20055089 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -442,7 +442,10 @@ fn discover(self: *SharedGridSet) !?*Discover { // If we initialized, use it if (self.font_discover) |*v| return v; - self.font_discover = .init(); + self.font_discover = if (comptime Discover == discovery.Windows) + .init(self.font_lib) + else + .init(); return &self.font_discover.?; } diff --git a/src/font/backend.zig b/src/font/backend.zig index d24edec79..867421de7 100644 --- a/src/font/backend.zig +++ b/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/src/font/discovery.zig b/src/font/discovery.zig index c419d36a6..09d155104 100644 --- a/src/font/discovery.zig +++ b/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, @@ -879,6 +884,239 @@ 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, + .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, + user_path: ?[:0]const u8, + + const State = enum { system, user, done }; + + const system_fonts_dir = "C:\\Windows\\Fonts"; + + pub fn deinit(self: *DiscoverIterator) void { + if (self.dir) |*d| d.close(); + 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 => { + self.dir = std.fs.openDirAbsoluteZ( + system_fonts_dir, + .{ .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; + } + } + + 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 => system_fonts_dir, + .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; diff --git a/src/font/face.zig b/src/font/face.zig index a1312c45a..d77253adf 100644 --- a/src/font/face.zig +++ b/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/src/font/library.zig b/src/font/library.zig index dce6dbd5a..56946343e 100644 --- a/src/font/library.zig +++ b/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/src/font/shape.zig b/src/font/shape.zig index 864c0f012..bc19c1f23 100644 --- a/src/font/shape.zig +++ b/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, From fe2a909782607b6046b2a93d866b4ba86b361a94 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Thu, 23 Apr 2026 13:39:35 +0900 Subject: [PATCH 2/5] font/discovery: use %SYSTEMROOT%\Fonts instead of a hardcoded path Resolve the system font directory from SYSTEMROOT rather than assuming it lives on C:. If SYSTEMROOT is somehow unset we skip the system directory instead of falling back to a literal drive letter. Co-authored-by: Claude --- src/font/discovery.zig | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/font/discovery.zig b/src/font/discovery.zig index 09d155104..58bd0b17e 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -919,6 +919,7 @@ pub const Windows = struct { .state = .system, .dir = null, .iter = null, + .system_path = null, .user_path = null, }; } @@ -941,14 +942,14 @@ pub const Windows = struct { 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 }; - const system_fonts_dir = "C:\\Windows\\Fonts"; - 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; } @@ -959,8 +960,13 @@ pub const Windows = struct { 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( - system_fonts_dir, + path, .{ .iterate = true }, ) catch { self.state = .user; @@ -1007,6 +1013,24 @@ pub const Windows = struct { } } + /// 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, @@ -1026,7 +1050,7 @@ pub const Windows = struct { name: []const u8, ) !?DeferredFace { const dir_path = switch (self.state) { - .system => system_fonts_dir, + .system => self.system_path.?, .user => self.user_path.?, .done => return null, }; From 5aef2541b044e1c68bf830aa6878e07e7128c301 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Thu, 23 Apr 2026 23:06:21 +0900 Subject: [PATCH 3/5] address review: Discover.init takes a Library across all backends Per review feedback, drop the `if (Discover == Windows)` comptime branches in SharedGridSet and list_fonts by making every backend's `init` take a Library and ignore it when unused. Call sites just do `Discover.init(self.font_lib)` now. Also adds a discovery test for the Windows backend that looks up Arial and checks the returned face has the 'A' codepoint. Co-authored-by: Claude --- src/cli/list_fonts.zig | 19 +++++--------- src/font/SharedGridSet.zig | 5 +--- src/font/discovery.zig | 52 +++++++++++++++++++++++++++++++++----- 3 files changed, 52 insertions(+), 24 deletions(-) diff --git a/src/cli/list_fonts.zig b/src/cli/list_fonts.zig index 26f684431..6e08aa838 100644 --- a/src/cli/list_fonts.zig +++ b/src/cli/list_fonts.zig @@ -4,7 +4,6 @@ const ArenaAllocator = std.heap.ArenaAllocator; const Action = @import("ghostty.zig").Action; const args = @import("args.zig"); const font = @import("../font/main.zig"); -const discovery = @import("../font/discovery.zig"); const log = std.log.scoped(.list_fonts); @@ -101,18 +100,12 @@ fn runArgs(alloc_gpa: Allocator, argsIter: anytype) !u8 { var families: std.ArrayList([]const u8) = .empty; var map: std.StringHashMap(std.ArrayListUnmanaged([]const u8)) = .init(alloc); - // Look up all available fonts. The Windows backend needs a FreeType - // library handle so it can open candidate font files while scanning - // the system/user font directories. - var font_lib = if (comptime font.Discover == discovery.Windows) - try font.Library.init(alloc) - else {}; - defer if (comptime font.Discover == discovery.Windows) font_lib.deinit(); - - var disco = if (comptime font.Discover == discovery.Windows) - font.Discover.init(font_lib) - else - font.Discover.init(); + // Look up all available fonts. The library is only used by backends + // that need it (the Windows backend opens candidate font files with + // FreeType); other backends ignore it. + var font_lib = try font.Library.init(alloc); + defer font_lib.deinit(); + var disco = font.Discover.init(font_lib); defer disco.deinit(); var disco_it = try disco.discover(alloc, .{ .family = config.family, diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index c20055089..9d8148bdc 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -442,10 +442,7 @@ fn discover(self: *SharedGridSet) !?*Discover { // If we initialized, use it if (self.font_discover) |*v| return v; - self.font_discover = if (comptime Discover == discovery.Windows) - .init(self.font_lib) - else - .init(); + self.font_discover = .init(self.font_lib); return &self.font_discover.?; } diff --git a/src/font/discovery.zig b/src/font/discovery.zig index 58bd0b17e..b945aa01b 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -247,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() }; @@ -338,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 .{}; } @@ -1162,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(); @@ -1174,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(); @@ -1196,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(); @@ -1214,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(); @@ -1243,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, @@ -1309,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)); +} From fe725b5da19d5019f3b4d1338cfe342f63257e5f Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Fri, 24 Apr 2026 01:27:58 +0900 Subject: [PATCH 4/5] address review: update shaper test discover callsites CI on Windows (MSVC) surfaced three remaining callers of the old zero-arg `Discover.init()` in shaper test helpers that the earlier commit missed. Pass `lib` to match the new signature. Co-authored-by: Claude --- src/font/shaper/coretext.zig | 4 ++-- src/font/shaper/harfbuzz.zig | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index ff7c6d9d3..3f69af6d2 100644 --- a/src/font/shaper/coretext.zig +++ b/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/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index 9c78d9de3..a400ecaff 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/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, From 0343a4d98fdecb58306f8d8712455b496cf8b2d1 Mon Sep 17 00:00:00 2001 From: Yasuhiro Matsumoto Date: Fri, 24 Apr 2026 01:40:01 +0900 Subject: [PATCH 5/5] address review: update DeferredFace test discover callsites Two more holdouts in DeferredFace.zig test helpers calling Fontconfig.init / CoreText.init with no args; Nix test CI surfaced them for the fontconfig path. Co-authored-by: Claude --- src/font/DeferredFace.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index 4bca46bc6..425f3c283 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -478,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(); @@ -510,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()).?;