From cc6f183a903d05aa52c6de60729fdf7c138e0c78 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 5 Nov 2026 16:50:45 +0100 Subject: [PATCH] :bug: Let vertical Chinese and Korean ruby overhang its neighbours The pairwise vertical path (Chinese, Korean and plain text) grew ruby bases without first computing overhang rooms, so a long Pinyin reading always spread its base. Horizontal text, and vertical Japanese, let the reading hang over an adjacent kana, comma, full stop or middle dot. Run set_ruby_overhang_rooms before grow_ruby_bases on that path too. It uses ruby_overhang_room, the rule horizontal applies to every language, so Han neighbours still take no overhang and ruby-overhang none keeps the old growth. A comment at the early return notes that warichu never splits there: only Japanese renders warichu. AI-assisted-by: claude-opus-5-5 --- render-wasm/docs/cjk-layout.md | 6 +- .../src/shapes/text_vertical/annotations.rs | 63 +++++++++++++++++++ .../src/shapes/text_vertical/layout.rs | 5 ++ 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/render-wasm/docs/cjk-layout.md b/render-wasm/docs/cjk-layout.md index 71aa15f8e5..5f944ef288 100644 --- a/render-wasm/docs/cjk-layout.md +++ b/render-wasm/docs/cjk-layout.md @@ -81,7 +81,11 @@ merges their styles. Emphasis marks retain their per-base placement across wraps; text bounds include the marks so individual exports keep them whole. Without a text selection, presentation changes affect existing readings. Horizontal readings form one Latin group above or below the base; vertical -readings form one sideways Latin group beside it. Pronunciation is entered +readings form one sideways Latin group beside it. A reading longer than its +base overhangs an adjacent comma, full stop or middle dot (or kana) without +its own reading, by at most one ruby character and half the neighbour, in +both orientations. Han neighbours take no overhang. What overhang cannot +absorb, or all of it under ruby-overhang `none`, spreads the base. Pronunciation is entered by the author; the editor does not generate or distribute syllables. Composed and combining tone marks retain their source text and shaping. diff --git a/render-wasm/src/shapes/text_vertical/annotations.rs b/render-wasm/src/shapes/text_vertical/annotations.rs index 94ab790913..18cf50a347 100644 --- a/render-wasm/src/shapes/text_vertical/annotations.rs +++ b/render-wasm/src/shapes/text_vertical/annotations.rs @@ -1575,6 +1575,69 @@ mod tests { assert!(ruby.glyph_tops.last().unwrap() + ruby.font_size <= base.top + base.extent + 0.01); } + /// 文 中(zhuang) , 文 in vertical Chinese: the reading is longer than + /// its base, with a Han character before and a comma after. + fn chinese_pinyin_neighbour_layout(overhang: RubyOverhang) -> VerticalLayout { + let mut content = spans_content( + vec![ + make_span("文"), + TextSpan { + ruby: "zhuangzhuang".to_string(), + ruby_overhang: overhang, + ..make_span("中") + }, + make_span(",文"), + ], + 1000.0, + ); + content.paragraphs_mut()[0].set_language("zh-Hant-TW".to_string()); + layout_with_height(&provider(PINYIN_TEST_FONT), &content, 1000.0) + } + + fn column_extent(layout: &VerticalLayout) -> f32 { + layout + .cells + .iter() + .map(|cell| cell.top + cell.extent) + .fold(0.0, f32::max) + } + + #[test] + fn vertical_chinese_pinyin_overhangs_a_following_comma() { + let overhung = chinese_pinyin_neighbour_layout(RubyOverhang::Auto); + let grown = chinese_pinyin_neighbour_layout(RubyOverhang::None); + assert!( + column_extent(&overhung) < column_extent(&grown) - 0.5, + "overhang shortens the column: {} vs {}", + column_extent(&overhung), + column_extent(&grown) + ); + let ruby = ruby_of_span(&overhung, 1); + let base = overhung.cells.iter().find(|cell| cell.span == 1).unwrap(); + let ruby_end = ruby.glyph_tops.last().unwrap() + ruby.glyphs.last().unwrap().extent; + assert!( + ruby_end > base.top + base.extent + 0.5, + "the reading hangs over the comma" + ); + assert!( + ruby.glyph_tops[0] >= base.top - 0.01, + "a Han neighbour takes no overhang, as in horizontal text" + ); + } + + #[test] + fn vertical_chinese_pinyin_without_overhang_grows_its_base() { + let layout = chinese_pinyin_neighbour_layout(RubyOverhang::None); + let ruby = ruby_of_span(&layout, 1); + let base = layout.cells.iter().find(|cell| cell.span == 1).unwrap(); + let ruby_length: f32 = ruby.glyphs.iter().map(|glyph| glyph.extent).sum(); + assert!(ruby_length > EM, "the reading is longer than its base"); + assert!((base.extent - ruby_length).abs() < 0.01); + assert!(ruby.glyph_tops[0] >= base.top - 0.01); + let ruby_end = ruby.glyph_tops.last().unwrap() + ruby.glyphs.last().unwrap().extent; + assert!(ruby_end <= base.top + base.extent + 0.01); + } + #[test] fn ruby_base_moves_to_the_next_column_whole() { let content = |height: f32| { diff --git a/render-wasm/src/shapes/text_vertical/layout.rs b/render-wasm/src/shapes/text_vertical/layout.rs index 737ba2e5c8..43ff4db5d6 100644 --- a/render-wasm/src/shapes/text_vertical/layout.rs +++ b/render-wasm/src/shapes/text_vertical/layout.rs @@ -569,6 +569,11 @@ impl PlannedParagraph { let policy = cells.paragraph.layout_policy(); if !policy.uses_jlreq() { + // Warichu pieces never split here: only Japanese renders + // warichu, and the frontend drops it for other languages. + // Readings overhang neighbours by the rule horizontal text + // applies to every language (`ruby_overhang_room`). + set_ruby_overhang_rooms(&flow, &mut ruby_units); grow_ruby_bases(&mut flow, &ruby_units); let text: String = cells .text_map