From 932426eaadd2af652b0885e48ce4c9f68466924c Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 1 Nov 2026 08:22:06 -0800 Subject: [PATCH] Type: a line holding right-to-left text pushes out A line with right-to-left text is reordered before it is drawn, and justification closes the gaps between its glyphs in that order, which can differ from the gaps the push-in counted. Such a line could be taken as fitting and then run past its frame. Kinsoku Shori Type now pushes it out. --- crates/text/src/layout/breaking.rs | 9 +++++++-- crates/text/src/layout/wrapping_tests.rs | 9 +++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/crates/text/src/layout/breaking.rs b/crates/text/src/layout/breaking.rs index e996cbba8..f41e4943f 100644 --- a/crates/text/src/layout/breaking.rs +++ b/crates/text/src/layout/breaking.rs @@ -518,8 +518,8 @@ const PUSH_IN_RUN: usize = 16; /// before it, makes room for them. Push In First tries this before it pushes out. Push Out First /// tries it only when pushing out, to the line's last allowed break `pushed_out`, would leave the /// line short by more than an em of the character before `j`. Push Out Only never pushes in, and -/// neither does a line holding a tab, nor one where the characters taken in reach a space after a -/// character that may not end a line. With `hanging` (Optical Margin Alignment or Roman Hanging +/// neither does a line holding a tab or right-to-left text, nor one where the characters taken in +/// reach a space after a character that may not end a line. With `hanging` (Optical Margin Alignment or Roman Hanging /// Punctuation), the gaps that hanging punctuation keeps open are not counted. #[allow(clippy::too_many_arguments)] fn push_in( @@ -569,6 +569,11 @@ fn push_in( while last > i && g[last - 1].is_space() { last -= 1; } + // A line holding right-to-left text is reordered before it is drawn, and justification closes + // the gaps between its glyphs in that order: it pushes out. + if g[i..last].iter().any(|h| h.len > 0 && h.level.is_rtl()) { + return None; + } let punct = |k: usize| g.get(k).is_some_and(optical_punct); let quote = |k: usize| g.get(k).is_some_and(|h| is_quote(h.ch)); let may_hang = |a: usize| { diff --git a/crates/text/src/layout/wrapping_tests.rs b/crates/text/src/layout/wrapping_tests.rs index e6b6ad08f..ea12f601b 100644 --- a/crates/text/src/layout/wrapping_tests.rs +++ b/crates/text/src/layout/wrapping_tests.rs @@ -223,4 +223,13 @@ fn kinsoku_shori_type_pushes_a_closing_bracket_in_or_out() { assert_eq!(end(tabbed, 146.0, Kinsoku::Hard, PushOutOnly), 12); assert_eq!(end(tabbed, 146.0, Kinsoku::Hard, PushOutFirst), 12); assert_eq!(end(tabbed, 146.0, Kinsoku::Hard, PushInFirst), 12); + + // A line holding right-to-left text is reordered before it is drawn, so the gaps that close + // differ from the ones counted here: it pushes out. + let mixed = "一二三א五六七八九十一二三四五」六"; + let mut glyphs = full_width(mixed); + glyphs[3].level = unicode_bidi::Level::rtl(); + let mixed_end = |ty: KinsokuShoriType| break_line(mixed, &glyphs, 0, 150.0, &[], 0.0, None, Burasagari::None, Kinsoku::Hard, ty, false).0; + assert_eq!(mixed_end(PushInFirst), 14, "right-to-left text on the line: 五 is pushed out with 」"); + assert_eq!(mixed_end(PushOutOnly), 14); }