From 70cfd2d4fa84ef44fd7552cc31ac42bf22a12819 Mon Sep 17 00:00:00 2001 From: con3code Date: Sun, 11 Oct 2026 08:30:39 +0900 Subject: [PATCH] UI: installed CJK faces are read once, without waiting for the font scan - install_fonts no longer waits for the system font catalog on the first frame: the built-in faces are installed at once, the scan runs in the background, and the installed CJK faces are added when it is done. - The installed faces are read once and shared: changing the interface language reorders them without copying the font files into egui again. - fonts: line_metrics no longer sits between feature's doc comment and feature. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013phQ3ekq6uGBEWD6FjFYG2 --- crates/fonts/src/fontdb.rs | 6 +++ crates/fonts/src/lib.rs | 2 +- crates/ui-egui/src/lib.rs | 14 ++++-- crates/ui-egui/src/theme.rs | 91 ++++++++++++++++++++++--------------- 4 files changed, 73 insertions(+), 40 deletions(-) diff --git a/crates/fonts/src/fontdb.rs b/crates/fonts/src/fontdb.rs index 14441e144..c631f3020 100644 --- a/crates/fonts/src/fontdb.rs +++ b/crates/fonts/src/fontdb.rs @@ -881,6 +881,12 @@ impl FontDb { }); } + /// Whether the installed fonts are cataloged: a lookup by family name won't wait for the + /// scan (always true on wasm, which has none). See [`FontDb::scan_in_background`]. + pub fn system_fonts_cataloged(&self) -> bool { + cfg!(target_arch = "wasm32") || self.cataloged.get().is_some() + } + /// Catalog the installed fonts on a background thread, so the first lookup by family name /// (opening a file, the font menus) doesn't wait for the scan. A no-op once they are /// cataloged, and on wasm. diff --git a/crates/fonts/src/lib.rs b/crates/fonts/src/lib.rs index 43d5837ff..630888315 100644 --- a/crates/fonts/src/lib.rs +++ b/crates/fonts/src/lib.rs @@ -80,7 +80,6 @@ pub struct ShapedGlyph { pub rtl: bool, } -/// An OpenType feature setting: `"liga"`, `"-kern"`, `"ss01"`. /// Line metrics of the font at `index` of `data`, in font units: (units per em, ascent, descent /// (negative below the baseline), line gap) — what text layouts such as egui's build lines from. /// `None` if the font can't be read. @@ -91,6 +90,7 @@ pub fn line_metrics(data: &[u8], index: u32) -> Option<(f32, f32, f32, f32)> { Some((f32::from(m.units_per_em.max(1)), m.ascent, m.descent, m.leading)) } +/// An OpenType feature setting: `"liga"`, `"-kern"`, `"ss01"`. pub fn feature(tag: &str) -> Option { let (on, t) = match tag.strip_prefix('-') { Some(r) => (false, r), diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index fb81e9c34..29b1ec0b4 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -474,6 +474,9 @@ pub struct DesignApp { styled: bool, /// The interface language the UI fonts were installed for (it orders the CJK fallbacks). fonts_lang: String, + /// The installed CJK faces are among the UI fonts (false while the system fonts are still + /// being cataloged: the fonts are installed again when they are). + fonts_system: bool, pub restyle: bool, fonts_ready: bool, pub integrated_titlebar: bool, @@ -514,6 +517,7 @@ impl DesignApp { shot_token: 0, styled: false, fonts_lang: String::new(), + fonts_system: false, restyle: false, fonts_ready: false, integrated_titlebar: false, @@ -726,17 +730,21 @@ impl DesignApp { ctx.set_zoom_factor(scale); } if !self.styled { - theme::install_fonts(ctx, &self.ui.language); + self.fonts_system = theme::install_fonts(ctx, &self.ui.language); self.fonts_lang = self.ui.language.clone(); self.styled = true; self.restyle = true; } else { - if self.fonts_lang != self.ui.language { - theme::install_fonts(ctx, &self.ui.language); + if self.fonts_lang != self.ui.language || (!self.fonts_system && theme::system_fonts_ready()) { + self.fonts_system = theme::install_fonts(ctx, &self.ui.language); self.fonts_lang = self.ui.language.clone(); } self.fonts_ready = true; } + if !self.fonts_system { + // The font scan runs in the background: look again shortly. + ctx.request_repaint_after(std::time::Duration::from_millis(250)); + } if self.restyle { theme::apply(ctx, &theme::Tokens::for_brightness(self.ui.brightness)); self.restyle = false; diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index eb294d575..14918008f 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -254,11 +254,23 @@ impl Tokens { } /// Install the UI fonts for the interface language `lang` (it orders the CJK fallbacks). -pub fn install_fonts(ctx: &egui::Context, lang: &str) { +/// Returns false while the installed fonts are still being cataloged: the built-in faces are +/// installed without waiting, and the caller installs again once they are (see +/// [`system_fonts_ready`]) to add the installed CJK faces. +pub fn install_fonts(ctx: &egui::Context, lang: &str) -> bool { let craft = designcraft_fonts::CRAFT_FONTS; let mut defs = font_definitions(craft, lang); - add_system_fallbacks(&mut defs, &system_ui_fonts(craft), lang); + let system = system_ui_fonts(craft); + if let Some(system) = system { + add_system_fallbacks(&mut defs, system, lang); + } ctx.set_fonts(defs); + system.is_some() +} + +/// Whether [`install_fonts`] would find the installed fonts cataloged. +pub fn system_fonts_ready() -> bool { + designcraft_fonts::FontDb::global().system_fonts_cataloged() } /// An installed font the UI falls back to for a script the built-in faces lack. @@ -266,8 +278,16 @@ pub(crate) struct SystemFont { /// "Jpan", "Hans", "Hant" or "Kore". script: &'static str, family: String, - bytes: Vec, - index: u32, + /// The face as egui takes it, on the UI face's baseline. Shared: installing the fonts again + /// (another interface language) doesn't copy the file. + data: Arc, +} + +impl SystemFont { + pub(crate) fn new(script: &'static str, family: String, bytes: Vec, index: u32) -> Self { + let tweak = baseline_tweak(&bytes, index); + SystemFont { script, family, data: Arc::new(FontData { index, tweak, ..FontData::from_owned(bytes) }) } + } } /// Installed UI faces (sans serif) per script, the first one found of each list: macOS, then @@ -299,23 +319,29 @@ const SYSTEM_UI_FONTS: &[(&str, &[&str])] = &[ /// the interface shows Japanese and Chinese without the craft-fonts build input, and Korean and /// Traditional Chinese (font names in the font menus) in any build. egui can't /// find system fonts itself; the document font scan (not on the web) does. -fn system_ui_fonts(craft: &[designcraft_fonts::CraftFont]) -> Vec { +fn system_ui_fonts(craft: &[designcraft_fonts::CraftFont]) -> Option<&'static [SystemFont]> { + static FACES: std::sync::OnceLock> = std::sync::OnceLock::new(); let db = designcraft_fonts::FontDb::global(); - SYSTEM_UI_FONTS - .iter() - .filter(|(script, _)| !craft.iter().any(|f| f.scripts.contains(script))) - .filter_map(|(script, families)| { - let family = families.iter().find(|f| db.has_family(f))?; - let face = db.face(family, "Regular"); - // `face` falls back to another family when this one can't be read: keep only the real one. - (face.family.eq_ignore_ascii_case(family) && !face.data().is_empty()).then(|| SystemFont { - script, - family: (*family).to_string(), - bytes: face.data().to_vec(), - index: face.index(), + // Never wait for the font scan here (the first frame): it runs in the background, and the + // faces are read once, when it is done. + // (Tests wait instead, so that the fonts don't change in the middle of one.) + if !db.system_fonts_cataloged() && !cfg!(test) { + db.scan_in_background(); + return None; + } + Some(FACES.get_or_init(|| { + SYSTEM_UI_FONTS + .iter() + .filter(|(script, _)| !craft.iter().any(|f| f.scripts.contains(script))) + .filter_map(|(script, families)| { + let family = families.iter().find(|f| db.has_family(f))?; + let face = db.face(family, "Regular"); + // `face` falls back to another family when this one can't be read: keep only the real one. + (face.family.eq_ignore_ascii_case(family) && !face.data().is_empty()) + .then(|| SystemFont::new(script, (*family).to_string(), face.data().to_vec(), face.index())) }) - }) - .collect() + .collect() + })) } /// The UI face every family starts with (Source Sans 3; the semibold shares its line metrics). @@ -354,10 +380,7 @@ fn cjk_script(lang: &str) -> &'static str { pub(crate) fn add_system_fallbacks(fonts: &mut FontDefinitions, system: &[SystemFont], lang: &str) { let name = |f: &SystemFont| format!("system-{}", f.family); for f in system { - fonts.font_data.insert( - name(f), - Arc::new(FontData { index: f.index, tweak: baseline_tweak(&f.bytes, f.index), ..FontData::from_owned(f.bytes.clone()) }), - ); + fonts.font_data.insert(name(f), f.data.clone()); } let first = cjk_script(lang); let preferred: Vec = system.iter().filter(|f| f.script == first).map(name).collect(); @@ -533,7 +556,7 @@ mod japanese_font_tests { #[test] fn installed_fonts_give_japanese_glyphs_without_craft_fonts() { let bytes = designcraft_fonts::testing::font_with("DC UI Japanese", &['日', '本', '語']).unwrap(); - let system = [super::SystemFont { script: "Jpan", family: "DC UI Japanese".into(), bytes, index: 0 }]; + let system = [super::SystemFont::new("Jpan", "DC UI Japanese".into(), bytes, 0)]; let mut defs = super::font_definitions(&[], "ja"); super::add_system_fallbacks(&mut defs, &system, "ja"); for family in families() { @@ -575,11 +598,8 @@ mod japanese_font_tests { /// from an installed Korean face, after the Japanese and Chinese ones. #[test] fn installed_korean_faces_give_hangul_glyphs() { - let face = |family: &str, chars: &[char], script| super::SystemFont { - script, - family: family.into(), - bytes: designcraft_fonts::testing::font_with(family, chars).unwrap(), - index: 0, + let face = |family: &str, chars: &[char], script| { + super::SystemFont::new(script, family.into(), designcraft_fonts::testing::font_with(family, chars).unwrap(), 0) }; let system = [face("DC UI Korean", &['한', '글'], "Kore"), face("DC UI Japanese", &['日'], "Jpan")]; let mut defs = super::font_definitions(&[], "ja"); @@ -601,12 +621,8 @@ mod japanese_font_tests { /// before the built-in CJK faces and the other scripts' faces. #[test] fn interface_language_orders_the_cjk_faces() { - let face = |family: &str, script| super::SystemFont { - script, - family: family.into(), - bytes: designcraft_fonts::testing::font_with(family, &['日']).unwrap(), - index: 0, - }; + let face = + |family: &str, script| super::SystemFont::new(script, family.into(), designcraft_fonts::testing::font_with(family, &['日']).unwrap(), 0); let system = [face("DC Ja", "Jpan"), face("DC Hans", "Hans"), face("DC Hant", "Hant"), face("DC Ko", "Kore")]; for (lang, first) in [("ja", "DC Ja"), ("zh", "DC Hans"), ("zh-hant", "DC Hant"), ("ko", "DC Ko"), ("", "DC Ja")] { let mut defs = super::font_definitions(&[], lang); @@ -618,7 +634,10 @@ mod japanese_font_tests { let first_at = at(&format!("system-{first}")); assert!(first_at < at("craft-Stand-in"), "{lang}: {stack:?}"); for f in &system { - assert!(first_at <= at(&format!("system-{}", f.family)), "{lang}: {stack:?}"); + let name = format!("system-{}", f.family); + assert!(first_at <= at(&name), "{lang}: {stack:?}"); + // Another language shares the face's data instead of copying the font file. + assert!(std::sync::Arc::ptr_eq(&defs.font_data[&name], &f.data), "{lang}: {name}"); } } }