From 3a10a749db1bc91ea69b2af1188bb3b3759d3616 Mon Sep 17 00:00:00 2001 From: aiongg Date: Wed, 7 Oct 2026 23:08:13 -0400 Subject: [PATCH] Text: a missing font's substitute takes fallback fonts across a colour change Text is shaped in pieces that differ only in paint, so each piece group, and each piece shaped apart, takes its style's environment: a missing font's substitute draws the characters it lacks from fallback fonts whatever Draw Missing Glyphs from Fallback Fonts says. --- crates/compose/src/shape.rs | 7 +++++-- crates/compose/src/tests.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/crates/compose/src/shape.rs b/crates/compose/src/shape.rs index 8e5eefdd9..b50b39d1e 100644 --- a/crates/compose/src/shape.rs +++ b/crates/compose/src/shape.rs @@ -375,14 +375,17 @@ pub(crate) fn shape_para( let range = group[0].range.start..group[j - i - 1].range.end; let runs: Vec<(usize, u32)> = group.iter().map(|p| (p.range.start, p.style)).collect(); let start = glyphs.len(); - shape_run(db, &story.text, range, &first.props, auto_leading, &StyleRuns(&runs), sub, &mut glyphs); + // A missing font's substitute stands in for the whole font: fallback fonts help it. + // Pieces shaped together share their font, so the first one's says for all. + shape_run(db, &story.text, range, &first.props, table.env_for(auto_leading, first.style), &StyleRuns(&runs), sub, &mut glyphs); // A cluster (a ligature, a letter with its marks) that spans two styles would paint one // of them in the other's colour: shape those pieces apart instead. let spans_cut = |g: &Glyph| runs[1..].iter().any(|&(cut, _)| g.byte < cut && cut < g.byte + g.len); if group.len() > 1 && glyphs[start..].iter().any(spans_cut) { glyphs.truncate(start); for p in group { - shape_run(db, &story.text, p.range.clone(), &p.props, auto_leading, &StyleRuns(&[(p.range.start, p.style)]), sub, &mut glyphs); + let env = table.env_for(auto_leading, p.style); + shape_run(db, &story.text, p.range.clone(), &p.props, env, &StyleRuns(&[(p.range.start, p.style)]), sub, &mut glyphs); } } i = j; diff --git a/crates/compose/src/tests.rs b/crates/compose/src/tests.rs index 66b064887..6cd4105d4 100644 --- a/crates/compose/src/tests.rs +++ b/crates/compose/src/tests.rs @@ -1570,6 +1570,32 @@ fn missing_glyphs_are_the_fonts_box_unless_fallback_is_on() { assert_ne!(gid, 0, "a fallback font draws it"); } +/// A missing font's substitute draws what it lacks from fallback fonts whatever the setting, also +/// in text shaped as one piece across a colour change. +#[test] +fn a_missing_fonts_substitute_takes_fallback_fonts() { + use designcraft_fonts::testing::font_with; + designcraft_fonts::FontDb::global().add_font(font_with("DC Test Missing Glyph Helper", &['語']).unwrap()); + let text = "a語b語"; + let (mut d, sid, _) = doc_with(text, Rect::new(36.0, 36.0, 300.0, 100.0), ParaAttrs::default()); + { + let s = d.story_mut(sid).unwrap(); + s.format_chars(0..text.len(), |f| { + f.over.language = Some("Japanese".into()); + f.over.font_family = Some("DC Test No Such Family".into()); + }); + s.format_chars("a語".len()..text.len(), |f| f.over.fill = Some("Red".into())); + } + assert!(!d.settings.glyph_fallback); + let cs = compose_story(&d, sid, &ComposeOptions::default()); + let glyphs: Vec<&PlacedGlyph> = all_lines(&cs).into_iter().flat_map(|l| l.glyphs.iter()).filter(|g| text[g.byte..].starts_with('語')).collect(); + assert_eq!(glyphs.len(), 2); + for g in glyphs { + assert_ne!(g.face.family, designcraft_fonts::DEFAULT_FAMILY, "byte {}", g.byte); + assert_ne!(g.gid, 0, "byte {}: a fallback font draws it", g.byte); + } +} + #[test] fn kenten_missing_from_the_font_are_its_box_unless_fallback_is_on() { use designcraft_fonts::testing::font_with;