Make size normalization reference customizable per face

This commit is contained in:
Daniel Wennberg
2025-07-17 10:21:06 -07:00
parent 6491ea41fb
commit e7d28a85c8
6 changed files with 385 additions and 275 deletions

View File

@ -190,7 +190,7 @@ pub fn getIndex(
// Discovery is supposed to only return faces that have our
// codepoint but we can't search presentation in discovery so
// we have to check it here.
const face: Collection.Entry = .{ .fallback_deferred = deferred_face };
const face: Collection.Entry = .init(.{ .fallback_deferred = deferred_face });
if (!face.hasCodepoint(cp, p_mode)) {
deferred_face.deinit();
continue;
@ -266,7 +266,7 @@ fn getIndexCodepointOverride(
const idx = try self.collection.add(
alloc,
.regular,
.{ .deferred = face },
.init(.{ .deferred = face }),
);
try self.descriptor_cache.put(alloc, desc, idx);
@ -388,31 +388,31 @@ test getIndex {
{
errdefer c.deinit(alloc);
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
_ = try c.add(alloc, .regular, .init(.{ .loaded = try .init(
lib,
testFont,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
) });
) }));
if (comptime !font.options.backend.hasCoretext()) {
// Coretext doesn't support Noto's format
_ = try c.add(
alloc,
.regular,
.{ .loaded = try .init(
.init(.{ .loaded = try .init(
lib,
testEmoji,
.{ .size = .{ .points = 12 } },
) },
) }),
);
}
_ = try c.add(
alloc,
.regular,
.{ .loaded = try .init(
.init(.{ .loaded = try .init(
lib,
testEmojiText,
.{ .size = .{ .points = 12 } },
) },
) }),
);
}
@ -467,21 +467,21 @@ test "getIndex disabled font style" {
var c = Collection.init();
c.load_options = .{ .library = lib };
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
_ = try c.add(alloc, .regular, .init(.{ .loaded = try .init(
lib,
testFont,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
) });
_ = try c.add(alloc, .bold, .{ .loaded = try .init(
) }));
_ = try c.add(alloc, .bold, .init(.{ .loaded = try .init(
lib,
testFont,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
) });
_ = try c.add(alloc, .italic, .{ .loaded = try .init(
) }));
_ = try c.add(alloc, .italic, .init(.{ .loaded = try .init(
lib,
testFont,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
) });
) }));
var r: CodepointResolver = .{ .collection = c };
defer r.deinit(alloc);

View File

