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/CodepointResolver.zig b/src/font/CodepointResolver.zig index 16536300c..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 = .{ .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, - .{ .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, .{ .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, - .{ .loaded = try .init( - lib, - testEmoji, - .{ .size = .{ .points = 12 } }, - ) }, - ); - } - _ = try c.add( - alloc, - .regular, - .{ .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, .{ .loaded = try .init( + _ = try c.add(alloc, try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) }); - _ = try c.add(alloc, .bold, .{ .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, .{ .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 eb4349fb0..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. @@ -62,149 +68,124 @@ 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(); + // 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); } 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 || - AdjustSizeError || + Face.GetMetricsError || error{ + /// There's no more room in the collection. CollectionFull, - 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, face); - - var owned: *Entry = list.at(idx); - - // 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; - } - } - - 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. - // - // 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; - - 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; - - // 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) - primary_ic / face_ic - else - primary_ex / face_ex, + // Scale factor to adjust the size of the added face. + const scale_factor = self.scaleFactor( + try owned_face.getMetrics(), + opts.size_adjustment, ); - // 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)); + // 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 opts; + 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 @@ -213,30 +194,15 @@ pub fn adjustedSize( /// 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; + if (index.idx >= list.len) return error.IndexOutOfBounds; + return list.at(index.idx).getEntry(); } /// Get the face from an entry. @@ -245,41 +211,45 @@ 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.*) { - inline .deferred, .fallback_deferred => |*d, tag| deferred: { - const opts = self.load_options orelse + return switch (entry.face) { + .deferred => |*d| deferred: { + var opts = self.load_options orelse return error.DeferredLoadingUnavailable; + + // 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(); - // If we need to adjust the size, do so. - if (adjust) if (try self.adjustedSize(&face)) |new_opts| { - try face.setSize(new_opts.faceOptions()); - }; + // Set the loaded face on the entry. + entry.face = .{ .loaded = face }; - entry.* = switch (tag) { - .deferred => .{ .loaded = face }, - .fallback_deferred => .{ .fallback_loaded = face }, - else => unreachable, - }; - - break :deferred switch (tag) { - .deferred => &entry.loaded, - .fallback_deferred => &entry.fallback_loaded, - else => unreachable, - }; + // Return the pointer to it. + break :deferred &entry.face.loaded; }, - .loaded, .fallback_loaded => |*f| f, - - // When setting `entry` above, we ensure we don't end up with - // an alias. - .alias => unreachable, + .loaded => |*f| f, }; } @@ -297,8 +267,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 (entry_or_alias.getConstEntry().hasCodepoint(cp, p_mode)) { return .{ .style = style, .idx = @intCast(i), @@ -324,7 +294,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 list.at(index.idx).getConstEntry().hasCodepoint(cp, p_mode); } pub const CompleteError = Allocator.Error || error{ @@ -362,10 +332,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.getEntry(); + const face = self.getFaceFromEntry(entry) catch |err| { log.warn("error loading regular entry={d} err={}", .{ it.index - 1, err, @@ -408,8 +379,12 @@ pub fn completeStyles( break :italic; }; + const synthetic_entry: Entry = .{ + .face = .{ .loaded = synthetic }, + .fallback = false, + }; 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. @@ -428,8 +403,12 @@ pub fn completeStyles( break :bold; }; + const synthetic_entry: Entry = .{ + .face = .{ .loaded = synthetic }, + .fallback = false, + }; 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 @@ -445,9 +424,14 @@ 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).getEntry(); + 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: Entry = .{ + .face = .{ .loaded = synthetic }, + .fallback = false, + }; + try bold_italic_list.append(alloc, .{ .entry = synthetic_entry }); break :bold_italic; } else |_| {} @@ -455,23 +439,14 @@ 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).getEntry(); 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: Entry = .{ + .face = .{ .loaded = synthetic }, + .fallback = false, + }; + try bold_italic_list.append(alloc, .{ .entry = synthetic_entry }); break :bold_italic; } else |_| {} @@ -480,7 +455,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; @@ -489,7 +464,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; @@ -500,7 +480,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; @@ -509,7 +489,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; @@ -520,43 +505,153 @@ fn syntheticItalic(self: *Collection, entry: *Entry) !Face { return face; } +pub const 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 /// 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 - else - return error.DeferredLoadingUnavailable; + const opts = &(self.load_options orelse return error.DeferredLoadingUnavailable); opts.size = size; + 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); - 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()); - }, + // 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; - // 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, + const entry = entry_or_alias.getEntry(); - // Alias faces don't own their size. - .alias => continue, - }; + 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, }; @@ -568,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); @@ -585,11 +680,11 @@ 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. -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. @@ -643,49 +738,54 @@ 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) { + /// 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, + }; - // 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, + + /// Whether this face is a fallback face, see + /// main doc comment on Entry for more info. + fallback: bool, + + /// 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.*) { - 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 => {}, + switch (self.face) { + inline .deferred, .loaded => |*v| v.deinit(), } } - /// 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.*) { - .deferred, .fallback_deferred => null, - .loaded, .fallback_loaded => |*face| face, - .alias => |v| v.getLoaded(), + return switch (self.face) { + .deferred => null, + .loaded => |*face| face, }; } /// True if the entry is deferred. fn isDeferred(self: Entry) bool { - return switch (self) { - .deferred, .fallback_deferred => true, - .loaded, .fallback_loaded => false, - .alias => |v| v.isDeferred(), + return switch (self.face) { + .deferred => true, + .loaded => false, }; } @@ -695,49 +795,59 @@ pub const Entry = union(enum) { cp: u32, p_mode: PresentationMode, ) bool { - return switch (self) { - .alias => |v| v.hasCodepoint(cp, p_mode), + 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, - // 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), - }, + .explicit => |p| switch (self.face) { + .deferred => |v| v.hasCodepoint(cp, p), - .loaded => |face| switch (p_mode) { - .explicit => |p| explicit: { + .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 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, + + /// Get a pointer to the underlying entry. + pub fn getEntry(self: *EntryOrAlias) *Entry { + return switch (self.*) { + .entry => |*v| v, + .alias => |v| v, + }; + } + + /// 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, + }; + } +}; + /// The requested presentation for a codepoint. pub const PresentationMode = union(enum) { /// The codepoint has an explicit presentation that is required, @@ -838,11 +948,15 @@ 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, try .init( lib, testFont, .{ .size = .{ .points = 12 } }, - ) }); + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); } var face = try Face.init( @@ -855,7 +969,11 @@ test "add full" { defer face.deinit(); try testing.expectError( error.CollectionFull, - c.add(alloc, .regular, .{ .loaded = face }), + c.add(alloc, face, .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }), ); } @@ -866,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. - .{ .deferred = undefined }, + undefined, + .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }, )); } @@ -886,11 +1007,15 @@ 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, try .init( lib, testFont, .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, - ) }); + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); { const face1 = try c.getFace(idx); @@ -910,11 +1035,15 @@ test getIndex { var c = init(); defer c.deinit(alloc); - _ = try c.add(alloc, .regular, .{ .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; @@ -942,11 +1071,15 @@ test completeStyles { defer c.deinit(alloc); c.load_options = .{ .library = lib }; - _ = try c.add(alloc, .regular, .{ .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); @@ -969,11 +1102,15 @@ test setSize { defer c.deinit(alloc); c.load_options = .{ .library = lib }; - _ = try c.add(alloc, .regular, .{ .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 }); @@ -992,11 +1129,15 @@ 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, 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 = {} })); @@ -1016,11 +1157,15 @@ 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, 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 = {} })); @@ -1040,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, .{ .loaded = try .init( + _ = try c.add(alloc, try .init( lib, testFont, .{ .size = size }, - ) }); + ), .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); try c.updateMetrics(); @@ -1099,6 +1248,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(); @@ -1109,43 +1259,109 @@ 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, 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, .{ .loaded = try .init( - lib, - fallback, - .{ .size = size }, - ) }); + inline for ([_][]const u8{ "ex_height", "cap_height" }) |metric| { + // 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 ex heights should match. - { - const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics(); - const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics(); + // 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.expectApproxEqAbs( - primary_metrics.ex_height.?, - fallback_metrics.ex_height.?, - // We accept anything within half a pixel. - 0.5, - ); + try std.testing.expectApproxEqAbs( + @field(primary_metrics, metric).?, + @field(fallback_metrics, metric).?, + // We accept anything within half a pixel. + 0.5, + ); + } + + // 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.expectApproxEqAbs( + @field(primary_metrics, metric).?, + @field(fallback_metrics, metric).?, + // We accept anything within half a pixel. + 0.5, + ); + } + + // Reset size for the next iteration + try c.setSize(size); } - // Resize should keep that relationship. - try c.setSize(.{ .points = 37, .xdpi = 96, .ydpi = 96 }); + { + // 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, try .init( + lib, + symbol, + .{ .size = size }, + ), .{ + .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 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.?, + primary_metrics.lineHeight(), + symbol_metrics.lineHeight(), // We accept anything within half a pixel. 0.5, ); diff --git a/src/font/Metrics.zig b/src/font/Metrics.zig index 89f6a507f..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. @@ -120,6 +125,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 +206,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 diff --git a/src/font/SharedGrid.zig b/src/font/SharedGrid.zig index 3ccac7fa1..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, .{ .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 14a8babad..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, - .{ .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, - .{ .deferred = face }, - ); + _ = try c.addDeferred(self.alloc, face, .{ + .style = style, + .fallback = false, + // No size adjustment for primary fonts. + .size_adjustment = .none, + }); continue; } @@ -257,42 +259,58 @@ fn collection( // Our built-in font will be used as a backup _ = try c.add( self.alloc, - .regular, - .{ .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, - .{ .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, - .{ .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, - .{ .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(), @@ -301,12 +319,17 @@ fn collection( // Nerd-font symbols fallback. _ = try c.add( self.alloc, - .regular, - .{ .fallback_loaded = try Face.init( + try .init( self.font_lib, font.embedded.symbols_nerd_font, load_options.faceOptions(), - ) }, + ), + .{ + .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 @@ -321,11 +344,12 @@ fn collection( }); defer disco_it.deinit(); if (try disco_it.next()) |face| { - _ = try c.add( - self.alloc, - .regular, - .{ .fallback_deferred = face }, - ); + _ = try c.addDeferred(self.alloc, face, .{ + .style = .regular, + .fallback = true, + // No size adjustment for emojis. + .size_adjustment = .none, + }); } } @@ -334,21 +358,31 @@ fn collection( if (comptime !builtin.target.os.tag.isDarwin() or Discover == void) { _ = try c.add( self.alloc, - .regular, - .{ .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, - .{ .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.zig b/src/font/face.zig index 968ee1302..a20df8c11 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -52,9 +52,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; } /// Make this a valid gobject if we're in a GTK environment. @@ -233,7 +233,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, @@ -244,22 +244,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; @@ -383,11 +367,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; diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index cde50ab37..2a3696d3f 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(); @@ -240,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; } @@ -842,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 f42868e5c..4e7100396 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 @@ -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/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 diff --git a/src/font/shaper/coretext.zig b/src/font/shaper/coretext.zig index f4f01d105..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, .{ .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, .{ .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, .{ .deferred = face }); + _ = try c.addDeferred(alloc, face, .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); } - _ = try c.add(alloc, .regular, .{ .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 4209f795c..8a0beab8b 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] }); @@ -1247,19 +1242,27 @@ 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, 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, .{ .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(); @@ -1272,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, .{ .deferred = face }); + _ = try c.addDeferred(alloc, face, .{ + .style = .regular, + .fallback = false, + .size_adjustment = .none, + }); } - _ = try c.add(alloc, .regular, .{ .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)),