From 3677a1647450dd056e15d80c8a54c670cd3633c0 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Tue, 20 Oct 2026 18:29:51 -0700 Subject: [PATCH] Type: the Every-line Composer ends a line after the spaces that follow it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The composer offered a break after a dash, a Thai word or a space even when a space came next, so it could start the next line with that space. A leading space makes the next line wider, and the composer could score that break better. With mixed type sizes now composed in rectangular area type, Thai text whose words come from a Thai font broke as "สวัสดีครับ" and " ภาษาไทยง่าย". The composer now offers only the break after the spaces, as the single-line composer ends its lines. --- crates/text/src/layout.rs | 6 ++++-- crates/text/src/tests_combos.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index fa75899a2..8637ea1cc 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -1607,7 +1607,9 @@ fn hyphen_breaks(text: &str, g: &[SGlyph], ws: usize, we: usize, opts: &Hyphenat } /// Break candidates for the every-line composer: hyphenation points under `hyphenate` (the -/// paragraph's Hyphenation options) when it is given. +/// paragraph's Hyphenation options) when it is given. No break comes before a space: a line that +/// may end after a dash, a Thai word or a space ends after the spaces that follow it, as in +/// [`break_line`]. fn candidates(text: &str, g: &[SGlyph], hyphenate: Option<&HyphenationOptions>, kinsoku: Kinsoku) -> Vec { let mut v = vec![]; let mut ws = 0; @@ -1622,7 +1624,7 @@ fn candidates(text: &str, g: &[SGlyph], hyphenate: Option<&HyphenationOptions>, } } let hy = if gl.is_soft_hyphen() { hyphen_glyph(gl).adv } else { 0.0 }; - if j + 1 < g.len() && g[j + 1].byte != gl.byte && kinsoku_allows(g, j, kinsoku) { + if j + 1 < g.len() && g[j + 1].byte != gl.byte && !g[j + 1].is_space() && kinsoku_allows(g, j, kinsoku) { v.push(Breakpoint { end: j + 1, hyphen: hy }); } ws = j + 1; diff --git a/crates/text/src/tests_combos.rs b/crates/text/src/tests_combos.rs index 73ecbf848..d61dccc51 100644 --- a/crates/text/src/tests_combos.rs +++ b/crates/text/src/tests_combos.rs @@ -387,6 +387,33 @@ fn composer_x_mixed_line_metrics() { assert!(wrong.is_empty(), "{wrong:?}"); } +#[test] +fn composer_x_lines_start_after_the_spaces() { + // A line may end after a dash or between two spaces, but the every-line composer breaks after + // the spaces that follow, as the single-line composer does: no line starts with a space, in + // uniform 12 pt text and with a 24 pt word in it. + let st = sans(12.0); + let runs = |x: CharStyle| { + vec![run("Destroy all creatures \u{2014} with toughness ", st.clone()), run("X", x), run(" or less \u{2014} then draw a card.", st.clone())] + }; + let mut wrong = vec![]; + for x in [sans(12.0), sans(24.0)] { + for justify in [Justify::Left, Justify::JustifyLeft] { + for w in (40..400).step_by(4) { + let mut t = area(runs(x.clone()), Rect::new(0.0, 0.0, f64::from(w), 2000.0)); + t.para.composer = Composer::EveryLine; + t.para.justify = justify; + let text = t.plain_text(); + let l = layout(db(), &t); + if l.lines.iter().any(|li| text[li.start..].starts_with(' ')) { + wrong.push((x.size, justify, w, line_texts(&t, &l))); + } + } + } + } + assert!(wrong.is_empty(), "{wrong:?}"); +} + const INLINE: char = vectorcraft_doc::text::INLINE_CHAR; // ---------- vertical alignment × … ----------