Merge branch 'main' into jacob/uucode

This commit is contained in:
Mitchell Hashimoto
2025-09-30 06:52:07 -07:00
committed by GitHub
55 changed files with 2365 additions and 1182 deletions

View File

@@ -1213,6 +1213,9 @@ test "metrics" {
// 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,
@@ -1223,7 +1226,10 @@ test "metrics" {
.overline_thickness = 1,
.box_thickness = 1,
.cursor_height = 17,
.icon_height = 11,
.icon_height = 12.24,
.face_width = 8.0,
.face_height = 16.784,
.face_y = @round(3.04) - @as(f64, 3.04), // use f64, not comptime float, for exact match with runtime value
}, c.metrics);
// Resize should change metrics
@@ -1240,7 +1246,10 @@ test "metrics" {
.overline_thickness = 2,
.box_thickness = 2,
.cursor_height = 34,
.icon_height = 23,
.icon_height = 24.48,
.face_width = 16.0,
.face_height = 33.568,
.face_y = @round(6.08) - @as(f64, 6.08), // use f64, not comptime float, for exact match with runtime value
}, c.metrics);
}
@@ -1369,3 +1378,155 @@ test "adjusted sizes" {
);
}
}
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| {
inline for (@typeInfo(font.Metrics.FaceMetrics).@"struct".fields) |field| {
const expected = @field(metricsExpected, field.name);
const actual = @field(metricsActual, field.name);
// Unwrap optional fields
const expectedValue, const actualValue = unwrap: switch (@typeInfo(field.type)) {
.optional => {
if (expected) |expectedValue| if (actual) |actualValue| {
break :unwrap .{ expectedValue, actualValue };
};
// Null values can be compared directly
try std.testing.expectEqual(expected, actual);
continue;
},
else => break :unwrap .{ expected, actual },
};
// All non-null values are floats
const eps = std.math.floatEps(@TypeOf(actualValue - expectedValue));
try std.testing.expectApproxEqRel(
expectedValue,
actualValue,
std.math.sqrt(eps),
);
}
}
// 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(),
);
}

View File

