Add text last-line alignment modes for "Justify Center", "Justify Right", and "Justify All" (#4024)

* feat: Add justify proper alignments(right/left/centre/all)

* chore: fmt

* chore: refactor

* Cleanup

* Fix crash when changing selected layer's alignment from the Text tool control bar

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Jatin Bharti
2026-05-06 19:16:19 -07:00
committed by GitHub
co-authored by Keavon Chambers
parent 1c2ac19b16
commit c0a8241f50
8 changed files with 142 additions and 32 deletions
+37 -1
View File
@@ -108,14 +108,50 @@ impl TextContext {
})
.unwrap_or_default();
let alignment_width = typesetting.max_width.map(|w| w as f32).unwrap_or_else(|| layout.full_width());
let last_line_correction = typesetting.align.last_line_correction();
let mut path_builder = PathBuilder::new(per_glyph_items, layout.scale() as f64, text_frame_size, first_glyph_offset);
for line in layout.lines() {
let range = line.text_range();
// Parley always includes a hard-break `\n` as the last byte of the preceding line's range, so the line
// is at the end of a paragraph if it's the very last line of the buffer or its text ends with `\n`.
let is_last_para_line = range.end == text.len() || text.get(range.clone()).is_some_and(|s| s.ends_with('\n'));
let (x_offset, space_extra) = if let (true, Some(correction)) = (is_last_para_line, last_line_correction) {
let metrics = line.metrics();
let content_advance = metrics.advance - metrics.trailing_whitespace;
let free_space = alignment_width - content_advance;
match correction {
parley::Alignment::Center => (free_space * 0.5, 0.),
parley::Alignment::Right => (free_space, 0.),
parley::Alignment::Justify => {
// Exclude trailing-whitespace clusters from the divisor so the redistribution stretches only the internal spaces.
// Parley's `trailing_whitespace` is in advance units, not bytes, so we re-derive the byte boundary here to filter cluster ranges.
let line_text = text.get(range.clone()).unwrap_or("");
let trailing_len = line_text.len() - line_text.trim_end().len();
let visible_end_index = range.end - trailing_len;
let space_count: usize = line
.runs()
.map(|run| run.clusters().filter(|c| c.is_space_or_nbsp() && c.text_range().start < visible_end_index).count())
.sum();
let extra = if space_count > 0 { free_space / space_count as f32 } else { 0. };
(0., extra)
}
_ => (0., 0.),
}
} else {
(0., 0.)
};
for item in line.items() {
if let PositionedLayoutItem::GlyphRun(glyph_run) = item
&& typesetting.max_height.filter(|&max_height| glyph_run.baseline() > max_height as f32).is_none()
{
path_builder.render_glyph_run(&glyph_run, typesetting.tilt, per_glyph_items);
path_builder.render_glyph_run(&glyph_run, typesetting.tilt, per_glyph_items, x_offset, space_extra);
}
}
}