diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index ecac4c782c..a20dba68af 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -6742,3 +6742,68 @@ fn a_lone_row_aligns_across_the_column_it_sits_in() { align(&mut doc, added::Align::Right); assert!((title(&doc)[2] - column[1]).abs() < 1.0, "right: on the column's right edge, {:?} vs {column:?}", title(&doc)); } + +/// An edited paragraph's text has a line break between its source rows (`editor_text`), as the +/// paragraph editor sends it. Those are the rows of one paragraph, not paragraph ends: justified +/// in a font of its own (the single-style rewrite), every row but the last still fills the width. +#[test] +fn a_justified_paragraph_from_the_editor_fills_its_rows() { + let mut doc = + text_page("BT /F1 10 Tf 12 TL 100 700 Td (One two three four five six seven) Tj T* (eight nine ten eleven twelve) Tj T* (end) Tj ET"); + let block = text::text_blocks(&doc, 0).unwrap()[0].clone(); + assert_eq!(block.editor_text.matches('\n').count(), 2); + let style = text::BlockStyle { align: Some(added::Align::Justify), family: Some((Family::Helvetica, true, false)), ..Default::default() }; + text::rewrite_block(&mut doc, 0, 0, Some(&block.editor_text), &style).unwrap(); + let lines = text::text_lines(&doc, 0).unwrap(); + let row = |t: &str| lines.iter().find(|l| l.text.starts_with(t)).unwrap().rect; + let right = row("One")[2]; + assert!((row("eight")[2] - right).abs() < 1.0, "the second row is stretched: {:?} vs {right}", row("eight")); + assert!(row("end")[2] < right - 20.0, "the last row isn't"); +} + +/// The same on a page of two columns, where the other column has more rows: a title over the +/// first column is aligned across the first column, not its own box. +#[test] +fn a_lone_row_aligns_across_its_own_column_of_two() { + let mut doc = text_page( + "BT /F1 18 Tf 1 0 0 1 120 700 Tm (Title) Tj ET \ + BT /F1 10 Tf 12 TL 50 660 Td (The first column runs to here.) Tj T* (A second row of it.) Tj ET \ + BT /F1 10 Tf 12 TL 320 660 Td (The second column has more rows.) Tj T* (One.) Tj T* (Two.) Tj T* (Three.) Tj T* (Four.) Tj ET", + ); + let blocks = text::text_blocks(&doc, 0).unwrap(); + let column = blocks.iter().find(|b| b.text.starts_with("The first")).unwrap().rect; + let index = blocks.iter().position(|b| b.text == "Title").unwrap(); + let style = text::BlockStyle { aligns: vec![Some(added::Align::Left)], ..Default::default() }; + text::rewrite_block(&mut doc, 0, index, Some("Title"), &style).unwrap(); + let title = text::text_lines(&doc, 0).unwrap().into_iter().find(|l| l.text == "Title").unwrap().rect; + assert!((title[0] - column[0]).abs() < 0.5, "left: on the first column's edge, {title:?} vs {column:?}"); +} + +/// A one-row title in two colours is rewritten by the styled-region path, which is given the +/// same column frame: centred twice, it stays centred in the column instead of moving right. +#[test] +fn a_lone_row_of_two_styles_aligns_across_its_column() { + let mut doc = text_page( + "BT /F1 18 Tf 1 0 0 1 250 700 Tm (Big ) Tj 1 0 0 rg (Title) Tj ET \ + BT 0 g /F1 10 Tf 12 TL 72 650 Td (The body of the page runs across the whole column here.) Tj T* (It has a second row that is a little shorter.) Tj ET", + ); + let blocks = text::text_blocks(&doc, 0).unwrap(); + assert_eq!(blocks[0].text, "Big Title"); + let column = [blocks[1].rect[0], blocks[1].rect[2]]; + let title = |doc: &Document| { + let lines = text::text_lines(doc, 0).unwrap(); + let block = text::text_blocks(doc, 0).unwrap().into_iter().find(|b| b.text == "Big Title").unwrap(); + block.lines.iter().map(|i| lines[*i].rect).reduce(|a, b| [a[0].min(b[0]), a[1].min(b[1]), a[2].max(b[2]), a[3].max(b[3])]).unwrap() + }; + let centre = |doc: &mut Document| { + let blocks = text::text_blocks(doc, 0).unwrap(); + let (index, block) = blocks.iter().enumerate().find(|(_, b)| b.text == "Big Title").unwrap(); + let style = text::BlockStyle { aligns: vec![Some(added::Align::Center)], ..Default::default() }; + text::rewrite_block(doc, 0, index, Some(&block.editor_text), &style).unwrap(); + }; + centre(&mut doc); + let centred = title(&doc); + assert!(((centred[0] + centred[2]) / 2.0 - (column[0] + column[1]) / 2.0).abs() < 1.0, "centred in the column: {centred:?} vs {column:?}"); + centre(&mut doc); + assert!((title(&doc)[0] - centred[0]).abs() < 0.5, "centring again leaves it where it is: {:?} vs {centred:?}", title(&doc)); +} diff --git a/crates/edit/src/text_regions.rs b/crates/edit/src/text_regions.rs index d9465cdffd..c8758d4422 100644 --- a/crates/edit/src/text_regions.rs +++ b/crates/edit/src/text_regions.rs @@ -30,11 +30,12 @@ fn frame_row_rect(row: &[usize], lines: &[TextLine]) -> Option<[f64; 4]> { /// The left edge the rest of the page's text sits on, ignoring the rows of `block`. A single row /// that does not share it is not merely an indented or full-width left-aligned line, so its own -/// centre is the only remaining explanation for where it sits. -fn neighbour_left(lines: &[TextLine], block: &TextBlock) -> Option { +/// centre is the only remaining explanation for where it sits. With `over`, only rows reaching +/// across that span count: the edge of the column under it, however many rows another column has. +fn neighbour_left(lines: &[TextLine], block: &TextBlock, over: Option<[f64; 2]>) -> Option { let mut edges: HashMap = HashMap::new(); for (i, line) in lines.iter().enumerate() { - if block.lines.contains(&i) { + if block.lines.contains(&i) || over.is_some_and(|[left, right]| line.rect[2] <= left || line.rect[0] >= right) { continue; } // Quarter-point buckets: equal edges found through different runs still agree. @@ -45,8 +46,8 @@ fn neighbour_left(lines: &[TextLine], block: &TextBlock) -> Option { } /// Where an alignment picked for a one-row paragraph is measured, as `[left, right]`: the column of -/// text it sits in (the left edge the rest of the page's text uses, to the furthest right those -/// rows reach) when that column holds it, otherwise its own box. A lone row's own box is no frame +/// text it sits in (the left edge the text under or over it uses, to the furthest right those rows +/// reach) when that column holds it, otherwise its own box. A lone row's own box is no frame /// to align in: left-aligning a title centred on the page left it where it was, and since a single /// row grows to the page's margin, centring it again moved it right each time. `None` when no /// alignment is asked for, or when the paragraph is several rows, turned, or given a width or a @@ -58,7 +59,7 @@ pub(super) fn alignment_frame(lines: &[TextLine], block: &TextBlock, style: &Blo } let own = [block.rect[0], block.rect[2]]; let tolerance = block.size.max(1.0) * 0.5; - let Some(left) = neighbour_left(lines, block) else { return Some(own) }; + let Some(left) = neighbour_left(lines, block, Some(own)) else { return Some(own) }; let right = lines .iter() .enumerate() @@ -85,9 +86,9 @@ pub(super) fn is_centred(block: &TextBlock, lines: &[TextLine], page_centre: f64 return left_span > size * 0.5 && block.rows.iter().all(|row| row_rect(row, lines).is_some_and(|r| ((r[0] + r[2]) / 2.0 - centre).abs() < size)); } - lefts - .first() - .is_some_and(|left| (centre - page_centre).abs() < size && neighbour_left(lines, block).is_none_or(|edge| (*left - edge).abs() > size * 0.5)) + lefts.first().is_some_and(|left| { + (centre - page_centre).abs() < size && neighbour_left(lines, block, None).is_none_or(|edge| (*left - edge).abs() > size * 0.5) + }) } pub(super) fn group_blocks(lines: &[TextLine]) -> Vec {