diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-07 18:29:21 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-05-07 18:29:21 +0300 |
| commit | dcfe11a7588fcd42471159453810abc9b7720a57 (patch) | |
| tree | c32a77266028e73ca2382b96c96f8438a5fcd621 /vendor/libghostty-vt/src/font/discovery.zig | |
| parent | 2dfed860103c4e4505ac7f25def9fe37da1e1e6a (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/discovery.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/font/discovery.zig | 314 |
1 files changed, 307 insertions, 7 deletions
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)); +} |
