diff --git a/crates/compose/src/breaker.rs b/crates/compose/src/breaker.rs index 754cfc3d0..2aa9b3ae7 100644 --- a/crates/compose/src/breaker.rs +++ b/crates/compose/src/breaker.rs @@ -303,8 +303,17 @@ fn items(glyphs: &[Glyph], hyph_after: &[bool], emergency_after: &[bool], sp: &S let n = glyphs.len(); let mut hy_cache = Vec::new(); let mut last_space = NONE; + // Spaces before the paragraph end or a forced break hang: they take no room on the line (as + // in InDesign, where "2 VOOR " fits a frame as wide as "2 VOOR"). + let mut hangs = vec![false; n]; + for i in (0..n).rev() { + hangs[i] = glyphs[i].is_space() && glyphs.get(i + 1).is_none_or(|nx| hangs[i + 1] || is_forced(nx.ch)); + } for (i, g) in glyphs.iter().enumerate() { let prev_hang = if i > 0 { right_hang(&glyphs[i - 1], sp) } else { 0.0 }; + if hangs[i] { + continue; + } if is_forced(g.ch) { it.push(Item::glue(0.0, INF, 0.0)); ig.push(i); diff --git a/crates/compose/src/tests.rs b/crates/compose/src/tests.rs index 7a0e790d5..fd7484fce 100644 --- a/crates/compose/src/tests.rs +++ b/crates/compose/src/tests.rs @@ -2929,3 +2929,18 @@ fn empty_paragraphs_get_no_bullet_or_number() { assert_eq!(list_labels(&cs, LABEL_CHARS), ["1.", "", "2.", ""], "list `{name}`"); } } + +#[test] +fn spaces_at_the_end_of_a_paragraph_take_no_room() { + // A frame as wide as "2 VOOR" also holds "2 VOOR " on one line: the trailing space hangs. + let (d, sid, _) = doc_with("2 VOOR", Rect::new(0.0, 0.0, 500.0, 100.0), ParaAttrs::default()); + let cs = compose_story(&d, sid, &ComposeOptions::default()); + let l = all_lines(&cs)[0]; + let width = l.end_x - l.x0; + for text in ["2 VOOR ", "2 VOOR ", "2 VOOR \u{2028}more"] { + let (d, sid, _) = doc_with(text, Rect::new(0.0, 0.0, width + 0.02, 100.0), ParaAttrs::default()); + let cs = compose_story(&d, sid, &ComposeOptions::default()); + let first = all_lines(&cs)[0]; + assert_eq!(&text[first.range.clone()].trim_end(), &"2 VOOR", "{text:?}"); + } +}