From 5360aeb8aab98cbf921107432e90ed8d4817be17 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Fri, 3 Oct 2025 22:20:40 -0700 Subject: [PATCH 1/2] Fix botched cherry-pick from #8990 --- src/font/face/coretext.zig | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/font/face/coretext.zig b/src/font/face/coretext.zig index bd1716a61..9e7bc4d5d 100644 --- a/src/font/face/coretext.zig +++ b/src/font/face/coretext.zig @@ -363,7 +363,11 @@ pub const Face = struct { // 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. - 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. x += (cell_width - metrics.face_width) / 2; } @@ -378,18 +382,6 @@ pub const Face = struct { 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") // 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 From 6bc60b6c643cf3036b553ce15b202d895d4b9308 Mon Sep 17 00:00:00 2001 From: Daniel Wennberg Date: Fri, 3 Oct 2025 21:51:25 -0700 Subject: [PATCH 2/2] Add comprehensive constraint tests --- src/font/face.zig | 193 +++++++++++++++++++++++++++++++++++++ src/font/face/freetype.zig | 37 ------- 2 files changed, 193 insertions(+), 37 deletions(-) diff --git a/src/font/face.zig b/src/font/face.zig index f660565fe..586de4ad6 100644 --- a/src/font/face.zig +++ b/src/font/face.zig @@ -507,3 +507,196 @@ test "Variation.Id: slnt should be 1936486004" { try testing.expectEqual(@as(u32, 1936486004), @as(u32, @bitCast(id))); 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), + ); + } +} diff --git a/src/font/face/freetype.zig b/src/font/face/freetype.zig index 259e91b8c..4958c48c8 100644 --- a/src/font/face/freetype.zig +++ b/src/font/face/freetype.zig @@ -1177,43 +1177,6 @@ test "color emoji" { const glyph_id = ft_font.glyphIndex('🥸').?; 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" {