mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-15 00:06:09 +03:00
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:
21
build.zig
21
build.zig
@ -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.
|
// Build options, see the build options help for more info.
|
||||||
var tracy: bool = false;
|
var tracy: bool = false;
|
||||||
|
var enable_fontconfig: bool = false;
|
||||||
|
|
||||||
pub fn build(b: *std.build.Builder) !void {
|
pub fn build(b: *std.build.Builder) !void {
|
||||||
const mode = b.standardReleaseOptions();
|
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)",
|
"Enable Tracy integration (default true in Debug on Linux)",
|
||||||
) orelse (mode == .Debug and target.isLinux());
|
) 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(
|
const static = b.option(
|
||||||
bool,
|
bool,
|
||||||
"static",
|
"static",
|
||||||
@ -49,12 +56,12 @@ pub fn build(b: *std.build.Builder) !void {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const exe = b.addExecutable("ghostty", "src/main.zig");
|
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
|
// Exe
|
||||||
{
|
{
|
||||||
const exe_options = b.addOptions();
|
|
||||||
exe_options.addOption(bool, "tracy_enabled", tracy);
|
|
||||||
|
|
||||||
exe.setTarget(target);
|
exe.setTarget(target);
|
||||||
exe.setBuildMode(mode);
|
exe.setBuildMode(mode);
|
||||||
exe.addOptions("build_options", exe_options);
|
exe.addOptions("build_options", exe_options);
|
||||||
@ -119,6 +126,7 @@ pub fn build(b: *std.build.Builder) !void {
|
|||||||
|
|
||||||
main_test.setTarget(target);
|
main_test.setTarget(target);
|
||||||
try addDeps(b, main_test, true);
|
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 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", .{});
|
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).
|
// we wrote (are in the "pkg/" directory).
|
||||||
for (main_test.packages.items) |pkg_| {
|
for (main_test.packages.items) |pkg_| {
|
||||||
const pkg: std.build.Pkg = 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;
|
if (std.mem.eql(u8, pkg.name, "glfw")) continue;
|
||||||
var test_ = b.addTestSource(pkg.source);
|
var test_ = b.addTestSource(pkg.source);
|
||||||
|
|
||||||
@ -158,7 +167,7 @@ fn addDeps(
|
|||||||
static: bool,
|
static: bool,
|
||||||
) !void {
|
) !void {
|
||||||
// We always need the Zig packages
|
// We always need the Zig packages
|
||||||
step.addPackage(fontconfig.pkg);
|
if (enable_fontconfig) step.addPackage(fontconfig.pkg);
|
||||||
step.addPackage(freetype.pkg);
|
step.addPackage(freetype.pkg);
|
||||||
step.addPackage(harfbuzz.pkg);
|
step.addPackage(harfbuzz.pkg);
|
||||||
step.addPackage(glfw.pkg);
|
step.addPackage(glfw.pkg);
|
||||||
@ -195,7 +204,7 @@ fn addDeps(
|
|||||||
step.linkSystemLibrary("libuv");
|
step.linkSystemLibrary("libuv");
|
||||||
step.linkSystemLibrary("zlib");
|
step.linkSystemLibrary("zlib");
|
||||||
|
|
||||||
if (step.target.isLinux()) step.linkSystemLibrary("fontconfig");
|
if (enable_fontconfig) step.linkSystemLibrary("fontconfig");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Other dependencies, we may dynamically link
|
// Other dependencies, we may dynamically link
|
||||||
@ -237,7 +246,7 @@ fn addDeps(
|
|||||||
system_sdk.include(b, libuv_step, .{});
|
system_sdk.include(b, libuv_step, .{});
|
||||||
|
|
||||||
// Only Linux gets fontconfig
|
// Only Linux gets fontconfig
|
||||||
if (step.target.isLinux()) {
|
if (enable_fontconfig) {
|
||||||
// Libxml2
|
// Libxml2
|
||||||
const libxml2_lib = try libxml2.create(
|
const libxml2_lib = try libxml2.create(
|
||||||
b,
|
b,
|
||||||
|
@ -32,6 +32,18 @@ pub fn version(self: Library) Version {
|
|||||||
return v;
|
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.
|
/// 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 {
|
pub fn initMemoryFace(self: Library, data: []const u8, index: i32) Error!Face {
|
||||||
var face: Face = undefined;
|
var face: Face = undefined;
|
||||||
|
@ -89,7 +89,7 @@ pub fn buildFreetype(
|
|||||||
"-DHAVE_UNISTD_H",
|
"-DHAVE_UNISTD_H",
|
||||||
"-DHAVE_FCNTL_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.libpng.enabled) try flags.append("-DFT_CONFIG_OPTION_USE_PNG=1");
|
||||||
if (opt.zlib.enabled) try flags.append("-DFT_CONFIG_OPTION_SYSTEM_ZLIB=1");
|
if (opt.zlib.enabled) try flags.append("-DFT_CONFIG_OPTION_SYSTEM_ZLIB=1");
|
||||||
|
55
src/Grid.zig
55
src/Grid.zig
@ -58,8 +58,7 @@ texture: gl.Texture,
|
|||||||
texture_color: gl.Texture,
|
texture_color: gl.Texture,
|
||||||
|
|
||||||
/// The font structures.
|
/// The font structures.
|
||||||
font_lib: font.Library,
|
font_group: *font.GroupCache,
|
||||||
font_group: font.GroupCache,
|
|
||||||
font_shaper: font.Shaper,
|
font_shaper: font.Shaper,
|
||||||
|
|
||||||
/// Whether the cursor is visible or not. This is used to control cursor
|
/// Whether the cursor is visible or not. This is used to control cursor
|
||||||
@ -152,45 +151,7 @@ const GPUCellMode = enum(u8) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(
|
pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Grid {
|
||||||
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);
|
|
||||||
|
|
||||||
// Create the initial font shaper
|
// Create the initial font shaper
|
||||||
var shape_buf = try alloc.alloc(font.Shaper.Cell, 1);
|
var shape_buf = try alloc.alloc(font.Shaper.Cell, 1);
|
||||||
errdefer alloc.free(shape_buf);
|
errdefer alloc.free(shape_buf);
|
||||||
@ -326,7 +287,6 @@ pub fn init(
|
|||||||
.vbo = vbo,
|
.vbo = vbo,
|
||||||
.texture = tex,
|
.texture = tex,
|
||||||
.texture_color = tex_color,
|
.texture_color = tex_color,
|
||||||
.font_lib = font_lib,
|
|
||||||
.font_group = font_group,
|
.font_group = font_group,
|
||||||
.font_shaper = shaper,
|
.font_shaper = shaper,
|
||||||
.cursor_visible = true,
|
.cursor_visible = true,
|
||||||
@ -339,8 +299,6 @@ pub fn init(
|
|||||||
pub fn deinit(self: *Grid) void {
|
pub fn deinit(self: *Grid) void {
|
||||||
self.font_shaper.deinit();
|
self.font_shaper.deinit();
|
||||||
self.alloc.free(self.font_shaper.cell_buf);
|
self.alloc.free(self.font_shaper.cell_buf);
|
||||||
self.font_group.deinit(self.alloc);
|
|
||||||
self.font_lib.deinit();
|
|
||||||
|
|
||||||
self.texture.destroy();
|
self.texture.destroy();
|
||||||
self.texture_color.destroy();
|
self.texture_color.destroy();
|
||||||
@ -423,7 +381,7 @@ pub fn rebuildCells(self: *Grid, term: *Terminal) !void {
|
|||||||
const start = self.cells.items.len;
|
const start = self.cells.items.len;
|
||||||
|
|
||||||
// Split our row into runs and shape each one.
|
// 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| {
|
while (try iter.next(self.alloc)) |run| {
|
||||||
for (try self.font_shaper.shape(run)) |shaper_cell| {
|
for (try self.font_shaper.shape(run)) |shaper_cell| {
|
||||||
assert(try self.updateCell(
|
assert(try self.updateCell(
|
||||||
@ -610,7 +568,7 @@ pub fn updateCell(
|
|||||||
// If the cell has a character, draw it
|
// If the cell has a character, draw it
|
||||||
if (cell.char > 0) {
|
if (cell.char > 0) {
|
||||||
// Render
|
// 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(
|
const glyph = try self.font_group.renderGlyph(
|
||||||
self.alloc,
|
self.alloc,
|
||||||
shaper_run.font_index,
|
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, 3), grid.columns);
|
||||||
try testing.expectEqual(@as(GridSize.Unit, 2), grid.rows);
|
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");
|
|
||||||
|
115
src/Window.zig
115
src/Window.zig
@ -35,6 +35,10 @@ const WRITE_REQ_PREALLOC = std.math.pow(usize, 2, 5);
|
|||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
alloc_io_arena: std.heap.ArenaAllocator,
|
alloc_io_arena: std.heap.ArenaAllocator,
|
||||||
|
|
||||||
|
/// The font structures
|
||||||
|
font_lib: font.Library,
|
||||||
|
font_group: *font.GroupCache,
|
||||||
|
|
||||||
/// The glfw window handle.
|
/// The glfw window handle.
|
||||||
window: glfw.Window,
|
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.glEnable(gl.c.GL_BLEND);
|
||||||
gl.c.glBlendFunc(gl.c.GL_SRC_ALPHA, gl.c.GL_ONE_MINUS_SRC_ALPHA);
|
gl.c.glBlendFunc(gl.c.GL_SRC_ALPHA, gl.c.GL_ONE_MINUS_SRC_ALPHA);
|
||||||
|
|
||||||
// Create our terminal grid with the initial window size
|
// The font size we desire along with the DPI determiend for the window
|
||||||
const window_size = try window.getSize();
|
const font_size: font.Face.DesiredSize = .{
|
||||||
var grid = try Grid.init(alloc, .{
|
|
||||||
.points = config.@"font-size",
|
.points = config.@"font-size",
|
||||||
.xdpi = @floatToInt(u16, x_dpi),
|
.xdpi = @floatToInt(u16, x_dpi),
|
||||||
.ydpi = @floatToInt(u16, y_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 });
|
try grid.setScreenSize(.{ .width = window_size.width, .height = window_size.height });
|
||||||
grid.background = .{
|
grid.background = .{
|
||||||
.r = config.background.r,
|
.r = config.background.r,
|
||||||
@ -333,6 +432,8 @@ pub fn create(alloc: Allocator, loop: libuv.Loop, config: *const Config) !*Windo
|
|||||||
self.* = .{
|
self.* = .{
|
||||||
.alloc = alloc,
|
.alloc = alloc,
|
||||||
.alloc_io_arena = io_arena,
|
.alloc_io_arena = io_arena,
|
||||||
|
.font_lib = font_lib,
|
||||||
|
.font_group = font_group,
|
||||||
.window = window,
|
.window = window,
|
||||||
.cursor = cursor,
|
.cursor = cursor,
|
||||||
.focused = false,
|
.focused = false,
|
||||||
@ -419,6 +520,9 @@ pub fn destroy(self: *Window) void {
|
|||||||
// windows using it to the default.
|
// windows using it to the default.
|
||||||
self.cursor.destroy();
|
self.cursor.destroy();
|
||||||
|
|
||||||
|
self.font_group.deinit(self.alloc);
|
||||||
|
self.font_lib.deinit();
|
||||||
|
|
||||||
self.alloc_io_arena.deinit();
|
self.alloc_io_arena.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1715,3 +1819,8 @@ pub fn invokeCharset(
|
|||||||
) !void {
|
) !void {
|
||||||
self.terminal.invokeCharset(active, slot, single);
|
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");
|
||||||
|
@ -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);
|
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.
|
/// 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
|
// No parseCLI, magic the value based on the type
|
||||||
@field(dst, field.name) = switch (Field) {
|
@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);
|
const buf = try alloc.alloc(u8, slice.len);
|
||||||
mem.copy(u8, buf, slice);
|
mem.copy(u8, buf, slice);
|
||||||
break :value buf;
|
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"),
|
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,
|
else => unreachable,
|
||||||
};
|
};
|
||||||
@ -180,6 +195,28 @@ test "parse: simple" {
|
|||||||
try testing.expect(!data.@"b-f");
|
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" {
|
test "parseIntoField: string" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
var arena = ArenaAllocator.init(testing.allocator);
|
var arena = ArenaAllocator.init(testing.allocator);
|
||||||
@ -194,6 +231,21 @@ test "parseIntoField: string" {
|
|||||||
try testing.expectEqualStrings("42", data.a);
|
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" {
|
test "parseIntoField: bool" {
|
||||||
const testing = std.testing;
|
const testing = std.testing;
|
||||||
var arena = ArenaAllocator.init(testing.allocator);
|
var arena = ArenaAllocator.init(testing.allocator);
|
||||||
|
@ -6,6 +6,12 @@ const inputpkg = @import("input.zig");
|
|||||||
/// Config is the main config struct. These fields map directly to the
|
/// 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.
|
/// CLI flag names hence we use a lot of `@""` syntax to support hyphens.
|
||||||
pub const Config = struct {
|
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 in points
|
||||||
@"font-size": u8 = 12,
|
@"font-size": u8 = 12,
|
||||||
|
|
||||||
@ -116,6 +122,25 @@ pub const Config = struct {
|
|||||||
|
|
||||||
return result;
|
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.
|
/// Color represents a color using RGB.
|
||||||
|
188
src/font/DeferredFace.zig
Normal file
188
src/font/DeferredFace.zig
Normal 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);
|
||||||
|
}
|
@ -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.
|
/// Initialize a new font face with the given source in-memory.
|
||||||
pub fn init(lib: Library, source: [:0]const u8, size: DesiredSize) !Face {
|
pub fn init(lib: Library, source: [:0]const u8, size: DesiredSize) !Face {
|
||||||
const face = try lib.lib.initMemoryFace(source, 0);
|
const face = try lib.lib.initMemoryFace(source, 0);
|
||||||
|
@ -11,11 +11,13 @@ const assert = std.debug.assert;
|
|||||||
const Allocator = std.mem.Allocator;
|
const Allocator = std.mem.Allocator;
|
||||||
|
|
||||||
const Atlas = @import("../Atlas.zig");
|
const Atlas = @import("../Atlas.zig");
|
||||||
|
const DeferredFace = @import("main.zig").DeferredFace;
|
||||||
const Face = @import("main.zig").Face;
|
const Face = @import("main.zig").Face;
|
||||||
const Library = @import("main.zig").Library;
|
const Library = @import("main.zig").Library;
|
||||||
const Glyph = @import("main.zig").Glyph;
|
const Glyph = @import("main.zig").Glyph;
|
||||||
const Style = @import("main.zig").Style;
|
const Style = @import("main.zig").Style;
|
||||||
const Presentation = @import("main.zig").Presentation;
|
const Presentation = @import("main.zig").Presentation;
|
||||||
|
const options = @import("main.zig").options;
|
||||||
|
|
||||||
const log = std.log.scoped(.font_group);
|
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
|
// 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
|
// most important memory efficiency we can look for. This is totally opaque
|
||||||
// to the user so we can change this later.
|
// 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.
|
/// The available faces we have. This shouldn't be modified manually.
|
||||||
/// Instead, use the functions available on Group.
|
/// Instead, use the functions available on Group.
|
||||||
faces: StyleArray,
|
faces: StyleArray,
|
||||||
|
|
||||||
pub fn init(alloc: Allocator) !Group {
|
pub fn init(
|
||||||
var result = Group{ .faces = undefined };
|
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.
|
// Initialize all our styles to initially sized lists.
|
||||||
var i: usize = 0;
|
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 takes ownership of the face. The face will be deallocated when
|
||||||
/// the group is deallocated.
|
/// 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);
|
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 {
|
fn indexForCodepointExact(self: Group, cp: u32, style: Style, p: ?Presentation) ?FontIndex {
|
||||||
for (self.faces.get(style).items) |face, i| {
|
for (self.faces.get(style).items) |deferred, i| {
|
||||||
// If the presentation is null, we allow the first presentation we
|
if (deferred.hasCodepoint(cp, p)) {
|
||||||
// can find. Otherwise, we check for the specific one requested.
|
|
||||||
if (p != null and face.presentation != p.?) continue;
|
|
||||||
|
|
||||||
if (face.glyphIndex(cp) != null) {
|
|
||||||
return FontIndex{
|
return FontIndex{
|
||||||
.style = style,
|
.style = style,
|
||||||
.idx = @intCast(FontIndex.IndexInt, i),
|
.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.
|
/// Return the Face represented by a given FontIndex.
|
||||||
pub fn faceFromIndex(self: Group, index: FontIndex) Face {
|
pub fn faceFromIndex(self: Group, index: FontIndex) !Face {
|
||||||
return self.faces.get(index.style).items[@intCast(usize, index.idx)];
|
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
|
/// Render a glyph by glyph index into the given font atlas and return
|
||||||
@ -150,8 +160,9 @@ pub fn renderGlyph(
|
|||||||
index: FontIndex,
|
index: FontIndex,
|
||||||
glyph_index: u32,
|
glyph_index: u32,
|
||||||
) !Glyph {
|
) !Glyph {
|
||||||
const face = self.faces.get(index.style).items[@intCast(usize, index.idx)];
|
const face = &self.faces.get(index.style).items[@intCast(usize, index.idx)];
|
||||||
return try face.renderGlyph(alloc, atlas, glyph_index);
|
try face.load(self.lib, self.size);
|
||||||
|
return try face.face.?.renderGlyph(alloc, atlas, glyph_index);
|
||||||
}
|
}
|
||||||
|
|
||||||
test {
|
test {
|
||||||
@ -167,12 +178,12 @@ test {
|
|||||||
var lib = try Library.init();
|
var lib = try Library.init();
|
||||||
defer lib.deinit();
|
defer lib.deinit();
|
||||||
|
|
||||||
var group = try init(alloc);
|
var group = try init(alloc, lib, .{ .points = 12 });
|
||||||
defer group.deinit(alloc);
|
defer group.deinit(alloc);
|
||||||
|
|
||||||
try group.addFace(alloc, .regular, try Face.init(lib, testFont, .{ .points = 12 }));
|
try group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testFont, .{ .points = 12 })));
|
||||||
try group.addFace(alloc, .regular, try Face.init(lib, testEmoji, .{ .points = 12 }));
|
try group.addFace(alloc, .regular, DeferredFace.initLoaded(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, testEmojiText, .{ .points = 12 })));
|
||||||
|
|
||||||
// Should find all visible ASCII
|
// Should find all visible ASCII
|
||||||
var i: u32 = 32;
|
var i: u32 = 32;
|
||||||
@ -182,7 +193,7 @@ test {
|
|||||||
try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx);
|
try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx);
|
||||||
|
|
||||||
// Render it
|
// Render it
|
||||||
const face = group.faceFromIndex(idx);
|
const face = try group.faceFromIndex(idx);
|
||||||
const glyph_index = face.glyphIndex(i).?;
|
const glyph_index = face.glyphIndex(i).?;
|
||||||
_ = try group.renderGlyph(
|
_ = try group.renderGlyph(
|
||||||
alloc,
|
alloc,
|
||||||
@ -211,3 +222,43 @@ test {
|
|||||||
try testing.expectEqual(@as(FontIndex.IndexInt, 1), idx.idx);
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -7,6 +7,7 @@ const Allocator = std.mem.Allocator;
|
|||||||
|
|
||||||
const Atlas = @import("../Atlas.zig");
|
const Atlas = @import("../Atlas.zig");
|
||||||
const Face = @import("main.zig").Face;
|
const Face = @import("main.zig").Face;
|
||||||
|
const DeferredFace = @import("main.zig").DeferredFace;
|
||||||
const Library = @import("main.zig").Library;
|
const Library = @import("main.zig").Library;
|
||||||
const Glyph = @import("main.zig").Glyph;
|
const Glyph = @import("main.zig").Glyph;
|
||||||
const Style = @import("main.zig").Style;
|
const Style = @import("main.zig").Style;
|
||||||
@ -93,7 +94,7 @@ pub fn metrics(self: *GroupCache, alloc: Allocator) !Metrics {
|
|||||||
var i: u32 = 32;
|
var i: u32 = 32;
|
||||||
while (i <= 126) : (i += 1) {
|
while (i <= 126) : (i += 1) {
|
||||||
const index = (try self.indexForCodepoint(alloc, i, .regular, .text)).?;
|
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_index = face.glyphIndex(i).?;
|
||||||
const glyph = try self.renderGlyph(alloc, index, glyph_index);
|
const glyph = try self.renderGlyph(alloc, index, glyph_index);
|
||||||
if (glyph.advance_x > cell_width) {
|
if (glyph.advance_x > cell_width) {
|
||||||
@ -109,7 +110,7 @@ pub fn metrics(self: *GroupCache, alloc: Allocator) !Metrics {
|
|||||||
const cell_height: f32 = cell_height: {
|
const cell_height: f32 = cell_height: {
|
||||||
// Get the '_' char for height
|
// Get the '_' char for height
|
||||||
const index = (try self.indexForCodepoint(alloc, '_', .regular, .text)).?;
|
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_index = face.glyphIndex('_').?;
|
||||||
const glyph = try self.renderGlyph(alloc, index, glyph_index);
|
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 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(
|
break :cell_baseline cell_height - @intToFloat(
|
||||||
f32,
|
f32,
|
||||||
face.unitsToPxY(face.face.handle.*.ascender),
|
face.unitsToPxY(face.face.handle.*.ascender),
|
||||||
@ -178,7 +179,7 @@ pub fn renderGlyph(
|
|||||||
if (gop.found_existing) return gop.value_ptr.*;
|
if (gop.found_existing) return gop.value_ptr.*;
|
||||||
|
|
||||||
// Uncached, render it
|
// 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 atlas: *Atlas = if (face.hasColor()) &self.atlas_color else &self.atlas_greyscale;
|
||||||
const glyph = self.group.renderGlyph(
|
const glyph = self.group.renderGlyph(
|
||||||
alloc,
|
alloc,
|
||||||
@ -217,11 +218,19 @@ test {
|
|||||||
var lib = try Library.init();
|
var lib = try Library.init();
|
||||||
defer lib.deinit();
|
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);
|
defer cache.deinit(alloc);
|
||||||
|
|
||||||
// Setup group
|
// 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;
|
const group = cache.group;
|
||||||
|
|
||||||
// Visible ASCII. Do it twice to verify cache.
|
// Visible ASCII. Do it twice to verify cache.
|
||||||
@ -232,7 +241,7 @@ test {
|
|||||||
try testing.expectEqual(@as(Group.FontIndex.IndexInt, 0), idx.idx);
|
try testing.expectEqual(@as(Group.FontIndex.IndexInt, 0), idx.idx);
|
||||||
|
|
||||||
// Render
|
// Render
|
||||||
const face = cache.group.faceFromIndex(idx);
|
const face = try cache.group.faceFromIndex(idx);
|
||||||
const glyph_index = face.glyphIndex(i).?;
|
const glyph_index = face.glyphIndex(i).?;
|
||||||
_ = try cache.renderGlyph(
|
_ = try cache.renderGlyph(
|
||||||
alloc,
|
alloc,
|
||||||
@ -253,7 +262,7 @@ test {
|
|||||||
try testing.expectEqual(@as(Group.FontIndex.IndexInt, 0), idx.idx);
|
try testing.expectEqual(@as(Group.FontIndex.IndexInt, 0), idx.idx);
|
||||||
|
|
||||||
// Render
|
// Render
|
||||||
const face = group.faceFromIndex(idx);
|
const face = try group.faceFromIndex(idx);
|
||||||
const glyph_index = face.glyphIndex(i).?;
|
const glyph_index = face.glyphIndex(i).?;
|
||||||
_ = try cache.renderGlyph(
|
_ = try cache.renderGlyph(
|
||||||
alloc,
|
alloc,
|
||||||
|
@ -8,6 +8,7 @@ const harfbuzz = @import("harfbuzz");
|
|||||||
const trace = @import("tracy").trace;
|
const trace = @import("tracy").trace;
|
||||||
const Atlas = @import("../Atlas.zig");
|
const Atlas = @import("../Atlas.zig");
|
||||||
const Face = @import("main.zig").Face;
|
const Face = @import("main.zig").Face;
|
||||||
|
const DeferredFace = @import("main.zig").DeferredFace;
|
||||||
const Group = @import("main.zig").Group;
|
const Group = @import("main.zig").Group;
|
||||||
const GroupCache = @import("main.zig").GroupCache;
|
const GroupCache = @import("main.zig").GroupCache;
|
||||||
const Library = @import("main.zig").Library;
|
const Library = @import("main.zig").Library;
|
||||||
@ -61,7 +62,7 @@ pub fn shape(self: *Shaper, run: TextRun) ![]Cell {
|
|||||||
harfbuzz.Feature.fromString("liga").?,
|
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);
|
harfbuzz.shape(face.hb_font, self.hb_buf, hb_feats);
|
||||||
|
|
||||||
// If our buffer is empty, we short-circuit the rest of the work
|
// 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);
|
var cache_ptr = try alloc.create(GroupCache);
|
||||||
errdefer alloc.destroy(cache_ptr);
|
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);
|
errdefer cache_ptr.*.deinit(alloc);
|
||||||
|
|
||||||
// Setup group
|
// Setup group
|
||||||
try cache_ptr.group.addFace(alloc, .regular, try Face.init(lib, testFont, .{ .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, try Face.init(lib, testEmoji, .{ .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, try Face.init(lib, testEmojiText, .{ .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);
|
var cell_buf = try alloc.alloc(Cell, 80);
|
||||||
errdefer alloc.free(cell_buf);
|
errdefer alloc.free(cell_buf);
|
||||||
|
148
src/font/discovery.zig
Normal file
148
src/font/discovery.zig
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,23 @@
|
|||||||
const std = @import("std");
|
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 Face = @import("Face.zig");
|
||||||
pub const Group = @import("Group.zig");
|
pub const Group = @import("Group.zig");
|
||||||
pub const GroupCache = @import("GroupCache.zig");
|
pub const GroupCache = @import("GroupCache.zig");
|
||||||
pub const Glyph = @import("Glyph.zig");
|
pub const Glyph = @import("Glyph.zig");
|
||||||
pub const Library = @import("Library.zig");
|
pub const Library = @import("Library.zig");
|
||||||
pub const Shaper = @import("Shaper.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.
|
/// The styles that a family can take.
|
||||||
pub const Style = enum(u3) {
|
pub const Style = enum(u3) {
|
||||||
|
@ -16,7 +16,7 @@ const log = std.log.scoped(.main);
|
|||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
// Output some debug information right away
|
// Output some debug information right away
|
||||||
log.info("dependency harfbuzz={s}", .{harfbuzz.versionString()});
|
log.info("dependency harfbuzz={s}", .{harfbuzz.versionString()});
|
||||||
if (builtin.os.tag == .linux) {
|
if (options.fontconfig) {
|
||||||
log.info("dependency fontconfig={d}", .{fontconfig.version()});
|
log.info("dependency fontconfig={d}", .{fontconfig.version()});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user