Font discovery on Linux via fontconfig

This adds the `--font-family` set of configuration flags to choose a font that is installed on the system rather than the embedded font in the binary. This uses fontconfig on Linux and only supports Linux at the moment.

Fontconfig is an optional dependency, building with `-Dfontconfig=false` will disable fontconfig and font discovery on all platforms. Fontconfig is only enabled by default on Linux.
This commit is contained in:
Mitchell Hashimoto
2022-09-29 15:01:22 -07:00
committed by GitHub
15 changed files with 687 additions and 97 deletions

View File

@ -16,6 +16,7 @@ const system_sdk = @import("vendor/mach/libs/glfw/system_sdk.zig");
// Build options, see the build options help for more info.
var tracy: bool = false;
var enable_fontconfig: bool = false;
pub fn build(b: *std.build.Builder) !void {
const mode = b.standardReleaseOptions();
@ -36,6 +37,12 @@ pub fn build(b: *std.build.Builder) !void {
"Enable Tracy integration (default true in Debug on Linux)",
) orelse (mode == .Debug and target.isLinux());
enable_fontconfig = b.option(
bool,
"fontconfig",
"Enable fontconfig for font discovery (default true on Linux)",
) orelse target.isLinux();
const static = b.option(
bool,
"static",
@ -49,12 +56,12 @@ pub fn build(b: *std.build.Builder) !void {
);
const exe = b.addExecutable("ghostty", "src/main.zig");
const exe_options = b.addOptions();
exe_options.addOption(bool, "tracy_enabled", tracy);
exe_options.addOption(bool, "fontconfig", enable_fontconfig);
// Exe
{
const exe_options = b.addOptions();
exe_options.addOption(bool, "tracy_enabled", tracy);
exe.setTarget(target);
exe.setBuildMode(mode);
exe.addOptions("build_options", exe_options);
@ -119,6 +126,7 @@ pub fn build(b: *std.build.Builder) !void {
main_test.setTarget(target);
try addDeps(b, main_test, true);
main_test.addOptions("build_options", exe_options);
var before = b.addLog("\x1b[" ++ color_map.get("cyan").? ++ "\x1b[" ++ color_map.get("b").? ++ "[{s} tests]" ++ "\x1b[" ++ color_map.get("d").? ++ " ----" ++ "\x1b[0m", .{"ghostty"});
var after = b.addLog("\x1b[" ++ color_map.get("d").? ++ "---\n\n" ++ "\x1b[0m", .{});
@ -132,6 +140,7 @@ pub fn build(b: *std.build.Builder) !void {
// we wrote (are in the "pkg/" directory).
for (main_test.packages.items) |pkg_| {
const pkg: std.build.Pkg = pkg_;
if (std.mem.eql(u8, pkg.name, "build_options")) continue;
if (std.mem.eql(u8, pkg.name, "glfw")) continue;
var test_ = b.addTestSource(pkg.source);
@ -158,7 +167,7 @@ fn addDeps(
static: bool,
) !void {
// We always need the Zig packages
step.addPackage(fontconfig.pkg);
if (enable_fontconfig) step.addPackage(fontconfig.pkg);
step.addPackage(freetype.pkg);
step.addPackage(harfbuzz.pkg);
step.addPackage(glfw.pkg);
@ -195,7 +204,7 @@ fn addDeps(
step.linkSystemLibrary("libuv");
step.linkSystemLibrary("zlib");
if (step.target.isLinux()) step.linkSystemLibrary("fontconfig");
if (enable_fontconfig) step.linkSystemLibrary("fontconfig");
}
// Other dependencies, we may dynamically link
@ -237,7 +246,7 @@ fn addDeps(
system_sdk.include(b, libuv_step, .{});
// Only Linux gets fontconfig
if (step.target.isLinux()) {
if (enable_fontconfig) {
// Libxml2
const libxml2_lib = try libxml2.create(
b,

View File

@ -32,6 +32,18 @@ pub fn version(self: Library) Version {
return v;
}
/// Call FT_New_Face to open a font from a file.
pub fn initFace(self: Library, path: [:0]const u8, index: i32) Error!Face {
var face: Face = undefined;
try intToError(c.FT_New_Face(
self.handle,
path.ptr,
index,
&face.handle,
));
return face;
}
/// Call FT_Open_Face to open a font that has been loaded into memory.
pub fn initMemoryFace(self: Library, data: []const u8, index: i32) Error!Face {
var face: Face = undefined;

View File

@ -89,7 +89,7 @@ pub fn buildFreetype(
"-DHAVE_UNISTD_H",
"-DHAVE_FCNTL_H",
//"-fno-sanitize=undefined",
"-fno-sanitize=undefined",
});
if (opt.libpng.enabled) try flags.append("-DFT_CONFIG_OPTION_USE_PNG=1");
if (opt.zlib.enabled) try flags.append("-DFT_CONFIG_OPTION_SYSTEM_ZLIB=1");

View File

@ -58,8 +58,7 @@ texture: gl.Texture,
texture_color: gl.Texture,
/// The font structures.
font_lib: font.Library,
font_group: font.GroupCache,
font_group: *font.GroupCache,
font_shaper: font.Shaper,
/// Whether the cursor is visible or not. This is used to control cursor
@ -152,45 +151,7 @@ const GPUCellMode = enum(u8) {
}
};
pub fn init(
alloc: Allocator,
font_size: font.Face.DesiredSize,
) !Grid {
// Build our font group
var font_lib = try font.Library.init();
errdefer font_lib.deinit();
var font_group = try font.GroupCache.init(alloc, group: {
var group = try font.Group.init(alloc);
errdefer group.deinit(alloc);
// Our regular font
try group.addFace(
alloc,
.regular,
try font.Face.init(font_lib, face_ttf, font_size),
);
try group.addFace(
alloc,
.bold,
try font.Face.init(font_lib, face_bold_ttf, font_size),
);
// Emoji
try group.addFace(
alloc,
.regular,
try font.Face.init(font_lib, face_emoji_ttf, font_size),
);
try group.addFace(
alloc,
.regular,
try font.Face.init(font_lib, face_emoji_text_ttf, font_size),
);
break :group group;
});
errdefer font_group.deinit(alloc);
pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Grid {
// Create the initial font shaper
var shape_buf = try alloc.alloc(font.Shaper.Cell, 1);
errdefer alloc.free(shape_buf);
@ -326,7 +287,6 @@ pub fn init(
.vbo = vbo,
.texture = tex,
.texture_color = tex_color,
.font_lib = font_lib,
.font_group = font_group,
.font_shaper = shaper,
.cursor_visible = true,
@ -339,8 +299,6 @@ pub fn init(
pub fn deinit(self: *Grid) void {
self.font_shaper.deinit();
self.alloc.free(self.font_shaper.cell_buf);
self.font_group.deinit(self.alloc);
self.font_lib.deinit();
self.texture.destroy();
self.texture_color.destroy();
@ -423,7 +381,7 @@ pub fn rebuildCells(self: *Grid, term: *Terminal) !void {
const start = self.cells.items.len;
// Split our row into runs and shape each one.
var iter = self.font_shaper.runIterator(&self.font_group, row);
var iter = self.font_shaper.runIterator(self.font_group, row);
while (try iter.next(self.alloc)) |run| {
for (try self.font_shaper.shape(run)) |shaper_cell| {
assert(try self.updateCell(
@ -610,7 +568,7 @@ pub fn updateCell(
// If the cell has a character, draw it
if (cell.char > 0) {
// Render
const face = self.font_group.group.faceFromIndex(shaper_run.font_index);
const face = try self.font_group.group.faceFromIndex(shaper_run.font_index);
const glyph = try self.font_group.renderGlyph(
self.alloc,
shaper_run.font_index,
@ -912,8 +870,3 @@ test "GridSize update rounding" {
try testing.expectEqual(@as(GridSize.Unit, 3), grid.columns);
try testing.expectEqual(@as(GridSize.Unit, 2), grid.rows);
}
const face_ttf = @embedFile("font/res/FiraCode-Regular.ttf");
const face_bold_ttf = @embedFile("font/res/FiraCode-Bold.ttf");
const face_emoji_ttf = @embedFile("font/res/NotoColorEmoji.ttf");
const face_emoji_text_ttf = @embedFile("font/res/NotoEmoji-Regular.ttf");

View File

@ -35,6 +35,10 @@ const WRITE_REQ_PREALLOC = std.math.pow(usize, 2, 5);
alloc: Allocator,
alloc_io_arena: std.heap.ArenaAllocator,
/// The font structures
font_lib: font.Library,
font_group: *font.GroupCache,
/// The glfw window handle.
window: glfw.Window,
@ -237,13 +241,108 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo
gl.c.glEnable(gl.c.GL_BLEND);
gl.c.glBlendFunc(gl.c.GL_SRC_ALPHA, gl.c.GL_ONE_MINUS_SRC_ALPHA);
// Create our terminal grid with the initial window size
const window_size = try window.getSize();
var grid = try Grid.init(alloc, .{
// The font size we desire along with the DPI determiend for the window
const font_size: font.Face.DesiredSize = .{
.points = config.@"font-size",
.xdpi = @floatToInt(u16, x_dpi),
.ydpi = @floatToInt(u16, y_dpi),
};
// Find all the fonts for this window
var font_lib = try font.Library.init();
errdefer font_lib.deinit();
var font_group = try alloc.create(font.GroupCache);
errdefer alloc.destroy(font_group);
font_group.* = try font.GroupCache.init(alloc, group: {
var group = try font.Group.init(alloc, font_lib, font_size);
errdefer group.deinit(alloc);
// Search for fonts
if (font.Discover != void) {
var disco = font.Discover.init();
defer disco.deinit();
if (config.@"font-family") |family| {
var disco_it = try disco.discover(.{
.family = family,
.size = font_size.points,
});
defer disco_it.deinit();
if (try disco_it.next()) |face| {
log.debug("font regular: {s}", .{try face.name()});
try group.addFace(alloc, .regular, face);
}
}
if (config.@"font-family-bold") |family| {
var disco_it = try disco.discover(.{
.family = family,
.size = font_size.points,
.bold = true,
});
defer disco_it.deinit();
if (try disco_it.next()) |face| {
log.debug("font bold: {s}", .{try face.name()});
try group.addFace(alloc, .bold, face);
}
}
if (config.@"font-family-italic") |family| {
var disco_it = try disco.discover(.{
.family = family,
.size = font_size.points,
.italic = true,
});
defer disco_it.deinit();
if (try disco_it.next()) |face| {
log.debug("font italic: {s}", .{try face.name()});
try group.addFace(alloc, .italic, face);
}
}
if (config.@"font-family-bold-italic") |family| {
var disco_it = try disco.discover(.{
.family = family,
.size = font_size.points,
.bold = true,
.italic = true,
});
defer disco_it.deinit();
if (try disco_it.next()) |face| {
log.debug("font bold+italic: {s}", .{try face.name()});
try group.addFace(alloc, .bold_italic, face);
}
}
}
// Our built-in font will be used as a backup
try group.addFace(
alloc,
.regular,
font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_ttf, font_size)),
);
try group.addFace(
alloc,
.bold,
font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_bold_ttf, font_size)),
);
// Emoji
try group.addFace(
alloc,
.regular,
font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_emoji_ttf, font_size)),
);
try group.addFace(
alloc,
.regular,
font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_emoji_text_ttf, font_size)),
);
break :group group;
});
errdefer font_group.deinit(alloc);
// Create our terminal grid with the initial window size
const window_size = try window.getSize();
var grid = try Grid.init(alloc, font_group);
try grid.setScreenSize(.{ .width = window_size.width, .height = window_size.height });
grid.background = .{
.r = config.background.r,
@ -333,6 +432,8 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo
self.* = .{
.alloc = alloc,
.alloc_io_arena = io_arena,
.font_lib = font_lib,
.font_group = font_group,
.window = window,
.cursor = cursor,
.focused = false,
@ -419,6 +520,9 @@ pub fn destroy(self: *Window) void {
// windows using it to the default.
self.cursor.destroy();
self.font_group.deinit(self.alloc);
self.font_lib.deinit();
self.alloc_io_arena.deinit();
}
@ -1715,3 +1819,8 @@ pub fn invokeCharset(
) !void {
self.terminal.invokeCharset(active, slot, single);
}
const face_ttf = @embedFile("font/res/FiraCode-Regular.ttf");
const face_bold_ttf = @embedFile("font/res/FiraCode-Bold.ttf");
const face_emoji_ttf = @embedFile("font/res/NotoColorEmoji.ttf");
const face_emoji_text_ttf = @embedFile("font/res/NotoEmoji-Regular.ttf");

View File

@ -60,6 +60,8 @@ pub fn parse(comptime T: type, alloc: Allocator, dst: *T, iter: anytype) !void {
try parseIntoField(T, arena_alloc, dst, key, value);
}
}
if (@hasDecl(T, "finalize")) try dst.finalize();
}
/// Parse a single key/value pair into the destination type T.
@ -110,15 +112,28 @@ fn parseIntoField(
// No parseCLI, magic the value based on the type
@field(dst, field.name) = switch (Field) {
[]const u8 => if (value) |slice| value: {
[]const u8 => value: {
const slice = value orelse return error.ValueRequired;
const buf = try alloc.alloc(u8, slice.len);
mem.copy(u8, buf, slice);
break :value buf;
} else return error.ValueRequired,
},
[:0]const u8 => value: {
const slice = value orelse return error.ValueRequired;
const buf = try alloc.allocSentinel(u8, slice.len, 0);
mem.copy(u8, buf, slice);
buf[slice.len] = 0;
break :value buf;
},
bool => try parseBool(value orelse "t"),
u8 => try std.fmt.parseInt(u8, value orelse return error.ValueRequired, 0),
u8 => try std.fmt.parseInt(
u8,
value orelse return error.ValueRequired,
0,
),
else => unreachable,
};
@ -180,6 +195,28 @@ test "parse: simple" {
try testing.expect(!data.@"b-f");
}
test "parse: finalize" {
const testing = std.testing;
var data: struct {
a: []const u8 = "",
_arena: ?ArenaAllocator = null,
pub fn finalize(self: *@This()) !void {
self.a = "YO";
}
} = .{};
defer if (data._arena) |arena| arena.deinit();
var iter = try std.process.ArgIteratorGeneral(.{}).init(
testing.allocator,
"--a=42",
);
defer iter.deinit();
try parse(@TypeOf(data), testing.allocator, &data, &iter);
try testing.expectEqualStrings("YO", data.a);
}
test "parseIntoField: string" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
@ -194,6 +231,21 @@ test "parseIntoField: string" {
try testing.expectEqualStrings("42", data.a);
}
test "parseIntoField: sentinel string" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
var data: struct {
a: [:0]const u8,
} = undefined;
try parseIntoField(@TypeOf(data), alloc, &data, "a", "42");
try testing.expectEqualStrings("42", data.a);
try testing.expectEqual(@as(u8, 0), data.a[data.a.len]);
}
test "parseIntoField: bool" {
const testing = std.testing;
var arena = ArenaAllocator.init(testing.allocator);

View File

@ -6,6 +6,12 @@ const inputpkg = @import("input.zig");
/// Config is the main config struct. These fields map directly to the
/// CLI flag names hence we use a lot of `@""` syntax to support hyphens.
pub const Config = struct {
/// The font families to use.
@"font-family": ?[:0]const u8 = null,
@"font-family-bold": ?[:0]const u8 = null,
@"font-family-italic": ?[:0]const u8 = null,
@"font-family-bold-italic": ?[:0]const u8 = null,
/// Font size in points
@"font-size": u8 = 12,
@ -116,6 +122,25 @@ pub const Config = struct {
return result;
}
pub fn finalize(self: *Config) !void {
// If we have a font-family set and don't set the others, default
// the others to the font family. This way, if someone does
// --font-family=foo, then we try to get the stylized versions of
// "foo" as well.
if (self.@"font-family") |family| {
const fields = &[_][]const u8{
"font-family-bold",
"font-family-italic",
"font-family-bold-italic",
};
inline for (fields) |field| {
if (@field(self, field) == null) {
@field(self, field) = family;
}
}
}
}
};
/// Color represents a color using RGB.

188
src/font/DeferredFace.zig Normal file
View File

@ -0,0 +1,188 @@
//! 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,
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;
}
/// 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) ![:0]const u8 {
if (options.fontconfig) {
if (self.fc) |fc|
return (try fc.pattern.get(.fullname, 0)).string;
}
return "TODO: built-in font names";
}
/// Load the deferred font face. This does nothing if the face is loaded.
pub fn load(
self: *DeferredFace,
lib: Library,
size: Face.DesiredSize,
) !void {
// No-op if we already loaded
if (self.face != null) return;
if (options.fontconfig) {
try self.loadFontconfig(lib, size);
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,
size: Face.DesiredSize,
) !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, 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());
// Verify we can get the name
const n = try def.name();
try testing.expect(n.len > 0);
// Load it and verify it works
try def.load(lib, .{ .points = 12 });
try testing.expect(def.hasCodepoint(' ', null));
try testing.expect(def.face.?.glyphIndex(' ') != null);
}

View File

@ -50,6 +50,23 @@ pub const DesiredSize = struct {
}
};
/// Initialize a new font face with the given source in-memory.
pub fn initFile(lib: Library, path: [:0]const u8, index: i32, size: DesiredSize) !Face {
const face = try lib.lib.initFace(path, index);
errdefer face.deinit();
try face.selectCharmap(.unicode);
try setSize_(face, size);
const hb_font = try harfbuzz.freetype.createFont(face.handle);
errdefer hb_font.destroy();
return Face{
.face = face,
.hb_font = hb_font,
.presentation = if (face.hasColor()) .emoji else .text,
};
}
/// Initialize a new font face with the given source in-memory.
pub fn init(lib: Library, source: [:0]const u8, size: DesiredSize) !Face {
const face = try lib.lib.initMemoryFace(source, 0);

View File

@ -11,11 +11,13 @@ const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const Atlas = @import("../Atlas.zig");
const DeferredFace = @import("main.zig").DeferredFace;
const Face = @import("main.zig").Face;
const Library = @import("main.zig").Library;
const Glyph = @import("main.zig").Glyph;
const Style = @import("main.zig").Style;
const Presentation = @import("main.zig").Presentation;
const options = @import("main.zig").options;
const log = std.log.scoped(.font_group);
@ -24,14 +26,24 @@ const log = std.log.scoped(.font_group);
// usually only one font group for the entire process so this isn't the
// most important memory efficiency we can look for. This is totally opaque
// to the user so we can change this later.
const StyleArray = std.EnumArray(Style, std.ArrayListUnmanaged(Face));
const StyleArray = std.EnumArray(Style, std.ArrayListUnmanaged(DeferredFace));
/// The library being used for all the faces.
lib: Library,
/// The desired font size. All fonts in a group must share the same size.
size: Face.DesiredSize,
/// The available faces we have. This shouldn't be modified manually.
/// Instead, use the functions available on Group.
faces: StyleArray,
pub fn init(alloc: Allocator) !Group {
var result = Group{ .faces = undefined };
pub fn init(
alloc: Allocator,
lib: Library,
size: Face.DesiredSize,
) !Group {
var result = Group{ .lib = lib, .size = size, .faces = undefined };
// Initialize all our styles to initially sized lists.
var i: usize = 0;
@ -57,7 +69,7 @@ pub fn deinit(self: *Group, alloc: Allocator) void {
///
/// The group takes ownership of the face. The face will be deallocated when
/// the group is deallocated.
pub fn addFace(self: *Group, alloc: Allocator, style: Style, face: Face) !void {
pub fn addFace(self: *Group, alloc: Allocator, style: Style, face: DeferredFace) !void {
try self.faces.getPtr(style).append(alloc, face);
}
@ -110,12 +122,8 @@ pub fn indexForCodepoint(
}
fn indexForCodepointExact(self: Group, cp: u32, style: Style, p: ?Presentation) ?FontIndex {
for (self.faces.get(style).items) |face, i| {
// If the presentation is null, we allow the first presentation we
// can find. Otherwise, we check for the specific one requested.
if (p != null and face.presentation != p.?) continue;
if (face.glyphIndex(cp) != null) {
for (self.faces.get(style).items) |deferred, i| {
if (deferred.hasCodepoint(cp, p)) {
return FontIndex{
.style = style,
.idx = @intCast(FontIndex.IndexInt, i),
@ -128,8 +136,10 @@ fn indexForCodepointExact(self: Group, cp: u32, style: Style, p: ?Presentation)
}
/// Return the Face represented by a given FontIndex.
pub fn faceFromIndex(self: Group, index: FontIndex) Face {
return self.faces.get(index.style).items[@intCast(usize, index.idx)];
pub fn faceFromIndex(self: Group, index: FontIndex) !Face {
const deferred = &self.faces.get(index.style).items[@intCast(usize, index.idx)];
try deferred.load(self.lib, self.size);
return deferred.face.?;
}
/// Render a glyph by glyph index into the given font atlas and return
@ -150,8 +160,9 @@ pub fn renderGlyph(
index: FontIndex,
glyph_index: u32,
) !Glyph {
const face = self.faces.get(index.style).items[@intCast(usize, index.idx)];
return try face.renderGlyph(alloc, atlas, glyph_index);
const face = &self.faces.get(index.style).items[@intCast(usize, index.idx)];
try face.load(self.lib, self.size);
return try face.face.?.renderGlyph(alloc, atlas, glyph_index);
}
test {
@ -167,12 +178,12 @@ test {
var lib = try Library.init();
defer lib.deinit();
var group = try init(alloc);
var group = try init(alloc, lib, .{ .points = 12 });
defer group.deinit(alloc);
try group.addFace(alloc, .regular, try Face.init(lib, testFont, .{ .points = 12 }));
try group.addFace(alloc, .regular, try Face.init(lib, testEmoji, .{ .points = 12 }));
try group.addFace(alloc, .regular, try Face.init(lib, testEmojiText, .{ .points = 12 }));
try group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testFont, .{ .points = 12 })));
try group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testEmoji, .{ .points = 12 })));
try group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testEmojiText, .{ .points = 12 })));
// Should find all visible ASCII
var i: u32 = 32;
@ -182,7 +193,7 @@ test {
try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx);
// Render it
const face = group.faceFromIndex(idx);
const face = try group.faceFromIndex(idx);
const glyph_index = face.glyphIndex(i).?;
_ = try group.renderGlyph(
alloc,
@ -211,3 +222,43 @@ test {
try testing.expectEqual(@as(FontIndex.IndexInt, 1), idx.idx);
}
}
test {
if (!options.fontconfig) return error.SkipZigTest;
const testing = std.testing;
const alloc = testing.allocator;
const Discover = @import("main.zig").Discover;
// Search for fonts
var fc = Discover.init();
var it = try fc.discover(.{ .family = "monospace", .size = 12 });
defer it.deinit();
// Initialize the group with the deferred face
var lib = try Library.init();
defer lib.deinit();
var group = try init(alloc, lib, .{ .points = 12 });
defer group.deinit(alloc);
try group.addFace(alloc, .regular, (try it.next()).?);
// Should find all visible ASCII
var atlas_greyscale = try Atlas.init(alloc, 512, .greyscale);
defer atlas_greyscale.deinit(alloc);
var i: u32 = 32;
while (i < 127) : (i += 1) {
const idx = group.indexForCodepoint(i, .regular, null).?;
try testing.expectEqual(Style.regular, idx.style);
try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx);
// Render it
const face = try group.faceFromIndex(idx);
const glyph_index = face.glyphIndex(i).?;
_ = try group.renderGlyph(
alloc,
&atlas_greyscale,
idx,
glyph_index,
);
}
}

View File

@ -7,6 +7,7 @@ const Allocator = std.mem.Allocator;
const Atlas = @import("../Atlas.zig");
const Face = @import("main.zig").Face;
const DeferredFace = @import("main.zig").DeferredFace;
const Library = @import("main.zig").Library;
const Glyph = @import("main.zig").Glyph;
const Style = @import("main.zig").Style;
@ -93,7 +94,7 @@ pub fn metrics(self: *GroupCache, alloc: Allocator) !Metrics {
var i: u32 = 32;
while (i <= 126) : (i += 1) {
const index = (try self.indexForCodepoint(alloc, i, .regular, .text)).?;
const face = self.group.faceFromIndex(index);
const face = try self.group.faceFromIndex(index);
const glyph_index = face.glyphIndex(i).?;
const glyph = try self.renderGlyph(alloc, index, glyph_index);
if (glyph.advance_x > cell_width) {
@ -109,7 +110,7 @@ pub fn metrics(self: *GroupCache, alloc: Allocator) !Metrics {
const cell_height: f32 = cell_height: {
// Get the '_' char for height
const index = (try self.indexForCodepoint(alloc, '_', .regular, .text)).?;
const face = self.group.faceFromIndex(index);
const face = try self.group.faceFromIndex(index);
const glyph_index = face.glyphIndex('_').?;
const glyph = try self.renderGlyph(alloc, index, glyph_index);
@ -129,7 +130,7 @@ pub fn metrics(self: *GroupCache, alloc: Allocator) !Metrics {
};
const cell_baseline = cell_baseline: {
const face = self.group.faces.get(.regular).items[0];
const face = self.group.faces.get(.regular).items[0].face.?;
break :cell_baseline cell_height - @intToFloat(
f32,
face.unitsToPxY(face.face.handle.*.ascender),
@ -178,7 +179,7 @@ pub fn renderGlyph(
if (gop.found_existing) return gop.value_ptr.*;
// Uncached, render it
const face = self.group.faceFromIndex(index);
const face = try self.group.faceFromIndex(index);
const atlas: *Atlas = if (face.hasColor()) &self.atlas_color else &self.atlas_greyscale;
const glyph = self.group.renderGlyph(
alloc,
@ -217,11 +218,19 @@ test {
var lib = try Library.init();
defer lib.deinit();
var cache = try init(alloc, try Group.init(alloc));
var cache = try init(alloc, try Group.init(
alloc,
lib,
.{ .points = 12 },
));
defer cache.deinit(alloc);
// Setup group
try cache.group.addFace(alloc, .regular, try Face.init(lib, testFont, .{ .points = 12 }));
try cache.group.addFace(
alloc,
.regular,
DeferredFace.initLoaded(try Face.init(lib, testFont, .{ .points = 12 })),
);
const group = cache.group;
// Visible ASCII. Do it twice to verify cache.
@ -232,7 +241,7 @@ test {
try testing.expectEqual(@as(Group.FontIndex.IndexInt, 0), idx.idx);
// Render
const face = cache.group.faceFromIndex(idx);
const face = try cache.group.faceFromIndex(idx);
const glyph_index = face.glyphIndex(i).?;
_ = try cache.renderGlyph(
alloc,
@ -253,7 +262,7 @@ test {
try testing.expectEqual(@as(Group.FontIndex.IndexInt, 0), idx.idx);
// Render
const face = group.faceFromIndex(idx);
const face = try group.faceFromIndex(idx);
const glyph_index = face.glyphIndex(i).?;
_ = try cache.renderGlyph(
alloc,

View File

@ -8,6 +8,7 @@ const harfbuzz = @import("harfbuzz");
const trace = @import("tracy").trace;
const Atlas = @import("../Atlas.zig");
const Face = @import("main.zig").Face;
const DeferredFace = @import("main.zig").DeferredFace;
const Group = @import("main.zig").Group;
const GroupCache = @import("main.zig").GroupCache;
const Library = @import("main.zig").Library;
@ -61,7 +62,7 @@ pub fn shape(self: *Shaper, run: TextRun) ![]Cell {
harfbuzz.Feature.fromString("liga").?,
};
const face = run.group.group.faceFromIndex(run.font_index);
const face = try run.group.group.faceFromIndex(run.font_index);
harfbuzz.shape(face.hb_font, self.hb_buf, hb_feats);
// If our buffer is empty, we short-circuit the rest of the work
@ -595,13 +596,17 @@ fn testShaper(alloc: Allocator) !TestShaper {
var cache_ptr = try alloc.create(GroupCache);
errdefer alloc.destroy(cache_ptr);
cache_ptr.* = try GroupCache.init(alloc, try Group.init(alloc));
cache_ptr.* = try GroupCache.init(alloc, try Group.init(
alloc,
lib,
.{ .points = 12 },
));
errdefer cache_ptr.*.deinit(alloc);
// Setup group
try cache_ptr.group.addFace(alloc, .regular, try Face.init(lib, testFont, .{ .points = 12 }));
try cache_ptr.group.addFace(alloc, .regular, try Face.init(lib, testEmoji, .{ .points = 12 }));
try cache_ptr.group.addFace(alloc, .regular, try Face.init(lib, testEmojiText, .{ .points = 12 }));
try cache_ptr.group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testFont, .{ .points = 12 })));
try cache_ptr.group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testEmoji, .{ .points = 12 })));
try cache_ptr.group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testEmojiText, .{ .points = 12 })));
var cell_buf = try alloc.alloc(Cell, 80);
errdefer alloc.free(cell_buf);

148
src/font/discovery.zig Normal file
View File

@ -0,0 +1,148 @@
const std = @import("std");
const assert = std.debug.assert;
const fontconfig = @import("fontconfig");
const options = @import("main.zig").options;
const DeferredFace = @import("main.zig").DeferredFace;
const log = std.log.named(.discovery);
/// Discover implementation for the compile options.
pub const Discover = if (options.fontconfig) Fontconfig else void;
/// Descriptor is used to search for fonts. The only required field
/// is "family". The rest are ignored unless they're set to a non-zero
/// value.
pub const Descriptor = struct {
/// Font family to search for. This can be a fully qualified font
/// name such as "Fira Code", "monospace", "serif", etc. Memory is
/// owned by the caller and should be freed when this descriptor
/// is no longer in use. The discovery structs will never store the
/// descriptor.
///
/// On systems that use fontconfig (Linux), this can be a full
/// fontconfig pattern, such as "Fira Code-14:bold".
family: [:0]const u8,
/// Font size in points that the font should support. For conversion
/// to pixels, we will use 72 DPI for Mac and 96 DPI for everything else.
/// (If pixel conversion is necessary, i.e. emoji fonts)
size: u16,
/// True if we want to search specifically for a font that supports
/// bold, italic, or both.
bold: bool = false,
italic: bool = false,
/// Convert to Fontconfig pattern to use for lookup. The pattern does
/// not have defaults filled/substituted (Fontconfig thing) so callers
/// must still do this.
pub fn toFcPattern(self: Descriptor) *fontconfig.Pattern {
const pat = fontconfig.Pattern.create();
assert(pat.add(.family, .{ .string = self.family }, false));
if (self.size > 0) assert(pat.add(
.size,
.{ .integer = self.size },
false,
));
if (self.bold) assert(pat.add(
.weight,
.{ .integer = @enumToInt(fontconfig.Weight.bold) },
false,
));
if (self.italic) assert(pat.add(
.slant,
.{ .integer = @enumToInt(fontconfig.Slant.italic) },
false,
));
return pat;
}
};
pub const Fontconfig = struct {
fc_config: *fontconfig.Config,
pub fn init() Fontconfig {
// safe to call multiple times and concurrently
_ = fontconfig.init();
return .{ .fc_config = fontconfig.initLoadConfigAndFonts() };
}
pub fn deinit(self: *Fontconfig) void {
_ = self;
}
/// Discover fonts from a descriptor. This returns an iterator that can
/// be used to build up the deferred fonts.
pub fn discover(self: *Fontconfig, desc: Descriptor) !DiscoverIterator {
// Build our pattern that we'll search for
const pat = desc.toFcPattern();
errdefer pat.destroy();
assert(self.fc_config.substituteWithPat(pat, .pattern));
pat.defaultSubstitute();
// Search
const res = self.fc_config.fontSort(pat, true, null);
if (res.result != .match) return error.FontConfigFailed;
errdefer res.fs.destroy();
return DiscoverIterator{
.config = self.fc_config,
.pattern = pat,
.set = res.fs,
.fonts = res.fs.fonts(),
.i = 0,
};
}
pub const DiscoverIterator = struct {
config: *fontconfig.Config,
pattern: *fontconfig.Pattern,
set: *fontconfig.FontSet,
fonts: []*fontconfig.Pattern,
i: usize,
pub fn deinit(self: *DiscoverIterator) void {
self.set.destroy();
self.pattern.destroy();
self.* = undefined;
}
pub fn next(self: *DiscoverIterator) fontconfig.Error!?DeferredFace {
if (self.i >= self.fonts.len) return null;
// Get the copied pattern from our fontset that has the
// attributes configured for rendering.
const font_pattern = try self.config.fontRenderPrepare(
self.pattern,
self.fonts[self.i],
);
errdefer font_pattern.destroy();
// Increment after we return
defer self.i += 1;
return DeferredFace{
.face = null,
.fc = .{
.pattern = font_pattern,
.charset = (try font_pattern.get(.charset, 0)).char_set,
.langset = (try font_pattern.get(.lang, 0)).lang_set,
},
};
}
};
};
test {
if (!options.fontconfig) return error.SkipZigTest;
const testing = std.testing;
var fc = Fontconfig.init();
var it = try fc.discover(.{ .family = "monospace", .size = 12 });
defer it.deinit();
while (try it.next()) |face| {
try testing.expect(!face.loaded());
}
}

View File

@ -1,11 +1,23 @@
const std = @import("std");
const build_options = @import("build_options");
pub const discovery = @import("discovery.zig");
pub const DeferredFace = @import("DeferredFace.zig");
pub const Face = @import("Face.zig");
pub const Group = @import("Group.zig");
pub const GroupCache = @import("GroupCache.zig");
pub const Glyph = @import("Glyph.zig");
pub const Library = @import("Library.zig");
pub const Shaper = @import("Shaper.zig");
pub const Descriptor = discovery.Descriptor;
pub const Discover = discovery.Discover;
/// Build options
pub const options: struct {
fontconfig: bool = false,
} = .{
.fontconfig = build_options.fontconfig,
};
/// The styles that a family can take.
pub const Style = enum(u3) {

View File

@ -16,7 +16,7 @@ const log = std.log.scoped(.main);
pub fn main() !void {
// Output some debug information right away
log.info("dependency harfbuzz={s}", .{harfbuzz.versionString()});
if (builtin.os.tag == .linux) {
if (options.fontconfig) {
log.info("dependency fontconfig={d}", .{fontconfig.version()});
}