mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 00:36:07 +03:00
437 lines
14 KiB
Zig
437 lines
14 KiB
Zig
//! 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 assert = std.debug.assert;
|
|
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);
|
|
|
|
/// The struct used for deferred face state.
|
|
///
|
|
/// TODO: Change the "fc", "ct", "wc" fields in @This to just use one field
|
|
/// with the state since there should be no world in which multiple are used.
|
|
const FaceState = switch (options.backend) {
|
|
.freetype => void,
|
|
.fontconfig_freetype => Fontconfig,
|
|
.coretext_freetype, .coretext => CoreText,
|
|
.web_canvas => WebCanvas,
|
|
};
|
|
|
|
/// 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,
|
|
|
|
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(),
|
|
.coretext, .coretext_freetype => if (self.ct) |*ct| ct.deinit(),
|
|
.freetype => {},
|
|
.web_canvas => if (self.wc) |*wc| wc.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 => if (self.ct) |ct| {
|
|
const family_name = ct.font.copyAttribute(.family_name);
|
|
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 => 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 => 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);
|
|
return face;
|
|
}
|
|
|
|
fn loadCoreText(
|
|
self: *DeferredFace,
|
|
lib: Library,
|
|
opts: font.face.Options,
|
|
) !Face {
|
|
_ = lib;
|
|
const ct = self.ct.?;
|
|
return try Face.initFontCopy(ct.font, opts);
|
|
}
|
|
|
|
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);
|
|
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;
|
|
|
|
// TODO: face index 0 is not correct long term and we should switch
|
|
// to using CoreText for rendering, too.
|
|
//std.log.warn("path={s}", .{path_slice});
|
|
return try Face.initFile(lib, buf[0..path_slice.len :0], 0, opts);
|
|
}
|
|
|
|
fn loadWebCanvas(
|
|
self: *DeferredFace,
|
|
opts: font.face.Options,
|
|
) !Face {
|
|
const wc = self.wc.?;
|
|
return try Face.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 => {
|
|
// If we are using coretext, we check the loaded CT font.
|
|
if (self.ct) |ct| {
|
|
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 {
|
|
var 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();
|
|
|
|
var 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: u16) 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();
|
|
defer lib.deinit();
|
|
|
|
// Get a deferred face from fontconfig
|
|
var def = def: {
|
|
var fc = discovery.Fontconfig.init();
|
|
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
|
|
const face = try def.load(lib, .{ .points = 12 });
|
|
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();
|
|
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
|
|
const face = try def.load(lib, .{ .points = 12 });
|
|
try testing.expect(face.glyphIndex(' ') != null);
|
|
}
|