From ec86a8fd3c29f2fa0061c4bf0a104333e6c7bb8e Mon Sep 17 00:00:00 2001 From: Youssef Azriouil Date: Wed, 14 Oct 2026 18:57:44 +0200 Subject: [PATCH] Breaker: keep a page number's digits together The emergency break inside a word wider than the measure could fall between glyphs of one source character: an automatic page number wider than its frame came out as "13" over "4". Glyphs sharing a cluster (the generated digits of a page number, multi-glyph clusters) now stay together and the number hangs out of its frame, as in InDesign. --- crates/compose/src/breaker.rs | 4 ++++ crates/compose/src/tests.rs | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/crates/compose/src/breaker.rs b/crates/compose/src/breaker.rs index c2db91fb..d5742f1d 100644 --- a/crates/compose/src/breaker.rs +++ b/crates/compose/src/breaker.rs @@ -462,7 +462,11 @@ fn overlong_words(items: &[Item], ig: &[usize], glyphs: &[Glyph], measure: f64) let marks = out.get_or_insert_with(|| vec![false; glyphs.len()]); for i in a..b { let (Some(g), Some(next)) = (glyphs.get(i), glyphs.get(i + 1)) else { continue }; + // Glyphs of one source character (an automatic page number's digits, a + // multi-glyph cluster) stay together, as InDesign keeps them. + let one_char = next.byte == g.byte && (next.len == 0 || next.len == g.len); let ok = !g.no_break + && !one_char && g.break_after != Some(false) && next.ch != SOFT_HYPHEN && !is_forced(next.ch) diff --git a/crates/compose/src/tests.rs b/crates/compose/src/tests.rs index 97eeb8cd..e1710cb8 100644 --- a/crates/compose/src/tests.rs +++ b/crates/compose/src/tests.rs @@ -358,6 +358,23 @@ fn jump_object_keeps_grid_aligned_lines_on_the_grid() { assert!((l.baseline - want).abs() < 1e-6, "{} vs {want}", l.baseline); } +/// An automatic page number wider than its frame stays on one line and hangs out, as in InDesign +/// ("136" in a frame made for two digits); breaking it put "13" over "4". +#[test] +fn page_number_wider_than_its_frame_stays_whole() { + let text = designcraft_doc::story::PAGE_NUMBER.to_string(); + let (d, sid, _) = doc_with(&text, Rect::new(0.0, 0.0, 10.0, 40.0), ParaAttrs::default()); + let mut specs = frame_specs(&d, sid); + specs[0].page_name = Some("136".into()); + let cs = compose(&d, d.story(sid).unwrap(), &specs, &ComposeOptions::default()); + let lines = all_lines(&cs); + assert_eq!(lines.len(), 1, "{lines:?}"); + assert!(!cs.is_overset()); + // A word of ordinary letters that doesn't fit still breaks where it must. + let (d, sid, _) = doc_with("Wordwiderthanframe", Rect::new(0.0, 0.0, 30.0, 200.0), ParaAttrs::default()); + assert!(all_lines(&compose_story(&d, sid, &ComposeOptions::default())).len() > 1); +} + #[test] fn hostile_wrap_geometry_terminates() { let (d, sid) = wrap_doc(ParaAttrs { grid_align: Some(GridAlign::AllLines), ..Default::default() }, &[]);