From 5f196fdfffbc5de0924ae948978ae0b81198c824 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 13 Nov 2026 19:47:46 -0800 Subject: [PATCH] Type: right-to-left tabbed lines are measured with their line-end trim where it is drawn A right-to-left line draws its last character before its tabs. When that character loses its Japanese-Latin space at the line's end, the layout drew it narrower before placing the tabs, but measured the line with the full advance and took the trim off afterwards. The measured line was narrower than the drawn one by the trim: a right-aligned line ended past the frame's right edge (305 pt in a 300 pt frame), and a line that did not fit as drawn was kept whole. drawn_width now takes the end trim off the glyph it belongs to, so the tabs drawn after it take their stops from where it ends, both where lines break and where a line is measured for alignment. --- crates/text/src/layout/breaking.rs | 13 +++++++++---- crates/text/src/layout/flow.rs | 11 +++++++---- crates/text/src/tests/justify.rs | 30 ++++++++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/crates/text/src/layout/breaking.rs b/crates/text/src/layout/breaking.rs index 671ebee7a1..9bc79e1bc8 100644 --- a/crates/text/src/layout/breaking.rs +++ b/crates/text/src/layout/breaking.rs @@ -243,11 +243,16 @@ pub(super) fn drawn_after_tab<'g>(sg: &'g [SGlyph], drawn: &'g [usize], k: usize /// them) set on a line whose content starts `x_start` from the tab origin: tabs of the text take /// what [`tab_advance`] gives them from where the glyphs drawn before them end, aligning the glyphs /// drawn after them, not the width of their glyph. The tab of a list marker (length 0) keeps its -/// own width. -pub(super) fn drawn_width(sg: &[SGlyph], drawn: &[usize], tabs: &[vectorcraft_doc::TabStop], x_start: f64) -> f64 { +/// own width. With `end_trim` (a glyph index and an amount), that glyph is set that much narrower +/// where it is drawn (a line's end trim), before the tabs drawn after it take their stops. +pub(super) fn drawn_width(sg: &[SGlyph], drawn: &[usize], tabs: &[vectorcraft_doc::TabStop], x_start: f64, end_trim: Option<(usize, f64)>) -> f64 { let mut x = x_start; - for (k, g) in drawn.iter().enumerate().filter_map(|(k, &j)| sg.get(j).map(|g| (k, g))) { - x += if g.ch == '\t' && g.len > 0 { tab_advance(tabs, 0.0, x, drawn_after_tab(sg, drawn, k)).0 } else { g.adv }; + for (k, j, g) in drawn.iter().enumerate().filter_map(|(k, &j)| sg.get(j).map(|g| (k, j, g))) { + x += if g.ch == '\t' && g.len > 0 { + tab_advance(tabs, 0.0, x, drawn_after_tab(sg, drawn, k)).0 + } else { + g.adv - end_trim.filter(|&(at, _)| at == j).map_or(0.0, |(_, trim)| trim) + }; } x - x_start } diff --git a/crates/text/src/layout/flow.rs b/crates/text/src/layout/flow.rs index 1ee9a3fe50..f722caf22a 100644 --- a/crates/text/src/layout/flow.rs +++ b/crates/text/src/layout/flow.rs @@ -449,8 +449,9 @@ fn place_flow_line(cx: &mut Ctx<'_>, pen: &mut Pen<'_>, context: &mut PlacementC .filter(|&j| j < trimmed) .collect(); let hyphen = if hyphenated { end.checked_sub(1).and_then(|k| glyphs.get(k)).map_or(0.0, |g| hyphen_glyph(g).adv) } else { 0.0 }; - let credit = trimmed.checked_sub(1).filter(|&k| k >= start).and_then(|k| line_end.get(k)).copied().unwrap_or(0.0); - drawn_width(glyphs, &drawn, ¶.tabs, ind_l) + hyphen - credit <= width + EPS + // The last glyph's end trim comes off where it is drawn, before the tabs after it. + let credit = trimmed.checked_sub(1).filter(|&k| k >= start).and_then(|k| line_end.get(k).map(|&trim| (k, trim))); + drawn_width(glyphs, &drawn, ¶.tabs, ind_l, credit) + hyphen <= width + EPS }; let fits: Option<&dyn Fn(usize, bool) -> bool> = (!cx.vertical && bidi.is_some_and(|b| b.has_rtl())).then_some(&drawn_fits); let line = |ty| { @@ -649,9 +650,11 @@ fn prepare_line_geometry(cx: &Ctx<'_>, t: &TextObject, paragraph: &ParagraphData let line_end = sg.get(position.end).map_or(paragraph.pr.end, |g| g.byte); let order = visual_order(paragraph.bidi.as_ref().filter(|_| !cx.vertical), paragraph.pr.start, line_start..line_end, &sg[index..position.end]); // The line is as wide as it is drawn: its glyphs before its trailing spaces in drawing order, - // each tab up to its stop from where the glyphs drawn before it end. + // each tab up to its stop from where the glyphs drawn before it end, with the last glyph's end + // trim taken off where it is drawn. let measured: Vec = order.iter().map(|&offset| index + offset).filter(|&j| j < trimmed).collect(); - let measured_width = drawn_width(sg, &measured, ¶.tabs, position.ind_l) + hyphen.as_ref().map_or(0.0, |g| g.adv) - end_trim; + let trim_at = trimmed.checked_sub(1).filter(|&k| k >= index).map(|k| (k, end_trim)); + let measured_width = drawn_width(sg, &measured, ¶.tabs, position.ind_l, trim_at) + hyphen.as_ref().map_or(0.0, |g| g.adv); // The glyphs before `stop` in visual order, each with the advance it is set with. A list // marker does not hang: it is counted with no width. let visible = |stop: usize| -> (Vec, Vec<(&SGlyph, f64)>) { diff --git a/crates/text/src/tests/justify.rs b/crates/text/src/tests/justify.rs index bc1e5b1b13..5c82700cde 100644 --- a/crates/text/src/tests/justify.rs +++ b/crates/text/src/tests/justify.rs @@ -668,6 +668,36 @@ fn a_right_to_left_line_with_a_tab_and_many_breaks_wraps_when_it_does_not_fit_as } } +/// A right-to-left line with a tab whose last character loses its Japanese–Latin space at the +/// line's end is measured with that space taken off where the character is drawn, before the tab: +/// right aligned, it ends on the frame's right edge, and it wraps when it does not fit as drawn. +#[test] +fn a_right_to_left_tabbed_line_is_measured_without_its_line_end_space() { + use vectorcraft_doc::Mojikumi; + // א (0), ב (2), tab (4), ג (5), 漢 (7), then a Hebrew word too long for the frame: the first + // line ends after 漢, which loses the quarter em it had before the Hebrew word. + let text = format!("אב\tג漢{}", "ד".repeat(40)); + let lay = |width: f64| { + let mut t = area(&text, width, Justify::Right, no_ranges()); + t.para.mojikumi = Mojikumi::GyomatsuYakumonoZenkaku; + t.para.tabs = vec![TabStop { position: 150.0, align: TabAlign::Left, leader: String::new(), align_on: '.' }]; + layout(db(), &t) + }; + let l = lay(300.0); + assert!(l.lines.len() >= 2, "wraps: {} line(s)", l.lines.len()); + assert!(l.glyphs.iter().filter(|g| g.line == 0).all(|g| g.byte < 10), "the first line ends after 漢"); + let (_, right) = drawn_extent(&l, &text, 0); + assert!((right - 300.0).abs() < 1e-6, "ends on the right edge: {right}"); + assert!((l.lines[0].x1 - right).abs() < 1e-6, "the line's record ends where it is drawn: {}", l.lines[0].x1); + // Drawn, that line is as wide as the stop and the text after the tab (אב). In a frame a point + // narrower, less than the quarter em, it does not fit and the first line stays in the frame. + let after_tab: f64 = l.glyphs.iter().filter(|g| g.byte < 4).map(|g| g.advance).sum(); + let width = 150.0 + after_tab - 1.0; + let l = lay(width); + let (left, right) = drawn_extent(&l, &text, 0); + assert!(left >= -1e-6 && right <= width + 1e-6, "the first line stays in the frame: {left} {right} (frame {width})"); +} + /// Point type has no measure to fill: a justified line with a tab keeps its natural width. #[test] fn justified_point_type_with_a_tab_keeps_its_width() {