aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/font/DeferredFace.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/font/DeferredFace.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/font/DeferredFace.zig')
-rw-r--r--vendor/libghostty-vt/src/font/DeferredFace.zig463
1 files changed, 463 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/font/DeferredFace.zig b/vendor/libghostty-vt/src/font/DeferredFace.zig
new file mode 100644
index 00000000..e818cca3
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/DeferredFace.zig
@@ -0,0 +1,463 @@
+//! A deferred face represents a single font face with all the information
+//! necessary to load it, but defers loading the full face until it is
+//! needed.
+//!
+//! This allows us to have many fallback fonts to look for glyphs, but
+//! only load them if they're really needed.
+const DeferredFace = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const fontconfig = @import("fontconfig");
+const macos = @import("macos");
+const font = @import("main.zig");
+const options = @import("main.zig").options;
+const Library = @import("main.zig").Library;
+const Face = @import("main.zig").Face;
+const Presentation = @import("main.zig").Presentation;
+
+const log = std.log.scoped(.deferred_face);
+
+/// Fontconfig
+fc: if (options.backend == .fontconfig_freetype) ?Fontconfig else void =
+ if (options.backend == .fontconfig_freetype) null else {},
+
+/// CoreText
+ct: if (font.Discover == font.discovery.CoreText) ?CoreText else void =
+ if (font.Discover == font.discovery.CoreText) null else {},
+
+/// Canvas
+wc: if (options.backend == .web_canvas) ?WebCanvas else void =
+ if (options.backend == .web_canvas) null else {},
+
+/// Fontconfig specific data. This is only present if building with fontconfig.
+pub const Fontconfig = struct {
+ /// The pattern for this font. This must be the "render prepared" pattern.
+ /// (i.e. call FcFontRenderPrepare).
+ pattern: *fontconfig.Pattern,
+
+ /// Charset and Langset are used for quick lookup if a codepoint and
+ /// presentation style are supported. They can be derived from pattern
+ /// but are cached since they're frequently used.
+ charset: *const fontconfig.CharSet,
+ langset: *const fontconfig.LangSet,
+
+ /// Variations to apply to this font.
+ variations: []const font.face.Variation,
+
+ pub fn deinit(self: *Fontconfig) void {
+ self.pattern.destroy();
+ self.* = undefined;
+ }
+};
+
+/// CoreText specific data. This is only present when building with CoreText.
+pub const CoreText = struct {
+ /// The initialized font
+ font: *macos.text.Font,
+
+ /// Variations to apply to this font. We apply the variations to the
+ /// search descriptor but sometimes when the font collection is
+ /// made the variation axes are reset so we have to reapply them.
+ variations: []const font.face.Variation,
+
+ pub fn deinit(self: *CoreText) void {
+ self.font.release();
+ self.* = undefined;
+ }
+};
+
+/// WebCanvas specific data. This is only present when building with canvas.
+pub const WebCanvas = struct {
+ /// The allocator to use for fonts
+ alloc: Allocator,
+
+ /// The string to use for the "font" attribute for the canvas
+ font_str: [:0]const u8,
+
+ /// The presentation for this font.
+ presentation: Presentation,
+
+ pub fn deinit(self: *WebCanvas) void {
+ self.alloc.free(self.font_str);
+ self.* = undefined;
+ }
+};
+
+pub fn deinit(self: *DeferredFace) void {
+ switch (options.backend) {
+ .fontconfig_freetype => if (self.fc) |*fc| fc.deinit(),
+ .freetype => {},
+ .web_canvas => if (self.wc) |*wc| wc.deinit(),
+ .coretext,
+ .coretext_freetype,
+ .coretext_harfbuzz,
+ .coretext_noshape,
+ => if (self.ct) |*ct| ct.deinit(),
+ }
+ self.* = undefined;
+}
+
+/// Returns the family name of the font.
+pub fn familyName(self: DeferredFace, buf: []u8) ![]const u8 {
+ switch (options.backend) {
+ .freetype => {},
+
+ .fontconfig_freetype => if (self.fc) |fc|
+ return (try fc.pattern.get(.family, 0)).string,
+
+ .coretext,
+ .coretext_freetype,
+ .coretext_harfbuzz,
+ .coretext_noshape,
+ => if (self.ct) |ct| {
+ const family_name = ct.font.copyAttribute(.family_name) orelse
+ return "unknown";
+ return family_name.cstringPtr(.utf8) orelse unsupported: {
+ break :unsupported family_name.cstring(buf, .utf8) orelse
+ return error.OutOfMemory;
+ };
+ },
+
+ .web_canvas => if (self.wc) |wc| return wc.font_str,
+ }
+
+ return "";
+}
+
+/// Returns the name of this face. The memory is always owned by the
+/// face so it doesn't have to be freed.
+pub fn name(self: DeferredFace, buf: []u8) ![]const u8 {
+ switch (options.backend) {
+ .freetype => {},
+
+ .fontconfig_freetype => if (self.fc) |fc|
+ return (try fc.pattern.get(.fullname, 0)).string,
+
+ .coretext,
+ .coretext_freetype,
+ .coretext_harfbuzz,
+ .coretext_noshape,
+ => if (self.ct) |ct| {
+ const display_name = ct.font.copyDisplayName();
+ return display_name.cstringPtr(.utf8) orelse unsupported: {
+ // "NULL if the internal storage of theString does not allow
+ // this to be returned efficiently." In this case, we need
+ // to allocate. But we can't return an allocated string because
+ // we don't have an allocator. Let's use the stack and log it.
+ break :unsupported display_name.cstring(buf, .utf8) orelse
+ return error.OutOfMemory;
+ };
+ },
+
+ .web_canvas => if (self.wc) |wc| return wc.font_str,
+ }
+
+ return "";
+}
+
+/// Load the deferred font face. This does nothing if the face is loaded.
+pub fn load(
+ self: *DeferredFace,
+ lib: Library,
+ opts: font.face.Options,
+) !Face {
+ return switch (options.backend) {
+ .fontconfig_freetype => try self.loadFontconfig(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),
+
+ // Unreachable because we must be already loaded or have the
+ // proper configuration for one of the other deferred mechanisms.
+ .freetype => unreachable,
+ };
+}
+
+fn loadFontconfig(
+ self: *DeferredFace,
+ lib: Library,
+ opts: font.face.Options,
+) !Face {
+ const fc = self.fc.?;
+
+ // Filename and index for our face so we can load it
+ const filename = (try fc.pattern.get(.file, 0)).string;
+ const face_index = (try fc.pattern.get(.index, 0)).integer;
+
+ var face = try Face.initFile(lib, filename, face_index, opts);
+ errdefer face.deinit();
+ try face.setVariations(fc.variations, opts);
+ return face;
+}
+
+fn loadCoreText(
+ self: *DeferredFace,
+ lib: Library,
+ opts: font.face.Options,
+) !Face {
+ _ = lib;
+ const ct = self.ct.?;
+ var face = try Face.initFontCopy(ct.font, opts);
+ errdefer face.deinit();
+ try face.setVariations(ct.variations, opts);
+ return face;
+}
+
+fn loadCoreTextFreetype(
+ self: *DeferredFace,
+ lib: Library,
+ opts: font.face.Options,
+) !Face {
+ const ct = self.ct.?;
+
+ // Get the URL for the font so we can get the filepath
+ const url = ct.font.copyAttribute(.url) orelse
+ return error.FontHasNoFile;
+ defer url.release();
+
+ // Get the path from the URL
+ const path = url.copyPath() orelse return error.FontHasNoFile;
+ defer path.release();
+
+ // URL decode the path
+ const blank = try macos.foundation.String.createWithBytes("", .utf8, false);
+ defer blank.release();
+ const decoded = try macos.foundation.URL.createStringByReplacingPercentEscapes(
+ path,
+ blank,
+ );
+ defer decoded.release();
+
+ // Decode into a c string. 1024 bytes should be enough for anybody.
+ var buf: [1024]u8 = undefined;
+ const path_slice = decoded.cstring(buf[0..1023], .utf8) orelse
+ return error.FontPathCantDecode;
+
+ // Freetype requires null-terminated. We always leave space at
+ // the end for a zero so we set that up here.
+ buf[path_slice.len] = 0;
+
+ // Face index 0 is not always correct. We don't ship this configuration
+ // in a release build. Users should use the pure CoreText builds.
+ //std.log.warn("path={s}", .{path_slice});
+ var face = try Face.initFile(lib, buf[0..path_slice.len :0], 0, opts);
+ errdefer face.deinit();
+ try face.setVariations(ct.variations, opts);
+
+ return face;
+}
+
+fn loadWebCanvas(
+ self: *DeferredFace,
+ opts: font.face.Options,
+) !Face {
+ const wc = self.wc.?;
+ return try .initNamed(wc.alloc, wc.font_str, opts, wc.presentation);
+}
+
+/// Returns true if this face can satisfy the given codepoint and
+/// presentation. If presentation is null, then it just checks if the
+/// codepoint is present at all.
+///
+/// This should not require the face to be loaded IF we're using a
+/// discovery mechanism (i.e. fontconfig). If no discovery is used,
+/// the face is always expected to be loaded.
+pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool {
+ switch (options.backend) {
+ .fontconfig_freetype => {
+ // If we are using fontconfig, use the fontconfig metadata to
+ // avoid loading the face.
+ if (self.fc) |fc| {
+ // Check if char exists
+ if (!fc.charset.hasChar(cp)) return false;
+
+ // If we have a presentation, check it matches
+ if (p) |desired| {
+ const emoji_lang = "und-zsye";
+ const actual: Presentation = if (fc.langset.hasLang(emoji_lang))
+ .emoji
+ else
+ .text;
+
+ return desired == actual;
+ }
+
+ return true;
+ }
+ },
+
+ .coretext,
+ .coretext_freetype,
+ .coretext_harfbuzz,
+ .coretext_noshape,
+ => {
+ // If we are using coretext, we check the loaded CT font.
+ if (self.ct) |ct| {
+ // This presentation check isn't as detailed as isColorGlyph
+ // because forced presentation modes are only used for emoji and
+ // emoji should always have color glyphs set. This can be
+ // more correct by using the isColorGlyph logic but I'd want
+ // to find a font that actually requires this so we can write
+ // a test for it before changing it.
+ if (p) |desired_p| {
+ const traits = ct.font.getSymbolicTraits();
+ const actual_p: Presentation = if (traits.color_glyphs) .emoji else .text;
+ if (actual_p != desired_p) return false;
+ }
+
+ // Turn UTF-32 into UTF-16 for CT API
+ var unichars: [2]u16 = undefined;
+ const pair = macos.foundation.stringGetSurrogatePairForLongCharacter(cp, &unichars);
+ const len: usize = if (pair) 2 else 1;
+
+ // Get our glyphs
+ var glyphs = [2]macos.graphics.Glyph{ 0, 0 };
+ return ct.font.getGlyphsForCharacters(unichars[0..len], glyphs[0..len]);
+ }
+ },
+
+ // Canvas always has the codepoint because we have no way of
+ // really checking and we let the browser handle it.
+ .web_canvas => if (self.wc) |wc| {
+ // Fast-path if we have a specific presentation and we
+ // don't match, then it is definitely not this face.
+ if (p) |desired| if (wc.presentation != desired) return false;
+
+ // Slow-path: we initialize the font, render it, and check
+ // if it works and the presentation matches.
+ var face = Face.initNamed(
+ wc.alloc,
+ wc.font_str,
+ .{ .points = 12 },
+ wc.presentation,
+ ) catch |err| {
+ log.warn("failed to init face for codepoint check " ++
+ "face={s} err={}", .{
+ wc.font_str,
+ err,
+ });
+
+ return false;
+ };
+ defer face.deinit();
+ return face.glyphIndex(cp) != null;
+ },
+
+ .freetype => {},
+ }
+
+ // This is unreachable because discovery mechanisms terminate, and
+ // if we're not using a discovery mechanism, the face MUST be loaded.
+ unreachable;
+}
+
+/// The wasm-compatible API.
+pub const Wasm = struct {
+ const wasm = @import("../os/wasm.zig");
+ const alloc = wasm.alloc;
+
+ export fn deferred_face_new(ptr: [*]const u8, len: usize, presentation: u16) ?*DeferredFace {
+ return deferred_face_new_(ptr, len, presentation) catch |err| {
+ log.warn("error creating deferred face err={}", .{err});
+ return null;
+ };
+ }
+
+ fn deferred_face_new_(ptr: [*]const u8, len: usize, presentation: u16) !*DeferredFace {
+ const font_str = try alloc.dupeZ(u8, ptr[0..len]);
+ errdefer alloc.free(font_str);
+
+ var face: DeferredFace = .{
+ .wc = .{
+ .alloc = alloc,
+ .font_str = font_str,
+ .presentation = @enumFromInt(presentation),
+ },
+ };
+ errdefer face.deinit();
+
+ const result = try alloc.create(DeferredFace);
+ errdefer alloc.destroy(result);
+ result.* = face;
+ return result;
+ }
+
+ export fn deferred_face_free(ptr: ?*DeferredFace) void {
+ if (ptr) |v| {
+ v.deinit();
+ alloc.destroy(v);
+ }
+ }
+
+ export fn deferred_face_load(self: *DeferredFace, pts: f32) void {
+ self.load(.{}, .{ .points = pts }) catch |err| {
+ log.warn("error loading deferred face err={}", .{err});
+ return;
+ };
+ }
+};
+
+test "fontconfig" {
+ if (options.backend != .fontconfig_freetype) return error.SkipZigTest;
+
+ const discovery = @import("main.zig").discovery;
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ // Load freetype
+ var lib = try Library.init(alloc);
+ defer lib.deinit();
+
+ // Get a deferred face from fontconfig
+ var def = def: {
+ var fc = discovery.Fontconfig.init();
+ defer fc.deinit();
+ var it = try fc.discover(alloc, .{ .family = "monospace", .size = 12 });
+ defer it.deinit();
+ break :def (try it.next()).?;
+ };
+ defer def.deinit();
+
+ // Verify we can get the name
+ var buf: [1024]u8 = undefined;
+ const n = try def.name(&buf);
+ try testing.expect(n.len > 0);
+
+ // Load it and verify it works
+ var face = try def.load(lib, .{ .size = .{ .points = 12 } });
+ defer face.deinit();
+ try testing.expect(face.glyphIndex(' ') != null);
+}
+
+test "coretext" {
+ if (options.backend != .coretext) return error.SkipZigTest;
+
+ const discovery = @import("main.zig").discovery;
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ // Load freetype
+ var lib = try Library.init(alloc);
+ defer lib.deinit();
+
+ // Get a deferred face from fontconfig
+ var def = def: {
+ var fc = discovery.CoreText.init();
+ var it = try fc.discover(alloc, .{ .family = "Monaco", .size = 12 });
+ defer it.deinit();
+ break :def (try it.next()).?;
+ };
+ defer def.deinit();
+ try testing.expect(def.hasCodepoint(' ', null));
+
+ // Verify we can get the name
+ var buf: [1024]u8 = undefined;
+ const n = try def.name(&buf);
+ try testing.expect(n.len > 0);
+
+ // Load it and verify it works
+ var face = try def.load(lib, .{ .size = .{ .points = 12 } });
+ defer face.deinit();
+ try testing.expect(face.glyphIndex(' ') != null);
+}