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.
This commit is contained in:
RiceDee
2026-10-24 02:34:48 +11:00
parent 5c1c3462eb
commit 1f29575d34
3 changed files with 69 additions and 1 deletions
+33
View File
@@ -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));
}
+11 -1
View File
@@ -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<LineEdit, EditError> {
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);
+25
View File
@@ -44,6 +44,31 @@ fn neighbour_left(lines: &[TextLine], block: &TextBlock) -> Option<f64> {
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