aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/font/discovery.zig
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/discovery.zig
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/discovery.zig')
-rw-r--r--vendor/libghostty-vt/src/font/discovery.zig314
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));
+}