From b10b0f06c329b865b72ee40c3122f009c457f2d4 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 6 Jul 2025 15:53:59 -0600 Subject: [PATCH 1/5] font: remove unused fields from Glyph We can reintroduce `advance` if we ever want to do proportional string drawing, but we don't use it anywhere right now. And we also don't need `sprite` anymore since that was just there to disable constraints for sprites back when we did them on the GPU. --- src/font/Glyph.zig | 6 ------ src/font/face/coretext.zig | 6 ------ src/font/face/freetype.zig | 2 -- src/font/face/web_canvas.zig | 1 - src/font/sprite/Face.zig | 3 --- 5 files changed, 18 deletions(-) diff --git a/src/font/Glyph.zig b/src/font/Glyph.zig index fa29e44fa..f99370271 100644 --- a/src/font/Glyph.zig +++ b/src/font/Glyph.zig @@ -17,9 +17,3 @@ offset_y: i32, /// be normalized to be between 0 and 1 prior to use in shaders. atlas_x: u32, atlas_y: u32, - -/// horizontal position to increase drawing position for strings -advance_x: f32, - -/// Whether we drew this glyph ourselves with the sprite font. -sprite: bool = false, diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 5c9c259d2..7d750b0d6 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -333,7 +333,6 @@ pub const Face = struct { .offset_y = 0, .atlas_x = 0, .atlas_y = 0, - .advance_x = 0, }; const metrics = opts.grid_metrics; @@ -498,10 +497,6 @@ pub const Face = struct { break :offset_x result; }; - // Get our advance - var advances: [glyphs.len]macos.graphics.Size = undefined; - _ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances); - return .{ .width = px_width, .height = px_height, @@ -509,7 +504,6 @@ pub const Face = struct { .offset_y = offset_y, .atlas_x = region.x, .atlas_y = region.y, - .advance_x = @floatCast(advances[0].width), }; } diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index b27b28ab8..079cf5b2d 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -373,7 +373,6 @@ pub const Face = struct { .offset_y = 0, .atlas_x = 0, .atlas_y = 0, - .advance_x = 0, }; // For synthetic bold, we embolden the glyph. @@ -662,7 +661,6 @@ pub const Face = struct { .offset_y = offset_y, .atlas_x = region.x, .atlas_y = region.y, - .advance_x = f26dot6ToFloat(glyph.*.advance.x), }; } diff --git a/src/font/face/web_canvas.zig b/src/font/face/web_canvas.zig index 30540191d..7ea2f0426 100644 --- a/src/font/face/web_canvas.zig +++ b/src/font/face/web_canvas.zig @@ -235,7 +235,6 @@ pub const Face = struct { .offset_y = 0, .atlas_x = region.x, .atlas_y = region.y, - .advance_x = 0, }; } diff --git a/src/font/sprite/Face.zig b/src/font/sprite/Face.zig index 1463fb38b..dfff8fa75 100644 --- a/src/font/sprite/Face.zig +++ b/src/font/sprite/Face.zig @@ -195,7 +195,6 @@ pub fn renderGlyph( .offset_y = 0, .atlas_x = 0, .atlas_y = 0, - .advance_x = 0, }; const metrics = self.metrics; @@ -227,8 +226,6 @@ pub fn renderGlyph( .offset_y = @as(i32, @intCast(region.height +| canvas.clip_bottom)) - @as(i32, @intCast(padding_y)), .atlas_x = region.x, .atlas_y = region.y, - .advance_x = @floatFromInt(width), - .sprite = true, }; } From d3aece21d8eb5985681f0d57ed7f280c9a7065b5 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 6 Jul 2025 16:32:22 -0600 Subject: [PATCH 2/5] font: more generic bearing adjustments This generally adjusts the bearings of any glyph whose original advance was narrower than the cell, which helps a lot with proportional fallback glyphs so they aren't just left-aligned. This only applies to situations where the glyph was originally narrower than the cell, so that we don't mess up ligatures, and this centers the old advance width in the new one rather than adjusting proportionally, because otherwise we can mess up glyphs that are meant to align with others when placed vertically. --- src/font/face/coretext.zig | 48 +++++++++++++++++++++++++++----------- src/font/face/freetype.zig | 46 +++++++++++++++++++++++++----------- 2 files changed, 68 insertions(+), 26 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 7d750b0d6..6aedd7696 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -481,20 +481,42 @@ pub const Face = struct { // This should be the distance from the left of // the cell to the left of the glyph's bounding box. const offset_x: i32 = offset_x: { - var result: i32 = @intFromFloat(@round(x)); - - // If our cell was resized then we adjust our glyph's - // position relative to the new center. This keeps glyphs - // centered in the cell whether it was made wider or narrower. - if (metrics.original_cell_width) |original_width| { - const before: i32 = @intCast(original_width); - const after: i32 = @intCast(metrics.cell_width); - // Increase the offset by half of the difference - // between the widths to keep things centered. - result += @divTrunc(after - before, 2); + // If the glyph's advance is narrower than the cell width then we + // center the advance of the glyph within the cell width. At first + // I implemented this to proportionally scale the center position + // of the glyph but that messes up glyphs that are meant to align + // vertically with others, so this is a compromise. + // + // This makes it so that when the `adjust-cell-width` config is + // used, or when a fallback font with a different advance width + // is used, we don't get weirdly aligned glyphs. + // + // We don't do this if the constraint has a horizontal alignment, + // since in that case the position was already calculated with the + // new cell width in mind. + if (opts.constraint.align_horizontal == .none) { + var advances: [glyphs.len]macos.graphics.Size = undefined; + _ = self.font.getAdvancesForGlyphs(.horizontal, &glyphs, &advances); + const advance = advances[0].width; + const new_advance = + cell_width * @as(f64, @floatFromInt(opts.cell_width orelse 1)); + // If the original advance is greater than the cell width then + // it's possible that this is a ligature or other glyph that is + // intended to overflow the cell to one side or the other, and + // adjusting the bearings could mess that up, so we just leave + // it alone if that's the case. + // + // We also don't want to do anything if the advance is zero or + // less, since this is used for stuff like combining characters. + if (advance > new_advance or advance <= 0.0) { + break :offset_x @intFromFloat(@round(x)); + } + break :offset_x @intFromFloat( + @round(x + (new_advance - advance) / 2), + ); + } else { + break :offset_x @intFromFloat(@round(x)); } - - break :offset_x result; }; return .{ diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 079cf5b2d..6aeb951af 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -638,20 +638,40 @@ pub const Face = struct { // This should be the distance from the left of // the cell to the left of the glyph's bounding box. const offset_x: i32 = offset_x: { - var result: i32 = @intFromFloat(@floor(x)); - - // If our cell was resized then we adjust our glyph's - // position relative to the new center. This keeps glyphs - // centered in the cell whether it was made wider or narrower. - if (metrics.original_cell_width) |original_width| { - const before: i32 = @intCast(original_width); - const after: i32 = @intCast(metrics.cell_width); - // Increase the offset by half of the difference - // between the widths to keep things centered. - result += @divTrunc(after - before, 2); + // If the glyph's advance is narrower than the cell width then we + // center the advance of the glyph within the cell width. At first + // I implemented this to proportionally scale the center position + // of the glyph but that messes up glyphs that are meant to align + // vertically with others, so this is a compromise. + // + // This makes it so that when the `adjust-cell-width` config is + // used, or when a fallback font with a different advance width + // is used, we don't get weirdly aligned glyphs. + // + // We don't do this if the constraint has a horizontal alignment, + // since in that case the position was already calculated with the + // new cell width in mind. + if (opts.constraint.align_horizontal == .none) { + const advance = f26dot6ToFloat(glyph.*.advance.x); + const new_advance = + cell_width * @as(f64, @floatFromInt(opts.cell_width orelse 1)); + // If the original advance is greater than the cell width then + // it's possible that this is a ligature or other glyph that is + // intended to overflow the cell to one side or the other, and + // adjusting the bearings could mess that up, so we just leave + // it alone if that's the case. + // + // We also don't want to do anything if the advance is zero or + // less, since this is used for stuff like combining characters. + if (advance > new_advance or advance <= 0.0) { + break :offset_x @intFromFloat(@floor(x)); + } + break :offset_x @intFromFloat( + @floor(x + (new_advance - advance) / 2), + ); + } else { + break :offset_x @intFromFloat(@floor(x)); } - - break :offset_x result; }; return Glyph{ From db08bf1655d10f5c459f41f2c01963bede98db55 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 6 Jul 2025 16:37:15 -0600 Subject: [PATCH 3/5] font: adjust fallback font sizes to match primary metrics This better harmonizes fallback fonts with the primary font by matching the heights of lowercase letters. This should be a big improvement for users who use mixed scripts and so rely heavily on fallback fonts. --- src/font/Collection.zig | 169 ++++++++++++++++++++++++++++++++----- src/font/Metrics.zig | 12 +++ src/font/face/coretext.zig | 15 ++++ src/font/face/freetype.zig | 22 ++++- 4 files changed, 196 insertions(+), 22 deletions(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 8533331bc..cdbd3d84f 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -69,10 +69,14 @@ pub fn deinit(self: *Collection, alloc: Allocator) void { if (self.load_options) |*v| v.deinit(alloc); } -pub const AddError = Allocator.Error || error{ - CollectionFull, - DeferredLoadingUnavailable, -}; +pub const AddError = + Allocator.Error || + AdjustSizeError || + error{ + CollectionFull, + DeferredLoadingUnavailable, + SetSizeFailed, + }; /// Add a face to the collection for the given style. This face will be added /// next in priority if others exist already, i.e. it'll be the _last_ to be @@ -81,10 +85,9 @@ pub const AddError = Allocator.Error || error{ /// If no error is encountered then the collection takes ownership of the face, /// in which case face will be deallocated when the collection is deallocated. /// -/// If a loaded face is added to the collection, it should be the same -/// size as all the other faces in the collection. This function will not -/// verify or modify the size until the size of the entire collection is -/// changed. +/// If a loaded face is added to the collection, its size will be changed to +/// match the size specified in load_options, adjusted for harmonization with +/// the primary face. pub fn add( self: *Collection, alloc: Allocator, @@ -103,9 +106,106 @@ pub fn add( return error.DeferredLoadingUnavailable; try list.append(alloc, face); + + var owned: *Entry = list.at(idx); + + // If the face is already loaded, apply font size adjustment + // now, otherwise we'll apply it whenever we do load it. + if (owned.getLoaded()) |loaded| { + if (try self.adjustedSize(loaded)) |opts| { + loaded.setSize(opts.faceOptions()) catch return error.SetSizeFailed; + } + } + return .{ .style = style, .idx = @intCast(idx) }; } +pub const AdjustSizeError = font.Face.GetMetricsError; + +// Calculate a size for the provided face that will match it with the primary +// font, metrically, to improve consistency with fallback fonts. Right now we +// match the font based on the ex height, or the ideograph width if the font +// has ideographs in it. +// +// This returns null if load options is null or if self.load_options is null. +// +// +// This is very much like the `font-size-adjust` CSS property in how it works. +// ref: https://developer.mozilla.org/en-US/docs/Web/CSS/font-size-adjust +// +// TODO: In the future, provide config options that allow the user to select +// which metric should be matched for fallback fonts, instead of hard +// coding it as ex height. +pub fn adjustedSize( + self: *Collection, + face: *Face, +) AdjustSizeError!?LoadOptions { + const load_options = self.load_options orelse return null; + + // We silently do nothing if we can't get the primary + // face, because this might be the primary face itself. + const primary_face = self.getFace(.{ .idx = 0 }) catch return null; + + // We do nothing if the primary face and this face are the same. + if (@intFromPtr(primary_face) == @intFromPtr(face)) return null; + + const primary_metrics = try primary_face.getMetrics(); + const face_metrics = try face.getMetrics(); + + // We use the ex height to match our font sizes, so that the height of + // lower-case letters matches between all fonts in the fallback chain. + // + // We estimate ex height as 0.75 * cap height if it's not specifically + // provided, and we estimate cap height as 0.75 * ascent in the same case. + // + // If the fallback font has an ic_width we prefer that, for normalization + // of CJK font sizes when mixed with latin fonts. + // + // We estimate the ic_width as twice the cell width if it isn't provided. + var primary_cap = primary_metrics.cap_height orelse 0.0; + if (primary_cap <= 0) primary_cap = primary_metrics.ascent * 0.75; + + var primary_ex = primary_metrics.ex_height orelse 0.0; + if (primary_ex <= 0) primary_ex = primary_cap * 0.75; + + var primary_ic = primary_metrics.ic_width orelse 0.0; + if (primary_ic <= 0) primary_ic = primary_metrics.cell_width * 2; + + var face_cap = face_metrics.cap_height orelse 0.0; + if (face_cap <= 0) face_cap = face_metrics.ascent * 0.75; + + var face_ex = face_metrics.ex_height orelse 0.0; + if (face_ex <= 0) face_ex = face_cap * 0.75; + + var face_ic = face_metrics.ic_width orelse 0.0; + if (face_ic <= 0) face_ic = face_metrics.cell_width * 2; + + // If the line height of the scaled font would be larger than + // the line height of the primary font, we don't want that, so + // we take the minimum between matching the ic/ex and the line + // height. + // + // NOTE: We actually allow the line height to be up to 1.2 + // times the primary line height because empirically + // this is usually fine and is better for CJK. + // + // TODO: We should probably provide a config option that lets + // the user pick what metric to use for size adjustment. + const scale = @min( + 1.2 * primary_metrics.lineHeight() / face_metrics.lineHeight(), + if (face_metrics.ic_width != null) + primary_ic / face_ic + else + primary_ex / face_ex, + ); + + // Make a copy of our load options and multiply the size by our scale. + var opts = load_options; + 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. /// @@ -129,21 +229,38 @@ pub fn getFace(self: *Collection, index: Index) !*Face { break :item item; }; - return try self.getFaceFromEntry(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; } /// Get the face from an entry. /// /// This entry must not be an alias. -fn getFaceFromEntry(self: *Collection, entry: *Entry) !*Face { +fn getFaceFromEntry( + self: *Collection, + entry: *Entry, + /// Whether to adjust the font size to match the primary face after loading. + adjust: bool, +) !*Face { assert(entry.* != .alias); return switch (entry.*) { inline .deferred, .fallback_deferred => |*d, tag| deferred: { const opts = self.load_options orelse return error.DeferredLoadingUnavailable; - const face = try d.load(opts.library, opts.faceOptions()); + var face = try d.load(opts.library, opts.faceOptions()); 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) { .deferred => .{ .loaded = face }, .fallback_deferred => .{ .fallback_loaded = face }, @@ -247,7 +364,7 @@ pub fn completeStyles( while (it.next()) |entry| { // 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) catch |err| { + const face = self.getFaceFromEntry(entry, false) catch |err| { log.warn("error loading regular entry={d} err={}", .{ it.index - 1, err, @@ -371,7 +488,7 @@ 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); + const regular = try self.getFaceFromEntry(entry, false); const face = try regular.syntheticBold(opts.faceOptions()); var buf: [256]u8 = undefined; @@ -391,7 +508,7 @@ 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); + const regular = try self.getFaceFromEntry(entry, false); const face = try regular.syntheticItalic(opts.faceOptions()); var buf: [256]u8 = undefined; @@ -420,9 +537,12 @@ pub fn setSize(self: *Collection, size: DesiredSize) !void { while (it.next()) |array| { var entry_it = array.value.iterator(0); while (entry_it.next()) |entry| switch (entry.*) { - .loaded, .fallback_loaded => |*f| try f.setSize( - opts.faceOptions(), - ), + .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. @@ -549,6 +669,16 @@ pub const Entry = union(enum) { } } + /// If this face is loaded, or is an alias to a loaded face, + /// then this returns the `Face`, otherwise returns null. + pub fn getLoaded(self: *Entry) ?*Face { + return switch (self.*) { + .deferred, .fallback_deferred => null, + .loaded, .fallback_loaded => |*face| face, + .alias => |v| v.getLoaded(), + }; + } + /// True if the entry is deferred. fn isDeferred(self: Entry) bool { return switch (self) { @@ -906,12 +1036,13 @@ test "metrics" { var c = init(); defer c.deinit(alloc); - c.load_options = .{ .library = lib }; + const size: DesiredSize = .{ .points = 12, .xdpi = 96, .ydpi = 96 }; + c.load_options = .{ .library = lib, .size = size }; _ = try c.add(alloc, .regular, .{ .loaded = try .init( lib, testFont, - .{ .size = .{ .points = 12, .xdpi = 96, .ydpi = 96 } }, + .{ .size = size }, ) }); try c.updateMetrics(); diff --git a/src/font/Metrics.zig b/src/font/Metrics.zig index bf527a021..069606c06 100644 --- a/src/font/Metrics.zig +++ b/src/font/Metrics.zig @@ -107,6 +107,18 @@ pub const FaceMetrics = struct { /// a provided ex height metric or measured from the height of the /// lowercase x glyph. ex_height: ?f64 = null, + + /// The width of the character "水" (CJK water ideograph, U+6C34), + /// if present. This is used for font size adjustment, to normalize + /// the width of CJK fonts mixed with latin fonts. + /// + /// NOTE: IC = Ideograph Character + ic_width: ?f64 = null, + + /// Convenience function for getting the line height (ascent - descent). + pub inline fn lineHeight(self: FaceMetrics) f64 { + return self.ascent - self.descent; + } }; /// Calculate our metrics based on values extracted from a font. diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 6aedd7696..c1f16e025 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -757,6 +757,20 @@ pub const Face = struct { break :cell_width max; }; + // Measure "水" (CJK water ideograph, U+6C34) for our ic width. + const ic_width: ?f64 = ic_width: { + const glyph = self.glyphIndex('水') orelse break :ic_width null; + + var advances: [1]macos.graphics.Size = undefined; + _ = ct_font.getAdvancesForGlyphs( + .horizontal, + &.{@intCast(glyph)}, + &advances, + ); + + break :ic_width advances[0].width; + }; + return .{ .cell_width = cell_width, .ascent = ascent, @@ -768,6 +782,7 @@ pub const Face = struct { .strikethrough_thickness = strikethrough_thickness, .cap_height = cap_height, .ex_height = ex_height, + .ic_width = ic_width, }; } diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 6aeb951af..db5a3622e 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -851,7 +851,7 @@ pub const Face = struct { while (c < 127) : (c += 1) { if (face.getCharIndex(c)) |glyph_index| { if (face.loadGlyph(glyph_index, .{ - .render = true, + .render = false, .no_svg = true, })) { max = @max( @@ -889,7 +889,7 @@ pub const Face = struct { defer self.ft_mutex.unlock(); if (face.getCharIndex('H')) |glyph_index| { if (face.loadGlyph(glyph_index, .{ - .render = true, + .render = false, .no_svg = true, })) { break :cap f26dot6ToF64(face.handle.*.glyph.*.metrics.height); @@ -902,7 +902,7 @@ pub const Face = struct { defer self.ft_mutex.unlock(); if (face.getCharIndex('x')) |glyph_index| { if (face.loadGlyph(glyph_index, .{ - .render = true, + .render = false, .no_svg = true, })) { break :ex f26dot6ToF64(face.handle.*.glyph.*.metrics.height); @@ -913,6 +913,21 @@ pub const Face = struct { }; }; + // Measure "水" (CJK water ideograph, U+6C34) for our ic width. + const ic_width: ?f64 = ic_width: { + self.ft_mutex.lock(); + defer self.ft_mutex.unlock(); + + const glyph = face.getCharIndex('水') orelse break :ic_width null; + + face.loadGlyph(glyph, .{ + .render = false, + .no_svg = true, + }) catch break :ic_width null; + + break :ic_width f26dot6ToF64(face.handle.*.glyph.*.advance.x); + }; + return .{ .cell_width = cell_width, @@ -928,6 +943,7 @@ pub const Face = struct { .cap_height = cap_height, .ex_height = ex_height, + .ic_width = ic_width, }; } From d33161ad66d46e5ca4d7f41a11abd85b9937a406 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 6 Jul 2025 22:40:43 -0600 Subject: [PATCH 4/5] fix(font): include line gap in `lineHeight` helper --- src/font/Metrics.zig | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/font/Metrics.zig b/src/font/Metrics.zig index 069606c06..f96d753b3 100644 --- a/src/font/Metrics.zig +++ b/src/font/Metrics.zig @@ -115,9 +115,10 @@ pub const FaceMetrics = struct { /// NOTE: IC = Ideograph Character ic_width: ?f64 = null, - /// Convenience function for getting the line height (ascent - descent). + /// Convenience function for getting the line height + /// (ascent - descent + line_gap). pub inline fn lineHeight(self: FaceMetrics) f64 { - return self.ascent - self.descent; + return self.ascent - self.descent + self.line_gap; } }; From 08fd1688ff451c4cf2d1cc3e4864f05e71cefbdc Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Sun, 6 Jul 2025 22:45:13 -0600 Subject: [PATCH 5/5] font: add test for size adjustment, fix small bug in resize Previously produced very wrong values when calling Collection.setSize, since it was assuming that the provided face had the same point size as the primary face, which isn't true during resize-- so instead we just have faces keep track of their set size, this is generally useful. --- src/font/Collection.zig | 64 ++++++++++++++++++++++++++++++++++++-- src/font/face/coretext.zig | 4 +++ src/font/face/freetype.zig | 5 +++ 3 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index cdbd3d84f..1d85d8a28 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -129,7 +129,6 @@ pub const AdjustSizeError = font.Face.GetMetricsError; // // 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 // @@ -199,8 +198,10 @@ pub fn adjustedSize( primary_ex / face_ex, ); - // Make a copy of our load options and multiply the size by our scale. + // 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; @@ -1089,3 +1090,62 @@ test "metrics" { .cursor_height = 34, }, 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; + + 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, .regular, .{ .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( + lib, + fallback, + .{ .size = size }, + ) }); + + // The ex heights should match. + { + const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics(); + const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics(); + + try std.testing.expectApproxEqAbs( + primary_metrics.ex_height.?, + fallback_metrics.ex_height.?, + // We accept anything within half a pixel. + 0.5, + ); + } + + // Resize should keep that relationship. + try c.setSize(.{ .points = 37, .xdpi = 96, .ydpi = 96 }); + { + const primary_metrics = try (try c.getFace(.{ .idx = 0 })).getMetrics(); + const fallback_metrics = try (try c.getFace(fallback_idx)).getMetrics(); + + try std.testing.expectApproxEqAbs( + primary_metrics.ex_height.?, + fallback_metrics.ex_height.?, + // We accept anything within half a pixel. + 0.5, + ); + } +} diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index c1f16e025..00cc31b26 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -31,6 +31,9 @@ pub const Face = struct { /// tables). color: ?ColorState = null, + /// The current size this font is set to. + size: font.face.DesiredSize, + /// True if our build is using Harfbuzz. If we're not, we can avoid /// some Harfbuzz-specific code paths. const harfbuzz_shaper = font.options.backend.hasHarfbuzz(); @@ -106,6 +109,7 @@ pub const Face = struct { .font = ct_font, .hb_font = hb_font, .color = color, + .size = opts.size, }; result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result); diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index db5a3622e..ae3bd0968 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -59,6 +59,9 @@ pub const Face = struct { bold: bool = false, } = .{}, + /// The current size this font is set to. + size: font.face.DesiredSize, + /// Initialize a new font face with the given source in-memory. pub fn initFile( lib: Library, @@ -107,6 +110,7 @@ pub const Face = struct { .hb_font = hb_font, .ft_mutex = ft_mutex, .load_flags = opts.freetype_load_flags, + .size = opts.size, }; result.quirks_disable_default_font_features = quirks.disableDefaultFontFeatures(&result); @@ -203,6 +207,7 @@ pub const Face = struct { /// for clearing any glyph caches, font atlas data, etc. pub fn setSize(self: *Face, opts: font.face.Options) !void { try setSize_(self.face, opts.size); + self.size = opts.size; } fn setSize_(face: freetype.Face, size: font.face.DesiredSize) !void {