From 54b9b45a7fa61a47c6ae1480a95adb7d4755efc8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 4 Oct 2023 17:23:57 -0700 Subject: [PATCH] font: rework font init to use a struct with modifiersets everywhere --- src/Surface.zig | 15 +++++++--- src/config/Config.zig | 17 +++++++++++ src/font/DeferredFace.zig | 3 +- src/font/Group.zig | 58 ++++++++++++++++++++++++++++-------- src/font/GroupCache.zig | 8 +++-- src/font/face.zig | 6 ++++ src/font/face/coretext.zig | 26 +++++++++------- src/font/shaper/harfbuzz.zig | 18 +++++++++-- 8 files changed, 118 insertions(+), 33 deletions(-) diff --git a/src/Surface.zig b/src/Surface.zig index 872e8fe1d..1d60177c7 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -224,6 +224,13 @@ pub fn init( var group = try font.Group.init(alloc, font_lib, font_size); errdefer group.deinit(); + // Setup our font metric modifiers if we have any. + group.metric_modifiers = set: { + var set: font.face.Metrics.ModifierSet = .{}; + errdefer set.deinit(alloc); + break :set null; + }; + // If we have codepoint mappings, set those. if (config.@"font-codepoint-map".map.list.len > 0) { group.codepoint_map = config.@"font-codepoint-map".map; @@ -306,11 +313,11 @@ pub fn init( // Our built-in font will be used as a backup _ = try group.addFace( .regular, - .{ .loaded = try font.Face.init(font_lib, face_ttf, font_size) }, + .{ .loaded = try font.Face.init(font_lib, face_ttf, group.faceOptions()) }, ); _ = try group.addFace( .bold, - .{ .loaded = try font.Face.init(font_lib, face_bold_ttf, font_size) }, + .{ .loaded = try font.Face.init(font_lib, face_bold_ttf, group.faceOptions()) }, ); // Auto-italicize if we have to. @@ -321,11 +328,11 @@ pub fn init( if (builtin.os.tag != .macos or font.Discover == void) { _ = try group.addFace( .regular, - .{ .loaded = try font.Face.init(font_lib, face_emoji_ttf, font_size) }, + .{ .loaded = try font.Face.init(font_lib, face_emoji_ttf, group.faceOptions()) }, ); _ = try group.addFace( .regular, - .{ .loaded = try font.Face.init(font_lib, face_emoji_text_ttf, font_size) }, + .{ .loaded = try font.Face.init(font_lib, face_emoji_text_ttf, group.faceOptions()) }, ); } diff --git a/src/config/Config.zig b/src/config/Config.zig index f9913e440..6701095d0 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -15,6 +15,7 @@ const cli = @import("../cli.zig"); const Key = @import("key.zig").Key; const KeyValue = @import("key.zig").Value; const ErrorList = @import("ErrorList.zig"); +const MetricModifier = fontpkg.face.Metrics.Modifier; const log = std.log.scoped(.config); @@ -122,6 +123,22 @@ const c = @cImport({ /// currently on macOS. @"font-thicken": bool = false, +/// All of the configurations behavior adjust various metrics determined +/// by the font. The values can be integers (1, -1, etc.) or a percentage +/// (20%, -15%, etc.). In each case, the values represent the amount to +/// change the original value. +/// +/// For example, a value of "1" increases the value by 1; it does not set +/// it to literally 1. A value of "20%" increases the value by 20%. And so +/// on. +/// +/// There is little to no validation on these values so the wrong values +/// (i.e. "-100%") can cause the terminal to be unusable. Use with caution +/// and reason. +@"adjust-cell-width": ?MetricModifier = null, +@"adjust-cell-height": ?MetricModifier = null, +@"adjust-font-baseline": ?MetricModifier = null, + /// Background color for the window. background: Color = .{ .r = 0x28, .g = 0x2C, .b = 0x34 }, diff --git a/src/font/DeferredFace.zig b/src/font/DeferredFace.zig index bddf012a4..243537002 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -192,7 +192,8 @@ fn loadCoreText( ) !Face { _ = lib; const ct = self.ct.?; - return try Face.initFontCopy(ct.font, size); + // TODO: make options + return try Face.initFontCopy(ct.font, .{ .size = size }); } fn loadCoreTextFreetype( diff --git a/src/font/Group.zig b/src/font/Group.zig index 69f41da38..f257185d5 100644 --- a/src/font/Group.zig +++ b/src/font/Group.zig @@ -71,6 +71,10 @@ lib: Library, /// The desired font size. All fonts in a group must share the same size. size: font.face.DesiredSize, +/// Metric modifiers to apply to loaded fonts. The Group takes ownership +/// over the memory and will use the associated allocator to free it. +metric_modifiers: ?font.face.Metrics.ModifierSet = null, + /// The available faces we have. This shouldn't be modified manually. /// Instead, use the functions available on Group. faces: StyleArray, @@ -139,9 +143,20 @@ pub fn deinit(self: *Group) void { } } + if (self.metric_modifiers) |*v| v.deinit(self.alloc); + self.descriptor_cache.deinit(self.alloc); } +/// Returns the options for initializing a face based on the options associated +/// with this font group. +pub fn faceOptions(self: *const Group) font.face.Options { + return .{ + .size = self.size, + .metric_modifiers = if (self.metric_modifiers) |*v| v else null, + }; +} + /// Add a face to the list for the given style. This face will be added as /// next in priority if others exist already, i.e. it'll be the _last_ to be /// searched for a glyph in that list. @@ -205,7 +220,7 @@ pub fn setSize(self: *Group, size: font.face.DesiredSize) !void { while (it.next()) |entry| { for (entry.value.items) |*elem| switch (elem.*) { .deferred => continue, - .loaded => |*f| try f.setSize(size), + .loaded => |*f| try f.setSize(.{ .size = size }), }; } @@ -623,13 +638,22 @@ test { var group = try init(alloc, lib, .{ .points = 12 }); defer group.deinit(); - _ = try group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, .{ .points = 12 }) }); + _ = try group.addFace( + .regular, + .{ .loaded = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } }) }, + ); if (font.options.backend != .coretext) { // Coretext doesn't support Noto's format - _ = try group.addFace(.regular, .{ .loaded = try Face.init(lib, testEmoji, .{ .points = 12 }) }); + _ = try group.addFace( + .regular, + .{ .loaded = try Face.init(lib, testEmoji, .{ .size = .{ .points = 12 } }) }, + ); } - _ = try group.addFace(.regular, .{ .loaded = try Face.init(lib, testEmojiText, .{ .points = 12 }) }); + _ = try group.addFace( + .regular, + .{ .loaded = try Face.init(lib, testEmojiText, .{ .size = .{ .points = 12 } }) }, + ); // Should find all visible ASCII var i: u32 = 32; @@ -694,9 +718,10 @@ test "disabled font style" { group.styles.set(.bold, false); // Same font but we can test the style in the index - _ = try group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, .{ .points = 12 }) }); - _ = try group.addFace(.bold, .{ .loaded = try Face.init(lib, testFont, .{ .points = 12 }) }); - _ = try group.addFace(.italic, .{ .loaded = try Face.init(lib, testFont, .{ .points = 12 }) }); + const opts: font.face.Options = .{ .size = .{ .points = 12 } }; + _ = try group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, opts) }); + _ = try group.addFace(.bold, .{ .loaded = try Face.init(lib, testFont, opts) }); + _ = try group.addFace(.italic, .{ .loaded = try Face.init(lib, testFont, opts) }); // Regular should work fine { @@ -731,16 +756,17 @@ test "face count limit" { var lib = try Library.init(); defer lib.deinit(); - var group = try init(alloc, lib, .{ .points = 12 }); + const opts: font.face.Options = .{ .size = .{ .points = 12 } }; + var group = try init(alloc, lib, opts.size); defer group.deinit(); for (0..FontIndex.Special.start - 1) |_| { - _ = try group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, .{ .points = 12 }) }); + _ = try group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, opts) }); } try testing.expectError(error.GroupFull, group.addFace( .regular, - .{ .loaded = try Face.init(lib, testFont, .{ .points = 12 }) }, + .{ .loaded = try Face.init(lib, testFont, opts) }, )); } @@ -790,7 +816,11 @@ test "resize" { var group = try init(alloc, lib, .{ .points = 12, .xdpi = 96, .ydpi = 96 }); defer group.deinit(); - _ = try group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, .{ .points = 12, .xdpi = 96, .ydpi = 96 }) }); + _ = try group.addFace(.regular, .{ .loaded = try Face.init( + lib, + testFont, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ) }); // Load a letter { @@ -881,7 +911,11 @@ test "faceFromIndex returns pointer" { var group = try init(alloc, lib, .{ .points = 12, .xdpi = 96, .ydpi = 96 }); defer group.deinit(); - _ = try group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, .{ .points = 12, .xdpi = 96, .ydpi = 96 }) }); + _ = try group.addFace(.regular, .{ .loaded = try Face.init( + lib, + testFont, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ) }); { const idx = group.indexForCodepoint('A', .regular, null).?; diff --git a/src/font/GroupCache.zig b/src/font/GroupCache.zig index 504a88ba9..511ec8137 100644 --- a/src/font/GroupCache.zig +++ b/src/font/GroupCache.zig @@ -184,7 +184,7 @@ test { // Setup group _ = try cache.group.addFace( .regular, - .{ .loaded = try Face.init(lib, testFont, .{ .points = 12 }) }, + .{ .loaded = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } }) }, ); var group = cache.group; @@ -340,7 +340,11 @@ test "resize" { // Setup group _ = try cache.group.addFace( .regular, - .{ .loaded = try Face.init(lib, testFont, .{ .points = 12, .xdpi = 96, .ydpi = 96 }) }, + .{ .loaded = try Face.init( + lib, + testFont, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ) }, ); // Load a letter diff --git a/src/font/face.zig b/src/font/face.zig index 3fdd8fbc5..e6dd746b2 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -22,6 +22,12 @@ pub const Face = switch (options.backend) { /// using whatever platform method you can. pub const default_dpi = if (builtin.os.tag == .macos) 72 else 96; +/// Options for initializing a font face. +pub const Options = struct { + size: DesiredSize, + metric_modifiers: ?*const Metrics.ModifierSet = null, +}; + /// The desired size for loading a font. pub const DesiredSize = struct { // Desired size in points diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 7b738a322..fbfda7d90 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -36,7 +36,7 @@ pub const Face = struct { }; /// Initialize a CoreText-based font from a TTF/TTC in memory. - pub fn init(lib: font.Library, source: [:0]const u8, size: font.face.DesiredSize) !Face { + pub fn init(lib: font.Library, source: [:0]const u8, opts: font.face.Options) !Face { _ = lib; const data = try macos.foundation.Data.createWithBytesNoCopy(source); @@ -51,18 +51,22 @@ pub const Face = struct { const ct_font = try macos.text.Font.createWithFontDescriptor(desc, 12); defer ct_font.release(); - return try initFontCopy(ct_font, size); + return try initFontCopy(ct_font, opts); } /// Initialize a CoreText-based face from another initialized font face /// but with a new size. This is often how CoreText fonts are initialized /// because the font is loaded at a default size during discovery, and then /// adjusted to the final size for final load. - pub fn initFontCopy(base: *macos.text.Font, size: font.face.DesiredSize) !Face { + pub fn initFontCopy(base: *macos.text.Font, opts: font.face.Options) !Face { // Create a copy. The copyWithAttributes docs say the size is in points, // but we need to scale the points by the DPI and to do that we use our // function called "pixels". - const ct_font = try base.copyWithAttributes(@floatFromInt(size.pixels()), null, null); + const ct_font = try base.copyWithAttributes( + @floatFromInt(opts.size.pixels()), + null, + null, + ); errdefer ct_font.release(); return try initFont(ct_font); @@ -161,9 +165,9 @@ pub const Face = struct { /// Resize the font in-place. If this succeeds, the caller is responsible /// for clearing any glyph caches, font atlas data, etc. - pub fn setSize(self: *Face, size: font.face.DesiredSize) !void { + pub fn setSize(self: *Face, opts: font.face.Options) !void { // We just create a copy and replace ourself - const face = try initFontCopy(self.font, size); + const face = try initFontCopy(self.font, opts); self.deinit(); self.* = face; } @@ -514,7 +518,7 @@ test { const ct_font = try macos.text.Font.createWithFontDescriptor(desc, 12); defer ct_font.release(); - var face = try Face.initFontCopy(ct_font, .{ .points = 12 }); + var face = try Face.initFontCopy(ct_font, .{ .size = .{ .points = 12 } }); defer face.deinit(); try testing.expectEqual(font.Presentation.text, face.presentation); @@ -537,7 +541,7 @@ test "emoji" { const ct_font = try macos.text.Font.createWithFontDescriptor(desc, 12); defer ct_font.release(); - var face = try Face.initFontCopy(ct_font, .{ .points = 18 }); + var face = try Face.initFontCopy(ct_font, .{ .size = .{ .points = 18 } }); defer face.deinit(); // Presentation @@ -558,7 +562,7 @@ test "in-memory" { var lib = try font.Library.init(); defer lib.deinit(); - var face = try Face.init(lib, testFont, .{ .points = 12 }); + var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } }); defer face.deinit(); try testing.expectEqual(font.Presentation.text, face.presentation); @@ -582,7 +586,7 @@ test "variable" { var lib = try font.Library.init(); defer lib.deinit(); - var face = try Face.init(lib, testFont, .{ .points = 12 }); + var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } }); defer face.deinit(); try testing.expectEqual(font.Presentation.text, face.presentation); @@ -606,7 +610,7 @@ test "variable set variation" { var lib = try font.Library.init(); defer lib.deinit(); - var face = try Face.init(lib, testFont, .{ .points = 12 }); + var face = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } }); defer face.deinit(); try testing.expectEqual(font.Presentation.text, face.presentation); diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index 6c7c91329..781d5a708 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -899,11 +899,19 @@ fn testShaper(alloc: Allocator) !TestShaper { errdefer cache_ptr.*.deinit(alloc); // Setup group - _ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init(lib, testFont, .{ .points = 12 }) }); + _ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init( + lib, + testFont, + .{ .size = .{ .points = 12 } }, + ) }); if (font.options.backend != .coretext) { // Coretext doesn't support Noto's format - _ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init(lib, testEmoji, .{ .points = 12 }) }); + _ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init( + lib, + testEmoji, + .{ .size = .{ .points = 12 } }, + ) }); } else { // On CoreText we want to load Apple Emoji, we should have it. var disco = font.Discover.init(); @@ -918,7 +926,11 @@ fn testShaper(alloc: Allocator) !TestShaper { errdefer face.deinit(); _ = try cache_ptr.group.addFace(.regular, .{ .deferred = face }); } - _ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init(lib, testEmojiText, .{ .points = 12 }) }); + _ = try cache_ptr.group.addFace(.regular, .{ .loaded = try Face.init( + lib, + testEmojiText, + .{ .size = .{ .points = 12 } }, + ) }); var shaper = try Shaper.init(alloc, .{}); errdefer shaper.deinit();