From 06a283232ee11911b8cd20852a0877c508e8f4ed Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 12 Nov 2026 23:03:39 -0800 Subject: [PATCH] Type: no justification gap inside a right-to-left warichu block Justification gaps follow the drawn order, and right to left the bidi order draws a block's first glyph after the others. The glyph drawn second then did not continue the block, so Full Justify and CJK justification opened a gap inside it. Both gap rules now also skip a pair whose glyphs belong to the same block. --- crates/text/src/layout/flow.rs | 20 ++++++++++++++++---- crates/text/src/layout/wrapping_tests.rs | 23 +++++++++++++++++++++++ 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/crates/text/src/layout/flow.rs b/crates/text/src/layout/flow.rs index cfa28aca27..c949b84213 100644 --- a/crates/text/src/layout/flow.rs +++ b/crates/text/src/layout/flow.rs @@ -753,12 +753,24 @@ fn gap_pair<'a>(glyph: usize, held: &[usize], after: &[Option], index: us visible_after(index, glyph, after, sg).and_then(|next| sg.get(next)).map(|b| (a, b)) } +// Are `a` and `b` glyphs of the same warichu block? Right to left, the bidi order can draw a +// block's first glyph after the others, so the glyph drawn second need not continue the block. +fn same_warichu_block(a: &SGlyph, b: &SGlyph) -> bool { + a.wari.zip(b.wari).is_some_and(|(first, second)| first.start == second.start) +} + // Japanese (and Chinese) lines are justified between their characters (JLREQ 3.8): the // gaps next to a CJK character, not inside a Latin word, a tate-chu-yoko or warichu block or a // shaped cluster. -fn is_cjk_gap(glyph: usize, held: &[usize], after: &[Option], index: usize, sg: &[SGlyph]) -> bool { +pub(super) fn is_cjk_gap(glyph: usize, held: &[usize], after: &[Option], index: usize, sg: &[SGlyph]) -> bool { gap_pair(glyph, held, after, index, sg).is_some_and(|(a, b)| { - a.byte != b.byte && !a.is_space() && !b.is_space() && !b.continues_block() && a.ch != '\t' && (is_cjk(a.ch) || is_cjk(b.ch)) + a.byte != b.byte + && !a.is_space() + && !b.is_space() + && !b.continues_block() + && !same_warichu_block(a, b) + && a.ch != '\t' + && (is_cjk(a.ch) || is_cjk(b.ch)) }) && !in_pair(glyph, index, after, sg) } @@ -788,12 +800,12 @@ fn in_pair(glyph: usize, index: usize, after: &[Option], sg: &[SGlyph]) - // warichu block (set as one piece), a shaped cluster, whose glyphs (a letter and its combining marks, a conjunct and its matra) stay // together, or an inseparable pair. On a line that ends in a drawn hyphen, the gap before the // hyphen counts too: it follows the line's last visible glyph. -fn is_letter_gap(glyph: usize, held: &[usize], after: &[Option], index: usize, sg: &[SGlyph], hyphen: bool) -> bool { +pub(super) fn is_letter_gap(glyph: usize, held: &[usize], after: &[Option], index: usize, sg: &[SGlyph], hyphen: bool) -> bool { if held.contains(&glyph) || sg.get(glyph).is_none_or(|a| is_format_control(a.ch)) || in_pair(glyph, index, after, sg) { return false; } match gap_pair(glyph, held, after, index, sg) { - Some((a, b)) => a.byte != b.byte && !b.continues_block(), + Some((a, b)) => a.byte != b.byte && !b.continues_block() && !same_warichu_block(a, b), None => hyphen, } } diff --git a/crates/text/src/layout/wrapping_tests.rs b/crates/text/src/layout/wrapping_tests.rs index 24f0f02e58..790f8447f9 100644 --- a/crates/text/src/layout/wrapping_tests.rs +++ b/crates/text/src/layout/wrapping_tests.rs @@ -717,3 +717,26 @@ fn bunri_kinshi_progresses_with_an_overlong_pair_and_no_break() { assert_eq!(line(text, &pairs(text, false), 0, 46.5, Kinsoku::Hard, PushInFirst), 5); assert_eq!(line(text, &pairs(text, true), 0, 46.5, Kinsoku::Hard, PushInFirst), 3); } + +/// Justification adds no gap between two glyphs of one warichu block, whichever of them is drawn +/// first: right to left, the block's first glyph (the one that starts it) is drawn after the +/// others. Between glyphs of two blocks, the gap stays. +#[test] +fn justification_adds_no_gap_inside_a_warichu_block_drawn_right_to_left() { + let block = |g: &mut SGlyph, start: usize, pen: f64| { + g.wari = Some(crate::shape::Wari { start, pen, along: 0.0, across: 0.0, advance: g.adv, em: 10.0 }); + }; + // Glyph 1 is drawn first, then glyph 0, which starts the block. + let after = [None, Some(0)]; + for (first, second) in [('א', 'ב'), ('漢', 'א')] { + let mut glyphs = synthetic_glyphs(&[(0, 2, 10.0, first), (2, 2, 0.0, second)]); + block(&mut glyphs[0], 0, 0.0); + block(&mut glyphs[1], 0, 10.0); + assert!(!flow::is_letter_gap(1, &[], &after, 0, &glyphs, false), "{first}{second}"); + assert!(!flow::is_cjk_gap(1, &[], &after, 0, &glyphs), "{first}{second}"); + // The same two glyphs as the first glyphs of two blocks. + block(&mut glyphs[1], 2, 0.0); + assert!(flow::is_letter_gap(1, &[], &after, 0, &glyphs, false), "{first}{second}"); + assert_eq!(flow::is_cjk_gap(1, &[], &after, 0, &glyphs), first == '漢', "{first}{second}"); + } +}