aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/font/Collection.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/font/Collection.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/font/Collection.zig')
-rw-r--r--vendor/libghostty-vt/src/font/Collection.zig1523
1 files changed, 1523 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/font/Collection.zig b/vendor/libghostty-vt/src/font/Collection.zig
new file mode 100644
index 00000000..5d7bfa51
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/Collection.zig
@@ -0,0 +1,1523 @@
+//! A font collection is a list of faces of different styles. The list is
+//! ordered by priority (per style). All fonts in a collection share the same
+//! size so they can be used interchangeably in cases a glyph is missing in one
+//! and present in another.
+//!
+//! The purpose of a collection is to store a list of fonts by style
+//! and priority order. A collection does not handle searching for font
+//! callbacks, rasterization, etc. For this, see CodepointResolver.
+//!
+//! The collection can contain both loaded and deferred faces. Deferred faces
+//! typically use less memory while still providing some necessary information
+//! such as codepoint support, presentation, etc. This is useful for looking
+//! for fallback fonts as efficiently as possible. For example, when the glyph
+//! "X" is not found, we can quickly search through deferred fonts rather
+//! than loading the font completely.
+const Collection = @This();
+
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const config = @import("../config.zig");
+const comparison = @import("../datastruct/comparison.zig");
+const font = @import("main.zig");
+const options = font.options;
+const DeferredFace = font.DeferredFace;
+const DesiredSize = font.face.DesiredSize;
+const Face = font.Face;
+const Library = font.Library;
+const Metrics = font.Metrics;
+const Presentation = font.Presentation;
+const Style = font.Style;
+
+const log = std.log.scoped(.font_collection);
+
+/// The available faces we have. This shouldn't be modified manually.
+/// Instead, use the functions available on Collection.
+faces: StyleArray,
+
+/// The metric modifiers to use for this collection. The memory
+/// for this is owned by the user and is not freed by the collection.
+///
+/// Call `Collection.updateMetrics` to recompute the
+/// collection's metrics after making changes to these.
+metric_modifiers: Metrics.ModifierSet = .{},
+
+/// Metrics for this collection. Call `Collection.updateMetrics` to (re)compute
+/// 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.
+load_options: ?LoadOptions = null,
+
+/// Initialize an empty collection.
+pub fn init() Collection {
+ // Initialize our styles array, preallocating some space that is
+ // likely to be used.
+ return .{ .faces = .initFill(.{}) };
+}
+
+pub fn deinit(self: *Collection, alloc: Allocator) void {
+ var it = self.faces.iterator();
+ while (it.next()) |array| {
+ var entry_it = array.value.iterator(0);
+ // 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 ||
+ error{
+ /// There's no more room in the collection.
+ CollectionFull,
+ /// The call to `face.setSize` failed.
+ SetSizeFailed,
+ };
+
+/// 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.
+///
+/// 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,
+ face: Face,
+ opts: AddOptions,
+) AddError!Index {
+ 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;
+
+ var owned_face = face;
+
+ // Scale factor to adjust the size of the added face.
+ const scale_factor = self.scaleFactor(
+ owned_face.getMetrics(),
+ opts.size_adjustment,
+ );
+
+ // 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;
+ }
+
+ 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
+/// 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));
+}
+
+pub const EntryError = error{
+ /// Index represents a special font (built-in) and these don't
+ /// have an associated face. This should be caught upstream and use
+ /// alternate logic.
+ SpecialHasNoFace,
+
+ /// Invalid index.
+ IndexOutOfBounds,
+};
+
+/// Get the unaliased entry from an index
+pub fn getEntry(self: *Collection, index: Index) EntryError!*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();
+}
+
+/// Get the face from an entry.
+///
+/// This entry must not be an alias.
+fn getFaceFromEntry(
+ self: *Collection,
+ entry: *Entry,
+) !*Face {
+ 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.
+ if (entry.scale_factor == .adjustment) {
+ const factor = self.scaleFactor(
+ face.getMetrics(),
+ entry.scale_factor.adjustment,
+ );
+ entry.scale_factor = .{ .scale = factor };
+ }
+
+ // 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();
+
+ // Set the loaded face on the entry.
+ entry.face = .{ .loaded = face };
+
+ // Return the pointer to it.
+ break :deferred &entry.face.loaded;
+ },
+
+ .loaded => |*f| f,
+ };
+}
+
+/// Return the index of the font in this collection that contains
+/// the given codepoint, style, and presentation. If no font is found,
+/// null is returned.
+///
+/// This does not trigger font loading; deferred fonts can be
+/// searched for codepoints.
+pub fn getIndex(
+ self: *const Collection,
+ cp: u32,
+ style: Style,
+ p_mode: PresentationMode,
+) ?Index {
+ var i: usize = 0;
+ var it = self.faces.get(style).constIterator(0);
+ while (it.next()) |entry_or_alias| {
+ if (entry_or_alias.getConstEntry().hasCodepoint(cp, p_mode)) {
+ return .{
+ .style = style,
+ .idx = @intCast(i),
+ };
+ }
+
+ i += 1;
+ }
+
+ // Not found
+ return null;
+}
+
+/// Check if a specific font index has a specific codepoint. This does not
+/// necessarily force the font to load. The presentation value "p" will
+/// verify the Emoji representation matches if it is non-null. If "p" is
+/// null then any presentation will be accepted.
+pub fn hasCodepoint(
+ self: *const Collection,
+ index: Index,
+ cp: u32,
+ p_mode: PresentationMode,
+) bool {
+ const list = self.faces.get(index.style);
+ if (index.idx >= list.count()) return false;
+ return list.at(index.idx).getConstEntry().hasCodepoint(cp, p_mode);
+}
+
+pub const CompleteError = Allocator.Error || error{
+ DefaultUnavailable,
+};
+
+/// Ensure we have an option for all styles in the collection, such
+/// as italic and bold by synthesizing them if necessary from the
+/// first regular face that has text glyphs.
+///
+/// If there is no regular face that has text glyphs, then this
+/// does nothing.
+pub fn completeStyles(
+ self: *Collection,
+ alloc: Allocator,
+ synthetic_config: config.FontSyntheticStyle,
+) CompleteError!void {
+ // If every style has at least one entry then we're done!
+ // This is the most common case.
+ empty: {
+ var it = self.faces.iterator();
+ while (it.next()) |entry| {
+ if (entry.value.count() == 0) break :empty;
+ }
+
+ return;
+ }
+
+ // Find the first regular face that has non-colorized text glyphs.
+ // This is the font we want to fallback to. This may not be index zero
+ // if a user configures something like an Emoji font first.
+ const regular_entry: *Entry = entry: {
+ const list = self.faces.getPtr(.regular);
+ if (list.count() == 0) return;
+
+ // Find our first regular face that has text glyphs.
+ var it = list.iterator(0);
+ 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.getEntry();
+ const face = self.getFaceFromEntry(entry) catch |err| {
+ log.warn("error loading regular entry={d} err={}", .{
+ it.index - 1,
+ err,
+ });
+
+ continue;
+ };
+
+ // We have two conditionals here. The color check is obvious:
+ // we want to auto-italicize a normal text font. The second
+ // check is less obvious... for mixed color/non-color fonts, we
+ // accept the regular font if it has basic ASCII. This may not
+ // be strictly correct (especially with international fonts) but
+ // it's a reasonable heuristic and the first case will match 99%
+ // of the time.
+ if (!face.hasColor() or face.glyphIndex('A') != null) {
+ break :entry entry;
+ }
+ }
+
+ // No regular text face found. We can't provide any fallback.
+ return error.DefaultUnavailable;
+ };
+
+ // If we don't have italic, attempt to create a synthetic italic face.
+ // If we can't create a synthetic italic face, we'll just use the regular
+ // face for italic.
+ const italic_list = self.faces.getPtr(.italic);
+ const have_italic = italic_list.count() > 0;
+ if (!have_italic) italic: {
+ if (!synthetic_config.italic) {
+ log.info("italic style not available and synthetic italic disabled", .{});
+ try italic_list.append(alloc, .{ .alias = regular_entry });
+ break :italic;
+ }
+
+ const synthetic = self.syntheticItalic(regular_entry) catch |err| {
+ log.warn("failed to create synthetic italic, italic style will not be available err={}", .{err});
+ try italic_list.append(alloc, .{ .alias = regular_entry });
+ break :italic;
+ };
+
+ const synthetic_entry: Entry = .{
+ .face = .{ .loaded = synthetic },
+ .fallback = false,
+ };
+ log.info("synthetic italic face created", .{});
+ try italic_list.append(alloc, .{ .entry = synthetic_entry });
+ }
+
+ // If we don't have bold, use the regular font.
+ const bold_list = self.faces.getPtr(.bold);
+ const have_bold = bold_list.count() > 0;
+ if (!have_bold) bold: {
+ if (!synthetic_config.bold) {
+ log.info("bold style not available and synthetic bold disabled", .{});
+ try bold_list.append(alloc, .{ .alias = regular_entry });
+ break :bold;
+ }
+
+ const synthetic = self.syntheticBold(regular_entry) catch |err| {
+ log.warn("failed to create synthetic bold, bold style will not be available err={}", .{err});
+ try bold_list.append(alloc, .{ .alias = regular_entry });
+ break :bold;
+ };
+
+ const synthetic_entry: Entry = .{
+ .face = .{ .loaded = synthetic },
+ .fallback = false,
+ };
+ log.info("synthetic bold face created", .{});
+ try bold_list.append(alloc, .{ .entry = synthetic_entry });
+ }
+
+ // If we don't have bold italic, we attempt to synthesize a bold variant
+ // of the italic font. If we can't do that, we'll use the italic font.
+ const bold_italic_list = self.faces.getPtr(.bold_italic);
+ if (bold_italic_list.count() == 0) bold_italic: {
+ if (!synthetic_config.@"bold-italic") {
+ log.info("bold italic style not available and synthetic bold italic disabled", .{});
+ try bold_italic_list.append(alloc, .{ .alias = regular_entry });
+ break :bold_italic;
+ }
+
+ // 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).getEntry();
+ if (self.syntheticItalic(base_entry)) |synthetic| {
+ log.info("synthetic bold italic face created from bold", .{});
+ const synthetic_entry: Entry = .{
+ .face = .{ .loaded = synthetic },
+ .fallback = false,
+ };
+ try bold_italic_list.append(alloc, .{ .entry = synthetic_entry });
+ break :bold_italic;
+ } else |_| {}
+
+ // If synthesizing italic failed, then we try to synthesize
+ // bold on whatever italic font we have.
+ }
+
+ 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: Entry = .{
+ .face = .{ .loaded = synthetic },
+ .fallback = false,
+ };
+ try bold_italic_list.append(alloc, .{ .entry = synthetic_entry });
+ break :bold_italic;
+ } else |_| {}
+
+ log.warn("bold italic style not available, using italic font", .{});
+ try bold_italic_list.append(alloc, .{ .alias = base_entry });
+ }
+}
+
+/// 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;
+
+ // We require loading options to create a synthetic bold face.
+ const opts = self.load_options orelse return error.DeferredLoadingUnavailable;
+
+ // Try to bold it.
+ 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;
+ if (face.name(&buf)) |name| {
+ log.info("font synthetic bold created family={s}", .{name});
+ } else |_| {}
+
+ return face;
+}
+
+/// 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;
+
+ // We require loading options to create a synthetic italic face.
+ const opts = self.load_options orelse return error.DeferredLoadingUnavailable;
+
+ // Try to italicize it.
+ 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;
+ if (face.name(&buf)) |name| {
+ log.info("font synthetic italic created family={s}", .{name});
+ } else |_| {}
+
+ 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,
+) SetSizeError!void {
+ // Get a pointer to our options so we can modify the size.
+ 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);
+ // 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();
+ }
+
+ 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 = error{
+ CannotLoadPrimaryFont,
+};
+
+/// Update the cell metrics for this collection, based on
+/// the primary font and the modifiers in `metric_modifiers`.
+///
+/// This requires a primary font (index `0`) to be present.
+pub fn updateMetrics(self: *Collection) UpdateMetricsError!void {
+ const primary_face = self.getFace(.{ .idx = 0 }) catch return error.CannotLoadPrimaryFont;
+
+ self.primary_face_metrics = primary_face.getMetrics();
+
+ var metrics = Metrics.calc(self.primary_face_metrics.?);
+
+ metrics.apply(self.metric_modifiers);
+
+ self.metrics = metrics;
+}
+
+/// Packed array of all Style enum cases mapped to a growable list of faces.
+///
+/// We use this data structure because there aren't many styles and all
+/// styles are typically loaded for a terminal session. The overhead per
+/// style even if it is not used or barely used is minimal given the
+/// small style count.
+///
+/// We use a segmented list because the entry values must be pointer-stable
+/// 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(EntryOrAlias, 0));
+
+/// Load options are used to configure all the details a Collection
+/// needs to load deferred faces.
+pub const LoadOptions = struct {
+ /// The library to use for loading faces. This is not owned by
+ /// the collection and can be used by multiple collections. When
+ /// deinitializing the collection, the library is not deinitialized.
+ library: Library,
+
+ /// The desired font size for all loaded faces.
+ size: DesiredSize = .{ .points = 12 },
+
+ /// Freetype Load Flags to use when loading glyphs. This is a list of
+ /// bitfield constants that controls operations to perform during glyph
+ /// loading. Only a subset is exposed for configuration, for the whole set
+ /// of flags see `pkg.freetype.face.LoadFlags`.
+ freetype_load_flags: font.face.FreetypeLoadFlags = font.face.freetype_load_flags_default,
+
+ pub fn deinit(self: *LoadOptions, alloc: Allocator) void {
+ _ = self;
+ _ = alloc;
+ }
+
+ /// The options to use for loading faces.
+ pub fn faceOptions(self: *const LoadOptions) font.face.Options {
+ return .{
+ .size = self.size,
+ .freetype_load_flags = self.freetype_load_flags,
+ };
+ }
+};
+
+/// A entry in a collection can be deferred or loaded. A deferred face
+/// is not yet fully loaded and only represents the font descriptor
+/// and usually uses less resources. A loaded face is fully parsed,
+/// ready to rasterize, and usually uses more resources than a
+/// deferred version.
+///
+/// A face can also be a "fallback" variant that is still either
+/// deferred or loaded. Today, there is only one difference between
+/// fallback and non-fallback (or "explicit") faces: the handling
+/// of emoji presentation.
+///
+/// For explicit faces, when an explicit emoji presentation is
+/// not requested, we will use any glyph for that codepoint found
+/// even if the font presentation does not match the UCD
+/// (Unicode Character Database) value. When an explicit presentation
+/// is requested (via either VS15/V16), that is always honored.
+/// The reason we do this is because we assume that if a user
+/// explicitly chosen a font face (hence it is "explicit" and
+/// 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 = struct {
+ const AnyFace = union(enum) {
+ /// Not yet loaded.
+ deferred: DeferredFace,
+
+ /// Loaded.
+ loaded: Face,
+ };
+
+ 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.face) {
+ inline .deferred, .loaded => |*v| v.deinit(),
+ }
+ }
+
+ /// If this face is loaded, then this returns the `Face`,
+ /// otherwise returns null.
+ pub fn getLoaded(self: *Entry) ?*Face {
+ return switch (self.face) {
+ .deferred => null,
+ .loaded => |*face| face,
+ };
+ }
+
+ /// True if the entry is deferred.
+ fn isDeferred(self: Entry) bool {
+ return switch (self.face) {
+ .deferred => true,
+ .loaded => false,
+ };
+ }
+
+ /// True if this face satisfies the given codepoint and presentation.
+ pub fn hasCodepoint(
+ self: Entry,
+ cp: u32,
+ p_mode: PresentationMode,
+ ) bool {
+ 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,
+
+ .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),
+ };
+ },
+ },
+
+ .any => switch (self.face) {
+ .deferred => |v| v.hasCodepoint(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,
+ /// i.e. VS15/V16.
+ explicit: Presentation,
+
+ /// The codepoint has no explicit presentation and we should use
+ /// the presentation from the UCD.
+ default: Presentation,
+
+ /// The codepoint can be any presentation.
+ any: void,
+};
+
+/// This represents a specific font in the collection.
+///
+/// The backing size of this packed struct represents the total number
+/// of possible usable fonts in a collection. And the number of bits
+/// used for the index and not the style represents the total number
+/// of possible usable fonts for a given style.
+///
+/// The goal is to keep the size of this struct as small as practical. We
+/// accept the limitations that this imposes so long as they're reasonable.
+/// At the time of writing this comment, this is a 16-bit struct with 13
+/// bits used for the index, supporting up to 8192 fonts per style. This
+/// seems more than reasonable. There are synthetic scenarios where this
+/// could be a limitation but I can't think of any that are practical.
+///
+/// If you somehow need more fonts per style, you can increase the size of
+/// the Backing type and everything should just work fine.
+pub const Index = packed struct(Index.Backing) {
+ const Backing = u16;
+ const backing_bits = @typeInfo(Backing).int.bits;
+
+ /// The number of bits we use for the index.
+ const idx_bits = backing_bits - @typeInfo(@typeInfo(Style).@"enum".tag_type).int.bits;
+ pub const IndexInt = @Type(.{ .int = .{ .signedness = .unsigned, .bits = idx_bits } });
+
+ /// The special-case fonts that we support.
+ pub const Special = enum(IndexInt) {
+ // We start all special fonts at this index so they can be detected.
+ pub const start = std.math.maxInt(IndexInt);
+
+ /// Sprite drawing, this is rendered JIT using 2D graphics APIs.
+ sprite = start,
+ };
+
+ style: Style = .regular,
+ idx: IndexInt = 0,
+
+ /// Initialize a special font index.
+ pub fn initSpecial(v: Special) Index {
+ return .{ .style = .regular, .idx = @intFromEnum(v) };
+ }
+
+ /// Convert to int
+ pub fn int(self: Index) Backing {
+ return @bitCast(self);
+ }
+
+ /// Returns true if this is a "special" index which doesn't map to
+ /// a real font face. We can still render it but there is no face for
+ /// this font.
+ pub fn special(self: Index) ?Special {
+ if (self.idx < Special.start) return null;
+ return @enumFromInt(self.idx);
+ }
+
+ test {
+ // We never want to take up more than a byte since font indexes are
+ // everywhere so if we increase the size of this we'll dramatically
+ // increase our memory usage.
+ try std.testing.expectEqual(@sizeOf(Backing), @sizeOf(Index));
+
+ // Just so we're aware when this changes. The current maximum number
+ // of fonts for a style is 13 bits or 8192 fonts.
+ try std.testing.expectEqual(13, idx_bits);
+ }
+};
+
+test init {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var c = init();
+ defer c.deinit(alloc);
+}
+
+test "add full" {
+ // This test is way too slow to run under Valgrind, unfortunately.
+ if (std.valgrind.runningOnValgrind() > 0) return error.SkipZigTest;
+
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const testFont = font.embedded.regular;
+
+ var lib = try Library.init(alloc);
+ defer lib.deinit();
+
+ var c = init();
+ defer c.deinit(alloc);
+
+ for (0..Index.Special.start - 1) |_| {
+ _ = try c.add(alloc, try .init(
+ lib,
+ testFont,
+ .{ .size = .{ .points = 12 } },
+ ), .{
+ .style = .regular,
+ .fallback = false,
+ .size_adjustment = .none,
+ });
+ }
+
+ var face = try Face.init(
+ lib,
+ testFont,
+ .{ .size = .{ .points = 12 } },
+ );
+ // We have to deinit it manually since the
+ // collection doesn't do it if adding fails.
+ defer face.deinit();
+ try testing.expectError(
+ error.CollectionFull,
+ c.add(alloc, face, .{
+ .style = .regular,
+ .fallback = false,
+ .size_adjustment = .none,
+ }),
+ );
+}
+
+test "add deferred without loading options" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var c = init();
+ defer c.deinit(alloc);
+
+ try testing.expectError(error.DeferredLoadingUnavailable, c.addDeferred(
+ alloc,
+ // This can be undefined because it should never be accessed.
+ undefined,
+ .{
+ .style = .regular,
+ .fallback = false,
+ .size_adjustment = .none,
+ },
+ ));
+}
+
+test getFace {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const testFont = font.embedded.regular;
+
+ var lib = try Library.init(alloc);
+ defer lib.deinit();
+
+ var c = init();
+ defer c.deinit(alloc);
+
+ 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);
+ const face2 = try c.getFace(idx);
+ try testing.expectEqual(@intFromPtr(face1), @intFromPtr(face2));
+ }
+}
+
+test getIndex {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const testFont = font.embedded.regular;
+
+ var lib = try Library.init(alloc);
+ defer lib.deinit();
+
+ var c = init();
+ defer c.deinit(alloc);
+
+ _ = 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;
+ while (i < 127) : (i += 1) {
+ const idx = c.getIndex(i, .regular, .{ .any = {} });
+ try testing.expect(idx != null);
+ }
+
+ // Should not find emoji
+ {
+ const idx = c.getIndex('🥸', .regular, .{ .any = {} });
+ try testing.expect(idx == null);
+ }
+}
+
+test completeStyles {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const testFont = font.embedded.regular;
+
+ var lib = try Library.init(alloc);
+ defer lib.deinit();
+
+ var c = init();
+ defer c.deinit(alloc);
+ c.load_options = .{ .library = lib };
+
+ _ = 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);
+ try testing.expect(c.getIndex('A', .bold_italic, .{ .any = {} }) == null);
+ try c.completeStyles(alloc, .{});
+ try testing.expect(c.getIndex('A', .bold, .{ .any = {} }) != null);
+ try testing.expect(c.getIndex('A', .italic, .{ .any = {} }) != null);
+ try testing.expect(c.getIndex('A', .bold_italic, .{ .any = {} }) != null);
+}
+
+test setSize {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const testFont = font.embedded.regular;
+
+ var lib = try Library.init(alloc);
+ defer lib.deinit();
+
+ var c = init();
+ defer c.deinit(alloc);
+ c.load_options = .{ .library = lib };
+
+ _ = 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 });
+ try testing.expectEqual(@as(u32, 24), c.load_options.?.size.points);
+}
+
+test hasCodepoint {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const testFont = font.embedded.regular;
+
+ var lib = try Library.init(alloc);
+ defer lib.deinit();
+
+ var c = init();
+ defer c.deinit(alloc);
+ c.load_options = .{ .library = lib };
+
+ 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 = {} }));
+}
+
+test "hasCodepoint emoji default graphical" {
+ if (options.backend != .fontconfig_freetype) return error.SkipZigTest;
+
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const testEmoji = font.embedded.emoji;
+
+ var lib = try Library.init(alloc);
+ defer lib.deinit();
+
+ var c = init();
+ defer c.deinit(alloc);
+ c.load_options = .{ .library = lib };
+
+ 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 = {} }));
+ // TODO(fontmem): test explicit/implicit
+}
+
+test "metrics" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const testFont = font.embedded.inconsolata;
+
+ var lib = try Library.init(alloc);
+ defer lib.deinit();
+
+ var c = init();
+ defer c.deinit(alloc);
+ const size: DesiredSize = .{ .points = 12, .xdpi = 96, .ydpi = 96 };
+ c.load_options = .{ .library = lib, .size = size };
+
+ _ = try c.add(alloc, try .init(
+ lib,
+ testFont,
+ .{ .size = size },
+ ), .{
+ .style = .regular,
+ .fallback = false,
+ .size_adjustment = .none,
+ });
+
+ try c.updateMetrics();
+
+ try comparison.expectApproxEqual(font.Metrics{
+ .cell_width = 8,
+ // The cell height is 17 px because the calculation is
+ //
+ // ascender - descender + gap
+ //
+ // which, for inconsolata is
+ //
+ // 859 - -190 + 0
+ //
+ // font units, at 1000 units per em that works out to 1.049 em,
+ // and 1em should be the point size * dpi scale, so 12 * (96/72)
+ // which is 16, and 16 * 1.049 = 16.784, which finally is rounded
+ // to 17.
+ //
+ // The icon height is (2 * cap_height + face_height) / 3
+ // = (2 * 623 + 1049) / 3 = 765, and 16 * 0.765 = 12.24.
+ .cell_height = 17,
+ .cell_baseline = 3,
+ .underline_position = 17,
+ .underline_thickness = 1,
+ .strikethrough_position = 10,
+ .strikethrough_thickness = 1,
+ .overline_position = 0,
+ .overline_thickness = 1,
+ .box_thickness = 1,
+ .cursor_height = 17,
+ .icon_height = 16.784,
+ .icon_height_single = 12.24,
+ .face_width = 8.0,
+ .face_height = 16.784,
+ .face_y = -0.04,
+ }, c.metrics);
+
+ // Resize should change metrics
+ try c.setSize(.{ .points = 24, .xdpi = 96, .ydpi = 96 });
+ try comparison.expectApproxEqual(font.Metrics{
+ .cell_width = 16,
+ .cell_height = 34,
+ .cell_baseline = 6,
+ .underline_position = 34,
+ .underline_thickness = 2,
+ .strikethrough_position = 19,
+ .strikethrough_thickness = 2,
+ .overline_position = 0,
+ .overline_thickness = 2,
+ .box_thickness = 2,
+ .cursor_height = 34,
+ .icon_height = 33.568,
+ .icon_height_single = 24.48,
+ .face_width = 16.0,
+ .face_height = 33.568,
+ .face_y = -0.08,
+ }, c.metrics);
+}
+
+// TODO: Also test CJK fallback sizing, we don't currently have a CJK test font.
+test "adjusted sizes" {
+ const testing = std.testing;
+ 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();
+
+ var c = init();
+ defer c.deinit(alloc);
+ const size: DesiredSize = .{ .points = 12, .xdpi = 96, .ydpi = 96 };
+ c.load_options = .{ .library = lib, .size = size };
+
+ // Add our primary face.
+ _ = try c.add(alloc, try .init(
+ lib,
+ testFont,
+ .{ .size = size },
+ ), .{
+ .style = .regular,
+ .fallback = false,
+ .size_adjustment = .none,
+ });
+
+ try c.updateMetrics();
+
+ 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 chosen metric should match.
+ {
+ const primary_metrics = (try c.getFace(.{ .idx = 0 })).getMetrics();
+ const fallback_metrics = (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,
+ );
+ }
+
+ // Resize should keep that relationship.
+ try c.setSize(.{ .points = 37, .xdpi = 96, .ydpi = 96 });
+ {
+ const primary_metrics = (try c.getFace(.{ .idx = 0 })).getMetrics();
+ const fallback_metrics = (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);
+ }
+
+ {
+ // 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 c.getFace(.{ .idx = 0 })).getMetrics();
+ const symbol_metrics = (try c.getFace(symbol_idx)).getMetrics();
+
+ try std.testing.expectApproxEqAbs(
+ primary_metrics.lineHeight(),
+ symbol_metrics.lineHeight(),
+ // We accept anything within half a pixel.
+ 0.5,
+ );
+ }
+}
+
+test "face metrics" {
+ // The web canvas backend doesn't calculate face metrics, only cell metrics
+ if (options.backend != .web_canvas) return error.SkipZigTest;
+
+ const testing = std.testing;
+ const alloc = testing.allocator;
+ const narrowFont = font.embedded.cozette;
+ const wideFont = font.embedded.geist_mono;
+
+ var lib = try Library.init(alloc);
+ defer lib.deinit();
+
+ var c = init();
+ defer c.deinit(alloc);
+ const size: DesiredSize = .{ .points = 12, .xdpi = 96, .ydpi = 96 };
+ c.load_options = .{ .library = lib, .size = size };
+
+ const narrowIndex = try c.add(alloc, try .init(
+ lib,
+ narrowFont,
+ .{ .size = size },
+ ), .{
+ .style = .regular,
+ .fallback = false,
+ .size_adjustment = .none,
+ });
+ const wideIndex = try c.add(alloc, try .init(
+ lib,
+ wideFont,
+ .{ .size = size },
+ ), .{
+ .style = .regular,
+ .fallback = false,
+ .size_adjustment = .none,
+ });
+
+ const narrowMetrics: font.Metrics.FaceMetrics = (try c.getFace(narrowIndex)).getMetrics();
+ const wideMetrics: font.Metrics.FaceMetrics = (try c.getFace(wideIndex)).getMetrics();
+
+ // Verify provided/measured metrics. Measured
+ // values are backend-dependent due to hinting.
+ const narrowMetricsExpected = font.Metrics.FaceMetrics{
+ .px_per_em = 16.0,
+ .cell_width = switch (options.backend) {
+ .freetype,
+ .fontconfig_freetype,
+ .coretext_freetype,
+ => 8.0,
+ .coretext,
+ .coretext_harfbuzz,
+ .coretext_noshape,
+ => 7.3828125,
+ .web_canvas => unreachable,
+ },
+ .ascent = 12.3046875,
+ .descent = -3.6953125,
+ .line_gap = 0.0,
+ .underline_position = -1.2265625,
+ .underline_thickness = 1.2265625,
+ .strikethrough_position = 6.15625,
+ .strikethrough_thickness = 1.234375,
+ .cap_height = 9.84375,
+ .ex_height = 7.3828125,
+ .ascii_height = switch (options.backend) {
+ .freetype,
+ .fontconfig_freetype,
+ .coretext_freetype,
+ => 18.0625,
+ .coretext,
+ .coretext_harfbuzz,
+ .coretext_noshape,
+ => 16.0,
+ .web_canvas => unreachable,
+ },
+ };
+ const wideMetricsExpected = font.Metrics.FaceMetrics{
+ .px_per_em = 16.0,
+ .cell_width = switch (options.backend) {
+ .freetype,
+ .fontconfig_freetype,
+ .coretext_freetype,
+ => 10.0,
+ .coretext,
+ .coretext_harfbuzz,
+ .coretext_noshape,
+ => 9.6,
+ .web_canvas => unreachable,
+ },
+ .ascent = 14.72,
+ .descent = -3.52,
+ .line_gap = 1.6,
+ .underline_position = -1.6,
+ .underline_thickness = 0.8,
+ .strikethrough_position = 4.24,
+ .strikethrough_thickness = 0.8,
+ .cap_height = 11.36,
+ .ex_height = 8.48,
+ .ascii_height = switch (options.backend) {
+ .freetype,
+ .fontconfig_freetype,
+ .coretext_freetype,
+ => 16.0,
+ .coretext,
+ .coretext_harfbuzz,
+ .coretext_noshape,
+ => 15.472000000000001,
+ .web_canvas => unreachable,
+ },
+ };
+
+ inline for (
+ .{ narrowMetricsExpected, wideMetricsExpected },
+ .{ narrowMetrics, wideMetrics },
+ ) |metricsExpected, metricsActual| {
+ try comparison.expectApproxEqual(metricsExpected, metricsActual);
+ }
+
+ // Verify estimated metrics. icWidth() should equal the smaller of
+ // 2 * cell_width and ascii_height. For a narrow (wide) font, the
+ // smaller quantity is the former (latter).
+ try std.testing.expectEqual(
+ 2 * narrowMetrics.cell_width,
+ narrowMetrics.icWidth(),
+ );
+ try std.testing.expectEqual(
+ wideMetrics.ascii_height,
+ wideMetrics.icWidth(),
+ );
+}