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() {