summaryrefslogtreecommitdiff
path: root/src/font
diff options
context:
space:
mode:
authorDaniel Wennberg <daniel.wennberg@gmail.com>2025-07-15 12:57:23 -0700
committerDaniel Wennberg <daniel.wennberg@gmail.com>2025-07-17 15:45:47 -0700
commit6491ea41fb4cc671c6e3089bf6eb3d1ec752de2b (patch)
tree0e87cfa744b01767977080f00825c13cf0045a87 /src/font
parent155ddc3f8f0e99731d30f15653c62ac7e2476c46 (diff)
Move face metric fallback estimates to the FaceMetric struct
Diffstat (limited to 'src/font')
-rw-r--r--src/font/Collection.zig29
-rw-r--r--src/font/Metrics.zig90
2 files changed, 68 insertions, 51 deletions
diff --git a/src/font/Collection.zig b/src/font/Collection.zig
index eb4349fb0..702a3fd7c 100644
--- a/src/font/Collection.zig
+++ b/src/font/Collection.zig
@@ -154,30 +154,13 @@ pub fn adjustedSize(
// 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;
+ const primary_ex = primary_metrics.exHeight();
+ const primary_ic = primary_metrics.icWidth();
- var face_ic = face_metrics.ic_width orelse 0.0;
- if (face_ic <= 0) face_ic = face_metrics.cell_width * 2;
+ 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
@@ -192,7 +175,9 @@ pub fn adjustedSize(
// 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)
+ 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,
diff --git a/src/font/Metrics.zig b/src/font/Metrics.zig
index 89f6a507f..320a4f504 100644
--- a/src/font/Metrics.zig
+++ b/src/font/Metrics.zig
@@ -120,6 +120,60 @@ pub const FaceMetrics = struct {
pub inline fn lineHeight(self: FaceMetrics) f64 {
return self.ascent - self.descent + self.line_gap;
}
+
+ /// Convenience function for getting the cap height. If this is not
+ /// defined in the font, we estimate it as 75% of the ascent.
+ pub inline fn capHeight(self: FaceMetrics) f64 {
+ if (self.cap_height) |value| if (value > 0) return value;
+ return 0.75 * self.ascent;
+ }
+
+ /// Convenience function for getting the ex height. If this is not
+ /// defined in the font, we estimate it as 75% of the cap height.
+ pub inline fn exHeight(self: FaceMetrics) f64 {
+ if (self.ex_height) |value| if (value > 0) return value;
+ return 0.75 * self.capHeight();
+ }
+
+ /// Convenience function for getting the ideograph width. If this is
+ /// not defined in the font, we estimate it as two cell widths.
+ pub inline fn icWidth(self: FaceMetrics) f64 {
+ if (self.ic_width) |value| if (value > 0) return value;
+ return 2 * self.cell_width;
+ }
+
+ /// Convenience function for getting the underline thickness. If
+ /// this is not defined in the font, we estimate it as 15% of the ex
+ /// height.
+ pub inline fn underlineThickness(self: FaceMetrics) f64 {
+ if (self.underline_thickness) |value| if (value > 0) return value;
+ return 0.15 * self.exHeight();
+ }
+
+ /// Convenience function for getting the strikethrough thickness. If
+ /// this is not defined in the font, we set it equal to the
+ /// underline thickness.
+ pub inline fn strikethroughThickness(self: FaceMetrics) f64 {
+ if (self.strikethrough_thickness) |value| if (value > 0) return value;
+ return self.underlineThickness();
+ }
+
+ // NOTE: The getters below return positions, not sizes, so both
+ // positive and negative values are valid, hence no sign validation.
+
+ /// Convenience function for getting the underline position. If
+ /// this is not defined in the font, we place it one underline
+ /// thickness below the baseline.
+ pub inline fn underlinePosition(self: FaceMetrics) f64 {
+ return self.underline_position orelse -self.underlineThickness();
+ }
+
+ /// Convenience function for getting the strikethrough position. If
+ /// this is not defined in the font, we center it at half the ex
+ /// height, so that it's perfectly centered on lower case text.
+ pub inline fn strikethroughPosition(self: FaceMetrics) f64 {
+ return self.strikethrough_position orelse (self.exHeight() + self.strikethroughThickness()) * 0.5;
+ }
};
/// Calculate our metrics based on values extracted from a font.
@@ -147,35 +201,13 @@ pub fn calc(face: FaceMetrics) Metrics {
// We calculate a top_to_baseline to make following calculations simpler.
const top_to_baseline = cell_height - cell_baseline;
- // If we don't have a provided cap height,
- // we estimate it as 75% of the ascent.
- const cap_height = face.cap_height orelse face.ascent * 0.75;
-
- // If we don't have a provided ex height,
- // we estimate it as 75% of the cap height.
- const ex_height = face.ex_height orelse cap_height * 0.75;
-
- // If we don't have a provided underline thickness,
- // we estimate it as 15% of the ex height.
- const underline_thickness = @max(1, @ceil(face.underline_thickness orelse 0.15 * ex_height));
-
- // If we don't have a provided strikethrough thickness
- // then we just use the underline thickness for it.
- const strikethrough_thickness = @max(1, @ceil(face.strikethrough_thickness orelse underline_thickness));
-
- // If we don't have a provided underline position then
- // we place it 1 underline-thickness below the baseline.
- const underline_position = @round(top_to_baseline -
- (face.underline_position orelse
- -underline_thickness));
-
- // If we don't have a provided strikethrough position
- // then we center the strikethrough stroke at half the
- // ex height, so that it's perfectly centered on lower
- // case text.
- const strikethrough_position = @round(top_to_baseline -
- (face.strikethrough_position orelse
- ex_height * 0.5 + strikethrough_thickness * 0.5));
+ // Get the other font metrics or their estimates. See doc comments
+ // in FaceMetrics for explanations of the estimation heuristics.
+ const cap_height = face.capHeight();
+ const underline_thickness = @max(1, @ceil(face.underlineThickness()));
+ const strikethrough_thickness = @max(1, @ceil(face.strikethroughThickness()));
+ const underline_position = @round(top_to_baseline - face.underlinePosition());
+ const strikethrough_position = @round(top_to_baseline - face.strikethroughPosition());
// The calculation for icon height in the nerd fonts patcher
// is two thirds cap height to one third line height, but we