mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-24 12:46:10 +03:00
173 lines
5.4 KiB
Zig
173 lines
5.4 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 fontconfig = @import("fontconfig");
|
|
const options = @import("main.zig").options;
|
|
const Library = @import("main.zig").Library;
|
|
const Face = @import("main.zig").Face;
|
|
const Presentation = @import("main.zig").Presentation;
|
|
|
|
/// The loaded face (once loaded).
|
|
face: ?Face = null,
|
|
|
|
/// Fontconfig
|
|
fc: if (options.fontconfig) ?Fontconfig else void = if (options.fontconfig) 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,
|
|
|
|
/// The requested size in points for this font. This is used for loading.
|
|
/// This can't be derived from pattern because the requested size may
|
|
/// differ from the size the font advertises supported.
|
|
req_size: u16,
|
|
|
|
pub fn deinit(self: *Fontconfig) void {
|
|
self.pattern.destroy();
|
|
self.* = undefined;
|
|
}
|
|
};
|
|
|
|
/// Initialize a deferred face that is already pre-loaded. The deferred face
|
|
/// takes ownership over the loaded face, deinit will deinit the loaded face.
|
|
pub fn initLoaded(face: Face) DeferredFace {
|
|
return .{ .face = face };
|
|
}
|
|
|
|
pub fn deinit(self: *DeferredFace) void {
|
|
if (self.face) |*face| face.deinit();
|
|
if (options.fontconfig) if (self.fc) |*fc| fc.deinit();
|
|
self.* = undefined;
|
|
}
|
|
|
|
/// Returns true if the face has been loaded.
|
|
pub inline fn loaded(self: DeferredFace) bool {
|
|
return self.face != null;
|
|
}
|
|
|
|
/// Load the deferred font face. This does nothing if the face is loaded.
|
|
pub fn load(self: *DeferredFace, lib: Library) !void {
|
|
// No-op if we already loaded
|
|
if (self.face != null) return;
|
|
|
|
if (options.fontconfig) {
|
|
try self.loadFontconfig(lib);
|
|
return;
|
|
}
|
|
|
|
// Unreachable because we must be already loaded or have the
|
|
// proper configuration for one of the other deferred mechanisms.
|
|
unreachable;
|
|
}
|
|
|
|
fn loadFontconfig(self: *DeferredFace, lib: Library) !void {
|
|
assert(self.face == null);
|
|
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;
|
|
|
|
self.face = try Face.initFile(lib, filename, face_index, .{
|
|
.points = fc.req_size,
|
|
});
|
|
}
|
|
|
|
/// 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 {
|
|
// If we have the face, use the face.
|
|
if (self.face) |face| {
|
|
if (p) |desired| if (face.presentation != desired) return false;
|
|
return face.glyphIndex(cp) != null;
|
|
}
|
|
|
|
// If we are using fontconfig, use the fontconfig metadata to
|
|
// avoid loading the face.
|
|
if (options.fontconfig) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
// This is unreachable because discovery mechanisms terminate, and
|
|
// if we're not using a discovery mechanism, the face MUST be loaded.
|
|
unreachable;
|
|
}
|
|
|
|
test "preloaded" {
|
|
const testing = std.testing;
|
|
const testFont = @import("test.zig").fontRegular;
|
|
|
|
var lib = try Library.init();
|
|
defer lib.deinit();
|
|
|
|
var face = try Face.init(lib, testFont, .{ .points = 12 });
|
|
errdefer face.deinit();
|
|
|
|
var def = initLoaded(face);
|
|
defer def.deinit();
|
|
|
|
try testing.expect(def.hasCodepoint(' ', null));
|
|
}
|
|
|
|
test "fontconfig" {
|
|
if (!options.fontconfig) return error.SkipZigTest;
|
|
|
|
const discovery = @import("main.zig").discovery;
|
|
const testing = std.testing;
|
|
|
|
// 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(.{ .family = "monospace", .size = 12 });
|
|
defer it.deinit();
|
|
break :def (try it.next()).?;
|
|
};
|
|
defer def.deinit();
|
|
try testing.expect(!def.loaded());
|
|
|
|
// Load it and verify it works
|
|
try def.load(lib);
|
|
try testing.expect(def.hasCodepoint(' ', null));
|
|
try testing.expect(def.face.?.glyphIndex(' ') != null);
|
|
}
|