From 141182aa1371f356b9c6115fc05eba64ff75894c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 16 Sep 2022 15:06:00 -0700 Subject: [PATCH 01/14] start adding fontconfig conditional compilation --- build.zig | 21 ++++++++--- src/font/DeferredFace.zig | 37 +++++++++++++++++++ src/font/Group.zig | 15 +++++--- src/font/GroupCache.zig | 2 +- src/font/discovery.zig | 77 +++++++++++++++++++++++++++++++++++++++ src/font/main.zig | 12 ++++++ src/main.zig | 2 +- 7 files changed, 153 insertions(+), 13 deletions(-) create mode 100644 src/font/DeferredFace.zig create mode 100644 src/font/discovery.zig diff --git a/build.zig b/build.zig index 206e028ab..30f038a16 100644 --- a/build.zig +++ b/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. 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, diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig new file mode 100644 index 000000000..0a3f974ff --- /dev/null +++ b/src/font/DeferredFace.zig @@ -0,0 +1,37 @@ +//! 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 Face = @import("main.zig").Face; + +/// The loaded face (once loaded). +face: ?Face = null, + +/// Fontconfig +fc: if (options.fontconfig) Fontconfig else void = undefined, + +/// Fontconfig specific data. This is only present if building with fontconfig. +pub const Fontconfig = struct { + pattern: *fontconfig.Pattern, + charset: *fontconfig.CharSet, + langset: *fontconfig.LangSet, +}; + +pub fn deinit(self: *DeferredFace) void { + if (self.face) |*face| face.deinit(); + + self.* = undefined; +} + +/// Returns true if the face has been loaded. +pub inline fn loaded(self: DeferredFace) bool { + return self.face != null; +} diff --git a/src/font/Group.zig b/src/font/Group.zig index dcc451450..80d6e72e0 100644 --- a/src/font/Group.zig +++ b/src/font/Group.zig @@ -11,6 +11,7 @@ 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; @@ -24,7 +25,7 @@ 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 available faces we have. This shouldn't be modified manually. /// Instead, use the functions available on Group. @@ -58,7 +59,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 { - try self.faces.getPtr(style).append(alloc, face); + try self.faces.getPtr(style).append(alloc, .{ .face = face }); } /// This represents a specific font in the group. @@ -110,7 +111,9 @@ pub fn indexForCodepoint( } 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| { + const face = deferred.face.?; + // 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; @@ -129,7 +132,8 @@ 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)]; + const deferred = self.faces.get(index.style).items[@intCast(usize, index.idx)]; + return deferred.face.?; } /// Render a glyph by glyph index into the given font atlas and return @@ -151,7 +155,8 @@ pub fn renderGlyph( glyph_index: u32, ) !Glyph { const face = self.faces.get(index.style).items[@intCast(usize, index.idx)]; - return try face.renderGlyph(alloc, atlas, glyph_index); + assert(face.loaded()); + return try face.face.?.renderGlyph(alloc, atlas, glyph_index); } test { diff --git a/src/font/GroupCache.zig b/src/font/GroupCache.zig index 06e7fc9de..90f33da5a 100644 --- a/src/font/GroupCache.zig +++ b/src/font/GroupCache.zig @@ -129,7 +129,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), diff --git a/src/font/discovery.zig b/src/font/discovery.zig new file mode 100644 index 000000000..e352fa923 --- /dev/null +++ b/src/font/discovery.zig @@ -0,0 +1,77 @@ +const std = @import("std"); +const assert = std.debug.assert; +const fontconfig = @import("fontconfig"); + +const log = std.log.named(.discovery); + +/// 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. + size: u16 = 0, + + /// 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.initLoadConfig() }; + } + + pub fn discover(self: *Fontconfig, desc: Descriptor) void { + // Build our pattern that we'll search for + const pat = desc.toFcPattern(); + defer pat.destroy(); + assert(self.fc_config.substituteWithPat(pat, .pattern)); + pat.defaultSubstitute(); + + // Search + const res = self.fc_config.fontSort(pat, true, null); + defer res.fs.destroy(); + } +}; + +test { + defer fontconfig.fini(); + var fc = Fontconfig.init(); + + fc.discover(.{ .family = "monospace" }); +} diff --git a/src/font/main.zig b/src/font/main.zig index 11de6c30d..bdb6060da 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -1,5 +1,7 @@ const std = @import("std"); +const build_options = @import("build_options"); +pub const DeferredFace = @import("DeferredFace.zig"); pub const Face = @import("Face.zig"); pub const Group = @import("Group.zig"); pub const GroupCache = @import("GroupCache.zig"); @@ -7,6 +9,13 @@ pub const Glyph = @import("Glyph.zig"); pub const Library = @import("Library.zig"); pub const Shaper = @import("Shaper.zig"); +/// 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) { regular = 0, @@ -33,4 +42,7 @@ pub const Metrics = struct { test { @import("std").testing.refAllDecls(@This()); + + // TODO + if (options.fontconfig) _ = @import("discovery.zig"); } diff --git a/src/main.zig b/src/main.zig index c01c6cbd3..b543215c7 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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()}); } From 1c1da63c7e634bc84ce386987a46e376b4c2225b Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 16 Sep 2022 15:29:13 -0700 Subject: [PATCH 02/14] deferred face hasCodepoint --- src/font/DeferredFace.zig | 40 +++++++++++++++++++++++++++++++++++++++ src/font/Group.zig | 8 +------- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index 0a3f974ff..dc3c46d50 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -11,6 +11,7 @@ const assert = std.debug.assert; const fontconfig = @import("fontconfig"); const options = @import("main.zig").options; const Face = @import("main.zig").Face; +const Presentation = @import("main.zig").Presentation; /// The loaded face (once loaded). face: ?Face = null, @@ -35,3 +36,42 @@ pub fn deinit(self: *DeferredFace) void { pub inline fn loaded(self: DeferredFace) bool { return self.face != null; } + +/// 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) { + // Check if char exists + if (!self.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 (self.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; +} diff --git a/src/font/Group.zig b/src/font/Group.zig index 80d6e72e0..27ef4545d 100644 --- a/src/font/Group.zig +++ b/src/font/Group.zig @@ -112,13 +112,7 @@ pub fn indexForCodepoint( fn indexForCodepointExact(self: Group, cp: u32, style: Style, p: ?Presentation) ?FontIndex { for (self.faces.get(style).items) |deferred, i| { - const face = deferred.face.?; - - // 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) { + if (deferred.hasCodepoint(cp, p)) { return FontIndex{ .style = style, .idx = @intCast(FontIndex.IndexInt, i), From ac26c20e94c8098df90f126ab28069a390bcb30a Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 17 Sep 2022 09:21:23 -0700 Subject: [PATCH 03/14] font discovery builds up a set of deferred faces --- src/font/DeferredFace.zig | 39 ++++++++++++--------- src/font/discovery.zig | 72 +++++++++++++++++++++++++++++++++++---- 2 files changed, 88 insertions(+), 23 deletions(-) diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index dc3c46d50..e5e4cc135 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -17,18 +17,23 @@ const Presentation = @import("main.zig").Presentation; face: ?Face = null, /// Fontconfig -fc: if (options.fontconfig) Fontconfig else void = undefined, +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 { pattern: *fontconfig.Pattern, - charset: *fontconfig.CharSet, - langset: *fontconfig.LangSet, + charset: *const fontconfig.CharSet, + langset: *const fontconfig.LangSet, + + pub fn deinit(self: *Fontconfig) void { + self.pattern.destroy(); + self.* = undefined; + } }; pub fn deinit(self: *DeferredFace) void { if (self.face) |*face| face.deinit(); - + if (options.fontconfig) if (self.fc) |*fc| fc.deinit(); self.* = undefined; } @@ -54,21 +59,23 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool { // If we are using fontconfig, use the fontconfig metadata to // avoid loading the face. if (options.fontconfig) { - // Check if char exists - if (!self.fc.charset.hasChar(cp)) return false; + 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 (self.fc.langset.hasLang(emoji_lang)) - .emoji - else - .text; + // 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 desired == actual; + } + + return true; } - - return true; } // This is unreachable because discovery mechanisms terminate, and diff --git a/src/font/discovery.zig b/src/font/discovery.zig index e352fa923..06790e01f 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -1,9 +1,14 @@ const std = @import("std"); const assert = std.debug.assert; const fontconfig = @import("fontconfig"); +const DeferredFace = @import("main.zig").DeferredFace; const log = std.log.named(.discovery); +pub const Error = error{ + FontConfigFailed, +}; + /// 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 { @@ -53,25 +58,78 @@ pub const Fontconfig = struct { pub fn init() Fontconfig { // safe to call multiple times and concurrently _ = fontconfig.init(); - return .{ .fc_config = fontconfig.initLoadConfig() }; + return .{ .fc_config = fontconfig.initLoadConfigAndFonts() }; } - pub fn discover(self: *Fontconfig, desc: Descriptor) void { + /// 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(); - defer pat.destroy(); + errdefer pat.destroy(); assert(self.fc_config.substituteWithPat(pat, .pattern)); pat.defaultSubstitute(); // Search const res = self.fc_config.fontSort(pat, true, null); - defer res.fs.destroy(); + 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 { - defer fontconfig.fini(); - var fc = Fontconfig.init(); + const testing = std.testing; - fc.discover(.{ .family = "monospace" }); + var fc = Fontconfig.init(); + var it = try fc.discover(.{ .family = "monospace" }); + defer it.deinit(); + while (try it.next()) |face| { + try testing.expect(!face.loaded()); + } } From 92251ed913cf7b36103d41da2dc832831c764808 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 17 Sep 2022 09:29:34 -0700 Subject: [PATCH 04/14] font group works with deferred faces exclusively --- src/Grid.zig | 8 ++++---- src/font/DeferredFace.zig | 24 ++++++++++++++++++++++++ src/font/Group.zig | 10 +++++----- src/font/GroupCache.zig | 7 ++++++- src/font/Shaper.zig | 7 ++++--- 5 files changed, 43 insertions(+), 13 deletions(-) diff --git a/src/Grid.zig b/src/Grid.zig index fb41064aa..9630cfc6e 100644 --- a/src/Grid.zig +++ b/src/Grid.zig @@ -167,24 +167,24 @@ pub fn init( try group.addFace( alloc, .regular, - try font.Face.init(font_lib, face_ttf, font_size), + font.DeferredFace.initLoaded(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), + font.DeferredFace.initLoaded(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), + font.DeferredFace.initLoaded(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), + font.DeferredFace.initLoaded(try font.Face.init(font_lib, face_emoji_text_ttf, font_size)), ); break :group group; diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index e5e4cc135..43efbcb72 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -13,6 +13,8 @@ const options = @import("main.zig").options; const Face = @import("main.zig").Face; const Presentation = @import("main.zig").Presentation; +const Library = @import("main.zig").Library; + /// The loaded face (once loaded). face: ?Face = null, @@ -31,6 +33,12 @@ pub const Fontconfig = struct { } }; +/// 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(); @@ -82,3 +90,19 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool { // if we're not using a discovery mechanism, the face MUST be loaded. unreachable; } + +test { + 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)); +} diff --git a/src/font/Group.zig b/src/font/Group.zig index 27ef4545d..b49ef0c58 100644 --- a/src/font/Group.zig +++ b/src/font/Group.zig @@ -58,8 +58,8 @@ 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 { - try self.faces.getPtr(style).append(alloc, .{ .face = face }); +pub fn addFace(self: *Group, alloc: Allocator, style: Style, face: DeferredFace) !void { + try self.faces.getPtr(style).append(alloc, face); } /// This represents a specific font in the group. @@ -169,9 +169,9 @@ test { var group = try init(alloc); 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; diff --git a/src/font/GroupCache.zig b/src/font/GroupCache.zig index 90f33da5a..c81ea42e8 100644 --- a/src/font/GroupCache.zig +++ b/src/font/GroupCache.zig @@ -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; @@ -221,7 +222,11 @@ test { 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. diff --git a/src/font/Shaper.zig b/src/font/Shaper.zig index 9063ae45b..4c901bd06 100644 --- a/src/font/Shaper.zig +++ b/src/font/Shaper.zig @@ -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; @@ -599,9 +600,9 @@ fn testShaper(alloc: Allocator) !TestShaper { 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); From bc9a0a36a8e21c02a8369b8e5c7cb733ac089acb Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 17 Sep 2022 10:05:26 -0700 Subject: [PATCH 05/14] store requested size alongside deferred font for loading --- src/font/DeferredFace.zig | 11 +++++++++++ src/font/discovery.zig | 11 ++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index 43efbcb72..8a4eb9939 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -23,10 +23,21 @@ fc: if (options.fontconfig) ?Fontconfig else void = if (options.fontconfig) null /// Fontconfig specific data. This is only present if building with fontconfig. pub const Fontconfig = struct { + /// The pattern for this font. This must be the "render prepared" pattern. + /// (i.e. call FcFontRenderPrepare). pattern: *fontconfig.Pattern, + + /// Charset and Langset are used for quick lookup if a codepoint and + /// presentation style are supported. They can be derived from pattern + /// but are cached since they're frequently used. charset: *const fontconfig.CharSet, langset: *const fontconfig.LangSet, + /// The requested size in points for this font. This is used for loading. + /// This can't be derived from pattern because the requested size may + /// differ from the size the font advertises supported. + req_size: u16, + pub fn deinit(self: *Fontconfig) void { self.pattern.destroy(); self.* = undefined; diff --git a/src/font/discovery.zig b/src/font/discovery.zig index 06790e01f..dcdb818f6 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -22,8 +22,10 @@ pub const Descriptor = struct { /// fontconfig pattern, such as "Fira Code-14:bold". family: [:0]const u8, - /// Font size in points that the font should support. - size: u16 = 0, + /// 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. @@ -81,6 +83,7 @@ pub const Fontconfig = struct { .set = res.fs, .fonts = res.fs.fonts(), .i = 0, + .req_size = @floatToInt(u16, (try pat.get(.size, 0)).double), }; } @@ -90,6 +93,7 @@ pub const Fontconfig = struct { set: *fontconfig.FontSet, fonts: []*fontconfig.Pattern, i: usize, + req_size: u16, pub fn deinit(self: *DiscoverIterator) void { self.set.destroy(); @@ -117,6 +121,7 @@ pub const Fontconfig = struct { .pattern = font_pattern, .charset = (try font_pattern.get(.charset, 0)).char_set, .langset = (try font_pattern.get(.lang, 0)).lang_set, + .req_size = self.req_size, }, }; } @@ -127,7 +132,7 @@ test { const testing = std.testing; var fc = Fontconfig.init(); - var it = try fc.discover(.{ .family = "monospace" }); + var it = try fc.discover(.{ .family = "monospace", .size = 12 }); defer it.deinit(); while (try it.next()) |face| { try testing.expect(!face.loaded()); From 88a4cb65f3dee1245c3dd42328b2df06a77686ab Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 23 Sep 2022 12:09:40 -0700 Subject: [PATCH 06/14] src/font: working on initiaizing deferred fonts from fc --- pkg/freetype/Library.zig | 12 ++++++++++++ src/font/DeferredFace.zig | 25 +++++++++++++++++++++++++ src/font/Face.zig | 17 +++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/pkg/freetype/Library.zig b/pkg/freetype/Library.zig index 202025839..df99d2b94 100644 --- a/pkg/freetype/Library.zig +++ b/pkg/freetype/Library.zig @@ -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; diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index 8a4eb9939..7c82cb5de 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -61,6 +61,31 @@ pub inline fn loaded(self: DeferredFace) bool { return self.face != null; } +pub fn load(self: *DeferredFace) !void { + // No-op if we already loaded + if (self.face != null) return; + + if (options.fontconfig) { + try self.loadFontconfig(); + return; + } + + unreachable; +} + +fn loadFontconfig(self: *DeferredFace) !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(filename, face_index, .{ + .points = fc.req_size, + }); +} + /// Returns true if this face can satisfy the given codepoint and /// presentation. If presentation is null, then it just checks if the /// codepoint is present at all. diff --git a/src/font/Face.zig b/src/font/Face.zig index 9777567e7..3068b8cbc 100644 --- a/src/font/Face.zig +++ b/src/font/Face.zig @@ -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); From b11ed06fc27986257211c59883e9e6c6602cdcf8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 24 Sep 2022 06:24:51 -0700 Subject: [PATCH 07/14] font: test loading deferred face for fontconfig --- src/font/DeferredFace.zig | 42 ++++++++++++++++++++++++++++++++------- src/font/discovery.zig | 13 +++++++----- src/font/main.zig | 6 +++--- 3 files changed, 46 insertions(+), 15 deletions(-) diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index 7c82cb5de..4693ed5bc 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -10,11 +10,10 @@ 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; -const Library = @import("main.zig").Library; - /// The loaded face (once loaded). face: ?Face = null, @@ -61,19 +60,22 @@ pub inline fn loaded(self: DeferredFace) bool { return self.face != null; } -pub fn load(self: *DeferredFace) !void { +/// Load the deferred font face. This does nothing if the face is loaded. +pub fn load(self: *DeferredFace, lib: Library) !void { // No-op if we already loaded if (self.face != null) return; if (options.fontconfig) { - try self.loadFontconfig(); + try self.loadFontconfig(lib); return; } + // Unreachable because we must be already loaded or have the + // proper configuration for one of the other deferred mechanisms. unreachable; } -fn loadFontconfig(self: *DeferredFace) !void { +fn loadFontconfig(self: *DeferredFace, lib: Library) !void { assert(self.face == null); const fc = self.fc.?; @@ -81,7 +83,7 @@ fn loadFontconfig(self: *DeferredFace) !void { const filename = (try fc.pattern.get(.file, 0)).string; const face_index = (try fc.pattern.get(.index, 0)).integer; - self.face = try Face.initFile(filename, face_index, .{ + self.face = try Face.initFile(lib, filename, face_index, .{ .points = fc.req_size, }); } @@ -127,7 +129,7 @@ pub fn hasCodepoint(self: DeferredFace, cp: u32, p: ?Presentation) bool { unreachable; } -test { +test "preloaded" { const testing = std.testing; const testFont = @import("test.zig").fontRegular; @@ -142,3 +144,29 @@ test { try testing.expect(def.hasCodepoint(' ', null)); } + +test "fontconfig" { + if (!options.fontconfig) return error.SkipZigTest; + + const discovery = @import("main.zig").discovery; + const testing = std.testing; + + // Load freetype + var lib = try Library.init(); + defer lib.deinit(); + + // Get a deferred face from fontconfig + var def = def: { + var fc = discovery.Fontconfig.init(); + var it = try fc.discover(.{ .family = "monospace", .size = 12 }); + defer it.deinit(); + break :def (try it.next()).?; + }; + defer def.deinit(); + try testing.expect(!def.loaded()); + + // Load it and verify it works + try def.load(lib); + try testing.expect(def.hasCodepoint(' ', null)); + try testing.expect(def.face.?.glyphIndex(' ') != null); +} diff --git a/src/font/discovery.zig b/src/font/discovery.zig index dcdb818f6..b5f725a42 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -1,16 +1,17 @@ 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); -pub const Error = error{ - FontConfigFailed, -}; +/// 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. +/// 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 @@ -74,7 +75,7 @@ pub const Fontconfig = struct { // Search const res = self.fc_config.fontSort(pat, true, null); - if (res.result != .match) return Error.FontConfigFailed; + if (res.result != .match) return error.FontConfigFailed; errdefer res.fs.destroy(); return DiscoverIterator{ @@ -129,6 +130,8 @@ pub const Fontconfig = struct { }; test { + if (!options.fontconfig) return error.SkipZigTest; + const testing = std.testing; var fc = Fontconfig.init(); diff --git a/src/font/main.zig b/src/font/main.zig index bdb6060da..eaa947dfa 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -1,6 +1,7 @@ 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"); @@ -8,6 +9,8 @@ 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 { @@ -42,7 +45,4 @@ pub const Metrics = struct { test { @import("std").testing.refAllDecls(@This()); - - // TODO - if (options.fontconfig) _ = @import("discovery.zig"); } From 30a5041a3841f3cdd386828cb6b0f255396a89a5 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 29 Sep 2022 10:30:45 -0700 Subject: [PATCH 08/14] font Group has Lib, loads deferred faces when needed --- src/Grid.zig | 4 +-- src/font/Group.zig | 61 +++++++++++++++++++++++++++++++++++------ src/font/GroupCache.zig | 12 ++++---- src/font/Shaper.zig | 4 +-- 4 files changed, 63 insertions(+), 18 deletions(-) diff --git a/src/Grid.zig b/src/Grid.zig index 9630cfc6e..6ff0eaa08 100644 --- a/src/Grid.zig +++ b/src/Grid.zig @@ -160,7 +160,7 @@ pub fn init( 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); + var group = try font.Group.init(alloc, font_lib); errdefer group.deinit(alloc); // Our regular font @@ -610,7 +610,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, diff --git a/src/font/Group.zig b/src/font/Group.zig index b49ef0c58..3c9454e28 100644 --- a/src/font/Group.zig +++ b/src/font/Group.zig @@ -17,6 +17,7 @@ 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); @@ -27,12 +28,15 @@ const log = std.log.scoped(.font_group); // to the user so we can change this later. const StyleArray = std.EnumArray(Style, std.ArrayListUnmanaged(DeferredFace)); +/// The library being used for all the faces. +lib: Library, + /// 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) !Group { + var result = Group{ .lib = lib, .faces = undefined }; // Initialize all our styles to initially sized lists. var i: usize = 0; @@ -125,8 +129,9 @@ 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 { - const deferred = 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); return deferred.face.?; } @@ -148,8 +153,8 @@ pub fn renderGlyph( index: FontIndex, glyph_index: u32, ) !Glyph { - const face = self.faces.get(index.style).items[@intCast(usize, index.idx)]; - assert(face.loaded()); + const face = &self.faces.get(index.style).items[@intCast(usize, index.idx)]; + try face.load(self.lib); return try face.face.?.renderGlyph(alloc, atlas, glyph_index); } @@ -166,7 +171,7 @@ test { var lib = try Library.init(); defer lib.deinit(); - var group = try init(alloc); + var group = try init(alloc, lib); defer group.deinit(alloc); try group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testFont, .{ .points = 12 }))); @@ -181,7 +186,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, @@ -210,3 +215,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); + 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, + ); + } +} diff --git a/src/font/GroupCache.zig b/src/font/GroupCache.zig index c81ea42e8..479d11b4b 100644 --- a/src/font/GroupCache.zig +++ b/src/font/GroupCache.zig @@ -94,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) { @@ -110,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); @@ -179,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, @@ -218,7 +218,7 @@ 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)); defer cache.deinit(alloc); // Setup group @@ -237,7 +237,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, @@ -258,7 +258,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, diff --git a/src/font/Shaper.zig b/src/font/Shaper.zig index 4c901bd06..e1968e92a 100644 --- a/src/font/Shaper.zig +++ b/src/font/Shaper.zig @@ -62,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 @@ -596,7 +596,7 @@ 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)); errdefer cache_ptr.*.deinit(alloc); // Setup group From fdbf40d3ee484527536a9adfd4c6470ba888d21c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 29 Sep 2022 11:39:09 -0700 Subject: [PATCH 09/14] pkg/freetype: disable ubsan --- pkg/freetype/build.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/freetype/build.zig b/pkg/freetype/build.zig index c4cf4690c..04840624e 100644 --- a/pkg/freetype/build.zig +++ b/pkg/freetype/build.zig @@ -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"); From b6a4fff6d8d141f6738e0d01efcfb7518e8f1602 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 29 Sep 2022 11:57:29 -0700 Subject: [PATCH 10/14] search for fonts on startup --- src/Grid.zig | 30 +++++++++++++++++++++++++++-- src/font/DeferredFace.zig | 40 +++++++++++++++++++++++++++------------ src/font/Group.zig | 19 +++++++++++++------ src/font/GroupCache.zig | 6 +++++- src/font/Shaper.zig | 6 +++++- src/font/discovery.zig | 13 +++++++++---- 6 files changed, 88 insertions(+), 26 deletions(-) diff --git a/src/Grid.zig b/src/Grid.zig index 6ff0eaa08..735063946 100644 --- a/src/Grid.zig +++ b/src/Grid.zig @@ -160,10 +160,36 @@ pub fn init( 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, font_lib); + var group = try font.Group.init(alloc, font_lib, font_size); errdefer group.deinit(alloc); - // Our regular font + // Search for fonts + { + var disco = font.Discover.init(); + defer disco.deinit(); + + { + var disco_it = try disco.discover(.{ + .family = "Fira Code", + .size = font_size.points, + }); + defer disco_it.deinit(); + if (try disco_it.next()) |face| + try group.addFace(alloc, .regular, face); + } + { + var disco_it = try disco.discover(.{ + .family = "Fira Code", + .size = font_size.points, + .bold = true, + }); + defer disco_it.deinit(); + if (try disco_it.next()) |face| + try group.addFace(alloc, .bold, face); + } + } + + // Our built-in font will be used as a backup try group.addFace( alloc, .regular, diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index 4693ed5bc..b6bef7384 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -32,11 +32,6 @@ pub const Fontconfig = struct { charset: *const fontconfig.CharSet, langset: *const fontconfig.LangSet, - /// The requested size in points for this font. This is used for loading. - /// This can't be derived from pattern because the requested size may - /// differ from the size the font advertises supported. - req_size: u16, - pub fn deinit(self: *Fontconfig) void { self.pattern.destroy(); self.* = undefined; @@ -60,13 +55,28 @@ 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) !void { +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); + try self.loadFontconfig(lib, size); return; } @@ -75,7 +85,11 @@ pub fn load(self: *DeferredFace, lib: Library) !void { unreachable; } -fn loadFontconfig(self: *DeferredFace, lib: Library) !void { +fn loadFontconfig( + self: *DeferredFace, + lib: Library, + size: Face.DesiredSize, +) !void { assert(self.face == null); const fc = self.fc.?; @@ -83,9 +97,7 @@ fn loadFontconfig(self: *DeferredFace, lib: Library) !void { const filename = (try fc.pattern.get(.file, 0)).string; const face_index = (try fc.pattern.get(.index, 0)).integer; - self.face = try Face.initFile(lib, filename, face_index, .{ - .points = fc.req_size, - }); + self.face = try Face.initFile(lib, filename, face_index, size); } /// Returns true if this face can satisfy the given codepoint and @@ -165,8 +177,12 @@ test "fontconfig" { 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); + try def.load(lib, .{ .points = 12 }); try testing.expect(def.hasCodepoint(' ', null)); try testing.expect(def.face.?.glyphIndex(' ') != null); } diff --git a/src/font/Group.zig b/src/font/Group.zig index 3c9454e28..623af7802 100644 --- a/src/font/Group.zig +++ b/src/font/Group.zig @@ -31,12 +31,19 @@ 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, lib: Library) !Group { - var result = Group{ .lib = lib, .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; @@ -131,7 +138,7 @@ 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 { const deferred = &self.faces.get(index.style).items[@intCast(usize, index.idx)]; - try deferred.load(self.lib); + try deferred.load(self.lib, self.size); return deferred.face.?; } @@ -154,7 +161,7 @@ pub fn renderGlyph( glyph_index: u32, ) !Glyph { const face = &self.faces.get(index.style).items[@intCast(usize, index.idx)]; - try face.load(self.lib); + try face.load(self.lib, self.size); return try face.face.?.renderGlyph(alloc, atlas, glyph_index); } @@ -171,7 +178,7 @@ test { var lib = try Library.init(); defer lib.deinit(); - var group = try init(alloc, lib); + var group = try init(alloc, lib, .{ .points = 12 }); defer group.deinit(alloc); try group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testFont, .{ .points = 12 }))); @@ -231,7 +238,7 @@ test { // Initialize the group with the deferred face var lib = try Library.init(); defer lib.deinit(); - var group = try init(alloc, lib); + var group = try init(alloc, lib, .{ .points = 12 }); defer group.deinit(alloc); try group.addFace(alloc, .regular, (try it.next()).?); diff --git a/src/font/GroupCache.zig b/src/font/GroupCache.zig index 479d11b4b..86bbbd520 100644 --- a/src/font/GroupCache.zig +++ b/src/font/GroupCache.zig @@ -218,7 +218,11 @@ test { var lib = try Library.init(); defer lib.deinit(); - var cache = try init(alloc, try Group.init(alloc, lib)); + var cache = try init(alloc, try Group.init( + alloc, + lib, + .{ .points = 12 }, + )); defer cache.deinit(alloc); // Setup group diff --git a/src/font/Shaper.zig b/src/font/Shaper.zig index e1968e92a..dde0a5235 100644 --- a/src/font/Shaper.zig +++ b/src/font/Shaper.zig @@ -596,7 +596,11 @@ 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, lib)); + cache_ptr.* = try GroupCache.init(alloc, try Group.init( + alloc, + lib, + .{ .points = 12 }, + )); errdefer cache_ptr.*.deinit(alloc); // Setup group diff --git a/src/font/discovery.zig b/src/font/discovery.zig index b5f725a42..d3b325782 100644 --- a/src/font/discovery.zig +++ b/src/font/discovery.zig @@ -39,7 +39,11 @@ pub const Descriptor = struct { 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.size > 0) assert(pat.add( + .size, + .{ .integer = self.size }, + false, + )); if (self.bold) assert(pat.add( .weight, .{ .integer = @enumToInt(fontconfig.Weight.bold) }, @@ -64,6 +68,10 @@ pub const Fontconfig = struct { 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 { @@ -84,7 +92,6 @@ pub const Fontconfig = struct { .set = res.fs, .fonts = res.fs.fonts(), .i = 0, - .req_size = @floatToInt(u16, (try pat.get(.size, 0)).double), }; } @@ -94,7 +101,6 @@ pub const Fontconfig = struct { set: *fontconfig.FontSet, fonts: []*fontconfig.Pattern, i: usize, - req_size: u16, pub fn deinit(self: *DiscoverIterator) void { self.set.destroy(); @@ -122,7 +128,6 @@ pub const Fontconfig = struct { .pattern = font_pattern, .charset = (try font_pattern.get(.charset, 0)).char_set, .langset = (try font_pattern.get(.lang, 0)).lang_set, - .req_size = self.req_size, }, }; } From 7f45916b899d9a18a6fd539ae8290e9a280ca59f Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 29 Sep 2022 11:58:00 -0700 Subject: [PATCH 11/14] only discover if it is enabled --- src/Grid.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Grid.zig b/src/Grid.zig index 735063946..8d100b33a 100644 --- a/src/Grid.zig +++ b/src/Grid.zig @@ -164,7 +164,7 @@ pub fn init( errdefer group.deinit(alloc); // Search for fonts - { + if (font.Discover != void) { var disco = font.Discover.init(); defer disco.deinit(); From 5567564dd0b37da47db6252d04f6f1b05ddb9305 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 29 Sep 2022 13:14:20 -0700 Subject: [PATCH 12/14] cli args fix stage1 miscompilation, add font families --- src/Grid.zig | 33 +++++++++++++++++++++++++++++++-- src/cli_args.zig | 11 ++++++++--- src/config.zig | 6 ++++++ 3 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/Grid.zig b/src/Grid.zig index 8d100b33a..1f393cbc6 100644 --- a/src/Grid.zig +++ b/src/Grid.zig @@ -174,8 +174,10 @@ pub fn init( .size = font_size.points, }); defer disco_it.deinit(); - if (try disco_it.next()) |face| + if (try disco_it.next()) |face| { + log.debug("font regular: {s}", .{try face.name()}); try group.addFace(alloc, .regular, face); + } } { var disco_it = try disco.discover(.{ @@ -184,8 +186,35 @@ pub fn init( .bold = true, }); defer disco_it.deinit(); - if (try disco_it.next()) |face| + if (try disco_it.next()) |face| { + log.debug("font bold: {s}", .{try face.name()}); try group.addFace(alloc, .bold, face); + } + } + { + var disco_it = try disco.discover(.{ + .family = "Fira Code", + .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); + } + } + { + var disco_it = try disco.discover(.{ + .family = "Fira Code", + .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); + } } } diff --git a/src/cli_args.zig b/src/cli_args.zig index 080cdbfb7..75cbfb5be 100644 --- a/src/cli_args.zig +++ b/src/cli_args.zig @@ -110,15 +110,20 @@ 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, + }, 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, }; diff --git a/src/config.zig b/src/config.zig index a1adb6bb0..d93f86783 100644 --- a/src/config.zig +++ b/src/config.zig @@ -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": ?[]const u8 = null, + @"font-family-bold": ?[]const u8 = null, + @"font-family-italic": ?[]const u8 = null, + @"font-family-bold-italic": ?[]const u8 = null, + /// Font size in points @"font-size": u8 = 12, From 7eac0afff24d4883c5c32c4444be21b1b86c4b10 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 29 Sep 2022 14:45:37 -0700 Subject: [PATCH 13/14] cli args can parse null-terminated strings --- src/cli_args.zig | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/cli_args.zig b/src/cli_args.zig index 75cbfb5be..502cfcf3f 100644 --- a/src/cli_args.zig +++ b/src/cli_args.zig @@ -117,6 +117,14 @@ fn parseIntoField( break :value buf; }, + [: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( @@ -199,6 +207,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); From 53aab0a163baf726104bb46738f4fe77c95e8412 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Thu, 29 Sep 2022 14:51:31 -0700 Subject: [PATCH 14/14] --font-family CLI config --- src/Grid.zig | 108 ++------------------------------------------ src/Window.zig | 115 +++++++++++++++++++++++++++++++++++++++++++++-- src/cli_args.zig | 24 ++++++++++ src/config.zig | 27 +++++++++-- 4 files changed, 162 insertions(+), 112 deletions(-) diff --git a/src/Grid.zig b/src/Grid.zig index 1f393cbc6..de0eea79d 100644 --- a/src/Grid.zig +++ b/src/Grid.zig @@ -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,100 +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, font_lib, font_size); - errdefer group.deinit(alloc); - - // Search for fonts - if (font.Discover != void) { - var disco = font.Discover.init(); - defer disco.deinit(); - - { - var disco_it = try disco.discover(.{ - .family = "Fira Code", - .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); - } - } - { - var disco_it = try disco.discover(.{ - .family = "Fira Code", - .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); - } - } - { - var disco_it = try disco.discover(.{ - .family = "Fira Code", - .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); - } - } - { - var disco_it = try disco.discover(.{ - .family = "Fira Code", - .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); - +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); @@ -381,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, @@ -394,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(); @@ -478,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( @@ -967,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"); diff --git a/src/Window.zig b/src/Window.zig index 2d3f91ca0..c84909bfd 100644 --- a/src/Window.zig +++ b/src/Window.zig @@ -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"); diff --git a/src/cli_args.zig b/src/cli_args.zig index 502cfcf3f..b6263f048 100644 --- a/src/cli_args.zig +++ b/src/cli_args.zig @@ -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. @@ -193,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); diff --git a/src/config.zig b/src/config.zig index d93f86783..7448f7ae1 100644 --- a/src/config.zig +++ b/src/config.zig @@ -7,10 +7,10 @@ const inputpkg = @import("input.zig"); /// CLI flag names hence we use a lot of `@""` syntax to support hyphens. pub const Config = struct { /// The font families to use. - @"font-family": ?[]const u8 = null, - @"font-family-bold": ?[]const u8 = null, - @"font-family-italic": ?[]const u8 = null, - @"font-family-bold-italic": ?[]const u8 = null, + @"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, @@ -122,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.