diff --git a/crates/edit/src/tests.rs b/crates/edit/src/tests.rs index 692a21c8c2..ecac4c782c 100644 --- a/crates/edit/src/tests.rs +++ b/crates/edit/src/tests.rs @@ -6709,3 +6709,36 @@ fn added_text_paragraphs_keep_their_own_alignments() { let tidy = paragraph_align::tidy(vec![Some(Align::Left), Some(Align::Right)], Align::Left); assert_eq!(tidy, [None, Some(Align::Right)]); } + +/// A one-row title over a body column: an alignment picked for it is measured across the column. +/// Left puts it on the column's left edge, and centring it again leaves it where it is, rather +/// than aligning it in its own box (a no-op) or toward the page's margin (moving it right each +/// time). It is aligned as the editor aligns a selected row, in whichever paragraph holds it. +#[test] +fn a_lone_row_aligns_across_the_column_it_sits_in() { + let mut doc = text_page( + "BT /F1 24 Tf 1 0 0 1 250 700 Tm (Title) Tj ET BT /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, "Title"); + let column = [blocks[1].rect[0], blocks[1].rect[2]]; + let title = |doc: &Document| text::text_lines(doc, 0).unwrap().into_iter().find(|l| l.text == "Title").unwrap().rect; + let align = |doc: &mut Document, a: added::Align| { + let lines = text::text_lines(doc, 0).unwrap(); + let line = lines.iter().position(|l| l.text == "Title").unwrap(); + let blocks = text::text_blocks(doc, 0).unwrap(); + let (index, block) = blocks.iter().enumerate().find(|(_, b)| b.lines.contains(&line)).unwrap(); + // The title is the paragraph's first row. + let style = text::BlockStyle { aligns: vec![Some(a)], ..Default::default() }; + text::rewrite_block(doc, 0, index, Some(&block.editor_text), &style).unwrap(); + }; + align(&mut doc, added::Align::Center); + 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:?}"); + align(&mut doc, added::Align::Center); + assert!((title(&doc)[0] - centred[0]).abs() < 0.5, "centring again leaves it where it is: {:?} vs {centred:?}", title(&doc)); + align(&mut doc, added::Align::Left); + assert!((title(&doc)[0] - column[0]).abs() < 0.5, "left: back on the column's edge, {:?} vs {column:?}", title(&doc)); + 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)); +} diff --git a/crates/edit/src/text.rs b/crates/edit/src/text.rs index 3ce185a985..e8e80d29f5 100644 --- a/crates/edit/src/text.rs +++ b/crates/edit/src/text.rs @@ -24,7 +24,7 @@ use crate::EditError; mod regions; #[path = "text_session.rs"] mod session; -use regions::{group_blocks, is_centred, rewrite_styled_block}; +use regions::{alignment_frame, group_blocks, is_centred, rewrite_styled_block}; pub use session::TextSession; /// One line of existing text. @@ -2678,6 +2678,16 @@ fn rewrite_block_from( ) -> Result { let blocks = group_blocks(&lines); let b = blocks.get(block).cloned().ok_or_else(|| EditError::Invalid(format!("page {} has no paragraph {}", page + 1, block + 1)))?; + // An alignment for a one-row paragraph is laid out across the column it sits in, as a + // paragraph of that width moved to its left edge. + let framed; + let style = match alignment_frame(&lines, &b, style) { + Some([left, right]) => { + framed = BlockStyle { width: Some(right - left), offset: Some([left - b.rect[0], 0.0]), ..style.clone() }; + &framed + } + None => style, + }; let members: Vec<&TextLine> = b.lines.iter().map(|i| &lines[*i]).collect(); // One clipping run is enough: rewriting the paragraph would drop its mask with the rest. let mut dropped = coincident_ops(&lines, &members); diff --git a/crates/edit/src/text_regions.rs b/crates/edit/src/text_regions.rs index 11ae6c5a2c..d9465cdffd 100644 --- a/crates/edit/src/text_regions.rs +++ b/crates/edit/src/text_regions.rs @@ -44,6 +44,31 @@ fn neighbour_left(lines: &[TextLine], block: &TextBlock) -> Option { Some(best as f64 / 4.0) } +/// 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 +/// 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 +/// move of its own. +pub(super) fn alignment_frame(lines: &[TextLine], block: &TextBlock, style: &BlockStyle) -> Option<[f64; 2]> { + let asked = style.align.is_some() || style.aligns.iter().any(Option::is_some); + if !asked || style.width.is_some() || style.offset.is_some() || block.rows.len() != 1 || block.rotation.abs() > 0.01 { + return None; + } + 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 right = lines + .iter() + .enumerate() + .filter(|(i, line)| !block.lines.contains(i) && (line.rect[0] - left).abs() < tolerance) + .map(|(_, line)| line.rect[2]) + .fold(f64::NEG_INFINITY, f64::max); + let holds = right.is_finite() && right - left > own[1] - own[0] && own[0] >= left - tolerance && own[1] <= right + tolerance; + Some(if holds { [left, right] } else { own }) +} + /// Whether a block reads as centred, and so must keep its centre when it grows or wraps. /// /// Several ragged rows whose left edges disagree while their centres agree are centred. A one row