font: Add comprehensive constraint tests (#9023)

As promised in #8990.

I opted for hardcoded metrics and bounding boxes rather than actually
loading fonts and glyphs, both to avoid backend dependence and limit the
focus to the constraint calculations themselves, and because I wanted to
test a case that isn't exhibited by any of the fonts available in the
repo.

This also fixes an error from #8990, probably due to a botched
cherry-pick or rebase.
This commit is contained in:
Mitchell Hashimoto
2025-10-04 06:17:28 -07:00
committed by GitHub
3 changed files with 198 additions and 50 deletions

View File

@@ -507,3 +507,196 @@ test "Variation.Id: slnt should be 1936486004" {
try testing.expectEqual(@as(u32, 1936486004), @as(u32, @bitCast(id))); try testing.expectEqual(@as(u32, 1936486004), @as(u32, @bitCast(id)));
try testing.expectEqualStrings("slnt", &(id.str())); try testing.expectEqualStrings("slnt", &(id.str()));
} }
test "Constraints" {
const comparison = @import("../datastruct/comparison.zig");
const getConstraint = @import("nerd_font_attributes.zig").getConstraint;
// Hardcoded data matches metrics from CoreText at size 12 and DPI 96.
// Define grid metrics (matches font-family = JetBrains Mono)
const metrics: Metrics = .{
.cell_width = 10,
.cell_height = 22,
.cell_baseline = 5,
.underline_position = 19,
.underline_thickness = 1,
.strikethrough_position = 12,
.strikethrough_thickness = 1,
.overline_position = 0,
.overline_thickness = 1,
.box_thickness = 1,
.cursor_thickness = 1,
.cursor_height = 22,
.icon_height = 44.48 / 3.0,
.face_width = 9.6,
.face_height = 21.12,
.face_y = 0.2,
};
// ASCII (no constraint).
{
const constraint: RenderOptions.Constraint = .none;
// BBox of 'x' from JetBrains Mono.
const glyph_x: GlyphSize = .{
.width = 6.784,
.height = 15.28,
.x = 1.408,
.y = 4.84,
};
// Any constraint width: do nothing.
inline for (.{ 1, 2 }) |constraint_width| {
try comparison.expectApproxEqual(
glyph_x,
constraint.constrain(glyph_x, metrics, constraint_width),
);
}
}
// Symbol (same constraint as hardcoded in Renderer.addGlyph).
{
const constraint: RenderOptions.Constraint = .{ .size = .fit };
// BBox of '■' (0x25A0 black square) from Iosevka.
// NOTE: This glyph is designed to span two cells.
const glyph_25A0: GlyphSize = .{
.width = 10.272,
.height = 10.272,
.x = 2.864,
.y = 5.304,
};
// Constraint width 1: scale down and shift to fit a single cell.
try comparison.expectApproxEqual(
GlyphSize{
.width = metrics.face_width,
.height = metrics.face_width,
.x = 0,
.y = 5.64,
},
constraint.constrain(glyph_25A0, metrics, 1),
);
// Constraint width 2: do nothing.
try comparison.expectApproxEqual(
glyph_25A0,
constraint.constrain(glyph_25A0, metrics, 2),
);
}
// Emoji (same constraint as hardcoded in SharedGrid.renderGlyph).
{
const constraint: RenderOptions.Constraint = .{
.size = .cover,
.align_horizontal = .center,
.align_vertical = .center,
.pad_left = 0.025,
.pad_right = 0.025,
};
// BBox of '🥸' (0x1F978) from Apple Color Emoji.
const glyph_1F978: GlyphSize = .{
.width = 20,
.height = 20,
.x = 0.46,
.y = 1,
};
// Constraint width 2: scale to cover two cells with padding, center;
try comparison.expectApproxEqual(
GlyphSize{
.width = 18.72,
.height = 18.72,
.x = 0.44,
.y = 1.4,
},
constraint.constrain(glyph_1F978, metrics, 2),
);
}
// Nerd Font default.
{
const constraint = getConstraint(0xea61).?;
// Verify that this is the constraint we expect.
try std.testing.expectEqual(.fit_cover1, constraint.size);
try std.testing.expectEqual(.icon, constraint.height);
try std.testing.expectEqual(.center1, constraint.align_horizontal);
try std.testing.expectEqual(.center1, constraint.align_vertical);
// BBox of '' (0xEA61 nf-cod-lightbulb) from Symbols Only.
// NOTE: This icon is part of a group, so the
// constraint applies to a larger bounding box.
const glyph_EA61: GlyphSize = .{
.width = 9.015625,
.height = 13.015625,
.x = 3.015625,
.y = 3.76525,
};
// Constraint width 1: scale and shift group to fit a single cell.
try comparison.expectApproxEqual(
GlyphSize{
.width = 7.2125,
.height = 10.4125,
.x = 0.8125,
.y = 5.950695224719102,
},
constraint.constrain(glyph_EA61, metrics, 1),
);
// Constraint width 2: no scaling; left-align and vertically center group.
try comparison.expectApproxEqual(
GlyphSize{
.width = glyph_EA61.width,
.height = glyph_EA61.height,
.x = 1.015625,
.y = 4.7483690308988775,
},
constraint.constrain(glyph_EA61, metrics, 2),
);
}
// Nerd Font stretch.
{
const constraint = getConstraint(0xe0c0).?;
// Verify that this is the constraint we expect.
try std.testing.expectEqual(.stretch, constraint.size);
try std.testing.expectEqual(.cell, constraint.height);
try std.testing.expectEqual(.start, constraint.align_horizontal);
try std.testing.expectEqual(.center1, constraint.align_vertical);
// BBox of ' ' (0xE0C0 nf-ple-flame_thick) from Symbols Only.
const glyph_E0C0: GlyphSize = .{
.width = 16.796875,
.height = 16.46875,
.x = -0.796875,
.y = 1.7109375,
};
// Constraint width 1: stretch and position to exactly cover one cell.
try comparison.expectApproxEqual(
GlyphSize{
.width = @floatFromInt(metrics.cell_width),
.height = @floatFromInt(metrics.cell_height),
.x = 0,
.y = 0,
},
constraint.constrain(glyph_E0C0, metrics, 1),
);
// Constraint width 1: stretch and position to exactly cover two cells.
try comparison.expectApproxEqual(
GlyphSize{
.width = @floatFromInt(2 * metrics.cell_width),
.height = @floatFromInt(metrics.cell_height),
.x = 0,
.y = 0,
},
constraint.constrain(glyph_E0C0, metrics, 2),
);
}
}