@ -62,7 +62,9 @@ pub fn deinit(self: *Collection, alloc: Allocator) void {
var it = self.faces.iterator();
while (it.next()) |array| {
var entry_it = array.value.iterator(0);
while (entry_it.next()) |entry| entry.deinit();
while (entry_it.next()) |entry_or_alias| {
if (entry_or_alias.unwrapNoAlias()) |entry| entry.deinit();
}
array.value.deinit(alloc);
}
@ -105,123 +107,35 @@ pub fn add(
if (face.isDeferred() and self.load_options == null)
return error.DeferredLoadingUnavailable;
try list.append(alloc, face);
try list.append(alloc, .{ .entry = face });
var owned: *Entry = list.at(idx);
const owned: *Entry = list.at(idx).unwrapNoAlias().?;
// If the face is already loaded, apply font size adjustment
// now, otherwise we'll apply it whenever we do load it.
if (owned.getLoaded()) |loaded| {
if (try self.adjustedSize(loaded)) |opts| {
loaded.setSize(opts.faceOptions()) catch return error.SetSizeFailed;
}
// If we have load options, we update the size such that it's scaled
// to the primary if possible. If the face is not loaded, this is a
// no-op and scaling will be done when loading happens.
if (self.load_options) |opts| {
const primary_entry = self.getEntry(.{ .idx = 0 }) catch null;
try owned.setSize(opts.faceOptions(), primary_entry);
}
return .{ .style = style, .idx = @intCast(idx) };
}
pub const AdjustSizeError = font.Face.GetMetricsError;
// Calculate a size for the provided face that will match it with the primary
// font, metrically, to improve consistency with fallback fonts. Right now we
// match the font based on the ex height, or the ideograph width if the font
// has ideographs in it.
//
// This returns null if load options is null or if self.load_options is null.
//
// This is very much like the `font-size-adjust` CSS property in how it works.
// ref: https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust
//
// TODO: In the future, provide config options that allow the user to select
// which metric should be matched for fallback fonts, instead of hard
// coding it as ex height.
pub fn adjustedSize(
self: *Collection,
face: *Face,
) AdjustSizeError!?LoadOptions {
const load_options = self.load_options orelse return null;
// We silently do nothing if we can't get the primary
// face, because this might be the primary face itself.
const primary_face = self.getFace(.{ .idx = 0 }) catch return null;
// We do nothing if the primary face and this face are the same.
if (@intFromPtr(primary_face) == @intFromPtr(face)) return null;
const primary_metrics = try primary_face.getMetrics();
const face_metrics = try face.getMetrics();
// We use the ex height to match our font sizes, so that the height of
// lower-case letters matches between all fonts in the fallback chain.
//
// If the fallback font has an ic_width we prefer that, for normalization
// of CJK font sizes when mixed with latin fonts.
const primary_ex = primary_metrics.exHeight();
const primary_ic = primary_metrics.icWidth();
const face_ex = face_metrics.exHeight();
const face_ic = face_metrics.icWidth();
// If the line height of the scaled font would be larger than
// the line height of the primary font, we don't want that, so
// we take the minimum between matching the ic/ex and the line
// height.
//
// NOTE: We actually allow the line height to be up to 1.2
// times the primary line height because empirically
// this is usually fine and is better for CJK.
//
// TODO: We should probably provide a config option that lets
// the user pick what metric to use for size adjustment.
const scale = @min(
1.2 * primary_metrics.lineHeight() / face_metrics.lineHeight(),
if ((face_metrics.ic_width != null) and (face_metrics.ic_width == face_ic))
// It's possible for .ic_width to be non-null and still invalid, e.g.,
// zero, so we only take this branch if it's also equal to .icWidth().
primary_ic / face_ic
else
primary_ex / face_ex,
);
// Make a copy of our load options, set the size to the size of
// the provided face, and then multiply that by our scaling factor.
var opts = load_options;
opts.size = face.size;
opts.size.points *= @as(f32, @floatCast(scale));
return opts;
}
/// Return the Face represented by a given Index. The returned pointer
/// is only valid as long as this collection is not modified.
///
/// This will initialize the face if it is deferred and not yet loaded,
/// which can fail.
pub fn getFace(self: *Collection, index: Index) !*Face {
return try self.getFaceFromEntry(try self.getEntry(index));
}
/// Get the unaliased entry from an index
pub fn getEntry(self: *Collection, index: Index) !*Entry {
if (index.special() != null) return error.SpecialHasNoFace;
const list = self.faces.getPtr(index.style);
const item: *Entry = item: {
var item = list.at(index.idx);
switch (item.*) {
.alias => |ptr| item = ptr,
.deferred,
.fallback_deferred,
.loaded,
.fallback_loaded,
=> {},
}
assert(item.* != .alias);
break :item item;
};
const face = try self.getFaceFromEntry(
item,
// We only want to adjust the size if this isn't the primary face.
index.style != .regular or index.idx > 0,
);
return face;
return list.at(index.idx).unwrap();
}
/// Get the face from an entry.
@ -230,41 +144,33 @@ pub fn getFace(self: *Collection, index: Index) !*Face {
fn getFaceFromEntry(
self: *Collection,
entry: *Entry,
/// Whether to adjust the font size to match the primary face after loading.
adjust: bool,
) !*Face {
assert(entry.* != .alias);
return switch (entry.*) {
return switch (entry.face) {
inline .deferred, .fallback_deferred => |*d, tag| deferred: {
const opts = self.load_options orelse
return error.DeferredLoadingUnavailable;
var face = try d.load(opts.library, opts.faceOptions());
const face_opts = opts.faceOptions();
const face = try d.load(opts.library, face_opts);
d.deinit();
// If we need to adjust the size, do so.
if (adjust) if (try self.adjustedSize(&face)) |new_opts| {
try face.setSize(new_opts.faceOptions());
};
entry.* = switch (tag) {
entry.face = switch (tag) {
.deferred => .{ .loaded = face },
.fallback_deferred => .{ .fallback_loaded = face },
else => unreachable,
};
// Adjust the size, passing the primary font for scaling
const primary_entry = self.getEntry(.{ .idx = 0 }) catch null;
try entry.setSize(face_opts, primary_entry);
break :deferred switch (tag) {
.deferred => &entry.loaded,
.fallback_deferred => &entry.fallback_loaded,
.deferred => &entry.face.loaded,
.fallback_deferred => &entry.face.fallback_loaded,
else => unreachable,
};
},
.loaded, .fallback_loaded => |*f| f,
// When setting `entry` above, we ensure we don't end up with
// an alias.
.alias => unreachable,
};
}
@ -282,8 +188,8 @@ pub fn getIndex(
) ?Index {
var i: usize = 0;
var it = self.faces.get(style).constIterator(0);
while (it.next()) |entry| {
if (entry.hasCodepoint(cp, p_mode)) {
while (it.next()) |entry_or_alias| {
if (@constCast(entry_or_alias).unwrap().hasCodepoint(cp, p_mode)) {
return .{
.style = style,
.idx = @intCast(i),
@ -309,7 +215,7 @@ pub fn hasCodepoint(
) bool {
const list = self.faces.get(index.style);
if (index.idx >= list.count()) return false;
return list.at(index.idx).hasCodepoint(cp, p_mode);
return @constCast(list.at(index.idx)).unwrap().hasCodepoint(cp, p_mode);
}
pub const CompleteError = Allocator.Error || error{
@ -347,10 +253,11 @@ pub fn completeStyles(
// Find our first regular face that has text glyphs.
var it = list.iterator(0);
while (it.next()) |entry| {
while (it.next()) |entry_or_alias| {
// Load our face. If we fail to load it, we just skip it and
// continue on to try the next one.
const face = self.getFaceFromEntry(entry, false) catch |err| {
const entry = entry_or_alias.unwrap();
const face = self.getFaceFromEntry(entry) catch |err| {
log.warn("error loading regular entry={d} err={}", .{
it.index - 1,
err,
@ -393,8 +300,9 @@ pub fn completeStyles(
break :italic;
};
const synthetic_entry = regular_entry.initCopy(.{ .loaded = synthetic });
log.info("synthetic italic face created", .{});
try italic_list.append(alloc, .{ .loaded = synthetic });
try italic_list.append(alloc, .{ .entry = synthetic_entry });
}
// If we don't have bold, use the regular font.
@ -413,8 +321,9 @@ pub fn completeStyles(
break :bold;
};
const synthetic_entry = regular_entry.initCopy(.{ .loaded = synthetic });
log.info("synthetic bold face created", .{});
try bold_list.append(alloc, .{ .loaded = synthetic });
try bold_list.append(alloc, .{ .entry = synthetic_entry });
}
// If we don't have bold italic, we attempt to synthesize a bold variant
@ -430,9 +339,11 @@ pub fn completeStyles(
// Prefer to synthesize on top of the face we already had. If we
// have bold then we try to synthesize italic on top of bold.
if (have_bold) {
if (self.syntheticItalic(bold_list.at(0))) |synthetic| {
const base_entry: *Entry = bold_list.at(0).unwrap();
if (self.syntheticItalic(base_entry)) |synthetic| {
log.info("synthetic bold italic face created from bold", .{});
try bold_italic_list.append(alloc, .{ .loaded = synthetic });
const synthetic_entry = base_entry.initCopy(.{ .loaded = synthetic });
try bold_italic_list.append(alloc, .{ .entry = synthetic_entry });
break :bold_italic;
} else |_| {}
@ -440,23 +351,11 @@ pub fn completeStyles(
// bold on whatever italic font we have.
}
// Nested alias isn't allowed so we need to unwrap the italic entry.
const base_entry = base: {
const italic_entry = italic_list.at(0);
break :base switch (italic_entry.*) {
.alias => |v| v,
.loaded,
.fallback_loaded,
.deferred,
.fallback_deferred,
=> italic_entry,
};
};
const base_entry: *Entry = italic_list.at(0).unwrap();
if (self.syntheticBold(base_entry)) |synthetic| {
log.info("synthetic bold italic face created from italic", .{});
try bold_italic_list.append(alloc, .{ .loaded = synthetic });
const synthetic_entry = base_entry.initCopy(.{ .loaded = synthetic });
try bold_italic_list.append(alloc, .{ .entry = synthetic_entry });
break :bold_italic;
} else |_| {}
@ -474,7 +373,12 @@ fn syntheticBold(self: *Collection, entry: *Entry) !Face {
const opts = self.load_options orelse return error.DeferredLoadingUnavailable;
// Try to bold it.
const regular = try self.getFaceFromEntry(entry, false);
const regular = try self.getFaceFromEntry(entry);
// Inherit size from regular; it may be different than opts.size
// due to scaling adjustments
var face_opts = opts.faceOptions();
face_opts.size = regular.size;
const face = try regular.syntheticBold(opts.faceOptions());
var buf: [256]u8 = undefined;
@ -494,7 +398,12 @@ fn syntheticItalic(self: *Collection, entry: *Entry) !Face {
const opts = self.load_options orelse return error.DeferredLoadingUnavailable;
// Try to italicize it.
const regular = try self.getFaceFromEntry(entry, false);
const regular = try self.getFaceFromEntry(entry);
// Inherit size from regular; it may be different than opts.size
// due to scaling adjustments
var face_opts = opts.faceOptions();
face_opts.size = regular.size;
const face = try regular.syntheticItalic(opts.faceOptions());
var buf: [256]u8 = undefined;
@ -517,31 +426,42 @@ pub fn setSize(self: *Collection, size: DesiredSize) !void {
else
return error.DeferredLoadingUnavailable;
opts.size = size;
const face_opts = opts.faceOptions();
// Get the primary face if we can, and update that first,
// such that the relative scaling of the remaining faces
// is correct
const primary_entry = primary_entry: {
if (self.getEntry(.{ .idx = 0 })) |pe| {
try pe.setSize(face_opts, null);
break :primary_entry pe;
} else |_| break :primary_entry null;
};
// Resize all our faces that are loaded
var it = self.faces.iterator();
while (it.next()) |array| {
var entry_it = array.value.iterator(0);
while (entry_it.next()) |entry| switch (entry.*) {
.loaded,
.fallback_loaded,
=> |*f| {
const new_opts = try self.adjustedSize(f) orelse opts.*;
try f.setSize(new_opts.faceOptions());
},
// Deferred aren't loaded so we don't need to set their size.
// The size for when they're loaded is set since `opts` changed.
.deferred, .fallback_deferred => continue,
// Alias faces don't own their size.
.alias => continue,
};
while (entry_it.next()) |entry_or_alias| {
if (entry_or_alias.unwrapNoAlias()) |entry| {
if (entry == primary_entry) continue;
try entry.setSize(face_opts, primary_entry);
}
}
}
try self.updateMetrics();
}
pub fn setReferenceMetric(self: *Collection, index: Index, scale_reference: ReferenceMetric) !void {
var entry = try self.getEntry(index);
entry.scale_reference = scale_reference;
if (self.load_options) |opts| {
const primary_entry = self.getEntry(.{ .idx = 0 }) catch null;
try entry.setSize(opts.faceOptions(), primary_entry);
}
}
const UpdateMetricsError = font.Face.GetMetricsError || error{
CannotLoadPrimaryFont,
};
@ -574,7 +494,7 @@ pub fn updateMetrics(self: *Collection) UpdateMetricsError!void {
///
/// WARNING: We cannot use any prealloc yet for the segmented list because
/// the collection is copied around by value and pointers aren't stable.
const StyleArray = std.EnumArray(Style, std.SegmentedList(Entry, 0));
const StyleArray = std.EnumArray(Style, std.SegmentedList(EntryOrAlias, 0));
/// Load options are used to configure all the details a Collection
/// needs to load deferred faces.
@ -628,49 +548,70 @@ pub const LoadOptions = struct {
/// not "fallback"), they want to use any glyphs possible within that
/// font face. Fallback fonts on the other hand are picked as a
/// last resort, so we should prefer exactness if possible.
pub const Entry = union(enum) {
deferred: DeferredFace, // Not loaded
loaded: Face, // Loaded, explicit use
pub const Entry = struct {
const AnyFace = union(enum) {
deferred: DeferredFace, // Not loaded
loaded: Face, // Loaded, explicit use
// The same as deferred/loaded but fallback font semantics (see large
// comment above Entry).
fallback_deferred: DeferredFace,
fallback_loaded: Face,
// The same as deferred/loaded but fallback font semantics (see large
// comment above Entry).
fallback_deferred: DeferredFace,
fallback_loaded: Face,
};
// An alias to another entry. This is used to share the same face,
// avoid memory duplication. An alias must point to a non-alias entry.
alias: *Entry,
face: AnyFace,
// Metric by which to normalize the font's size to the primary font.
// Default to ic_width to ensure appropriate normalization of CJK
// font sizes when mixed with latin fonts. See scaleFactor()
// implementation for fallback rules when the font does not define
// the specified metric.
//
// NOTE: In the future, if additional modifiers are needed for the
// translation of global collection options to individual font
// options, this should be promoted to a new FontModifiers type.
scale_reference: ReferenceMetric = .ic_width,
/// Convenience initializer so that users won't have to write nested
/// expressions depending on internals, like .{ .face = .{ .loaded = face } }
pub fn init(face: AnyFace) Entry {
return .{ .face = face };
}
/// Convenience initializer that also takes a scale reference
pub fn initWithScaleReference(face: AnyFace, scale_reference: ReferenceMetric) Entry {
return .{ .face = face, .scale_reference = scale_reference };
}
/// Initialize a new entry with the same scale reference as an existing entry
pub fn initCopy(self: Entry, face: AnyFace) Entry {
return .{ .face = face, .scale_reference = self.scale_reference };
}
pub fn deinit(self: *Entry) void {
switch (self.*) {
switch (self.face) {
inline .deferred,
.loaded,
.fallback_deferred,
.fallback_loaded,
=> |*v| v.deinit(),
// Aliased fonts are not owned by this entry so we let them
// be deallocated by the owner.
.alias => {},
}
}
/// If this face is loaded, or is an alias to a loaded face,
/// then this returns the `Face`, otherwise returns null.
/// If this face is loaded, then this returns the `Face`,
/// otherwise returns null.
pub fn getLoaded(self: *Entry) ?*Face {
return switch (self.*) {
return switch (self.face) {
.deferred, .fallback_deferred => null,
.loaded, .fallback_loaded => |*face| face,
.alias => |v| v.getLoaded(),
};
}
/// True if the entry is deferred.
fn isDeferred(self: Entry) bool {
return switch (self) {
return switch (self.face) {
.deferred, .fallback_deferred => true,
.loaded, .fallback_loaded => false,
.alias => |v| v.isDeferred(),
};
}
@ -680,9 +621,7 @@ pub const Entry = union(enum) {
cp: u32,
p_mode: PresentationMode,
) bool {
return switch (self) {
.alias => |v| v.hasCodepoint(cp, p_mode),
return switch (self.face) {
// Non-fallback fonts require explicit presentation matching but
// otherwise don't care about presentation
.deferred => |v| switch (p_mode) {
@ -721,6 +660,142 @@ pub const Entry = union(enum) {
},
};
}
// Set the size of the face, rescaling to match the primary if given
pub fn setSize(self: *Entry, opts: font.face.Options, primary_entry: ?*Entry) !void {
// If not loaded, nothing to do
var face = self.getLoaded() orelse return;
// First set to the raw size from opts, even if we're scaling,
// otherwise scaling calculations will be incorrect.
face.setSize(opts) catch return error.SetSizeFailed;
// If we have a primary we rescale
if (primary_entry) |pe| if (try self.scaleFactor(pe)) |scale| {
var opts_scaled = opts;
opts_scaled.size.points *= scale;
face.setSize(opts_scaled) catch return error.SetSizeFailed;
};
}
// Calculate a size for the face that will match it with the primary font,
// metrically, to improve consistency with fallback fonts.
//
// This returns null if this or the primary face aren't loaded or, if
// scaling doesn't apply to this face.
//
// This is very much like the `font-size-adjust` CSS property in how it works.
// ref: https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust
//
// TODO: In the future, provide config options that allow the user to select
// which metric should be matched for fallback fonts, instead of hard
// coding at the point where a face is added to the collection.
pub fn scaleFactor(self: *Entry, primary_entry: *Entry) !?f32 {
// If the reference metric is the em size, no scaling
if (self.scale_reference == .em_size) return null;
// If the primary is us, no scaling
if (@intFromPtr(self) == @intFromPtr(primary_entry)) return null;
// If we or the primary face aren't loaded, we don't know our
// metrics, so we can't do anything
const primary_face = primary_entry.getLoaded() orelse return null;
const face = self.getLoaded() orelse return null;
// Verify that the two faces are currently loaded at the same
// size, otherwise what follows is nonsense
if (!std.meta.eql(face.size, primary_face.size)) {
return error.InconsistentSizesForScaling;
}
const primary_metrics = try primary_face.getMetrics();
const face_metrics = try face.getMetrics();
// The preferred metric to normalize by is self.scale_reference,
// however we don't want to use a metric not explicitly defined
// in `self`, so if needed we fall back through other metrics in
// the order shown in the switch statement below. If the metric
// is not defined in `primary`, that's OK, we'll use the estimate.
const line_height_ratio = primary_metrics.lineHeight() / face_metrics.lineHeight();
const scale: f64 = normalize_by: switch (self.scale_reference) {
// Even if a metric is non-null, it may be invalid (e.g., negative),
// so we check for equality with the estimator before using it
.ic_width => {
if (face_metrics.ic_width) |value| if (value == face_metrics.icWidth()) {
break :normalize_by primary_metrics.icWidth() / value;
};
continue :normalize_by .ex_height;
},
.ex_height => {
if (face_metrics.ex_height) |value| if (value == face_metrics.exHeight()) {
break :normalize_by primary_metrics.exHeight() / value;
};
continue :normalize_by .cap_height;
},
.cap_height => {
if (face_metrics.cap_height) |value| if (value == face_metrics.capHeight()) {
break :normalize_by primary_metrics.capHeight() / value;
};
continue :normalize_by .line_height;
},
.line_height => line_height_ratio,
.em_size => unreachable,
};
// If the line height of the scaled font would be larger than
// the line height of the primary font, we don't want that, so
// we take the minimum between matching the reference metric
// and keeping the line heights within some margin.
//
// NOTE: We actually allow the line height to be up to 1.2
// times the primary line height because empirically
// this is usually fine and is better for CJK.
const capped_scale = @min(scale, 1.2 * line_height_ratio);
return @floatCast(capped_scale);
}
};
pub const EntryOrAlias = union(enum) {
entry: Entry,
// An alias to another entry. This is used to share the same face,
// avoid memory duplication. An alias must point to a non-alias entry.
alias: *Entry,
pub fn unwrap(self: *EntryOrAlias) *Entry {
return switch (self.*) {
.entry => |*v| v,
.alias => |v| v,
};
}
pub fn unwrapNoAlias(self: *EntryOrAlias) ?*Entry {
return switch (self.*) {
.entry => |*v| v,
.alias => null,
};
}
};
pub const AdjustSizeError = font.Face.GetMetricsError || error{InconsistentSizesForScaling};
pub const ReferenceMetric = enum {
// The font's ideograph width
ic_width,
// The font's ex height
ex_height,
// The font's cap height
cap_height,
// The font's line height
line_height,
// The font's em size
// Conventionally equivalent to line height, but the semantics
// differ: using em_size directly sets the point sizes to the same
// value, while using line_height calculates the scaling ratio for
// matching line heights even if it differs from 1 em for one or
// both fonts
em_size,
};
/// The requested presentation for a codepoint.
@ -823,11 +898,11 @@ test "add full" {
defer c.deinit(alloc);
for (0..Index.Special.start - 1) |_| {
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
_ = try c.add(alloc, .regular, .init(.{ .loaded = try .init(
lib,
testFont,
.{ .size = .{ .points = 12 } },
) });
) }));
}
var face = try Face.init(
@ -840,7 +915,7 @@ test "add full" {
defer face.deinit();
try testing.expectError(
error.CollectionFull,
c.add(alloc, .regular, .{ .loaded = face }),
c.add(alloc, .regular, .init(.{ .loaded = face })),
);
}
@ -856,7 +931,7 @@ test "add deferred without loading options" {
.regular,
// This can be undefined because it should never be accessed.
.{ .deferred = undefined },
.init(.{ .deferred = undefined }),
));
}
@ -871,11 +946,11 @@ test getFace {
var c = init();
defer c.deinit(alloc);
const idx = try c.add(alloc, .regular, .{ .loaded = try .init(
const idx = try c.add(alloc, .regular, .init(.{ .loaded = try .init(
lib,
testFont,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
) });
) }));
{
const face1 = try c.getFace(idx);
@ -895,11 +970,11 @@ test getIndex {
var c = init();
defer c.deinit(alloc);
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
_ = try c.add(alloc, .regular, .init(.{ .loaded = try .init(
lib,
testFont,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
) });
) }));
// Should find all visible ASCII
var i: u32 = 32;
@ -927,11 +1002,11 @@ test completeStyles {
defer c.deinit(alloc);
c.load_options = .{ .library = lib };
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
_ = try c.add(alloc, .regular, .init(.{ .loaded = try .init(
lib,
testFont,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
) });
) }));
try testing.expect(c.getIndex('A', .bold, .{ .any = {} }) == null);
try testing.expect(c.getIndex('A', .italic, .{ .any = {} }) == null);
@ -954,11 +1029,11 @@ test setSize {
defer c.deinit(alloc);
c.load_options = .{ .library = lib };
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
_ = try c.add(alloc, .regular, .init(.{ .loaded = try .init(
lib,
testFont,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
) });
) }));
try testing.expectEqual(@as(u32, 12), c.load_options.?.size.points);
try c.setSize(.{ .points = 24 });
@ -977,11 +1052,11 @@ test hasCodepoint {
defer c.deinit(alloc);
c.load_options = .{ .library = lib };
const idx = try c.add(alloc, .regular, .{ .loaded = try .init(
const idx = try c.add(alloc, .regular, .init(.{ .loaded = try .init(
lib,
testFont,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
) });
) }));
try testing.expect(c.hasCodepoint(idx, 'A', .{ .any = {} }));
try testing.expect(!c.hasCodepoint(idx, '🥸', .{ .any = {} }));
@ -1001,11 +1076,11 @@ test "hasCodepoint emoji default graphical" {
defer c.deinit(alloc);
c.load_options = .{ .library = lib };
const idx = try c.add(alloc, .regular, .{ .loaded = try .init(
const idx = try c.add(alloc, .regular, .init(.{ .loaded = try .init(
lib,
testEmoji,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
) });
) }));
try testing.expect(!c.hasCodepoint(idx, 'A', .{ .any = {} }));
try testing.expect(c.hasCodepoint(idx, '🥸', .{ .any = {} }));
@ -1025,11 +1100,11 @@ test "metrics" {
const size: DesiredSize = .{ .points = 12, .xdpi = 96, .ydpi = 96 };
c.load_options = .{ .library = lib, .size = size };
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
_ = try c.add(alloc, .regular, .init(.{ .loaded = try .init(
lib,
testFont,
.{ .size = size },
) });
) }));
try c.updateMetrics();
@ -1084,6 +1159,7 @@ test "adjusted sizes" {
const alloc = testing.allocator;
const testFont = font.embedded.inconsolata;
const fallback = font.embedded.monaspace_neon;
const symbol = font.embedded.symbols_nerd_font;
var lib = try Library.init(alloc);
defer lib.deinit();
@ -1094,45 +1170,79 @@ test "adjusted sizes" {
c.load_options = .{ .library = lib, .size = size };
// Add our primary face.
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
_ = try c.add(alloc, .regular, .init(.{ .loaded = try .init(
lib,
testFont,
.{ .size = size },
) });
) }));
try c.updateMetrics();
// Add the fallback face.
const fallback_idx = try c.add(alloc, .regular, .{ .loaded = try .init(
const fallback_idx = try c.add(alloc, .regular, .init(.{ .loaded = try .init(
lib,
fallback,
.{ .size = size },
) });
) }));
// The ex heights should match.
inline for ([_][]const u8{ "ex_height", "cap_height" }) |metric| {
try c.setReferenceMetric(fallback_idx, @field(ReferenceMetric, metric));
// The chosen metric should match.
{
const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics();
const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics();
try std.testing.expectApproxEqRel(
@field(primary_metrics, metric).?,
@field(fallback_metrics, metric).?,
// We accept anything within 5 %.
0.05,
);
}
// Resize should keep that relationship.
try c.setSize(.{ .points = 37, .xdpi = 96, .ydpi = 96 });
{
const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics();
const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics();
try std.testing.expectApproxEqRel(
@field(primary_metrics, metric).?,
@field(fallback_metrics, metric).?,
// We accept anything within 5 %.
0.05,
);
}
// Reset size for the next iteration
try c.setSize(size);
}
// Add the symbol face.
const symbol_idx = try c.add(alloc, .regular, .initWithScaleReference(.{ .loaded = try .init(
lib,
symbol,
.{ .size = size },
) }, .ex_height));
// Test fallback to lineHeight() (ex_height and cap_height not defined in symbols font).
{
const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics();
const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics();
const symbol_metrics = try (try c.getFace(symbol_idx)).getMetrics();
try std.testing.expectApproxEqAbs(
primary_metrics.ex_height.?,
fallback_metrics.ex_height.?,
// We accept anything within half a pixel.
0.5,
try std.testing.expectApproxEqRel(
primary_metrics.lineHeight(),
symbol_metrics.lineHeight(),
// We accept anything within 5 %.
0.05,
);
}
// Resize should keep that relationship.
try c.setSize(.{ .points = 37, .xdpi = 96, .ydpi = 96 });
{
const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics();
const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics();
// Test em_size giving exact font size equality
try c.setReferenceMetric(symbol_idx, .em_size);
try std.testing.expectApproxEqAbs(
primary_metrics.ex_height.?,
fallback_metrics.ex_height.?,
// We accept anything within half a pixel.
0.5,
);
}
try std.testing.expectEqual(
(try c.getFace(.{ .idx = 0 })).size.points,
(try c.getFace(symbol_idx)).size.points,
);
}

View File

@ -376,11 +376,11 @@ fn testGrid(mode: TestMode, alloc: Allocator, lib: Library) !SharedGrid {
switch (mode) {
.normal => {
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
_ = try c.add(alloc, .regular, .init(.{ .loaded = try .init(
lib,
testFont,
.{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } },
) });
) }));
},
}

View File

@ -203,7 +203,7 @@ fn collection(
_ = try c.add(
self.alloc,
style,
.{ .deferred = face },
.init(.{ .deferred = face }),
);
continue;
@ -233,7 +233,7 @@ fn collection(
_ = try c.add(
self.alloc,
style,
.{ .deferred = face },
.init(.{ .deferred = face }),
);
continue;
@ -258,20 +258,20 @@ fn collection(
_ = try c.add(
self.alloc,
.regular,
.{ .fallback_loaded = try .init(
.init(.{ .fallback_loaded = try .init(
self.font_lib,
font.embedded.variable,
load_options.faceOptions(),
) },
) }),
);
try (try c.getFace(try c.add(
self.alloc,
.bold,
.{ .fallback_loaded = try .init(
.init(.{ .fallback_loaded = try .init(
self.font_lib,
font.embedded.variable,
load_options.faceOptions(),
) },
) }),
))).setVariations(
&.{.{ .id = .init("wght"), .value = 700 }},
load_options.faceOptions(),
@ -279,20 +279,20 @@ fn collection(
_ = try c.add(
self.alloc,
.italic,
.{ .fallback_loaded = try .init(
.init(.{ .fallback_loaded = try .init(
self.font_lib,
font.embedded.variable_italic,
load_options.faceOptions(),
) },
) }),
);
try (try c.getFace(try c.add(
self.alloc,
.bold_italic,
.{ .fallback_loaded = try .init(
.init(.{ .fallback_loaded = try .init(
self.font_lib,
font.embedded.variable_italic,
load_options.faceOptions(),
) },
) }),
))).setVariations(
&.{.{ .id = .init("wght"), .value = 700 }},
load_options.faceOptions(),
@ -302,11 +302,11 @@ fn collection(
_ = try c.add(
self.alloc,
.regular,
.{ .fallback_loaded = try Face.init(
.init(.{ .fallback_loaded = try Face.init(
self.font_lib,
font.embedded.symbols_nerd_font,
load_options.faceOptions(),
) },
) }),
);
// On macOS, always search for and add the Apple Emoji font
@ -324,7 +324,7 @@ fn collection(
_ = try c.add(
self.alloc,
.regular,
.{ .fallback_deferred = face },
.init(.{ .fallback_deferred = face }),
);
}
}
@ -335,20 +335,20 @@ fn collection(
_ = try c.add(
self.alloc,
.regular,
.{ .fallback_loaded = try .init(
.init(.{ .fallback_loaded = try .init(
self.font_lib,
font.embedded.emoji,
load_options.faceOptions(),
) },
) }),
);
_ = try c.add(
self.alloc,
.regular,
.{ .fallback_loaded = try .init(
.init(.{ .fallback_loaded = try .init(
self.font_lib,
font.embedded.emoji_text,
load_options.faceOptions(),
) },
) }),
);
}

View File

@ -1779,19 +1779,19 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
c.load_options = .{ .library = lib };
// Setup group
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
_ = try c.add(alloc, .regular, .init(.{ .loaded = try .init(
lib,
testFont,
.{ .size = .{ .points = 12 } },
) });
) }));
if (font.options.backend != .coretext) {
// Coretext doesn't support Noto's format
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
_ = try c.add(alloc, .regular, .init(.{ .loaded = try .init(
lib,
testEmoji,
.{ .size = .{ .points = 12 } },
) });
) }));
} else {
// On CoreText we want to load Apple Emoji, we should have it.
var disco = font.Discover.init();
@ -1804,13 +1804,13 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
defer disco_it.deinit();
var face = (try disco_it.next()).?;
errdefer face.deinit();
_ = try c.add(alloc, .regular, .{ .deferred = face });
_ = try c.add(alloc, .regular, .init(.{ .deferred = face }));
}
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
_ = try c.add(alloc, .regular, .init(.{ .loaded = try .init(
lib,
testEmojiText,
.{ .size = .{ .points = 12 } },
) });
) }));
const grid_ptr = try alloc.create(SharedGrid);
errdefer alloc.destroy(grid_ptr);

View File

@ -1247,19 +1247,19 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
c.load_options = .{ .library = lib };
// Setup group
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
_ = try c.add(alloc, .regular, .init(.{ .loaded = try .init(
lib,
testFont,
.{ .size = .{ .points = 12 } },
) });
) }));
if (comptime !font.options.backend.hasCoretext()) {
// Coretext doesn't support Noto's format
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
_ = try c.add(alloc, .regular, .init(.{ .loaded = try .init(
lib,
testEmoji,
.{ .size = .{ .points = 12 } },
) });
) }));
} else {
// On CoreText we want to load Apple Emoji, we should have it.
var disco = font.Discover.init();
@ -1272,13 +1272,13 @@ fn testShaperWithFont(alloc: Allocator, font_req: TestFont) !TestShaper {
defer disco_it.deinit();
var face = (try disco_it.next()).?;
errdefer face.deinit();
_ = try c.add(alloc, .regular, .{ .deferred = face });
_ = try c.add(alloc, .regular, .init(.{ .deferred = face }));
}
_ = try c.add(alloc, .regular, .{ .loaded = try .init(
_ = try c.add(alloc, .regular, .init(.{ .loaded = try .init(
lib,
testEmojiText,
.{ .size = .{ .points = 12 } },
) });
) }));
const grid_ptr = try alloc.create(SharedGrid);
errdefer alloc.destroy(grid_ptr);