Always clamp scaled glyph to cell

Also take padding into account for centered alignment, necessary since
our constraint type allows asymmetric padding.
This commit is contained in:
Daniel Wennberg
2025-10-01 15:21:41 -07:00
committed by Mitchell Hashimoto
parent 7acf617763
commit 32f8c71be3
3 changed files with 64 additions and 52 deletions

View File

@@ -281,14 +281,9 @@ 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);
group.y += self.offset_vertical(group, metrics); group.x = self.aligned_x(group, metrics, min_constraint_width);
}
// Align horizontally
if (self.align_horizontal != .none) {
group.x += self.offset_horizontal(group, metrics, min_constraint_width);
}
// Transfer the scaling and alignment back to the glyph and return. // Transfer the scaling and alignment back to the glyph and return.
return .{ return .{
@@ -376,63 +371,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;
} }
}; };
}; };

View File

@@ -370,11 +370,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;
} }

View File

@@ -495,11 +495,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.
// //
// NOTE: We round this to a whole-pixel amount because under // NOTE: We round this to a whole-pixel amount because under