diff --git a/crates/ui-egui/src/fonts.rs b/crates/ui-egui/src/fonts.rs index 2accc482..00c9c868 100644 --- a/crates/ui-egui/src/fonts.rs +++ b/crates/ui-egui/src/fonts.rs @@ -3,7 +3,8 @@ //! The UI's Latin font always comes first; a CJK font (then a Hangul font) is only ever appended as //! a fallback, so English text renders exactly as before and only glyphs the Latin font lacks use //! the CJK face. The Hangul fallback covers Korean where the CJK font found (Meiryo, Hiragino) -//! lacks Hangul. +//! lacks Hangul. The order of the CJK and Chinese faces follows the UI language +//! (see [`fallback_order`]): one face drawing a whole word keeps its glyphs on one baseline. use std::sync::Arc; @@ -428,33 +429,52 @@ fn find_system_cjk_font() -> Option> { None } -/// Configure font definitions with system CJK, Chinese and Hangul fallbacks where available. +/// The fallback entries in the order they join each family for a UI language. +/// +/// Chinese goes first for `zh`: a Japanese font (Meiryo, Yu Gothic) lacks the simplified +/// characters, so the rest of the word falls through to the Chinese font per glyph — and +/// epaint centres each face's own line height in the row, so glyphs from two faces of very +/// different height (Meiryo ≈ 2.0 em, Microsoft YaHei ≈ 1.4 em) sit at different heights +/// inside one word. One face for the whole word keeps them on one baseline. +pub fn fallback_order(language: &str) -> [&'static str; 3] { + if language == "zh" { [CHINESE_FALLBACK, CJK_FALLBACK, HANGUL_FALLBACK] } else { [CJK_FALLBACK, CHINESE_FALLBACK, HANGUL_FALLBACK] } +} + +/// Configure font definitions with system CJK, Chinese and Hangul fallbacks where available, +/// ordered for the UI language (see [`fallback_order`]). #[cfg(not(target_arch = "wasm32"))] -pub fn setup_fonts(ctx: &egui::Context) { +pub fn setup_fonts_for(ctx: &egui::Context, language: &str) { + let cjk = find_system_cjk_font(); + let chinese = find_system_chinese_font().filter(|bytes| cjk.as_ref() != Some(bytes)); + let hangul = HANGUL_FONT_CANDIDATES.iter().find_map(|path| try_read_font(std::path::Path::new(path))); let mut fonts = egui::FontDefinitions::default(); let mut found = false; - let cjk = find_system_cjk_font(); - if let Some(bytes) = cjk.clone().and_then(|bytes| with_cjk_fallback(fonts.clone(), bytes)) { - fonts = bytes; - found = true; - } - // Chinese after the CJK fallback: a Japanese-only CJK font lacks simplified characters, - // so Chinese falls through to this one per glyph. - let chinese = find_system_chinese_font().filter(|bytes| cjk.as_ref() != Some(bytes)); - if let Some(with_chinese) = chinese.and_then(|bytes| with_fallback(fonts.clone(), CHINESE_FALLBACK, bytes)) { - fonts = with_chinese; - found = true; - } - let hangul = HANGUL_FONT_CANDIDATES.iter().find_map(|path| try_read_font(std::path::Path::new(path))); - if let Some(with_hangul) = hangul.and_then(|bytes| with_fallback(fonts.clone(), HANGUL_FALLBACK, bytes)) { - fonts = with_hangul; - found = true; + for name in fallback_order(language) { + let bytes = match name { + CJK_FALLBACK => cjk.clone(), + CHINESE_FALLBACK => chinese.clone(), + _ => hangul.clone(), + }; + if let Some(bytes) = bytes + && let Some(with_fallback) = with_fallback(fonts.clone(), name, bytes) + { + fonts = with_fallback; + found = true; + } } if found { ctx.set_fonts(fonts); } } +/// Configure font definitions with system fallbacks for an English UI (all fallbacks are +/// installed whatever the language, so CJK text in a design still renders; only their order +/// follows the language, see [`setup_fonts_for`]). +#[cfg(not(target_arch = "wasm32"))] +pub fn setup_fonts(ctx: &egui::Context) { + setup_fonts_for(ctx, "en"); +} + /// Find a font covering simplified Chinese in the known candidate paths or by scanning the /// system font directories for [`CHINESE_FONT_KEYWORDS`] names. #[cfg(not(target_arch = "wasm32"))] @@ -478,6 +498,9 @@ fn find_system_chinese_font() -> Option> { /// On web, setup_fonts does not perform synchronous disk reads. /// Use asynchronous loading through [`solidcraft_web`] or host scripts. +#[cfg(target_arch = "wasm32")] +pub fn setup_fonts_for(_ctx: &egui::Context, _language: &str) {} + #[cfg(target_arch = "wasm32")] pub fn setup_fonts(_ctx: &egui::Context) {} @@ -509,6 +532,24 @@ mod tests { assert!(with_cjk_fallback(defaults, b"not a font".to_vec()).is_none()); } + #[test] + fn a_chinese_ui_puts_the_chinese_face_before_the_japanese_one() { + assert_eq!(fallback_order("zh"), [CHINESE_FALLBACK, CJK_FALLBACK, HANGUL_FALLBACK]); + for lang in ["en", "ja", "ko", "de"] { + assert_eq!(fallback_order(lang), [CJK_FALLBACK, CHINESE_FALLBACK, HANGUL_FALLBACK], "{lang} keeps the Japanese font first"); + } + let defaults = egui::FontDefinitions::default(); + let mut fonts = defaults.clone(); + for name in fallback_order("zh") { + fonts = with_fallback(fonts, name, latin_font_bytes()).unwrap(); + } + for family in [egui::FontFamily::Proportional, egui::FontFamily::Monospace] { + let list = &fonts.families[&family]; + assert_eq!(list.first(), defaults.families[&family].first(), "the Latin UI font stays first"); + assert_eq!(&list[list.len() - 3..], [CHINESE_FALLBACK, CJK_FALLBACK, HANGUL_FALLBACK]); + } + } + #[test] fn hangul_font_falls_back_after_the_cjk_font() { let defaults = egui::FontDefinitions::default(); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index afc4e37f..c54662b7 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -300,6 +300,9 @@ pub struct SolveApp { pub file_load: file_load::FileLoad, /// The other open designs (tabs). pub docs: documents::Documents, + /// The language the installed font fallback chain is ordered for (`fonts::fallback_order`); + /// changing `preferences.language` re-installs the fonts on the next frame. + applied_fonts_language: String, } impl SolveApp { @@ -358,6 +361,7 @@ impl SolveApp { home: home::HomeState::default(), file_load: file_load::FileLoad::default(), docs: documents::Documents::default(), + applied_fonts_language: "en".into(), } } @@ -1066,6 +1070,10 @@ impl SolveApp { pub fn ui(&mut self, ui: &mut egui::Ui) { expression_complete::begin_frame(ui.ctx()); let t0 = now_ms(); + if self.preferences.language != self.applied_fonts_language { + self.applied_fonts_language.clone_from(&self.preferences.language); + crate::fonts::setup_fonts_for(ui.ctx(), &self.preferences.language); + } let _language = crate::i18n::scope(&self.preferences.language); let mut report_input = None; issue_report::CadInput::block(self, ui, &mut report_input);