From c47459b4a2887a8301927a92368f01da738743e3 Mon Sep 17 00:00:00 2001 From: Qwerasd Date: Mon, 7 Jul 2025 09:34:56 -0600 Subject: [PATCH] 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. --- src/font/Collection.zig | 2 ++ src/font/Metrics.zig | 19 +++++++++++- src/font/face.zig | 51 +++++++++++++++++++++---------- src/font/face/coretext.zig | 5 ++- src/font/face/freetype.zig | 6 ++-- src/font/nerd_font_attributes.zig | 6 ++-- src/font/nerd_font_codegen.py | 11 ++++--- 7 files changed, 71 insertions(+), 29 deletions(-) diff --git a/src/font/Collection.zig b/src/font/Collection.zig index 1d85d8a28..eb4349fb0 100644 --- a/src/font/Collection.zig +++ b/src/font/Collection.zig @@ -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); } diff --git a/src/font/Metrics.zig b/src/font/Metrics.zig index 097f600cb..89f6a507f 100644 --- a/src/font/Metrics.zig +++ b/src/font/Metrics.zig @@ -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, }; } diff --git a/src/font/face.zig b/src/font/face.zig index 245edcf4b..8c1171fb4 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -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; } diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index 00cc31b26..009b4b2b3 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -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, ); diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index ae3bd0968..f42868e5c 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -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, diff --git a/src/font/nerd_font_attributes.zig b/src/font/nerd_font_attributes.zig index 70920bb0a..e72c7a00e 100644 --- a/src/font/nerd_font_attributes.zig +++ b/src/font/nerd_font_attributes.zig @@ -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, }, diff --git a/src/font/nerd_font_codegen.py b/src/font/nerd_font_codegen.py index e74b2ead1..4087b9ac6 100644 --- a/src/font/nerd_font_codegen.py +++ b/src/font/nerd_font_codegen.py @@ -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`.