diff --git a/crates/gpui_ce_parley/src/text_system.rs b/crates/gpui_ce_parley/src/text_system.rs index 7492504972..8af4561f82 100644 --- a/crates/gpui_ce_parley/src/text_system.rs +++ b/crates/gpui_ce_parley/src/text_system.rs @@ -1463,6 +1463,34 @@ impl ParleyTextSystem { self } + /// Appends installed `families` after the operating system's own fallback for `script`. + /// + /// Some platforms answer a script fallback with a single face, chosen from one sample + /// character, so a character that face lacks has no candidate at all. Families that are + /// not installed are skipped, and the system's pick keeps its place first. + pub fn with_script_fallback_families( + self, + script: [u8; 4], + families: impl IntoIterator>, + ) -> Self { + { + let mut state = self.parley.lock(); + let collection = &mut state.font_context.collection; + let key = fontique::FallbackKey::new(fontique::Script::from_bytes(script), None); + // Resolve the system's pick first: a key that already has families never asks it. + let mut ids = collection.fallback_families(key).collect::>(); + for family in families { + if let Some(id) = collection.family_id(family.as_ref()) + && !ids.contains(&id) + { + ids.push(id); + } + } + collection.set_fallbacks(key, ids.into_iter()); + } + self + } + fn font_families<'a>( &'a self, descriptor: &'a Font, diff --git a/crates/gpui_windows/src/direct_write.rs b/crates/gpui_windows/src/direct_write.rs index efb40879c6..33f2e4f40c 100644 --- a/crates/gpui_windows/src/direct_write.rs +++ b/crates/gpui_windows/src/direct_write.rs @@ -91,7 +91,19 @@ impl DirectWriteTextSystem { "Segoe UI", BitmapFallbackGlyphRasterizer::new(SharedDirectWriteRenderer(renderer.clone())), ) - .with_fallback_families(["Lilex", "IBM Plex Sans", "Arial"]); + .with_fallback_families(["Lilex", "IBM Plex Sans", "Arial"]) + // DirectWrite picks one Han face from a sample character; on a Japanese or Korean + // pick, Simplified-only characters such as 简 would otherwise draw as .notdef. + .with_script_fallback_families( + *b"Hani", + [ + "Microsoft YaHei UI", + "Microsoft JhengHei UI", + "Yu Gothic UI", + "Malgun Gothic", + "SimSun-ExtB", + ], + ); Ok(Self { parley, renderer }) } @@ -959,6 +971,37 @@ mod tests { use gpui_parley::FontSynthesis; use std::collections::BTreeMap; + #[test] + fn han_fallback_covers_simplified_only_characters() -> Result<()> { + let system = DirectWriteTextSystem::new_headless()?; + // 简 is Simplified-only and absent from Japanese and Korean UI fonts; the rest + // are shared Han, Japanese and Hangul text that must keep a real glyph too. + for text in ["简体中文", "繁體中文", "日本語", "한국어"] { + let runs = [gpui::TextRun { + len: text.len(), + font: font("Segoe UI"), + ..Default::default() + }]; + let layout = system.layout_text(TextLayoutRequest { + text, + font_size: px(16.0), + runs: &runs, + options: Default::default(), + }); + let glyphs = layout + .paint_fragments + .iter() + .flat_map(|fragment| fragment.glyphs.iter()) + .collect::>(); + assert_eq!(glyphs.len(), text.chars().count(), "{text}"); + assert!( + glyphs.iter().all(|glyph| glyph.id != GlyphId(0)), + "{text} shaped a .notdef glyph" + ); + } + Ok(()) + } + #[test] fn fixed_fonts_cover_native_modes_instances_and_empty_glyphs() -> Result<()> { let system = DirectWriteTextSystem::new_headless()?;