@@ -36,11 +36,17 @@ cursor_thickness: u32 = 1,
cursor_height: u32,
/// The constraint height for nerd fonts icons.
icon_height: u32,
icon_height: f64,
/// Original cell width in pixels. This is used to keep
/// glyphs centered if the cell width is adjusted wider.
original_cell_width: ?u32 = null,
/// The unrounded face width, used in scaling calculations.
face_width: f64,
/// The unrounded face height, used in scaling calculations.
face_height: f64,
/// The vertical bearing of face within the pixel-rounded
/// and possibly height-adjusted cell
face_y: f64,
/// Minimum acceptable values for some fields to prevent modifiers
/// from being able to, for example, cause 0-thickness underlines.
@@ -53,7 +59,9 @@ const Minimums = struct {
const box_thickness = 1;
const cursor_thickness = 1;
const cursor_height = 1;
const icon_height = 1;
const icon_height = 1.0;
const face_height = 1.0;
const face_width = 1.0;
};
/// Metrics extracted from a font face, based on
@@ -214,8 +222,10 @@ pub fn calc(face: FaceMetrics) Metrics {
// We use the ceiling of the provided cell width and height to ensure
// that the cell is large enough for the provided size, since we cast
// it to an integer later.
const cell_width = @ceil(face.cell_width);
const cell_height = @ceil(face.lineHeight());
const face_width = face.cell_width;
const face_height = face.lineHeight();
const cell_width = @ceil(face_width);
const cell_height = @ceil(face_height);
// We split our line gap in two parts, and put half of it on the top
// of the cell and the other half on the bottom, so that our text never
@@ -224,7 +234,11 @@ pub fn calc(face: FaceMetrics) Metrics {
// Unlike all our other metrics, `cell_baseline` is relative to the
// BOTTOM of the cell.
const cell_baseline = @round(half_line_gap - face.descent);
const face_baseline = half_line_gap - face.descent;
const cell_baseline = @round(face_baseline);
// We keep track of the vertical bearing of the face in the cell
const face_y = cell_baseline - face_baseline;
// We calculate a top_to_baseline to make following calculations simpler.
const top_to_baseline = cell_height - cell_baseline;
@@ -237,16 +251,8 @@ pub fn calc(face: FaceMetrics) Metrics {
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
// use an opinionated default of 1.2 * cap height instead.
//
// Doing this prevents fonts with very large line heights
// from having excessively oversized icons, and allows fonts
// with very small line heights to still have roomy icons.
//
// We do cap it at `cell_height` though for obvious reasons.
const icon_height = @min(cell_height, cap_height * 1.2);
// Same heuristic as the font_patcher script
const icon_height = (2 * cap_height + face_height) / 3;
var result: Metrics = .{
.cell_width = @intFromFloat(cell_width),
@@ -260,7 +266,10 @@ pub fn calc(face: FaceMetrics) Metrics {
.overline_thickness = @intFromFloat(underline_thickness),
.box_thickness = @intFromFloat(underline_thickness),
.cursor_height = @intFromFloat(cell_height),
.icon_height = @intFromFloat(icon_height),
.icon_height = icon_height,
.face_width = face_width,
.face_height = face_height,
.face_y = face_y,
};
// Ensure all metrics are within their allowable range.
@@ -286,11 +295,6 @@ pub fn apply(self: *Metrics, mods: ModifierSet) void {
const new = @max(entry.value_ptr.apply(original), 1);
if (new == original) continue;
// Preserve the original cell width if not set.
if (self.original_cell_width == null) {
self.original_cell_width = self.cell_width;
}
// Set the new value
@field(self, @tagName(tag)) = new;
@@ -307,6 +311,7 @@ pub fn apply(self: *Metrics, mods: ModifierSet) void {
const diff = new - original;
const diff_bottom = diff / 2;
const diff_top = diff - diff_bottom;
self.face_y += @floatFromInt(diff_bottom);
self.cell_baseline +|= diff_bottom;
self.underline_position +|= diff_top;
self.strikethrough_position +|= diff_top;
@@ -315,6 +320,7 @@ pub fn apply(self: *Metrics, mods: ModifierSet) void {
const diff = original - new;
const diff_bottom = diff / 2;
const diff_top = diff - diff_bottom;
self.face_y -= @floatFromInt(diff_bottom);
self.cell_baseline -|= diff_bottom;
self.underline_position -|= diff_top;
self.strikethrough_position -|= diff_top;
@@ -417,25 +423,35 @@ pub const Modifier = union(enum) {
/// Apply a modifier to a numeric value.
pub fn apply(self: Modifier, v: anytype) @TypeOf(v) {
const T = @TypeOf(v);
const signed = @typeInfo(T).int.signedness == .signed;
return switch (self) {
.percent => |p| percent: {
const p_clamped: f64 = @max(0, p);
const v_f64: f64 = @floatFromInt(v);
const applied_f64: f64 = @round(v_f64 * p_clamped);
const applied_T: T = @intFromFloat(applied_f64);
break :percent applied_T;
},
const Tinfo = @typeInfo(T);
return switch (comptime Tinfo) {
.int, .comptime_int => switch (self) {
.percent => |p| percent: {
const p_clamped: f64 = @max(0, p);
const v_f64: f64 = @floatFromInt(v);
const applied_f64: f64 = @round(v_f64 * p_clamped);
const applied_T: T = @intFromFloat(applied_f64);
break :percent applied_T;
},
.absolute => |abs| absolute: {
const v_i64: i64 = @intCast(v);
const abs_i64: i64 = @intCast(abs);
const applied_i64: i64 = v_i64 +| abs_i64;
const clamped_i64: i64 = if (signed) applied_i64 else @max(0, applied_i64);
const applied_T: T = std.math.cast(T, clamped_i64) orelse
std.math.maxInt(T) * @as(T, @intCast(std.math.sign(clamped_i64)));
break :absolute applied_T;
.absolute => |abs| absolute: {
const v_i64: i64 = @intCast(v);
const abs_i64: i64 = @intCast(abs);
const applied_i64: i64 = v_i64 +| abs_i64;
const clamped_i64: i64 = if (Tinfo.int.signedness == .signed)
applied_i64
else
@max(0, applied_i64);
const applied_T: T = std.math.cast(T, clamped_i64) orelse
std.math.maxInt(T) * @as(T, @intCast(std.math.sign(clamped_i64)));
break :absolute applied_T;
},
},
.float, .comptime_float => return switch (self) {
.percent => |p| v * @max(0, p),
.absolute => |abs| v + @as(T, @floatFromInt(abs)),
},
else => {},
};
}
@@ -481,7 +497,7 @@ pub const Key = key: {
var enumFields: [field_infos.len]std.builtin.Type.EnumField = undefined;
var count: usize = 0;
for (field_infos, 0..) |field, i| {
if (field.type != u32 and field.type != i32) continue;
if (field.type != u32 and field.type != i32 and field.type != f64) continue;
enumFields[i] = .{ .name = field.name, .value = i };
count += 1;
}
@@ -512,7 +528,10 @@ fn init() Metrics {
.overline_thickness = 0,
.box_thickness = 0,
.cursor_height = 0,
.icon_height = 0,
.icon_height = 0.0,
.face_width = 0.0,
.face_height = 0.0,
.face_y = 0.0,
};
}
@@ -542,6 +561,7 @@ test "Metrics: adjust cell height smaller" {
try set.put(alloc, .cell_height, .{ .percent = 0.75 });
var m: Metrics = init();
m.face_y = 0.33;
m.cell_baseline = 50;
m.underline_position = 55;
m.strikethrough_position = 30;
@@ -549,6 +569,7 @@ test "Metrics: adjust cell height smaller" {
m.cell_height = 100;
m.cursor_height = 100;
m.apply(set);
try testing.expectEqual(-11.67, m.face_y);
try testing.expectEqual(@as(u32, 75), m.cell_height);
try testing.expectEqual(@as(u32, 38), m.cell_baseline);
try testing.expectEqual(@as(u32, 42), m.underline_position);
@@ -570,6 +591,7 @@ test "Metrics: adjust cell height larger" {
try set.put(alloc, .cell_height, .{ .percent = 1.75 });
var m: Metrics = init();
m.face_y = 0.33;
m.cell_baseline = 50;
m.underline_position = 55;
m.strikethrough_position = 30;
@@ -577,6 +599,7 @@ test "Metrics: adjust cell height larger" {
m.cell_height = 100;
m.cursor_height = 100;
m.apply(set);
try testing.expectEqual(37.33, m.face_y);
try testing.expectEqual(@as(u32, 175), m.cell_height);
try testing.expectEqual(@as(u32, 87), m.cell_baseline);
try testing.expectEqual(@as(u32, 93), m.underline_position);

View File

@@ -270,11 +270,9 @@ pub fn renderGlyph(
// Always use these constraints for emoji.
if (p == .emoji) {
render_opts.constraint = .{
// Make the emoji as wide as possible, scaling proportionally,
// but then scale it down as necessary if its new size exceeds
// the cell height.
.size_horizontal = .cover,
.size_vertical = .fit,
// Scale emoji to be as large as possible
// while preserving their aspect ratio.
.size = .cover,
// Center the emoji in its cells.
.align_horizontal = .center,

View File

@@ -93,6 +93,14 @@ pub const Variation = struct {
};
};
/// The size and position of a glyph.
pub const GlyphSize = struct {
width: f64,
height: f64,
x: f64,
y: f64,
};
/// Additional options for rendering glyphs.
pub const RenderOptions = struct {
/// The metrics that are defining the grid layout. These are usually
@@ -136,10 +144,8 @@ pub const RenderOptions = struct {
/// Don't constrain the glyph in any way.
pub const none: Constraint = .{};
/// Vertical sizing rule.
size_vertical: Size = .none,
/// Horizontal sizing rule.
size_horizontal: Size = .none,
/// Sizing rule.
size: Size = .none,
/// Vertical alignment rule.
align_vertical: Align = .none,
@@ -155,42 +161,40 @@ pub const RenderOptions = struct {
/// Bottom padding when resizing.
pad_bottom: f64 = 0.0,
// This acts as a multiple of the provided width when applying
// constraints, so if this is 1.6 for example, then a width of
// 10 would be treated as though it were 16.
group_width: f64 = 1.0,
// This acts as a multiple of the provided height when applying
// constraints, so if this is 1.6 for example, then a height of
// 10 would be treated as though it were 16.
group_height: f64 = 1.0,
// This is an x offset for the actual width within the group width.
// If this is 0.5 then the glyph will be offset so that its left
// edge sits at the halfway point of the group width.
group_x: f64 = 0.0,
// This is a y offset for the actual height within the group height.
// If this is 0.5 then the glyph will be offset so that its bottom
// edge sits at the halfway point of the group height.
group_y: f64 = 0.0,
// Size and bearings of the glyph relative
// to the bounding box of its scale group.
relative_width: f64 = 1.0,
relative_height: f64 = 1.0,
relative_x: f64 = 0.0,
relative_y: f64 = 0.0,
/// Maximum ratio of width to height when resizing.
/// Maximum aspect ratio (width/height) to allow when stretching.
max_xy_ratio: ?f64 = null,
/// Maximum number of cells horizontally to use.
max_constraint_width: u2 = 2,
/// What to use as the height metric when constraining the glyph.
/// What to use as the height metric when constraining the glyph and
/// the constraint width is 1,
height: Height = .cell,
pub const Size = enum {
/// Don't change the size of this glyph.
none,
/// Move the glyph and optionally scale it down
/// proportionally to fit within the given axis.
/// Scale the glyph down if needed to fit within the bounds,
/// preserving aspect ratio.
fit,
/// Move and resize the glyph proportionally to
/// cover the given axis.
/// Scale the glyph up or down to exactly match the bounds,
/// preserving aspect ratio.
cover,
/// Same as `cover` but not proportional.
/// Scale the glyph down if needed to fit within the bounds,
/// preserving aspect ratio. If the glyph doesn't cover a
/// single cell, scale up. If the glyph exceeds a single
/// cell but is within the bounds, do nothing.
/// (Nerd Font specific rule.)
fit_cover1,
/// Stretch the glyph to exactly fit the bounds in both
/// directions, disregarding aspect ratio.
stretch,
};
@@ -205,30 +209,27 @@ pub const RenderOptions = struct {
end,
/// Move the glyph so that it is centered on this axis.
center,
/// Move the glyph so that it is centered on this axis,
/// but always with respect to the first cell even for
/// multi-cell constraints. (Nerd Font specific rule.)
center1,
};
pub const Height = enum {
/// Use the full height of the cell for constraining this glyph.
/// Always use the full height of the cell for constraining this glyph.
cell,
/// Use the "icon height" from the grid metrics as the height.
/// When the constraint width is 1, use the "icon height" from the grid
/// metrics as the height. (When the constraint width is >1, the
/// constraint height is always the full cell height.)
icon,
};
/// The size and position of a glyph.
pub const GlyphSize = struct {
width: f64,
height: f64,
x: f64,
y: f64,
};
/// Returns true if the constraint does anything. If it doesn't,
/// because it neither sizes nor positions the glyph, then this
/// returns false.
pub inline fn doesAnything(self: Constraint) bool {
return self.size_horizontal != .none or
return self.size != .none or
self.align_horizontal != .none or
self.size_vertical != .none or
self.align_vertical != .none;
}
@@ -241,156 +242,202 @@ pub const RenderOptions = struct {
/// Number of cells horizontally available for this glyph.
constraint_width: u2,
) GlyphSize {
var g = glyph;
if (!self.doesAnything()) return glyph;
const available_width: f64 = @floatFromInt(
metrics.cell_width * @min(
self.max_constraint_width,
constraint_width,
),
);
const available_height: f64 = @floatFromInt(switch (self.height) {
.cell => metrics.cell_height,
.icon => metrics.icon_height,
});
// For extra wide font faces, never stretch glyphs across two cells.
// This mirrors font_patcher.
const min_constraint_width: u2 = if ((self.size == .stretch) and (metrics.face_width > 0.9 * metrics.face_height))
1
else
@min(self.max_constraint_width, constraint_width);
const w = available_width -
self.pad_left * available_width -
self.pad_right * available_width;
const h = available_height -
self.pad_top * available_height -
self.pad_bottom * available_height;
// Subtract padding from the bearings so that our
// alignment and sizing code works correctly. We
// re-add before returning.
g.x -= self.pad_left * available_width;
g.y -= self.pad_bottom * available_height;
// Multiply by group width and height for better sizing.
g.width *= self.group_width;
g.height *= self.group_height;
switch (self.size_horizontal) {
.none => {},
.fit => if (g.width > w) {
const orig_height = g.height;
// Adjust our height and width to proportionally
// scale them to fit the glyph to the cell width.
g.height *= w / g.width;
g.width = w;
// Set our x to 0 since anything else would mean
// the glyph extends outside of the cell width.
g.x = 0;
// Compensate our y to keep things vertically
// centered as they're scaled down.
g.y += (orig_height - g.height) / 2;
} else if (g.width + g.x > w) {
// If the width of the glyph can fit in the cell but
// is currently outside due to the left bearing, then
// we reduce the left bearing just enough to fit it
// back in the cell.
g.x = w - g.width;
} else if (g.x < 0) {
g.x = 0;
},
.cover => {
const orig_height = g.height;
g.height *= w / g.width;
g.width = w;
g.x = 0;
g.y += (orig_height - g.height) / 2;
},
.stretch => {
g.width = w;
g.x = 0;
},
}
switch (self.size_vertical) {
.none => {},
.fit => if (g.height > h) {
const orig_width = g.width;
// Adjust our height and width to proportionally
// scale them to fit the glyph to the cell height.
g.width *= h / g.height;
g.height = h;
// Set our y to 0 since anything else would mean
// the glyph extends outside of the cell height.
g.y = 0;
// Compensate our x to keep things horizontally
// centered as they're scaled down.
g.x += (orig_width - g.width) / 2;
} else if (g.height + g.y > h) {
// If the height of the glyph can fit in the cell but
// is currently outside due to the bottom bearing, then
// we reduce the bottom bearing just enough to fit it
// back in the cell.
g.y = h - g.height;
} else if (g.y < 0) {
g.y = 0;
},
.cover => {
const orig_width = g.width;
g.width *= h / g.height;
g.height = h;
g.y = 0;
g.x += (orig_width - g.width) / 2;
},
.stretch => {
g.height = h;
g.y = 0;
},
}
// Add group-relative position
g.x += self.group_x * g.width;
g.y += self.group_y * g.height;
// Divide group width and height back out before we align.
g.width /= self.group_width;
g.height /= self.group_height;
if (self.max_xy_ratio) |ratio| if (g.width > g.height * ratio) {
const orig_width = g.width;
g.width = g.height * ratio;
g.x += (orig_width - g.width) / 2;
// The bounding box for the glyph's scale group.
// Scaling and alignment rules are calculated for
// this box and then applied to the glyph.
var group: GlyphSize = group: {
const group_width = glyph.width / self.relative_width;
const group_height = glyph.height / self.relative_height;
break :group .{
.width = group_width,
.height = group_height,
.x = glyph.x - (group_width * self.relative_x),
.y = glyph.y - (group_height * self.relative_y),
};
};
switch (self.align_horizontal) {
.none => {},
.start => g.x = 0,
.end => g.x = w - g.width,
.center => g.x = (w - g.width) / 2,
// The new, constrained glyph size
var constrained_glyph = glyph;
// Apply prescribed scaling
const width_factor, const height_factor = self.scale_factors(group, metrics, min_constraint_width);
constrained_glyph.width *= width_factor;
constrained_glyph.x *= width_factor;
constrained_glyph.height *= height_factor;
constrained_glyph.y *= height_factor;
// NOTE: font_patcher jumps through a lot of hoops at this
// point to ensure that the glyph remains within the target
// bounding box after rounding to font definition units.
// This is irrelevant here as we're not rounding, we're
// staying in f64 and heading straight to rendering.
// Align vertically
if (self.align_vertical != .none) {
// Vertically scale group bounding box.
group.height *= height_factor;
group.y *= height_factor;
// Calculate offset and shift the glyph
constrained_glyph.y += self.offset_vertical(group, metrics);
}
switch (self.align_vertical) {
.none => {},
.start => g.y = 0,
.end => g.y = h - g.height,
.center => g.y = (h - g.height) / 2,
// Align horizontally
if (self.align_horizontal != .none) {
// Horizontally scale group bounding box.
group.width *= width_factor;
group.x *= width_factor;
// Calculate offset and shift the glyph
constrained_glyph.x += self.offset_horizontal(group, metrics, min_constraint_width);
}
// Re-add our padding before returning.
g.x += self.pad_left * available_width;
g.y += self.pad_bottom * available_height;
return constrained_glyph;
}
// If the available height is less than the cell height, we
// add half of the difference to center it in the full height.
//
// If necessary, in the future, we can adjust this to account
// for alignment, but that isn't necessary with any of the nf
// icons afaict.
const cell_height: f64 = @floatFromInt(metrics.cell_height);
g.y += (cell_height - available_height) / 2;
/// Return width and height scaling factors for this scaling group.
fn scale_factors(
self: Constraint,
group: GlyphSize,
metrics: Metrics,
min_constraint_width: u2,
) struct { f64, f64 } {
if (self.size == .none) {
return .{ 1.0, 1.0 };
}
return g;
const multi_cell = (min_constraint_width > 1);
const pad_width_factor = @as(f64, @floatFromInt(min_constraint_width)) - (self.pad_left + self.pad_right);
const pad_height_factor = 1 - (self.pad_bottom + self.pad_top);
const target_width = pad_width_factor * metrics.face_width;
const target_height = pad_height_factor * switch (self.height) {
.cell => metrics.face_height,
// icon_height only applies with single-cell constraints.
// This mirrors font_patcher.
.icon => if (multi_cell)
metrics.face_height
else
metrics.icon_height,
};
var width_factor = target_width / group.width;
var height_factor = target_height / group.height;
switch (self.size) {
.none => unreachable,
.fit => {
// Scale down to fit if needed
height_factor = @min(1, width_factor, height_factor);
width_factor = height_factor;
},
.cover => {
// Scale to cover
height_factor = @min(width_factor, height_factor);
width_factor = height_factor;
},
.fit_cover1 => {
// Scale down to fit or up to cover at least one cell
// NOTE: This is similar to font_patcher's "pa" mode,
// however, font_patcher will only do the upscaling
// part if the constraint width is 1, resulting in
// some icons becoming smaller when the constraint
// width increases. You'd see icons shrinking when
// opening up a space after them. This makes no
// sense, so we've fixed the rule such that these
// icons are scaled to the same size for multi-cell
// constraints as they would be for single-cell.
height_factor = @min(width_factor, height_factor);
if (multi_cell and (height_factor > 1)) {
// Call back into this function with
// constraint width 1 to get single-cell scale
// factors. We use the height factor as width
// could have been modified by max_xy_ratio.
_, const single_height_factor = self.scale_factors(group, metrics, 1);
height_factor = @max(1, single_height_factor);
}
width_factor = height_factor;
},
.stretch => {},
}
// Reduce aspect ratio if required
if (self.max_xy_ratio) |ratio| {
if (group.width * width_factor > group.height * height_factor * ratio) {
width_factor = group.height * height_factor * ratio / group.width;
}
}
return .{ width_factor, height_factor };
}
/// Return vertical offset needed to align this group
fn offset_vertical(
self: Constraint,
group: GlyphSize,
metrics: Metrics,
) f64 {
// We use face_height and offset by face_y, rather than
// using cell_height directly, to account for the asymmetry
// of the pixel cell around the face (a consequence of
// aligning the baseline with a pixel boundary rather than
// vertically centering the face).
const new_group_y = metrics.face_y + switch (self.align_vertical) {
.none => return 0.0,
.start => self.pad_bottom * metrics.face_height,
.end => end: {
const pad_top_dy = self.pad_top * metrics.face_height;
break :end metrics.face_height - pad_top_dy - group.height;
},
.center, .center1 => (metrics.face_height - group.height) / 2,
};
return new_group_y - group.y;
}
/// Return horizontal offset needed to align this group
fn offset_horizontal(
self: Constraint,
group: GlyphSize,
metrics: Metrics,
min_constraint_width: u2,
) f64 {
// For multi-cell constraints, we align relative to the span
// from the left edge of the first face cell to the right
// edge of the last face cell as they sit within the rounded
// and adjusted pixel cell (centered if narrower than the
// pixel cell, left-aligned if wider).
const face_x, const full_face_span = facecalcs: {
const cell_width: f64 = @floatFromInt(metrics.cell_width);
const full_width: f64 = @floatFromInt(min_constraint_width * metrics.cell_width);
const cell_margin = cell_width - metrics.face_width;
break :facecalcs .{ @max(0, cell_margin / 2), full_width - cell_margin };
};
const pad_left_x = self.pad_left * metrics.face_width;
const new_group_x = face_x + switch (self.align_horizontal) {
.none => return 0.0,
.start => pad_left_x,
.end => end: {
const pad_right_dx = self.pad_right * metrics.face_width;
break :end @max(pad_left_x, full_face_span - pad_right_dx - group.width);
},
.center => @max(pad_left_x, (full_face_span - group.width) / 2),
// NOTE: .center1 implements the font_patcher rule of centering
// in the first cell even for multi-cell constraints. Since glyphs
// are not allowed to protrude to the left, this results in the
// left-alignment like .start when the glyph is wider than a cell.
.center1 => @max(pad_left_x, (metrics.face_width - group.width) / 2),
};
return new_group_x - group.x;
}
};
};

View File

@@ -388,19 +388,16 @@ pub const Face = struct {
y = @round(y);
}
// If the cell width was adjusted wider, we re-center all glyphs
// in the new width, so that they aren't weirdly off to the left.
if (metrics.original_cell_width) |original| recenter: {
// 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) break :recenter;
// If the original width was wider then we don't do anything.
if (original >= metrics.cell_width) break :recenter;
// We center all glyphs within the pixel-rounded and adjusted
// cell width if it's larger than the face width, so that they
// aren't weirdly off to the left.
//
// 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) and (metrics.face_width < cell_width)) {
// We add half the difference to re-center.
x += (cell_width - @as(f64, @floatFromInt(original))) / 2;
x += (cell_width - metrics.face_width) / 2;
}
// Our whole-pixel bearings for the final glyph.

View File

@@ -170,7 +170,7 @@ pub const Face = struct {
if (string.len > 1024) break :skip;
var tmp: [512]u16 = undefined;
const max = string.len / 2;
for (@as([]const u16, @alignCast(@ptrCast(string))), 0..) |c, j| tmp[j] = @byteSwap(c);
for (@as([]const u16, @ptrCast(@alignCast(string))), 0..) |c, j| tmp[j] = @byteSwap(c);
const len = std.unicode.utf16LeToUtf8(buf, tmp[0..max]) catch return string;
return buf[0..len];
}
@@ -351,26 +351,16 @@ pub const Face = struct {
return glyph.*.bitmap.pixel_mode == freetype.c.FT_PIXEL_MODE_BGRA;
}
/// Render a glyph using the glyph index. The rendered glyph is stored in the
/// given texture atlas.
pub fn renderGlyph(
self: Face,
alloc: Allocator,
atlas: *font.Atlas,
glyph_index: u32,
opts: font.face.RenderOptions,
) !Glyph {
self.ft_mutex.lock();
defer self.ft_mutex.unlock();
/// Set the load flags to use when loading a glyph for measurement or
/// rendering.
fn glyphLoadFlags(self: Face, constrained: bool) freetype.LoadFlags {
// Hinting should only be enabled if the configured load flags specify
// it and the provided constraint doesn't actually do anything, since
// if it does, then it'll mess up the hinting anyway when it moves or
// resizes the glyph.
const do_hinting = self.load_flags.hinting and !opts.constraint.doesAnything();
const do_hinting = self.load_flags.hinting and !constrained;
// Load the glyph.
try self.face.loadGlyph(glyph_index, .{
return .{
// If our glyph has color, we want to render the color
.color = self.face.hasColor(),
@@ -392,42 +382,56 @@ pub const Face = struct {
// SVG glyphs under FreeType, since that requires bundling another
// dependency to handle rendering the SVG.
.no_svg = true,
});
};
}
/// Get a rect that represents the position and size of the loaded glyph.
fn getGlyphSize(glyph: freetype.c.FT_GlyphSlot) font.face.GlyphSize {
// If we're dealing with an outline glyph then we get the
// outline's bounding box instead of using the built-in
// metrics, since that's more precise and allows better
// cell-fitting.
if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_OUTLINE) {
// Get the glyph's bounding box before we transform it at all.
// We use this rather than the metrics, since it's more precise.
var bbox: freetype.c.FT_BBox = undefined;
_ = freetype.c.FT_Outline_Get_BBox(&glyph.*.outline, &bbox);
return .{
.x = f26dot6ToF64(bbox.xMin),
.y = f26dot6ToF64(bbox.yMin),
.width = f26dot6ToF64(bbox.xMax - bbox.xMin),
.height = f26dot6ToF64(bbox.yMax - bbox.yMin),
};
}
return .{
.x = f26dot6ToF64(glyph.*.metrics.horiBearingX),
.y = f26dot6ToF64(glyph.*.metrics.horiBearingY - glyph.*.metrics.height),
.width = f26dot6ToF64(glyph.*.metrics.width),
.height = f26dot6ToF64(glyph.*.metrics.height),
};
}
/// Render a glyph using the glyph index. The rendered glyph is stored in the
/// given texture atlas.
pub fn renderGlyph(
self: Face,
alloc: Allocator,
atlas: *font.Atlas,
glyph_index: u32,
opts: font.face.RenderOptions,
) !Glyph {
self.ft_mutex.lock();
defer self.ft_mutex.unlock();
// Load the glyph.
try self.face.loadGlyph(glyph_index, self.glyphLoadFlags(opts.constraint.doesAnything()));
const glyph = self.face.handle.*.glyph;
// We get a rect that represents the position
// and size of the glyph before any changes.
const rect: struct {
x: f64,
y: f64,
width: f64,
height: f64,
} = metrics: {
// If we're dealing with an outline glyph then we get the
// outline's bounding box instead of using the built-in
// metrics, since that's more precise and allows better
// cell-fitting.
if (glyph.*.format == freetype.c.FT_GLYPH_FORMAT_OUTLINE) {
// Get the glyph's bounding box before we transform it at all.
// We use this rather than the metrics, since it's more precise.
var bbox: freetype.c.FT_BBox = undefined;
_ = freetype.c.FT_Outline_Get_BBox(&glyph.*.outline, &bbox);
break :metrics .{
.x = f26dot6ToF64(bbox.xMin),
.y = f26dot6ToF64(bbox.yMin),
.width = f26dot6ToF64(bbox.xMax - bbox.xMin),
.height = f26dot6ToF64(bbox.yMax - bbox.yMin),
};
}
break :metrics .{
.x = f26dot6ToF64(glyph.*.metrics.horiBearingX),
.y = f26dot6ToF64(glyph.*.metrics.horiBearingY - glyph.*.metrics.height),
.width = f26dot6ToF64(glyph.*.metrics.width),
.height = f26dot6ToF64(glyph.*.metrics.height),
};
};
const rect = getGlyphSize(glyph);
// If our glyph is smaller than a quarter pixel in either axis
// then it has no outlines or they're too small to render.
@@ -498,17 +502,14 @@ pub const Face = struct {
y = @round(y);
}
// If the cell width was adjusted wider, we re-center all glyphs
// in the new width, so that they aren't weirdly off to the left.
if (metrics.original_cell_width) |original| recenter: {
// 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) break :recenter;
// If the original width was wider then we don't do anything.
if (original >= metrics.cell_width) break :recenter;
// We center all glyphs within the pixel-rounded and adjusted
// cell width if it's larger than the face width, so that they
// aren't weirdly off to the left.
//
// 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) and (metrics.face_width < cell_width)) {
// We add half the difference to re-center.
//
// NOTE: We round this to a whole-pixel amount because under
@@ -516,7 +517,7 @@ pub const Face = struct {
// the case under CoreText. If we move the outlines by
// a non-whole-pixel amount, it completely ruins the
// hinting.
x += @round((cell_width - @as(f64, @floatFromInt(original))) / 2);
x += @round((cell_width - metrics.face_width) / 2);
}
// Now we can render the glyph.
@@ -976,23 +977,15 @@ pub const Face = struct {
var c: u8 = ' ';
while (c < 127) : (c += 1) {
if (face.getCharIndex(c)) |glyph_index| {
if (face.loadGlyph(glyph_index, .{
.render = false,
.no_svg = true,
})) {
if (face.loadGlyph(glyph_index, self.glyphLoadFlags(false))) {
const glyph = face.handle.*.glyph;
max = @max(
f26dot6ToF64(glyph.*.advance.x),
max,
);
top = @max(
f26dot6ToF64(glyph.*.metrics.horiBearingY),
top,
);
bottom = @min(
f26dot6ToF64(glyph.*.metrics.horiBearingY - glyph.*.metrics.height),
bottom,
);
const rect = getGlyphSize(glyph);
top = @max(rect.y + rect.height, top);
bottom = @min(rect.y, bottom);
} else |_| {}
}
}
@@ -1031,11 +1024,8 @@ pub const Face = struct {
self.ft_mutex.lock();
defer self.ft_mutex.unlock();
if (face.getCharIndex('H')) |glyph_index| {
if (face.loadGlyph(glyph_index, .{
.render = false,
.no_svg = true,
})) {
break :cap f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
if (face.loadGlyph(glyph_index, self.glyphLoadFlags(false))) {
break :cap getGlyphSize(face.handle.*.glyph).height;
} else |_| {}
}
break :cap null;
@@ -1044,11 +1034,8 @@ pub const Face = struct {
self.ft_mutex.lock();
defer self.ft_mutex.unlock();
if (face.getCharIndex('x')) |glyph_index| {
if (face.loadGlyph(glyph_index, .{
.render = false,
.no_svg = true,
})) {
break :ex f26dot6ToF64(face.handle.*.glyph.*.metrics.height);
if (face.loadGlyph(glyph_index, self.glyphLoadFlags(false))) {
break :ex getGlyphSize(face.handle.*.glyph).height;
} else |_| {}
}
break :ex null;
@@ -1063,10 +1050,7 @@ pub const Face = struct {
const glyph = face.getCharIndex('水') orelse break :ic_width null;
face.loadGlyph(glyph, .{
.render = false,
.no_svg = true,
}) catch break :ic_width null;
face.loadGlyph(glyph, self.glyphLoadFlags(false)) catch break :ic_width null;
const ft_glyph = face.handle.*.glyph;
@@ -1078,21 +1062,19 @@ pub const Face = struct {
// This can sometimes happen if there's a CJK font that has been
// patched with the nerd fonts patcher and it butchers the advance
// values so the advance ends up half the width of the actual glyph.
if (ft_glyph.*.metrics.width > ft_glyph.*.advance.x) {
const ft_glyph_width = getGlyphSize(ft_glyph).width;
const advance = f26dot6ToF64(ft_glyph.*.advance.x);
if (ft_glyph_width > advance) {
var buf: [1024]u8 = undefined;
const font_name = self.name(&buf) catch "<Error getting font name>";
log.warn(
"(getMetrics) Width of glyph '水' for font \"{s}\" is greater than its advance ({d} > {d}), discarding ic_width metric.",
.{
font_name,
f26dot6ToF64(ft_glyph.*.metrics.width),
f26dot6ToF64(ft_glyph.*.advance.x),
},
.{ font_name, ft_glyph_width, advance },
);
break :ic_width null;
}
break :ic_width f26dot6ToF64(ft_glyph.*.advance.x);
break :ic_width advance;
};
return .{
@@ -1211,25 +1193,31 @@ test "color emoji" {
alloc,
&atlas,
ft_font.glyphIndex('🥸').?,
.{ .grid_metrics = .{
.cell_width = 13,
.cell_height = 24,
.cell_baseline = 0,
.underline_position = 0,
.underline_thickness = 0,
.strikethrough_position = 0,
.strikethrough_thickness = 0,
.overline_position = 0,
.overline_thickness = 0,
.box_thickness = 0,
.cursor_height = 0,
.icon_height = 0,
}, .constraint_width = 2, .constraint = .{
.size_horizontal = .cover,
.size_vertical = .cover,
.align_horizontal = .center,
.align_vertical = .center,
} },
.{
.grid_metrics = .{
.cell_width = 13,
.cell_height = 24,
.cell_baseline = 0,
.underline_position = 0,
.underline_thickness = 0,
.strikethrough_position = 0,
.strikethrough_thickness = 0,
.overline_position = 0,
.overline_thickness = 0,
.box_thickness = 0,
.cursor_height = 0,
.icon_height = 0,
.face_width = 13,
.face_height = 24,
.face_y = 0,
},
.constraint_width = 2,
.constraint = .{
.size = .fit,
.align_horizontal = .center,
.align_vertical = .center,
},
},
);
try testing.expectEqual(@as(u32, 24), glyph.height);
}

File diff suppressed because it is too large Load Diff

View File

@@ -50,10 +50,10 @@ class PatchSetAttributeEntry(TypedDict):
stretch: str
params: dict[str, float | bool]
group_x: float
group_y: float
group_width: float
group_height: float
relative_x: float
relative_y: float
relative_width: float
relative_height: float
class PatchSet(TypedDict):
@@ -143,7 +143,7 @@ def parse_alignment(val: str) -> str | None:
return {
"l": ".start",
"r": ".end",
"c": ".center",
"c": ".center1", # font-patcher specific centering rule, see face.zig
"": None,
}.get(val, ".none")
@@ -158,10 +158,10 @@ def attr_key(attr: PatchSetAttributeEntry) -> AttributeHash:
float(params.get("overlap", 0.0)),
float(params.get("xy-ratio", -1.0)),
float(params.get("ypadding", 0.0)),
float(attr.get("group_x", 0.0)),
float(attr.get("group_y", 0.0)),
float(attr.get("group_width", 1.0)),
float(attr.get("group_height", 1.0)),
float(attr.get("relative_x", 0.0)),
float(attr.get("relative_y", 0.0)),
float(attr.get("relative_width", 1.0)),
float(attr.get("relative_height", 1.0)),
)
@@ -187,10 +187,10 @@ def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry)
stretch = attr.get("stretch", "")
params = attr.get("params", {})
group_x = attr.get("group_x", 0.0)
group_y = attr.get("group_y", 0.0)
group_width = attr.get("group_width", 1.0)
group_height = attr.get("group_height", 1.0)
relative_x = attr.get("relative_x", 0.0)
relative_y = attr.get("relative_y", 0.0)
relative_width = attr.get("relative_width", 1.0)
relative_height = attr.get("relative_height", 1.0)
overlap = params.get("overlap", 0.0)
xy_ratio = params.get("xy-ratio", -1.0)
@@ -204,28 +204,30 @@ def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry)
s = f"{keys}\n => .{{\n"
# These translations don't quite capture the way
# the actual patcher does scaling, but they're a
# good enough compromise.
if "xy" in stretch:
s += " .size_horizontal = .stretch,\n"
s += " .size_vertical = .stretch,\n"
elif "!" in stretch or "^" in stretch:
s += " .size_horizontal = .cover,\n"
s += " .size_vertical = .fit,\n"
# This maps the font_patcher stretch rules to a Constrain instance
# NOTE: some comments in font_patcher indicate that only x or y
# would also be a valid spec, but no icons use it, so we won't
# support it until we have to.
if "pa" in stretch:
if "!" in stretch or overlap:
s += " .size = .cover,\n"
else:
s += " .size = .fit_cover1,\n"
elif "xy" in stretch:
s += " .size = .stretch,\n"
else:
s += " .size_horizontal = .fit,\n"
s += " .size_vertical = .fit,\n"
print(f"Warning: Unknown stretch rule {stretch}")
# `^` indicates that scaling should fill
# the whole cell, not just the icon height.
# `^` indicates that scaling should use the
# full cell height, not just the icon height,
# even when the constraint width is 1
if "^" not in stretch:
s += " .height = .icon,\n"
# There are two cases where we want to limit the constraint width to 1:
# - If there's a `1` in the stretch mode string.
# - If the stretch mode is `xy` and there's not an explicit `2`.
if "1" in stretch or ("xy" in stretch and "2" not in stretch):
# - If the stretch mode is not `pa` and there's not an explicit `2`.
if "1" in stretch or ("pa" not in stretch and "2" not in stretch):
s += " .max_constraint_width = 1,\n"
if align is not None:
@@ -233,14 +235,14 @@ def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry)
if valign is not None:
s += f" .align_vertical = {valign},\n"
if group_width != 1.0:
s += f" .group_width = {group_width:.16f},\n"
if group_height != 1.0:
s += f" .group_height = {group_height:.16f},\n"
if group_x != 0.0:
s += f" .group_x = {group_x:.16f},\n"
if group_y != 0.0:
s += f" .group_y = {group_y:.16f},\n"
if relative_width != 1.0:
s += f" .relative_width = {relative_width:.16f},\n"
if relative_height != 1.0:
s += f" .relative_height = {relative_height:.16f},\n"
if relative_x != 0.0:
s += f" .relative_x = {relative_x:.16f},\n"
if relative_y != 0.0:
s += f" .relative_y = {relative_y:.16f},\n"
# `overlap` and `ypadding` are mutually exclusive,
# this is asserted in the nerd fonts patcher itself.
@@ -286,7 +288,7 @@ def generate_zig_switch_arms(
yMin = math.inf
xMax = -math.inf
yMax = -math.inf
individual_bounds: dict[int, tuple[int, int, int ,int]] = {}
individual_bounds: dict[int, tuple[int, int, int, int]] = {}
for cp in group:
if cp not in cmap:
continue
@@ -306,10 +308,10 @@ def generate_zig_switch_arms(
this_bounds = individual_bounds[cp]
this_width = this_bounds[2] - this_bounds[0]
this_height = this_bounds[3] - this_bounds[1]
entries[cp]["group_width"] = group_width / this_width
entries[cp]["group_height"] = group_height / this_height
entries[cp]["group_x"] = (this_bounds[0] - xMin) / group_width
entries[cp]["group_y"] = (this_bounds[1] - yMin) / group_height
entries[cp]["relative_width"] = this_width / group_width
entries[cp]["relative_height"] = this_height / group_height
entries[cp]["relative_x"] = (this_bounds[0] - xMin) / group_width
entries[cp]["relative_y"] = (this_bounds[1] - yMin) / group_height
del entries[0]
@@ -350,7 +352,7 @@ if __name__ == "__main__":
const Constraint = @import("face.zig").RenderOptions.Constraint;
/// Get the a constraints for the provided codepoint.
/// Get the constraints for the provided codepoint.
pub fn getConstraint(cp: u21) ?Constraint {
return switch (cp) {
""")