diff --git a/render-wasm/src/shapes/text_horizontal/ruby.rs b/render-wasm/src/shapes/text_horizontal/ruby.rs index 4869c9f3a0..60942d6142 100644 --- a/render-wasm/src/shapes/text_horizontal/ruby.rs +++ b/render-wasm/src/shapes/text_horizontal/ruby.rs @@ -384,11 +384,22 @@ pub(super) fn horizontal_ruby_pieces( /// Ink edge shared by the glyphs of one ruby line: the lowest ink bottom for /// over ruby, the highest ink top for under ruby. One edge for the whole line /// keeps marks whose ink sits mid-height, such as ー and ・, on the line's -/// baseline instead of pulling each of them onto the base. +/// baseline instead of pulling each of them onto the base. Western letters +/// (Pinyin, romaji) take the font's descent and cap height instead of their +/// ink, so every reading keeps one baseline whatever its descenders, ascenders +/// or tone marks. pub(super) fn ruby_line_ink_edge(shaped: &[ShapedRun], glyphs: &[RubyGlyphRef], over: bool) -> f32 { - let edges = glyphs.iter().map(|(run_index, glyph, _, _)| { + let edges = glyphs.iter().map(|(run_index, glyph, _, western)| { let run = &shaped[*run_index]; let (_, metrics) = run.font.metrics(); + if *western { + if over { + return metrics.descent; + } + if metrics.cap_height > 0.0 { + return -metrics.cap_height; + } + } let fallback = if over { metrics.descent } else { diff --git a/render-wasm/src/shapes/text_horizontal/tests.rs b/render-wasm/src/shapes/text_horizontal/tests.rs index 4b9d30047f..1cf4136f76 100644 --- a/render-wasm/src/shapes/text_horizontal/tests.rs +++ b/render-wasm/src/shapes/text_horizontal/tests.rs @@ -1341,6 +1341,70 @@ fn horizontal_ruby_glyphs_share_one_baseline() { ); } +#[test] +fn latin_readings_share_one_baseline_whatever_their_letters() { + // Pinyin over 友聊: the descender of "yu" lifted it above "lia" when each + // reading attached its own ink bottom to the base. + use crate::shapes::text_vertical::test_support::{provider, EM}; + use crate::shapes::{FontFamily, FontStyle}; + use skia_safe::FontMgr; + + let font_provider = provider(include_bytes!("../../fonts/sourcesanspro-regular.ttf")); + let family = format!("{}", FontFamily::new(Uuid::nil(), 400, FontStyle::Normal)); + let size = EM / 2.0; + let shape = |text: &str| { + shape_segment_with_fallbacks( + text, + size, + &[family.clone()], + &font_provider, + false, + FontFeatures::None, + &FontMgr::from(font_provider.clone()), + "zh-Hans", + ) + }; + let base = skia::Rect::from_xywh(0.0, 0.0, EM, EM); + for side in [RubySide::Over, RubySide::Under] { + let baseline = |text: &str| { + let span = TextSpan { + ruby: text.to_string(), + ruby_side: side, + font_size: EM, + ..make_span("友", 0.0) + }; + let shaped = shape(text); + let glyphs = ruby_glyph_refs(text, &shaped, size); + assert!(glyphs.iter().all(|(_, _, _, western)| *western)); + let line = RubyLine { + span: &span, + shaped: &shaped, + glyphs: &glyphs, + base, + leading_gap: 0.0, + room: (0.0, 0.0), + }; + (line.baseline(0.0), shaped) + }; + let (descender, shaped) = baseline("yu"); + let (plain, _) = baseline("lia"); + assert!( + (descender - plain).abs() < 0.01, + "{side:?}: yu at {descender}, lia at {plain}" + ); + if side == RubySide::Over { + let mut bounds = [skia::Rect::default()]; + shaped[0] + .font + .get_bounds(&shaped[0].glyphs[..1], &mut bounds, None); + assert!( + descender + bounds[0].bottom <= horizontal_annotation_over_top(base, EM) + 0.01, + "the descender of y reaches into the base" + ); + } + } +} + #[test] fn emphasis_position_overrides_the_language_and_stacks_on_its_own_side() { let placement = super::emphasis::HorizontalEmphasisPlacement {