diff --git a/crates/compose/src/lib.rs b/crates/compose/src/lib.rs index 9d383d270..0e32881d9 100644 --- a/crates/compose/src/lib.rs +++ b/crates/compose/src/lib.rs @@ -1817,6 +1817,8 @@ fn place_sub_objects( } } +/// Sets one paragraph's lines, column after column and frame after frame. It stops when the +/// paragraph ends, when the frames run out, or when an earlier paragraph has to be set again. fn place_paragraph_lines(shared: PlacementShared<'_, '_>, paragraph: ParagraphLines<'_>) -> LinePlacement { let PlacementShared { doc, @@ -1916,6 +1918,9 @@ fn place_paragraph_lines(shared: PlacementShared<'_, '_>, paragraph: ParagraphLi }); } let pp = indented.as_ref().unwrap_or(para_props); + // The Indent to Here character sits past the drop cap, so the lines that start at it + // aren't indented by the drop cap again. + let beside_cap = drop_cap.filter(|_| indented.is_none()); let f = &frames[cur.fi]; let base = cols[cur.fi][cur.col.min(cols[cur.fi].len() - 1)]; let col = cur.clip(base); @@ -1941,7 +1946,7 @@ fn place_paragraph_lines(shared: PlacementShared<'_, '_>, paragraph: ParagraphLi props: pp, slots: &slots, column: col, - drop_cap, + drop_cap: beside_cap, line_no, col_first_line, rtl, @@ -2066,7 +2071,7 @@ fn place_paragraph_lines(shared: PlacementShared<'_, '_>, paragraph: ParagraphLi let col_w = notes.area(&cols[cur.fi], cur.col).map_or(0.0, |r| r.width()); let note_column = NoteColumn { frame_index: cur.fi, column_index: cur.col, width: col_w, frame: f }; // A drop cap sits at the start-side indent; the first-line indent doesn't move it. - let beside = drop_cap.filter(|dc| line_no < dc.lines && col_first_line == 0).map(|dc| dc.indent()); + let beside = beside_cap.filter(|dc| line_no < dc.lines && col_first_line == 0).map(|dc| dc.indent()); let first_line = if line_no == 0 { pp.first_line_indent } else { 0.0 }; let (ind_l, ind_r) = match beside { Some(w) if rtl => (pp.left_indent, pp.right_indent + w), @@ -4963,7 +4968,9 @@ fn distribute(line: &[Glyph], spaces: &[usize], extra: f64, sp: &Spacing, add: & /// The paragraph settings for the lines after `line` when it holds an Indent to Here character /// (the last one, if several): the start-side indent reaches the character, measured from the -/// start edge of the line's segment `x0..x1`. `None` when there is none or the indent is unchanged. +/// start edge of the line's segment `x0..x1`. Like the paragraph's own indents it then applies +/// from each later line's segment, so beside a text wrap it starts at the wrap's edge. `None` +/// when there is none or the indent is unchanged. fn indent_here( line: &[Glyph], placed: &[PlacedGlyph], diff --git a/crates/compose/src/tests.rs b/crates/compose/src/tests.rs index 3aff83ae6..7867d2ec8 100644 --- a/crates/compose/src/tests.rs +++ b/crates/compose/src/tests.rs @@ -1934,6 +1934,60 @@ fn indent_to_here_starts_the_paragraphs_later_lines_at_the_character() { assert!((start - 78.0).abs() < 0.01, "plain second line starts at {start}"); } +/// Where each line after the first starts, and the Indent to Here character's x on the first. +fn indent_to_here_starts(text: &str, para: ParaAttrs) -> (f64, Vec) { + let (d, sid, _) = doc_with(text, Rect::new(72.0, 36.0, 272.0, 600.0), para); + let cs = compose_story(&d, sid, &Default::default()); + let lines = all_lines(&cs); + assert!(lines.len() > 4, "the text wraps"); + let byte = text.find('\u{e005}').unwrap(); + let here = lines[0].glyphs.iter().find(|g| g.byte == byte && g.len > 0).unwrap().x; + let starts = lines.iter().skip(1).map(|l| l.glyphs.iter().find(|g| g.visible).unwrap().x).collect(); + (here, starts) +} + +#[test] +fn indent_to_here_after_a_tab_hangs_the_text_after_the_number() { + // The usual hanging number: "1.", a tab to the next stop, then Indent to Here. + let tabs = vec![designcraft_doc::TabStop { position: 24.0, align: TabAlign::Left, leader: String::new(), align_on: String::new() }]; + let (here, starts) = indent_to_here_starts(&format!("1.\t\u{e005}{LOREM}"), ParaAttrs { tabs: Some(tabs), ..Default::default() }); + assert!((here - (72.0 + 24.0)).abs() < 0.01, "the character sits at the tab stop: {here}"); + for (i, start) in starts.iter().enumerate() { + assert!((start - here).abs() < 0.01, "line {} starts at {start}, not at {here}", i + 1); + } +} + +#[test] +fn indent_to_here_beside_a_drop_cap_is_not_indented_by_the_drop_cap_again() { + // A three-line drop cap, then Indent to Here on the first line: the lines beside the drop + // cap and the lines below it all start at the character, none further in. + let (here, starts) = indent_to_here_starts(&format!("Drop \u{e005}{LOREM}"), drop_cap(3, 1)); + for (i, start) in starts.iter().enumerate() { + assert!((start - here).abs() < 0.01, "line {} starts at {start}, not at {here}", i + 1); + } +} + +#[test] +fn indent_to_here_in_a_right_to_left_paragraph_indents_from_the_right() { + // Hebrew words (the bidi classes decide the order; no Hebrew font has to be present): the + // lines after the first end where the character sits, measured from the right edge. + let words = "שלום עולם ".repeat(40); + let text = format!("שלום \u{e005}{words}"); + let para = ParaAttrs { direction: Some(designcraft_doc::TextDirection::RightToLeft), align: Some(Align::Right), ..Default::default() }; + let (d, sid, _) = doc_with(&text, Rect::new(72.0, 36.0, 272.0, 600.0), para); + let cs = compose_story(&d, sid, &Default::default()); + let lines = all_lines(&cs); + assert!(lines.len() > 3, "the text wraps"); + let byte = text.find('\u{e005}').unwrap(); + let here = lines[0].glyphs.iter().find(|g| g.byte == byte && g.len > 0).unwrap().x; + assert!(here < 272.0 - 1.0, "the character follows the first word: {here}"); + let right_edge = |l: &Line| l.glyphs.iter().filter(|g| g.visible).map(|g| g.x + g.adv).fold(f64::MIN, f64::max); + for (i, line) in lines.iter().enumerate().skip(1) { + let end = right_edge(line); + assert!((end - here).abs() < 0.01, "line {i} ends at {end}, not at {here}"); + } +} + #[test] fn right_indent_tab_leaders_follow_stop_precedence_without_moving_text() { let text = "Intro\u{e006}12";