From 141182aa1371f356b9c6115fc05eba64ff75894c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Fri, 16 Sep 2022 15:06:00 -0700 Subject: [PATCH] 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()}); }