mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-10-09 11:26:41 +00:00
fix(font): Fix positioning of scaled glyphs that don’t specify alignment (#8990)
Follow-up to #8563, which broke scaling without alignment. This change recovers the behavior from before #8563, such that a scaled group is clamped to the constraint width and height if necessary, and otherwise, scaling does not shift the center of the group bounding box. As a part of this change, horizontal alignment was rewritten to assume the face is flush with the left edge of the cell. The cell-to-face offset in the rendering code is then applied regardless of the value of `align_horizontal`. This both simplifies the code and improves consistency, as it ensures that the offset is the same for all non-bitmap glyphs (rounded in FreeType, not rounded in CoreText). It's the right thing to do following the align-to-face changes in #8563.
This commit is contained in:
@@ -244,6 +244,39 @@ pub const RenderOptions = struct {
|
|||||||
) GlyphSize {
|
) GlyphSize {
|
||||||
if (!self.doesAnything()) return glyph;
|
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.
|
// For extra wide font faces, never stretch glyphs across two cells.
|
||||||
// This mirrors font_patcher.
|
// This mirrors font_patcher.
|
||||||
const min_constraint_width: u2 = if ((self.size == .stretch) and (metrics.face_width > 0.9 * metrics.face_height))
|
const min_constraint_width: u2 = if ((self.size == .stretch) and (metrics.face_width > 0.9 * metrics.face_height))
|
||||||
@@ -265,15 +298,15 @@ pub const RenderOptions = struct {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
// The new, constrained glyph size
|
// Apply prescribed scaling, preserving the
|
||||||
var constrained_glyph = glyph;
|
// center bearings of the group bounding box
|
||||||
|
|
||||||
// Apply prescribed scaling
|
|
||||||
const width_factor, const height_factor = self.scale_factors(group, metrics, min_constraint_width);
|
const width_factor, const height_factor = self.scale_factors(group, metrics, min_constraint_width);
|
||||||
constrained_glyph.width *= width_factor;
|
const center_x = group.x + (group.width / 2);
|
||||||
constrained_glyph.x *= width_factor;
|
const center_y = group.y + (group.height / 2);
|
||||||
constrained_glyph.height *= height_factor;
|
group.width *= width_factor;
|
||||||
constrained_glyph.y *= height_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
|
// NOTE: font_patcher jumps through a lot of hoops at this
|
||||||
// point to ensure that the glyph remains within the target
|
// point to ensure that the glyph remains within the target
|
||||||
@@ -281,27 +314,17 @@ pub const RenderOptions = struct {
|
|||||||
// This is irrelevant here as we're not rounding, we're
|
// This is irrelevant here as we're not rounding, we're
|
||||||
// staying in f64 and heading straight to rendering.
|
// staying in f64 and heading straight to rendering.
|
||||||
|
|
||||||
// Align vertically
|
// Apply prescribed alignment
|
||||||
if (self.align_vertical != .none) {
|
group.y = self.aligned_y(group, metrics);
|
||||||
// Vertically scale group bounding box.
|
group.x = self.aligned_x(group, metrics, min_constraint_width);
|
||||||
group.height *= height_factor;
|
|
||||||
group.y *= height_factor;
|
|
||||||
|
|
||||||
// Calculate offset and shift the glyph
|
// Transfer the scaling and alignment back to the glyph and return.
|
||||||
constrained_glyph.y += self.offset_vertical(group, metrics);
|
return .{
|
||||||
}
|
.width = width_factor * glyph.width,
|
||||||
|
.height = height_factor * glyph.height,
|
||||||
// Align horizontally
|
.x = group.x + (group.width * self.relative_x),
|
||||||
if (self.align_horizontal != .none) {
|
.y = group.y + (group.height * self.relative_y),
|
||||||
// Horizontally scale group bounding box.
|
};
|
||||||
group.width *= width_factor;
|
|
||||||
group.x *= width_factor;
|
|
||||||
|
|
||||||
// Calculate offset and shift the glyph
|
|
||||||
constrained_glyph.x += self.offset_horizontal(group, metrics, min_constraint_width);
|
|
||||||
}
|
|
||||||
|
|
||||||
return constrained_glyph;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return width and height scaling factors for this scaling group.
|
/// Return width and height scaling factors for this scaling group.
|
||||||
@@ -381,63 +404,88 @@ pub const RenderOptions = struct {
|
|||||||
return .{ width_factor, height_factor };
|
return .{ width_factor, height_factor };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return vertical offset needed to align this group
|
/// Return vertical bearing for aligning this group
|
||||||
fn offset_vertical(
|
fn aligned_y(
|
||||||
self: Constraint,
|
self: Constraint,
|
||||||
group: GlyphSize,
|
group: GlyphSize,
|
||||||
metrics: Metrics,
|
metrics: Metrics,
|
||||||
) f64 {
|
) 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
|
// We use face_height and offset by face_y, rather than
|
||||||
// using cell_height directly, to account for the asymmetry
|
// using cell_height directly, to account for the asymmetry
|
||||||
// of the pixel cell around the face (a consequence of
|
// of the pixel cell around the face (a consequence of
|
||||||
// aligning the baseline with a pixel boundary rather than
|
// aligning the baseline with a pixel boundary rather than
|
||||||
// vertically centering the face).
|
// vertically centering the face).
|
||||||
const new_group_y = metrics.face_y + switch (self.align_vertical) {
|
const pad_bottom_dy = self.pad_bottom * metrics.face_height;
|
||||||
.none => return 0.0,
|
const pad_top_dy = self.pad_top * metrics.face_height;
|
||||||
.start => self.pad_bottom * metrics.face_height,
|
const start_y = metrics.face_y + pad_bottom_dy;
|
||||||
.end => end: {
|
const end_y = metrics.face_y + (metrics.face_height - group.height - pad_top_dy);
|
||||||
const pad_top_dy = self.pad_top * metrics.face_height;
|
const center_y = (start_y + end_y) / 2;
|
||||||
break :end metrics.face_height - pad_top_dy - group.height;
|
return switch (self.align_vertical) {
|
||||||
},
|
// NOTE: Even if there is no prescribed alignment, we ensure
|
||||||
.center, .center1 => (metrics.face_height - group.height) / 2,
|
// 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 new_group_y - group.y;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return horizontal offset needed to align this group
|
/// Return horizontal bearing for aligning this group
|
||||||
fn offset_horizontal(
|
fn aligned_x(
|
||||||
self: Constraint,
|
self: Constraint,
|
||||||
group: GlyphSize,
|
group: GlyphSize,
|
||||||
metrics: Metrics,
|
metrics: Metrics,
|
||||||
min_constraint_width: u2,
|
min_constraint_width: u2,
|
||||||
) f64 {
|
) 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
|
// For multi-cell constraints, we align relative to the span
|
||||||
// from the left edge of the first face cell to the right
|
// from the left edge of the first cell to the right edge of
|
||||||
// edge of the last face cell as they sit within the rounded
|
// the last face cell assuming it's left-aligned within the
|
||||||
// and adjusted pixel cell (centered if narrower than the
|
// rounded and adjusted pixel cell. Any horizontal offset to
|
||||||
// pixel cell, left-aligned if wider).
|
// center the face within the grid cell is the responsibility
|
||||||
const face_x, const full_face_span = facecalcs: {
|
// of the backend-specific rendering code, and should be done
|
||||||
const cell_width: f64 = @floatFromInt(metrics.cell_width);
|
// after applying constraints.
|
||||||
const full_width: f64 = @floatFromInt(min_constraint_width * metrics.cell_width);
|
const full_face_span = metrics.face_width + @as(f64, @floatFromInt((min_constraint_width - 1) * metrics.cell_width));
|
||||||
const cell_margin = cell_width - metrics.face_width;
|
const pad_left_dx = self.pad_left * metrics.face_width;
|
||||||
break :facecalcs .{ @max(0, cell_margin / 2), full_width - cell_margin };
|
const pad_right_dx = self.pad_right * metrics.face_width;
|
||||||
};
|
const start_x = pad_left_dx;
|
||||||
const pad_left_x = self.pad_left * metrics.face_width;
|
const end_x = full_face_span - group.width - pad_right_dx;
|
||||||
const new_group_x = face_x + switch (self.align_horizontal) {
|
return switch (self.align_horizontal) {
|
||||||
.none => return 0.0,
|
// NOTE: Even if there is no prescribed alignment, we ensure
|
||||||
.start => pad_left_x,
|
// that the glyph doesn't protrude outside the padded cell,
|
||||||
.end => end: {
|
// since this is implied by every available size constraint. The
|
||||||
const pad_right_dx = self.pad_right * metrics.face_width;
|
// left-side bound has priority if the group is too wide, though
|
||||||
break :end @max(pad_left_x, full_face_span - pad_right_dx - group.width);
|
// if we hit the .none prong we always have self.size != .none,
|
||||||
},
|
// so this should never happen.
|
||||||
.center => @max(pad_left_x, (full_face_span - group.width) / 2),
|
.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
|
// NOTE: .center1 implements the font_patcher rule of centering
|
||||||
// in the first cell even for multi-cell constraints. Since glyphs
|
// in the first cell even for multi-cell constraints. Since glyphs
|
||||||
// are not allowed to protrude to the left, this results in the
|
// are not allowed to protrude to the left, this results in the
|
||||||
// left-alignment like .start when the glyph is wider than a cell.
|
// left-alignment like .start when the glyph is wider than a cell.
|
||||||
.center1 => @max(pad_left_x, (metrics.face_width - group.width) / 2),
|
.center1 => center1: {
|
||||||
|
const end1_x = metrics.face_width - group.width - pad_right_dx;
|
||||||
|
break :center1 @max(start_x, (start_x + end1_x) / 2);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
return new_group_x - group.x;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@@ -338,14 +338,7 @@ pub const Face = struct {
|
|||||||
const cell_height: f64 = @floatFromInt(metrics.cell_height);
|
const cell_height: f64 = @floatFromInt(metrics.cell_height);
|
||||||
|
|
||||||
// Next we apply any constraints to get the final size of the glyph.
|
// Next we apply any constraints to get the final size of the glyph.
|
||||||
var constraint = opts.constraint;
|
const constraint = opts.constraint;
|
||||||
|
|
||||||
// We eliminate any negative vertical padding since these overlap
|
|
||||||
// values aren't needed with how precisely we apply constraints,
|
|
||||||
// and they can lead to extra height that looks bad for things like
|
|
||||||
// powerline glyphs.
|
|
||||||
constraint.pad_top = @max(0.0, constraint.pad_top);
|
|
||||||
constraint.pad_bottom = @max(0.0, constraint.pad_bottom);
|
|
||||||
|
|
||||||
// We need to add the baseline position before passing to the constrain
|
// We need to add the baseline position before passing to the constrain
|
||||||
// function since it operates on cell-relative positions, not baseline.
|
// function since it operates on cell-relative positions, not baseline.
|
||||||
@@ -370,11 +363,7 @@ pub const Face = struct {
|
|||||||
// We center all glyphs within the pixel-rounded and adjusted
|
// We center all glyphs within the pixel-rounded and adjusted
|
||||||
// cell width if it's larger than the face width, so that they
|
// cell width if it's larger than the face width, so that they
|
||||||
// aren't weirdly off to the left.
|
// aren't weirdly off to the left.
|
||||||
//
|
if (metrics.face_width < cell_width) {
|
||||||
// We don't do this if the constraint has a horizontal alignment,
|
|
||||||
// since in that case the position was already calculated with the
|
|
||||||
// new cell width in mind.
|
|
||||||
if ((opts.constraint.align_horizontal == .none) and (metrics.face_width < cell_width)) {
|
|
||||||
// We add half the difference to re-center.
|
// We add half the difference to re-center.
|
||||||
x += (cell_width - metrics.face_width) / 2;
|
x += (cell_width - metrics.face_width) / 2;
|
||||||
}
|
}
|
||||||
@@ -389,6 +378,18 @@ pub const Face = struct {
|
|||||||
y = @round(y);
|
y = @round(y);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We center all glyphs within the pixel-rounded and adjusted
|
||||||
|
// cell width if it's larger than the face width, so that they
|
||||||
|
// aren't weirdly off to the left.
|
||||||
|
//
|
||||||
|
// We don't do this if the glyph has a stretch constraint,
|
||||||
|
// since in that case the position was already calculated with the
|
||||||
|
// new cell width in mind.
|
||||||
|
if ((constraint.size != .stretch) and (metrics.face_width < cell_width)) {
|
||||||
|
// We add half the difference to re-center.
|
||||||
|
x += (cell_width - metrics.face_width) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
// We make an assumption that font smoothing ("thicken")
|
// We make an assumption that font smoothing ("thicken")
|
||||||
// adds no more than 1 extra pixel to any edge. We don't
|
// adds no more than 1 extra pixel to any edge. We don't
|
||||||
// add extra size if it's a sbix color font though, since
|
// add extra size if it's a sbix color font though, since
|
||||||
|
@@ -463,14 +463,7 @@ pub const Face = struct {
|
|||||||
const cell_height: f64 = @floatFromInt(metrics.cell_height);
|
const cell_height: f64 = @floatFromInt(metrics.cell_height);
|
||||||
|
|
||||||
// Next we apply any constraints to get the final size of the glyph.
|
// Next we apply any constraints to get the final size of the glyph.
|
||||||
var constraint = opts.constraint;
|
const constraint = opts.constraint;
|
||||||
|
|
||||||
// We eliminate any negative vertical padding since these overlap
|
|
||||||
// values aren't needed with how precisely we apply constraints,
|
|
||||||
// and they can lead to extra height that looks bad for things like
|
|
||||||
// powerline glyphs.
|
|
||||||
constraint.pad_top = @max(0.0, constraint.pad_top);
|
|
||||||
constraint.pad_bottom = @max(0.0, constraint.pad_bottom);
|
|
||||||
|
|
||||||
// We need to add the baseline position before passing to the constrain
|
// We need to add the baseline position before passing to the constrain
|
||||||
// function since it operates on cell-relative positions, not baseline.
|
// function since it operates on cell-relative positions, not baseline.
|
||||||
@@ -496,10 +489,10 @@ pub const Face = struct {
|
|||||||
// cell width if it's larger than the face width, so that they
|
// cell width if it's larger than the face width, so that they
|
||||||
// aren't weirdly off to the left.
|
// aren't weirdly off to the left.
|
||||||
//
|
//
|
||||||
// We don't do this if the constraint has a horizontal alignment,
|
// We don't do this if the glyph has a stretch constraint,
|
||||||
// since in that case the position was already calculated with the
|
// since in that case the position was already calculated with the
|
||||||
// new cell width in mind.
|
// new cell width in mind.
|
||||||
if ((opts.constraint.align_horizontal == .none) and (metrics.face_width < cell_width)) {
|
if ((constraint.size != .stretch) and (metrics.face_width < cell_width)) {
|
||||||
// We add half the difference to re-center.
|
// We add half the difference to re-center.
|
||||||
//
|
//
|
||||||
// NOTE: We round this to a whole-pixel amount because under
|
// NOTE: We round this to a whole-pixel amount because under
|
||||||
|
Reference in New Issue
Block a user