diff --git a/crates/fonts/src/fontdb.rs b/crates/fonts/src/fontdb.rs index 12c2c573b..f4f1dff1d 100644 --- a/crates/fonts/src/fontdb.rs +++ b/crates/fonts/src/fontdb.rs @@ -1010,6 +1010,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/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 701903176..e4ac5fab3 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -492,6 +492,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, /// Files the operating system asked to open (macOS Finder, Dock, Open With), opened once the @@ -551,6 +554,7 @@ impl DesignApp { shot_token: 0, styled: false, fonts_lang: String::new(), + fonts_system: false, restyle: false, fonts_ready: false, os_documents: Vec::new(), @@ -888,17 +892,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 418a03f72..d845f2a59 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -1,7 +1,6 @@ //! Design tokens (InDesign's four interface brightness levels), fonts and egui style. -use std::borrow::Cow; -use std::sync::{Arc, OnceLock}; +use std::sync::Arc; use egui::{Color32, CornerRadius, FontData, FontDefinitions, FontFamily, FontId, Stroke, TextStyle, Visuals}; use serde::{Deserialize, Serialize}; @@ -255,21 +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) { - // Installed faces are read once per process, so a language change only reorders them. Their - // bytes are leaked once and borrowed by every `FontData` (`set_fonts` takes the definitions - // by value on each call). - static SYSTEM: OnceLock> = OnceLock::new(); +/// 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 system = SYSTEM.get_or_init(|| { - system_ui_fonts(craft) - .into_iter() - .map(|f| SystemFont { bytes: Cow::Borrowed(Box::leak(f.bytes.into_owned().into_boxed_slice())), ..f }) - .collect() - }); let mut defs = font_definitions(craft, lang); - add_system_fallbacks(&mut defs, system, 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. @@ -277,8 +278,16 @@ pub(crate) struct SystemFont { /// "Jpan", "Hans", "Hant" or "Kore". script: &'static str, family: String, - bytes: Cow<'static, [u8]>, - 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 @@ -310,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: Cow::Owned(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). @@ -365,7 +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 { font: f.bytes.clone(), index: f.index, tweak: baseline_tweak(&f.bytes, f.index) })); + 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(); @@ -541,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: bytes.into(), 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() { @@ -583,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().into(), - 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"); @@ -609,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().into(), - 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); @@ -626,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}"); } } }