From 1f29575d34cda035b52997c14e31253ff8dd7f3e Mon Sep 17 00:00:00 2001 From: RiceDee <345114586+RiceDee@users.noreply.github.com> Date: Fri, 23 Oct 2026 12:42:12 +1100 Subject: [PATCH] M7: align a one-row paragraph across the column it sits in Centring one highlighted line of a paragraph moves it out of the paragraph's rows, so the next alignment of it saw a one-row paragraph and aligned it in its own box: Left left it where it was, and since a single row grows to the page's margin, each Centre moved it further right. An alignment for a one-row paragraph is now laid out across the column of text it sits in (the left edge the page's other text uses, to the furthest right those rows reach), or its own box when no column holds it. --- crates/edit/src/tests.rs | 33 +++++++++++++++++++++++++++++++++ crates/edit/src/text.rs | 12 +++++++++++- crates/edit/src/text_regions.rs | 25 +++++++++++++++++++++++++ 3 files changed, 69 insertions(+), 1 deletion(-) 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