From 6491ea41fb4cc671c6e3089bf6eb3d1ec752de2b Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Tue, 15 Jul 2025 12:57:23 -0700 Subject: [PATCH 01/11] Move face metric fallback estimates to the FaceMetric struct --- src/font/Collection.zig | 29 ++++--------- src/font/Metrics.zig | 90 ++++++++++++++++++++++++++++------------- 2 files changed, 68 insertions(+), 51 deletions(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index eb4349fb0..702a3fd7c 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -154,30 +154,13 @@ pub fn adjustedSize( // We use the ex height to match our font sizes, so that the height of // lower-case letters matches between all fonts in the fallback chain. // - // We estimate ex height as 0.75 * cap height if it's not specifically - // provided, and we estimate cap height as 0.75 * ascent in the same case. - // // If the fallback font has an ic_width we prefer that, for normalization // of CJK font sizes when mixed with latin fonts. - // - // We estimate the ic_width as twice the cell width if it isn't provided. - var primary_cap = primary_metrics.cap_height orelse 0.0; - if (primary_cap <= 0) primary_cap = primary_metrics.ascent * 0.75; + const primary_ex = primary_metrics.exHeight(); + const primary_ic = primary_metrics.icWidth(); - var primary_ex = primary_metrics.ex_height orelse 0.0; - if (primary_ex <= 0) primary_ex = primary_cap * 0.75; - - var primary_ic = primary_metrics.ic_width orelse 0.0; - if (primary_ic <= 0) primary_ic = primary_metrics.cell_width * 2; - - var face_cap = face_metrics.cap_height orelse 0.0; - if (face_cap <= 0) face_cap = face_metrics.ascent * 0.75; - - var face_ex = face_metrics.ex_height orelse 0.0; - if (face_ex <= 0) face_ex = face_cap * 0.75; - - var face_ic = face_metrics.ic_width orelse 0.0; - if (face_ic <= 0) face_ic = face_metrics.cell_width * 2; + const face_ex = face_metrics.exHeight(); + const face_ic = face_metrics.icWidth(); // If the line height of the scaled font would be larger than // the line height of the primary font, we don't want that, so @@ -192,7 +175,9 @@ pub fn adjustedSize( // the user pick what metric to use for size adjustment. const scale = @min( 1.2 * primary_metrics.lineHeight() / face_metrics.lineHeight(), - if (face_metrics.ic_width != null) + if ((face_metrics.ic_width != null) and (face_metrics.ic_width == face_ic)) + // It's possible for .ic_width to be non-null and still invalid, e.g., + // zero, so we only take this branch if it's also equal to .icWidth(). primary_ic / face_ic else primary_ex / face_ex, diff --git a/src/font/Metrics.zig b/src/font/Metrics.zig index 89f6a507f..320a4f504 100644 --- a/src/font/Metrics.zig +++ b/src/font/Metrics.zig @@ -120,6 +120,60 @@ pub const FaceMetrics = struct { pub inline fn lineHeight(self: FaceMetrics) f64 { return self.ascent - self.descent + self.line_gap; } + + /// Convenience function for getting the cap height. If this is not + /// defined in the font, we estimate it as 75% of the ascent. + pub inline fn capHeight(self: FaceMetrics) f64 { + if (self.cap_height) |value| if (value > 0) return value; + return 0.75 * self.ascent; + } + + /// Convenience function for getting the ex height. If this is not + /// defined in the font, we estimate it as 75% of the cap height. + pub inline fn exHeight(self: FaceMetrics) f64 { + if (self.ex_height) |value| if (value > 0) return value; + return 0.75 * self.capHeight(); + } + + /// Convenience function for getting the ideograph width. If this is + /// not defined in the font, we estimate it as two cell widths. + pub inline fn icWidth(self: FaceMetrics) f64 { + if (self.ic_width) |value| if (value > 0) return value; + return 2 * self.cell_width; + } + + /// Convenience function for getting the underline thickness. If + /// this is not defined in the font, we estimate it as 15% of the ex + /// height. + pub inline fn underlineThickness(self: FaceMetrics) f64 { + if (self.underline_thickness) |value| if (value > 0) return value; + return 0.15 * self.exHeight(); + } + + /// Convenience function for getting the strikethrough thickness. If + /// this is not defined in the font, we set it equal to the + /// underline thickness. + pub inline fn strikethroughThickness(self: FaceMetrics) f64 { + if (self.strikethrough_thickness) |value| if (value > 0) return value; + return self.underlineThickness(); + } + + // NOTE: The getters below return positions, not sizes, so both + // positive and negative values are valid, hence no sign validation. + + /// Convenience function for getting the underline position. If + /// this is not defined in the font, we place it one underline + /// thickness below the baseline. + pub inline fn underlinePosition(self: FaceMetrics) f64 { + return self.underline_position orelse -self.underlineThickness(); + } + + /// Convenience function for getting the strikethrough position. If + /// this is not defined in the font, we center it at half the ex + /// height, so that it's perfectly centered on lower case text. + pub inline fn strikethroughPosition(self: FaceMetrics) f64 { + return self.strikethrough_position orelse (self.exHeight() + self.strikethroughThickness()) * 0.5; + } }; /// Calculate our metrics based on values extracted from a font. @@ -147,35 +201,13 @@ pub fn calc(face: FaceMetrics) Metrics { // We calculate a top_to_baseline to make following calculations simpler. const top_to_baseline = cell_height - cell_baseline; - // If we don't have a provided cap height, - // we estimate it as 75% of the ascent. - const cap_height = face.cap_height orelse face.ascent * 0.75; - - // If we don't have a provided ex height, - // we estimate it as 75% of the cap height. - const ex_height = face.ex_height orelse cap_height * 0.75; - - // If we don't have a provided underline thickness, - // we estimate it as 15% of the ex height. - const underline_thickness = @max(1, @ceil(face.underline_thickness orelse 0.15 * ex_height)); - - // If we don't have a provided strikethrough thickness - // then we just use the underline thickness for it. - const strikethrough_thickness = @max(1, @ceil(face.strikethrough_thickness orelse underline_thickness)); - - // If we don't have a provided underline position then - // we place it 1 underline-thickness below the baseline. - const underline_position = @round(top_to_baseline - - (face.underline_position orelse - -underline_thickness)); - - // If we don't have a provided strikethrough position - // then we center the strikethrough stroke at half the - // ex height, so that it's perfectly centered on lower - // case text. - const strikethrough_position = @round(top_to_baseline - - (face.strikethrough_position orelse - ex_height * 0.5 + strikethrough_thickness * 0.5)); + // Get the other font metrics or their estimates. See doc comments + // in FaceMetrics for explanations of the estimation heuristics. + const cap_height = face.capHeight(); + const underline_thickness = @max(1, @ceil(face.underlineThickness())); + const strikethrough_thickness = @max(1, @ceil(face.strikethroughThickness())); + const underline_position = @round(top_to_baseline - face.underlinePosition()); + const strikethrough_position = @round(top_to_baseline - face.strikethroughPosition()); // The calculation for icon height in the nerd fonts patcher // is two thirds cap height to one third line height, but we From e7d28a85c8f1e06b2e00c2f17fe2f57e33e117c1 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Thu, 17 Jul 2025 10:21:06 -0700 Subject: [PATCH 02/11] Make size normalization reference customizable per face --- src/font/CodepointResolver.zig | 28 +- src/font/Collection.zig | 566 ++++++++++++++++++++------------- src/font/SharedGrid.zig | 4 +- src/font/SharedGridSet.zig | 34 +- src/font/shaper/coretext.zig | 14 +- src/font/shaper/harfbuzz.zig | 14 +- 6 files changed, 385 insertions(+), 275 deletions(-) diff --git a/src/font/CodepointResolver.zig b/src/font/CodepointResolver.zig index 16536300c..9cfcae12d 100644 --- a/src/font/CodepointResolver.zig +++ b/src/font/CodepointResolver.zig @@ -190,7 +190,7 @@ pub fn getIndex( // Discovery is supposed to only return faces that have our // codepoint but we can't search presentation in discovery so // we have to check it here. - const face: Collection.Entry = .{ .fallback_deferred = deferred_face }; + const face: Collection.Entry = .init(.{ .fallback_deferred = deferred_face }); if (!face.hasCodepoint(cp, p_mode)) { deferred_face.deinit(); continue; @@ -266,7 +266,7 @@ fn getIndexCodepointOverride( const idx = try self.collection.add( alloc, .regular, - .{ .deferred = face }, + .init(.{ .deferred = face }), ); try self.descriptor_cache.put(alloc, desc, idx); @@ -388,31 +388,31 @@ test getIndex { { errdefer c.deinit(alloc); - _ = try c.add(alloc, .regular, .{ .loaded = try .init( + _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) }); + ) })); if (comptime !font.options.backend.hasCoretext()) { // Coretext doesn't support Noto's format _ = try c.add( alloc, .regular, - .{ .loaded = try .init( + .init(.{ .loaded = try .init( lib, testEmoji, .{ .size = .{ .points = 12 } }, - ) }, + ) }), ); } _ = try c.add( alloc, .regular, - .{ .loaded = try .init( + .init(.{ .loaded = try .init( lib, testEmojiText, .{ .size = .{ .points = 12 } }, - ) }, + ) }), ); } @@ -467,21 +467,21 @@ test "getIndex disabled font style" { var c = Collection.init(); c.load_options = .{ .library = lib }; - _ = try c.add(alloc, .regular, .{ .loaded = try .init( + _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) }); - _ = try c.add(alloc, .bold, .{ .loaded = try .init( + ) })); + _ = try c.add(alloc, .bold, .init(.{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) }); - _ = try c.add(alloc, .italic, .{ .loaded = try .init( + ) })); + _ = try c.add(alloc, .italic, .init(.{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) }); + ) })); var r: CodepointResolver = .{ .collection = c }; defer r.deinit(alloc); diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 702a3fd7c..33541a825 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -62,7 +62,9 @@ pub fn deinit(self: *Collection, alloc: Allocator) void { var it = self.faces.iterator(); while (it.next()) |array| { var entry_it = array.value.iterator(0); - while (entry_it.next()) |entry| entry.deinit(); + while (entry_it.next()) |entry_or_alias| { + if (entry_or_alias.unwrapNoAlias()) |entry| entry.deinit(); + } array.value.deinit(alloc); } @@ -105,123 +107,35 @@ pub fn add( if (face.isDeferred() and self.load_options == null) return error.DeferredLoadingUnavailable; - try list.append(alloc, face); + try list.append(alloc, .{ .entry = face }); - var owned: *Entry = list.at(idx); + const owned: *Entry = list.at(idx).unwrapNoAlias().?; - // If the face is already loaded, apply font size adjustment - // now, otherwise we'll apply it whenever we do load it. - if (owned.getLoaded()) |loaded| { - if (try self.adjustedSize(loaded)) |opts| { - loaded.setSize(opts.faceOptions()) catch return error.SetSizeFailed; - } + // If we have load options, we update the size such that it's scaled + // to the primary if possible. If the face is not loaded, this is a + // no-op and scaling will be done when loading happens. + if (self.load_options) |opts| { + const primary_entry = self.getEntry(.{ .idx = 0 }) catch null; + try owned.setSize(opts.faceOptions(), primary_entry); } return .{ .style = style, .idx = @intCast(idx) }; } -pub const AdjustSizeError = font.Face.GetMetricsError; - -// Calculate a size for the provided face that will match it with the primary -// font, metrically, to improve consistency with fallback fonts. Right now we -// match the font based on the ex height, or the ideograph width if the font -// has ideographs in it. -// -// This returns null if load options is null or if self.load_options is null. -// -// This is very much like the `font-size-adjust` CSS property in how it works. -// ref: https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust -// -// TODO: In the future, provide config options that allow the user to select -// which metric should be matched for fallback fonts, instead of hard -// coding it as ex height. -pub fn adjustedSize( - self: *Collection, - face: *Face, -) AdjustSizeError!?LoadOptions { - const load_options = self.load_options orelse return null; - - // We silently do nothing if we can't get the primary - // face, because this might be the primary face itself. - const primary_face = self.getFace(.{ .idx = 0 }) catch return null; - - // We do nothing if the primary face and this face are the same. - if (@intFromPtr(primary_face) == @intFromPtr(face)) return null; - - const primary_metrics = try primary_face.getMetrics(); - const face_metrics = try face.getMetrics(); - - // We use the ex height to match our font sizes, so that the height of - // lower-case letters matches between all fonts in the fallback chain. - // - // If the fallback font has an ic_width we prefer that, for normalization - // of CJK font sizes when mixed with latin fonts. - const primary_ex = primary_metrics.exHeight(); - const primary_ic = primary_metrics.icWidth(); - - const face_ex = face_metrics.exHeight(); - const face_ic = face_metrics.icWidth(); - - // If the line height of the scaled font would be larger than - // the line height of the primary font, we don't want that, so - // we take the minimum between matching the ic/ex and the line - // height. - // - // NOTE: We actually allow the line height to be up to 1.2 - // times the primary line height because empirically - // this is usually fine and is better for CJK. - // - // TODO: We should probably provide a config option that lets - // the user pick what metric to use for size adjustment. - const scale = @min( - 1.2 * primary_metrics.lineHeight() / face_metrics.lineHeight(), - if ((face_metrics.ic_width != null) and (face_metrics.ic_width == face_ic)) - // It's possible for .ic_width to be non-null and still invalid, e.g., - // zero, so we only take this branch if it's also equal to .icWidth(). - primary_ic / face_ic - else - primary_ex / face_ex, - ); - - // Make a copy of our load options, set the size to the size of - // the provided face, and then multiply that by our scaling factor. - var opts = load_options; - opts.size = face.size; - opts.size.points *= @as(f32, @floatCast(scale)); - - return opts; -} - /// Return the Face represented by a given Index. The returned pointer /// is only valid as long as this collection is not modified. /// /// This will initialize the face if it is deferred and not yet loaded, /// which can fail. pub fn getFace(self: *Collection, index: Index) !*Face { + return try self.getFaceFromEntry(try self.getEntry(index)); +} + +/// Get the unaliased entry from an index +pub fn getEntry(self: *Collection, index: Index) !*Entry { if (index.special() != null) return error.SpecialHasNoFace; const list = self.faces.getPtr(index.style); - const item: *Entry = item: { - var item = list.at(index.idx); - switch (item.*) { - .alias => |ptr| item = ptr, - - .deferred, - .fallback_deferred, - .loaded, - .fallback_loaded, - => {}, - } - assert(item.* != .alias); - break :item item; - }; - - const face = try self.getFaceFromEntry( - item, - // We only want to adjust the size if this isn't the primary face. - index.style != .regular or index.idx > 0, - ); - - return face; + return list.at(index.idx).unwrap(); } /// Get the face from an entry. @@ -230,41 +144,33 @@ pub fn getFace(self: *Collection, index: Index) !*Face { fn getFaceFromEntry( self: *Collection, entry: *Entry, - /// Whether to adjust the font size to match the primary face after loading. - adjust: bool, ) !*Face { - assert(entry.* != .alias); - - return switch (entry.*) { + return switch (entry.face) { inline .deferred, .fallback_deferred => |*d, tag| deferred: { const opts = self.load_options orelse return error.DeferredLoadingUnavailable; - var face = try d.load(opts.library, opts.faceOptions()); + const face_opts = opts.faceOptions(); + const face = try d.load(opts.library, face_opts); d.deinit(); - // If we need to adjust the size, do so. - if (adjust) if (try self.adjustedSize(&face)) |new_opts| { - try face.setSize(new_opts.faceOptions()); - }; - - entry.* = switch (tag) { + entry.face = switch (tag) { .deferred => .{ .loaded = face }, .fallback_deferred => .{ .fallback_loaded = face }, else => unreachable, }; + // Adjust the size, passing the primary font for scaling + const primary_entry = self.getEntry(.{ .idx = 0 }) catch null; + try entry.setSize(face_opts, primary_entry); + break :deferred switch (tag) { - .deferred => &entry.loaded, - .fallback_deferred => &entry.fallback_loaded, + .deferred => &entry.face.loaded, + .fallback_deferred => &entry.face.fallback_loaded, else => unreachable, }; }, .loaded, .fallback_loaded => |*f| f, - - // When setting `entry` above, we ensure we don't end up with - // an alias. - .alias => unreachable, }; } @@ -282,8 +188,8 @@ pub fn getIndex( ) ?Index { var i: usize = 0; var it = self.faces.get(style).constIterator(0); - while (it.next()) |entry| { - if (entry.hasCodepoint(cp, p_mode)) { + while (it.next()) |entry_or_alias| { + if (@constCast(entry_or_alias).unwrap().hasCodepoint(cp, p_mode)) { return .{ .style = style, .idx = @intCast(i), @@ -309,7 +215,7 @@ pub fn hasCodepoint( ) bool { const list = self.faces.get(index.style); if (index.idx >= list.count()) return false; - return list.at(index.idx).hasCodepoint(cp, p_mode); + return @constCast(list.at(index.idx)).unwrap().hasCodepoint(cp, p_mode); } pub const CompleteError = Allocator.Error || error{ @@ -347,10 +253,11 @@ pub fn completeStyles( // Find our first regular face that has text glyphs. var it = list.iterator(0); - while (it.next()) |entry| { + while (it.next()) |entry_or_alias| { // Load our face. If we fail to load it, we just skip it and // continue on to try the next one. - const face = self.getFaceFromEntry(entry, false) catch |err| { + const entry = entry_or_alias.unwrap(); + const face = self.getFaceFromEntry(entry) catch |err| { log.warn("error loading regular entry={d} err={}", .{ it.index - 1, err, @@ -393,8 +300,9 @@ pub fn completeStyles( break :italic; }; + const synthetic_entry = regular_entry.initCopy(.{ .loaded = synthetic }); log.info("synthetic italic face created", .{}); - try italic_list.append(alloc, .{ .loaded = synthetic }); + try italic_list.append(alloc, .{ .entry = synthetic_entry }); } // If we don't have bold, use the regular font. @@ -413,8 +321,9 @@ pub fn completeStyles( break :bold; }; + const synthetic_entry = regular_entry.initCopy(.{ .loaded = synthetic }); log.info("synthetic bold face created", .{}); - try bold_list.append(alloc, .{ .loaded = synthetic }); + try bold_list.append(alloc, .{ .entry = synthetic_entry }); } // If we don't have bold italic, we attempt to synthesize a bold variant @@ -430,9 +339,11 @@ pub fn completeStyles( // Prefer to synthesize on top of the face we already had. If we // have bold then we try to synthesize italic on top of bold. if (have_bold) { - if (self.syntheticItalic(bold_list.at(0))) |synthetic| { + const base_entry: *Entry = bold_list.at(0).unwrap(); + if (self.syntheticItalic(base_entry)) |synthetic| { log.info("synthetic bold italic face created from bold", .{}); - try bold_italic_list.append(alloc, .{ .loaded = synthetic }); + const synthetic_entry = base_entry.initCopy(.{ .loaded = synthetic }); + try bold_italic_list.append(alloc, .{ .entry = synthetic_entry }); break :bold_italic; } else |_| {} @@ -440,23 +351,11 @@ pub fn completeStyles( // bold on whatever italic font we have. } - // Nested alias isn't allowed so we need to unwrap the italic entry. - const base_entry = base: { - const italic_entry = italic_list.at(0); - break :base switch (italic_entry.*) { - .alias => |v| v, - - .loaded, - .fallback_loaded, - .deferred, - .fallback_deferred, - => italic_entry, - }; - }; - + const base_entry: *Entry = italic_list.at(0).unwrap(); if (self.syntheticBold(base_entry)) |synthetic| { log.info("synthetic bold italic face created from italic", .{}); - try bold_italic_list.append(alloc, .{ .loaded = synthetic }); + const synthetic_entry = base_entry.initCopy(.{ .loaded = synthetic }); + try bold_italic_list.append(alloc, .{ .entry = synthetic_entry }); break :bold_italic; } else |_| {} @@ -474,7 +373,12 @@ fn syntheticBold(self: *Collection, entry: *Entry) !Face { const opts = self.load_options orelse return error.DeferredLoadingUnavailable; // Try to bold it. - const regular = try self.getFaceFromEntry(entry, false); + const regular = try self.getFaceFromEntry(entry); + + // Inherit size from regular; it may be different than opts.size + // due to scaling adjustments + var face_opts = opts.faceOptions(); + face_opts.size = regular.size; const face = try regular.syntheticBold(opts.faceOptions()); var buf: [256]u8 = undefined; @@ -494,7 +398,12 @@ fn syntheticItalic(self: *Collection, entry: *Entry) !Face { const opts = self.load_options orelse return error.DeferredLoadingUnavailable; // Try to italicize it. - const regular = try self.getFaceFromEntry(entry, false); + const regular = try self.getFaceFromEntry(entry); + + // Inherit size from regular; it may be different than opts.size + // due to scaling adjustments + var face_opts = opts.faceOptions(); + face_opts.size = regular.size; const face = try regular.syntheticItalic(opts.faceOptions()); var buf: [256]u8 = undefined; @@ -517,31 +426,42 @@ pub fn setSize(self: *Collection, size: DesiredSize) !void { else return error.DeferredLoadingUnavailable; opts.size = size; + const face_opts = opts.faceOptions(); + + // Get the primary face if we can, and update that first, + // such that the relative scaling of the remaining faces + // is correct + const primary_entry = primary_entry: { + if (self.getEntry(.{ .idx = 0 })) |pe| { + try pe.setSize(face_opts, null); + break :primary_entry pe; + } else |_| break :primary_entry null; + }; // Resize all our faces that are loaded var it = self.faces.iterator(); while (it.next()) |array| { var entry_it = array.value.iterator(0); - while (entry_it.next()) |entry| switch (entry.*) { - .loaded, - .fallback_loaded, - => |*f| { - const new_opts = try self.adjustedSize(f) orelse opts.*; - try f.setSize(new_opts.faceOptions()); - }, - - // Deferred aren't loaded so we don't need to set their size. - // The size for when they're loaded is set since `opts` changed. - .deferred, .fallback_deferred => continue, - - // Alias faces don't own their size. - .alias => continue, - }; + while (entry_it.next()) |entry_or_alias| { + if (entry_or_alias.unwrapNoAlias()) |entry| { + if (entry == primary_entry) continue; + try entry.setSize(face_opts, primary_entry); + } + } } try self.updateMetrics(); } +pub fn setReferenceMetric(self: *Collection, index: Index, scale_reference: ReferenceMetric) !void { + var entry = try self.getEntry(index); + entry.scale_reference = scale_reference; + if (self.load_options) |opts| { + const primary_entry = self.getEntry(.{ .idx = 0 }) catch null; + try entry.setSize(opts.faceOptions(), primary_entry); + } +} + const UpdateMetricsError = font.Face.GetMetricsError || error{ CannotLoadPrimaryFont, }; @@ -574,7 +494,7 @@ pub fn updateMetrics(self: *Collection) UpdateMetricsError!void { /// /// WARNING: We cannot use any prealloc yet for the segmented list because /// the collection is copied around by value and pointers aren't stable. -const StyleArray = std.EnumArray(Style, std.SegmentedList(Entry, 0)); +const StyleArray = std.EnumArray(Style, std.SegmentedList(EntryOrAlias, 0)); /// Load options are used to configure all the details a Collection /// needs to load deferred faces. @@ -628,49 +548,70 @@ pub const LoadOptions = struct { /// not "fallback"), they want to use any glyphs possible within that /// font face. Fallback fonts on the other hand are picked as a /// last resort, so we should prefer exactness if possible. -pub const Entry = union(enum) { - deferred: DeferredFace, // Not loaded - loaded: Face, // Loaded, explicit use +pub const Entry = struct { + const AnyFace = union(enum) { + deferred: DeferredFace, // Not loaded + loaded: Face, // Loaded, explicit use - // The same as deferred/loaded but fallback font semantics (see large - // comment above Entry). - fallback_deferred: DeferredFace, - fallback_loaded: Face, + // The same as deferred/loaded but fallback font semantics (see large + // comment above Entry). + fallback_deferred: DeferredFace, + fallback_loaded: Face, + }; - // An alias to another entry. This is used to share the same face, - // avoid memory duplication. An alias must point to a non-alias entry. - alias: *Entry, + face: AnyFace, + + // Metric by which to normalize the font's size to the primary font. + // Default to ic_width to ensure appropriate normalization of CJK + // font sizes when mixed with latin fonts. See scaleFactor() + // implementation for fallback rules when the font does not define + // the specified metric. + // + // NOTE: In the future, if additional modifiers are needed for the + // translation of global collection options to individual font + // options, this should be promoted to a new FontModifiers type. + scale_reference: ReferenceMetric = .ic_width, + + /// Convenience initializer so that users won't have to write nested + /// expressions depending on internals, like .{ .face = .{ .loaded = face } } + pub fn init(face: AnyFace) Entry { + return .{ .face = face }; + } + + /// Convenience initializer that also takes a scale reference + pub fn initWithScaleReference(face: AnyFace, scale_reference: ReferenceMetric) Entry { + return .{ .face = face, .scale_reference = scale_reference }; + } + + /// Initialize a new entry with the same scale reference as an existing entry + pub fn initCopy(self: Entry, face: AnyFace) Entry { + return .{ .face = face, .scale_reference = self.scale_reference }; + } pub fn deinit(self: *Entry) void { - switch (self.*) { + switch (self.face) { inline .deferred, .loaded, .fallback_deferred, .fallback_loaded, => |*v| v.deinit(), - - // Aliased fonts are not owned by this entry so we let them - // be deallocated by the owner. - .alias => {}, } } - /// If this face is loaded, or is an alias to a loaded face, - /// then this returns the `Face`, otherwise returns null. + /// If this face is loaded, then this returns the `Face`, + /// otherwise returns null. pub fn getLoaded(self: *Entry) ?*Face { - return switch (self.*) { + return switch (self.face) { .deferred, .fallback_deferred => null, .loaded, .fallback_loaded => |*face| face, - .alias => |v| v.getLoaded(), }; } /// True if the entry is deferred. fn isDeferred(self: Entry) bool { - return switch (self) { + return switch (self.face) { .deferred, .fallback_deferred => true, .loaded, .fallback_loaded => false, - .alias => |v| v.isDeferred(), }; } @@ -680,9 +621,7 @@ pub const Entry = union(enum) { cp: u32, p_mode: PresentationMode, ) bool { - return switch (self) { - .alias => |v| v.hasCodepoint(cp, p_mode), - + return switch (self.face) { // Non-fallback fonts require explicit presentation matching but // otherwise don't care about presentation .deferred => |v| switch (p_mode) { @@ -721,6 +660,142 @@ pub const Entry = union(enum) { }, }; } + + // Set the size of the face, rescaling to match the primary if given + pub fn setSize(self: *Entry, opts: font.face.Options, primary_entry: ?*Entry) !void { + // If not loaded, nothing to do + var face = self.getLoaded() orelse return; + + // First set to the raw size from opts, even if we're scaling, + // otherwise scaling calculations will be incorrect. + face.setSize(opts) catch return error.SetSizeFailed; + + // If we have a primary we rescale + if (primary_entry) |pe| if (try self.scaleFactor(pe)) |scale| { + var opts_scaled = opts; + opts_scaled.size.points *= scale; + face.setSize(opts_scaled) catch return error.SetSizeFailed; + }; + } + + // Calculate a size for the face that will match it with the primary font, + // metrically, to improve consistency with fallback fonts. + // + // This returns null if this or the primary face aren't loaded or, if + // scaling doesn't apply to this face. + // + // This is very much like the `font-size-adjust` CSS property in how it works. + // ref: https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust + // + // TODO: In the future, provide config options that allow the user to select + // which metric should be matched for fallback fonts, instead of hard + // coding at the point where a face is added to the collection. + pub fn scaleFactor(self: *Entry, primary_entry: *Entry) !?f32 { + // If the reference metric is the em size, no scaling + if (self.scale_reference == .em_size) return null; + + // If the primary is us, no scaling + if (@intFromPtr(self) == @intFromPtr(primary_entry)) return null; + + // If we or the primary face aren't loaded, we don't know our + // metrics, so we can't do anything + const primary_face = primary_entry.getLoaded() orelse return null; + const face = self.getLoaded() orelse return null; + + // Verify that the two faces are currently loaded at the same + // size, otherwise what follows is nonsense + if (!std.meta.eql(face.size, primary_face.size)) { + return error.InconsistentSizesForScaling; + } + + const primary_metrics = try primary_face.getMetrics(); + const face_metrics = try face.getMetrics(); + + // The preferred metric to normalize by is self.scale_reference, + // however we don't want to use a metric not explicitly defined + // in `self`, so if needed we fall back through other metrics in + // the order shown in the switch statement below. If the metric + // is not defined in `primary`, that's OK, we'll use the estimate. + const line_height_ratio = primary_metrics.lineHeight() / face_metrics.lineHeight(); + const scale: f64 = normalize_by: switch (self.scale_reference) { + // Even if a metric is non-null, it may be invalid (e.g., negative), + // so we check for equality with the estimator before using it + .ic_width => { + if (face_metrics.ic_width) |value| if (value == face_metrics.icWidth()) { + break :normalize_by primary_metrics.icWidth() / value; + }; + continue :normalize_by .ex_height; + }, + .ex_height => { + if (face_metrics.ex_height) |value| if (value == face_metrics.exHeight()) { + break :normalize_by primary_metrics.exHeight() / value; + }; + continue :normalize_by .cap_height; + }, + .cap_height => { + if (face_metrics.cap_height) |value| if (value == face_metrics.capHeight()) { + break :normalize_by primary_metrics.capHeight() / value; + }; + continue :normalize_by .line_height; + }, + .line_height => line_height_ratio, + .em_size => unreachable, + }; + + // If the line height of the scaled font would be larger than + // the line height of the primary font, we don't want that, so + // we take the minimum between matching the reference metric + // and keeping the line heights within some margin. + // + // NOTE: We actually allow the line height to be up to 1.2 + // times the primary line height because empirically + // this is usually fine and is better for CJK. + const capped_scale = @min(scale, 1.2 * line_height_ratio); + + return @floatCast(capped_scale); + } +}; + +pub const EntryOrAlias = union(enum) { + entry: Entry, + + // An alias to another entry. This is used to share the same face, + // avoid memory duplication. An alias must point to a non-alias entry. + alias: *Entry, + + pub fn unwrap(self: *EntryOrAlias) *Entry { + return switch (self.*) { + .entry => |*v| v, + .alias => |v| v, + }; + } + + pub fn unwrapNoAlias(self: *EntryOrAlias) ?*Entry { + return switch (self.*) { + .entry => |*v| v, + .alias => null, + }; + } +}; + +pub const AdjustSizeError = font.Face.GetMetricsError || error{InconsistentSizesForScaling}; + +pub const ReferenceMetric = enum { + // The font's ideograph width + ic_width, + // The font's ex height + ex_height, + // The font's cap height + cap_height, + // The font's line height + line_height, + // The font's em size + // Conventionally equivalent to line height, but the semantics + // differ: using em_size directly sets the point sizes to the same + // value, while using line_height calculates the scaling ratio for + // matching line heights even if it differs from 1 em for one or + // both fonts + em_size, }; /// The requested presentation for a codepoint. @@ -823,11 +898,11 @@ test "add full" { defer c.deinit(alloc); for (0..Index.Special.start - 1) |_| { - _ = try c.add(alloc, .regular, .{ .loaded = try .init( + _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12 } }, - ) }); + ) })); } var face = try Face.init( @@ -840,7 +915,7 @@ test "add full" { defer face.deinit(); try testing.expectError( error.CollectionFull, - c.add(alloc, .regular, .{ .loaded = face }), + c.add(alloc, .regular, .init(.{ .loaded = face })), ); } @@ -856,7 +931,7 @@ test "add deferred without loading options" { .regular, // This can be undefined because it should never be accessed. - .{ .deferred = undefined }, + .init(.{ .deferred = undefined }), )); } @@ -871,11 +946,11 @@ test getFace { var c = init(); defer c.deinit(alloc); - const idx = try c.add(alloc, .regular, .{ .loaded = try .init( + const idx = try c.add(alloc, .regular, .init(.{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) }); + ) })); { const face1 = try c.getFace(idx); @@ -895,11 +970,11 @@ test getIndex { var c = init(); defer c.deinit(alloc); - _ = try c.add(alloc, .regular, .{ .loaded = try .init( + _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) }); + ) })); // Should find all visible ASCII var i: u32 = 32; @@ -927,11 +1002,11 @@ test completeStyles { defer c.deinit(alloc); c.load_options = .{ .library = lib }; - _ = try c.add(alloc, .regular, .{ .loaded = try .init( + _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) }); + ) })); try testing.expect(c.getIndex('A', .bold, .{ .any = {} }) == null); try testing.expect(c.getIndex('A', .italic, .{ .any = {} }) == null); @@ -954,11 +1029,11 @@ test setSize { defer c.deinit(alloc); c.load_options = .{ .library = lib }; - _ = try c.add(alloc, .regular, .{ .loaded = try .init( + _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) }); + ) })); try testing.expectEqual(@as(u32, 12), c.load_options.?.size.points); try c.setSize(.{ .points = 24 }); @@ -977,11 +1052,11 @@ test hasCodepoint { defer c.deinit(alloc); c.load_options = .{ .library = lib }; - const idx = try c.add(alloc, .regular, .{ .loaded = try .init( + const idx = try c.add(alloc, .regular, .init(.{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) }); + ) })); try testing.expect(c.hasCodepoint(idx, 'A', .{ .any = {} })); try testing.expect(!c.hasCodepoint(idx, '🥸', .{ .any = {} })); @@ -1001,11 +1076,11 @@ test "hasCodepoint emoji default graphical" { defer c.deinit(alloc); c.load_options = .{ .library = lib }; - const idx = try c.add(alloc, .regular, .{ .loaded = try .init( + const idx = try c.add(alloc, .regular, .init(.{ .loaded = try .init( lib, testEmoji, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) }); + ) })); try testing.expect(!c.hasCodepoint(idx, 'A', .{ .any = {} })); try testing.expect(c.hasCodepoint(idx, '🥸', .{ .any = {} })); @@ -1025,11 +1100,11 @@ test "metrics" { const size: DesiredSize = .{ .points = 12, .xdpi = 96, .ydpi = 96 }; c.load_options = .{ .library = lib, .size = size }; - _ = try c.add(alloc, .regular, .{ .loaded = try .init( + _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( lib, testFont, .{ .size = size }, - ) }); + ) })); try c.updateMetrics(); @@ -1084,6 +1159,7 @@ test "adjusted sizes" { const alloc = testing.allocator; const testFont = font.embedded.inconsolata; const fallback = font.embedded.monaspace_neon; + const symbol = font.embedded.symbols_nerd_font; var lib = try Library.init(alloc); defer lib.deinit(); @@ -1094,45 +1170,79 @@ test "adjusted sizes" { c.load_options = .{ .library = lib, .size = size }; // Add our primary face. - _ = try c.add(alloc, .regular, .{ .loaded = try .init( + _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( lib, testFont, .{ .size = size }, - ) }); + ) })); try c.updateMetrics(); // Add the fallback face. - const fallback_idx = try c.add(alloc, .regular, .{ .loaded = try .init( + const fallback_idx = try c.add(alloc, .regular, .init(.{ .loaded = try .init( lib, fallback, .{ .size = size }, - ) }); + ) })); - // The ex heights should match. + inline for ([_][]const u8{ "ex_height", "cap_height" }) |metric| { + try c.setReferenceMetric(fallback_idx, @field(ReferenceMetric, metric)); + + // The chosen metric should match. + { + const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics(); + const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics(); + + try std.testing.expectApproxEqRel( + @field(primary_metrics, metric).?, + @field(fallback_metrics, metric).?, + // We accept anything within 5 %. + 0.05, + ); + } + + // Resize should keep that relationship. + try c.setSize(.{ .points = 37, .xdpi = 96, .ydpi = 96 }); + { + const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics(); + const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics(); + + try std.testing.expectApproxEqRel( + @field(primary_metrics, metric).?, + @field(fallback_metrics, metric).?, + // We accept anything within 5 %. + 0.05, + ); + } + // Reset size for the next iteration + try c.setSize(size); + } + + // Add the symbol face. + const symbol_idx = try c.add(alloc, .regular, .initWithScaleReference(.{ .loaded = try .init( + lib, + symbol, + .{ .size = size }, + ) }, .ex_height)); + + // Test fallback to lineHeight() (ex_height and cap_height not defined in symbols font). { const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics(); - const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics(); + const symbol_metrics = try (try c.getFace(symbol_idx)).getMetrics(); - try std.testing.expectApproxEqAbs( - primary_metrics.ex_height.?, - fallback_metrics.ex_height.?, - // We accept anything within half a pixel. - 0.5, + try std.testing.expectApproxEqRel( + primary_metrics.lineHeight(), + symbol_metrics.lineHeight(), + // We accept anything within 5 %. + 0.05, ); } - // Resize should keep that relationship. - try c.setSize(.{ .points = 37, .xdpi = 96, .ydpi = 96 }); - { - const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics(); - const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics(); + // Test em_size giving exact font size equality + try c.setReferenceMetric(symbol_idx, .em_size); - try std.testing.expectApproxEqAbs( - primary_metrics.ex_height.?, - fallback_metrics.ex_height.?, - // We accept anything within half a pixel. - 0.5, - ); - } + try std.testing.expectEqual( + (try c.getFace(.{ .idx = 0 })).size.points, + (try c.getFace(symbol_idx)).size.points, + ); } diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig index 3ccac7fa1..ea0bc4458 100644 --- a/src/font/SharedGrid.zig +++ b/src/font/SharedGrid.zig @@ -376,11 +376,11 @@ fn testGrid(mode: TestMode, alloc: Allocator, lib: Library) !SharedGrid { switch (mode) { .normal => { - _ = try c.add(alloc, .regular, .{ .loaded = try .init( + _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) }); + ) })); }, } diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index 14a8babad..14b0c6f68 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -203,7 +203,7 @@ fn collection( _ = try c.add( self.alloc, style, - .{ .deferred = face }, + .init(.{ .deferred = face }), ); continue; @@ -233,7 +233,7 @@ fn collection( _ = try c.add( self.alloc, style, - .{ .deferred = face }, + .init(.{ .deferred = face }), ); continue; @@ -258,20 +258,20 @@ fn collection( _ = try c.add( self.alloc, .regular, - .{ .fallback_loaded = try .init( + .init(.{ .fallback_loaded = try .init( self.font_lib, font.embedded.variable, load_options.faceOptions(), - ) }, + ) }), ); try (try c.getFace(try c.add( self.alloc, .bold, - .{ .fallback_loaded = try .init( + .init(.{ .fallback_loaded = try .init( self.font_lib, font.embedded.variable, load_options.faceOptions(), - ) }, + ) }), ))).setVariations( &.{.{ .id = .init("wght"), .value = 700 }}, load_options.faceOptions(), @@ -279,20 +279,20 @@ fn collection( _ = try c.add( self.alloc, .italic, - .{ .fallback_loaded = try .init( + .init(.{ .fallback_loaded = try .init( self.font_lib, font.embedded.variable_italic, load_options.faceOptions(), - ) }, + ) }), ); try (try c.getFace(try c.add( self.alloc, .bold_italic, - .{ .fallback_loaded = try .init( + .init(.{ .fallback_loaded = try .init( self.font_lib, font.embedded.variable_italic, load_options.faceOptions(), - ) }, + ) }), ))).setVariations( &.{.{ .id = .init("wght"), .value = 700 }}, load_options.faceOptions(), @@ -302,11 +302,11 @@ fn collection( _ = try c.add( self.alloc, .regular, - .{ .fallback_loaded = try Face.init( + .init(.{ .fallback_loaded = try Face.init( self.font_lib, font.embedded.symbols_nerd_font, load_options.faceOptions(), - ) }, + ) }), ); // On macOS, always search for and add the Apple Emoji font @@ -324,7 +324,7 @@ fn collection( _ = try c.add( self.alloc, .regular, - .{ .fallback_deferred = face }, + .init(.{ .fallback_deferred = face }), ); } } @@ -335,20 +335,20 @@ fn collection( _ = try c.add( self.alloc, .regular, - .{ .fallback_loaded = try .init( + .init(.{ .fallback_loaded = try .init( self.font_lib, font.embedded.emoji, load_options.faceOptions(), - ) }, + ) }), ); _ = try c.add( self.alloc, .regular, - .{ .fallback_loaded = try .init( + .init(.{ .fallback_loaded = try .init( self.font_lib, font.embedded.emoji_text, load_options.faceOptions(), - ) }, + ) }), ); } diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index f4f01d105..a5f63d466 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -1779,19 +1779,19 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { c.load_options = .{ .library = lib }; // Setup group - _ = try c.add(alloc, .regular, .{ .loaded = try .init( + _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12 } }, - ) }); + ) })); if (font.options.backend != .coretext) { // Coretext doesn't support Noto's format - _ = try c.add(alloc, .regular, .{ .loaded = try .init( + _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( lib, testEmoji, .{ .size = .{ .points = 12 } }, - ) }); + ) })); } else { // On CoreText we want to load Apple Emoji, we should have it. var disco = font.Discover.init(); @@ -1804,13 +1804,13 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { defer disco_it.deinit(); var face = (try disco_it.next()).?; errdefer face.deinit(); - _ = try c.add(alloc, .regular, .{ .deferred = face }); + _ = try c.add(alloc, .regular, .init(.{ .deferred = face })); } - _ = try c.add(alloc, .regular, .{ .loaded = try .init( + _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( lib, testEmojiText, .{ .size = .{ .points = 12 } }, - ) }); + ) })); const grid_ptr = try alloc.create(SharedGrid); errdefer alloc.destroy(grid_ptr); diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index 4209f795c..04a48b2ec 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -1247,19 +1247,19 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { c.load_options = .{ .library = lib }; // Setup group - _ = try c.add(alloc, .regular, .{ .loaded = try .init( + _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( lib, testFont, .{ .size = .{ .points = 12 } }, - ) }); + ) })); if (comptime !font.options.backend.hasCoretext()) { // Coretext doesn't support Noto's format - _ = try c.add(alloc, .regular, .{ .loaded = try .init( + _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( lib, testEmoji, .{ .size = .{ .points = 12 } }, - ) }); + ) })); } else { // On CoreText we want to load Apple Emoji, we should have it. var disco = font.Discover.init(); @@ -1272,13 +1272,13 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { defer disco_it.deinit(); var face = (try disco_it.next()).?; errdefer face.deinit(); - _ = try c.add(alloc, .regular, .{ .deferred = face }); + _ = try c.add(alloc, .regular, .init(.{ .deferred = face })); } - _ = try c.add(alloc, .regular, .{ .loaded = try .init( + _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( lib, testEmojiText, .{ .size = .{ .points = 12 } }, - ) }); + ) })); const grid_ptr = try alloc.create(SharedGrid); errdefer alloc.destroy(grid_ptr); From ce507f35df48a2540b37ffce8dc615b4c520d640 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Tue, 15 Jul 2025 13:04:51 -0700 Subject: [PATCH 03/11] Use em size as nerd font reference metric --- src/build/SharedDeps.zig | 2 +- src/font/SharedGridSet.zig | 7 +++++-- src/font/face.zig | 23 +---------------------- 3 files changed, 7 insertions(+), 25 deletions(-) diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index f1a6f80c8..c03746a48 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -531,7 +531,7 @@ pub fn add( const nf_symbols = b.dependency("nerd_fonts_symbols_only", .{}); step.root_module.addAnonymousImport( "nerd_fonts_symbols_only", - .{ .root_source_file = nf_symbols.path("SymbolsNerdFontMono-Regular.ttf") }, + .{ .root_source_file = nf_symbols.path("SymbolsNerdFont-Regular.ttf") }, ); } diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index 14b0c6f68..5f388e09d 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -299,14 +299,17 @@ fn collection( ); // Nerd-font symbols fallback. + // For proper icon scaling, this should be loaded at the same point + // size as the primary font and not undergo size normalization, + // hence we use the em size as scale reference. _ = try c.add( self.alloc, .regular, - .init(.{ .fallback_loaded = try Face.init( + .initWithScaleReference(.{ .fallback_loaded = try .init( self.font_lib, font.embedded.symbols_nerd_font, load_options.faceOptions(), - ) }), + ) }, .em_size), ); // On macOS, always search for and add the Apple Emoji font diff --git a/src/font/face.zig b/src/font/face.zig index fc5118c3d..cf2ed7218 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -222,7 +222,7 @@ pub const RenderOptions = struct { ) GlyphSize { var g = glyph; - var available_width: f64 = @floatFromInt( + const available_width: f64 = @floatFromInt( metrics.cell_width * @min( self.max_constraint_width, constraint_width, @@ -233,22 +233,6 @@ pub const RenderOptions = struct { .icon => metrics.icon_height, }); - // We make the opinionated choice here to reduce the width - // of icon-height symbols by the same amount horizontally, - // since otherwise wide aspect ratio icons like folders end - // up far too wide. - // - // But we *only* do this if the constraint width is 2, since - // otherwise it would make them way too small when sized for - // a single cell. - const is_icon_width = self.height == .icon and @min(self.max_constraint_width, constraint_width) > 1; - const orig_avail_width = available_width; - if (is_icon_width) { - const cell_height: f64 = @floatFromInt(metrics.cell_height); - const ratio = available_height / cell_height; - available_width *= ratio; - } - const w = available_width - self.pad_left * available_width - self.pad_right * available_width; @@ -372,11 +356,6 @@ pub const RenderOptions = struct { .center => g.y = (h - g.height) / 2, } - // Add offset for icon width restriction, to keep it centered. - if (is_icon_width) { - g.x += (orig_avail_width - available_width) / 2; - } - // Re-add our padding before returning. g.x += self.pad_left * available_width; g.y += self.pad_bottom * available_height; From 054b7325dcb43322988fbd704053d72b5d19a32f Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Thu, 17 Jul 2025 17:26:37 -0700 Subject: [PATCH 04/11] Add unwrapConst, avoid constCast --- src/font/Collection.zig | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 33541a825..e2dad6372 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -189,7 +189,7 @@ pub fn getIndex( var i: usize = 0; var it = self.faces.get(style).constIterator(0); while (it.next()) |entry_or_alias| { - if (@constCast(entry_or_alias).unwrap().hasCodepoint(cp, p_mode)) { + if (entry_or_alias.unwrapConst().hasCodepoint(cp, p_mode)) { return .{ .style = style, .idx = @intCast(i), @@ -215,7 +215,7 @@ pub fn hasCodepoint( ) bool { const list = self.faces.get(index.style); if (index.idx >= list.count()) return false; - return @constCast(list.at(index.idx)).unwrap().hasCodepoint(cp, p_mode); + return list.at(index.idx).unwrapConst().hasCodepoint(cp, p_mode); } pub const CompleteError = Allocator.Error || error{ @@ -770,6 +770,13 @@ pub const EntryOrAlias = union(enum) { }; } + pub fn unwrapConst(self: *const EntryOrAlias) *const Entry { + return switch (self.*) { + .entry => |*v| v, + .alias => |v| v, + }; + } + pub fn unwrapNoAlias(self: *EntryOrAlias) ?*Entry { return switch (self.*) { .entry => |*v| v, From a7c560c159921b744d45192a9d1af3df23f6b458 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Fri, 18 Jul 2025 00:54:02 -0700 Subject: [PATCH 05/11] Calculate scaled size directly, eliminate redundant resizes --- src/font/Collection.zig | 107 ++++++++++++++++++++++------------------ 1 file changed, 60 insertions(+), 47 deletions(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index e2dad6372..14279f787 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -111,9 +111,9 @@ pub fn add( const owned: *Entry = list.at(idx).unwrapNoAlias().?; - // If we have load options, we update the size such that it's scaled - // to the primary if possible. If the face is not loaded, this is a - // no-op and scaling will be done when loading happens. + // If we have load options, we update the size to ensure it's matches and is + // normalized to the primary if possible. If the face is not loaded, this is + // a no-op and sizing/scaling will happen whenever we do load it. if (self.load_options) |opts| { const primary_entry = self.getEntry(.{ .idx = 0 }) catch null; try owned.setSize(opts.faceOptions(), primary_entry); @@ -159,9 +159,12 @@ fn getFaceFromEntry( else => unreachable, }; - // Adjust the size, passing the primary font for scaling - const primary_entry = self.getEntry(.{ .idx = 0 }) catch null; - try entry.setSize(face_opts, primary_entry); + // Adjust the size if we have access to the primary font for + // scaling. Otherwise, nothing to do, calling setSize would + // be redundant as the same face_opts were used when loading. + if (self.getEntry(.{ .idx = 0 })) |primary_entry| { + try entry.setSize(face_opts, primary_entry); + } else |_| {} break :deferred switch (tag) { .deferred => &entry.face.loaded, @@ -428,15 +431,11 @@ pub fn setSize(self: *Collection, size: DesiredSize) !void { opts.size = size; const face_opts = opts.faceOptions(); - // Get the primary face if we can, and update that first, - // such that the relative scaling of the remaining faces - // is correct - const primary_entry = primary_entry: { - if (self.getEntry(.{ .idx = 0 })) |pe| { - try pe.setSize(face_opts, null); - break :primary_entry pe; - } else |_| break :primary_entry null; - }; + // Get the primary face if we can, for size normalization. No need + // to jump through hoops to make sure this is resized first, as + // Entry.setSize will get it right regardless. (That said, it's + // likely the first iterate and hence resized first anyway.) + const primary_entry = self.getEntry(.{ .idx = 0 }) catch null; // Resize all our faces that are loaded var it = self.faces.iterator(); @@ -444,7 +443,6 @@ pub fn setSize(self: *Collection, size: DesiredSize) !void { var entry_it = array.value.iterator(0); while (entry_it.next()) |entry_or_alias| { if (entry_or_alias.unwrapNoAlias()) |entry| { - if (entry == primary_entry) continue; try entry.setSize(face_opts, primary_entry); } } @@ -666,23 +664,27 @@ pub const Entry = struct { // If not loaded, nothing to do var face = self.getLoaded() orelse return; - // First set to the raw size from opts, even if we're scaling, - // otherwise scaling calculations will be incorrect. - face.setSize(opts) catch return error.SetSizeFailed; + var modified_opts = opts; // If we have a primary we rescale - if (primary_entry) |pe| if (try self.scaleFactor(pe)) |scale| { - var opts_scaled = opts; - opts_scaled.size.points *= scale; - face.setSize(opts_scaled) catch return error.SetSizeFailed; - }; + if (primary_entry) |p| { + modified_opts.size = try self.scaledSize(modified_opts.size, p); + } + + // Before going through with the resize, we check whether the requested + // size after scaling is actually different from the existing size. + if (!std.meta.eql(modified_opts.size, face.size)) { + face.setSize(modified_opts) catch return error.SetSizeFailed; + } } // Calculate a size for the face that will match it with the primary font, // metrically, to improve consistency with fallback fonts. // - // This returns null if this or the primary face aren't loaded or, if - // scaling doesn't apply to this face. + // This returns a scaled copy of the nominal_size, where the points size has + // been scaled by the font metric ratio specified by self.scale_reference. + // If either this or the primary face are not yet loaded, or the primary + // face is the same as this, nominal_size is returned unchanged. // // This is very much like the `font-size-adjust` CSS property in how it works. // ref: https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust @@ -690,51 +692,58 @@ pub const Entry = struct { // TODO: In the future, provide config options that allow the user to select // which metric should be matched for fallback fonts, instead of hard // coding at the point where a face is added to the collection. - pub fn scaleFactor(self: *Entry, primary_entry: *Entry) !?f32 { - // If the reference metric is the em size, no scaling - if (self.scale_reference == .em_size) return null; + pub fn scaledSize(self: *Entry, nominal_size: DesiredSize, primary_entry: *Entry) !DesiredSize { + // If the scale reference is the em size, no scaling + if (self.scale_reference == .em_size) return nominal_size; // If the primary is us, no scaling - if (@intFromPtr(self) == @intFromPtr(primary_entry)) return null; + if (@intFromPtr(self) == @intFromPtr(primary_entry)) return nominal_size; - // If we or the primary face aren't loaded, we don't know our - // metrics, so we can't do anything - const primary_face = primary_entry.getLoaded() orelse return null; - const face = self.getLoaded() orelse return null; - - // Verify that the two faces are currently loaded at the same - // size, otherwise what follows is nonsense - if (!std.meta.eql(face.size, primary_face.size)) { - return error.InconsistentSizesForScaling; - } + // If we or the primary face aren't loaded, we don't know our metrics, + // so unable to scale + const primary_face = primary_entry.getLoaded() orelse return nominal_size; + const face = self.getLoaded() orelse return nominal_size; const primary_metrics = try primary_face.getMetrics(); const face_metrics = try face.getMetrics(); + // The face metrics are in pixel units, and both point sizes and dpis + // may differ. The following factors are used to convert ratios of face + // metrics to scaling factors that are size- and dpi-independent and can + // be used to scale point sizes directly. + const primary_y_px_per_72em = primary_face.size.points * @as(f32, @floatFromInt(primary_face.size.ydpi)); + const primary_x_px_per_72em = primary_face.size.points * @as(f32, @floatFromInt(primary_face.size.xdpi)); + + const face_y_px_per_72em = face.size.points * @as(f32, @floatFromInt(face.size.ydpi)); + const face_x_px_per_72em = face.size.points * @as(f32, @floatFromInt(face.size.xdpi)); + + const y_ratio: f64 = face_y_px_per_72em / primary_y_px_per_72em; + const x_ratio: f64 = face_x_px_per_72em / primary_x_px_per_72em; + // The preferred metric to normalize by is self.scale_reference, // however we don't want to use a metric not explicitly defined // in `self`, so if needed we fall back through other metrics in // the order shown in the switch statement below. If the metric // is not defined in `primary`, that's OK, we'll use the estimate. - const line_height_ratio = primary_metrics.lineHeight() / face_metrics.lineHeight(); - const scale: f64 = normalize_by: switch (self.scale_reference) { + const line_height_ratio = y_ratio * primary_metrics.lineHeight() / face_metrics.lineHeight(); + const scale = normalize_by: switch (self.scale_reference) { // Even if a metric is non-null, it may be invalid (e.g., negative), // so we check for equality with the estimator before using it .ic_width => { if (face_metrics.ic_width) |value| if (value == face_metrics.icWidth()) { - break :normalize_by primary_metrics.icWidth() / value; + break :normalize_by x_ratio * (primary_metrics.icWidth() / value); }; continue :normalize_by .ex_height; }, .ex_height => { if (face_metrics.ex_height) |value| if (value == face_metrics.exHeight()) { - break :normalize_by primary_metrics.exHeight() / value; + break :normalize_by y_ratio * primary_metrics.exHeight() / value; }; continue :normalize_by .cap_height; }, .cap_height => { if (face_metrics.cap_height) |value| if (value == face_metrics.capHeight()) { - break :normalize_by primary_metrics.capHeight() / value; + break :normalize_by y_ratio * primary_metrics.capHeight() / value; }; continue :normalize_by .line_height; }, @@ -752,7 +761,11 @@ pub const Entry = struct { // this is usually fine and is better for CJK. const capped_scale = @min(scale, 1.2 * line_height_ratio); - return @floatCast(capped_scale); + // Scale the target size by the final scaling factor and return. + var scaled_size = nominal_size; + scaled_size.points *= @floatCast(capped_scale); + + return scaled_size; } }; @@ -785,7 +798,7 @@ pub const EntryOrAlias = union(enum) { } }; -pub const AdjustSizeError = font.Face.GetMetricsError || error{InconsistentSizesForScaling}; +pub const AdjustSizeError = font.Face.GetMetricsError; pub const ReferenceMetric = enum { // The font's ideograph width From 652bae73790fe8e708177dcdf1b7041a39d2bd8e Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Fri, 18 Jul 2025 00:56:14 -0700 Subject: [PATCH 06/11] Update a straggling name and signature --- src/font/Collection.zig | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 14279f787..2bc6159b7 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -451,8 +451,9 @@ pub fn setSize(self: *Collection, size: DesiredSize) !void { try self.updateMetrics(); } -pub fn setReferenceMetric(self: *Collection, index: Index, scale_reference: ReferenceMetric) !void { - var entry = try self.getEntry(index); +/// Update the scale reference metric associated with a face. This will +/// also rescale the face's size accordingly. +pub fn setScaleReference(self: *Collection, entry: *Entry, scale_reference: ReferenceMetric) !void { entry.scale_reference = scale_reference; if (self.load_options) |opts| { const primary_entry = self.getEntry(.{ .idx = 0 }) catch null; @@ -1206,7 +1207,7 @@ test "adjusted sizes" { ) })); inline for ([_][]const u8{ "ex_height", "cap_height" }) |metric| { - try c.setReferenceMetric(fallback_idx, @field(ReferenceMetric, metric)); + try c.setScaleReference(try c.getEntry(fallback_idx), @field(ReferenceMetric, metric)); // The chosen metric should match. { @@ -1259,7 +1260,7 @@ test "adjusted sizes" { } // Test em_size giving exact font size equality - try c.setReferenceMetric(symbol_idx, .em_size); + try c.setScaleReference(try c.getEntry(symbol_idx), .em_size); try std.testing.expectEqual( (try c.getFace(.{ .idx = 0 })).size.points, From 2054a065331194077f35a3bbdda43bed2c1ab00a Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Thu, 24 Jul 2025 17:48:57 -0600 Subject: [PATCH 07/11] cleanup A variety of naming, commenting, and formatting improvements + a few explicit error sets. This commit has no functional changes, though it does remove a couple functions that didn't really need to exist. --- src/font/Collection.zig | 233 ++++++++++++++++++++----------------- src/font/SharedGridSet.zig | 2 +- 2 files changed, 129 insertions(+), 106 deletions(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 2bc6159b7..ec04c5b3f 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -62,9 +62,12 @@ pub fn deinit(self: *Collection, alloc: Allocator) void { var it = self.faces.iterator(); while (it.next()) |array| { var entry_it = array.value.iterator(0); - while (entry_it.next()) |entry_or_alias| { - if (entry_or_alias.unwrapNoAlias()) |entry| entry.deinit(); - } + // Deinit all entries, aliases can be ignored. + while (entry_it.next()) |entry_or_alias| + switch (entry_or_alias.*) { + .entry => |*entry| entry.deinit(), + .alias => {}, + }; array.value.deinit(alloc); } @@ -73,11 +76,12 @@ pub fn deinit(self: *Collection, alloc: Allocator) void { pub const AddError = Allocator.Error || - AdjustSizeError || + SetSizeError || error{ + /// There's no more room in the collection. CollectionFull, + /// Trying to add a deferred face and `self.load_options` is `null`. DeferredLoadingUnavailable, - SetSizeFailed, }; /// Add a face to the collection for the given style. This face will be added @@ -109,7 +113,7 @@ pub fn add( try list.append(alloc, .{ .entry = face }); - const owned: *Entry = list.at(idx).unwrapNoAlias().?; + const owned: *Entry = list.at(idx).getEntry(); // If we have load options, we update the size to ensure it's matches and is // normalized to the primary if possible. If the face is not loaded, this is @@ -135,7 +139,7 @@ pub fn getFace(self: *Collection, index: Index) !*Face { pub fn getEntry(self: *Collection, index: Index) !*Entry { if (index.special() != null) return error.SpecialHasNoFace; const list = self.faces.getPtr(index.style); - return list.at(index.idx).unwrap(); + return list.at(index.idx).getEntry(); } /// Get the face from an entry. @@ -192,7 +196,7 @@ pub fn getIndex( var i: usize = 0; var it = self.faces.get(style).constIterator(0); while (it.next()) |entry_or_alias| { - if (entry_or_alias.unwrapConst().hasCodepoint(cp, p_mode)) { + if (entry_or_alias.getConstEntry().hasCodepoint(cp, p_mode)) { return .{ .style = style, .idx = @intCast(i), @@ -218,7 +222,7 @@ pub fn hasCodepoint( ) bool { const list = self.faces.get(index.style); if (index.idx >= list.count()) return false; - return list.at(index.idx).unwrapConst().hasCodepoint(cp, p_mode); + return list.at(index.idx).getConstEntry().hasCodepoint(cp, p_mode); } pub const CompleteError = Allocator.Error || error{ @@ -259,7 +263,7 @@ pub fn completeStyles( while (it.next()) |entry_or_alias| { // Load our face. If we fail to load it, we just skip it and // continue on to try the next one. - const entry = entry_or_alias.unwrap(); + const entry = entry_or_alias.getEntry(); const face = self.getFaceFromEntry(entry) catch |err| { log.warn("error loading regular entry={d} err={}", .{ it.index - 1, @@ -342,7 +346,7 @@ pub fn completeStyles( // Prefer to synthesize on top of the face we already had. If we // have bold then we try to synthesize italic on top of bold. if (have_bold) { - const base_entry: *Entry = bold_list.at(0).unwrap(); + const base_entry: *Entry = bold_list.at(0).getEntry(); if (self.syntheticItalic(base_entry)) |synthetic| { log.info("synthetic bold italic face created from bold", .{}); const synthetic_entry = base_entry.initCopy(.{ .loaded = synthetic }); @@ -354,7 +358,7 @@ pub fn completeStyles( // bold on whatever italic font we have. } - const base_entry: *Entry = italic_list.at(0).unwrap(); + const base_entry: *Entry = italic_list.at(0).getEntry(); if (self.syntheticBold(base_entry)) |synthetic| { log.info("synthetic bold italic face created from italic", .{}); const synthetic_entry = base_entry.initCopy(.{ .loaded = synthetic }); @@ -367,7 +371,7 @@ pub fn completeStyles( } } -// Create a synthetic bold font face from the given entry and return it. +/// Create a synthetic bold font face from the given entry and return it. fn syntheticBold(self: *Collection, entry: *Entry) !Face { // Not all font backends support synthetic bold. if (comptime !@hasDecl(Face, "syntheticBold")) return error.SyntheticBoldUnavailable; @@ -392,7 +396,7 @@ fn syntheticBold(self: *Collection, entry: *Entry) !Face { return face; } -// Create a synthetic italic font face from the given entry and return it. +/// Create a synthetic italic font face from the given entry and return it. fn syntheticItalic(self: *Collection, entry: *Entry) !Face { // Not all font backends support synthetic italicization. if (comptime !@hasDecl(Face, "syntheticItalic")) return error.SyntheticItalicUnavailable; @@ -417,12 +421,23 @@ fn syntheticItalic(self: *Collection, entry: *Entry) !Face { return face; } +pub const SetSizeError = + Entry.SetSizeError || + UpdateMetricsError || + error{ + /// `self.load_options` is `null`. + DeferredLoadingUnavailable, + }; + /// Update the size of all faces in the collection. This will /// also update the size in the load options for future deferred /// face loading. /// /// This requires load options to be set. -pub fn setSize(self: *Collection, size: DesiredSize) !void { +pub fn setSize( + self: *Collection, + size: DesiredSize, +) SetSizeError!void { // Get a pointer to our options so we can modify the size. const opts = if (self.load_options) |*v| v @@ -441,26 +456,17 @@ pub fn setSize(self: *Collection, size: DesiredSize) !void { var it = self.faces.iterator(); while (it.next()) |array| { var entry_it = array.value.iterator(0); - while (entry_it.next()) |entry_or_alias| { - if (entry_or_alias.unwrapNoAlias()) |entry| { - try entry.setSize(face_opts, primary_entry); - } - } + // Resize all entries, aliases can be ignored. + while (entry_it.next()) |entry_or_alias| + switch (entry_or_alias.*) { + .entry => |*entry| try entry.setSize(face_opts, primary_entry), + .alias => {}, + }; } try self.updateMetrics(); } -/// Update the scale reference metric associated with a face. This will -/// also rescale the face's size accordingly. -pub fn setScaleReference(self: *Collection, entry: *Entry, scale_reference: ReferenceMetric) !void { - entry.scale_reference = scale_reference; - if (self.load_options) |opts| { - const primary_entry = self.getEntry(.{ .idx = 0 }) catch null; - try entry.setSize(opts.faceOptions(), primary_entry); - } -} - const UpdateMetricsError = font.Face.GetMetricsError || error{ CannotLoadPrimaryFont, }; @@ -489,7 +495,7 @@ pub fn updateMetrics(self: *Collection) UpdateMetricsError!void { /// small style count. /// /// We use a segmented list because the entry values must be pointer-stable -/// to support the "alias" field in Entry. +/// to support aliases. /// /// WARNING: We cannot use any prealloc yet for the segmented list because /// the collection is copied around by value and pointers aren't stable. @@ -562,29 +568,41 @@ pub const Entry = struct { // Metric by which to normalize the font's size to the primary font. // Default to ic_width to ensure appropriate normalization of CJK - // font sizes when mixed with latin fonts. See scaleFactor() + // font sizes when mixed with latin fonts. See the `scaleSize(...)` // implementation for fallback rules when the font does not define // the specified metric. - // - // NOTE: In the future, if additional modifiers are needed for the - // translation of global collection options to individual font - // options, this should be promoted to a new FontModifiers type. - scale_reference: ReferenceMetric = .ic_width, + size_adjust_metric: SizeAdjustmentMetric = .ic_width, - /// Convenience initializer so that users won't have to write nested - /// expressions depending on internals, like .{ .face = .{ .loaded = face } } + /// Font metrics that can be specified for font size adjustment. + pub const SizeAdjustmentMetric = enum { + /// Don't adjust the size for this font, use the original point size. + none, + /// Match ideograph character width with the primary font. + ic_width, + /// Match ex height with the primary font. + ex_height, + /// Match cap height with the primary font. + cap_height, + /// Match line height with the primary font. + line_height, + }; + + /// Create an entry for the provided face. pub fn init(face: AnyFace) Entry { return .{ .face = face }; } /// Convenience initializer that also takes a scale reference - pub fn initWithScaleReference(face: AnyFace, scale_reference: ReferenceMetric) Entry { - return .{ .face = face, .scale_reference = scale_reference }; + pub fn initWithScaleReference( + face: AnyFace, + scale_reference: SizeAdjustmentMetric, + ) Entry { + return .{ .face = face, .size_adjust_metric = scale_reference }; } /// Initialize a new entry with the same scale reference as an existing entry pub fn initCopy(self: Entry, face: AnyFace) Entry { - return .{ .face = face, .scale_reference = self.scale_reference }; + return .{ .face = face, .size_adjust_metric = self.size_adjust_metric }; } pub fn deinit(self: *Entry) void { @@ -660,42 +678,60 @@ pub const Entry = struct { }; } - // Set the size of the face, rescaling to match the primary if given - pub fn setSize(self: *Entry, opts: font.face.Options, primary_entry: ?*Entry) !void { + pub const SetSizeError = + font.Face.GetMetricsError || + error{ + /// The call to `face.setSize` failed. + SetSizeFailed, + }; + + /// Set the size of the face for this entry if it's loaded. + /// + /// This takes in to account the `size_adjust_metric` of this entry, + /// adjusting the size in the provided options if a primary entry is + /// provided to scale against. + fn setSize( + self: *Entry, + opts: font.face.Options, + primary_entry: ?*Entry, + ) Entry.SetSizeError!void { // If not loaded, nothing to do var face = self.getLoaded() orelse return; - var modified_opts = opts; + var new_opts = opts; // If we have a primary we rescale if (primary_entry) |p| { - modified_opts.size = try self.scaledSize(modified_opts.size, p); + new_opts.size = try self.scaledSize(new_opts.size, p); } // Before going through with the resize, we check whether the requested // size after scaling is actually different from the existing size. - if (!std.meta.eql(modified_opts.size, face.size)) { - face.setSize(modified_opts) catch return error.SetSizeFailed; + if (!std.meta.eql(new_opts.size, face.size)) { + face.setSize(new_opts) catch return error.SetSizeFailed; } } - // Calculate a size for the face that will match it with the primary font, - // metrically, to improve consistency with fallback fonts. - // - // This returns a scaled copy of the nominal_size, where the points size has - // been scaled by the font metric ratio specified by self.scale_reference. - // If either this or the primary face are not yet loaded, or the primary - // face is the same as this, nominal_size is returned unchanged. - // - // This is very much like the `font-size-adjust` CSS property in how it works. - // ref: https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust - // - // TODO: In the future, provide config options that allow the user to select - // which metric should be matched for fallback fonts, instead of hard - // coding at the point where a face is added to the collection. - pub fn scaledSize(self: *Entry, nominal_size: DesiredSize, primary_entry: *Entry) !DesiredSize { - // If the scale reference is the em size, no scaling - if (self.scale_reference == .em_size) return nominal_size; + /// Calculate a size for the face that will match it with the primary font, + /// metrically, to improve consistency with fallback fonts. + /// + /// This returns a scaled copy of the nominal_size, where the points size has + /// been scaled by the font metric ratio specified by self.scale_reference. + /// If either this or the primary face are not yet loaded, or the primary + /// face is the same as this, nominal_size is returned unchanged. + /// + /// This is very much like the `font-size-adjust` CSS property in how it works. + /// ref: https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust + /// + /// TODO: In the future, provide config options that allow the user to select + /// which metric should be matched for fallback fonts, instead of hard + /// coding at the point where a face is added to the collection. + fn scaledSize( + self: *Entry, + nominal_size: DesiredSize, + primary_entry: *Entry, + ) font.Face.GetMetricsError!DesiredSize { + if (self.size_adjust_metric == .none) return nominal_size; // If the primary is us, no scaling if (@intFromPtr(self) == @intFromPtr(primary_entry)) return nominal_size; @@ -727,29 +763,34 @@ pub const Entry = struct { // the order shown in the switch statement below. If the metric // is not defined in `primary`, that's OK, we'll use the estimate. const line_height_ratio = y_ratio * primary_metrics.lineHeight() / face_metrics.lineHeight(); - const scale = normalize_by: switch (self.scale_reference) { + const scale = normalize_by: switch (self.size_adjust_metric) { // Even if a metric is non-null, it may be invalid (e.g., negative), // so we check for equality with the estimator before using it + .ic_width => { if (face_metrics.ic_width) |value| if (value == face_metrics.icWidth()) { break :normalize_by x_ratio * (primary_metrics.icWidth() / value); }; continue :normalize_by .ex_height; }, + .ex_height => { if (face_metrics.ex_height) |value| if (value == face_metrics.exHeight()) { break :normalize_by y_ratio * primary_metrics.exHeight() / value; }; continue :normalize_by .cap_height; }, + .cap_height => { if (face_metrics.cap_height) |value| if (value == face_metrics.capHeight()) { break :normalize_by y_ratio * primary_metrics.capHeight() / value; }; continue :normalize_by .line_height; }, + .line_height => line_height_ratio, - .em_size => unreachable, + + .none => unreachable, }; // If the line height of the scaled font would be larger than @@ -773,50 +814,25 @@ pub const Entry = struct { pub const EntryOrAlias = union(enum) { entry: Entry, - // An alias to another entry. This is used to share the same face, - // avoid memory duplication. An alias must point to a non-alias entry. + /// An alias to another entry. This is used to share the same face, + /// avoid memory duplication. An alias must point to a non-alias entry. alias: *Entry, - pub fn unwrap(self: *EntryOrAlias) *Entry { + /// Get a pointer to the underlying entry. + pub fn getEntry(self: *EntryOrAlias) *Entry { return switch (self.*) { .entry => |*v| v, .alias => |v| v, }; } - pub fn unwrapConst(self: *const EntryOrAlias) *const Entry { + /// Get a const pointer to the underlying entry. + pub fn getConstEntry(self: *const EntryOrAlias) *const Entry { return switch (self.*) { .entry => |*v| v, .alias => |v| v, }; } - - pub fn unwrapNoAlias(self: *EntryOrAlias) ?*Entry { - return switch (self.*) { - .entry => |*v| v, - .alias => null, - }; - } -}; - -pub const AdjustSizeError = font.Face.GetMetricsError; - -pub const ReferenceMetric = enum { - // The font's ideograph width - ic_width, - // The font's ex height - ex_height, - // The font's cap height - cap_height, - // The font's line height - line_height, - // The font's em size - // Conventionally equivalent to line height, but the semantics - // differ: using em_size directly sets the point sizes to the same - // value, while using line_height calculates the scaling ratio for - // matching line heights even if it differs from 1 em for one or - // both fonts - em_size, }; /// The requested presentation for a codepoint. @@ -1206,8 +1222,11 @@ test "adjusted sizes" { .{ .size = size }, ) })); + const primary_entry = try c.getEntry(.{ .idx = 0 }); inline for ([_][]const u8{ "ex_height", "cap_height" }) |metric| { - try c.setScaleReference(try c.getEntry(fallback_idx), @field(ReferenceMetric, metric)); + const entry = try c.getEntry(fallback_idx); + entry.size_adjust_metric = @field(Entry.SizeAdjustmentMetric, metric); + try entry.setSize(c.load_options.?.faceOptions(), primary_entry); // The chosen metric should match. { @@ -1259,11 +1278,15 @@ test "adjusted sizes" { ); } - // Test em_size giving exact font size equality - try c.setScaleReference(try c.getEntry(symbol_idx), .em_size); + // A reference metric of "none" should leave the size unchanged. + { + const entry = try c.getEntry(symbol_idx); + entry.size_adjust_metric = .none; + try entry.setSize(c.load_options.?.faceOptions(), primary_entry); - try std.testing.expectEqual( - (try c.getFace(.{ .idx = 0 })).size.points, - (try c.getFace(symbol_idx)).size.points, - ); + try std.testing.expectEqual( + (try c.getFace(.{ .idx = 0 })).size.points, + (try c.getFace(symbol_idx)).size.points, + ); + } } diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index 5f388e09d..74dd7db08 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -309,7 +309,7 @@ fn collection( self.font_lib, font.embedded.symbols_nerd_font, load_options.faceOptions(), - ) }, .em_size), + ) }, .none), ); // On macOS, always search for and add the Apple Emoji font From c0ee4a252abaee577932ed03e3c157a4fcc8ca7d Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Thu, 24 Jul 2025 17:52:41 -0600 Subject: [PATCH 08/11] font: revert switch to non-mono symbols nerd font This change might be good, but it is incomplete and not relevant to the PR that it's a part of. I'll explore making this change separately after this, since it might be a good idea. (It's incomplete since the attribute data was not re-generated based on the non-mono file.) --- src/build/SharedDeps.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index c03746a48..f1a6f80c8 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -531,7 +531,7 @@ pub fn add( const nf_symbols = b.dependency("nerd_fonts_symbols_only", .{}); step.root_module.addAnonymousImport( "nerd_fonts_symbols_only", - .{ .root_source_file = nf_symbols.path("SymbolsNerdFont-Regular.ttf") }, + .{ .root_source_file = nf_symbols.path("SymbolsNerdFontMono-Regular.ttf") }, ); } From 9405522dd5d4471f318d35bde5c465e6613f0035 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 25 Jul 2025 11:22:25 -0600 Subject: [PATCH 09/11] font: allow fractional pixel sizes --- src/font/face.zig | 4 ++-- src/font/face/coretext.zig | 5 +++-- src/font/face/freetype.zig | 2 +- src/font/shaper/harfbuzz.zig | 15 +++++---------- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/font/face.zig b/src/font/face.zig index cf2ed7218..d685d4107 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -51,9 +51,9 @@ pub const DesiredSize = struct { ydpi: u16 = default_dpi, // Converts points to pixels - pub fn pixels(self: DesiredSize) u16 { + pub fn pixels(self: DesiredSize) f32 { // 1 point = 1/72 inch - return @intFromFloat(@round((self.points * @as(f32, @floatFromInt(self.ydpi))) / 72)); + return (self.points * @as(f32, @floatFromInt(self.ydpi))) / 72; } }; diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index bb9a472d2..a92407ef1 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -78,7 +78,7 @@ pub const Face = struct { // 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(opts.size.pixels()), + opts.size.pixels(), null, null, ); @@ -94,7 +94,8 @@ pub const Face = struct { var hb_font = if (comptime harfbuzz_shaper) font: { var hb_font = try harfbuzz.coretext.createFont(ct_font); - hb_font.setScale(opts.size.pixels(), opts.size.pixels()); + const pixels: opentype.sfnt.F26Dot6 = .from(opts.size.pixels()); + hb_font.setScale(@bitCast(pixels), @bitCast(pixels)); break :font hb_font; } else {}; errdefer if (comptime harfbuzz_shaper) hb_font.destroy(); diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index f42868e5c..5d5ed28c4 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -217,7 +217,7 @@ pub const Face = struct { if (face.isScalable()) { const size_26dot6: i32 = @intFromFloat(@round(size.points * 64)); try face.setCharSize(0, size_26dot6, size.xdpi, size.ydpi); - } else try selectSizeNearest(face, size.pixels()); + } else try selectSizeNearest(face, @intFromFloat(@round(size.pixels()))); } /// Selects the fixed size in the loaded face that is closest to the diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index 04a48b2ec..a170df382 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -158,16 +158,11 @@ pub const Shaper = struct { .glyph_index = info_v.codepoint, }); - if (font.options.backend.hasFreetype()) { - // Freetype returns 26.6 fixed point values, so we need to - // divide by 64 to get the actual value. I can't find any - // HB API to stop this. - cell_offset.x += pos_v.x_advance >> 6; - cell_offset.y += pos_v.y_advance >> 6; - } else { - cell_offset.x += pos_v.x_advance; - cell_offset.y += pos_v.y_advance; - } + // Under both FreeType and CoreText the harfbuzz scale is + // in 26.6 fixed point units, so we round to the nearest + // whole value here. + cell_offset.x += (pos_v.x_advance + 0b100_000) >> 6; + cell_offset.y += (pos_v.y_advance + 0b100_000) >> 6; // const i = self.cell_buf.items.len - 1; // log.warn("i={} info={} pos={} cell={}", .{ i, info_v, pos_v, self.cell_buf.items[i] }); From 6af6357949ef3a2904facc073b8644e2300d0e1c Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 25 Jul 2025 11:50:05 -0600 Subject: [PATCH 10/11] font: clean up Collection api somewhat Move size adjustment logic out of `Entry`, I understand the impulse to put it there but it results in passing a lot of stuff around which isn't great. Rework `add(...)` in to `add(...)` and `addDeferred(...)`, faces are passed directly now instead of passing an entry, and an options struct is used instead of positional arguments for things like style, fallback, and size adjustment. Change size adjustment test back to a half pixel tolerance instead of 5% because the previous commit (allowing fractional pixel sizes) fixed the root cause of large differences. --- src/font/CodepointResolver.zig | 88 ++-- src/font/Collection.zig | 749 ++++++++++++++++++--------------- src/font/Metrics.zig | 5 + src/font/SharedGrid.zig | 8 +- src/font/SharedGridSet.zig | 109 +++-- src/font/face/coretext.zig | 12 +- src/font/face/freetype.zig | 2 + src/font/main.zig | 5 + src/font/shaper/coretext.zig | 30 +- src/font/shaper/harfbuzz.zig | 30 +- src/font/sprite/Face.zig | 3 + 11 files changed, 617 insertions(+), 424 deletions(-) diff --git a/src/font/CodepointResolver.zig b/src/font/CodepointResolver.zig index 9cfcae12d..ba74065ab 100644 --- a/src/font/CodepointResolver.zig +++ b/src/font/CodepointResolver.zig @@ -190,7 +190,10 @@ pub fn getIndex( // Discovery is supposed to only return faces that have our // codepoint but we can't search presentation in discovery so // we have to check it here. - const face: Collection.Entry = .init(.{ .fallback_deferred = deferred_face }); + const face: Collection.Entry = .{ + .face = .{ .deferred = deferred_face }, + .fallback = true, + }; if (!face.hasCodepoint(cp, p_mode)) { deferred_face.deinit(); continue; @@ -201,7 +204,11 @@ pub fn getIndex( cp, deferred_face.name(&buf) catch "", }); - return self.collection.add(alloc, style, face) catch { + return self.collection.addDeferred(alloc, deferred_face, .{ + .style = style, + .fallback = true, + .size_adjustment = font.default_fallback_adjustment, + }) catch { deferred_face.deinit(); break :discover; }; @@ -263,11 +270,11 @@ fn getIndexCodepointOverride( // Add the font to our list of fonts so we can get an index for it, // and ensure the index is stored in the descriptor cache for next time. - const idx = try self.collection.add( - alloc, - .regular, - .init(.{ .deferred = face }), - ); + const idx = try self.collection.addDeferred(alloc, face, .{ + .style = .regular, + .fallback = false, + .size_adjustment = font.default_fallback_adjustment, + }); try self.descriptor_cache.put(alloc, desc, idx); break :idx idx; @@ -388,32 +395,36 @@ test getIndex { { errdefer c.deinit(alloc); - _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( + _ = try c.add(alloc, try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) })); + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); if (comptime !font.options.backend.hasCoretext()) { // Coretext doesn't support Noto's format - _ = try c.add( - alloc, - .regular, - .init(.{ .loaded = try .init( - lib, - testEmoji, - .{ .size = .{ .points = 12 } }, - ) }), - ); - } - _ = try c.add( - alloc, - .regular, - .init(.{ .loaded = try .init( + _ = try c.add(alloc, try .init( lib, - testEmojiText, + testEmoji, .{ .size = .{ .points = 12 } }, - ) }), - ); + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); + } + _ = try c.add(alloc, try .init( + lib, + testEmojiText, + .{ .size = .{ .points = 12 } }, + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); } var r: CodepointResolver = .{ .collection = c }; @@ -467,21 +478,33 @@ test "getIndex disabled font style" { var c = Collection.init(); c.load_options = .{ .library = lib }; - _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( + _ = try c.add(alloc, try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) })); - _ = try c.add(alloc, .bold, .init(.{ .loaded = try .init( + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); + _ = try c.add(alloc, try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) })); - _ = try c.add(alloc, .italic, .init(.{ .loaded = try .init( + ), .{ + .style = .bold, + .fallback = false, + .size_adjustment = .none, + }); + _ = try c.add(alloc, try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) })); + ), .{ + .style = .italic, + .fallback = false, + .size_adjustment = .none, + }); var r: CodepointResolver = .{ .collection = c }; defer r.deinit(alloc); @@ -522,6 +545,7 @@ test "getIndex box glyph" { .collection = c, .sprite = .{ .metrics = font.Metrics.calc(.{ + .px_per_em = 30.0, .cell_width = 18.0, .ascent = 30.0, .descent = -6.0, diff --git a/src/font/Collection.zig b/src/font/Collection.zig index ec04c5b3f..ef508b346 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -46,6 +46,12 @@ metric_modifiers: Metrics.ModifierSet = .{}, /// these after adding a primary font or making changes to `metric_modifiers`. metrics: ?Metrics = null, +/// The face metrics for the primary face in the collection. +/// +/// We keep this around so we don't need to re-compute it when calculating +/// the scale factor for additional fonts added to the collection. +primary_face_metrics: ?Metrics.FaceMetrics = null, + /// The load options for deferred faces in the face list. If this /// is not set, then deferred faces will not be loaded. Attempting to /// add a deferred face will result in an error. @@ -74,56 +80,112 @@ pub fn deinit(self: *Collection, alloc: Allocator) void { if (self.load_options) |*v| v.deinit(alloc); } +/// Options for adding a face to the collection. +pub const AddOptions = struct { + /// What style this face is. + style: Style, + /// What size adjustment to use. + size_adjustment: SizeAdjustment, + /// Whether this is a fallback face. + fallback: bool, +}; + pub const AddError = Allocator.Error || - SetSizeError || + Face.GetMetricsError || error{ /// There's no more room in the collection. CollectionFull, - /// Trying to add a deferred face and `self.load_options` is `null`. - DeferredLoadingUnavailable, + /// The call to `face.setSize` failed. + SetSizeFailed, }; -/// Add a face to the collection for the given style. This face will be added -/// next in priority if others exist already, i.e. it'll be the _last_ to be -/// searched for a glyph in that list. +/// Add a face to the collection. This face will be added next in priority if +/// others exist already, i.e. it'll be the _last_ to be searched for a glyph +/// in that list. /// /// If no error is encountered then the collection takes ownership of the face, /// in which case face will be deallocated when the collection is deallocated. /// -/// If a loaded face is added to the collection, its size will be changed to -/// match the size specified in load_options, adjusted for harmonization with -/// the primary face. +/// When added, the size of the face will be adjusted to match `load_options`. +/// +/// Returns the index for the added face. pub fn add( self: *Collection, alloc: Allocator, - style: Style, - face: Entry, + face: Face, + opts: AddOptions, ) AddError!Index { - const list = self.faces.getPtr(style); + const list = self.faces.getPtr(opts.style); // We have some special indexes so we must never pass those. const idx = list.count(); if (idx >= Index.Special.start - 1) return error.CollectionFull; - // If this is deferred and we don't have load options, we can't. - if (face.isDeferred() and self.load_options == null) - return error.DeferredLoadingUnavailable; + var owned_face = face; - try list.append(alloc, .{ .entry = face }); + // Scale factor to adjust the size of the added face. + const scale_factor = self.scaleFactor( + try owned_face.getMetrics(), + opts.size_adjustment, + ); - const owned: *Entry = list.at(idx).getEntry(); - - // If we have load options, we update the size to ensure it's matches and is - // normalized to the primary if possible. If the face is not loaded, this is - // a no-op and sizing/scaling will happen whenever we do load it. - if (self.load_options) |opts| { - const primary_entry = self.getEntry(.{ .idx = 0 }) catch null; - try owned.setSize(opts.faceOptions(), primary_entry); + // If we have load options, we update the size to ensure + // it's matches and is normalized to the primary if possible. + if (self.load_options) |load_opts| { + var new_opts = load_opts; + new_opts.size.points *= @floatCast(scale_factor); + owned_face.setSize(new_opts.faceOptions()) catch return error.SetSizeFailed; } - return .{ .style = style, .idx = @intCast(idx) }; + try list.append(alloc, .{ + .entry = .{ + .face = .{ .loaded = owned_face }, + .fallback = opts.fallback, + .scale_factor = .{ .scale = scale_factor }, + }, + }); + + return .{ .style = opts.style, .idx = @intCast(idx) }; +} + +pub const AddDeferredError = + Allocator.Error || + error{ + /// There's no more room in the collection. + CollectionFull, + /// `load_options` is null, can't do deferred loading. + DeferredLoadingUnavailable, + }; + +/// Add a deferred face to the collection. +/// +/// Returns the index for the added face. +pub fn addDeferred( + self: *Collection, + alloc: Allocator, + face: DeferredFace, + opts: AddOptions, +) AddDeferredError!Index { + const list = self.faces.getPtr(opts.style); + + if (self.load_options == null) return error.DeferredLoadingUnavailable; + + // We have some special indexes so we must never pass those. + const idx = list.count(); + if (idx >= Index.Special.start - 1) + return error.CollectionFull; + + try list.append(alloc, .{ + .entry = .{ + .face = .{ .deferred = face }, + .fallback = opts.fallback, + .scale_factor = .{ .adjustment = opts.size_adjustment }, + }, + }); + + return .{ .style = opts.style, .idx = @intCast(idx) }; } /// Return the Face represented by a given Index. The returned pointer @@ -139,6 +201,7 @@ pub fn getFace(self: *Collection, index: Index) !*Face { pub fn getEntry(self: *Collection, index: Index) !*Entry { if (index.special() != null) return error.SpecialHasNoFace; const list = self.faces.getPtr(index.style); + if (index.idx >= list.len) return error.IndexOutOfBounds; return list.at(index.idx).getEntry(); } @@ -150,34 +213,43 @@ fn getFaceFromEntry( entry: *Entry, ) !*Face { return switch (entry.face) { - inline .deferred, .fallback_deferred => |*d, tag| deferred: { - const opts = self.load_options orelse + .deferred => |*d| deferred: { + var opts = self.load_options orelse return error.DeferredLoadingUnavailable; - const face_opts = opts.faceOptions(); - const face = try d.load(opts.library, face_opts); + + // Load the face. + var face = try d.load(opts.library, opts.faceOptions()); + errdefer face.deinit(); + + // Calculate the scale factor for this + // entry now that we have a loaded face. + entry.scale_factor = .{ + .scale = self.scaleFactor( + try face.getMetrics(), + entry.scale_factor.adjustment, + ), + }; + + // If our scale factor is something other + // than 1.0 then we need to resize the face. + if (entry.scale_factor.scale != 1.0) { + opts.size.points *= @floatCast(entry.scale_factor.scale); + try face.setSize(opts.faceOptions()); + } + + // Deinit the deferred face now that we have + // loaded it and are past any possible errors. + errdefer comptime unreachable; d.deinit(); - entry.face = switch (tag) { - .deferred => .{ .loaded = face }, - .fallback_deferred => .{ .fallback_loaded = face }, - else => unreachable, - }; + // Set the loaded face on the entry. + entry.face = .{ .loaded = face }; - // Adjust the size if we have access to the primary font for - // scaling. Otherwise, nothing to do, calling setSize would - // be redundant as the same face_opts were used when loading. - if (self.getEntry(.{ .idx = 0 })) |primary_entry| { - try entry.setSize(face_opts, primary_entry); - } else |_| {} - - break :deferred switch (tag) { - .deferred => &entry.face.loaded, - .fallback_deferred => &entry.face.fallback_loaded, - else => unreachable, - }; + // Return the pointer to it. + break :deferred &entry.face.loaded; }, - .loaded, .fallback_loaded => |*f| f, + .loaded => |*f| f, }; } @@ -307,7 +379,10 @@ pub fn completeStyles( break :italic; }; - const synthetic_entry = regular_entry.initCopy(.{ .loaded = synthetic }); + const synthetic_entry: Entry = .{ + .face = .{ .loaded = synthetic }, + .fallback = false, + }; log.info("synthetic italic face created", .{}); try italic_list.append(alloc, .{ .entry = synthetic_entry }); } @@ -328,7 +403,10 @@ pub fn completeStyles( break :bold; }; - const synthetic_entry = regular_entry.initCopy(.{ .loaded = synthetic }); + const synthetic_entry: Entry = .{ + .face = .{ .loaded = synthetic }, + .fallback = false, + }; log.info("synthetic bold face created", .{}); try bold_list.append(alloc, .{ .entry = synthetic_entry }); } @@ -349,7 +427,10 @@ pub fn completeStyles( const base_entry: *Entry = bold_list.at(0).getEntry(); if (self.syntheticItalic(base_entry)) |synthetic| { log.info("synthetic bold italic face created from bold", .{}); - const synthetic_entry = base_entry.initCopy(.{ .loaded = synthetic }); + const synthetic_entry: Entry = .{ + .face = .{ .loaded = synthetic }, + .fallback = false, + }; try bold_italic_list.append(alloc, .{ .entry = synthetic_entry }); break :bold_italic; } else |_| {} @@ -361,7 +442,10 @@ pub fn completeStyles( const base_entry: *Entry = italic_list.at(0).getEntry(); if (self.syntheticBold(base_entry)) |synthetic| { log.info("synthetic bold italic face created from italic", .{}); - const synthetic_entry = base_entry.initCopy(.{ .loaded = synthetic }); + const synthetic_entry: Entry = .{ + .face = .{ .loaded = synthetic }, + .fallback = false, + }; try bold_italic_list.append(alloc, .{ .entry = synthetic_entry }); break :bold_italic; } else |_| {} @@ -422,11 +506,12 @@ fn syntheticItalic(self: *Collection, entry: *Entry) !Face { } pub const SetSizeError = - Entry.SetSizeError || UpdateMetricsError || error{ /// `self.load_options` is `null`. DeferredLoadingUnavailable, + /// The call to `face.setSize` failed. + SetSizeFailed, }; /// Update the size of all faces in the collection. This will @@ -439,34 +524,134 @@ pub fn setSize( size: DesiredSize, ) SetSizeError!void { // Get a pointer to our options so we can modify the size. - const opts = if (self.load_options) |*v| - v - else - return error.DeferredLoadingUnavailable; + const opts = &(self.load_options orelse return error.DeferredLoadingUnavailable); opts.size = size; - const face_opts = opts.faceOptions(); - // Get the primary face if we can, for size normalization. No need - // to jump through hoops to make sure this is resized first, as - // Entry.setSize will get it right regardless. (That said, it's - // likely the first iterate and hence resized first anyway.) const primary_entry = self.getEntry(.{ .idx = 0 }) catch null; // Resize all our faces that are loaded var it = self.faces.iterator(); while (it.next()) |array| { var entry_it = array.value.iterator(0); - // Resize all entries, aliases can be ignored. - while (entry_it.next()) |entry_or_alias| - switch (entry_or_alias.*) { - .entry => |*entry| try entry.setSize(face_opts, primary_entry), - .alias => {}, - }; + // Resize all faces. We skip entries that are aliases, since + // the underlying face will have a non-alias entry somewhere. + while (entry_it.next()) |entry_or_alias| { + if (entry_or_alias.* == .alias) continue; + + const entry = entry_or_alias.getEntry(); + + if (entry.getLoaded()) |face| { + // If this isn't our primary face, we scale + // the size appropriately before setting it. + // + // If we don't have a primary face we also don't. + var new_opts = opts.*; + if (primary_entry != null and entry != primary_entry) { + new_opts.size.points *= @floatCast(entry.scale_factor.scale); + } + face.setSize(new_opts.faceOptions()) catch return error.SetSizeFailed; + } + } } try self.updateMetrics(); } +/// Options for adjusting the size of a face relative to the primary face. +pub const SizeAdjustment = enum { + /// Don't adjust the size for this face, use the original point size. + none, + /// Match ideograph character width with the primary face. + ic_width, + /// Match ex height with the primary face. + ex_height, + /// Match cap height with the primary face. + cap_height, + /// Match line height with the primary face. + line_height, +}; + +/// Calculate a factor by which to scale the provided face to match +/// it with the primary face, depending on the specified adjustment. +/// +/// If this encounters any problems loading the primary face or its +/// metrics then it just returns `1.0`. +/// +/// This functions very much like the `font-size-adjust` CSS property. +/// ref: https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust +fn scaleFactor( + self: *Collection, + face_metrics: Metrics.FaceMetrics, + adjustment: SizeAdjustment, +) f64 { + // If there's no adjustment, the scale is 1.0 + if (adjustment == .none) return 1.0; + + // If we haven't calculated our primary face metrics yet, do so now. + if (self.primary_face_metrics == null) { + @branchHint(.unlikely); + // If we can't load the primary face, just use 1.0 as the scale factor. + const primary_face = self.getFace(.{ .idx = 0 }) catch return 1.0; + self.primary_face_metrics = primary_face.getMetrics() catch return 1.0; + } + + const primary_metrics = self.primary_face_metrics.?; + + // We normalize the metrics values which are expressed in px to instead + // be in ems, so that it doesn't matter what size the faces actually are. + const primary_scale = 1 / primary_metrics.px_per_em; + const face_scale = 1 / face_metrics.px_per_em; + + // We get the target metrics from the primary face and this face depending + // on the specified `adjustment`. If this face doesn't explicitly define a + // metric, or if the value it defines is invalid, we fall through to other + // options in the order below. + // + // In order to make sure the value is valid, we compare it with the result + // of the estimator function, which rules out both null and invalid values. + const primary_metric: f64, const face_metric: f64 = + normalize_by: switch (adjustment) { + .ic_width => { + if (face_metrics.ic_width != face_metrics.icWidth()) + continue :normalize_by .ex_height; + + break :normalize_by .{ + primary_metrics.icWidth() * primary_scale, + face_metrics.icWidth() * face_scale, + }; + }, + + .ex_height => { + if (face_metrics.ex_height != face_metrics.exHeight()) + continue :normalize_by .cap_height; + + break :normalize_by .{ + primary_metrics.exHeight() * primary_scale, + face_metrics.exHeight() * face_scale, + }; + }, + + .cap_height => { + if (face_metrics.cap_height != face_metrics.capHeight()) + continue :normalize_by .line_height; + + break :normalize_by .{ + primary_metrics.capHeight() * primary_scale, + face_metrics.capHeight() * face_scale, + }; + }, + + .line_height => .{ + primary_metrics.lineHeight() * primary_scale, + face_metrics.lineHeight() * face_scale, + }, + + .none => unreachable, + }; + + return primary_metric / face_metric; +} + const UpdateMetricsError = font.Face.GetMetricsError || error{ CannotLoadPrimaryFont, }; @@ -478,9 +663,9 @@ const UpdateMetricsError = font.Face.GetMetricsError || error{ pub fn updateMetrics(self: *Collection) UpdateMetricsError!void { const primary_face = self.getFace(.{ .idx = 0 }) catch return error.CannotLoadPrimaryFont; - const face_metrics = try primary_face.getMetrics(); + self.primary_face_metrics = try primary_face.getMetrics(); - var metrics = Metrics.calc(face_metrics); + var metrics = Metrics.calc(self.primary_face_metrics.?); metrics.apply(self.metric_modifiers); @@ -555,63 +740,35 @@ pub const LoadOptions = struct { /// last resort, so we should prefer exactness if possible. pub const Entry = struct { const AnyFace = union(enum) { - deferred: DeferredFace, // Not loaded - loaded: Face, // Loaded, explicit use + /// Not yet loaded. + deferred: DeferredFace, - // The same as deferred/loaded but fallback font semantics (see large - // comment above Entry). - fallback_deferred: DeferredFace, - fallback_loaded: Face, + /// Loaded. + loaded: Face, }; face: AnyFace, - // Metric by which to normalize the font's size to the primary font. - // Default to ic_width to ensure appropriate normalization of CJK - // font sizes when mixed with latin fonts. See the `scaleSize(...)` - // implementation for fallback rules when the font does not define - // the specified metric. - size_adjust_metric: SizeAdjustmentMetric = .ic_width, + /// Whether this face is a fallback face, see + /// main doc comment on Entry for more info. + fallback: bool, - /// Font metrics that can be specified for font size adjustment. - pub const SizeAdjustmentMetric = enum { - /// Don't adjust the size for this font, use the original point size. - none, - /// Match ideograph character width with the primary font. - ic_width, - /// Match ex height with the primary font. - ex_height, - /// Match cap height with the primary font. - cap_height, - /// Match line height with the primary font. - line_height, - }; - - /// Create an entry for the provided face. - pub fn init(face: AnyFace) Entry { - return .{ .face = face }; - } - - /// Convenience initializer that also takes a scale reference - pub fn initWithScaleReference( - face: AnyFace, - scale_reference: SizeAdjustmentMetric, - ) Entry { - return .{ .face = face, .size_adjust_metric = scale_reference }; - } - - /// Initialize a new entry with the same scale reference as an existing entry - pub fn initCopy(self: Entry, face: AnyFace) Entry { - return .{ .face = face, .size_adjust_metric = self.size_adjust_metric }; - } + /// Factor to multiply the collection size by for this face, or + /// else the size adjustment that should be used to calculate + /// once the face is loaded. + /// + /// This is computed when the face is loaded, based on a scale + /// factor computed for an adjustment from the primary face to + /// this one, which allows fallback fonts to be harmonized with + /// the primary font by matching one of the metrics between them. + scale_factor: union(enum) { + adjustment: SizeAdjustment, + scale: f64, + } = .{ .scale = 1.0 }, pub fn deinit(self: *Entry) void { switch (self.face) { - inline .deferred, - .loaded, - .fallback_deferred, - .fallback_loaded, - => |*v| v.deinit(), + inline .deferred, .loaded => |*v| v.deinit(), } } @@ -619,16 +776,16 @@ pub const Entry = struct { /// otherwise returns null. pub fn getLoaded(self: *Entry) ?*Face { return switch (self.face) { - .deferred, .fallback_deferred => null, - .loaded, .fallback_loaded => |*face| face, + .deferred => null, + .loaded => |*face| face, }; } /// True if the entry is deferred. fn isDeferred(self: Entry) bool { return switch (self.face) { - .deferred, .fallback_deferred => true, - .loaded, .fallback_loaded => false, + .deferred => true, + .loaded => false, }; } @@ -638,177 +795,33 @@ pub const Entry = struct { cp: u32, p_mode: PresentationMode, ) bool { - return switch (self.face) { - // Non-fallback fonts require explicit presentation matching but - // otherwise don't care about presentation - .deferred => |v| switch (p_mode) { - .explicit => |p| v.hasCodepoint(cp, p), - .default, .any => v.hasCodepoint(cp, null), - }, + return mode: switch (p_mode) { + .default => |p| if (self.fallback) + // Fallback fonts require explicit presentation matching. + continue :mode .{ .explicit = p } + else + // Non-fallback fonts do not. + continue :mode .any, - .loaded => |face| switch (p_mode) { - .explicit => |p| explicit: { + .explicit => |p| switch (self.face) { + .deferred => |v| v.hasCodepoint(cp, p), + + .loaded => |face| explicit: { const index = face.glyphIndex(cp) orelse break :explicit false; break :explicit switch (p) { .text => !face.isColorGlyph(index), .emoji => face.isColorGlyph(index), }; }, - .default, .any => face.glyphIndex(cp) != null, }, - // Fallback fonts require exact presentation matching. - .fallback_deferred => |v| switch (p_mode) { - .explicit, .default => |p| v.hasCodepoint(cp, p), - .any => v.hasCodepoint(cp, null), - }, + .any => switch (self.face) { + .deferred => |v| v.hasCodepoint(cp, null), - .fallback_loaded => |face| switch (p_mode) { - .explicit, - .default, - => |p| explicit: { - const index = face.glyphIndex(cp) orelse break :explicit false; - break :explicit switch (p) { - .text => !face.isColorGlyph(index), - .emoji => face.isColorGlyph(index), - }; - }, - .any => face.glyphIndex(cp) != null, + .loaded => |face| face.glyphIndex(cp) != null, }, }; } - - pub const SetSizeError = - font.Face.GetMetricsError || - error{ - /// The call to `face.setSize` failed. - SetSizeFailed, - }; - - /// Set the size of the face for this entry if it's loaded. - /// - /// This takes in to account the `size_adjust_metric` of this entry, - /// adjusting the size in the provided options if a primary entry is - /// provided to scale against. - fn setSize( - self: *Entry, - opts: font.face.Options, - primary_entry: ?*Entry, - ) Entry.SetSizeError!void { - // If not loaded, nothing to do - var face = self.getLoaded() orelse return; - - var new_opts = opts; - - // If we have a primary we rescale - if (primary_entry) |p| { - new_opts.size = try self.scaledSize(new_opts.size, p); - } - - // Before going through with the resize, we check whether the requested - // size after scaling is actually different from the existing size. - if (!std.meta.eql(new_opts.size, face.size)) { - face.setSize(new_opts) catch return error.SetSizeFailed; - } - } - - /// Calculate a size for the face that will match it with the primary font, - /// metrically, to improve consistency with fallback fonts. - /// - /// This returns a scaled copy of the nominal_size, where the points size has - /// been scaled by the font metric ratio specified by self.scale_reference. - /// If either this or the primary face are not yet loaded, or the primary - /// face is the same as this, nominal_size is returned unchanged. - /// - /// This is very much like the `font-size-adjust` CSS property in how it works. - /// ref: https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust - /// - /// TODO: In the future, provide config options that allow the user to select - /// which metric should be matched for fallback fonts, instead of hard - /// coding at the point where a face is added to the collection. - fn scaledSize( - self: *Entry, - nominal_size: DesiredSize, - primary_entry: *Entry, - ) font.Face.GetMetricsError!DesiredSize { - if (self.size_adjust_metric == .none) return nominal_size; - - // If the primary is us, no scaling - if (@intFromPtr(self) == @intFromPtr(primary_entry)) return nominal_size; - - // If we or the primary face aren't loaded, we don't know our metrics, - // so unable to scale - const primary_face = primary_entry.getLoaded() orelse return nominal_size; - const face = self.getLoaded() orelse return nominal_size; - - const primary_metrics = try primary_face.getMetrics(); - const face_metrics = try face.getMetrics(); - - // The face metrics are in pixel units, and both point sizes and dpis - // may differ. The following factors are used to convert ratios of face - // metrics to scaling factors that are size- and dpi-independent and can - // be used to scale point sizes directly. - const primary_y_px_per_72em = primary_face.size.points * @as(f32, @floatFromInt(primary_face.size.ydpi)); - const primary_x_px_per_72em = primary_face.size.points * @as(f32, @floatFromInt(primary_face.size.xdpi)); - - const face_y_px_per_72em = face.size.points * @as(f32, @floatFromInt(face.size.ydpi)); - const face_x_px_per_72em = face.size.points * @as(f32, @floatFromInt(face.size.xdpi)); - - const y_ratio: f64 = face_y_px_per_72em / primary_y_px_per_72em; - const x_ratio: f64 = face_x_px_per_72em / primary_x_px_per_72em; - - // The preferred metric to normalize by is self.scale_reference, - // however we don't want to use a metric not explicitly defined - // in `self`, so if needed we fall back through other metrics in - // the order shown in the switch statement below. If the metric - // is not defined in `primary`, that's OK, we'll use the estimate. - const line_height_ratio = y_ratio * primary_metrics.lineHeight() / face_metrics.lineHeight(); - const scale = normalize_by: switch (self.size_adjust_metric) { - // Even if a metric is non-null, it may be invalid (e.g., negative), - // so we check for equality with the estimator before using it - - .ic_width => { - if (face_metrics.ic_width) |value| if (value == face_metrics.icWidth()) { - break :normalize_by x_ratio * (primary_metrics.icWidth() / value); - }; - continue :normalize_by .ex_height; - }, - - .ex_height => { - if (face_metrics.ex_height) |value| if (value == face_metrics.exHeight()) { - break :normalize_by y_ratio * primary_metrics.exHeight() / value; - }; - continue :normalize_by .cap_height; - }, - - .cap_height => { - if (face_metrics.cap_height) |value| if (value == face_metrics.capHeight()) { - break :normalize_by y_ratio * primary_metrics.capHeight() / value; - }; - continue :normalize_by .line_height; - }, - - .line_height => line_height_ratio, - - .none => unreachable, - }; - - // If the line height of the scaled font would be larger than - // the line height of the primary font, we don't want that, so - // we take the minimum between matching the reference metric - // and keeping the line heights within some margin. - // - // NOTE: We actually allow the line height to be up to 1.2 - // times the primary line height because empirically - // this is usually fine and is better for CJK. - const capped_scale = @min(scale, 1.2 * line_height_ratio); - - // Scale the target size by the final scaling factor and return. - var scaled_size = nominal_size; - scaled_size.points *= @floatCast(capped_scale); - - return scaled_size; - } }; pub const EntryOrAlias = union(enum) { @@ -935,11 +948,15 @@ test "add full" { defer c.deinit(alloc); for (0..Index.Special.start - 1) |_| { - _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( + _ = try c.add(alloc, try .init( lib, testFont, .{ .size = .{ .points = 12 } }, - ) })); + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); } var face = try Face.init( @@ -952,7 +969,11 @@ test "add full" { defer face.deinit(); try testing.expectError( error.CollectionFull, - c.add(alloc, .regular, .init(.{ .loaded = face })), + c.add(alloc, face, .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }), ); } @@ -963,12 +984,15 @@ test "add deferred without loading options" { var c = init(); defer c.deinit(alloc); - try testing.expectError(error.DeferredLoadingUnavailable, c.add( + try testing.expectError(error.DeferredLoadingUnavailable, c.addDeferred( alloc, - .regular, - // This can be undefined because it should never be accessed. - .init(.{ .deferred = undefined }), + undefined, + .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }, )); } @@ -983,11 +1007,15 @@ test getFace { var c = init(); defer c.deinit(alloc); - const idx = try c.add(alloc, .regular, .init(.{ .loaded = try .init( + const idx = try c.add(alloc, try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) })); + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); { const face1 = try c.getFace(idx); @@ -1007,11 +1035,15 @@ test getIndex { var c = init(); defer c.deinit(alloc); - _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( + _ = try c.add(alloc, try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) })); + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); // Should find all visible ASCII var i: u32 = 32; @@ -1039,11 +1071,15 @@ test completeStyles { defer c.deinit(alloc); c.load_options = .{ .library = lib }; - _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( + _ = try c.add(alloc, try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) })); + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); try testing.expect(c.getIndex('A', .bold, .{ .any = {} }) == null); try testing.expect(c.getIndex('A', .italic, .{ .any = {} }) == null); @@ -1066,11 +1102,15 @@ test setSize { defer c.deinit(alloc); c.load_options = .{ .library = lib }; - _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( + _ = try c.add(alloc, try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) })); + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); try testing.expectEqual(@as(u32, 12), c.load_options.?.size.points); try c.setSize(.{ .points = 24 }); @@ -1089,11 +1129,15 @@ test hasCodepoint { defer c.deinit(alloc); c.load_options = .{ .library = lib }; - const idx = try c.add(alloc, .regular, .init(.{ .loaded = try .init( + const idx = try c.add(alloc, try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) })); + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); try testing.expect(c.hasCodepoint(idx, 'A', .{ .any = {} })); try testing.expect(!c.hasCodepoint(idx, '🥸', .{ .any = {} })); @@ -1113,11 +1157,15 @@ test "hasCodepoint emoji default graphical" { defer c.deinit(alloc); c.load_options = .{ .library = lib }; - const idx = try c.add(alloc, .regular, .init(.{ .loaded = try .init( + const idx = try c.add(alloc, try .init( lib, testEmoji, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) })); + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); try testing.expect(!c.hasCodepoint(idx, 'A', .{ .any = {} })); try testing.expect(c.hasCodepoint(idx, '🥸', .{ .any = {} })); @@ -1137,11 +1185,15 @@ test "metrics" { const size: DesiredSize = .{ .points = 12, .xdpi = 96, .ydpi = 96 }; c.load_options = .{ .library = lib, .size = size }; - _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( + _ = try c.add(alloc, try .init( lib, testFont, .{ .size = size }, - ) })); + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); try c.updateMetrics(); @@ -1207,37 +1259,40 @@ test "adjusted sizes" { c.load_options = .{ .library = lib, .size = size }; // Add our primary face. - _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( + _ = try c.add(alloc, try .init( lib, testFont, .{ .size = size }, - ) })); + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); try c.updateMetrics(); - // Add the fallback face. - const fallback_idx = try c.add(alloc, .regular, .init(.{ .loaded = try .init( - lib, - fallback, - .{ .size = size }, - ) })); - - const primary_entry = try c.getEntry(.{ .idx = 0 }); inline for ([_][]const u8{ "ex_height", "cap_height" }) |metric| { - const entry = try c.getEntry(fallback_idx); - entry.size_adjust_metric = @field(Entry.SizeAdjustmentMetric, metric); - try entry.setSize(c.load_options.?.faceOptions(), primary_entry); + // Add the fallback face with the chosen adjustment metric. + const fallback_idx = try c.add(alloc, try .init( + lib, + fallback, + .{ .size = size }, + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = @field(SizeAdjustment, metric), + }); // The chosen metric should match. { const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics(); const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics(); - try std.testing.expectApproxEqRel( + try std.testing.expectApproxEqAbs( @field(primary_metrics, metric).?, @field(fallback_metrics, metric).?, - // We accept anything within 5 %. - 0.05, + // We accept anything within half a pixel. + 0.5, ); } @@ -1247,46 +1302,68 @@ test "adjusted sizes" { const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics(); const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics(); - try std.testing.expectApproxEqRel( + try std.testing.expectApproxEqAbs( @field(primary_metrics, metric).?, @field(fallback_metrics, metric).?, - // We accept anything within 5 %. - 0.05, + // We accept anything within half a pixel. + 0.5, ); } + // Reset size for the next iteration try c.setSize(size); } + { + // A reference metric of "none" should leave the size unchanged. + const fallback_idx = try c.add(alloc, try .init( + lib, + fallback, + .{ .size = size }, + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); + + try std.testing.expectEqual( + (try c.getFace(.{ .idx = 0 })).size.points, + (try c.getFace(fallback_idx)).size.points, + ); + + // Resize should keep that. + try c.setSize(.{ .points = 37, .xdpi = 96, .ydpi = 96 }); + + try std.testing.expectEqual( + (try c.getFace(.{ .idx = 0 })).size.points, + (try c.getFace(fallback_idx)).size.points, + ); + + // Reset collection size + try c.setSize(size); + } + // Add the symbol face. - const symbol_idx = try c.add(alloc, .regular, .initWithScaleReference(.{ .loaded = try .init( + const symbol_idx = try c.add(alloc, try .init( lib, symbol, .{ .size = size }, - ) }, .ex_height)); + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .ex_height, + }); // Test fallback to lineHeight() (ex_height and cap_height not defined in symbols font). { const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics(); const symbol_metrics = try (try c.getFace(symbol_idx)).getMetrics(); - try std.testing.expectApproxEqRel( + try std.testing.expectApproxEqAbs( primary_metrics.lineHeight(), symbol_metrics.lineHeight(), - // We accept anything within 5 %. - 0.05, - ); - } - - // A reference metric of "none" should leave the size unchanged. - { - const entry = try c.getEntry(symbol_idx); - entry.size_adjust_metric = .none; - try entry.setSize(c.load_options.?.faceOptions(), primary_entry); - - try std.testing.expectEqual( - (try c.getFace(.{ .idx = 0 })).size.points, - (try c.getFace(symbol_idx)).size.points, + // We accept anything within half a pixel. + 0.5, ); } } diff --git a/src/font/Metrics.zig b/src/font/Metrics.zig index 320a4f504..09c996290 100644 --- a/src/font/Metrics.zig +++ b/src/font/Metrics.zig @@ -55,6 +55,11 @@ const Minimums = struct { /// Metrics extracted from a font face, based on /// the metadata tables and glyph measurements. pub const FaceMetrics = struct { + /// Pixels per em, dividing the other values in this struct by this should + /// yield sizes in ems, to allow comparing metrics from faces of different + /// sizes. + px_per_em: f64, + /// The minimum cell width that can contain any glyph in the ASCII range. /// /// Determined by measuring all printable glyphs in the ASCII range. diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig index ea0bc4458..980b0314c 100644 --- a/src/font/SharedGrid.zig +++ b/src/font/SharedGrid.zig @@ -376,11 +376,15 @@ fn testGrid(mode: TestMode, alloc: Allocator, lib: Library) !SharedGrid { switch (mode) { .normal => { - _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( + _ = try c.add(alloc, try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) })); + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); }, } diff --git a/src/font/SharedGridSet.zig b/src/font/SharedGridSet.zig index 74dd7db08..813a8d6d0 100644 --- a/src/font/SharedGridSet.zig +++ b/src/font/SharedGridSet.zig @@ -200,11 +200,12 @@ fn collection( try face.name(&name_buf), }); - _ = try c.add( - self.alloc, - style, - .init(.{ .deferred = face }), - ); + _ = try c.addDeferred(self.alloc, face, .{ + .style = style, + .fallback = false, + // No size adjustment for primary fonts. + .size_adjustment = .none, + }); continue; } @@ -230,11 +231,12 @@ fn collection( try face.name(&name_buf), }); - _ = try c.add( - self.alloc, - style, - .init(.{ .deferred = face }), - ); + _ = try c.addDeferred(self.alloc, face, .{ + .style = style, + .fallback = false, + // No size adjustment for primary fonts. + .size_adjustment = .none, + }); continue; } @@ -257,59 +259,77 @@ fn collection( // Our built-in font will be used as a backup _ = try c.add( self.alloc, - .regular, - .init(.{ .fallback_loaded = try .init( + try .init( self.font_lib, font.embedded.variable, load_options.faceOptions(), - ) }), + ), + .{ + .style = .regular, + .fallback = true, + .size_adjustment = font.default_fallback_adjustment, + }, ); try (try c.getFace(try c.add( self.alloc, - .bold, - .init(.{ .fallback_loaded = try .init( + try .init( self.font_lib, font.embedded.variable, load_options.faceOptions(), - ) }), + ), + .{ + .style = .bold, + .fallback = true, + .size_adjustment = font.default_fallback_adjustment, + }, ))).setVariations( &.{.{ .id = .init("wght"), .value = 700 }}, load_options.faceOptions(), ); _ = try c.add( self.alloc, - .italic, - .init(.{ .fallback_loaded = try .init( + try .init( self.font_lib, font.embedded.variable_italic, load_options.faceOptions(), - ) }), + ), + .{ + .style = .italic, + .fallback = true, + .size_adjustment = font.default_fallback_adjustment, + }, ); try (try c.getFace(try c.add( self.alloc, - .bold_italic, - .init(.{ .fallback_loaded = try .init( + try .init( self.font_lib, font.embedded.variable_italic, load_options.faceOptions(), - ) }), + ), + .{ + .style = .bold_italic, + .fallback = true, + .size_adjustment = font.default_fallback_adjustment, + }, ))).setVariations( &.{.{ .id = .init("wght"), .value = 700 }}, load_options.faceOptions(), ); // Nerd-font symbols fallback. - // For proper icon scaling, this should be loaded at the same point - // size as the primary font and not undergo size normalization, - // hence we use the em size as scale reference. _ = try c.add( self.alloc, - .regular, - .initWithScaleReference(.{ .fallback_loaded = try .init( + try .init( self.font_lib, font.embedded.symbols_nerd_font, load_options.faceOptions(), - ) }, .none), + ), + .{ + .style = .regular, + .fallback = true, + // No size adjustment for the symbols font. + .size_adjustment = .none, + }, ); // On macOS, always search for and add the Apple Emoji font @@ -324,11 +344,12 @@ fn collection( }); defer disco_it.deinit(); if (try disco_it.next()) |face| { - _ = try c.add( - self.alloc, - .regular, - .init(.{ .fallback_deferred = face }), - ); + _ = try c.addDeferred(self.alloc, face, .{ + .style = .regular, + .fallback = true, + // No size adjustment for emojis. + .size_adjustment = .none, + }); } } @@ -337,21 +358,31 @@ fn collection( if (comptime !builtin.target.os.tag.isDarwin() or Discover == void) { _ = try c.add( self.alloc, - .regular, - .init(.{ .fallback_loaded = try .init( + try .init( self.font_lib, font.embedded.emoji, load_options.faceOptions(), - ) }), + ), + .{ + .style = .regular, + .fallback = true, + // No size adjustment for emojis. + .size_adjustment = .none, + }, ); _ = try c.add( self.alloc, - .regular, - .init(.{ .fallback_loaded = try .init( + try .init( self.font_lib, font.embedded.emoji_text, load_options.faceOptions(), - ) }), + ), + .{ + .style = .regular, + .fallback = true, + // No size adjustment for emojis. + .size_adjustment = .none, + }, ); } diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index a92407ef1..8aec9e7c4 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -241,10 +241,14 @@ pub const Face = struct { desc = next; } + // Put our current size in the opts so that we don't change size. + var new_opts = opts; + new_opts.size = self.size; + // Initialize a font based on these attributes. const ct_font = try self.font.copyWithAttributes(0, null, desc); errdefer ct_font.release(); - const face = try initFont(ct_font, opts); + const face = try initFont(ct_font, new_opts); self.deinit(); self.* = face; } @@ -843,14 +847,20 @@ pub const Face = struct { }; return .{ + .px_per_em = px_per_em, + .cell_width = cell_width, + .ascent = ascent, .descent = descent, .line_gap = line_gap, + .underline_position = underline_position, .underline_thickness = underline_thickness, + .strikethrough_position = strikethrough_position, .strikethrough_thickness = strikethrough_thickness, + .cap_height = cap_height, .ex_height = ex_height, .ic_width = ic_width, diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 5d5ed28c4..4e7100396 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -933,6 +933,8 @@ pub const Face = struct { }; return .{ + .px_per_em = px_per_em, + .cell_width = cell_width, .ascent = ascent, diff --git a/src/font/main.zig b/src/font/main.zig index 627fc6341..782b3e388 100644 --- a/src/font/main.zig +++ b/src/font/main.zig @@ -174,6 +174,11 @@ pub const Presentation = enum(u1) { /// A FontIndex that can be used to use the sprite font directly. pub const sprite_index = Collection.Index.initSpecial(.sprite); +/// The default font size adjustment we use when loading fallback fonts. +/// +/// TODO: Add user configuration for this instead of hard-coding it. +pub const default_fallback_adjustment: Collection.SizeAdjustment = .ic_width; + test { // For non-wasm we want to test everything we can if (!comptime builtin.target.cpu.arch.isWasm()) { diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index a5f63d466..afc7d9adb 100644 --- a/src/font/shaper/coretext.zig +++ b/src/font/shaper/coretext.zig @@ -1779,19 +1779,27 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { c.load_options = .{ .library = lib }; // Setup group - _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( + _ = try c.add(alloc, try .init( lib, testFont, .{ .size = .{ .points = 12 } }, - ) })); + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); if (font.options.backend != .coretext) { // Coretext doesn't support Noto's format - _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( + _ = try c.add(alloc, try .init( lib, testEmoji, .{ .size = .{ .points = 12 } }, - ) })); + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); } else { // On CoreText we want to load Apple Emoji, we should have it. var disco = font.Discover.init(); @@ -1804,13 +1812,21 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { defer disco_it.deinit(); var face = (try disco_it.next()).?; errdefer face.deinit(); - _ = try c.add(alloc, .regular, .init(.{ .deferred = face })); + _ = try c.addDeferred(alloc, face, .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); } - _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( + _ = try c.add(alloc, try .init( lib, testEmojiText, .{ .size = .{ .points = 12 } }, - ) })); + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); const grid_ptr = try alloc.create(SharedGrid); errdefer alloc.destroy(grid_ptr); diff --git a/src/font/shaper/harfbuzz.zig b/src/font/shaper/harfbuzz.zig index a170df382..8a0beab8b 100644 --- a/src/font/shaper/harfbuzz.zig +++ b/src/font/shaper/harfbuzz.zig @@ -1242,19 +1242,27 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { c.load_options = .{ .library = lib }; // Setup group - _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( + _ = try c.add(alloc, try .init( lib, testFont, .{ .size = .{ .points = 12 } }, - ) })); + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); if (comptime !font.options.backend.hasCoretext()) { // Coretext doesn't support Noto's format - _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( + _ = try c.add(alloc, try .init( lib, testEmoji, .{ .size = .{ .points = 12 } }, - ) })); + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); } else { // On CoreText we want to load Apple Emoji, we should have it. var disco = font.Discover.init(); @@ -1267,13 +1275,21 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper { defer disco_it.deinit(); var face = (try disco_it.next()).?; errdefer face.deinit(); - _ = try c.add(alloc, .regular, .init(.{ .deferred = face })); + _ = try c.addDeferred(alloc, face, .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); } - _ = try c.add(alloc, .regular, .init(.{ .loaded = try .init( + _ = try c.add(alloc, try .init( lib, testEmojiText, .{ .size = .{ .points = 12 } }, - ) })); + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); const grid_ptr = try alloc.create(SharedGrid); errdefer alloc.destroy(grid_ptr); diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig index dfff8fa75..cb335dff6 100644 --- a/src/font/sprite/Face.zig +++ b/src/font/sprite/Face.zig @@ -389,6 +389,9 @@ fn testDrawRanges( const alloc = testing.allocator; const metrics: font.Metrics = .calc(.{ + // Fudged number, not used in anything we care about here. + .px_per_em = 16, + .cell_width = @floatFromInt(width), .ascent = @floatFromInt(ascent), .descent = -@as(f64, @floatFromInt(descent)), From 92fa2228e9effa18a695dbb8b739f17077a9600a Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Fri, 25 Jul 2025 12:41:00 -0600 Subject: [PATCH 11/11] font: use non-mono symbols nerd font for embedded symbols I changed my mind, this is a pretty small change and relevant to the intent of the PR. This brings the appearance of the embedded symbols much closer to patched fonts. With this, the sizes of most symbols are nearly identical to a patched font, the only big difference is positioning (and TBH I think we do a better job positioning than the patcher does, since we have knowledge about the cell size). --- src/build/SharedDeps.zig | 2 +- src/font/nerd_font_attributes.zig | 388 +++++++++++++++--------------- src/font/nerd_font_codegen.py | 2 +- 3 files changed, 192 insertions(+), 200 deletions(-) diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index f1a6f80c8..c03746a48 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -531,7 +531,7 @@ pub fn add( const nf_symbols = b.dependency("nerd_fonts_symbols_only", .{}); step.root_module.addAnonymousImport( "nerd_fonts_symbols_only", - .{ .root_source_file = nf_symbols.path("SymbolsNerdFontMono-Regular.ttf") }, + .{ .root_source_file = nf_symbols.path("SymbolsNerdFont-Regular.ttf") }, ); } diff --git a/src/font/nerd_font_attributes.zig b/src/font/nerd_font_attributes.zig index 4ec55d2ff..55e5604c3 100644 --- a/src/font/nerd_font_attributes.zig +++ b/src/font/nerd_font_attributes.zig @@ -41,7 +41,7 @@ pub fn getConstraint(cp: u21) Constraint { .max_constraint_width = 1, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.4028056112224450, + .group_width = 1.3999999999999999, .group_height = 1.1222570532915361, .group_x = 0.1428571428571428, .group_y = 0.0349162011173184, @@ -395,8 +395,7 @@ pub fn getConstraint(cp: u21) Constraint { 0xeb72...0xeb89, 0xeb8b...0xeb99, 0xeb9b...0xebd4, - 0xebd6, - 0xebd8...0xec06, + 0xebd7...0xec06, 0xec08...0xec0a, 0xec0d...0xec1e, 0xed00...0xf018, @@ -412,8 +411,7 @@ pub fn getConstraint(cp: u21) Constraint { 0xf07c...0xf080, 0xf082...0xf08b, 0xf08d...0xf091, - 0xf093...0xf09b, - 0xf09d...0xf09e, + 0xf093...0xf09e, 0xf0a0, 0xf0a5...0xf0a9, 0xf0ab...0xf0c9, @@ -421,7 +419,7 @@ pub fn getConstraint(cp: u21) Constraint { 0xf0d7...0xf0dd, 0xf0df...0xf0e6, 0xf0e8...0xf295, - 0xf297...0xf2c1, + 0xf297...0xf2c3, 0xf2c6...0xf2ef, 0xf2f1...0xf305, 0xf307...0xf847, @@ -440,10 +438,10 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.3315669947009841, - .group_height = 1.0763840224246670, - .group_x = 0.0847072200113701, - .group_y = 0.0709635416666667, + .group_width = 1.3310225303292895, + .group_height = 1.0762439807383628, + .group_x = 0.0846354166666667, + .group_y = 0.0708426547352722, }, 0xea7d, => .{ @@ -452,10 +450,10 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.1913145539906103, - .group_height = 1.1428571428571428, - .group_x = 0.0916256157635468, - .group_y = 0.0415039062500000, + .group_width = 1.1912058627581612, + .group_height = 1.1426759670259987, + .group_x = 0.0917225950782998, + .group_y = 0.0416204217536071, }, 0xea99, => .{ @@ -464,10 +462,10 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.0639412997903563, - .group_height = 2.0940695296523519, - .group_x = 0.0295566502463054, - .group_y = 0.2270507812500000, + .group_width = 1.0642857142857143, + .group_height = 2.0929152148664345, + .group_x = 0.0302013422818792, + .group_y = 0.2269700332963374, }, 0xea9a, 0xeaa1, @@ -477,10 +475,10 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.3029525032092426, - .group_height = 1.1729667812142039, - .group_x = 0.1527093596059113, - .group_y = 0.0751953125000000, + .group_width = 1.3032069970845481, + .group_height = 1.1731770833333333, + .group_x = 0.1526845637583893, + .group_y = 0.0754716981132075, }, 0xea9b, => .{ @@ -489,10 +487,10 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.1639908256880733, - .group_height = 1.3128205128205128, - .group_x = 0.0719211822660099, - .group_y = 0.0869140625000000, + .group_width = 1.1640625000000000, + .group_height = 1.3134110787172011, + .group_x = 0.0721476510067114, + .group_y = 0.0871254162042175, }, 0xea9c, => .{ @@ -501,10 +499,10 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.1639908256880733, - .group_height = 1.3195876288659794, - .group_x = 0.0719211822660099, - .group_y = 0.0830078125000000, + .group_width = 1.1640625000000000, + .group_height = 1.3201465201465201, + .group_x = 0.0721476510067114, + .group_y = 0.0832408435072142, }, 0xea9d, 0xeaa0, @@ -514,10 +512,10 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 2.4457831325301207, - .group_height = 1.9692307692307693, - .group_x = 0.2857142857142857, - .group_y = 0.2763671875000000, + .group_width = 2.4493150684931506, + .group_height = 1.9693989071038251, + .group_x = 0.2863534675615212, + .group_y = 0.2763596004439512, }, 0xea9e...0xea9f, => .{ @@ -526,10 +524,10 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.9556840077071291, - .group_height = 2.4674698795180725, - .group_x = 0.2137931034482759, - .group_y = 0.3066406250000000, + .group_width = 1.9540983606557376, + .group_height = 2.4684931506849317, + .group_x = 0.2136465324384788, + .group_y = 0.3068812430632630, }, 0xeaa2, => .{ @@ -538,10 +536,10 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.2412121212121212, - .group_height = 1.0591799039527152, - .group_x = 0.0683593750000000, - .group_y = 0.0146484375000000, + .group_width = 1.2405228758169935, + .group_height = 1.0595187680461982, + .group_x = 0.0679662802950474, + .group_y = 0.0147523709167545, }, 0xeab4, => .{ @@ -550,9 +548,9 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.0049115913555993, - .group_height = 1.8998144712430427, - .group_y = 0.2026367187500000, + .group_width = 1.0054815974941269, + .group_height = 1.8994082840236686, + .group_y = 0.2024922118380062, }, 0xeab5, => .{ @@ -561,10 +559,10 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.8979591836734695, - .group_height = 1.0054000981836033, - .group_x = 0.2023460410557185, - .group_y = 0.0053710937500000, + .group_width = 1.8994082840236686, + .group_height = 1.0054815974941269, + .group_x = 0.2024922118380062, + .group_y = 0.0054517133956386, }, 0xeab6, => .{ @@ -573,9 +571,9 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.8979591836734695, - .group_height = 1.0054000981836033, - .group_x = 0.2707722385141740, + .group_width = 1.8994082840236686, + .group_height = 1.0054815974941269, + .group_x = 0.2710280373831775, }, 0xeab7, => .{ @@ -584,10 +582,10 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.0049115913555993, - .group_height = 1.8980537534754403, - .group_x = 0.0048875855327468, - .group_y = 0.2709960937500000, + .group_width = 1.0054815974941269, + .group_height = 1.8994082840236686, + .group_x = 0.0054517133956386, + .group_y = 0.2710280373831775, }, 0xead4...0xead5, => .{ @@ -596,8 +594,8 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.4152542372881356, - .group_x = 0.1486118671747414, + .group_width = 1.4144620811287478, + .group_x = 0.1483790523690773, }, 0xead6, => .{ @@ -606,8 +604,8 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_height = 1.1390433815350389, - .group_y = 0.0688476562500000, + .group_height = 1.1388535031847133, + .group_y = 0.0687919463087248, }, 0xeb43, => .{ @@ -616,10 +614,10 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.3635153129161119, - .group_height = 1.0002360944082516, - .group_x = 0.1992187500000000, - .group_y = 0.0002360386808388, + .group_width = 1.3631840796019901, + .group_height = 1.0003813300793167, + .group_x = 0.1991657977059437, + .group_y = 0.0003811847221163, }, 0xeb6e, 0xeb71, @@ -629,8 +627,8 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_height = 2.0197238658777121, - .group_y = 0.2524414062500000, + .group_height = 2.0183246073298431, + .group_y = 0.2522697795071336, }, 0xeb6f, => .{ @@ -639,8 +637,8 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 2.0098619329388558, - .group_x = 0.2492639842983317, + .group_width = 2.0104712041884816, + .group_x = 0.2493489583333333, }, 0xeb70, => .{ @@ -649,10 +647,10 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 2.0098619329388558, - .group_height = 1.0039215686274510, - .group_x = 0.2492639842983317, - .group_y = 0.0039062500000000, + .group_width = 2.0104712041884816, + .group_height = 1.0039062500000000, + .group_x = 0.2493489583333333, + .group_y = 0.0038910505836576, }, 0xeb8a, => .{ @@ -661,10 +659,10 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 2.8826979472140764, - .group_height = 2.9804097167804766, - .group_x = 0.2634791454730417, - .group_y = 0.3314678485576923, + .group_width = 2.8828125000000000, + .group_height = 2.9818561935339356, + .group_x = 0.2642276422764228, + .group_y = 0.3313050881410256, }, 0xeb9a, => .{ @@ -673,10 +671,10 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.1441340782122904, - .group_height = 1.0591799039527152, - .group_x = 0.0683593750000000, - .group_y = 0.0146484375000000, + .group_width = 1.1440626883664857, + .group_height = 1.0595187680461982, + .group_x = 0.0679662802950474, + .group_y = 0.0147523709167545, }, 0xebd5, => .{ @@ -685,19 +683,19 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.0723270440251573, - .group_height = 1.0728129910948141, - .group_y = 0.0678710937500000, + .group_width = 1.0727069351230425, + .group_height = 1.0730882652023592, + .group_y = 0.0681102082395584, }, - 0xebd7, + 0xebd6, => .{ .size_horizontal = .fit, .size_vertical = .fit, .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_height = 1.0000418544302916, - .group_y = 0.0000418526785714, + .group_height = 1.0003554839321263, + .group_y = 0.0003553576082064, }, 0xec07, => .{ @@ -706,10 +704,10 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 2.8615369874243446, - .group_height = 2.9789458113505249, - .group_x = 0.2609446802539727, - .group_y = 0.3313029661016949, + .group_width = 2.8604846818377689, + .group_height = 2.9804665603035656, + .group_x = 0.2615335565120357, + .group_y = 0.3311487268518519, }, 0xec0b, => .{ @@ -718,9 +716,9 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.0722513089005237, - .group_height = 1.0002360944082516, - .group_y = 0.0002360386808388, + .group_width = 1.0721073225265512, + .group_height = 1.0003813300793167, + .group_y = 0.0003811847221163, }, 0xec0c, => .{ @@ -729,18 +727,17 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.2487804878048780, - .group_x = 0.1992187500000000, + .group_width = 1.2486979166666667, + .group_x = 0.1991657977059437, }, 0xf019, - 0xf08c, => .{ .size_horizontal = .fit, .size_vertical = .fit, .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.0004882812500000, + .group_width = 1.1253968253968254, }, 0xf030, 0xf03e, @@ -750,9 +747,9 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.0004882812500000, - .group_height = 1.1428571428571428, - .group_y = 0.0625000000000000, + .group_width = 1.1253968253968254, + .group_height = 1.1426844014510278, + .group_y = 0.0624338624338624, }, 0xf03d, => .{ @@ -761,9 +758,8 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.0004882812500000, - .group_height = 1.5014662756598240, - .group_y = 0.1669921875000000, + .group_height = 1.3328631875881523, + .group_y = 0.1248677248677249, }, 0xf03f, => .{ @@ -772,8 +768,8 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.6018762826150690, - .group_x = 0.1876220107369448, + .group_width = 1.8003104407193382, + .group_x = 0.0005406676069582, }, 0xf040, => .{ @@ -782,10 +778,10 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.0006976906439684, - .group_height = 1.0001808776182035, - .group_x = 0.0006972042111134, - .group_y = 0.0001808449074074, + .group_width = 1.1263939384681190, + .group_height = 1.0007255897868335, + .group_x = 0.0003164442515641, + .group_y = 0.0001959631589261, }, 0xf044, => .{ @@ -794,10 +790,9 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.1029147024980515, - .group_height = 1.1024142703367676, - .group_x = 0.0463592039005675, - .group_y = 0.0430325010461710, + .group_width = 1.0087313432835820, + .group_height = 1.0077472527472529, + .group_y = 0.0002010014265405, }, 0xf04a, => .{ @@ -806,9 +801,9 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.0004882812500000, - .group_height = 1.3312975252838291, - .group_y = 0.1245571402616279, + .group_width = 1.1253968253968254, + .group_height = 1.3321224771947897, + .group_y = 0.1247354497354497, }, 0xf051, => .{ @@ -817,10 +812,9 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.6007812500000000, - .group_height = 1.3312170271945341, - .group_x = 0.1874084919472914, - .group_y = 0.1245117187500000, + .group_width = 1.7994923857868019, + .group_height = 1.3321224771947897, + .group_y = 0.1247354497354497, }, 0xf052, => .{ @@ -829,10 +823,9 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.1436671384194865, - .group_height = 1.1430165816326530, - .group_x = 0.0624629273607646, - .group_y = 0.0625610266424885, + .group_width = 1.1439802384724422, + .group_height = 1.1430071621244535, + .group_y = 0.0626172338785870, }, 0xf053, => .{ @@ -841,14 +834,11 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.8765709864847797, - .group_height = 1.0707191397207079, - .group_x = 0.2332599943628554, - .group_y = 0.0332682382480123, + .group_width = 2.0025185185185186, + .group_height = 1.1416267186919362, + .group_y = 0.0620882827561120, }, 0xf05a...0xf05b, - 0xf081, - 0xf092, 0xf0aa, => .{ .size_horizontal = .fit, @@ -856,9 +846,9 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.0005935142780173, - .group_height = 1.0001395089285714, - .group_y = 0.0000697447342726, + .group_width = 1.0012592592592593, + .group_height = 1.0002824582824583, + .group_y = 0.0002010014265405, }, 0xf071, => .{ @@ -867,10 +857,10 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.0014662756598240, - .group_height = 1.1428571428571428, - .group_x = 0.0004880429477794, - .group_y = 0.0625000000000000, + .group_width = 1.1253968253968254, + .group_height = 1.1426844014510278, + .group_x = 0.0004701457451810, + .group_y = 0.0624338624338624, }, 0xf078, => .{ @@ -879,10 +869,9 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.0717654378877508, - .group_height = 1.8757195185766613, - .group_x = 0.0331834301604062, - .group_y = 0.1670386385827870, + .group_width = 1.1434320241691844, + .group_height = 2.0026841590612778, + .group_y = 0.1879786499051550, }, 0xf07b, => .{ @@ -891,17 +880,32 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_height = 1.1428571428571428, - .group_y = 0.0625000000000000, + .group_width = 1.1253968253968254, + .group_height = 1.2285368802902055, + .group_y = 0.0930118110236220, }, - 0xf09c, + 0xf081, + 0xf092, => .{ .size_horizontal = .fit, .size_vertical = .fit, .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_height = 1.0810546875000000, + .group_width = 1.1441233373639663, + .group_height = 1.1430071621244535, + .group_y = 0.0626172338785870, + }, + 0xf08c, + => .{ + .size_horizontal = .fit, + .size_vertical = .fit, + .height = .icon, + .align_horizontal = .center, + .align_vertical = .center, + .group_width = 1.2859733978234582, + .group_height = 1.1426844014510278, + .group_y = 0.0624338624338624, }, 0xf09f, => .{ @@ -910,9 +914,8 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.7506617925122907, - .group_height = 1.0810546875000000, - .group_x = 0.2143937211981567, + .group_width = 1.7489690176588770, + .group_x = 0.0006952841596131, }, 0xf0a1, => .{ @@ -921,8 +924,9 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.0009775171065494, - .group_x = 0.0004882812500000, + .group_width = 1.1253968253968254, + .group_height = 1.0749103295228757, + .group_y = 0.0349409448818898, }, 0xf0a2, => .{ @@ -931,10 +935,10 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.1433271023627367, - .group_height = 1.0001395089285714, - .group_x = 0.0624235731978609, - .group_y = 0.0000697447342726, + .group_width = 1.1429529187840552, + .group_height = 1.0002824582824583, + .group_x = 0.0001253913778381, + .group_y = 0.0002010014265405, }, 0xf0a3, => .{ @@ -943,10 +947,9 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.0005760656161586, - .group_height = 1.0001220681837999, - .group_x = 0.0004792774839344, - .group_y = 0.0000610266424885, + .group_width = 1.0005921977940631, + .group_height = 1.0001448722153810, + .group_x = 0.0005918473033957, }, 0xf0a4, => .{ @@ -955,9 +958,9 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.0005935142780173, - .group_height = 1.3335193452380951, - .group_y = 0.1250523085507044, + .group_width = 1.0012592592592593, + .group_height = 1.3332396658348704, + .group_y = 0.1250334663306335, }, 0xf0ca, => .{ @@ -966,9 +969,9 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.0005935142780173, - .group_height = 1.1922501247297521, - .group_y = 0.0806249128190822, + .group_width = 1.0335226652102676, + .group_height = 1.2308163060897437, + .group_y = 0.0938253501046103, }, 0xf0d6, => .{ @@ -977,8 +980,8 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_height = 1.5014662756598240, - .group_y = 0.1669921875000000, + .group_height = 1.4330042313117066, + .group_y = 0.1510826771653543, }, 0xf0de, => .{ @@ -987,10 +990,10 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.2253421114919656, - .group_height = 2.5216400911161729, - .group_x = 0.0918898809523810, - .group_y = 0.6034327009936766, + .group_width = 1.3984670905653893, + .group_height = 2.6619718309859155, + .group_x = 0.0004030632809351, + .group_y = 0.5708994708994709, }, 0xf0e7, => .{ @@ -999,8 +1002,10 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.3336843856081169, - .group_x = 0.1247597299147187, + .group_width = 1.3348918927786344, + .group_height = 1.0001196386424678, + .group_x = 0.0006021702214782, + .group_y = 0.0001196243307751, }, 0xf296, => .{ @@ -1009,21 +1014,10 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.0005148743038617, - .group_height = 1.0385966606705219, - .group_x = 0.0005146093447336, - .group_y = 0.0186218440507742, - }, - 0xf2c2...0xf2c3, - => .{ - .size_horizontal = .fit, - .size_vertical = .fit, - .height = .icon, - .align_horizontal = .center, - .align_vertical = .center, - .group_width = 1.0000770970394737, - .group_height = 1.2864321608040201, - .group_y = 0.1113281250000000, + .group_width = 1.0005202277820979, + .group_height = 1.0386597451628128, + .group_x = 0.0001795653226322, + .group_y = 0.0187142907131644, }, 0xf2c4, => .{ @@ -1032,8 +1026,7 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.0344231791600214, - .group_x = 0.0166002826673519, + .group_width = 1.3292088488938882, }, 0xf2c5, => .{ @@ -1042,10 +1035,10 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.0004538836055876, - .group_height = 1.4840579710144928, - .group_x = 0.0004536776887225, - .group_y = 0.1630859375000000, + .group_width = 1.0118264574212998, + .group_height = 1.1664315937940761, + .group_x = 0.0004377219006858, + .group_y = 0.0713422007255139, }, 0xf2f0, => .{ @@ -1054,9 +1047,9 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.0005935142780173, - .group_height = 1.0334438518091393, - .group_y = 0.0161807783512345, + .group_width = 1.0012592592592593, + .group_height = 1.0342088873926949, + .group_y = 0.0165984862232646, }, 0xf306, => .{ @@ -1065,8 +1058,7 @@ pub fn getConstraint(cp: u21) Constraint { .height = .icon, .align_horizontal = .center, .align_vertical = .center, - .group_width = 1.2427184466019416, - .group_x = 0.0976562500000000, + .group_width = 1.3001222493887530, }, else => .none, }; diff --git a/src/font/nerd_font_codegen.py b/src/font/nerd_font_codegen.py index ad5cd0814..e314bbd02 100644 --- a/src/font/nerd_font_codegen.py +++ b/src/font/nerd_font_codegen.py @@ -9,7 +9,7 @@ be safe and not malicious or anything. This script requires Python 3.12 or greater, requires that the `fontTools` python module is installed, and requires that the path to a copy of the -SymbolsNerdFontMono font is passed as the first argument to the script. +SymbolsNerdFont (not Mono!) font is passed as the first argument to it. """ import ast