🐛 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
This commit is contained in:
alonso.torres 2026-11-05 16:50:45 +01:00
parent 31115ab29a
commit cc6f183a90
3 changed files with 73 additions and 1 deletions

View File

@ -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.

View File

@ -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| {

View File

@ -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