font: add icon height to nerd font constraints

Icons were often WAY too big before because they were filling the whole
cell in height, which isn't great lol. This commit adds an `icon_height`
metric which is used to constrain glyphs that shouldn't be the size of
the entire cell.
This commit is contained in:
Qwerasd
2025-07-07 09:34:56 -06:00
parent 23cc50b12c
commit c47459b4a2
7 changed files with 71 additions and 29 deletions

View File

@ -1072,6 +1072,7 @@ test "metrics" {
.overline_thickness = 1,
.box_thickness = 1,
.cursor_height = 17,
.icon_height = 11,
}, c.metrics);
// Resize should change metrics
@ -1088,6 +1089,7 @@ test "metrics" {
.overline_thickness = 2,
.box_thickness = 2,
.cursor_height = 34,
.icon_height = 23,
}, c.metrics);
}

View File

@ -35,6 +35,9 @@ cursor_thickness: u32 = 1,
/// The height in pixels of the cursor sprite.
cursor_height: u32,
/// The constraint height for nerd fonts icons.
icon_height: u32,
/// Minimum acceptable values for some fields to prevent modifiers
/// from being able to, for example, cause 0-thickness underlines.
const Minimums = struct {
@ -46,6 +49,7 @@ const Minimums = struct {
const box_thickness = 1;
const cursor_thickness = 1;
const cursor_height = 1;
const icon_height = 1;
};
/// Metrics extracted from a font face, based on
@ -129,7 +133,7 @@ pub fn calc(face: FaceMetrics) Metrics {
// 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.ascent - face.descent + face.line_gap);
const cell_height = @ceil(face.lineHeight());
// 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
@ -173,6 +177,17 @@ pub fn calc(face: FaceMetrics) Metrics {
(face.strikethrough_position orelse
ex_height * 0.5 + strikethrough_thickness * 0.5));
// 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);
var result: Metrics = .{
.cell_width = @intFromFloat(cell_width),
.cell_height = @intFromFloat(cell_height),
@ -185,6 +200,7 @@ 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),
};
// Ensure all metrics are within their allowable range.
@ -423,6 +439,7 @@ fn init() Metrics {
.overline_thickness = 0,
.box_thickness = 0,
.cursor_height = 0,
.icon_height = 0,
};
}

View File

@ -150,6 +150,9 @@ pub const RenderOptions = struct {
/// Maximum number of cells horizontally to use.
max_constraint_width: u2 = 2,
/// What to use as the height metric when constraining the glyph.
height: Height = .cell,
pub const Size = enum {
/// Don't change the size of this glyph.
none,
@ -176,6 +179,13 @@ pub const RenderOptions = struct {
center,
};
pub const Height = enum {
/// Use the full height of the cell for constraining this glyph.
cell,
/// Use the "icon height" from the grid metrics as the height.
icon,
};
/// The size and position of a glyph.
pub const GlyphSize = struct {
width: f64,
@ -189,35 +199,35 @@ pub const RenderOptions = struct {
pub fn constrain(
self: Constraint,
glyph: GlyphSize,
/// Width of one cell.
cell_width: f64,
/// Height of one cell.
cell_height: f64,
metrics: Metrics,
/// Number of cells horizontally available for this glyph.
constraint_width: u2,
) GlyphSize {
var g = glyph;
const available_width =
cell_width * @as(f64, @floatFromInt(
@min(
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,
});
const w = available_width -
self.pad_left * available_width -
self.pad_right * available_width;
const h = cell_height -
self.pad_top * cell_height -
self.pad_bottom * cell_height;
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 * cell_height;
g.y -= self.pad_bottom * available_height;
switch (self.size_horizontal) {
.none => {},
@ -319,7 +329,16 @@ pub const RenderOptions = struct {
// Re-add our padding before returning.
g.x += self.pad_left * available_width;
g.y += self.pad_bottom * cell_height;
g.y += self.pad_bottom * available_height;
// 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 g;
}

View File

@ -341,7 +341,7 @@ pub const Face = struct {
const metrics = opts.grid_metrics;
const cell_width: f64 = @floatFromInt(metrics.cell_width);
const cell_height: f64 = @floatFromInt(metrics.cell_height);
// const cell_height: f64 = @floatFromInt(metrics.cell_height);
const glyph_size = opts.constraint.constrain(
.{
@ -350,8 +350,7 @@ pub const Face = struct {
.x = rect.origin.x,
.y = rect.origin.y + @as(f64, @floatFromInt(metrics.cell_baseline)),
},
cell_width,
cell_height,
metrics,
opts.constraint_width,
);

View File

@ -395,7 +395,7 @@ pub const Face = struct {
const metrics = opts.grid_metrics;
const cell_width: f64 = @floatFromInt(metrics.cell_width);
const cell_height: f64 = @floatFromInt(metrics.cell_height);
// const cell_height: f64 = @floatFromInt(metrics.cell_height);
const glyph_x: f64 = f26dot6ToF64(glyph.*.metrics.horiBearingX);
const glyph_y: f64 = f26dot6ToF64(glyph.*.metrics.horiBearingY) - glyph_height;
@ -407,8 +407,7 @@ pub const Face = struct {
.x = glyph_x,
.y = glyph_y + @as(f64, @floatFromInt(metrics.cell_baseline)),
},
cell_width,
cell_height,
metrics,
opts.constraint_width,
);
@ -1058,6 +1057,7 @@ test "color emoji" {
.overline_thickness = 0,
.box_thickness = 0,
.cursor_height = 0,
.icon_height = 0,
}, .constraint_width = 2, .constraint = .{
.size_horizontal = .cover,
.size_vertical = .cover,

View File

@ -25,6 +25,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{
.size_horizontal = .cover,
.size_vertical = .fit,
.height = .icon,
.max_constraint_width = 1,
.align_horizontal = .center,
.align_vertical = .center,
@ -285,7 +286,7 @@ pub fn getConstraint(cp: u21) Constraint {
0xe0d0...0xe0d1,
=> .{
.size_horizontal = .cover,
.size_vertical = .cover,
.size_vertical = .fit,
.align_horizontal = .start,
.align_vertical = .center,
},
@ -294,7 +295,7 @@ pub fn getConstraint(cp: u21) Constraint {
0xe0d5,
=> .{
.size_horizontal = .cover,
.size_vertical = .cover,
.size_vertical = .fit,
.align_horizontal = .center,
.align_vertical = .center,
},
@ -362,6 +363,7 @@ pub fn getConstraint(cp: u21) Constraint {
=> .{
.size_horizontal = .fit,
.size_vertical = .fit,
.height = .icon,
.align_horizontal = .center,
.align_vertical = .center,
},

View File

@ -180,16 +180,19 @@ def emit_zig_entry_multikey(codepoints: list[int], attr: PatchSetAttributeEntry)
if "xy" in stretch:
s += " .size_horizontal = .stretch,\n"
s += " .size_vertical = .stretch,\n"
elif "!" in stretch:
elif "!" in stretch or "^" in stretch:
s += " .size_horizontal = .cover,\n"
s += " .size_vertical = .fit,\n"
elif "^" in stretch:
s += " .size_horizontal = .cover,\n"
s += " .size_vertical = .cover,\n"
else:
s += " .size_horizontal = .fit,\n"
s += " .size_vertical = .fit,\n"
# `^` indicates that scaling should fill
# the whole cell, not just the icon height.
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`.