From 285a3da81a21ca781526fb8b870667f1ccf3ef87 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Fri, 16 Oct 2026 20:12:40 +0200 Subject: [PATCH] Type: centre and right aligned lines set their tabs as if they started at the left (#2457) A tab's width depends on where it falls from the tab origin. Line breaking and the line width measure it from the left edge, but a centred or right aligned line is placed from its own start, so its tabs stopped elsewhere. The line is now laid out with its tabs measured from the left and then aligned as a whole. Co-Authored-By: Claude Opus 5.5 --- crates/text/src/layout.rs | 5 ++++- crates/text/src/tests.rs | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index 0559730e2..5b0f200f9 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -1837,6 +1837,9 @@ fn flow(cx: &mut Ctx<'_>, paras: &[Range], t: &TextObject, regions: Optio let mut x_end = start_x; // Tab stops are measured from the frame's left edge (point type: the origin). let tab_origin = if regions.is_some() { x0 } else { 0.0 }; + // A line with tabs is measured as if it started at the left (see `set_width`), then + // aligned as a whole: its tabs stop where they were measured, however it is aligned. + let tab_shift = start_x - if regions.is_some() { ax0 } else { ind_l }; let line_start = sg.get(i).map_or(pr.start, |g| g.byte); let line_end = sg.get(end).map_or(pr.end, |g| g.byte); let order = visual_order(bidi.as_ref().filter(|_| !cx.vertical), pr.start, line_start..line_end, &sg[i..end]); @@ -1854,7 +1857,7 @@ fn flow(cx: &mut Ctx<'_>, paras: &[Range], t: &TextObject, regions: Optio } let mut leader = ""; if g.ch == '\t' { - (adv, leader) = tab_advance(¶.tabs, tab_origin, x, &sg[j + 1..trimmed.max(j + 1)]); + (adv, leader) = tab_advance(¶.tabs, tab_origin, x - tab_shift, &sg[j + 1..trimmed.max(j + 1)]); } else if j < body { if g.is_space() { adv += per_space; diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 41718cf3d..03a0ec18b 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -501,6 +501,24 @@ fn a_leading_tab_takes_its_width_in_the_line_and_the_justification() { } } +#[test] +fn a_centred_or_right_aligned_line_sets_its_tabs_as_if_it_started_at_the_left() { + use vectorcraft_doc::{TabAlign, TabStop}; + // "AB" with a stop at 100 pt is 100 pt plus B wide whatever the alignment: the whole line + // moves, the gap between A and B stays 100 pt, and a right aligned line ends on the edge (#2457). + for justify in [Justify::Center, Justify::Right, Justify::JustifyCenter, Justify::JustifyRight] { + let mut t = area("A\tB", style(12.0), Rect::new(0.0, 0.0, 200.0, 100.0), justify); + t.para.tabs = vec![TabStop { position: 100.0, align: TabAlign::Left, leader: String::new(), align_on: '.' }]; + let lay = layout(db(), &t); + let at = |byte: usize| lay.glyphs.iter().find(|g| g.byte == byte).map(|g| g.origin.x).unwrap(); + assert!((at(2) - at(0) - 100.0).abs() < 1e-6, "{justify:?}: A at {}, B at {}", at(0), at(2)); + if matches!(justify, Justify::Right | Justify::JustifyRight) { + let b = lay.glyphs.iter().find(|g| g.byte == 2).unwrap(); + assert!((b.origin.x + b.advance - 200.0).abs() < 1e-6, "{justify:?}: the line ends at {}", b.origin.x + b.advance); + } + } +} + #[test] fn tab_leaders_line_up_from_line_to_line_and_stop_short_of_the_stop() { use vectorcraft_doc::{TabAlign, TabStop};