aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/font
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-16 23:28:05 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-16 23:29:28 +0300
commit4a3302d19a2baa3a33e910e0088c24e780905b90 (patch)
treedcaf76fae1c82102d933c9225b17ca7ac5d3a847 /vendor/libghostty-vt/src/font
parent550e9f8cc085f428429f5cf2da0bb2dba897e8b8 (diff)
chore: update vendored libghostty-vt
refs #276
Diffstat (limited to 'vendor/libghostty-vt/src/font')
-rw-r--r--vendor/libghostty-vt/src/font/CodepointResolver.zig2
-rw-r--r--vendor/libghostty-vt/src/font/Glyph.zig611
-rw-r--r--vendor/libghostty-vt/src/font/Metrics.zig4
-rw-r--r--vendor/libghostty-vt/src/font/SharedGrid.zig2
-rw-r--r--vendor/libghostty-vt/src/font/face.zig596
-rw-r--r--vendor/libghostty-vt/src/font/face/coretext.zig2
-rw-r--r--vendor/libghostty-vt/src/font/face/freetype.zig4
-rw-r--r--vendor/libghostty-vt/src/font/face/web_canvas.zig2
-rw-r--r--vendor/libghostty-vt/src/font/glyf_rasterize.zig825
-rw-r--r--vendor/libghostty-vt/src/font/glyf_rasterize_png_test.zig301
-rw-r--r--vendor/libghostty-vt/src/font/main.zig1
-rw-r--r--vendor/libghostty-vt/src/font/nerd_font_attributes.zig2
-rw-r--r--vendor/libghostty-vt/src/font/opentype/glyf.zig461
-rw-r--r--vendor/libghostty-vt/src/font/opentype/sfnt.zig6
-rw-r--r--vendor/libghostty-vt/src/font/shaper/coretext.zig2
-rw-r--r--vendor/libghostty-vt/src/font/shaper/harfbuzz.zig2
-rw-r--r--vendor/libghostty-vt/src/font/sprite/Face.zig124
-rw-r--r--vendor/libghostty-vt/src/font/testdata/glyf_rasterize.pngbin0 -> 1215 bytes
18 files changed, 2338 insertions, 609 deletions
diff --git a/vendor/libghostty-vt/src/font/CodepointResolver.zig b/vendor/libghostty-vt/src/font/CodepointResolver.zig
index a4f13c29..90fe2491 100644
--- a/vendor/libghostty-vt/src/font/CodepointResolver.zig
+++ b/vendor/libghostty-vt/src/font/CodepointResolver.zig
@@ -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;
diff --git a/vendor/libghostty-vt/src/font/Glyph.zig b/vendor/libghostty-vt/src/font/Glyph.zig
index f9937027..62c44e22 100644
--- a/vendor/libghostty-vt/src/font/Glyph.zig
+++ b/vendor/libghostty-vt/src/font/Glyph.zig
@@ -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),
+ );
+ }
+}
diff --git a/vendor/libghostty-vt/src/font/Metrics.zig b/vendor/libghostty-vt/src/font/Metrics.zig
index a72cb7be..60a300b0 100644
--- a/vendor/libghostty-vt/src/font/Metrics.zig
+++ b/vendor/libghostty-vt/src/font/Metrics.zig
@@ -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);
diff --git a/vendor/libghostty-vt/src/font/SharedGrid.zig b/vendor/libghostty-vt/src/font/SharedGrid.zig
index 5fd729b3..18870aab 100644
--- a/vendor/libghostty-vt/src/font/SharedGrid.zig
+++ b/vendor/libghostty-vt/src/font/SharedGrid.zig
@@ -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);
diff --git a/vendor/libghostty-vt/src/font/face.zig b/vendor/libghostty-vt/src/font/face.zig
index d77253ad..aeb7280c 100644
--- a/vendor/libghostty-vt/src/font/face.zig
+++ b/vendor/libghostty-vt/src/font/face.zig
@@ -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),
- );
- }
-}
diff --git a/vendor/libghostty-vt/src/font/face/coretext.zig b/vendor/libghostty-vt/src/font/face/coretext.zig
index 1d133388..cd8bc7d8 100644
--- a/vendor/libghostty-vt/src/font/face/coretext.zig
+++ b/vendor/libghostty-vt/src/font/face/coretext.zig
@@ -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)};
diff --git a/vendor/libghostty-vt/src/font/face/freetype.zig b/vendor/libghostty-vt/src/font/face/freetype.zig
index 528f72d5..d04fe180 100644
--- a/vendor/libghostty-vt/src/font/face/freetype.zig
+++ b/vendor/libghostty-vt/src/font/face/freetype.zig
@@ -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();
diff --git a/vendor/libghostty-vt/src/font/face/web_canvas.zig b/vendor/libghostty-vt/src/font/face/web_canvas.zig
index b4f9f5d5..a36a482d 100644
--- a/vendor/libghostty-vt/src/font/face/web_canvas.zig
+++ b/vendor/libghostty-vt/src/font/face/web_canvas.zig
@@ -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;
diff --git a/vendor/libghostty-vt/src/font/glyf_rasterize.zig b/vendor/libghostty-vt/src/font/glyf_rasterize.zig
new file mode 100644
index 00000000..f321214b
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/glyf_rasterize.zig
@@ -0,0 +1,825 @@
+//! Rasterization for OpenType glyf outlines.
+//!
+//! This module intentionally lives in `font` rather than `font/opentype`
+//! because I wanted to keep `font/opentype` dependency free on the font
+//! package.
+
+const std = @import("std");
+const assert = std.debug.assert;
+const Allocator = std.mem.Allocator;
+const z2d = @import("z2d");
+
+const Glyph = @import("Glyph.zig");
+const glyf = @import("opentype/glyf.zig");
+
+const DesignMetrics = Glyph.DesignMetrics;
+
+/// An owned, tightly packed alpha8 bitmap.
+pub const Bitmap = struct {
+ width: u32,
+ height: u32,
+ data: []u8,
+
+ // An empty 0x0 bitmap.
+ pub const empty: Bitmap = .{ .width = 0, .height = 0, .data = "" };
+
+ pub fn initEmpty(alloc: Allocator, width: u32, height: u32) Allocator.Error!Bitmap {
+ const data = try alloc.alloc(u8, @as(usize, width) * @as(usize, height));
+ @memset(data, 0);
+ return .{ .width = width, .height = height, .data = data };
+ }
+
+ pub fn deinit(self: *Bitmap, alloc: Allocator) void {
+ alloc.free(self.data);
+ self.* = undefined;
+ }
+};
+
+pub const Error = Allocator.Error || z2d.Path.Error || z2d.painter.FillError;
+
+/// Rasterize a decoded glyf outline to a full-cell alpha bitmap.
+///
+/// The returned bitmap is always `grid_metrics.cell_width * cell_width` by
+/// `grid_metrics.cell_height`. `opts.constraint` is applied using the same
+/// `RenderOptions.Constraint` machinery used by the platform font
+/// backends.
+///
+/// The caller owns the returned bitmap.
+pub fn rasterize(
+ alloc: Allocator,
+ outline: glyf.Glyf.Outline,
+ design: DesignMetrics,
+ opts: Glyph.RenderOptions,
+) Error!Bitmap {
+ assert(design.units_per_em > 0);
+ assert(design.advance_width > 0);
+ assert(design.line_height > 0);
+
+ // Calculate our final width/height.
+ const width: u32 = std.math.mul(
+ u32,
+ opts.grid_metrics.cell_width,
+ opts.cell_width orelse 1,
+ ) catch std.math.maxInt(u32);
+ const height = opts.grid_metrics.cell_height;
+ assert(width > 0 and height > 0);
+
+ // If we have no contours or points then we have no drawable shape, but the
+ // caller still asked for a cell-sized bitmap. Return that full bitmap with
+ // zero coverage so downstream atlas/upload code doesn't need a separate
+ // size contract for empty glyphs.
+ if (outline.contours.len == 0 or outline.points.len == 0) return Bitmap.initEmpty(alloc, width, height);
+
+ // Glyf entries have a header bounding box, but this rasterizer operates on
+ // the decoded Outline only. Recompute bounds from the decoded coordinate
+ // data so placement and scaling follow the geometry we actually draw. If a
+ // source glyf header disagrees with its points, the point data is the safer
+ // source of truth for rasterization; the header belongs in decode-time
+ // validation/metadata, not this font-level drawing API.
+ const bounds: Bounds = bounds: {
+ var bounds: Bounds = .{
+ .x_min = @floatFromInt(outline.points[0].x),
+ .y_min = @floatFromInt(outline.points[0].y),
+ .x_max = @floatFromInt(outline.points[0].x),
+ .y_max = @floatFromInt(outline.points[0].y),
+ };
+ for (outline.points[1..]) |p| {
+ const x: f64 = @floatFromInt(p.x);
+ const y: f64 = @floatFromInt(p.y);
+ bounds.x_min = @min(bounds.x_min, x);
+ bounds.y_min = @min(bounds.y_min, y);
+ bounds.x_max = @max(bounds.x_max, x);
+ bounds.y_max = @max(bounds.y_max, y);
+ }
+ break :bounds bounds;
+ };
+
+ // Degenerate point bounds can't produce filled area and would make the
+ // point-to-bitmap transform divide by zero, so return a full transparent
+ // bitmap just like an empty outline.
+ if (bounds.width() == 0 or bounds.height() == 0) return Bitmap.initEmpty(alloc, width, height);
+
+ // Build the surface we'll draw on. This is a simple alpha8 drawing.
+ var sfc: z2d.Surface = try .init(
+ .image_surface_alpha8,
+ alloc,
+ @intCast(width),
+ @intCast(height),
+ );
+ defer sfc.deinit(alloc);
+
+ var path: z2d.Path = .empty;
+ defer path.deinit(alloc);
+
+ const placement: Placement = .init(bounds, design, opts);
+ for (0..outline.contours.len) |i| try appendContourPath(
+ alloc,
+ &path,
+ outline.contour(i),
+ bounds,
+ placement,
+ );
+
+ try z2d.painter.fill(
+ alloc,
+ &sfc,
+ &.{ .opaque_pattern = .{
+ .pixel = .{ .alpha8 = .{ .a = 255 } },
+ } },
+ path.nodes.items,
+ .{},
+ );
+
+ return .{
+ .width = width,
+ .height = height,
+ .data = try alloc.dupe(u8, std.mem.sliceAsBytes(sfc.image_surface_alpha8.buf)),
+ };
+}
+
+const Bounds = struct {
+ x_min: f64,
+ y_min: f64,
+ x_max: f64,
+ y_max: f64,
+
+ fn width(self: Bounds) f64 {
+ return self.x_max - self.x_min;
+ }
+
+ fn height(self: Bounds) f64 {
+ return self.y_max - self.y_min;
+ }
+};
+
+/// Cell-relative pixel rectangle where the decoded outline bounds should be
+/// rasterized within the output bitmap.
+///
+/// This is deliberately the placement of the outline's computed point bounds,
+/// not the full declared advance/line-height box. `advance_width` and
+/// `line_height` describe the design-space layout box the outline was drawn
+/// within: they include intentional bearings and whitespace around the visible
+/// points. We use that declared box when applying `RenderOptions.Constraint` so
+/// sizing and alignment preserve those bearings consistently with other font
+/// backends; once that is resolved, we rasterize only the actual outline bounds
+/// into this rectangle.
+///
+/// ```text
+/// output bitmap / terminal cell
+/// ╭────────────────────────────────────────────────────────────────────────╮ top
+/// │ │
+/// │ declared advance/line-height box │
+/// │ (outer layout box used for constraints) │
+/// │ ╭────────────────────────────────────────────────────────────────╮ │
+/// │ │ │ │
+/// │◀──────── x ────────▶╭────────── width ──────────╮ │ │
+/// │ │ │ Placement │ ▲ height │ │
+/// │ │ │ outline point bounds │ │ │ │
+/// │ │ │ pixels to draw │ │ │ │
+/// │ │ │ │ │ │ │
+/// │ │ ╰───────────────────────────╯ ▼ │ │
+/// │ ╰─────────────────▲──────────────────────────────────────────────╯ │
+/// │ │ y │
+/// ╰────────────────────────────────────────────────────────────────────────╯ bottom
+/// x is measured from the bitmap left to the Placement left.
+/// y is measured from the bitmap bottom to the Placement bottom.
+/// bitmap_height is the full top-to-bottom bitmap height.
+/// ```
+///
+/// Constraints are applied to the outer box so the whitespace remains part of
+/// alignment decisions. `Placement` is the inner rectangle after that outer box
+/// has been constrained.
+const Placement = struct {
+ /// Left edge of the rasterized outline bounds in bitmap pixels, measured
+ /// from the bitmap's left edge.
+ x: f64,
+
+ /// 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.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.Glyph.RenderOptions.Constraint.
+ width: f64,
+
+ /// Height of the rasterized outline bounds in bitmap pixels after applying
+ /// font.Glyph.RenderOptions.Constraint.
+ height: f64,
+
+ /// Full bitmap height in pixels, used to convert cell-relative y-up-ish
+ /// placement into the y-down coordinate system used by z2d surfaces.
+ bitmap_height: f64,
+
+ /// Calculate where the decoded point bounds should land in the output
+ /// bitmap.
+ ///
+ /// The glyf protocol supplies declared metrics (`units_per_em`,
+ /// `advance_width`, and `line_height`) in design units, while Ghostty's
+ /// font constraint code works in cell-relative pixels. We first map the em
+ /// square to one cell height, matching the linked glyph rasterizer's
+ /// baseline model where design-space `y=0` is the bottom/baseline of the em
+ /// and `y=units_per_em` is its top. Then we describe the actual outline
+ /// bounds as a relative sub-rectangle of the declared advance/line-height
+ /// box. That declared box includes any intentional side bearings or
+ /// vertical whitespace around the outline; constraints should apply to that
+ /// layout box rather than to the tight point bounds alone. This returns the
+ /// final pixel rectangle for only the outline bounds that we will rasterize.
+ fn init(
+ bounds: Bounds,
+ design: DesignMetrics,
+ 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
+ // preserves the linked rasterizer's y=0 baseline/bottom behavior.
+ // Callers can then use RenderOptions.Constraint to fit/cover/stretch/
+ // align the declared advance/line-height box using existing font logic.
+ const scale = @as(f64, @floatFromInt(opts.grid_metrics.cell_height)) /
+ @as(f64, @floatFromInt(design.units_per_em));
+
+ // 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: Glyph.Size = .{
+ .width = bounds.width() * scale,
+ .height = bounds.height() * scale,
+ .x = bounds.x_min * scale,
+ .y = bounds.y_min * scale,
+ };
+
+ // Convert the declared layout box to pixels. This is the box that
+ // carries intentional bearings/whitespace and should be constrained.
+ const group_width = @as(f64, @floatFromInt(design.advance_width)) * scale;
+ const group_height = @as(f64, @floatFromInt(design.line_height)) * scale;
+
+ // 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: 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
+ // declared layout box. Constraint will size/align the outer box
+ // and then return the corresponding transformed inner box.
+ constraint.relative_width = glyph.width / group_width;
+ constraint.relative_height = glyph.height / group_height;
+ constraint.relative_x = glyph.x / group_width;
+ constraint.relative_y = glyph.y / group_height;
+ }
+ break :constraint constraint;
+ };
+ const constrained = constraint.constrain(
+ glyph,
+ opts.grid_metrics,
+ opts.constraint_width,
+ );
+
+ // Store the final outline placement plus the full bitmap height needed
+ // later to flip from cell-relative y to z2d's y-down surface space.
+ return .{
+ .x = constrained.x,
+ .y = constrained.y,
+ .width = constrained.width,
+ .height = constrained.height,
+ .bitmap_height = @floatFromInt(opts.grid_metrics.cell_height),
+ };
+ }
+};
+
+const Point = struct {
+ x: f64,
+ y: f64,
+};
+
+/// Append one contour to a z2d path.
+///
+/// Glyf contours are quadratic outlines with explicit on-curve points and
+/// off-curve control points. Consecutive off-curve points imply an on-curve
+/// point halfway between them, and a contour may begin with an off-curve point.
+/// This normalizes those cases while walking the closed contour and emits z2d
+/// line/cubic-curve operations in bitmap coordinates.
+fn appendContourPath(
+ alloc: Allocator,
+ path: *z2d.Path,
+ contour: []const glyf.Glyf.Outline.Point,
+ bounds: Bounds,
+ placement: Placement,
+) Error!void {
+ if (contour.len == 0) return;
+
+ const first = contour[0];
+ const last = contour[contour.len - 1];
+
+ var current: Point = undefined;
+ var i: usize = 0;
+
+ // Choose the starting on-curve point for this closed contour. If the first
+ // point is off-curve then the contour logically starts either at the final
+ // on-curve point, or at the implied midpoint between the final and first
+ // off-curve points.
+ if (first.on_curve) {
+ i = 1;
+ current = transformPoint(
+ first,
+ bounds,
+ placement,
+ );
+ } else if (last.on_curve) {
+ current = transformPoint(
+ last,
+ bounds,
+ placement,
+ );
+ } else {
+ current = midpoint(
+ transformPoint(last, bounds, placement),
+ transformPoint(first, bounds, placement),
+ );
+ }
+
+ // Move to the beginning
+ try path.moveTo(alloc, current.x, current.y);
+
+ // Go through the points and connect em!
+ while (i < contour.len) {
+ const p = contour[i];
+
+ // On-curve points connect to the current point with a straight line.
+ if (p.on_curve) {
+ current = transformPoint(p, bounds, placement);
+ try path.lineTo(alloc, current.x, current.y);
+ i += 1;
+ continue;
+ }
+
+ // Off-curve points are quadratic control points. The following point is
+ // either the curve endpoint or, if it is also off-curve, contributes an
+ // implied on-curve endpoint halfway between the two controls.
+ const control = transformPoint(p, bounds, placement);
+ const next = contour[(i + 1) % contour.len];
+ const end = if (next.on_curve) transformPoint(
+ next,
+ bounds,
+ placement,
+ ) else midpoint(
+ control,
+ transformPoint(next, bounds, placement),
+ );
+
+ // z2d paths only expose cubic curves, so convert the TrueType
+ // quadratic segment to an equivalent cubic segment before appending it.
+ const c1 = Point{
+ .x = current.x + ((2.0 / 3.0) * (control.x - current.x)),
+ .y = current.y + ((2.0 / 3.0) * (control.y - current.y)),
+ };
+ const c2 = Point{
+ .x = end.x + ((2.0 / 3.0) * (control.x - end.x)),
+ .y = end.y + ((2.0 / 3.0) * (control.y - end.y)),
+ };
+ try path.curveTo(
+ alloc,
+ c1.x,
+ c1.y,
+ c2.x,
+ c2.y,
+ end.x,
+ end.y,
+ );
+
+ current = end;
+
+ // If we consumed an explicit on-curve endpoint then skip it; otherwise
+ // the next off-curve point still needs to be used as the control point
+ // for the following quadratic segment.
+ i += if (next.on_curve) 2 else 1;
+ }
+
+ try path.close(alloc);
+}
+
+/// Convert a decoded glyf point from design-space coordinates to z2d bitmap
+/// coordinates.
+///
+/// `bounds` describes the decoded outline's point/control bounds in glyf
+/// design units. `placement` describes where those bounds should land in the
+/// output bitmap after constraints are applied. Glyf coordinates are y-up; z2d
+/// surfaces are y-down, so this also flips the y axis using
+/// `placement.bitmap_height`.
+fn transformPoint(
+ p: glyf.Glyf.Outline.Point,
+ bounds: Bounds,
+ placement: Placement,
+) Point {
+ const scale_x = placement.width / bounds.width();
+ const scale_y = placement.height / bounds.height();
+ const x_design: f64 = @floatFromInt(p.x);
+ const y_design: f64 = @floatFromInt(p.y);
+ return .{
+ .x = placement.x + ((x_design - bounds.x_min) * scale_x),
+ .y = placement.bitmap_height - placement.y -
+ ((y_design - bounds.y_min) * scale_y),
+ };
+}
+
+/// Return the implied on-curve point between two off-curve TrueType control
+/// points.
+fn midpoint(a: Point, b: Point) Point {
+ return .{
+ .x = (a.x + b.x) / 2.0,
+ .y = (a.y + b.y) / 2.0,
+ };
+}
+
+fn testMetrics(width: u32, height: u32) @import("Metrics.zig") {
+ return .{
+ .cell_width = width,
+ .cell_height = height,
+ .cell_baseline = 0,
+ .underline_position = height,
+ .underline_thickness = 1,
+ .strikethrough_position = height / 2,
+ .strikethrough_thickness = 1,
+ .overline_position = 0,
+ .overline_thickness = 1,
+ .box_thickness = 1,
+ .cursor_thickness = 1,
+ .cursor_height = height,
+ .icon_height = @floatFromInt(height),
+ .icon_height_single = @floatFromInt(height),
+ .face_width = @floatFromInt(width),
+ .face_height = @floatFromInt(height),
+ .face_y = 0,
+ };
+}
+
+test {
+ _ = @import("glyf_rasterize_png_test.zig");
+}
+
+test "glyf_rasterize: empty outline returns empty bitmap" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var bm = try rasterize(alloc, .{ .points = &.{}, .contours = &.{} }, .{
+ .units_per_em = 1000,
+ .advance_width = 1000,
+ .line_height = 1000,
+ }, .{
+ .grid_metrics = testMetrics(20, 20),
+ });
+ defer bm.deinit(alloc);
+
+ try testing.expectEqual(@as(u32, 20), bm.width);
+ try testing.expectEqual(@as(u32, 20), bm.height);
+ try testing.expectEqual(@as(usize, 20 * 20), bm.data.len);
+ for (bm.data) |v| try testing.expectEqual(@as(u8, 0), v);
+}
+
+test "glyf_rasterize: square fills bitmap center" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const outline: glyf.Glyf.Outline = .{
+ .points = &.{
+ .{ .x = 0, .y = 0, .on_curve = true },
+ .{ .x = 1000, .y = 0, .on_curve = true },
+ .{ .x = 1000, .y = 1000, .on_curve = true },
+ .{ .x = 0, .y = 1000, .on_curve = true },
+ },
+ .contours = &.{3},
+ };
+
+ var bm = try rasterize(alloc, outline, .{
+ .units_per_em = 1000,
+ .advance_width = 1000,
+ .line_height = 1000,
+ }, .{
+ .grid_metrics = testMetrics(20, 20),
+ });
+ defer bm.deinit(alloc);
+
+ try testing.expect(bm.data[10 * bm.width + 10] > 200);
+}
+
+test "glyf_rasterize: quadratic contour renders" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const outline: glyf.Glyf.Outline = .{
+ .points = &.{
+ .{ .x = 0, .y = 0, .on_curve = true },
+ .{ .x = 500, .y = 1000, .on_curve = false },
+ .{ .x = 1000, .y = 0, .on_curve = true },
+ },
+ .contours = &.{2},
+ };
+
+ var bm = try rasterize(alloc, outline, .{
+ .units_per_em = 1000,
+ .advance_width = 1000,
+ .line_height = 1000,
+ }, .{
+ .grid_metrics = testMetrics(20, 20),
+ });
+ defer bm.deinit(alloc);
+
+ var nonzero = false;
+ for (bm.data) |v| nonzero = nonzero or v != 0;
+ try testing.expect(nonzero);
+}
+
+test "glyf_rasterize: consecutive off-curve points render" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const outline: glyf.Glyf.Outline = .{
+ .points = &.{
+ .{ .x = 0, .y = 0, .on_curve = true },
+ .{ .x = 250, .y = 1000, .on_curve = false },
+ .{ .x = 750, .y = 1000, .on_curve = false },
+ .{ .x = 1000, .y = 0, .on_curve = true },
+ },
+ .contours = &.{3},
+ };
+
+ var bm = try rasterize(alloc, outline, .{
+ .units_per_em = 1000,
+ .advance_width = 1000,
+ .line_height = 1000,
+ }, .{
+ .grid_metrics = testMetrics(20, 20),
+ });
+ defer bm.deinit(alloc);
+
+ var nonzero = false;
+ for (bm.data) |v| nonzero = nonzero or v != 0;
+ try testing.expect(nonzero);
+}
+
+test "glyf_rasterize: units per em controls baseline scale" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const outline: glyf.Glyf.Outline = .{
+ .points = &.{
+ .{ .x = 0, .y = 0, .on_curve = true },
+ .{ .x = 1000, .y = 0, .on_curve = true },
+ .{ .x = 1000, .y = 1000, .on_curve = true },
+ .{ .x = 0, .y = 1000, .on_curve = true },
+ },
+ .contours = &.{3},
+ };
+
+ var bm = try rasterize(alloc, outline, .{
+ .units_per_em = 2000,
+ .advance_width = 1000,
+ .line_height = 1000,
+ }, .{
+ .grid_metrics = testMetrics(20, 20),
+ });
+ defer bm.deinit(alloc);
+
+ // With a 2000-unit em in a 20px cell, this 1000-unit square occupies the
+ // bottom half of the cell. This matches the linked rasterizer's y=0
+ // baseline/bottom behavior and proves units_per_em is the scale reference.
+ try testing.expect(bm.data[15 * bm.width + 5] > 200);
+ try testing.expectEqual(@as(u8, 0), bm.data[5 * bm.width + 5]);
+}
+
+test "glyf_rasterize: degenerate outline returns full empty bitmap" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const outline: glyf.Glyf.Outline = .{
+ .points = &.{
+ .{ .x = 0, .y = 0, .on_curve = true },
+ .{ .x = 1000, .y = 0, .on_curve = true },
+ .{ .x = 500, .y = 0, .on_curve = true },
+ },
+ .contours = &.{2},
+ };
+
+ var bm = try rasterize(alloc, outline, .{
+ .units_per_em = 1000,
+ .advance_width = 1000,
+ .line_height = 1000,
+ }, .{
+ .grid_metrics = testMetrics(20, 20),
+ });
+ defer bm.deinit(alloc);
+
+ try testing.expectEqual(@as(u32, 20), bm.width);
+ try testing.expectEqual(@as(u32, 20), bm.height);
+ try testing.expectEqual(@as(usize, 20 * 20), bm.data.len);
+ for (bm.data) |v| try testing.expectEqual(@as(u8, 0), v);
+}
+
+test "glyf_rasterize: contour can start off curve with final on curve point" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const outline: glyf.Glyf.Outline = .{
+ .points = &.{
+ .{ .x = 500, .y = 1000, .on_curve = false },
+ .{ .x = 1000, .y = 0, .on_curve = true },
+ .{ .x = 0, .y = 0, .on_curve = true },
+ },
+ .contours = &.{2},
+ };
+
+ var bm = try rasterize(alloc, outline, .{
+ .units_per_em = 1000,
+ .advance_width = 1000,
+ .line_height = 1000,
+ }, .{
+ .grid_metrics = testMetrics(20, 20),
+ });
+ defer bm.deinit(alloc);
+
+ var nonzero = false;
+ for (bm.data) |v| nonzero = nonzero or v != 0;
+ try testing.expect(nonzero);
+}
+
+test "glyf_rasterize: contour can start with implied midpoint" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const outline: glyf.Glyf.Outline = .{
+ .points = &.{
+ .{ .x = 250, .y = 1000, .on_curve = false },
+ .{ .x = 1000, .y = 0, .on_curve = true },
+ .{ .x = 750, .y = 1000, .on_curve = false },
+ .{ .x = 0, .y = 0, .on_curve = true },
+ },
+ .contours = &.{3},
+ };
+
+ var bm = try rasterize(alloc, outline, .{
+ .units_per_em = 1000,
+ .advance_width = 1000,
+ .line_height = 1000,
+ }, .{
+ .grid_metrics = testMetrics(20, 20),
+ });
+ defer bm.deinit(alloc);
+
+ var nonzero = false;
+ for (bm.data) |v| nonzero = nonzero or v != 0;
+ try testing.expect(nonzero);
+}
+
+test "glyf_rasterize: multiple contours render independently" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const outline: glyf.Glyf.Outline = .{
+ .points = &.{
+ .{ .x = 0, .y = 0, .on_curve = true },
+ .{ .x = 400, .y = 0, .on_curve = true },
+ .{ .x = 400, .y = 400, .on_curve = true },
+ .{ .x = 0, .y = 400, .on_curve = true },
+ .{ .x = 600, .y = 600, .on_curve = true },
+ .{ .x = 1000, .y = 600, .on_curve = true },
+ .{ .x = 1000, .y = 1000, .on_curve = true },
+ .{ .x = 600, .y = 1000, .on_curve = true },
+ },
+ .contours = &.{ 3, 7 },
+ };
+
+ var bm = try rasterize(alloc, outline, .{
+ .units_per_em = 1000,
+ .advance_width = 1000,
+ .line_height = 1000,
+ }, .{
+ .grid_metrics = testMetrics(20, 20),
+ });
+ defer bm.deinit(alloc);
+
+ try testing.expect(bm.data[16 * bm.width + 4] > 200);
+ try testing.expect(bm.data[4 * bm.width + 16] > 200);
+ try testing.expectEqual(@as(u8, 0), bm.data[10 * bm.width + 10]);
+}
+
+test "glyf_rasterize: non-zero bearings preserve declared whitespace" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const outline: glyf.Glyf.Outline = .{
+ .points = &.{
+ .{ .x = 250, .y = 0, .on_curve = true },
+ .{ .x = 750, .y = 0, .on_curve = true },
+ .{ .x = 750, .y = 1000, .on_curve = true },
+ .{ .x = 250, .y = 1000, .on_curve = true },
+ },
+ .contours = &.{3},
+ };
+
+ var bm = try rasterize(alloc, outline, .{
+ .units_per_em = 1000,
+ .advance_width = 1000,
+ .line_height = 1000,
+ }, .{
+ .grid_metrics = testMetrics(20, 20),
+ });
+ defer bm.deinit(alloc);
+
+ try testing.expectEqual(@as(u8, 0), bm.data[10 * bm.width + 2]);
+ try testing.expect(bm.data[10 * bm.width + 10] > 200);
+ try testing.expectEqual(@as(u8, 0), bm.data[10 * bm.width + 17]);
+}
+
+test "glyf_rasterize: negative y coordinates descend below baseline" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const outline: glyf.Glyf.Outline = .{
+ .points = &.{
+ .{ .x = 0, .y = -250, .on_curve = true },
+ .{ .x = 1000, .y = -250, .on_curve = true },
+ .{ .x = 1000, .y = 750, .on_curve = true },
+ .{ .x = 0, .y = 750, .on_curve = true },
+ },
+ .contours = &.{3},
+ };
+
+ var bm = try rasterize(alloc, outline, .{
+ .units_per_em = 1000,
+ .advance_width = 1000,
+ .line_height = 1000,
+ }, .{
+ .grid_metrics = testMetrics(20, 20),
+ });
+ defer bm.deinit(alloc);
+
+ try testing.expectEqual(@as(u8, 0), bm.data[2 * bm.width + 10]);
+ try testing.expect(bm.data[10 * bm.width + 10] > 200);
+ try testing.expect(bm.data[18 * bm.width + 10] > 200);
+}
+
+test "glyf_rasterize: two-cell bitmap and constraint render within width" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const outline: glyf.Glyf.Outline = .{
+ .points = &.{
+ .{ .x = 0, .y = 0, .on_curve = true },
+ .{ .x = 1000, .y = 0, .on_curve = true },
+ .{ .x = 1000, .y = 1000, .on_curve = true },
+ .{ .x = 0, .y = 1000, .on_curve = true },
+ },
+ .contours = &.{3},
+ };
+
+ var bm = try rasterize(alloc, outline, .{
+ .units_per_em = 1000,
+ .advance_width = 1000,
+ .line_height = 1000,
+ }, .{
+ .grid_metrics = testMetrics(20, 20),
+ .cell_width = 2,
+ .constraint_width = 2,
+ .constraint = .{
+ .size = .cover,
+ .align_horizontal = .center,
+ .align_vertical = .center,
+ },
+ });
+ defer bm.deinit(alloc);
+
+ try testing.expectEqual(@as(u32, 40), bm.width);
+ try testing.expectEqual(@as(u32, 20), bm.height);
+ try testing.expectEqual(@as(u8, 0), bm.data[10 * bm.width + 2]);
+ try testing.expect(bm.data[10 * bm.width + 20] > 200);
+ try testing.expectEqual(@as(u8, 0), bm.data[10 * bm.width + 37]);
+}
+
+test "glyf_rasterize: line height does not change unconstrained em scale" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const outline: glyf.Glyf.Outline = .{
+ .points = &.{
+ .{ .x = 0, .y = 0, .on_curve = true },
+ .{ .x = 1000, .y = 0, .on_curve = true },
+ .{ .x = 1000, .y = 1000, .on_curve = true },
+ .{ .x = 0, .y = 1000, .on_curve = true },
+ },
+ .contours = &.{3},
+ };
+
+ var bm = try rasterize(alloc, outline, .{
+ .units_per_em = 1000,
+ .advance_width = 1000,
+ .line_height = 2000,
+ }, .{
+ .grid_metrics = testMetrics(20, 20),
+ });
+ defer bm.deinit(alloc);
+
+ try testing.expect(bm.data[10 * bm.width + 10] > 200);
+ try testing.expect(bm.data[2 * bm.width + 10] > 200);
+ try testing.expect(bm.data[17 * bm.width + 10] > 200);
+}
diff --git a/vendor/libghostty-vt/src/font/glyf_rasterize_png_test.zig b/vendor/libghostty-vt/src/font/glyf_rasterize_png_test.zig
new file mode 100644
index 00000000..e8ad70fd
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/glyf_rasterize_png_test.zig
@@ -0,0 +1,301 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const wuffs = @import("wuffs");
+const z2d = @import("z2d");
+
+const glyf_rasterize = @import("glyf_rasterize.zig");
+const glyf = @import("opentype/glyf.zig");
+
+const log = std.log.scoped(.glyf_rasterize);
+
+const test_glyf_payloads = [_][]const u8{
+ // Nerd Font branch, folder, home, heart, and Rust cog outlines from:
+ // https://github.com/raphamorim/glyph-protocol-examples/blob/main/bubbletea/main.go
+ "AAIARv8zAhIDnQAZAB0AABcjNTQ3Njc3Njc2NTUjNxcjFRQGBwcGBwYVEQcRM82HJxs3SyoUE2aPjmY0NCUvDxSHh83rVzgoIzAbKSZAoaenvF5kIhkfHSM9AVdXAn8=",
+ "AAEAAP/UA5wC/AAVAAAXIiY1ETQ2MzMyFxcWMyEyFgcRFgYjcy9ERC/nOCQjER0BITBEAQFEMCxELwJBMEQvLhdEL/4yL0Q=",
+ "AAEAAP+aBBEDNgA+AAABFAYjIxMUBxUUBisFIiY9AjQmIyMiBh0CFAYrAiIiJwYiIyMiJjc1MjQ1NSMiJjQ3ATYzMhcBFgQOJBY5AQEqHh0GBzsrHioiGHMYIioeLDkBBAMBBAIcHiwBAToYIhIBzg4aFw8BzBcBahgi/t4KBB4eKioeLHQYIiIYdCweKgICKh7KBAJ+IDISAZQODP5qFA==",
+ "AAEAAP/dA5sC+QAZAAATJjU1NDY3NhYXFzc2NhcWFhUVFAcBBiMiJ1ZWel09eCwWFSx4PV16Vv66FB0eFAEhUHULXpAQCiYsFhYsJgoQkF4LdVD+zxMT",
+ "AAoAAP/YAyEC+AENARcBWgFlAW8BfQGIAbsBxAHNAAAAMhYXFhYyNjc2MzIXFhcWFxY3NjMyFhcWFxY3NjMyFxcHBxcWNzYXFgcGFxYzMhcWBw4CFAcUFQcUFxYWFRQGFhcWFgcGFBcWFxYGBwYUFxYGBw4CFhYXFgcGBwYVFBcWBwYjIgYXFgYnJgcGFxcHBiInJgYHBiMiJyYHBgcGBiMiJyYiBwYiJyYmBwYGJyYmJyYHBiImJyYmBwYiJjc3JyYHBicmNzYmIwYmNzY2Nzc0JyYmNTQ3NiYnJiY3NjQnJiY0Njc2NjQnJicmJjY3NjYnJjc3Mjc2NScmJyYnJjYXMjY1NCYmJyc3NhcWNzcnJjYzMhcWNjc2NjMyFxY3Njc2NjMyFxYyNzY3FyIHBhYzMjYnJgczBwYHBhUUMzIXFhYHBgcOAhUGFQciFhcWFxYXFhcWFjc2NzY3NjMzNzYvAiYnJjU0NzcnJicmJicnBwYGJyYnBwYGFxYyNzYmJyYFIgcGFBcWNicmBRcWBwYPAgYXFzM1NRcVMzY3NjU0JyYjBxUzMhcWFQcjIhUGFjMyNzYyFxYXFhUWFhcWNzY/AjY2Fxc2NjQjIiYnJicmJyYnJiMGIgcGFjMyNiclIgcGFjc2JyYBjQQICgcECAgIEQMJBgUCAwUGEBMDBgQEAwUDFBIFAwQEAQEEBRIZBQUGBQMCFxUFBwwBAgICARgSChoEFBcEExEUDwQECBATERMEGAoGCAQEBhEHAxUZCAsGAxcYBAUGChoVAgMBAQQEBhQSCgMECgQRFAUEBgcGBAUQEAgLDQwOCwoODQgFBg4CBRUPDAQEBAgTFAYIAQEEBREaBAYGBQQYGQYKAQQCARgSCg0NBBQXBBMQERIEBg4KCAIDCg0ECBEUBA0QBwQFEBcBAQICAgsIGRgCAgIBBAMFGhIFBAEBCAMFEhMIBAQEBgUREQQGCAcEBgQQDwoLCQQFCQcMDBARCg0HPgEPQjEfoJ8NJzACAykCBAQCAQEEAgIDGAgEAwggCg0BAQMCDBABAgIBHRsDBw4PAx8xFkQTCRQSEAkHEvoMDgcHGAgEBAcFAjAHBA0OFBQTBf3wBAsIAh0cAQMKBFOENjcIERsLLzEkJAECAXl5ARgBBBQZEAYEBQYBRCEnKiYSBgYGECAcARg/NhQLDgkLBQkQBimQEAQPChESCA4BVRAGCigLChEFAvgEEQsIBgkREA8FCAIBDA0KERYCAgkIBAQWFwIDBQYFBBkVAwIDBhkDBAQEAQEBAQcEAwQHBSQICAgMEREICwoFBggKDAgQEQwJBAQCCAgHGAYDAwQIAxAXBwMGFBkKBgQCAxYWBAQJCAQVHAwOAwQQEwYREBMVFBMCEA0GAgQmAgQPDAoSFgQJCQgXFgICBAYEBhgVBgEMFgQKAwMGBAQEBgQTEQgJCQwQEAgLCwQMBAkHBAoDAwkLDAgGCAgSFwcEAwQHAgIGBQQXDAEEBQEGCgQTBAcGBAICFxYICAkEFhEKDA0BARcSBBEPEw8ERQcKHiAKBScEESgaBAIDCjghJh4BBAIBAQEBBAECAhUjEQMJAgcIGBMBAQURFgcNDAMHCgYgIQY0IxAcBAETEwgDBBObARgMCwwIFAQEAgIIHAYKKAsDAwkYDQQNDRAjLg9eXgE4AQQHDhQHA4g0AgQqKwICGgUECAkVHAEHFAMDCAcKAx4iCgcGARwCBAwPJjAHDgQCwgMJIiIJAg0UFBIRDgQ=",
+};
+
+/// Return deterministic font metrics for the PNG reference test.
+///
+/// These are intentionally minimal: the rasterizer only needs cell geometry,
+/// face geometry, and icon heights for constraint calculations.
+fn testMetrics(width: u32, height: u32) @import("Metrics.zig") {
+ return .{
+ .cell_width = width,
+ .cell_height = height,
+ .cell_baseline = 0,
+ .underline_position = height,
+ .underline_thickness = 1,
+ .strikethrough_position = height / 2,
+ .strikethrough_thickness = 1,
+ .overline_position = 0,
+ .overline_thickness = 1,
+ .box_thickness = 1,
+ .cursor_thickness = 1,
+ .cursor_height = height,
+ .icon_height = @floatFromInt(height),
+ .icon_height_single = @floatFromInt(height),
+ .face_width = @floatFromInt(width),
+ .face_height = @floatFromInt(height),
+ .face_y = 0,
+ };
+}
+
+/// Decode a base64-encoded glyf protocol payload into an owned outline.
+///
+/// The payload is a complete simple-glyph `glyf` table entry. The returned
+/// outline owns decoded point and contour storage and must be deinitialized by
+/// the caller.
+fn decodeGlyfPayload(alloc: Allocator, payload: []const u8) !glyf.Glyf.Outline {
+ const decoder = std.base64.standard.Decoder;
+ const size = try decoder.calcSizeForSlice(payload);
+ const data = try alloc.alloc(u8, size);
+ defer alloc.free(data);
+
+ try decoder.decode(data, payload);
+ const entry = try glyf.Glyf.Entry.init(data);
+ return try entry.decode(alloc);
+}
+
+/// Copy a tightly packed alpha bitmap into the alpha atlas at `dst_x`, `dst_y`.
+///
+/// The destination rectangle must fit inside the atlas. This is a test helper,
+/// so it trusts the hardcoded atlas layout rather than clipping.
+fn blitBitmap(atlas: *z2d.Surface, bm: glyf_rasterize.Bitmap, dst_x: usize, dst_y: usize) void {
+ const dst_width: usize = @intCast(atlas.getWidth());
+ const dst = std.mem.sliceAsBytes(atlas.image_surface_alpha8.buf);
+ for (0..bm.height) |y| {
+ const src_start = y * bm.width;
+ const src_end = src_start + bm.width;
+ const dst_start = (dst_y + y) * dst_width + dst_x;
+ @memcpy(dst[dst_start .. dst_start + bm.width], bm.data[src_start..src_end]);
+ }
+}
+
+/// Draw faint terminal-cell outlines into one row of the alpha atlas.
+///
+/// The boxes make cell advance and placement behavior visible in the reference
+/// PNG without overpowering the rendered glyph coverage.
+fn drawCellBoxes(atlas: *z2d.Surface, y: usize, cell_width: usize, cell_height: usize) void {
+ const width: usize = @intCast(atlas.getWidth());
+ const dst = std.mem.sliceAsBytes(atlas.image_surface_alpha8.buf);
+ const alpha = 64;
+
+ var x: usize = 0;
+ while (x < width) : (x += cell_width) {
+ const right = @min(x + cell_width - 1, width - 1);
+ const bottom = y + cell_height - 1;
+
+ for (x..right + 1) |px| {
+ dst[y * width + px] = @max(dst[y * width + px], alpha);
+ dst[bottom * width + px] = @max(dst[bottom * width + px], alpha);
+ }
+ for (y..bottom + 1) |py| {
+ dst[py * width + x] = @max(dst[py * width + x], alpha);
+ dst[py * width + right] = @max(dst[py * width + right], alpha);
+ }
+ }
+}
+
+/// Compare a generated atlas PNG against the checked-in reference image.
+///
+/// On missing reference or mismatch, copy the generated PNG into the workspace
+/// as `glyf_rasterize_test.png`. On pixel mismatch, also write
+/// `glyf_rasterize_diff.png`, where red is reference-only coverage and green is
+/// newly generated coverage. Returns true when a difference was found.
+fn diffAtlas(
+ alloc: Allocator,
+ atlas: *z2d.Surface,
+ generated_path: []const u8,
+) !bool {
+ const ref_path = "src/font/testdata/glyf_rasterize.png";
+
+ const generated_file = try std.fs.openFileAbsolute(generated_path, .{ .mode = .read_only });
+ defer generated_file.close();
+ const generated_bytes = try generated_file.readToEndAlloc(alloc, std.math.maxInt(usize));
+ defer alloc.free(generated_bytes);
+
+ const cwd_absolute = try std.fs.cwd().realpathAlloc(alloc, ".");
+ defer alloc.free(cwd_absolute);
+
+ const ref_file = std.fs.cwd().openFile(ref_path, .{ .mode = .read_only }) catch |err| {
+ log.err("Can't open reference file {s}: {}", .{ ref_path, err });
+
+ const test_path = try std.fmt.allocPrint(alloc, "{s}/glyf_rasterize_test.png", .{cwd_absolute});
+ defer alloc.free(test_path);
+ try std.fs.copyFileAbsolute(generated_path, test_path, .{});
+ return true;
+ };
+ defer ref_file.close();
+ const ref_bytes = try ref_file.readToEndAlloc(alloc, std.math.maxInt(usize));
+ defer alloc.free(ref_bytes);
+
+ if (std.mem.eql(u8, generated_bytes, ref_bytes)) return false;
+
+ const test_path = try std.fmt.allocPrint(alloc, "{s}/glyf_rasterize_test.png", .{cwd_absolute});
+ defer alloc.free(test_path);
+ try std.fs.copyFileAbsolute(generated_path, test_path, .{});
+
+ const ref_rgba = try wuffs.png.decode(alloc, ref_bytes);
+ defer alloc.free(ref_rgba.data);
+
+ if (ref_rgba.width != atlas.getWidth() or ref_rgba.height != atlas.getHeight()) {
+ log.err(
+ "glyf rasterize visual output dimensions differ from reference: " ++
+ "test={s} ({d}x{d}), reference={s} ({d}x{d})",
+ .{ test_path, atlas.getWidth(), atlas.getHeight(), ref_path, ref_rgba.width, ref_rgba.height },
+ );
+ return true;
+ }
+
+ var diff = try z2d.Surface.init(
+ .image_surface_rgb,
+ alloc,
+ atlas.getWidth(),
+ atlas.getHeight(),
+ );
+ defer diff.deinit(alloc);
+
+ const test_gray = std.mem.sliceAsBytes(atlas.image_surface_alpha8.buf);
+ const diff_pix = diff.image_surface_rgb.buf;
+ var differs = false;
+ for (test_gray, 0..) |t, i| {
+ const r = ref_rgba.data[i * 4];
+ if (t == r) {
+ diff_pix[i].r = t / 3;
+ diff_pix[i].g = t / 3;
+ diff_pix[i].b = t / 3;
+ } else {
+ differs = true;
+ diff_pix[i].r = r;
+ diff_pix[i].g = t;
+ }
+ }
+
+ if (!differs) {
+ log.err(
+ "generated glyf rasterize PNG bytes differ from reference but pixels match; " ++
+ "test={s}, reference={s}",
+ .{ test_path, ref_path },
+ );
+ return true;
+ }
+
+ const diff_path = "./glyf_rasterize_diff.png";
+ try z2d.png_exporter.writeToPNGFile(diff, diff_path, .{});
+ log.err(
+ "glyf rasterize visual output differs from reference: test={s}, reference={s}, diff={s}",
+ .{ test_path, ref_path, diff_path },
+ );
+
+ return true;
+}
+
+test "glyf_rasterize: bubbletea glyph protocol examples match reference image" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ // The generated PNG is a visual atlas for reading placement behavior.
+ // Each column below is one terminal cell. The five payloads are rendered in
+ // order: branch, folder, home, heart, rust.
+ //
+ // ```text
+ // columns: 0 1 2 3 4 5 6 7 8 9
+ // ┌─┬─┬─┬─┬─┬─┬─┬─┬─┬─┐
+ // row 0 │B│F│H│♥│R│ │ │ │ │ │ narrow/default: one-cell bitmap stride
+ // ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
+ // row 1 │B │F │H │♥ │R │ width=2: same glyphs, two-cell bitmaps
+ // ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
+ // row 2 │ B│ F│ H│ ♥│ R│ width=2 + horizontal center alignment
+ // ├─┼─┼─┼─┼─┼─┼─┼─┼─┼─┤
+ // row 3 │B │F │H │♥ │R │ advance_width=2000: wider design box
+ // └─┴─┴─┴─┴─┴─┴─┴─┴─┴─┘
+ // ```
+ //
+ // The faint grid lines in the PNG are these cell boundaries. Rows 2 and 3
+ // are the design-metric/placement regression checks: row 2 centers a
+ // one-cell-wide design box inside two cells, while row 3 centers a two-cell
+ // design box so the visible glyph returns to the start of each span.
+ const cell_width = 20;
+ const cell_height = 20;
+ const columns = test_glyf_payloads.len;
+ const narrow_stride_x = cell_width;
+ const wide_stride_x = cell_width * 2;
+ const row_count = 4;
+
+ var atlas = try z2d.Surface.init(
+ .image_surface_alpha8,
+ alloc,
+ @intCast(wide_stride_x * columns),
+ cell_height * row_count,
+ );
+ defer atlas.deinit(alloc);
+
+ for (test_glyf_payloads, 0..) |payload, i| {
+ var outline = try decodeGlyfPayload(alloc, payload);
+ defer outline.deinit(alloc);
+
+ var narrow = try glyf_rasterize.rasterize(alloc, outline, .{
+ .units_per_em = 1000,
+ .advance_width = 1000,
+ .line_height = 1000,
+ }, .{
+ .grid_metrics = testMetrics(cell_width, cell_height),
+ });
+ defer narrow.deinit(alloc);
+ blitBitmap(&atlas, narrow, i * narrow_stride_x, 0);
+
+ var wide = try glyf_rasterize.rasterize(alloc, outline, .{
+ .units_per_em = 1000,
+ .advance_width = 1000,
+ .line_height = 1000,
+ }, .{
+ .grid_metrics = testMetrics(cell_width, cell_height),
+ .cell_width = 2,
+ });
+ defer wide.deinit(alloc);
+ blitBitmap(&atlas, wide, i * wide_stride_x, cell_height);
+
+ var centered = try glyf_rasterize.rasterize(alloc, outline, .{
+ .units_per_em = 1000,
+ .advance_width = 1000,
+ .line_height = 1000,
+ }, .{
+ .grid_metrics = testMetrics(cell_width, cell_height),
+ .cell_width = 2,
+ .constraint_width = 2,
+ .constraint = .{ .align_horizontal = .center },
+ });
+ defer centered.deinit(alloc);
+ blitBitmap(&atlas, centered, i * wide_stride_x, cell_height * 2);
+
+ var designed_wide = try glyf_rasterize.rasterize(alloc, outline, .{
+ .units_per_em = 1000,
+ .advance_width = 2000,
+ .line_height = 1000,
+ }, .{
+ .grid_metrics = testMetrics(cell_width, cell_height),
+ .cell_width = 2,
+ .constraint_width = 2,
+ .constraint = .{ .align_horizontal = .center },
+ });
+ defer designed_wide.deinit(alloc);
+ blitBitmap(&atlas, designed_wide, i * wide_stride_x, cell_height * 3);
+ }
+
+ for (0..row_count) |row| drawCellBoxes(&atlas, row * cell_height, cell_width, cell_height);
+
+ var dir = testing.tmpDir(.{});
+ defer dir.cleanup();
+ const tmp_dir = try dir.dir.realpathAlloc(alloc, ".");
+ defer alloc.free(tmp_dir);
+
+ const generated_path = try std.fmt.allocPrint(alloc, "{s}/glyf_rasterize.png", .{tmp_dir});
+ defer alloc.free(generated_path);
+ try z2d.png_exporter.writeToPNGFile(atlas, generated_path, .{});
+
+ try testing.expect(!try diffAtlas(alloc, &atlas, generated_path));
+}
diff --git a/vendor/libghostty-vt/src/font/main.zig b/vendor/libghostty-vt/src/font/main.zig
index a8522afe..0c218bbd 100644
--- a/vendor/libghostty-vt/src/font/main.zig
+++ b/vendor/libghostty-vt/src/font/main.zig
@@ -15,6 +15,7 @@ pub const Collection = @import("Collection.zig");
pub const DeferredFace = @import("DeferredFace.zig");
pub const Face = face.Face;
pub const Glyph = @import("Glyph.zig");
+pub const glyf_rasterize = @import("glyf_rasterize.zig");
pub const Metrics = @import("Metrics.zig");
pub const opentype = @import("opentype.zig");
pub const shape = @import("shape.zig");
diff --git a/vendor/libghostty-vt/src/font/nerd_font_attributes.zig b/vendor/libghostty-vt/src/font/nerd_font_attributes.zig
index f4a19d96..fc988496 100644
--- a/vendor/libghostty-vt/src/font/nerd_font_attributes.zig
+++ b/vendor/libghostty-vt/src/font/nerd_font_attributes.zig
@@ -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 {
diff --git a/vendor/libghostty-vt/src/font/opentype/glyf.zig b/vendor/libghostty-vt/src/font/opentype/glyf.zig
index 7392729a..912b5fe8 100644
--- a/vendor/libghostty-vt/src/font/opentype/glyf.zig
+++ b/vendor/libghostty-vt/src/font/opentype/glyf.zig
@@ -1,4 +1,5 @@
const std = @import("std");
+const Allocator = std.mem.Allocator;
const sfnt = @import("sfnt.zig");
/// Glyph Data Table
@@ -15,6 +16,49 @@ const sfnt = @import("sfnt.zig");
pub const Glyf = struct {
data: []const u8,
+ /// A decoded glyph outline.
+ ///
+ /// The `contours` slice is the list of end point indices and
+ /// `points` owns all the points. Glyf guarantees that contour
+ /// points are sequential so we can just store the end and calculate
+ /// the points that way. Use the helpers to make it ergonomic.
+ pub const Outline = struct {
+ /// List of contour end points. Calculate the full list of
+ /// points using points[prev...this+1]
+ contours: []const sfnt.uint16,
+
+ /// The backing storage of all points in the entry.
+ points: []const Point,
+
+ /// A single decoded point in a simple glyph contour.
+ pub const Point = struct {
+ x: i32,
+ y: i32,
+ on_curve: bool,
+ };
+
+ /// Return the point slice for the contour at `index`.
+ ///
+ /// The returned slice references `points` and is invalidated when
+ /// this outline is deinitialized.
+ pub fn contour(self: Outline, index: usize) []const Point {
+ const start = if (index == 0)
+ 0
+ else
+ @as(usize, self.contours[index - 1]) + 1;
+ const end = @as(usize, self.contours[index]) + 1;
+ return self.points[start..end];
+ }
+
+ /// Free all memory owned by this outline. Pass in the same
+ /// allocator used for decoding.
+ pub fn deinit(self: *Outline, alloc: Allocator) void {
+ alloc.free(self.contours);
+ alloc.free(self.points);
+ self.* = undefined;
+ }
+ };
+
/// https://learn.microsoft.com/en-us/typography/opentype/spec/glyf#table-organization
pub const Entry = struct {
header: Header,
@@ -241,6 +285,12 @@ pub const Glyf = struct {
TooManyPoints,
};
+ /// Errors that can be returned from `Entry.decode()`.
+ pub const DecodeError = SizeError || Allocator.Error || error{
+ /// Coordinate delta accumulation overflowed.
+ CoordinateOverflow,
+ };
+
/// Determines the size (in bytes) of this entry.
///
/// If the entry is valid, returns the number of bytes
@@ -393,6 +443,149 @@ pub const Glyf = struct {
// No issues found, the glyf entry is valid, return its length.
return @sizeOf(Header) + fbs.pos;
}
+
+ /// Decode this simple glyph entry into an owned outline.
+ ///
+ /// NOTE: Currently produces errors when given composite glyphs
+ /// or any glyphs that have hinting instructions included.
+ pub fn decode(self: Entry, alloc: Allocator) DecodeError!Glyf.Outline {
+ // We only support simple glyphs.
+ switch (self.entryType()) {
+ .simple => {},
+ .composite => return error.CompositeNotSupported,
+ }
+
+ var fbs = std.io.fixedBufferStream(self.data);
+ const reader = fbs.reader();
+
+ // A zero-contour glyph may be header-only. See size for the
+ // reason for the hardcoded 2 here.
+ const num_contours: usize = @intCast(self.header.numberOfContours);
+ if (num_contours == 0 and self.data.len < 2) return .{
+ .points = &.{},
+ .contours = &.{},
+ };
+
+ // We now know our full amount of contour ending points.
+ const end_points = try alloc.alloc(sfnt.uint16, num_contours);
+ errdefer alloc.free(end_points);
+
+ // If we have no contours, then the only possible remaining
+ // field is instructionLength. Instructions are not supported.
+ if (num_contours == 0) {
+ const instructions_length = try reader.readInt(sfnt.uint16, .big);
+ if (instructions_length > 0) return error.InstructionsNotSupported;
+ return .{ .points = &.{}, .contours = end_points };
+ }
+
+ // The number of points is determined by the final end point
+ // entry since the entries have to be monotonic (something
+ // we verify below).
+ const point_count: usize = point_count: {
+ var prev_end_point: isize = -1;
+
+ // Go through the end points array and update our end_points
+ // with the valid index. The final endpoint tells us our point
+ // count, since endpoints are stored as inclusive point indices.
+ for (0..end_points.len) |i| {
+ const index = try reader.readInt(sfnt.uint16, .big);
+ if (index <= prev_end_point) return error.EndPointsOutOfOrder;
+ prev_end_point = index;
+ end_points[i] = index;
+ }
+
+ // The final point tells us our point count.
+ break :point_count @as(usize, end_points[end_points.len - 1]) + 1;
+ };
+
+ // Instructions are not supported.
+ const instructions_length = try reader.readInt(sfnt.uint16, .big);
+ if (instructions_length > 0) return error.InstructionsNotSupported;
+
+ // Allocate our points right away even though the next entries
+ // are flags. We want to do this so that if the allocator is
+ // a bump allocator, the flags free will actually free it.
+ const points = try alloc.alloc(Glyf.Outline.Point, point_count);
+ errdefer alloc.free(points);
+
+ // This is EXTREMELY annoying but all the flags are separate
+ // from the points so we have to do some allocation here since
+ // its a dynamic amount and we need to save the values for later.
+ //
+ // Typical glyphs have small point counts, so use stack storage
+ // first while still falling back to the caller's allocator for
+ // unusually large outlines.
+ var flags_stack = std.heap.stackFallback(4096, alloc);
+ const flags_alloc = flags_stack.get();
+ const flags = try flags_alloc.alloc(SimpleFlags, point_count);
+ defer flags_alloc.free(flags);
+ {
+ var point_i: usize = 0;
+ while (point_i < point_count) {
+ const flag: SimpleFlags = @bitCast(try reader.readByte());
+ flags[point_i] = flag;
+ point_i += 1;
+
+ if (flag.repeat) {
+ const repeat_count: usize = try reader.readByte();
+ if (point_i + repeat_count > point_count) return error.TooManyPoints;
+
+ for (0..repeat_count) |_| {
+ flags[point_i] = flag;
+ point_i += 1;
+ }
+ }
+ }
+ }
+
+ // Go through x coordinate deltas
+ var x: i32 = 0;
+ for (flags, points) |flag, *point| {
+ const dx: i32 = if (flag.x_short) short: {
+ break :short if (flag.x_repeat_or_sign)
+ @as(i32, try reader.readByte())
+ else
+ -@as(i32, try reader.readByte());
+ } else if (!flag.x_repeat_or_sign)
+ @as(i32, try reader.readInt(sfnt.int16, .big))
+ else
+ 0;
+
+ x = std.math.add(
+ i32,
+ x,
+ dx,
+ ) catch return error.CoordinateOverflow;
+ point.x = x;
+ }
+
+ // Go through y coordinate deltas
+ var y: i32 = 0;
+ for (flags, points) |flag, *point| {
+ const dy: i32 = if (flag.y_short) short: {
+ break :short if (flag.y_repeat_or_sign)
+ @as(i32, try reader.readByte())
+ else
+ -@as(i32, try reader.readByte());
+ } else if (!flag.y_repeat_or_sign)
+ @as(i32, try reader.readInt(sfnt.int16, .big))
+ else
+ 0;
+
+ y = std.math.add(
+ i32,
+ y,
+ dy,
+ ) catch return error.CoordinateOverflow;
+ point.y = y;
+ point.on_curve = flag.on_curve;
+ }
+
+ return .{
+ .points = points,
+ .contours = end_points,
+ };
+ }
};
/// Initialize the table from the provided data.
@@ -451,7 +644,37 @@ pub fn getGlyph(font: sfnt.SFNT, index: usize) !struct { usize, Glyf.Entry } {
return .{ end_offset - start_offset, try glyf.entry(start_offset) };
}
+fn testAppendInt(
+ buf: *std.ArrayList(u8),
+ alloc: Allocator,
+ comptime T: type,
+ value: T,
+) !void {
+ var bytes: [@sizeOf(T)]u8 = undefined;
+ std.mem.writeInt(T, &bytes, value, .big);
+ try buf.appendSlice(alloc, &bytes);
+}
+
+fn testAppendHeader(
+ buf: *std.ArrayList(u8),
+ alloc: Allocator,
+ number_of_contours: i16,
+ x_min: i16,
+ y_min: i16,
+ x_max: i16,
+ y_max: i16,
+) !void {
+ try testAppendInt(buf, alloc, i16, number_of_contours);
+ try testAppendInt(buf, alloc, i16, x_min);
+ try testAppendInt(buf, alloc, i16, y_min);
+ try testAppendInt(buf, alloc, i16, x_max);
+ try testAppendInt(buf, alloc, i16, y_max);
+}
+
test "glyf" {
+ // lib-vt source archives intentionally exclude full Ghostty font fixtures.
+ if (comptime @import("terminal_options").artifact == .lib) return error.SkipZigTest;
+
const testing = std.testing;
const alloc = testing.allocator;
// Cozette because it doesn't have any hinting.
@@ -475,13 +698,188 @@ test "glyf" {
try testing.expect(glyph_A.entryType() == .simple);
try testing.expect(len_A >= try glyph_A.size());
+ var outline_A = try glyph_A.decode(alloc);
+ defer outline_A.deinit(alloc);
+ try testing.expectEqual(@as(usize, @intCast(glyph_A.header.numberOfContours)), outline_A.contours.len);
+ try testing.expect(outline_A.points.len > 0);
+
// Glyph "Ĩ" is at index 265.
const len_Itilde, const glyph_Itilde = try getGlyph(font, 265);
try testing.expect(glyph_Itilde.entryType() == .simple);
try testing.expect(len_Itilde >= try glyph_Itilde.size());
}
+test "glyf: decode triangle" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var buf: std.ArrayList(u8) = .empty;
+ defer buf.deinit(alloc);
+
+ try testAppendHeader(&buf, alloc, 1, 100, 100, 900, 900);
+ try testAppendInt(&buf, alloc, u16, 2); // endPtsOfContours[0]
+ try testAppendInt(&buf, alloc, u16, 0); // instructionLength
+ try buf.append(alloc, 0x01); // on curve
+ try buf.append(alloc, 0x01); // on curve
+ try buf.append(alloc, 0x01); // on curve
+ try testAppendInt(&buf, alloc, i16, 500);
+ try testAppendInt(&buf, alloc, i16, -400);
+ try testAppendInt(&buf, alloc, i16, 800);
+ try testAppendInt(&buf, alloc, i16, 900);
+ try testAppendInt(&buf, alloc, i16, -800);
+ try testAppendInt(&buf, alloc, i16, 0);
+
+ const glyph = try Glyf.Entry.init(buf.items);
+ var outline = try glyph.decode(alloc);
+ defer outline.deinit(alloc);
+
+ try testing.expectEqual(@as(i16, 100), glyph.header.xMin);
+ try testing.expectEqual(@as(i16, 900), glyph.header.xMax);
+ try testing.expectEqual(@as(usize, 1), outline.contours.len);
+ try testing.expectEqual(@as(usize, 3), outline.points.len);
+ const contour = outline.contour(0);
+ try testing.expectEqual(@as(usize, 3), contour.len);
+ try testing.expectEqual(Glyf.Outline.Point{ .x = 500, .y = 900, .on_curve = true }, contour[0]);
+ try testing.expectEqual(Glyf.Outline.Point{ .x = 100, .y = 100, .on_curve = true }, contour[1]);
+ try testing.expectEqual(Glyf.Outline.Point{ .x = 900, .y = 100, .on_curve = true }, contour[2]);
+}
+
+test "glyf: decode multiple contours" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var buf: std.ArrayList(u8) = .empty;
+ defer buf.deinit(alloc);
+
+ try testAppendHeader(&buf, alloc, 2, 0, 0, 30, 10);
+ try testAppendInt(&buf, alloc, u16, 1); // first contour ends at point 1
+ try testAppendInt(&buf, alloc, u16, 3); // second contour ends at point 3
+ try testAppendInt(&buf, alloc, u16, 0); // instructionLength
+ for (0..4) |_| try buf.append(alloc, 0x01); // on curve
+ for ([_]i16{ 0, 10, 10, 10 }) |dx| try testAppendInt(&buf, alloc, i16, dx);
+ for ([_]i16{ 0, 0, 10, 0 }) |dy| try testAppendInt(&buf, alloc, i16, dy);
+
+ const glyph = try Glyf.Entry.init(buf.items);
+ var outline = try glyph.decode(alloc);
+ defer outline.deinit(alloc);
+
+ try testing.expectEqual(@as(usize, 2), outline.contours.len);
+ try testing.expectEqual(@as(usize, 4), outline.points.len);
+ try testing.expectEqual(@as(u16, 1), outline.contours[0]);
+ try testing.expectEqual(@as(u16, 3), outline.contours[1]);
+ try testing.expectEqual(@as(usize, 2), outline.contour(0).len);
+ try testing.expectEqual(@as(usize, 2), outline.contour(1).len);
+ try testing.expectEqual(outline.points[0..2].ptr, outline.contour(0).ptr);
+ try testing.expectEqual(outline.points[2..4].ptr, outline.contour(1).ptr);
+}
+
+test "glyf: decode repeat and short vector flags" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var buf: std.ArrayList(u8) = .empty;
+ defer buf.deinit(alloc);
+
+ try testAppendHeader(&buf, alloc, 1, 0, -16, 16, 0);
+ try testAppendInt(&buf, alloc, u16, 3); // four points
+ try testAppendInt(&buf, alloc, u16, 0); // instructionLength
+ try buf.append(alloc, 0x01 | 0x02 | 0x04 | 0x08 | 0x10); // on, x short positive, y short negative, repeat
+ try buf.append(alloc, 3); // repeat for the next three points
+ for ([_]u8{ 1, 2, 4, 8 }) |dx| try buf.append(alloc, dx);
+ for ([_]u8{ 1, 2, 4, 8 }) |dy| try buf.append(alloc, dy);
+
+ const glyph = try Glyf.Entry.init(buf.items);
+ var outline = try glyph.decode(alloc);
+ defer outline.deinit(alloc);
+
+ try testing.expectEqual(@as(usize, 4), outline.points.len);
+ try testing.expectEqual(Glyf.Outline.Point{ .x = 1, .y = -1, .on_curve = true }, outline.points[0]);
+ try testing.expectEqual(Glyf.Outline.Point{ .x = 3, .y = -3, .on_curve = true }, outline.points[1]);
+ try testing.expectEqual(Glyf.Outline.Point{ .x = 7, .y = -7, .on_curve = true }, outline.points[2]);
+ try testing.expectEqual(Glyf.Outline.Point{ .x = 15, .y = -15, .on_curve = true }, outline.points[3]);
+}
+
+test "glyf: decode off curve and same coordinate flags" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var buf: std.ArrayList(u8) = .empty;
+ defer buf.deinit(alloc);
+
+ try testAppendHeader(&buf, alloc, 1, 0, 0, 7, 9);
+ try testAppendInt(&buf, alloc, u16, 1); // two points
+ try testAppendInt(&buf, alloc, u16, 0); // instructionLength
+ try buf.append(alloc, 0x10 | 0x20); // off curve, x same, y same
+ try buf.append(alloc, 0x01 | 0x02 | 0x04 | 0x10 | 0x20); // on curve, short positive x/y
+ try buf.append(alloc, 7); // x delta
+ try buf.append(alloc, 9); // y delta
+
+ const glyph = try Glyf.Entry.init(buf.items);
+ var outline = try glyph.decode(alloc);
+ defer outline.deinit(alloc);
+
+ try testing.expectEqual(Glyf.Outline.Point{ .x = 0, .y = 0, .on_curve = false }, outline.points[0]);
+ try testing.expectEqual(Glyf.Outline.Point{ .x = 7, .y = 9, .on_curve = true }, outline.points[1]);
+}
+
+test "glyf: decode one-point contour" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var buf: std.ArrayList(u8) = .empty;
+ defer buf.deinit(alloc);
+
+ try testAppendHeader(&buf, alloc, 1, 0, 0, 0, 0);
+ try testAppendInt(&buf, alloc, u16, 0); // endPtsOfContours[0]
+ try testAppendInt(&buf, alloc, u16, 0); // instructionLength
+ try buf.append(alloc, 0x01 | 0x10 | 0x20); // on curve, x same, y same
+
+ const glyph = try Glyf.Entry.init(buf.items);
+ var outline = try glyph.decode(alloc);
+ defer outline.deinit(alloc);
+
+ try testing.expectEqual(@as(usize, 1), outline.points.len);
+ try testing.expectEqual(@as(usize, 1), outline.contours.len);
+ try testing.expectEqual(@as(u16, 0), outline.contours[0]);
+ try testing.expectEqual(Glyf.Outline.Point{ .x = 0, .y = 0, .on_curve = true }, outline.contour(0)[0]);
+}
+
+test "glyf: decode contour ending at max point index" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var buf: std.ArrayList(u8) = .empty;
+ defer buf.deinit(alloc);
+
+ try testAppendHeader(&buf, alloc, 1, 0, 0, 0, 0);
+ try testAppendInt(&buf, alloc, u16, std.math.maxInt(u16)); // 65536 points
+ try testAppendInt(&buf, alloc, u16, 0); // instructionLength
+
+ const flag = 0x01 | 0x10 | 0x20; // on curve, x same, y same
+ var remaining: usize = @as(usize, std.math.maxInt(u16)) + 1;
+ while (remaining > 0) {
+ const run = @min(remaining, 256);
+ if (run == 1) {
+ try buf.append(alloc, flag);
+ } else {
+ try buf.append(alloc, flag | 0x08); // repeat
+ try buf.append(alloc, @intCast(run - 1));
+ }
+ remaining -= run;
+ }
+
+ const glyph = try Glyf.Entry.init(buf.items);
+ var outline = try glyph.decode(alloc);
+ defer outline.deinit(alloc);
+
+ try testing.expectEqual(@as(usize, 65536), outline.points.len);
+ try testing.expectEqual(@as(usize, 65536), outline.contour(0).len);
+}
+
test "glyf: reject glyphs with instructions and composite glyphs" {
+ // lib-vt source archives intentionally exclude full Ghostty font fixtures.
+ if (comptime @import("terminal_options").artifact == .lib) return error.SkipZigTest;
+
const testing = std.testing;
const alloc = testing.allocator;
const test_font = @import("../embedded.zig").jetbrains_mono;
@@ -496,6 +894,10 @@ test "glyf: reject glyphs with instructions and composite glyphs" {
Glyf.Entry.SizeError.InstructionsNotSupported,
glyph_notdef.size(),
);
+ try testing.expectError(
+ Glyf.Entry.DecodeError.InstructionsNotSupported,
+ glyph_notdef.decode(alloc),
+ );
// Glyph "Á" is at index 2.
const len_Aacute, const glyph_Aacute = try getGlyph(font, 2);
@@ -505,9 +907,16 @@ test "glyf: reject glyphs with instructions and composite glyphs" {
Glyf.Entry.SizeError.CompositeNotSupported,
glyph_Aacute.size(),
);
+ try testing.expectError(
+ Glyf.Entry.DecodeError.CompositeNotSupported,
+ glyph_Aacute.decode(alloc),
+ );
}
test "glyf: reject truncated" {
+ // lib-vt source archives intentionally exclude full Ghostty font fixtures.
+ if (comptime @import("terminal_options").artifact == .lib) return error.SkipZigTest;
+
const testing = std.testing;
const alloc = testing.allocator;
// Cozette because it doesn't have any hinting.
@@ -522,9 +931,13 @@ test "glyf: reject truncated" {
// it before the full length (which is 228 bytes).
glyph_nul.data = glyph_nul.data[0 .. 227 - @sizeOf(Glyf.Entry.Header)];
try testing.expectError(Glyf.Entry.SizeError.EndOfStream, glyph_nul.size());
+ try testing.expectError(Glyf.Entry.DecodeError.EndOfStream, glyph_nul.decode(alloc));
}
test "glyf: reject endpoints out of order" {
+ // lib-vt source archives intentionally exclude full Ghostty font fixtures.
+ if (comptime @import("terminal_options").artifact == .lib) return error.SkipZigTest;
+
const testing = std.testing;
const alloc = testing.allocator;
// Cozette because it doesn't have any hinting.
@@ -544,9 +957,16 @@ test "glyf: reject endpoints out of order" {
// copied, we can just const cast it back to mutable lol.
std.mem.bytesAsSlice(u16, @as([]u8, @constCast(glyph_nul.data)))[3] = 0;
try testing.expectError(Glyf.Entry.SizeError.EndPointsOutOfOrder, glyph_nul.size());
+ try testing.expectError(
+ Glyf.Entry.DecodeError.EndPointsOutOfOrder,
+ glyph_nul.decode(alloc),
+ );
}
test "glyf: reject too many points" {
+ // lib-vt source archives intentionally exclude full Ghostty font fixtures.
+ if (comptime @import("terminal_options").artifact == .lib) return error.SkipZigTest;
+
const testing = std.testing;
const alloc = testing.allocator;
// Cozette because it doesn't have any hinting.
@@ -568,10 +988,12 @@ test "glyf: reject too many points" {
@as([]u8, @constCast(glyph_nul.data))[107] |= 0x08;
@as([]u8, @constCast(glyph_nul.data))[108] = 0xFF;
try testing.expectError(Glyf.Entry.SizeError.TooManyPoints, glyph_nul.size());
+ try testing.expectError(Glyf.Entry.DecodeError.TooManyPoints, glyph_nul.decode(alloc));
}
test "glyf: zero-contour glyph can be header-only" {
const testing = std.testing;
+ const alloc = testing.allocator;
const header: Glyf.Entry.Header = .{
.numberOfContours = 0,
@@ -582,4 +1004,43 @@ test "glyf: zero-contour glyph can be header-only" {
};
const glyph = try Glyf.Entry.init(std.mem.asBytes(&header));
try testing.expectEqual(@sizeOf(Glyf.Entry.Header), try glyph.size());
+
+ var outline = try glyph.decode(alloc);
+ defer outline.deinit(alloc);
+ try testing.expectEqual(@as(usize, 0), outline.points.len);
+ try testing.expectEqual(@as(usize, 0), outline.contours.len);
+}
+
+test "glyf: zero-contour glyph can include instruction length" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var buf: std.ArrayList(u8) = .empty;
+ defer buf.deinit(alloc);
+
+ try testAppendHeader(&buf, alloc, 0, 0, 0, 0, 0);
+ try testAppendInt(&buf, alloc, u16, 0); // instructionLength
+
+ const glyph = try Glyf.Entry.init(buf.items);
+ try testing.expectEqual(@sizeOf(Glyf.Entry.Header) + 2, try glyph.size());
+
+ var outline = try glyph.decode(alloc);
+ defer outline.deinit(alloc);
+ try testing.expectEqual(@as(usize, 0), outline.points.len);
+ try testing.expectEqual(@as(usize, 0), outline.contours.len);
+}
+
+test "glyf: zero-contour glyph rejects instructions" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var buf: std.ArrayList(u8) = .empty;
+ defer buf.deinit(alloc);
+
+ try testAppendHeader(&buf, alloc, 0, 0, 0, 0, 0);
+ try testAppendInt(&buf, alloc, u16, 1); // instructionLength
+
+ const glyph = try Glyf.Entry.init(buf.items);
+ try testing.expectError(Glyf.Entry.SizeError.InstructionsNotSupported, glyph.size());
+ try testing.expectError(Glyf.Entry.DecodeError.InstructionsNotSupported, glyph.decode(alloc));
}
diff --git a/vendor/libghostty-vt/src/font/opentype/sfnt.zig b/vendor/libghostty-vt/src/font/opentype/sfnt.zig
index 9373cda0..55a9a0bd 100644
--- a/vendor/libghostty-vt/src/font/opentype/sfnt.zig
+++ b/vendor/libghostty-vt/src/font/opentype/sfnt.zig
@@ -285,6 +285,9 @@ pub const SFNT = struct {
const native_endian = @import("builtin").target.cpu.arch.endian();
test "parse font" {
+ // lib-vt source archives intentionally exclude full Ghostty font fixtures.
+ if (comptime @import("terminal_options").artifact == .lib) return error.SkipZigTest;
+
const testing = std.testing;
const alloc = testing.allocator;
@@ -298,6 +301,9 @@ test "parse font" {
}
test "get table" {
+ // lib-vt source archives intentionally exclude full Ghostty font fixtures.
+ if (comptime @import("terminal_options").artifact == .lib) return error.SkipZigTest;
+
const testing = std.testing;
const alloc = testing.allocator;
diff --git a/vendor/libghostty-vt/src/font/shaper/coretext.zig b/vendor/libghostty-vt/src/font/shaper/coretext.zig
index 3f69af6d..8a9374bd 100644
--- a/vendor/libghostty-vt/src/font/shaper/coretext.zig
+++ b/vendor/libghostty-vt/src/font/shaper/coretext.zig
@@ -1333,7 +1333,7 @@ test "shape emoji width long" {
var t = try terminal.Terminal.init(alloc, .{ .cols = 30, .rows = 3 });
defer t.deinit(alloc);
- var page = t.screens.active.pages.pages.first.?.data;
+ var page = t.screens.active.pages.pages.first.?.page();
var row = page.getRow(1);
const cell = &row.cells.ptr(page.memory)[0];
cell.* = .{
diff --git a/vendor/libghostty-vt/src/font/shaper/harfbuzz.zig b/vendor/libghostty-vt/src/font/shaper/harfbuzz.zig
index a400ecaf..996aabff 100644
--- a/vendor/libghostty-vt/src/font/shaper/harfbuzz.zig
+++ b/vendor/libghostty-vt/src/font/shaper/harfbuzz.zig
@@ -811,7 +811,7 @@ test "shape emoji width long" {
);
defer t.deinit(alloc);
- var page = t.screens.active.pages.pages.first.?.data;
+ var page = t.screens.active.pages.pages.first.?.page();
var row = page.getRow(1);
const cell = &row.cells.ptr(page.memory)[0];
cell.* = .{
diff --git a/vendor/libghostty-vt/src/font/sprite/Face.zig b/vendor/libghostty-vt/src/font/sprite/Face.zig
index 596a9204..fd029d13 100644
--- a/vendor/libghostty-vt/src/font/sprite/Face.zig
+++ b/vendor/libghostty-vt/src/font/sprite/Face.zig
@@ -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)) {
@@ -205,7 +205,16 @@ pub fn renderGlyph(
else => |width| metrics.cell_width * width,
};
- const height = metrics.cell_height;
+ // Sprite glyphs generally get the full cell height, but cursor glyphs need
+ // to be affected by `adjust-cursor-height`, so we use `cursor_height` for
+ // the height if it's one of the full-height cursors.
+ const height = switch (cp) {
+ @intFromEnum(Sprite.cursor_rect),
+ @intFromEnum(Sprite.cursor_hollow_rect),
+ @intFromEnum(Sprite.cursor_bar),
+ => metrics.cursor_height,
+ else => metrics.cell_height,
+ };
const padding_x = width / 4;
const padding_y = height / 4;
@@ -219,11 +228,32 @@ pub fn renderGlyph(
// Write the drawing to the atlas
const region = try canvas.writeAtlas(alloc, atlas);
+ // The X offset is the displacement from the left edge of the cell that our
+ // drawn sprite will be drawn in the grid. That's the same as the distance
+ // we have calculated from the left of the canvas, minus the padding, since
+ // the padding represents extra pixels to the left of the cell.
+ const offset_x: i32 =
+ @as(i32, @intCast(canvas.clip_left)) -
+ @as(i32, @intCast(padding_x));
+ // Similar logic applies for the Y offset, but with the additional factor
+ // that we want to re-center cursor glyphs in the cell if they were drawn
+ // taller or shorter than a normal cell is; this only applies to cursors
+ // currently, but could conceivably apply to other things in the future.
+ const offset_y: i32 =
+ @as(i32, @intCast(region.height +| canvas.clip_bottom)) -
+ @as(i32, @intCast(padding_y)) +
+ @divTrunc(
+ // By adding half the difference between the cell height and the
+ // height we passed to the draw function, we center it in the cell.
+ @as(i32, @intCast(metrics.cell_height)) - @as(i32, @intCast(height)),
+ 2,
+ );
+
return .{
.width = region.width,
.height = region.height,
- .offset_x = @as(i32, @intCast(canvas.clip_left)) - @as(i32, @intCast(padding_x)),
- .offset_y = @as(i32, @intCast(region.height +| canvas.clip_bottom)) - @as(i32, @intCast(padding_y)),
+ .offset_x = offset_x,
+ .offset_y = offset_y,
.atlas_x = region.x,
.atlas_y = region.y,
};
@@ -535,6 +565,92 @@ test "sprite face render all sprites" {
try std.testing.expect(!diff); // There should be no diffs from reference.
}
+test "full height cursor sprites respect cursor height metric" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var atlas: font.Atlas = try .init(alloc, 128, .grayscale);
+ defer atlas.deinit(alloc);
+
+ // face with a 8x16 cell dimension.
+ var face: Face = .{
+ .metrics = .calc(.{
+ // Fudged number, not used in anything we care about here.
+ .px_per_em = 16,
+
+ .cell_width = 8.0,
+ .ascent = 12.0,
+ .descent = -4.0,
+ .line_gap = 0.0,
+ }),
+ };
+
+ try testing.expectEqual(16, face.metrics.cell_height);
+
+ // --- smaller than cell height ---
+ face.metrics.cursor_height = 12;
+ // bar
+ {
+ const glyph = try face.renderGlyph(alloc, &atlas, @intFromEnum(Sprite.cursor_bar), .{ .grid_metrics = face.metrics });
+ try testing.expectEqual(12, glyph.height);
+ try testing.expectEqual(14, glyph.offset_y);
+ }
+ // rect
+ {
+ const glyph = try face.renderGlyph(alloc, &atlas, @intFromEnum(Sprite.cursor_rect), .{ .grid_metrics = face.metrics });
+ try testing.expectEqual(12, glyph.height);
+ try testing.expectEqual(14, glyph.offset_y);
+ }
+ // hollow rect
+ {
+ const glyph = try face.renderGlyph(alloc, &atlas, @intFromEnum(Sprite.cursor_hollow_rect), .{ .grid_metrics = face.metrics });
+ try testing.expectEqual(12, glyph.height);
+ try testing.expectEqual(14, glyph.offset_y);
+ }
+
+ // --- equal to the cell height ---
+ face.metrics.cursor_height = 16;
+ // bar
+ {
+ const glyph = try face.renderGlyph(alloc, &atlas, @intFromEnum(Sprite.cursor_bar), .{ .grid_metrics = face.metrics });
+ try testing.expectEqual(16, glyph.height);
+ try testing.expectEqual(16, glyph.offset_y);
+ }
+ // rect
+ {
+ const glyph = try face.renderGlyph(alloc, &atlas, @intFromEnum(Sprite.cursor_rect), .{ .grid_metrics = face.metrics });
+ try testing.expectEqual(16, glyph.height);
+ try testing.expectEqual(16, glyph.offset_y);
+ }
+ // hollow rect
+ {
+ const glyph = try face.renderGlyph(alloc, &atlas, @intFromEnum(Sprite.cursor_hollow_rect), .{ .grid_metrics = face.metrics });
+ try testing.expectEqual(16, glyph.height);
+ try testing.expectEqual(16, glyph.offset_y);
+ }
+
+ // --- greater than the cell height ---
+ face.metrics.cursor_height = 20;
+ // bar
+ {
+ const glyph = try face.renderGlyph(alloc, &atlas, @intFromEnum(Sprite.cursor_bar), .{ .grid_metrics = face.metrics });
+ try testing.expectEqual(20, glyph.height);
+ try testing.expectEqual(18, glyph.offset_y);
+ }
+ // rect
+ {
+ const glyph = try face.renderGlyph(alloc, &atlas, @intFromEnum(Sprite.cursor_rect), .{ .grid_metrics = face.metrics });
+ try testing.expectEqual(20, glyph.height);
+ try testing.expectEqual(18, glyph.offset_y);
+ }
+ // hollow rect
+ {
+ const glyph = try face.renderGlyph(alloc, &atlas, @intFromEnum(Sprite.cursor_hollow_rect), .{ .grid_metrics = face.metrics });
+ try testing.expectEqual(20, glyph.height);
+ try testing.expectEqual(18, glyph.offset_y);
+ }
+}
+
// test "sprite face print all sprites" {
// std.debug.print("\n\n", .{});
// inline for (ranges) |range| {
diff --git a/vendor/libghostty-vt/src/font/testdata/glyf_rasterize.png b/vendor/libghostty-vt/src/font/testdata/glyf_rasterize.png
new file mode 100644
index 00000000..f5f8fb81
--- /dev/null
+++ b/vendor/libghostty-vt/src/font/testdata/glyf_rasterize.png
Binary files differ