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/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/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"); diff --git a/src/Grid.zig b/src/Grid.zig index fb41064aa..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,45 +151,7 @@ const GPUCellMode = enum(u8) { } }; -pub fn init( - alloc: Allocator, - font_size: font.Face.DesiredSize, -) !Grid { - // Build our font group - var font_lib = try font.Library.init(); - errdefer font_lib.deinit(); - var font_group = try font.GroupCache.init(alloc, group: { - var group = try font.Group.init(alloc); - errdefer group.deinit(alloc); - - // Our regular font - try group.addFace( - alloc, - .regular, - try font.Face.init(font_lib, face_ttf, font_size), - ); - try group.addFace( - alloc, - .bold, - try font.Face.init(font_lib, face_bold_ttf, font_size), - ); - - // Emoji - try group.addFace( - alloc, - .regular, - try font.Face.init(font_lib, face_emoji_ttf, font_size), - ); - try group.addFace( - alloc, - .regular, - try font.Face.init(font_lib, face_emoji_text_ttf, font_size), - ); - - break :group group; - }); - errdefer font_group.deinit(alloc); - +pub fn init(alloc: Allocator, font_group: *font.GroupCache) !Grid { // Create the initial font shaper var shape_buf = try alloc.alloc(font.Shaper.Cell, 1); errdefer alloc.free(shape_buf); @@ -326,7 +287,6 @@ pub fn init( .vbo = vbo, .texture = tex, .texture_color = tex_color, - .font_lib = font_lib, .font_group = font_group, .font_shaper = shaper, .cursor_visible = true, @@ -339,8 +299,6 @@ pub fn init( pub fn deinit(self: *Grid) void { self.font_shaper.deinit(); self.alloc.free(self.font_shaper.cell_buf); - self.font_group.deinit(self.alloc); - self.font_lib.deinit(); self.texture.destroy(); self.texture_color.destroy(); @@ -423,7 +381,7 @@ pub fn rebuildCells(self: *Grid, term: *Terminal) !void { const start = self.cells.items.len; // Split our row into runs and shape each one. - var iter = self.font_shaper.runIterator(&self.font_group, row); + var iter = self.font_shaper.runIterator(self.font_group, row); while (try iter.next(self.alloc)) |run| { for (try self.font_shaper.shape(run)) |shaper_cell| { assert(try self.updateCell( @@ -610,7 +568,7 @@ pub fn updateCell( // If the cell has a character, draw it if (cell.char > 0) { // Render - const face = self.font_group.group.faceFromIndex(shaper_run.font_index); + const face = try self.font_group.group.faceFromIndex(shaper_run.font_index); const glyph = try self.font_group.renderGlyph( self.alloc, shaper_run.font_index, @@ -912,8 +870,3 @@ test "GridSize update rounding" { try testing.expectEqual(@as(GridSize.Unit, 3), grid.columns); try testing.expectEqual(@as(GridSize.Unit, 2), grid.rows); } - -const face_ttf = @embedFile("font/res/FiraCode-Regular.ttf"); -const face_bold_ttf = @embedFile("font/res/FiraCode-Bold.ttf"); -const face_emoji_ttf = @embedFile("font/res/NotoColorEmoji.ttf"); -const face_emoji_text_ttf = @embedFile("font/res/NotoEmoji-Regular.ttf"); 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 080cdbfb7..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. @@ -110,15 +112,28 @@ fn parseIntoField( // No parseCLI, magic the value based on the type @field(dst, field.name) = switch (Field) { - []const u8 => if (value) |slice| value: { + []const u8 => value: { + const slice = value orelse return error.ValueRequired; const buf = try alloc.alloc(u8, slice.len); mem.copy(u8, buf, slice); break :value buf; - } else return error.ValueRequired, + }, + + [:0]const u8 => value: { + const slice = value orelse return error.ValueRequired; + const buf = try alloc.allocSentinel(u8, slice.len, 0); + mem.copy(u8, buf, slice); + buf[slice.len] = 0; + break :value buf; + }, bool => try parseBool(value orelse "t"), - u8 => try std.fmt.parseInt(u8, value orelse return error.ValueRequired, 0), + u8 => try std.fmt.parseInt( + u8, + value orelse return error.ValueRequired, + 0, + ), else => unreachable, }; @@ -180,6 +195,28 @@ test "parse: simple" { try testing.expect(!data.@"b-f"); } +test "parse: finalize" { + const testing = std.testing; + + var data: struct { + a: []const u8 = "", + _arena: ?ArenaAllocator = null, + + pub fn finalize(self: *@This()) !void { + self.a = "YO"; + } + } = .{}; + defer if (data._arena) |arena| arena.deinit(); + + var iter = try std.process.ArgIteratorGeneral(.{}).init( + testing.allocator, + "--a=42", + ); + defer iter.deinit(); + try parse(@TypeOf(data), testing.allocator, &data, &iter); + try testing.expectEqualStrings("YO", data.a); +} + test "parseIntoField: string" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); @@ -194,6 +231,21 @@ test "parseIntoField: string" { try testing.expectEqualStrings("42", data.a); } +test "parseIntoField: sentinel string" { + const testing = std.testing; + var arena = ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const alloc = arena.allocator(); + + var data: struct { + a: [:0]const u8, + } = undefined; + + try parseIntoField(@TypeOf(data), alloc, &data, "a", "42"); + try testing.expectEqualStrings("42", data.a); + try testing.expectEqual(@as(u8, 0), data.a[data.a.len]); +} + test "parseIntoField: bool" { const testing = std.testing; var arena = ArenaAllocator.init(testing.allocator); diff --git a/src/config.zig b/src/config.zig index a1adb6bb0..7448f7ae1 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": ?[:0]const u8 = null, + @"font-family-bold": ?[:0]const u8 = null, + @"font-family-italic": ?[:0]const u8 = null, + @"font-family-bold-italic": ?[:0]const u8 = null, + /// Font size in points @"font-size": u8 = 12, @@ -116,6 +122,25 @@ pub const Config = struct { return result; } + + pub fn finalize(self: *Config) !void { + // If we have a font-family set and don't set the others, default + // the others to the font family. This way, if someone does + // --font-family=foo, then we try to get the stylized versions of + // "foo" as well. + if (self.@"font-family") |family| { + const fields = &[_][]const u8{ + "font-family-bold", + "font-family-italic", + "font-family-bold-italic", + }; + inline for (fields) |field| { + if (@field(self, field) == null) { + @field(self, field) = family; + } + } + } + } }; /// Color represents a color using RGB. diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig new file mode 100644 index 000000000..b6bef7384 --- /dev/null +++ b/src/font/DeferredFace.zig @@ -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); +} 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); diff --git a/src/font/Group.zig b/src/font/Group.zig index dcc451450..623af7802 100644 --- a/src/font/Group.zig +++ b/src/font/Group.zig @@ -11,11 +11,13 @@ const assert = std.debug.assert; const Allocator = std.mem.Allocator; const Atlas = @import("../Atlas.zig"); +const DeferredFace = @import("main.zig").DeferredFace; const Face = @import("main.zig").Face; const Library = @import("main.zig").Library; const Glyph = @import("main.zig").Glyph; const Style = @import("main.zig").Style; const Presentation = @import("main.zig").Presentation; +const options = @import("main.zig").options; const log = std.log.scoped(.font_group); @@ -24,14 +26,24 @@ const log = std.log.scoped(.font_group); // usually only one font group for the entire process so this isn't the // most important memory efficiency we can look for. This is totally opaque // to the user so we can change this later. -const StyleArray = std.EnumArray(Style, std.ArrayListUnmanaged(Face)); +const StyleArray = std.EnumArray(Style, std.ArrayListUnmanaged(DeferredFace)); + +/// The library being used for all the faces. +lib: Library, + +/// The desired font size. All fonts in a group must share the same size. +size: Face.DesiredSize, /// The available faces we have. This shouldn't be modified manually. /// Instead, use the functions available on Group. faces: StyleArray, -pub fn init(alloc: Allocator) !Group { - var result = Group{ .faces = undefined }; +pub fn init( + alloc: Allocator, + lib: Library, + size: Face.DesiredSize, +) !Group { + var result = Group{ .lib = lib, .size = size, .faces = undefined }; // Initialize all our styles to initially sized lists. var i: usize = 0; @@ -57,7 +69,7 @@ pub fn deinit(self: *Group, alloc: Allocator) void { /// /// The group takes ownership of the face. The face will be deallocated when /// the group is deallocated. -pub fn addFace(self: *Group, alloc: Allocator, style: Style, face: Face) !void { +pub fn addFace(self: *Group, alloc: Allocator, style: Style, face: DeferredFace) !void { try self.faces.getPtr(style).append(alloc, face); } @@ -110,12 +122,8 @@ pub fn indexForCodepoint( } fn indexForCodepointExact(self: Group, cp: u32, style: Style, p: ?Presentation) ?FontIndex { - for (self.faces.get(style).items) |face, i| { - // If the presentation is null, we allow the first presentation we - // can find. Otherwise, we check for the specific one requested. - if (p != null and face.presentation != p.?) continue; - - if (face.glyphIndex(cp) != null) { + for (self.faces.get(style).items) |deferred, i| { + if (deferred.hasCodepoint(cp, p)) { return FontIndex{ .style = style, .idx = @intCast(FontIndex.IndexInt, i), @@ -128,8 +136,10 @@ fn indexForCodepointExact(self: Group, cp: u32, style: Style, p: ?Presentation) } /// Return the Face represented by a given FontIndex. -pub fn faceFromIndex(self: Group, index: FontIndex) Face { - return self.faces.get(index.style).items[@intCast(usize, index.idx)]; +pub fn faceFromIndex(self: Group, index: FontIndex) !Face { + const deferred = &self.faces.get(index.style).items[@intCast(usize, index.idx)]; + try deferred.load(self.lib, self.size); + return deferred.face.?; } /// Render a glyph by glyph index into the given font atlas and return @@ -150,8 +160,9 @@ pub fn renderGlyph( index: FontIndex, glyph_index: u32, ) !Glyph { - const face = self.faces.get(index.style).items[@intCast(usize, index.idx)]; - return try face.renderGlyph(alloc, atlas, glyph_index); + const face = &self.faces.get(index.style).items[@intCast(usize, index.idx)]; + try face.load(self.lib, self.size); + return try face.face.?.renderGlyph(alloc, atlas, glyph_index); } test { @@ -167,12 +178,12 @@ test { var lib = try Library.init(); defer lib.deinit(); - var group = try init(alloc); + var group = try init(alloc, lib, .{ .points = 12 }); defer group.deinit(alloc); - try group.addFace(alloc, .regular, try Face.init(lib, testFont, .{ .points = 12 })); - try group.addFace(alloc, .regular, try Face.init(lib, testEmoji, .{ .points = 12 })); - try group.addFace(alloc, .regular, try Face.init(lib, testEmojiText, .{ .points = 12 })); + try group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testFont, .{ .points = 12 }))); + try group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testEmoji, .{ .points = 12 }))); + try group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testEmojiText, .{ .points = 12 }))); // Should find all visible ASCII var i: u32 = 32; @@ -182,7 +193,7 @@ test { try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx); // Render it - const face = group.faceFromIndex(idx); + const face = try group.faceFromIndex(idx); const glyph_index = face.glyphIndex(i).?; _ = try group.renderGlyph( alloc, @@ -211,3 +222,43 @@ test { try testing.expectEqual(@as(FontIndex.IndexInt, 1), idx.idx); } } + +test { + if (!options.fontconfig) return error.SkipZigTest; + + const testing = std.testing; + const alloc = testing.allocator; + const Discover = @import("main.zig").Discover; + + // Search for fonts + var fc = Discover.init(); + var it = try fc.discover(.{ .family = "monospace", .size = 12 }); + defer it.deinit(); + + // Initialize the group with the deferred face + var lib = try Library.init(); + defer lib.deinit(); + var group = try init(alloc, lib, .{ .points = 12 }); + defer group.deinit(alloc); + try group.addFace(alloc, .regular, (try it.next()).?); + + // Should find all visible ASCII + var atlas_greyscale = try Atlas.init(alloc, 512, .greyscale); + defer atlas_greyscale.deinit(alloc); + var i: u32 = 32; + while (i < 127) : (i += 1) { + const idx = group.indexForCodepoint(i, .regular, null).?; + try testing.expectEqual(Style.regular, idx.style); + try testing.expectEqual(@as(FontIndex.IndexInt, 0), idx.idx); + + // Render it + const face = try group.faceFromIndex(idx); + const glyph_index = face.glyphIndex(i).?; + _ = try group.renderGlyph( + alloc, + &atlas_greyscale, + idx, + glyph_index, + ); + } +} diff --git a/src/font/GroupCache.zig b/src/font/GroupCache.zig index 06e7fc9de..86bbbd520 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; @@ -93,7 +94,7 @@ pub fn metrics(self: *GroupCache, alloc: Allocator) !Metrics { var i: u32 = 32; while (i <= 126) : (i += 1) { const index = (try self.indexForCodepoint(alloc, i, .regular, .text)).?; - const face = self.group.faceFromIndex(index); + const face = try self.group.faceFromIndex(index); const glyph_index = face.glyphIndex(i).?; const glyph = try self.renderGlyph(alloc, index, glyph_index); if (glyph.advance_x > cell_width) { @@ -109,7 +110,7 @@ pub fn metrics(self: *GroupCache, alloc: Allocator) !Metrics { const cell_height: f32 = cell_height: { // Get the '_' char for height const index = (try self.indexForCodepoint(alloc, '_', .regular, .text)).?; - const face = self.group.faceFromIndex(index); + const face = try self.group.faceFromIndex(index); const glyph_index = face.glyphIndex('_').?; const glyph = try self.renderGlyph(alloc, index, glyph_index); @@ -129,7 +130,7 @@ pub fn metrics(self: *GroupCache, alloc: Allocator) !Metrics { }; const cell_baseline = cell_baseline: { - const face = self.group.faces.get(.regular).items[0]; + const face = self.group.faces.get(.regular).items[0].face.?; break :cell_baseline cell_height - @intToFloat( f32, face.unitsToPxY(face.face.handle.*.ascender), @@ -178,7 +179,7 @@ pub fn renderGlyph( if (gop.found_existing) return gop.value_ptr.*; // Uncached, render it - const face = self.group.faceFromIndex(index); + const face = try self.group.faceFromIndex(index); const atlas: *Atlas = if (face.hasColor()) &self.atlas_color else &self.atlas_greyscale; const glyph = self.group.renderGlyph( alloc, @@ -217,11 +218,19 @@ test { var lib = try Library.init(); defer lib.deinit(); - var cache = try init(alloc, try Group.init(alloc)); + var cache = try init(alloc, try Group.init( + alloc, + lib, + .{ .points = 12 }, + )); defer cache.deinit(alloc); // Setup group - try cache.group.addFace(alloc, .regular, try Face.init(lib, testFont, .{ .points = 12 })); + try cache.group.addFace( + alloc, + .regular, + DeferredFace.initLoaded(try Face.init(lib, testFont, .{ .points = 12 })), + ); const group = cache.group; // Visible ASCII. Do it twice to verify cache. @@ -232,7 +241,7 @@ test { try testing.expectEqual(@as(Group.FontIndex.IndexInt, 0), idx.idx); // Render - const face = cache.group.faceFromIndex(idx); + const face = try cache.group.faceFromIndex(idx); const glyph_index = face.glyphIndex(i).?; _ = try cache.renderGlyph( alloc, @@ -253,7 +262,7 @@ test { try testing.expectEqual(@as(Group.FontIndex.IndexInt, 0), idx.idx); // Render - const face = group.faceFromIndex(idx); + const face = try group.faceFromIndex(idx); const glyph_index = face.glyphIndex(i).?; _ = try cache.renderGlyph( alloc, diff --git a/src/font/Shaper.zig b/src/font/Shaper.zig index 9063ae45b..dde0a5235 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; @@ -61,7 +62,7 @@ pub fn shape(self: *Shaper, run: TextRun) ![]Cell { harfbuzz.Feature.fromString("liga").?, }; - const face = run.group.group.faceFromIndex(run.font_index); + const face = try run.group.group.faceFromIndex(run.font_index); harfbuzz.shape(face.hb_font, self.hb_buf, hb_feats); // If our buffer is empty, we short-circuit the rest of the work @@ -595,13 +596,17 @@ fn testShaper(alloc: Allocator) !TestShaper { var cache_ptr = try alloc.create(GroupCache); errdefer alloc.destroy(cache_ptr); - cache_ptr.* = try GroupCache.init(alloc, try Group.init(alloc)); + cache_ptr.* = try GroupCache.init(alloc, try Group.init( + alloc, + lib, + .{ .points = 12 }, + )); errdefer cache_ptr.*.deinit(alloc); // Setup group - try cache_ptr.group.addFace(alloc, .regular, try Face.init(lib, testFont, .{ .points = 12 })); - try cache_ptr.group.addFace(alloc, .regular, try Face.init(lib, testEmoji, .{ .points = 12 })); - try cache_ptr.group.addFace(alloc, .regular, try Face.init(lib, testEmojiText, .{ .points = 12 })); + try cache_ptr.group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testFont, .{ .points = 12 }))); + try cache_ptr.group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testEmoji, .{ .points = 12 }))); + try cache_ptr.group.addFace(alloc, .regular, DeferredFace.initLoaded(try Face.init(lib, testEmojiText, .{ .points = 12 }))); var cell_buf = try alloc.alloc(Cell, 80); errdefer alloc.free(cell_buf); diff --git a/src/font/discovery.zig b/src/font/discovery.zig new file mode 100644 index 000000000..d3b325782 --- /dev/null +++ b/src/font/discovery.zig @@ -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()); + } +} diff --git a/src/font/main.zig b/src/font/main.zig index 11de6c30d..eaa947dfa 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -1,11 +1,23 @@ const std = @import("std"); +const build_options = @import("build_options"); +pub const discovery = @import("discovery.zig"); +pub const DeferredFace = @import("DeferredFace.zig"); pub const Face = @import("Face.zig"); pub const Group = @import("Group.zig"); pub const GroupCache = @import("GroupCache.zig"); pub const Glyph = @import("Glyph.zig"); pub const Library = @import("Library.zig"); pub const Shaper = @import("Shaper.zig"); +pub const Descriptor = discovery.Descriptor; +pub const Discover = discovery.Discover; + +/// Build options +pub const options: struct { + fontconfig: bool = false, +} = .{ + .fontconfig = build_options.fontconfig, +}; /// The styles that a family can take. pub const Style = enum(u3) { 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()}); }