From 9d8df6f27f4934a2e05fb9d66e1b0fa37fc139c5 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 12 Nov 2026 11:05:35 -0800 Subject: [PATCH] Type: Bunri-Kinshi finds a pair by its clusters in right-to-left justification Single Word Justification adds no space inside an inseparable pair. A right-to-left line draws the second half's cluster before the first half, with the cluster's glyphs in logical order, so a dash followed by a combining mark puts the mark next to the first dash. The pair is now found from the two neighboring clusters instead of neighboring glyph indices. --- crates/text/src/layout/flow.rs | 23 ++++++++++++++++++----- crates/text/src/tests.rs | 20 ++++++++++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/crates/text/src/layout/flow.rs b/crates/text/src/layout/flow.rs index 95ee27184f..3b364a00ea 100644 --- a/crates/text/src/layout/flow.rs +++ b/crates/text/src/layout/flow.rs @@ -743,12 +743,25 @@ fn is_cjk_gap(glyph: usize, held: &[usize], after: &[Option], index: usiz }) && !in_pair(glyph, index, after, sg) } -// Bunri-Kinshi: are glyph `glyph` and the glyph drawn after it the two halves of an inseparable -// pair? Justification adds no space between them. +// Bunri-Kinshi: are glyph `glyph` and the glyph drawn after it in the two halves of an inseparable +// pair? Justification adds no space between them. The halves are neighboring clusters: the +// earlier one's last glyph keeps the next, and the later one starts right after it. Right to left, +// the clusters are drawn in the other order, each with its glyphs in logical order, so the two +// drawn neighbors need not be neighbors in `sg`. fn in_pair(glyph: usize, index: usize, after: &[Option], sg: &[SGlyph]) -> bool { - drawn_after(index, glyph, after).is_some_and(|next| { - (next == glyph + 1 && sg.get(glyph).is_some_and(|a| a.keeps_next)) || (glyph == next + 1 && sg.get(next).is_some_and(|b| b.keeps_next)) - }) + let Some(next) = drawn_after(index, glyph, after) else { + return false; + }; + let (Some(a), Some(b)) = (sg.get(glyph), sg.get(next)) else { + return false; + }; + if a.byte == b.byte { + return false; + } + let (first, later) = if a.byte < b.byte { (glyph, b.byte) } else { (next, a.byte) }; + let byte = sg.get(first).map_or(0, |g| g.byte); + let last = first + sg.iter().skip(first + 1).take_while(|g| g.byte == byte).count(); + sg.get(last).is_some_and(|g| g.keeps_next) && sg.get(last + 1).is_some_and(|g| g.byte == later) } // Single Word Justification's Full Justify spreads a line's leftover room over the gaps between diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 345a1af735..bbb7224b75 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -2631,6 +2631,26 @@ fn bunri_kinshi_adds_no_justification_space_inside_a_pair() { let (on, a) = desired(true); assert!((on - natural).abs() < 1e-6, "on: the dashes keep their spacing"); assert!(a > layout(db(), &point(text, style(20.0))).glyphs[0].advance + 1.0, "on: other letters still take Desired spacing"); + // Single Word Justification's Full Justify, right to left: the second dash's cluster (the dash + // and a combining mark) is drawn before the first dash, and the mark's gap, which opens next + // to the first dash, takes no space. The other gaps of the word still do. + let text = "אב——\u{301}גד"; + let (first, second) = ("אב".len(), "אב—".len()); + let width = |l: &TextLayout, b: usize| l.glyphs.iter().filter(|g| g.byte == b).map(|g| g.advance).sum::(); + let natural = layout(db(), &point(text, style(20.0))); + assert!(natural.glyphs.iter().filter(|g| g.byte == second).count() > 1, "the dash and its mark are one cluster"); + let set = |bunri: bool| { + let mut t = area(text, style(20.0), frame, Justify::JustifyAll); + t.para.bunri_kinshi = bunri; + let l = layout(db(), &t); + assert!((l.lines[0].x1 - frame.x1).abs() < 1e-6, "{bunri}: {:?}", l.lines[0]); + (width(&l, first), width(&l, second)) + }; + let (_, off) = set(false); + assert!(off > width(&natural, second) + 1.0, "off: Full Justify widens the gap between the dashes"); + let (dash, on) = set(true); + assert!((on - width(&natural, second)).abs() < 1e-6, "on: the pair keeps its spacing right to left"); + assert!(dash > width(&natural, first) + 1.0, "on: the gap on the pair's other side still widens"); } /// No Break (Character panel menu) keeps the marked words on one line, where the same text wraps