diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index e31a1a0a..c722b8c9 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -10,6 +10,8 @@ pub(crate) mod effects; /// Translations for the panels, tooltips and dialogs below the menu bar (see the module docs). mod ui; use effectcraft_engine::menus::{MenuEntry, MenuNode}; +#[cfg(not(target_arch = "wasm32"))] +pub(crate) use ui::catalog_characters; pub(crate) use ui::{set_current, tr, tr_args}; /// Every language with a catalog, in the order Settings ▸ General ▸ Language lists them. diff --git a/crates/ui-egui/src/i18n/ui.rs b/crates/ui-egui/src/i18n/ui.rs index 0a4c7b4c..b9cc3eb9 100644 --- a/crates/ui-egui/src/i18n/ui.rs +++ b/crates/ui-egui/src/i18n/ui.rs @@ -84,6 +84,13 @@ pub(super) fn missing(language: &str) -> Vec<&'static str> { ROWS.iter().flat_map(|(_, rows)| rows.iter().map(|(en, _)| *en)).filter(|en| seen.insert(*en) && !table.is_some_and(|c| c.contains_key(en))).collect() } +/// Characters required by the fixed panel catalog, for system-font coverage selection. +/// Sorted and deduplicated so fallback registration is deterministic. +#[cfg(not(target_arch = "wasm32"))] +pub(crate) fn catalog_characters(language: &str) -> Vec { + catalog(language).into_iter().flat_map(|c| c.values()).flat_map(|s| s.chars()).collect::>().into_iter().collect() +} + /// Translate one interface string: the current language's row for `text`, or `text` itself. pub(crate) fn tr(text: &str) -> &str { catalog(current()).and_then(|c| c.get(text)).copied().unwrap_or(text) diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index 290e3d4e..eca8b2fb 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -408,7 +408,14 @@ fn install_script_fallbacks(family_fonts: &mut FontDefinitions, language: &str) let cjk = if is_chinese(language) { [han, KANA] } else { [KANA, han] }; let mut names = Vec::new(); let mut seen = Vec::new(); - for &(probe, name) in cjk.iter().chain(SCRIPT_PROBES).chain(std::iter::once(&HANGUL)) { + // A font covering one Han probe may be a sparse subset. Register additional faces + // needed by the actual interface catalog instead of assuming whole-script coverage. + let extra = if is_chinese(language) { crate::i18n::catalog_characters(language) } else { Vec::new() }; + let extra: Vec<_> = extra.into_iter().filter(|ch| *ch >= '\u{2e80}').map(|ch| (ch, "catalog-system")).collect(); + for &(probe, name) in cjk.iter().chain(SCRIPT_PROBES).chain(std::iter::once(&HANGUL)).chain(&extra) { + if name == "catalog-system" && seen.iter().any(|&id| fonts::face(id).has_char(probe)) { + continue; + } // The interface asks for the platform's UI face first; the order a composed frame // renders with is `fonts::fallback_for`'s and is not touched here. Another context may // install a different language concurrently, so the shared fallback is asked with this @@ -432,7 +439,8 @@ fn install_script_fallbacks(family_fonts: &mut FontDefinitions, language: &str) // Use the already-read, parsed bytes rather than reading a font file twice. let mut data = FontData::from_owned(font.data().as_bytes().to_vec()); data.index = face.info.index; - family_fonts.font_data.insert(name.to_string(), Arc::new(data)); + let name = if name == "catalog-system" { format!("{name}-{:x}", probe as u32) } else { name.to_string() }; + family_fonts.font_data.insert(name.clone(), Arc::new(data)); for family in family_fonts.families.values_mut() { family.push(name.to_string()); }