font: reshuffle glyph sizing types to Glyph.zig

This commit is contained in:
Mitchell Hashimoto
2026-06-05 06:48:37 -07:00
parent d271b271f9
commit 1c0aac54bd
13 changed files with 648 additions and 639 deletions

View File

@@ -24,7 +24,7 @@ const Face = font.Face;
const Glyph = font.Glyph;
const Library = font.Library;
const Presentation = font.Presentation;
const RenderOptions = font.face.RenderOptions;
const RenderOptions = font.Glyph.RenderOptions;
const SpriteFace = font.SpriteFace;
const Style = font.Style;

View File

@@ -1,6 +1,9 @@
//! Glyph is a single loaded glyph for a face.
const Glyph = @This();
const std = @import("std");
const Metrics = @import("Metrics.zig");
/// width of glyph in pixels
width: u32,
@@ -17,3 +20,611 @@ offset_y: i32,
/// be normalized to be between 0 and 1 prior to use in shaders.
atlas_x: u32,
atlas_y: u32,
/// The size and position of a glyph.
pub const Size = struct {
width: f64,
height: f64,
x: f64,
y: f64,
};
/// Metrics describing the authored glyph coordinate space.
pub const DesignMetrics = struct {
/// Units-per-em for outline/design coordinates.
units_per_em: u32,
/// Authored advance width in design units.
advance_width: u32,
/// Authored line height in design units.
line_height: u32,
};
/// Additional options for rendering glyphs.
pub const RenderOptions = struct {
/// The metrics that are defining the grid layout. These are usually
/// the metrics of the primary font face. The grid metrics are used
/// by the font face to better layout the glyph in situations where
/// the font is not exactly the same size as the grid.
grid_metrics: Metrics,
/// The number of grid cells this glyph will take up. This can be used
/// optionally by the rasterizer to better layout the glyph.
cell_width: ?u2 = null,
/// Constraint and alignment properties for the glyph. The rasterizer
/// should call the `constrain` function on this with the original size
/// and bearings of the glyph to get remapped values that the glyph
/// should be scaled/moved to.
constraint: Constraint = .none,
/// The number of cells, horizontally that the glyph is free to take up
/// when resized and aligned by `constraint`. This is usually 1, but if
/// there's whitespace to the right of the cell then it can be 2.
constraint_width: u2 = 1,
/// Thicken the glyph. This draws the glyph with a thicker stroke width.
/// This is purely an aesthetic setting.
///
/// This only works with CoreText currently.
thicken: bool = false,
/// "Strength" of the thickening, between `0` and `255`.
/// Only has an effect when `thicken` is enabled.
///
/// `0` does not correspond to *no* thickening,
/// just the *lightest* thickening available.
///
/// CoreText only.
thicken_strength: u8 = 255,
/// See the `constraint` field.
pub const Constraint = struct {
/// Don't constrain the glyph in any way.
pub const none: Constraint = .{};
/// Sizing rule.
size: Constraint.Size = .none,
/// Vertical alignment rule.
align_vertical: Align = .none,
/// Horizontal alignment rule.
align_horizontal: Align = .none,
/// Top padding when resizing.
pad_top: f64 = 0.0,
/// Left padding when resizing.
pad_left: f64 = 0.0,
/// Right padding when resizing.
pad_right: f64 = 0.0,
/// Bottom padding when resizing.
pad_bottom: f64 = 0.0,
// Size and bearings of the glyph relative
// to the bounding box of its scale group.
relative_width: f64 = 1.0,
relative_height: f64 = 1.0,
relative_x: f64 = 0.0,
relative_y: f64 = 0.0,
/// Maximum aspect ratio (width/height) to allow when stretching.
max_xy_ratio: ?f64 = null,
/// Maximum number of cells horizontally to use.
max_constraint_width: u2 = 2,
/// What to use as the height metric when constraining the glyph and
/// the constraint width is 1,
height: Height = .cell,
pub const Size = enum {
/// Don't change the size of this glyph.
none,
/// Scale the glyph down if needed to fit within the bounds,
/// preserving aspect ratio.
fit,
/// Scale the glyph up or down to exactly match the bounds,
/// preserving aspect ratio.
cover,
/// Scale the glyph down if needed to fit within the bounds,
/// preserving aspect ratio. If the glyph doesn't cover a
/// single cell, scale up. If the glyph exceeds a single
/// cell but is within the bounds, do nothing.
/// (Nerd Font specific rule.)
fit_cover1,
/// Stretch the glyph to exactly fit the bounds in both
/// directions, disregarding aspect ratio.
stretch,
};
pub const Align = enum {
/// Don't move the glyph on this axis.
none,
/// Move the glyph so that its leading (bottom/left)
/// edge aligns with the leading edge of the axis.
start,
/// Move the glyph so that its trailing (top/right)
/// edge aligns with the trailing edge of the axis.
end,
/// Move the glyph so that it is centered on this axis.
center,
/// Move the glyph so that it is centered on this axis,
/// but always with respect to the first cell even for
/// multi-cell constraints. (Nerd Font specific rule.)
center1,
};
pub const Height = enum {
/// Use the full line height of the primary face for
/// constraining this glyph.
cell,
/// Use the icon height from the grid metrics for
/// constraining this glyph. Unlike `cell`, the value of
/// this height depends on both the constraint width and the
/// affected by the `adjust-icon-height` config option.
icon,
};
/// Returns true if the constraint does anything. If it doesn't,
/// because it neither sizes nor positions the glyph, then this
/// returns false.
pub inline fn doesAnything(self: Constraint) bool {
return self.size != .none or
self.align_horizontal != .none or
self.align_vertical != .none;
}
/// Apply this constraint to the provided glyph
/// size, given the available width and height.
pub fn constrain(
self: Constraint,
glyph: Glyph.Size,
metrics: Metrics,
/// Number of cells horizontally available for this glyph.
constraint_width: u2,
) Glyph.Size {
if (!self.doesAnything()) return glyph;
switch (self.size) {
.stretch => {
// Stretched glyphs are usually meant to align across cell
// boundaries, which works best if they're scaled and
// aligned to the grid rather than the face. This is most
// easily done by inserting this little fib in the metrics.
var m = metrics;
m.face_width = @floatFromInt(m.cell_width);
m.face_height = @floatFromInt(m.cell_height);
m.face_y = 0.0;
// Negative padding for stretched glyphs is a band-aid to
// avoid gaps due to pixel rounding, but at the cost of
// unsightly overlap artifacts. Since we scale and align to
// the grid rather than the face, we don't need it.
var c = self;
c.pad_bottom = @max(0, c.pad_bottom);
c.pad_top = @max(0, c.pad_top);
c.pad_left = @max(0, c.pad_left);
c.pad_right = @max(0, c.pad_right);
return c.constrainInner(glyph, m, constraint_width);
},
else => return self.constrainInner(glyph, metrics, constraint_width),
}
}
fn constrainInner(
self: Constraint,
glyph: Glyph.Size,
metrics: Metrics,
constraint_width: u2,
) Glyph.Size {
// For extra wide font faces, never stretch glyphs across two cells.
// This mirrors font_patcher.
const min_constraint_width: u2 = if ((self.size == .stretch) and (metrics.face_width > 0.9 * metrics.face_height))
1
else
@min(self.max_constraint_width, constraint_width);
// The bounding box for the glyph's scale group.
// Scaling and alignment rules are calculated for
// this box and then applied to the glyph.
var group: Glyph.Size = group: {
const group_width = glyph.width / self.relative_width;
const group_height = glyph.height / self.relative_height;
break :group .{
.width = group_width,
.height = group_height,
.x = glyph.x - (group_width * self.relative_x),
.y = glyph.y - (group_height * self.relative_y),
};
};
// Apply prescribed scaling, preserving the
// center bearings of the group bounding box
const width_factor, const height_factor = self.scale_factors(group, metrics, min_constraint_width);
const center_x = group.x + (group.width / 2);
const center_y = group.y + (group.height / 2);
group.width *= width_factor;
group.height *= height_factor;
group.x = center_x - (group.width / 2);
group.y = center_y - (group.height / 2);
// NOTE: font_patcher jumps through a lot of hoops at this
// point to ensure that the glyph remains within the target
// bounding box after rounding to font definition units.
// This is irrelevant here as we're not rounding, we're
// staying in f64 and heading straight to rendering.
// Apply prescribed alignment
group.y = self.aligned_y(group, metrics);
group.x = self.aligned_x(group, metrics, min_constraint_width);
// Transfer the scaling and alignment back to the glyph and return.
return .{
.width = width_factor * glyph.width,
.height = height_factor * glyph.height,
.x = group.x + (group.width * self.relative_x),
.y = group.y + (group.height * self.relative_y),
};
}
/// Return width and height scaling factors for this scaling group.
fn scale_factors(
self: Constraint,
group: Glyph.Size,
metrics: Metrics,
min_constraint_width: u2,
) struct { f64, f64 } {
if (self.size == .none) {
return .{ 1.0, 1.0 };
}
const multi_cell = (min_constraint_width > 1);
const pad_width_factor = @as(f64, @floatFromInt(min_constraint_width)) - (self.pad_left + self.pad_right);
const pad_height_factor = 1 - (self.pad_bottom + self.pad_top);
const target_width = pad_width_factor * metrics.face_width;
const target_height = pad_height_factor * switch (self.height) {
.cell => metrics.face_height,
// Like font-patcher, the icon constraint height depends on the
// constraint width. Unlike font-patcher, the multi-cell
// icon_height may be different from face_height due to the
// `adjust-icon-height` config option.
.icon => if (multi_cell)
metrics.icon_height
else
metrics.icon_height_single,
};
var width_factor = target_width / group.width;
var height_factor = target_height / group.height;
switch (self.size) {
.none => unreachable,
.fit => {
// Scale down to fit if needed
height_factor = @min(1, width_factor, height_factor);
width_factor = height_factor;
},
.cover => {
// Scale to cover
height_factor = @min(width_factor, height_factor);
width_factor = height_factor;
},
.fit_cover1 => {
// Scale down to fit or up to cover at least one cell
// NOTE: This is similar to font_patcher's "pa" mode,
// however, font_patcher will only do the upscaling
// part if the constraint width is 1, resulting in
// some icons becoming smaller when the constraint
// width increases. You'd see icons shrinking when
// opening up a space after them. This makes no
// sense, so we've fixed the rule such that these
// icons are scaled to the same size for multi-cell
// constraints as they would be for single-cell.
height_factor = @min(width_factor, height_factor);
if (multi_cell and (height_factor > 1)) {
// Call back into this function with
// constraint width 1 to get single-cell scale
// factors. We use the height factor as width
// could have been modified by max_xy_ratio.
_, const single_height_factor = self.scale_factors(group, metrics, 1);
height_factor = @max(1, single_height_factor);
}
width_factor = height_factor;
},
.stretch => {},
}
// Reduce aspect ratio if required
if (self.max_xy_ratio) |ratio| {
if (group.width * width_factor > group.height * height_factor * ratio) {
width_factor = group.height * height_factor * ratio / group.width;
}
}
return .{ width_factor, height_factor };
}
/// Return vertical bearing for aligning this group
fn aligned_y(
self: Constraint,
group: Glyph.Size,
metrics: Metrics,
) f64 {
if ((self.size == .none) and (self.align_vertical == .none)) {
// If we don't have any constraints affecting the vertical axis,
// we don't touch vertical alignment.
return group.y;
}
// We use face_height and offset by face_y, rather than
// using cell_height directly, to account for the asymmetry
// of the pixel cell around the face (a consequence of
// aligning the baseline with a pixel boundary rather than
// vertically centering the face).
const pad_bottom_dy = self.pad_bottom * metrics.face_height;
const pad_top_dy = self.pad_top * metrics.face_height;
const start_y = metrics.face_y + pad_bottom_dy;
const end_y = metrics.face_y + (metrics.face_height - group.height - pad_top_dy);
const center_y = (start_y + end_y) / 2;
return switch (self.align_vertical) {
// NOTE: Even if there is no prescribed alignment, we ensure
// that the group doesn't protrude outside the padded cell,
// since this is implied by every available size constraint. If
// the group is too high we fall back to centering, though if we
// hit the .none prong we always have self.size != .none, so
// this should never happen.
.none => if (end_y < start_y)
center_y
else
@max(start_y, @min(group.y, end_y)),
.start => start_y,
.end => end_y,
.center, .center1 => center_y,
};
}
/// Return horizontal bearing for aligning this group
fn aligned_x(
self: Constraint,
group: Glyph.Size,
metrics: Metrics,
min_constraint_width: u2,
) f64 {
if ((self.size == .none) and (self.align_horizontal == .none)) {
// If we don't have any constraints affecting the horizontal
// axis, we don't touch horizontal alignment.
return group.x;
}
// For multi-cell constraints, we align relative to the span
// from the left edge of the first cell to the right edge of
// the last face cell assuming it's left-aligned within the
// rounded and adjusted pixel cell. Any horizontal offset to
// center the face within the grid cell is the responsibility
// of the backend-specific rendering code, and should be done
// after applying constraints.
const full_face_span = metrics.face_width + @as(f64, @floatFromInt((min_constraint_width - 1) * metrics.cell_width));
const pad_left_dx = self.pad_left * metrics.face_width;
const pad_right_dx = self.pad_right * metrics.face_width;
const start_x = pad_left_dx;
const end_x = full_face_span - group.width - pad_right_dx;
return switch (self.align_horizontal) {
// NOTE: Even if there is no prescribed alignment, we ensure
// that the glyph doesn't protrude outside the padded cell,
// since this is implied by every available size constraint. The
// left-side bound has priority if the group is too wide, though
// if we hit the .none prong we always have self.size != .none,
// so this should never happen.
.none => @max(start_x, @min(group.x, end_x)),
.start => start_x,
.end => @max(start_x, end_x),
.center => @max(start_x, (start_x + end_x) / 2),
// NOTE: .center1 implements the font_patcher rule of centering
// in the first cell even for multi-cell constraints. Since glyphs
// are not allowed to protrude to the left, this results in the
// left-alignment like .start when the glyph is wider than a cell.
.center1 => center1: {
const end1_x = metrics.face_width - group.width - pad_right_dx;
break :center1 @max(start_x, (start_x + end1_x) / 2);
},
};
}
};
};
test "Constraints" {
const comparison = @import("../datastruct/comparison.zig");
const getConstraint = @import("nerd_font_attributes.zig").getConstraint;
const GlyphSize = Size;
// 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 = 21.12,
.icon_height_single = 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

@@ -560,6 +560,8 @@ pub const Modifier = union(enum) {
}
test "formatConfig percent" {
if (comptime @import("terminal_options").artifact == .lib) return;
const configpkg = @import("../config.zig");
const testing = std.testing;
var buf: std.Io.Writer.Allocating = .init(testing.allocator);
@@ -571,6 +573,8 @@ pub const Modifier = union(enum) {
}
test "formatConfig absolute" {
if (comptime @import("terminal_options").artifact == .lib) return;
const configpkg = @import("../config.zig");
const testing = std.testing;
var buf: std.Io.Writer.Allocating = .init(testing.allocator);

View File

@@ -33,7 +33,7 @@ const Library = font.Library;
const Metrics = font.Metrics;
const Presentation = font.Presentation;
const Style = font.Style;
const RenderOptions = font.face.RenderOptions;
const RenderOptions = font.Glyph.RenderOptions;
const log = std.log.scoped(.font_shared_grid);

View File

@@ -2,7 +2,6 @@ const std = @import("std");
const builtin = @import("builtin");
const build_config = @import("../build_config.zig");
const options = @import("main.zig").options;
const Metrics = @import("main.zig").Metrics;
const config = @import("../config.zig");
const freetype = @import("face/freetype.zig");
const coretext = @import("face/coretext.zig");
@@ -94,407 +93,6 @@ pub const Variation = struct {
};
};
/// The size and position of a glyph.
pub const GlyphSize = struct {
width: f64,
height: f64,
x: f64,
y: f64,
};
/// Additional options for rendering glyphs.
pub const RenderOptions = struct {
/// The metrics that are defining the grid layout. These are usually
/// the metrics of the primary font face. The grid metrics are used
/// by the font face to better layout the glyph in situations where
/// the font is not exactly the same size as the grid.
grid_metrics: Metrics,
/// The number of grid cells this glyph will take up. This can be used
/// optionally by the rasterizer to better layout the glyph.
cell_width: ?u2 = null,
/// Constraint and alignment properties for the glyph. The rasterizer
/// should call the `constrain` function on this with the original size
/// and bearings of the glyph to get remapped values that the glyph
/// should be scaled/moved to.
constraint: Constraint = .none,
/// The number of cells, horizontally that the glyph is free to take up
/// when resized and aligned by `constraint`. This is usually 1, but if
/// there's whitespace to the right of the cell then it can be 2.
constraint_width: u2 = 1,
/// Thicken the glyph. This draws the glyph with a thicker stroke width.
/// This is purely an aesthetic setting.
///
/// This only works with CoreText currently.
thicken: bool = false,
/// "Strength" of the thickening, between `0` and `255`.
/// Only has an effect when `thicken` is enabled.
///
/// `0` does not correspond to *no* thickening,
/// just the *lightest* thickening available.
///
/// CoreText only.
thicken_strength: u8 = 255,
/// See the `constraint` field.
pub const Constraint = struct {
/// Don't constrain the glyph in any way.
pub const none: Constraint = .{};
/// Sizing rule.
size: Size = .none,
/// Vertical alignment rule.
align_vertical: Align = .none,
/// Horizontal alignment rule.
align_horizontal: Align = .none,
/// Top padding when resizing.
pad_top: f64 = 0.0,
/// Left padding when resizing.
pad_left: f64 = 0.0,
/// Right padding when resizing.
pad_right: f64 = 0.0,
/// Bottom padding when resizing.
pad_bottom: f64 = 0.0,
// Size and bearings of the glyph relative
// to the bounding box of its scale group.
relative_width: f64 = 1.0,
relative_height: f64 = 1.0,
relative_x: f64 = 0.0,
relative_y: f64 = 0.0,
/// Maximum aspect ratio (width/height) to allow when stretching.
max_xy_ratio: ?f64 = null,
/// Maximum number of cells horizontally to use.
max_constraint_width: u2 = 2,
/// What to use as the height metric when constraining the glyph and
/// the constraint width is 1,
height: Height = .cell,
pub const Size = enum {
/// Don't change the size of this glyph.
none,
/// Scale the glyph down if needed to fit within the bounds,
/// preserving aspect ratio.
fit,
/// Scale the glyph up or down to exactly match the bounds,
/// preserving aspect ratio.
cover,
/// Scale the glyph down if needed to fit within the bounds,
/// preserving aspect ratio. If the glyph doesn't cover a
/// single cell, scale up. If the glyph exceeds a single
/// cell but is within the bounds, do nothing.
/// (Nerd Font specific rule.)
fit_cover1,
/// Stretch the glyph to exactly fit the bounds in both
/// directions, disregarding aspect ratio.
stretch,
};
pub const Align = enum {
/// Don't move the glyph on this axis.
none,
/// Move the glyph so that its leading (bottom/left)
/// edge aligns with the leading edge of the axis.
start,
/// Move the glyph so that its trailing (top/right)
/// edge aligns with the trailing edge of the axis.
end,
/// Move the glyph so that it is centered on this axis.
center,
/// Move the glyph so that it is centered on this axis,
/// but always with respect to the first cell even for
/// multi-cell constraints. (Nerd Font specific rule.)
center1,
};
pub const Height = enum {
/// Use the full line height of the primary face for
/// constraining this glyph.
cell,
/// Use the icon height from the grid metrics for
/// constraining this glyph. Unlike `cell`, the value of
/// this height depends on both the constraint width and the
/// affected by the `adjust-icon-height` config option.
icon,
};
/// Returns true if the constraint does anything. If it doesn't,
/// because it neither sizes nor positions the glyph, then this
/// returns false.
pub inline fn doesAnything(self: Constraint) bool {
return self.size != .none or
self.align_horizontal != .none or
self.align_vertical != .none;
}
/// Apply this constraint to the provided glyph
/// size, given the available width and height.
pub fn constrain(
self: Constraint,
glyph: GlyphSize,
metrics: Metrics,
/// Number of cells horizontally available for this glyph.
constraint_width: u2,
) GlyphSize {
if (!self.doesAnything()) return glyph;
switch (self.size) {
.stretch => {
// Stretched glyphs are usually meant to align across cell
// boundaries, which works best if they're scaled and
// aligned to the grid rather than the face. This is most
// easily done by inserting this little fib in the metrics.
var m = metrics;
m.face_width = @floatFromInt(m.cell_width);
m.face_height = @floatFromInt(m.cell_height);
m.face_y = 0.0;
// Negative padding for stretched glyphs is a band-aid to
// avoid gaps due to pixel rounding, but at the cost of
// unsightly overlap artifacts. Since we scale and align to
// the grid rather than the face, we don't need it.
var c = self;
c.pad_bottom = @max(0, c.pad_bottom);
c.pad_top = @max(0, c.pad_top);
c.pad_left = @max(0, c.pad_left);
c.pad_right = @max(0, c.pad_right);
return c.constrainInner(glyph, m, constraint_width);
},
else => return self.constrainInner(glyph, metrics, constraint_width),
}
}
fn constrainInner(
self: Constraint,
glyph: GlyphSize,
metrics: Metrics,
constraint_width: u2,
) GlyphSize {
// For extra wide font faces, never stretch glyphs across two cells.
// This mirrors font_patcher.
const min_constraint_width: u2 = if ((self.size == .stretch) and (metrics.face_width > 0.9 * metrics.face_height))
1
else
@min(self.max_constraint_width, constraint_width);
// The bounding box for the glyph's scale group.
// Scaling and alignment rules are calculated for
// this box and then applied to the glyph.
var group: GlyphSize = group: {
const group_width = glyph.width / self.relative_width;
const group_height = glyph.height / self.relative_height;
break :group .{
.width = group_width,
.height = group_height,
.x = glyph.x - (group_width * self.relative_x),
.y = glyph.y - (group_height * self.relative_y),
};
};
// Apply prescribed scaling, preserving the
// center bearings of the group bounding box
const width_factor, const height_factor = self.scale_factors(group, metrics, min_constraint_width);
const center_x = group.x + (group.width / 2);
const center_y = group.y + (group.height / 2);
group.width *= width_factor;
group.height *= height_factor;
group.x = center_x - (group.width / 2);
group.y = center_y - (group.height / 2);
// NOTE: font_patcher jumps through a lot of hoops at this
// point to ensure that the glyph remains within the target
// bounding box after rounding to font definition units.
// This is irrelevant here as we're not rounding, we're
// staying in f64 and heading straight to rendering.
// Apply prescribed alignment
group.y = self.aligned_y(group, metrics);
group.x = self.aligned_x(group, metrics, min_constraint_width);
// Transfer the scaling and alignment back to the glyph and return.
return .{
.width = width_factor * glyph.width,
.height = height_factor * glyph.height,
.x = group.x + (group.width * self.relative_x),
.y = group.y + (group.height * self.relative_y),
};
}
/// Return width and height scaling factors for this scaling group.
fn scale_factors(
self: Constraint,
group: GlyphSize,
metrics: Metrics,
min_constraint_width: u2,
) struct { f64, f64 } {
if (self.size == .none) {
return .{ 1.0, 1.0 };
}
const multi_cell = (min_constraint_width > 1);
const pad_width_factor = @as(f64, @floatFromInt(min_constraint_width)) - (self.pad_left + self.pad_right);
const pad_height_factor = 1 - (self.pad_bottom + self.pad_top);
const target_width = pad_width_factor * metrics.face_width;
const target_height = pad_height_factor * switch (self.height) {
.cell => metrics.face_height,
// Like font-patcher, the icon constraint height depends on the
// constraint width. Unlike font-patcher, the multi-cell
// icon_height may be different from face_height due to the
// `adjust-icon-height` config option.
.icon => if (multi_cell)
metrics.icon_height
else
metrics.icon_height_single,
};
var width_factor = target_width / group.width;
var height_factor = target_height / group.height;
switch (self.size) {
.none => unreachable,
.fit => {
// Scale down to fit if needed
height_factor = @min(1, width_factor, height_factor);
width_factor = height_factor;
},
.cover => {
// Scale to cover
height_factor = @min(width_factor, height_factor);
width_factor = height_factor;
},
.fit_cover1 => {
// Scale down to fit or up to cover at least one cell
// NOTE: This is similar to font_patcher's "pa" mode,
// however, font_patcher will only do the upscaling
// part if the constraint width is 1, resulting in
// some icons becoming smaller when the constraint
// width increases. You'd see icons shrinking when
// opening up a space after them. This makes no
// sense, so we've fixed the rule such that these
// icons are scaled to the same size for multi-cell
// constraints as they would be for single-cell.
height_factor = @min(width_factor, height_factor);
if (multi_cell and (height_factor > 1)) {
// Call back into this function with
// constraint width 1 to get single-cell scale
// factors. We use the height factor as width
// could have been modified by max_xy_ratio.
_, const single_height_factor = self.scale_factors(group, metrics, 1);
height_factor = @max(1, single_height_factor);
}
width_factor = height_factor;
},
.stretch => {},
}
// Reduce aspect ratio if required
if (self.max_xy_ratio) |ratio| {
if (group.width * width_factor > group.height * height_factor * ratio) {
width_factor = group.height * height_factor * ratio / group.width;
}
}
return .{ width_factor, height_factor };
}
/// Return vertical bearing for aligning this group
fn aligned_y(
self: Constraint,
group: GlyphSize,
metrics: Metrics,
) f64 {
if ((self.size == .none) and (self.align_vertical == .none)) {
// If we don't have any constraints affecting the vertical axis,
// we don't touch vertical alignment.
return group.y;
}
// We use face_height and offset by face_y, rather than
// using cell_height directly, to account for the asymmetry
// of the pixel cell around the face (a consequence of
// aligning the baseline with a pixel boundary rather than
// vertically centering the face).
const pad_bottom_dy = self.pad_bottom * metrics.face_height;
const pad_top_dy = self.pad_top * metrics.face_height;
const start_y = metrics.face_y + pad_bottom_dy;
const end_y = metrics.face_y + (metrics.face_height - group.height - pad_top_dy);
const center_y = (start_y + end_y) / 2;
return switch (self.align_vertical) {
// NOTE: Even if there is no prescribed alignment, we ensure
// that the group doesn't protrude outside the padded cell,
// since this is implied by every available size constraint. If
// the group is too high we fall back to centering, though if we
// hit the .none prong we always have self.size != .none, so
// this should never happen.
.none => if (end_y < start_y)
center_y
else
@max(start_y, @min(group.y, end_y)),
.start => start_y,
.end => end_y,
.center, .center1 => center_y,
};
}
/// Return horizontal bearing for aligning this group
fn aligned_x(
self: Constraint,
group: GlyphSize,
metrics: Metrics,
min_constraint_width: u2,
) f64 {
if ((self.size == .none) and (self.align_horizontal == .none)) {
// If we don't have any constraints affecting the horizontal
// axis, we don't touch horizontal alignment.
return group.x;
}
// For multi-cell constraints, we align relative to the span
// from the left edge of the first cell to the right edge of
// the last face cell assuming it's left-aligned within the
// rounded and adjusted pixel cell. Any horizontal offset to
// center the face within the grid cell is the responsibility
// of the backend-specific rendering code, and should be done
// after applying constraints.
const full_face_span = metrics.face_width + @as(f64, @floatFromInt((min_constraint_width - 1) * metrics.cell_width));
const pad_left_dx = self.pad_left * metrics.face_width;
const pad_right_dx = self.pad_right * metrics.face_width;
const start_x = pad_left_dx;
const end_x = full_face_span - group.width - pad_right_dx;
return switch (self.align_horizontal) {
// NOTE: Even if there is no prescribed alignment, we ensure
// that the glyph doesn't protrude outside the padded cell,
// since this is implied by every available size constraint. The
// left-side bound has priority if the group is too wide, though
// if we hit the .none prong we always have self.size != .none,
// so this should never happen.
.none => @max(start_x, @min(group.x, end_x)),
.start => start_x,
.end => @max(start_x, end_x),
.center => @max(start_x, (start_x + end_x) / 2),
// NOTE: .center1 implements the font_patcher rule of centering
// in the first cell even for multi-cell constraints. Since glyphs
// are not allowed to protrude to the left, this results in the
// left-alignment like .start when the glyph is wider than a cell.
.center1 => center1: {
const end1_x = metrics.face_width - group.width - pad_right_dx;
break :center1 @max(start_x, (start_x + end1_x) / 2);
},
};
}
};
};
test {
@import("std").testing.refAllDecls(@This());
}
@@ -512,197 +110,3 @@ 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 = 21.12,
.icon_height_single = 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

@@ -291,7 +291,7 @@ pub const Face = struct {
alloc: Allocator,
atlas: *font.Atlas,
glyph_index: u32,
opts: font.face.RenderOptions,
opts: font.Glyph.RenderOptions,
) !font.Glyph {
var glyphs = [_]macos.graphics.Glyph{@intCast(glyph_index)};

View File

@@ -393,7 +393,7 @@ pub const Face = struct {
}
/// Get a rect that represents the position and size of the loaded glyph.
fn getGlyphSize(glyph: freetype.c.FT_GlyphSlot) font.face.GlyphSize {
fn getGlyphSize(glyph: freetype.c.FT_GlyphSlot) font.Glyph.Size {
// If we're dealing with an outline glyph then we get the
// outline's bounding box instead of using the built-in
// metrics, since that's more precise and allows better
@@ -427,7 +427,7 @@ pub const Face = struct {
alloc: Allocator,
atlas: *font.Atlas,
glyph_index: u32,
opts: font.face.RenderOptions,
opts: font.Glyph.RenderOptions,
) !Glyph {
self.ft_mutex.lock();
defer self.ft_mutex.unlock();

View File

@@ -189,7 +189,7 @@ pub const Face = struct {
alloc: Allocator,
atlas: *font.Atlas,
glyph_index: u32,
opts: font.face.RenderOptions,
opts: font.Glyph.RenderOptions,
) !font.Glyph {
_ = opts;

View File

@@ -9,21 +9,10 @@ const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const z2d = @import("z2d");
const face = @import("face.zig");
const Glyph = @import("Glyph.zig");
const glyf = @import("opentype/glyf.zig");
/// Metrics describing the authored glyf coordinate space, since
/// a glyf table doesn't contain this on its own.
pub const DesignMetrics = struct {
/// Units-per-em for outline/design coordinates.
units_per_em: u32,
/// Authored advance width in design units.
advance_width: u32,
/// Authored line height in design units.
line_height: u32,
};
const DesignMetrics = Glyph.DesignMetrics;
/// An owned, tightly packed alpha8 bitmap.
pub const Bitmap = struct {
@@ -52,7 +41,7 @@ pub const Error = Allocator.Error || z2d.Path.Error || z2d.painter.FillError;
///
/// The returned bitmap is always `grid_metrics.cell_width * cell_width` by
/// `grid_metrics.cell_height`. `opts.constraint` is applied using the same
/// `face.RenderOptions.Constraint` machinery used by the platform font
/// `RenderOptions.Constraint` machinery used by the platform font
/// backends.
///
/// The caller owns the returned bitmap.
@@ -60,7 +49,7 @@ pub fn rasterize(
alloc: Allocator,
outline: glyf.Glyf.Outline,
design: DesignMetrics,
opts: face.RenderOptions,
opts: Glyph.RenderOptions,
) Error!Bitmap {
assert(design.units_per_em > 0);
assert(design.advance_width > 0);
@@ -207,16 +196,16 @@ const Placement = struct {
/// Bottom edge of the rasterized outline bounds in bitmap pixels, measured
/// from the bitmap's bottom edge. This matches the cell-relative y axis
/// used by font.face.GlyphSize and is converted to z2d's y-down axis when
/// used by font.Glyph.Size and is converted to z2d's y-down axis when
/// points are transformed.
y: f64,
/// Width of the rasterized outline bounds in bitmap pixels after applying
/// font.face.RenderOptions.Constraint.
/// font.Glyph.RenderOptions.Constraint.
width: f64,
/// Height of the rasterized outline bounds in bitmap pixels after applying
/// font.face.RenderOptions.Constraint.
/// font.Glyph.RenderOptions.Constraint.
height: f64,
/// Full bitmap height in pixels, used to convert cell-relative y-up-ish
@@ -240,7 +229,7 @@ const Placement = struct {
fn init(
bounds: Bounds,
design: DesignMetrics,
opts: face.RenderOptions,
opts: Glyph.RenderOptions,
) Placement {
// Start with protocol-like design units mapped so that the em square
// occupies one cell. This makes units_per_em the scale reference and
@@ -253,7 +242,7 @@ const Placement = struct {
// Convert the decoded point bounds into the same pixel coordinate space
// expected by RenderOptions.Constraint. This rectangle is the visible
// outline bounds, not the full advance/line-height layout box.
const glyph: face.GlyphSize = .{
const glyph: Glyph.Size = .{
.width = bounds.width() * scale,
.height = bounds.height() * scale,
.x = bounds.x_min * scale,
@@ -268,7 +257,7 @@ const Placement = struct {
// Apply the same fit/cover/stretch/alignment/padding rules used by
// normal font rendering. The result is still the outline bounds, but
// placed as if its containing advance/line-height box was constrained.
const constraint: face.RenderOptions.Constraint = constraint: {
const constraint: Glyph.RenderOptions.Constraint = constraint: {
var constraint = opts.constraint;
if (group_width > 0 and group_height > 0) {
// Tell Constraint that `glyph` is a sub-rectangle of the

View File

@@ -4,7 +4,7 @@
//! This file provides info extracted from the nerd fonts patcher script,
//! specifying the scaling/positioning attributes of various glyphs.
const Constraint = @import("face.zig").RenderOptions.Constraint;
const Constraint = @import("Glyph.zig").RenderOptions.Constraint;
/// Get the constraints for the provided codepoint.
pub fn getConstraint(cp: u21) ?Constraint {

View File

@@ -176,7 +176,7 @@ pub fn renderGlyph(
alloc: Allocator,
atlas: *font.Atlas,
cp: u32,
opts: font.face.RenderOptions,
opts: font.Glyph.RenderOptions,
) !font.Glyph {
if (std.debug.runtime_safety) {
if (!self.hasCodepoint(cp, null)) {

View File

@@ -155,9 +155,9 @@ pub const response = @import("glyph/response.zig");
pub const execute = @import("glyph/execute.zig").execute;
pub const CommandParser = request.CommandParser;
pub const Glossary = @import("glyph/Glossary.zig");
pub const Request = request.Request;
pub const Response = response.Response;
pub const Glossary = @import("glyph/Glossary.zig");
test {
std.testing.refAllDecls(@This());

View File

@@ -7,13 +7,15 @@ const std = @import("std");
const assert = std.debug.assert;
const Allocator = std.mem.Allocator;
const CircBuf = @import("../../../datastruct/circ_buf.zig").CircBuf;
const face = @import("../../../font/face.zig");
const FontGlyph = @import("../../../font/Glyph.zig");
const Glyf = @import("../../../font/opentype/glyf.zig").Glyf;
const glyf_rasterize = @import("../../../font/glyf_rasterize.zig");
const request = @import("request.zig");
const RegisterReq = request.Request.Register;
const DesignMetrics = FontGlyph.DesignMetrics;
const Constraint = FontGlyph.RenderOptions.Constraint;
/// Maximum entries allowed in the glossary before eviction.
/// Defined by the specification.
pub const max_entries = 1024;
@@ -124,13 +126,13 @@ pub const Entry = struct {
glyph: Glyph,
/// Authored metrics for the glyph's design coordinate space.
design: glyf_rasterize.DesignMetrics,
design: DesignMetrics,
/// Unicode cell width requested by the registration.
width: request.Width,
/// Normalized scale, alignment, and padding behavior for rasterization.
constraint: face.RenderOptions.Constraint,
constraint: Constraint,
/// Errors that can occur while constructing a glossary entry from a
/// register request.
@@ -152,7 +154,7 @@ pub const Entry = struct {
pub fn init(alloc: Allocator, req: RegisterReq) Entry.InitError!Entry {
// Validate format
const fmt = req.get(.fmt) orelse return error.InvalidOptions;
const design: glyf_rasterize.DesignMetrics = .{
const design: DesignMetrics = .{
.units_per_em = req.get(.upm) orelse return error.InvalidOptions,
.advance_width = req.get(.aw) orelse return error.InvalidOptions,
.line_height = req.get(.lh) orelse return error.InvalidOptions,
@@ -194,13 +196,12 @@ pub const Entry = struct {
/// Return the renderer constraint for a register request.
///
/// Glyph Protocol §8.5 defines sizing, alignment, and padding in terms of
/// the authored extent and render span. Ghostty's existing constraint type
/// is the closest renderer-native representation for these controls, but
/// it does not have exact equivalents for every protocol size mode, so this
/// function is the single normalization point for those policy choices.
/// the authored extent and render span. This function is the single
/// normalization point for how protocol sizing choices map to the
/// renderer-neutral constraint stored here.
fn constraintFromRegister(
req: RegisterReq,
) error{InvalidOptions}!face.RenderOptions.Constraint {
) error{InvalidOptions}!Constraint {
// Register.get applies the Glyph Protocol §6.1 defaults when options
// are omitted: size=height, align=center,center, and pad=0,0,0,0.
const size = req.get(.size) orelse return error.InvalidOptions;
@@ -307,9 +308,9 @@ test "Entry init decodes glyf payload and applies register fields" {
try testing.expectEqual(@as(u32, 1024), entry.design.advance_width);
try testing.expectEqual(@as(u32, 1536), entry.design.line_height);
try testing.expectEqual(request.Width.wide, entry.width);
try testing.expectEqual(face.RenderOptions.Constraint.Size.stretch, entry.constraint.size);
try testing.expectEqual(face.RenderOptions.Constraint.Align.end, entry.constraint.align_horizontal);
try testing.expectEqual(face.RenderOptions.Constraint.Align.start, entry.constraint.align_vertical);
try testing.expectEqual(Constraint.Size.stretch, entry.constraint.size);
try testing.expectEqual(Constraint.Align.end, entry.constraint.align_horizontal);
try testing.expectEqual(Constraint.Align.start, entry.constraint.align_vertical);
try testing.expectEqual(@as(f64, 0.1), entry.constraint.pad_top);
try testing.expectEqual(@as(f64, 0.2), entry.constraint.pad_right);
try testing.expectEqual(@as(f64, 0.3), entry.constraint.pad_bottom);