diff --git a/crates/compose/src/lib.rs b/crates/compose/src/lib.rs index 5283487a7..8f34d1177 100644 --- a/crates/compose/src/lib.rs +++ b/crates/compose/src/lib.rs @@ -933,8 +933,18 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C let lx0 = x0 + ind_l; let lx1 = x1 - pp.right_indent; let last = k + 1 == breaks.len(); + // The break character that ends this line, if any (it is the line's last glyph). + let brk = if b.forced && e > s { + e.checked_sub(1).and_then(|i| glyphs.get(i)).map(|g| g.ch).filter(|&c| breaker::is_forced(c)) + } else { + None + }; + // A column, frame or page break ends the line as a paragraph's last line (last-line + // alignment); a justified line ended by a forced line break stays justified. + let ends_para = last || matches!(brk, Some(story::COLUMN_BREAK | story::FRAME_BREAK | story::PAGE_BREAK)); + let forced_mid = brk == Some(story::FORCED_LINE_BREAK) && !last; let (mut placed, end_x, ratio) = - layout_line(&glyphs, s, e, b.hyphen, lx0, lx1, col.x0, &pp, &spacing, last, b.forced && !last, f.left_page, &bidi_info); + layout_line(&glyphs, s, e, b.hyphen, lx0, lx1, col.x0, &pp, &spacing, ends_para, forced_mid, f.left_page, &bidi_info); ruby::annotate(db, &styles_tab, &mut placed, doc.settings.glyph_fallback); let range_end = if last { prange.end } else { glyphs.get(g0 + b.next).map(|g| g.byte).unwrap_or(prange.end) }; let range_start = glyphs.get(s).map(|g| g.byte).unwrap_or(prange.start).min(range_end); @@ -983,7 +993,6 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C line_no += 1; // Column / frame / page break characters. if b.forced && e < glyphs.len() + 1 { - let brk = glyphs.get(g0 + b.next.saturating_sub(1)).map(|g| g.ch); let jumped = match brk { Some(story::COLUMN_BREAK) => { next_column(&mut cur); diff --git a/crates/compose/src/tests.rs b/crates/compose/src/tests.rs index 19a446f76..849d3a4e4 100644 --- a/crates/compose/src/tests.rs +++ b/crates/compose/src/tests.rs @@ -573,6 +573,32 @@ fn column_break_moves_following_text() { assert!(lines[1].baseline < 20.0, "second column starts at the top"); } +#[test] +fn line_before_a_break_character_is_a_last_line() { + // As in InDesign: column, frame and page breaks end the paragraph's last line (set with the + // last-line alignment), while a forced line break inside a justified paragraph is justified. + use designcraft_doc::story::{COLUMN_BREAK, FORCED_LINE_BREAK, FRAME_BREAK, PAGE_BREAK}; + for (brk, align, justified) in [ + (COLUMN_BREAK, Align::LeftJustified, false), + (FRAME_BREAK, Align::LeftJustified, false), + (PAGE_BREAK, Align::LeftJustified, false), + (PAGE_BREAK, Align::FullyJustified, true), + (FORCED_LINE_BREAK, Align::LeftJustified, true), + ] { + // A space after the break is skipped and does not keep the break from moving the text. + let text = format!("one two three{brk} four five six"); + let (mut d, sid, fid) = doc_with(&text, Rect::new(0.0, 0.0, 400.0, 300.0), ParaAttrs { align: Some(align), ..Default::default() }); + d.item_mut(fid).unwrap().text_frame_mut().unwrap().options.columns = 2; + let cs = compose_story(&d, sid, &ComposeOptions::default()); + let l = &cs.frames[0].lines[0]; + let full = (l.end_x - l.x1).abs() < 0.6; + assert_eq!(full, justified, "{brk:?} {align:?}: line ends at {} of {}", l.end_x, l.x1); + if brk == COLUMN_BREAK { + assert_eq!(all_lines(&cs)[1].column, 1, "the text after a column break starts the next column"); + } + } +} + #[test] fn tabs_without_stops_use_default_half_inch() { let (d, sid, _) = doc_with("A\tB", Rect::new(0.0, 0.0, 300.0, 100.0), ParaAttrs::default());