diff --git a/crates/text/src/layout.rs b/crates/text/src/layout.rs index f844b571..79209957 100644 --- a/crates/text/src/layout.rs +++ b/crates/text/src/layout.rs @@ -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, box_height: Option) 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, box_height: Option) .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, box_height: Option) }); } pen += it.adv; - if it.is_space() { + if it.stretches() { pen += justify_extra; } } diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 4475fbbc..d36c1589 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -519,3 +519,43 @@ fn lower_case_writes_final_sigma() { t.runs.push(figcraft_doc::StyleRun { start: 3, end: 4, style: other }); assert_eq!(gids(&t).iter().map(|g| g.0).collect::>(), want.iter().map(|g| g.0).collect::>(), "split runs"); } + +/// 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); + } + } +}