diff --git a/src/Surface.zig b/src/Surface.zig index 872e8fe1d..2d448216e 100644 --- a/src/Surface.zig +++ b/src/Surface.zig @@ -224,6 +224,20 @@ 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); + if (config.@"adjust-cell-width") |m| try set.put(alloc, .cell_width, m); + if (config.@"adjust-cell-height") |m| try set.put(alloc, .cell_height, m); + if (config.@"adjust-font-baseline") |m| try set.put(alloc, .cell_baseline, m); + if (config.@"adjust-underline-position") |m| try set.put(alloc, .underline_position, m); + if (config.@"adjust-underline-thickness") |m| try set.put(alloc, .underline_thickness, m); + if (config.@"adjust-strikethrough-position") |m| try set.put(alloc, .strikethrough_position, m); + if (config.@"adjust-strikethrough-thickness") |m| try set.put(alloc, .strikethrough_thickness, m); + break :set set; + }; + // 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 +320,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 +335,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..64f39dd9e 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,32 @@ 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. +/// +/// Some values are clamped to minimum or maximum values. This can make it +/// appear that certain values are ignored. For example, the underline +/// position is clamped to the height of a cell. If you set the underline +/// position so high that it extends beyond the bottom of the cell size, +/// it will be clamped to the bottom of the cell. +@"adjust-cell-width": ?MetricModifier = null, +@"adjust-cell-height": ?MetricModifier = null, +@"adjust-font-baseline": ?MetricModifier = null, +@"adjust-underline-position": ?MetricModifier = null, +@"adjust-underline-thickness": ?MetricModifier = null, +@"adjust-strikethrough-position": ?MetricModifier = null, +@"adjust-strikethrough-thickness": ?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..70dfafe7b 100644 --- a/src/font/DeferredFace.zig +++ b/src/font/DeferredFace.zig @@ -154,13 +154,13 @@ pub fn name(self: DeferredFace, buf: []u8) ![]const u8 { pub fn load( self: *DeferredFace, lib: Library, - size: font.face.DesiredSize, + opts: font.face.Options, ) !Face { return switch (options.backend) { - .fontconfig_freetype => try self.loadFontconfig(lib, size), - .coretext => try self.loadCoreText(lib, size), - .coretext_freetype => try self.loadCoreTextFreetype(lib, size), - .web_canvas => try self.loadWebCanvas(size), + .fontconfig_freetype => try self.loadFontconfig(lib, opts), + .coretext => try self.loadCoreText(lib, opts), + .coretext_freetype => try self.loadCoreTextFreetype(lib, opts), + .web_canvas => try self.loadWebCanvas(opts), // Unreachable because we must be already loaded or have the // proper configuration for one of the other deferred mechanisms. @@ -171,7 +171,7 @@ pub fn load( fn loadFontconfig( self: *DeferredFace, lib: Library, - size: font.face.DesiredSize, + opts: font.face.Options, ) !Face { const fc = self.fc.?; @@ -179,26 +179,26 @@ fn loadFontconfig( const filename = (try fc.pattern.get(.file, 0)).string; const face_index = (try fc.pattern.get(.index, 0)).integer; - var face = try Face.initFile(lib, filename, face_index, size); + var face = try Face.initFile(lib, filename, face_index, opts); errdefer face.deinit(); - try face.setVariations(fc.variations); + try face.setVariations(fc.variations, opts); return face; } fn loadCoreText( self: *DeferredFace, lib: Library, - size: font.face.DesiredSize, + opts: font.face.Options, ) !Face { _ = lib; const ct = self.ct.?; - return try Face.initFontCopy(ct.font, size); + return try Face.initFontCopy(ct.font, opts); } fn loadCoreTextFreetype( self: *DeferredFace, lib: Library, - size: font.face.DesiredSize, + opts: font.face.Options, ) !Face { const ct = self.ct.?; @@ -231,15 +231,15 @@ fn loadCoreTextFreetype( // TODO: face index 0 is not correct long term and we should switch // to using CoreText for rendering, too. //std.log.warn("path={s}", .{path_slice}); - return try Face.initFile(lib, buf[0..path_slice.len :0], 0, size); + return try Face.initFile(lib, buf[0..path_slice.len :0], 0, opts); } fn loadWebCanvas( self: *DeferredFace, - size: font.face.DesiredSize, + opts: font.face.Options, ) !Face { const wc = self.wc.?; - return try Face.initNamed(wc.alloc, wc.font_str, size, wc.presentation); + return try Face.initNamed(wc.alloc, wc.font_str, opts, wc.presentation); } /// Returns true if this face can satisfy the given codepoint and @@ -400,7 +400,7 @@ test "fontconfig" { try testing.expect(n.len > 0); // Load it and verify it works - const face = try def.load(lib, .{ .points = 12 }); + const face = try def.load(lib, .{ .size = .{ .points = 12 } }); try testing.expect(face.glyphIndex(' ') != null); } diff --git a/src/font/Group.zig b/src/font/Group.zig index 69f41da38..67503abf3 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. @@ -185,7 +200,7 @@ pub fn italicize(self: *Group) !void { }; // Try to italicize it. - const face = try regular.italicize(); + const face = try regular.italicize(self.faceOptions()); try italic_list.append(self.alloc, .{ .loaded = face }); var buf: [128]u8 = undefined; @@ -200,17 +215,17 @@ pub fn setSize(self: *Group, size: font.face.DesiredSize) !void { // currently handle it in any meaningful way if one face can resize // but another can't. + // Set our size for future loads + self.size = size; + // Resize all our faces that are loaded var it = self.faces.iterator(); while (it.next()) |entry| { for (entry.value.items) |*elem| switch (elem.*) { .deferred => continue, - .loaded => |*f| try f.setSize(size), + .loaded => |*f| try f.setSize(self.faceOptions()), }; } - - // Set our size for future loads - self.size = size; } /// This represents a specific font in the group. @@ -456,7 +471,7 @@ pub fn faceFromIndex(self: *Group, index: FontIndex) !*Face { const item = &list.items[index.idx]; return switch (item.*) { .deferred => |*d| deferred: { - const face = try d.load(self.lib, self.size); + const face = try d.load(self.lib, self.faceOptions()); d.deinit(); item.* = .{ .loaded = face }; break :deferred &item.loaded; @@ -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 765b7dfe2..e6dd746b2 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -1,6 +1,7 @@ const std = @import("std"); const builtin = @import("builtin"); const options = @import("main.zig").options; +pub const Metrics = @import("face/Metrics.zig"); const freetype = @import("face/freetype.zig"); const coretext = @import("face/coretext.zig"); pub const web_canvas = @import("face/web_canvas.zig"); @@ -21,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 @@ -62,28 +69,6 @@ pub const Variation = struct { }; }; -/// Metrics associated with the font that are useful for renderers to know. -pub const Metrics = struct { - /// Recommended cell width and height for a monospace grid using this font. - cell_width: u32, - cell_height: u32, - - /// For monospace grids, the recommended y-value from the bottom to set - /// the baseline for font rendering. This is chosen so that things such - /// as the bottom of a "g" or "y" do not drop below the cell. - cell_baseline: u32, - - /// The position of the underline from the top of the cell and the - /// thickness in pixels. - underline_position: u32, - underline_thickness: u32, - - /// The position and thickness of a strikethrough. Same units/style - /// as the underline fields. - strikethrough_position: u32, - strikethrough_thickness: u32, -}; - /// Additional options for rendering glyphs. pub const RenderOptions = struct { /// The maximum height of the glyph. If this is set, then any glyph diff --git a/src/font/face/Metrics.zig b/src/font/face/Metrics.zig new file mode 100644 index 000000000..2e7309dc4 --- /dev/null +++ b/src/font/face/Metrics.zig @@ -0,0 +1,226 @@ +const Metrics = @This(); + +const std = @import("std"); + +/// Recommended cell width and height for a monospace grid using this font. +cell_width: u32, +cell_height: u32, + +/// For monospace grids, the recommended y-value from the bottom to set +/// the baseline for font rendering. This is chosen so that things such +/// as the bottom of a "g" or "y" do not drop below the cell. +cell_baseline: u32, + +/// The position of the underline from the top of the cell and the +/// thickness in pixels. +underline_position: u32, +underline_thickness: u32, + +/// The position and thickness of a strikethrough. Same units/style +/// as the underline fields. +strikethrough_position: u32, +strikethrough_thickness: u32, + +/// Apply a set of modifiers. +pub fn apply(self: *Metrics, mods: ModifierSet) void { + var it = mods.iterator(); + while (it.next()) |entry| { + switch (entry.key_ptr.*) { + // We clamp these values to a minimum of 1 to prevent divide-by-zero + // in downstream operations. + inline .cell_width, + .cell_height, + => |tag| { + const original = @field(self, @tagName(tag)); + @field(self, @tagName(tag)) = @max( + entry.value_ptr.apply(original), + 1, + ); + }, + + inline else => |tag| { + @field(self, @tagName(tag)) = entry.value_ptr.apply(@field(self, @tagName(tag))); + }, + } + } +} + +/// A set of modifiers to apply to metrics. We use a hash map because +/// we expect most metrics to be unmodified and want to take up as +/// little space as possible. +pub const ModifierSet = std.AutoHashMapUnmanaged(Key, Modifier); + +/// A modifier to apply to a metrics value. The modifier value represents +/// a delta, so percent is a percentage to change, not a percentage of. +/// For example, "20%" is 20% larger, not 20% of the value. Likewise, +/// an absolute value of "20" is 20 larger, not literally 20. +pub const Modifier = union(enum) { + percent: f64, + absolute: i32, + + /// Parses the modifier value. If the value ends in "%" it is assumed + /// to be a percent, otherwise the value is parsed as an integer. + pub fn parse(input: []const u8) !Modifier { + if (input.len == 0) return error.InvalidFormat; + + if (input[input.len - 1] == '%') { + var percent = std.fmt.parseFloat( + f64, + input[0 .. input.len - 1], + ) catch return error.InvalidFormat; + percent /= 100; + + if (percent <= -1) return .{ .percent = 0 }; + if (percent < 0) return .{ .percent = 1 + percent }; + return .{ .percent = 1 + percent }; + } + + return .{ + .absolute = std.fmt.parseInt(i32, input, 10) catch + return error.InvalidFormat, + }; + } + + /// So it works with the config framework. + pub fn parseCLI(input: ?[]const u8) !Modifier { + return try parse(input orelse return error.ValueRequired); + } + + /// Apply a modifier to a numeric value. + pub fn apply(self: Modifier, v: u32) u32 { + return switch (self) { + .percent => |p| percent: { + const p_clamped: f64 = @max(0, p); + const v_f64: f64 = @floatFromInt(v); + const applied_f64: f64 = @round(v_f64 * p_clamped); + const applied_u32: u32 = @intFromFloat(applied_f64); + break :percent applied_u32; + }, + + .absolute => |abs| absolute: { + const v_i64: i64 = @intCast(v); + const abs_i64: i64 = @intCast(abs); + const applied_i64: i64 = @max(0, v_i64 +| abs_i64); + const applied_u32: u32 = std.math.cast(u32, applied_i64) orelse + std.math.maxInt(u32); + break :absolute applied_u32; + }, + }; + } +}; + +/// Key is an enum of all the available metrics keys. +pub const Key = key: { + const field_infos = std.meta.fields(Metrics); + var enumFields: [field_infos.len]std.builtin.Type.EnumField = undefined; + for (field_infos, 0..) |field, i| { + enumFields[i] = .{ + .name = field.name, + .value = i, + }; + } + + var decls = [_]std.builtin.Type.Declaration{}; + break :key @Type(.{ + .Enum = .{ + .tag_type = std.math.IntFittingRange(0, field_infos.len - 1), + .fields = &enumFields, + .decls = &decls, + .is_exhaustive = true, + }, + }); +}; + +// NOTE: This is purposely not pub because we want to force outside callers +// to use the `.{}` syntax so unused fields are detected by the compiler. +fn init() Metrics { + return .{ + .cell_width = 0, + .cell_height = 0, + .cell_baseline = 0, + .underline_position = 0, + .underline_thickness = 0, + .strikethrough_position = 0, + .strikethrough_thickness = 0, + }; +} + +test "Metrics: apply modifiers" { + const testing = std.testing; + const alloc = testing.allocator; + + var set: ModifierSet = .{}; + defer set.deinit(alloc); + try set.put(alloc, .cell_width, .{ .percent = 1.2 }); + + var m: Metrics = init(); + m.cell_width = 100; + m.apply(set); + try testing.expectEqual(@as(u32, 120), m.cell_width); +} + +test "Modifier: parse absolute" { + const testing = std.testing; + + { + const m = try Modifier.parse("100"); + try testing.expectEqual(Modifier{ .absolute = 100 }, m); + } + + { + const m = try Modifier.parse("-100"); + try testing.expectEqual(Modifier{ .absolute = -100 }, m); + } +} + +test "Modifier: parse percent" { + const testing = std.testing; + + { + const m = try Modifier.parse("20%"); + try testing.expectEqual(Modifier{ .percent = 1.2 }, m); + } + { + const m = try Modifier.parse("-20%"); + try testing.expectEqual(Modifier{ .percent = 0.8 }, m); + } + { + const m = try Modifier.parse("0%"); + try testing.expectEqual(Modifier{ .percent = 1 }, m); + } +} + +test "Modifier: percent" { + const testing = std.testing; + + { + const m: Modifier = .{ .percent = 0.8 }; + const v: u32 = m.apply(100); + try testing.expectEqual(@as(u32, 80), v); + } + { + const m: Modifier = .{ .percent = 1.8 }; + const v: u32 = m.apply(100); + try testing.expectEqual(@as(u32, 180), v); + } +} + +test "Modifier: absolute" { + const testing = std.testing; + + { + const m: Modifier = .{ .absolute = -100 }; + const v: u32 = m.apply(100); + try testing.expectEqual(@as(u32, 0), v); + } + { + const m: Modifier = .{ .absolute = -120 }; + const v: u32 = m.apply(100); + try testing.expectEqual(@as(u32, 0), v); + } + { + const m: Modifier = .{ .absolute = 100 }; + const v: u32 = m.apply(100); + try testing.expectEqual(@as(u32, 200), v); + } +} diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 7b738a322..803cb46b1 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,36 +51,45 @@ 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); + return try initFont(ct_font, opts); } /// Initialize a face with a CTFont. This will take ownership over /// the CTFont. This does NOT copy or retain the CTFont. - pub fn initFont(ct_font: *macos.text.Font) !Face { + pub fn initFont(ct_font: *macos.text.Font, opts: font.face.Options) !Face { var hb_font = try harfbuzz.coretext.createFont(ct_font); errdefer hb_font.destroy(); const traits = ct_font.getSymbolicTraits(); + const metrics = metrics: { + var metrics = try calcMetrics(ct_font); + if (opts.metric_modifiers) |v| metrics.apply(v.*); + break :metrics metrics; + }; var result: Face = .{ .font = ct_font, .hb_font = hb_font, .presentation = if (traits.color_glyphs) .emoji else .text, - .metrics = try calcMetrics(ct_font), + .metrics = metrics, }; result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result); @@ -140,10 +149,10 @@ pub const Face = struct { /// Return a new face that is the same as this but has a transformation /// matrix applied to italicize it. - pub fn italicize(self: *const Face) !Face { + pub fn italicize(self: *const Face, opts: font.face.Options) !Face { const ct_font = try self.font.copyWithAttributes(0.0, &italic_skew, null); errdefer ct_font.release(); - return try initFont(ct_font); + return try initFont(ct_font, opts); } /// Returns the font name. If allocation is required, buf will be used, @@ -161,9 +170,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 +523,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 +546,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 +567,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 +591,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 +615,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/face/freetype.zig b/src/font/face/freetype.zig index 9ae861c7d..66c27aa99 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -43,22 +43,22 @@ pub const Face = struct { quirks_disable_default_font_features: bool = false, /// Initialize a new font face with the given source in-memory. - pub fn initFile(lib: Library, path: [:0]const u8, index: i32, size: font.face.DesiredSize) !Face { + pub fn initFile(lib: Library, path: [:0]const u8, index: i32, opts: font.face.Options) !Face { const face = try lib.lib.initFace(path, index); errdefer face.deinit(); - return try initFace(lib, face, size); + return try initFace(lib, face, opts); } /// Initialize a new font face with the given source in-memory. - pub fn init(lib: Library, source: [:0]const u8, size: font.face.DesiredSize) !Face { + pub fn init(lib: Library, source: [:0]const u8, opts: font.face.Options) !Face { const face = try lib.lib.initMemoryFace(source, 0); errdefer face.deinit(); - return try initFace(lib, face, size); + return try initFace(lib, face, opts); } - fn initFace(lib: Library, face: freetype.Face, size: font.face.DesiredSize) !Face { + fn initFace(lib: Library, face: freetype.Face, opts: font.face.Options) !Face { try face.selectCharmap(.unicode); - try setSize_(face, size); + try setSize_(face, opts.size); var hb_font = try harfbuzz.freetype.createFont(face.handle); errdefer hb_font.destroy(); @@ -68,7 +68,7 @@ pub const Face = struct { .face = face, .hb_font = hb_font, .presentation = if (face.hasColor()) .emoji else .text, - .metrics = calcMetrics(face), + .metrics = calcMetrics(face, opts.metric_modifiers), }; result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result); @@ -127,9 +127,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 { - try setSize_(self.face, size); - self.metrics = calcMetrics(self.face); + pub fn setSize(self: *Face, opts: font.face.Options) !void { + try setSize_(self.face, opts.size); + self.metrics = calcMetrics(self.face, opts.metric_modifiers); } fn setSize_(face: freetype.Face, size: font.face.DesiredSize) !void { @@ -165,6 +165,7 @@ pub const Face = struct { pub fn setVariations( self: *Face, vs: []const font.face.Variation, + opts: font.face.Options, ) !void { // If this font doesn't support variations, we can't do anything. if (!self.face.hasMultipleMasters() or vs.len == 0) return; @@ -201,7 +202,7 @@ pub const Face = struct { try self.face.setVarDesignCoordinates(coords); // We need to recalculate font metrics which may have changed. - self.metrics = calcMetrics(self.face); + self.metrics = calcMetrics(self.face, opts.metric_modifiers); } /// Returns the glyph index for the given Unicode code point. If this @@ -470,7 +471,10 @@ pub const Face = struct { /// the faces with DeferredFaces and reload on demand. A Face can't be converted /// into a DeferredFace but a Face that comes from a DeferredFace can be /// deinitialized anytime and reloaded with the deferred face. - fn calcMetrics(face: freetype.Face) font.face.Metrics { + fn calcMetrics( + face: freetype.Face, + modifiers: ?*const font.face.Metrics.ModifierSet, + ) font.face.Metrics { const size_metrics = face.handle.*.size.*.metrics; // Cell width is calculated by preferring to use 'M' as the width of a @@ -574,7 +578,7 @@ pub const Face = struct { .thickness = underline_thickness, }; - const result = font.face.Metrics{ + var result = font.face.Metrics{ .cell_width = @intFromFloat(cell_width), .cell_height = @intFromFloat(cell_height), .cell_baseline = @intFromFloat(cell_baseline), @@ -583,6 +587,7 @@ pub const Face = struct { .strikethrough_position = @intFromFloat(strikethrough.pos), .strikethrough_thickness = @intFromFloat(strikethrough.thickness), }; + if (modifiers) |m| result.apply(m.*); // std.log.warn("font metrics={}", .{result}); @@ -607,7 +612,11 @@ test { var atlas = try font.Atlas.init(alloc, 512, .greyscale); defer atlas.deinit(alloc); - var ft_font = try Face.init(lib, testFont, .{ .points = 12, .xdpi = 96, .ydpi = 96 }); + var ft_font = try Face.init( + lib, + testFont, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ); defer ft_font.deinit(); try testing.expectEqual(Presentation.text, ft_font.presentation); @@ -623,7 +632,7 @@ test { const g1 = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('A').?, .{}); try testing.expectEqual(@as(u32, 11), g1.height); - try ft_font.setSize(.{ .points = 24, .xdpi = 96, .ydpi = 96 }); + try ft_font.setSize(.{ .size = .{ .points = 24, .xdpi = 96, .ydpi = 96 } }); const g2 = try ft_font.renderGlyph(alloc, &atlas, ft_font.glyphIndex('A').?, .{}); try testing.expectEqual(@as(u32, 21), g2.height); } @@ -639,7 +648,11 @@ test "color emoji" { var atlas = try font.Atlas.init(alloc, 512, .rgba); defer atlas.deinit(alloc); - var ft_font = try Face.init(lib, testFont, .{ .points = 12, .xdpi = 96, .ydpi = 96 }); + var ft_font = try Face.init( + lib, + testFont, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ); defer ft_font.deinit(); try testing.expectEqual(Presentation.emoji, ft_font.presentation); @@ -665,7 +678,11 @@ test "metrics" { var atlas = try font.Atlas.init(alloc, 512, .greyscale); defer atlas.deinit(alloc); - var ft_font = try Face.init(lib, testFont, .{ .points = 12, .xdpi = 96, .ydpi = 96 }); + var ft_font = try Face.init( + lib, + testFont, + .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + ); defer ft_font.deinit(); try testing.expectEqual(font.face.Metrics{ @@ -679,7 +696,7 @@ test "metrics" { }, ft_font.metrics); // Resize should change metrics - try ft_font.setSize(.{ .points = 24, .xdpi = 96, .ydpi = 96 }); + try ft_font.setSize(.{ .size = .{ .points = 24, .xdpi = 96, .ydpi = 96 } }); try testing.expectEqual(font.face.Metrics{ .cell_width = 16, .cell_height = 35, @@ -701,7 +718,7 @@ test "mono to rgba" { var atlas = try font.Atlas.init(alloc, 512, .rgba); defer atlas.deinit(alloc); - var ft_font = try Face.init(lib, testFont, .{ .points = 12 }); + var ft_font = try Face.init(lib, testFont, .{ .size = .{ .points = 12 } }); defer ft_font.deinit(); // glyph 3 is mono in Noto 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(); diff --git a/src/font/sprite/underline.zig b/src/font/sprite/underline.zig index 8979f2065..5317667e8 100644 --- a/src/font/sprite/underline.zig +++ b/src/font/sprite/underline.zig @@ -171,8 +171,9 @@ const Draw = struct { // wave. This constant is arbitrary, change it for aesthetics. const pos = pos: { const MIN_HEIGHT = 7; - const height = y_max - self.pos; - break :pos if (height < MIN_HEIGHT) self.pos -| MIN_HEIGHT else self.pos; + const clamped_pos = @min(y_max, self.pos); + const height = y_max - clamped_pos; + break :pos if (height < MIN_HEIGHT) clamped_pos -| MIN_HEIGHT else clamped_pos; }; // The full heightof the wave can be from the bottom to the diff --git a/src/renderer/Metal.zig b/src/renderer/Metal.zig index f33553f6b..28182f8d1 100644 --- a/src/renderer/Metal.zig +++ b/src/renderer/Metal.zig @@ -211,7 +211,8 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal { options.font_group.group.sprite = font.sprite.Face{ .width = metrics.cell_width, .height = metrics.cell_height, - .thickness = 2 * @as(u32, if (options.config.font_thicken) 2 else 1), + .thickness = metrics.underline_thickness * + @as(u32, if (options.config.font_thicken) 2 else 1), .underline_position = metrics.underline_position, }; diff --git a/src/renderer/OpenGL.zig b/src/renderer/OpenGL.zig index 96b3de5e1..ff4dab542 100644 --- a/src/renderer/OpenGL.zig +++ b/src/renderer/OpenGL.zig @@ -548,7 +548,7 @@ fn resetFontMetrics( font_group.group.sprite = font.sprite.Face{ .width = metrics.cell_width, .height = metrics.cell_height, - .thickness = 2 * @as(u32, if (font_thicken) 2 else 1), + .thickness = metrics.underline_thickness * @as(u32, if (font_thicken) 2 else 1), .underline_position = metrics.underline_position, };