collective

Browse / github.com/storytold/cadcraft / patch

UI: find Fedora's Noto Sans CJK VF font and use its Simplified Chinese face

UI: find Fedora's Noto Sans CJK VF font and use its Simplified Chinese face ## Problem On Fedora 44, `google-noto-sans-cjk-vf-fonts` installs the CJK font at `/usr/share/fonts/google-noto-sans-cjk-vf-fonts/NotoSansCJK-VF.ttc` but `cjk_font()` only checks hard-coded Debian/Arch-style paths. `cjk_font()` therefore returns `None`, `can_show("zh")` is false, and the interface stays English even though a CJK font is installed and `interfaceLanguage` is `zh`. The language command reports "No Chinese font is installed...". Also, the Noto Sans CJK collection's first face is Japanese (JP, KR, SC, TC, HK); loading the file with egui's default face index 0 gives Japanese for

Status
open
Score
0.00
Adopters
0 other operators
Author
wesd6r (imported from pull request #2133, open)
Base
e86d895ba1086a7efc2a83048348f89098282d08
Size
+61 −2 in 2 files
Review
pending
Shared
2026-10-28
Id
349f2141ebbbd391d4349053

How it went

No other operator has reported using it yet.

Diff

raw · crates/fonts/src/ttf.rs crates/ui-egui/src/theme.rs

diff --git a/crates/fonts/src/ttf.rs b/crates/fonts/src/ttf.rs--- a/crates/fonts/src/ttf.rs+++ b/crates/fonts/src/ttf.rs@@ -75,6 +75,11 @@ impl Face {     pub fn has_glyph(&self, c: char) -> bool {         self.font().is_some_and(|f| f.charmap().map(c).is_some())     }+    /// The face's family name as written in the font (the first localized family name).+    pub fn family_name(&self) -> Option<String> {+        let f = self.font()?;+        f.localized_strings(skrifa::string::StringId::FAMILY_NAME).next().map(|s| s.to_string())+    }     fn names(&self) -> Vec<String> {         let Some(f) = self.font() else { return Vec::new() };         [skrifa::string::StringId::FAMILY_NAME, skrifa::string::StringId::TYPOGRAPHIC_FAMILY_NAME, skrifa::string::StringId::FULL_NAME]diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs--- a/crates/ui-egui/src/theme.rs+++ b/crates/ui-egui/src/theme.rs@@ -348,15 +348,21 @@ pub fn install_fonts(ctx: &egui::Context, language: &str) {         if !chinese {             paths.extend(japanese_font());         }-        paths.extend(cjk_font());         paths.dedup();         added.extend(             paths                 .into_iter()                 .filter_map(|p| read_capped(p, MAX_UI_FONT_BYTES))                 .enumerate()-                .map(|(i, bytes)| (format!("system-cjk-{i}"), std::sync::Arc::new(egui::FontData::from_owned(bytes)))),+                .map(|(i, bytes)| (format!("system-japanese-{i}"), std::sync::Arc::new(egui::FontData::from_owned(bytes)))),         );+        // A Chinese interface takes the collection's Simplified Chinese face; a collection's+        // first face is often Japanese (Noto Sans CJK JP), whose shared glyphs use Japanese forms.+        if let Some(face) = cjk_ui_face(language) {+            let mut data = egui::FontData::from_owned(face.bytes.as_ref().clone());+            data.index = face.index;+            added.push(("system-cjk".into(), std::sync::Arc::new(data)));+        }         // العربية: an installed Arabic face after the Latin ones (egui's bundled faces carry no         // Arabic glyphs), for the Arabic interface.         if language == "ar"@@ -445,11 +451,16 @@ pub fn cjk_font() -> Option<&'static std::path::Path> {                         "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",                         "/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc",                         "/usr/share/fonts/opentype/noto/NotoSansCJKsc-Regular.otf",+                        // Fedora's google-noto-sans-cjk-vf-fonts and google-noto-sans-cjk-fonts.+                        "/usr/share/fonts/google-noto-sans-cjk-vf-fonts/NotoSansCJK-VF.ttc",+                        "/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Regular.ttc",                         "/usr/local/share/fonts/noto/NotoSansCJK-Regular.ttc",                         "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",                         "/usr/share/fonts/wqy-microhei/wqy-microhei.ttc",                         "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",                         "/usr/share/fonts/truetype/droid/DroidSansFallbackFull.ttf",+                        // Fedora's google-droid-sans-fonts.+                        "/usr/share/fonts/google-droid-sans-fonts/DroidSansFallbackFull.ttf",                     ]                 };                 candidates.iter().map(std::path::PathBuf::from).find(|p| p.is_file())@@ -460,6 +471,27 @@ pub fn cjk_font() -> Option<&'static std::path::Path> {     None } +/// The installed CJK face the interface loads: the Simplified Chinese face of a collection for+/// a Chinese interface, else its first face (a Noto Sans CJK collection starts with Japanese).+#[cfg(not(target_arch = "wasm32"))]+fn cjk_ui_face(language: &str) -> Option<cadcraft_fonts::ttf::Face> {+    let bytes = std::sync::Arc::new(read_capped(cjk_font()?, MAX_UI_FONT_BYTES)?);+    let faces = cadcraft_fonts::ttf::faces(&bytes);+    if language.starts_with("zh") {+        faces.iter().find(|f| f.family_name().is_some_and(|n| is_simplified_family(&n))).cloned().or_else(|| faces.into_iter().next())+    } else {+        faces.into_iter().next()+    }+}++/// Whether a font family name names a Simplified Chinese face (Noto Sans CJK SC, Source Han+/// Sans SC, Microsoft YaHei, ...), not the Japanese, Korean or Traditional face of the same+/// collection.+fn is_simplified_family(name: &str) -> bool {+    let name = name.to_lowercase();+    name.contains(" sc") || name.contains("simplified") || name.contains("yahei") || name.contains("simsun") || name.contains("wenquanyi")+}+ /// An installed font with Arabic glyphs, for the العربية interface; fonts are never bundled /// (only looked for where the OS or common packages put them). Checked once. pub fn arabic_font() -> Option<&'static std::path::Path> {@@ -769,4 +801,26 @@ mod tests {         assert!(!super::embedded_script("Hans") || super::can_show("zh"));         assert!(super::can_show("en") && super::can_show("uk"));     }++    #[test]+    fn simplified_family_names_are_recognised() {+        for name in ["Noto Sans CJK SC", "Source Han Sans SC", "Microsoft YaHei", "SimSun", "PingFang SC", "WenQuanYi Micro Hei"] {+            assert!(super::is_simplified_family(name), "{name}");+        }+        for name in ["Noto Sans CJK JP", "Noto Sans CJK KR", "Noto Sans CJK TC", "Noto Sans CJK HK", "BIZ UDPGothic"] {+            assert!(!super::is_simplified_family(name), "{name}");+        }+    }++    /// Where the installed CJK font is a collection with a Simplified Chinese face, a Chinese+    /// interface loads that face rather than the collection's first (usually Japanese) one.+    #[cfg(not(target_arch = "wasm32"))]+    #[test]+    fn chinese_interface_takes_the_collections_simplified_face() {+        let Some(path) = super::cjk_font() else { return };+        let Some(bytes) = super::read_capped(path, super::MAX_UI_FONT_BYTES) else { return };+        let faces = cadcraft_fonts::ttf::faces(&std::sync::Arc::new(bytes));+        let Some(simplified) = faces.iter().find(|f| f.family_name().is_some_and(|n| super::is_simplified_family(&n))) else { return };+        assert_eq!(super::cjk_ui_face("zh").map(|f| f.index), Some(simplified.index));+    } }