Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524//! 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 indexpub 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-adjustfn 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(), );}