diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-04-04 19:22:04 +0300 |
|---|---|---|
| committer | Can Celik <ogulcancelik@gmail.com> | 2026-04-05 01:23:06 +0300 |
| commit | 41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch) | |
| tree | 3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/font/Collection.zig | |
| parent | 55c138263e190a1e79d4b997bfb9397306c0ec1f (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.zig | 1523 |
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(), + ); +} |
