Nerd Font Icon Height Constraint (#7850)

Nerd font icons were ***WAY*** too big depending on your font setup,
this is because we were always using the full cell height when the nerd
font patcher instead uses an "icon height" for most things. The patcher
calculates the icon height as two thirds of the font's cap height and
one third of the line height, but I've chosen to instead use 1.2 times
the cap height for more consistent results across fonts-- if the user
wants their icons bigger, they can use the `adjust-icon-height` metric
modifier (and they can also use it to make them smaller if they want
that for some reason).

I also adjusted the attributes to user horizontal cover + vertical fit
for `^` stretch modes (proportional scaling but scale up), which makes
it so that it never exceeds the cell size, since first it covers
horizontally and then scales down to fit vertically if necessary;
previously, if there were a particularly wide glyph that was scaled with
cover/cover it would exceed the available width and overflow in to
neighboring cells which wasn't good.
This commit is contained in:
Qwerasd
2025-07-07 10:25:53 -06:00
committed by GitHub
9 changed files with 85 additions and 37 deletions

View File

@ -396,6 +396,18 @@ pub const compatibility = std.StaticStringMap(
/// Thickness in pixels or percentage adjustment of box drawing characters.
/// See the notes about adjustments in `adjust-cell-width`.
@"adjust-box-thickness": ?MetricModifier = null,
/// Height in pixels or percentage adjustment of maximum height for nerd font icons.
///
/// Increasing this value will allow nerd font icons to be larger, but won't
/// necessarily force them to be. Decreasing this value will make nerd font
/// icons smaller.
///
/// The default value for the icon height is 1.2 times the height of capital
/// letters in your primary font, so something like -16.6% would make icons
/// roughly the same height as capital letters.
///
/// See the notes about adjustments in `adjust-cell-width`.
@"adjust-icon-height": ?MetricModifier = null,
/// The method to use for calculating the cell width of a grapheme cluster.
/// The default value is `unicode` which uses the Unicode standard to determine

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,9 +35,8 @@ cursor_thickness: u32 = 1,
/// The height in pixels of the cursor sprite.
cursor_height: u32,
/// 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 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.
@ -50,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
@ -133,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
@ -177,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),
@ -189,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.
@ -214,11 +226,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;
@ -432,6 +439,7 @@ fn init() Metrics {
.overline_thickness = 0,
.box_thickness = 0,
.cursor_height = 0,
.icon_height = 0,
};
}

View File

@ -449,6 +449,7 @@ pub const DerivedConfig = struct {
@"adjust-cursor-thickness": ?Metrics.Modifier,
@"adjust-cursor-height": ?Metrics.Modifier,
@"adjust-box-thickness": ?Metrics.Modifier,
@"adjust-icon-height": ?Metrics.Modifier,
@"freetype-load-flags": font.face.FreetypeLoadFlags,
/// Initialize a DerivedConfig. The config should be either a
@ -488,6 +489,7 @@ pub const DerivedConfig = struct {
.@"adjust-cursor-thickness" = config.@"adjust-cursor-thickness",
.@"adjust-cursor-height" = config.@"adjust-cursor-height",
.@"adjust-box-thickness" = config.@"adjust-box-thickness",
.@"adjust-icon-height" = config.@"adjust-icon-height",
.@"freetype-load-flags" = if (font.face.FreetypeLoadFlags != void) config.@"freetype-load-flags" else {},
// This must be last so the arena contains all our allocations
@ -634,6 +636,7 @@ pub const Key = struct {
if (config.@"adjust-cursor-thickness") |m| try set.put(alloc, .cursor_thickness, m);
if (config.@"adjust-cursor-height") |m| try set.put(alloc, .cursor_height, m);
if (config.@"adjust-box-thickness") |m| try set.put(alloc, .box_thickness, m);
if (config.@"adjust-icon-height") |m| try set.put(alloc, .icon_height, m);
break :set set;
};

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(
self.max_constraint_width,
constraint_width,
),
));
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`.