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.
This commit is contained in:
dotCooCoo
2026-11-12 11:05:35 -08:00
parent 40f4c920e7
commit 9d8df6f27f
2 changed files with 38 additions and 5 deletions
+18 -5
View File
@@ -743,12 +743,25 @@ fn is_cjk_gap(glyph: usize, held: &[usize], after: &[Option<usize>], 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<usize>], 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
+20
View File
@@ -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::<f64>();
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