From aab3682629d08737bba2dfb902e6847e79012d0e Mon Sep 17 00:00:00 2001 From: mtlcl410 Date: Sun, 8 Nov 2026 14:19:30 +0800 Subject: [PATCH] M3: right-to-left punctuation doesn't decide a line's direction, and an alphabetic mark gives a taken-back neutral no direction (view.copy-text) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Pq7HyCHD1R3rywQBPCHXZA --- crates/render/src/text/order.rs | 15 +++++++++------ crates/render/src/text/tests.rs | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/crates/render/src/text/order.rs b/crates/render/src/text/order.rs index 98d2f9454d..7850b603c7 100644 --- a/crates/render/src/text/order.rs +++ b/crates/render/src/text/order.rs @@ -356,11 +356,13 @@ fn keep_taken_back_blanks(glyphs: &[&TextGlyph], placed: &mut [usize]) { let mark = |g: &TextGlyph| !g.text.is_empty() && g.text.chars().all(|c| unicode_bidi::bidi_class(c) == unicode_bidi::BidiClass::NSM); // The direction a glyph gives the text around it: Some(false) for left-to-right letters, Some(true) for // right-to-left letters and for numbers, which read as right-to-left beside neutrals (UAX #9 N1, as in - // `reverse_rtl`), None for a glyph without one. + // `reverse_rtl`), None for a glyph without one. Nonspacing marks give none, alphabetic ones (U+0345) included: a + // mark has the direction of the letter it sits on. let strong = |g: &TextGlyph| { - if g.text.chars().any(|c| c.is_alphabetic() && !is_rtl(c)) { + let spacing = || g.text.chars().filter(|&c| unicode_bidi::bidi_class(c) != unicode_bidi::BidiClass::NSM); + if spacing().any(|c| c.is_alphabetic() && !is_rtl(c)) { Some(false) - } else if g.text.chars().any(char::is_alphanumeric) { + } else if spacing().any(char::is_alphanumeric) { Some(true) } else { None @@ -597,13 +599,14 @@ fn bidi_class(text: &str) -> Bidi { } /// Whether a line of the glyphs `line` is right-to-left: it has more right-to-left letters than left-to-right ones. -/// Letters are counted one by one, so a ligature glyph ("ffi") counts each of its letters, and nonspacing marks count -/// toward neither direction: a mark has the direction of the letter it sits on. +/// Letters are counted one by one, so a ligature glyph ("ffi") counts each of its letters. Punctuation of a +/// right-to-left script (a maqaf, an Arabic comma) is not a letter and counts toward neither direction, nor do +/// nonspacing marks: a mark has the direction of the letter it sits on. fn is_rtl_line(glyphs: &[&TextGlyph], line: impl Iterator) -> bool { let (mut rtl, mut ltr) = (0usize, 0usize); for g in line.filter_map(|i| glyphs.get(i)) { for c in g.text.chars().filter(|&c| unicode_bidi::bidi_class(c) != unicode_bidi::BidiClass::NSM) { - if is_rtl(c) && !c.is_numeric() { + if is_rtl(c) && c.is_alphabetic() { rtl = rtl.saturating_add(1); } else if c.is_alphabetic() && !is_rtl(c) { ltr = ltr.saturating_add(1); diff --git a/crates/render/src/text/tests.rs b/crates/render/src/text/tests.rs index 755a042c17..5dde3d8ea8 100644 --- a/crates/render/src/text/tests.rs +++ b/crates/render/src/text/tests.rs @@ -2482,6 +2482,40 @@ fn a_ligature_counts_each_of_its_letters_toward_a_lines_direction() { assert_eq!(line(&["ffi"]), split); } +/// Punctuation of a right-to-left script is not a letter and doesn't decide a line's direction: two Latin letters, +/// a Hebrew letter and a maqaf and a sof pasuq after it read Latin first. +#[test] +fn right_to_left_punctuation_does_not_decide_a_lines_direction() { + let mut v = vec![em_box("a", 0.0, 0.0, 6.0, 11.0), em_box("b", 6.0, 0.0, 12.0, 11.0)]; + let mut x = 16.0; + for c in ["\u{5C3}", "\u{5BE}", "ש"] { + v.push(em_box(c, x, 0.0, x + 6.5, 11.0)); + x += 6.5; + } + let text = layout(v).plain_text(); + assert!(text.starts_with("ab"), "{text:?}"); +} + +/// An alphabetic nonspacing mark (U+0345) gives the text around a taken-back neutral no direction: a question mark +/// taken back over the space that starts a reverse-order show string reads as on the same line without the mark. +#[test] +fn an_alphabetic_mark_beside_a_taken_back_neutral_gives_it_no_direction() { + let line = |mark: bool| { + let mut v = vec![em_box(" ", 10.0, 0.0, 13.5, 11.0), em_box("?", 9.9, 0.0, 15.0, 11.0)]; + let mut x = 15.0; + for c in ["ף", "י", "ע", "ס"] { + v.push(em_box(c, x, 0.0, x + 6.5, 11.0)); + x += 6.5; + } + if mark { + v.push(em_box("\u{345}", 14.0, 0.0, 14.0, 11.0)); + } + layout(v).plain_text() + }; + let without = line(false); + assert_eq!(line(true).replace('\u{345}', ""), without); +} + /// A mark of a left-to-right script that rides on no letter has no direction of its own: U+0345 (alphabetic, bidi /// class NSM) drawn on the last letter of a Hebrew word, left of it and right after a Latin word, reads with the /// Hebrew word, after that letter, as a Hebrew point drawn there does.