View File

@@ -363,7 +363,11 @@ pub const Face = struct {
// We center all glyphs within the pixel-rounded and adjusted // We center all glyphs within the pixel-rounded and adjusted
// cell width if it's larger than the face width, so that they // cell width if it's larger than the face width, so that they
// aren't weirdly off to the left. // aren't weirdly off to the left.
if (metrics.face_width < cell_width) { //
// We don't do this if the glyph has a stretch constraint,
// since in that case the position was already calculated with the
// new cell width in mind.
if ((constraint.size != .stretch) and (metrics.face_width < cell_width)) {
// We add half the difference to re-center. // We add half the difference to re-center.
x += (cell_width - metrics.face_width) / 2; x += (cell_width - metrics.face_width) / 2;
} }
@@ -378,18 +382,6 @@ pub const Face = struct {
y = @round(y); y = @round(y);
} }
// 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 glyph has a stretch constraint,
// since in that case the position was already calculated with the
// new cell width in mind.
if ((constraint.size != .stretch) and (metrics.face_width < cell_width)) {
// We add half the difference to re-center.
x += (cell_width - metrics.face_width) / 2;
}
// We make an assumption that font smoothing ("thicken") // We make an assumption that font smoothing ("thicken")
// adds no more than 1 extra pixel to any edge. We don't // adds no more than 1 extra pixel to any edge. We don't
// add extra size if it's a sbix color font though, since // add extra size if it's a sbix color font though, since

View File

@@ -1177,43 +1177,6 @@ test "color emoji" {
const glyph_id = ft_font.glyphIndex('🥸').?; const glyph_id = ft_font.glyphIndex('🥸').?;
try testing.expect(ft_font.isColorGlyph(glyph_id)); try testing.expect(ft_font.isColorGlyph(glyph_id));
} }
// resize
// TODO: Comprehensive tests for constraints,
// this is just an adapted legacy test.
{
const glyph = try ft_font.renderGlyph(
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,
.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);
}
} }
test "mono to bgra" { test "mono to bgra" {