From e3367c8534c0f7a243f0b43bb943c2621a76bda1 Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Thu, 16 Jul 2026 11:23:12 +0200 Subject: [PATCH] fix symbols --- render-wasm/src/shapes/text_vertical.rs | 155 +++++++++++++++++++++++- 1 file changed, 152 insertions(+), 3 deletions(-) diff --git a/render-wasm/src/shapes/text_vertical.rs b/render-wasm/src/shapes/text_vertical.rs index 57fd8fc071..92fe5fb3fd 100644 --- a/render-wasm/src/shapes/text_vertical.rs +++ b/render-wasm/src/shapes/text_vertical.rs @@ -720,8 +720,38 @@ fn capped_equal_allocations(capacities: &[(usize, f32)], amount: f32) -> Vec<(us allocations } -fn spacing_owner(_before: JapaneseClass, after: JapaneseClass, boundary: usize) -> (usize, bool) { - if matches!( +fn explicit_pair_spacing_em(before: JapaneseClass, after: JapaneseClass) -> f32 { + if matches!(before, JapaneseClass::DividingPunctuation) { + pair_rule(before, after).preferred_em + } else { + 0.0 + } +} + +fn materialize_explicit_pair_spacing( + cells: &mut [VerticalCell], + items: &mut [FlowItem], + classes: &[Option], +) { + for boundary in 0..cells.len().saturating_sub(1) { + let (Some(before), Some(after)) = (classes[boundary], classes[boundary + 1]) else { + continue; + }; + let spacing_em = explicit_pair_spacing_em(before, after); + if spacing_em <= 0.0 { + continue; + } + let em = cells[boundary].font_size.min(cells[boundary + 1].font_size); + let spacing = spacing_em * em; + cells[boundary].extent += spacing; + items[boundary].extent += spacing; + } +} + +fn spacing_owner(before: JapaneseClass, after: JapaneseClass, boundary: usize) -> (usize, bool) { + if explicit_pair_spacing_em(before, after) > 0.0 { + (boundary, false) + } else if matches!( after, JapaneseClass::OpeningBracket | JapaneseClass::MiddleDot ) { @@ -733,6 +763,36 @@ fn spacing_owner(_before: JapaneseClass, after: JapaneseClass, boundary: usize) } } +fn discard_explicit_spacing_at_column_edges( + cells: &mut [VerticalCell], + items: &mut [FlowItem], + classes: &[Option], + pair_spacing_em: &mut [f32], + placements: &[(usize, f32)], +) -> bool { + let mut changed = false; + for boundary in 0..cells.len().saturating_sub(1) { + if placements[boundary].0 == placements[boundary + 1].0 { + continue; + } + let (Some(before), Some(after)) = (classes[boundary], classes[boundary + 1]) else { + continue; + }; + if explicit_pair_spacing_em(before, after) <= 0.0 { + continue; + } + let em = cells[boundary].font_size.min(cells[boundary + 1].font_size); + let spacing = pair_spacing_em[boundary] * em; + if spacing <= 0.0 { + continue; + } + reduce_cell_spacing(cells, items, boundary, spacing, false); + pair_spacing_em[boundary] = 0.0; + changed = true; + } + changed +} + fn reduce_cell_spacing( cells: &mut [VerticalCell], items: &mut [FlowItem], @@ -2197,6 +2257,12 @@ pub fn layout_vertical( items[i].extent = target; } + // cl-04 question/exclamation marks carry an explicit one-em space + // after their character frame. Unlike bracket/comma/full-stop aki, + // this space is not embedded in the font's full-width advance, so it + // must exist in the flow extent before oikomi is allowed to reduce it. + materialize_explicit_pair_spacing(&mut para_cells, &mut items, &flow_classes); + // Long ruby with no slack: grow the base span's flow extent *before* // column planning so the wrap itself makes room (forced spreading). // The growth becomes inter-character gaps, so only the cells before @@ -2255,6 +2321,18 @@ pub fn layout_vertical( // the preceding column. The loop is bounded and each iteration can // only shrink a previously untrimmed item. let mut placements = plan_columns(&items, max_height); + for _ in 0..para_cells.len() { + if !discard_explicit_spacing_at_column_edges( + &mut para_cells, + &mut items, + &flow_classes, + &mut pair_spacing_em, + &placements, + ) { + break; + } + placements = plan_columns(&items, max_height); + } for _ in 0..para_cells.len() { let mut changed = false; for i in 0..para_cells.len() { @@ -5548,10 +5626,18 @@ mod tests { } fn layout_with(provider: &TypefaceFontProvider, content: &TextContent) -> VerticalLayout { + layout_with_height(provider, content, 1000.0) + } + + fn layout_with_height( + provider: &TypefaceFontProvider, + content: &TextContent, + max_height: f32, + ) -> VerticalLayout { let fallback_mgr = FontMgr::from(provider.clone()); layout_vertical( content, - 1000.0, + max_height, provider, fallback_mgr, &[], @@ -5822,6 +5908,69 @@ mod tests { ); } + #[test] + fn dividing_punctuation_materializes_trailing_aki() { + let provider = vmtx_provider(); + let terminal = layout_with(&provider, &make_content(&["!"], 1000.0)); + let before_ideograph = layout_with(&provider, &make_content(&["!く"], 1000.0)); + let before_dividing = layout_with(&provider, &make_content(&["!?"], 1000.0)); + let before_closing = layout_with(&provider, &make_content(&["!」"], 1000.0)); + let em = 20.0; + let natural_extent = terminal.cells[0].extent; + + assert!( + (before_ideograph.cells[0].extent - natural_extent - em).abs() < 0.01, + "! before ordinary text adds one em, got {} over natural {natural_extent}", + before_ideograph.cells[0].extent + ); + assert!( + (before_dividing.cells[0].extent - natural_extent - em).abs() < 0.01, + "! before ? adds one em, got {} over natural {natural_extent}", + before_dividing.cells[0].extent + ); + assert!( + (before_closing.cells[0].extent - natural_extent).abs() < 0.01, + "! before a closing bracket adds no aki, got {} over natural {natural_extent}", + before_closing.cells[0].extent + ); + } + + #[test] + fn dividing_punctuation_oikomi_never_compresses_glyph_frames() { + let provider = vmtx_provider(); + let terminal_exclamation = layout_with(&provider, &make_content(&["!"], 1000.0)); + let terminal_question = layout_with(&provider, &make_content(&["?"], 1000.0)); + let layout = layout_with_height(&provider, &make_content(&["く!?く"], 60.0), 60.0); + + assert!( + layout.cells[1].extent + 0.01 >= terminal_exclamation.cells[0].extent, + "oikomi compressed the ! glyph frame from {} to {}", + terminal_exclamation.cells[0].extent, + layout.cells[1].extent + ); + assert!( + layout.cells[2].extent + 0.01 >= terminal_question.cells[0].extent, + "oikomi compressed the ? glyph frame from {} to {}", + terminal_question.cells[0].extent, + layout.cells[2].extent + ); + } + + #[test] + fn dividing_punctuation_discards_trailing_aki_at_column_edge() { + let provider = vmtx_provider(); + let terminal = layout_with(&provider, &make_content(&["!"], 1000.0)); + let layout = layout_with_height(&provider, &make_content(&["!く"], 20.0), 20.0); + + assert_ne!(layout.cells[0].column, layout.cells[1].column); + assert!( + (layout.cells[0].extent - terminal.cells[0].extent).abs() < 0.01, + "column-end ! should discard its trailing aki, got {} over natural {}", + layout.cells[0].extent, + terminal.cells[0].extent + ); + } + #[test] fn ordinary_opening_punctuation_keeps_preferred_aki() { // く「く: the opening bracket keeps its leading half-em aki in the