mirror of
https://github.com/storytold/filmcraft.git
synced 2026-11-02 18:09:21 +00:00
text: count trailing spaces at a hard line end into the alignment width (#3743)
* text: count trailing spaces at a hard line end into the alignment width (#3728) Centred and right-aligned lines were positioned by their content width with trailing whitespace excluded, so spaces typed at the end of a line did nothing. The last line of a paragraph now aligns by content plus its trailing spaces; soft-wrapped lines still let their trailing space hang, and Line.width and decorations still cover the content only. Tested with two layout tests: a centred line with three trailing spaces moves by 1.5 space widths (dx was 0 before), and a wrapped line is unchanged. Fixes #3728 * text: keep left and justified right-to-left lines at the edge (#3728)
This commit is contained in:
@@ -373,6 +373,9 @@ struct ParaLine {
|
||||
carets: Vec<(usize, f32)>,
|
||||
content: f32,
|
||||
left_trim: f32,
|
||||
/// Width of the trailing whitespace that counts into the alignment width: a hard line end (the last line of a
|
||||
/// paragraph) keeps its spaces, a soft-wrapped line lets them hang.
|
||||
trail: f32,
|
||||
ascent: f32,
|
||||
descent: f32,
|
||||
/// Underlined segments `(x0, x1, style)` in line coordinates (rich layouts only).
|
||||
@@ -422,6 +425,7 @@ fn paragraph(text: &str, base: usize, sty: &Styles, para: &ParagraphStyle) -> Ve
|
||||
carets: vec![(base, 0.0)],
|
||||
content: 0.0,
|
||||
left_trim: 0.0,
|
||||
trail: 0.0,
|
||||
ascent: pm.ascent,
|
||||
descent: pm.descent,
|
||||
underlines: Vec::new(),
|
||||
@@ -706,6 +710,7 @@ impl Para<'_> {
|
||||
carets,
|
||||
content: content_w,
|
||||
left_trim: if base_rtl { tw } else { 0.0 },
|
||||
trail: if li + 1 == nlines { tw } else { 0.0 },
|
||||
ascent,
|
||||
descent,
|
||||
underlines,
|
||||
@@ -754,14 +759,19 @@ pub fn layout_rich_uncached(text: &str, style: &TextStyle, runs: &[StyleRun], pa
|
||||
}
|
||||
}
|
||||
prev_desc = pl.descent;
|
||||
// spaces at a hard line end count into the width the line is aligned by; in a right-to-left paragraph they
|
||||
// sit left of the content, so the block starts at the aligned position instead of the content; left and
|
||||
// justified lines keep the content at the edge
|
||||
let aw = pl.content + pl.trail;
|
||||
let hang = if matches!(para.align, Align::Center | Align::Right) { pl.trail } else { 0.0 };
|
||||
let shift = match (para.width, para.align) {
|
||||
(None, Align::Left | Align::Justify) => 0.0,
|
||||
(None, Align::Center) => -pl.content / 2.0,
|
||||
(None, Align::Right) => -pl.content,
|
||||
(None, Align::Center) => -aw / 2.0,
|
||||
(None, Align::Right) => -aw,
|
||||
(Some(_), Align::Left | Align::Justify) => 0.0,
|
||||
(Some(w), Align::Center) => (w - pl.content) / 2.0,
|
||||
(Some(w), Align::Right) => w - pl.content,
|
||||
} - pl.left_trim;
|
||||
(Some(w), Align::Center) => (w - aw) / 2.0,
|
||||
(Some(w), Align::Right) => w - aw,
|
||||
} - if pl.left_trim > 0.0 { pl.left_trim - hang } else { 0.0 };
|
||||
let g0 = lay.glyphs.len();
|
||||
lay.glyphs.extend(pl.glyphs.into_iter().map(|mut g| {
|
||||
g.x += shift;
|
||||
@@ -971,3 +981,81 @@ pub fn measure(text: &str, style: &TextStyle) -> f32 {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod trailing_space_tests {
|
||||
use super::*;
|
||||
|
||||
fn lines(text: &str, width: Option<f32>) -> Vec<Line> {
|
||||
let st = TextStyle { size: 40.0, ..Default::default() };
|
||||
let para = ParagraphStyle { align: Align::Center, width, ..Default::default() };
|
||||
layout_uncached(text, &st, ¶).lines
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_spaces_at_a_hard_line_end_shift_a_centred_line() {
|
||||
let plain = lines("ab\ncd", None);
|
||||
let spaced = lines("ab \ncd", None);
|
||||
let space_w = lines("a b", None)[0].width - lines("ab", None)[0].width;
|
||||
assert!(space_w > 0.0);
|
||||
// the content moves left by half of the three spaces' width; the width of the content itself is unchanged
|
||||
let dx = plain[0].x - spaced[0].x;
|
||||
assert!((dx - 1.5 * space_w).abs() < 0.01, "dx {dx}, space {space_w}");
|
||||
assert_eq!(plain[0].width, spaced[0].width);
|
||||
assert_eq!(plain[1].x, spaced[1].x);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn soft_wrapped_lines_still_let_their_trailing_space_hang() {
|
||||
let wrapped = lines("ab cd", Some(lines("ab", None)[0].width + 4.0));
|
||||
let alone = lines("ab", Some(lines("ab", None)[0].width + 4.0));
|
||||
assert_eq!(wrapped.len(), 2);
|
||||
assert_eq!(wrapped[0].x, alone[0].x);
|
||||
}
|
||||
|
||||
fn aligned(text: &str, align: Align, width: Option<f32>) -> Vec<Line> {
|
||||
let st = TextStyle { size: 40.0, ..Default::default() };
|
||||
let para = ParagraphStyle { align, width, ..Default::default() };
|
||||
layout_uncached(text, &st, ¶).lines
|
||||
}
|
||||
|
||||
fn space_width() -> f32 {
|
||||
lines("a b", None)[0].width - lines("ab", None)[0].width
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rtl_left_and_justified_lines_keep_their_content_at_the_edge() {
|
||||
for align in [Align::Left, Align::Justify] {
|
||||
for width in [None, Some(400.0)] {
|
||||
let plain = aligned("\u{5d0}\u{5d1}", align, width);
|
||||
let spaced = aligned("\u{5d0}\u{5d1} ", align, width);
|
||||
assert_eq!(plain[0].x, spaced[0].x, "{align:?} {width:?}");
|
||||
assert_eq!(plain[0].width, spaced[0].width, "{align:?} {width:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trailing_spaces_shift_a_right_aligned_last_line_by_their_full_width() {
|
||||
let plain = aligned("ab\ncd", Align::Right, Some(400.0));
|
||||
let spaced = aligned("ab\ncd ", Align::Right, Some(400.0));
|
||||
assert_eq!(plain[0].x, spaced[0].x);
|
||||
let dx = plain[1].x - spaced[1].x;
|
||||
assert!((dx - 3.0 * space_width()).abs() < 0.01, "dx {dx}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_and_spaces_only_paragraphs_lay_out_sanely() {
|
||||
for align in [Align::Left, Align::Center, Align::Right, Align::Justify] {
|
||||
for width in [None, Some(400.0)] {
|
||||
for text in ["", " ", "ab\n", "ab\n "] {
|
||||
for l in aligned(text, align, width) {
|
||||
assert!(l.x.is_finite() && l.width.is_finite() && l.width >= 0.0, "{text:?} {align:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let empty = aligned("", Align::Center, Some(400.0));
|
||||
assert!(empty.iter().all(|l| l.width == 0.0));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user