mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-24 10:27:00 -06:00
M4: justified text doesn't widen U+200B
Thai, Lao, Khmer and Burmese are written without spaces between words. Tools that know the words insert U+200B ZERO WIDTH SPACE so lines can break between them, and line breaking already treats it as a space. But justification counted every space-class char as a gap, so a justified paragraph got extra room at every U+200B: visible gaps between all its words, which is not how these scripts are justified. Item::stretches() (a space other than U+200B) is used for the justify gap count, the trailing width and the pen advance. Line breaking is unchanged. Test: on a justified line of words joined by U+200B and one real space, the extra room goes to the real space only; with no real space the line is laid out as if left-aligned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
80f916326f
commit
30bba303aa
@@ -257,6 +257,11 @@ impl Item {
|
||||
fn is_space(&self) -> bool {
|
||||
crate::breaks::is_space(self.ch)
|
||||
}
|
||||
/// A space justified text widens: not U+200B, which only marks where a line may break (between
|
||||
/// the words of Thai, Lao, Khmer or Burmese text, written without spaces).
|
||||
fn stretches(&self) -> bool {
|
||||
self.is_space() && self.ch != '\u{200B}'
|
||||
}
|
||||
}
|
||||
|
||||
/// Bidi embedding levels per char, and whether each char's paragraph runs right to left.
|
||||
@@ -587,7 +592,7 @@ pub fn layout(text: &TextData, box_width: Option<f64>, box_height: Option<f64>)
|
||||
items.get(vis_end.wrapping_sub(1)).filter(|it| hang_end && !rtl_line && vis_end > *s + 1 && closes(it.ch)).map_or(0.0, |it| it.adv);
|
||||
let gstart = out.glyphs.len();
|
||||
// Justify: extra space per gap.
|
||||
let gaps = items.get(*s..vis_end).unwrap_or(&[]).iter().filter(|it| it.is_space()).count();
|
||||
let gaps = items.get(*s..vis_end).unwrap_or(&[]).iter().filter(|it| it.stretches()).count();
|
||||
let justify_extra = match (text.align_h, wrap) {
|
||||
(TextAlignH::Justified, Some(maxw)) if !para_end && !break_after(*e) && gaps > 0 => {
|
||||
((maxw - line_indent + hang_l + hang_r - width) / gaps as f64).max(0.0)
|
||||
@@ -603,7 +608,7 @@ pub fn layout(text: &TextData, box_width: Option<f64>, box_height: Option<f64>)
|
||||
.unwrap_or(&[])
|
||||
.iter()
|
||||
.filter(|it| !is_line_break(it.ch))
|
||||
.map(|it| it.adv + if it.is_space() { justify_extra } else { 0.0 })
|
||||
.map(|it| it.adv + if it.stretches() { justify_extra } else { 0.0 })
|
||||
.sum();
|
||||
let mut pen = if rtl_line { -trailing } else { line_indent - hang_l };
|
||||
// List marker (before the line's text; zero-length char range so carets skip it).
|
||||
@@ -698,7 +703,7 @@ pub fn layout(text: &TextData, box_width: Option<f64>, box_height: Option<f64>)
|
||||
});
|
||||
}
|
||||
pen += it.adv;
|
||||
if it.is_space() {
|
||||
if it.stretches() {
|
||||
pen += justify_extra;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -497,3 +497,43 @@ fn the_caret_after_a_line_break_sits_on_the_next_line() {
|
||||
let l = layout(&TextData::new("ab\n"), None, None);
|
||||
assert_eq!(l.caret(3), (0.0, 1));
|
||||
}
|
||||
|
||||
/// Thai is written without spaces between words; U+200B ZERO WIDTH SPACE marks where a line may
|
||||
/// break. ภาษา|ไทย|เขียน|ติดกัน ("Thai is written joined up") ไม่มี|ช่องว่าง|ระหว่าง|คำ ("with no
|
||||
/// spaces between words"), with one real space between the two phrases.
|
||||
#[test]
|
||||
fn justified_lines_dont_widen_zero_width_spaces() {
|
||||
if !have_fonts() {
|
||||
return;
|
||||
}
|
||||
let lay_out = |s: &str, align, w| {
|
||||
let mut t = TextData::new(s);
|
||||
t.style.font_size = 20.0;
|
||||
t.align_h = align;
|
||||
t.auto_resize = figcraft_doc::TextAutoResize::Height;
|
||||
layout(&t, w, None)
|
||||
};
|
||||
let chars_before = |s: &str, pat: &str| s.find(pat).map(|b| s[..b].chars().count()).unwrap();
|
||||
for (s, why) in [
|
||||
("ภาษา\u{200B}ไทย\u{200B}เขียน\u{200B}ติดกัน ไม่มี\u{200B}ช่องว่าง\u{200B}ระหว่าง\u{200B}คำ", "the real space takes the extra room"),
|
||||
("ภาษา\u{200B}ไทย\u{200B}เขียน\u{200B}ติดกัน\u{200B}ไม่มี\u{200B}ช่องว่าง\u{200B}ระหว่าง\u{200B}คำ", "no space to widen: as left-aligned"),
|
||||
] {
|
||||
// A box that ends inside ช่องว่าง: the first line breaks at the U+200B before it.
|
||||
let word = chars_before(s, "ช่องว่าง");
|
||||
let natural = lay_out(s, TextAlignH::Left, None);
|
||||
let (a, b) = (natural.caret(word).0, natural.caret(word + 4).0);
|
||||
assert!(b > a, "{why}: chars need advances");
|
||||
let w = a + (b - a) / 2.0;
|
||||
let left = lay_out(s, TextAlignH::Left, Some(w));
|
||||
let just = lay_out(s, TextAlignH::Justified, Some(w));
|
||||
assert!(just.lines.len() >= 2 && just.lines[0].char_end == word, "{why}: {:?}", just.lines);
|
||||
let space = s.chars().position(|c| c == ' ');
|
||||
let extra = w - left.lines[0].width;
|
||||
assert!(extra > 0.0, "{why}");
|
||||
assert_eq!(left.glyphs.len(), just.glyphs.len());
|
||||
for (l, j) in left.glyphs.iter().zip(&just.glyphs).filter(|(g, _)| g.char_end <= word) {
|
||||
let want = if space.is_some_and(|sp| l.char_start > sp) { extra } else { 0.0 };
|
||||
assert!((j.x - l.x - want).abs() < 1e-6, "{why}: char {} moved {} (want {want})", l.char_start, j.x - l.x);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user