From 640c87b537e01d53cbd39404e8914dfa3e5a4b53 Mon Sep 17 00:00:00 2001 From: nrq6993-a11y Date: Sun, 18 Oct 2026 12:04:29 +0300 Subject: [PATCH 1/3] Fix #2576: poll for fonts font managers activate while the app is in front Font services (NexusFont, Adobe Fonts) register faces with the OS without touching font-folder mtimes; only re-asking the platform lister sees them. Check every two seconds as well as on window focus, and test that path churn is detected on the system font scan. Co-authored-by: Cursor --- crates/text/src/fontdb.rs | 6 ++++++ crates/text/src/tests_sysfonts.rs | 19 +++++++++++++++++++ crates/ui-egui/src/lib.rs | 15 +++++++++++++-- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/crates/text/src/fontdb.rs b/crates/text/src/fontdb.rs index 23691e9ba..3acf0fd04 100644 --- a/crates/text/src/fontdb.rs +++ b/crates/text/src/fontdb.rs @@ -1611,6 +1611,12 @@ impl FontDb { Self::new(FontPaths::Fixed(font_dirs)) } + /// Same scan paths as [`FontDb::global`] (platform lister and font folders), for tests. + #[cfg(test)] + pub(crate) fn for_system_scan_test() -> Self { + Self::new(FontPaths::System) + } + fn new(font_paths: FontPaths) -> Self { let mut faces = Vec::new(); // Bundled fonts first, then explicitly supplied craft-fonts faces: Japanese (Mincho diff --git a/crates/text/src/tests_sysfonts.rs b/crates/text/src/tests_sysfonts.rs index 49721a2e2..e2afc4172 100644 --- a/crates/text/src/tests_sysfonts.rs +++ b/crates/text/src/tests_sysfonts.rs @@ -331,6 +331,25 @@ fn service_font() -> PathBuf { std::env::temp_dir().join(format!("vc-sysfonts-{}-service", std::process::id())).join(".29457") } +/// Fonts a font service activates after the first scan are picked up on the next check (#2576). +#[test] +fn the_database_tells_when_a_font_service_adds_a_font() { + use std::sync::Mutex; + static FILES: Mutex> = Mutex::new(Vec::new()); + set_platform_font_files(|| FILES.lock().unwrap_or_else(|e| e.into_inner()).clone()); + let file = service_font(); + std::fs::create_dir_all(file.parent().unwrap()).unwrap(); + let db = FontDb::for_system_scan_test(); + assert!(!db.installed_fonts_changed()); + db.load_system_fonts(); + assert!(!db.has_family("Service Sans3")); + std::fs::write(&file, renamed_to("SourceSans3-Regular.ttf", "Service Sans3")).unwrap(); + *FILES.lock().unwrap_or_else(|e| e.into_inner()) = vec![file.to_string_lossy().into_owned()]; + assert!(db.installed_fonts_changed(), "a font a font service activated since the scan"); + db.load_system_fonts(); + assert!(db.has_family("Service Sans3")); +} + /// The fonts a font service (Adobe Fonts on Windows, through DirectWrite) loads in place are /// cataloged: named by the platform's lister, read whatever their file is called (#579). #[test] diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 834174217..1455f1f80 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -455,6 +455,8 @@ pub struct VectorcraftApp { /// The look for fonts installed or removed while the app was in the background, running /// ([`Self::refresh_installed_fonts`]): whether they were. font_check: Option>, + /// When installed fonts were last checked in line (app time, s). + font_install_check_at: f64, /// Keyboard pastes of something other than text (see [`shortcuts::PasteChord`]). pub(crate) paste_chord: shortcuts::PasteChord, /// Saves and exports running in the background (Preferences → File Handling). @@ -500,6 +502,8 @@ struct PendingScreenshot { /// Seconds between two looks at the system clipboard for [`VectorcraftApp::system_paste`]. const SYSTEM_CLIPBOARD_POLL: f64 = 0.25; +/// Seconds between looks for fonts a font manager activated while the app stays in front (#2576). +const FONT_INSTALL_POLL: f64 = 2.0; impl VectorcraftApp { pub fn new(mut session: Session, services: Services) -> Self { @@ -562,6 +566,7 @@ impl VectorcraftApp { picks: picks::Picks::default(), screen_sampler: None, font_check: None, + font_install_check_at: f64::NEG_INFINITY, paste_chord: Default::default(), background: Default::default(), opening: Default::default(), @@ -1198,6 +1203,11 @@ impl VectorcraftApp { self.system_paste_at = now; self.system_paste = wanted && self.system_clipboard_pasteable(); } + #[cfg(not(target_arch = "wasm32"))] + if !(0.0..FONT_INSTALL_POLL).contains(&(now - self.font_install_check_at)) { + self.font_install_check_at = now; + self.refresh_installed_fonts(); + } self.poll_font_check(ctx); picks::poll(self, ctx); self.write_formats_before_text_input(ctx); @@ -1292,8 +1302,9 @@ impl VectorcraftApp { place::drop_files(self, files); } - /// Fonts installed or removed while the app was in the background are listed when it comes - /// back (Refresh Font List by itself): a look at the font folders, a scan only when they changed. + /// Fonts installed or removed while the app runs are listed without Refresh Font List: a look + /// at the font folders and the platform's font list on a timer and when the window comes back, + /// a scan only when they changed. /// The look runs on another thread (asking DirectWrite for the fonts font services loaded /// meanwhile takes tens of milliseconds on Windows, #579), [`Self::poll_font_check`] scans. fn refresh_installed_fonts(&mut self) { From 3041b6a672449de5c1f0518478ddfb23ec09fbf7 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 18 Oct 2026 18:56:30 +0700 Subject: [PATCH 2/3] Tests: the font-service checks of #579 and #2576 are one test, since the platform lister is set once per process (#2884) --- crates/text/src/tests_sysfonts.rs | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/crates/text/src/tests_sysfonts.rs b/crates/text/src/tests_sysfonts.rs index e2afc4172..c4f6a53fd 100644 --- a/crates/text/src/tests_sysfonts.rs +++ b/crates/text/src/tests_sysfonts.rs @@ -331,38 +331,31 @@ fn service_font() -> PathBuf { std::env::temp_dir().join(format!("vc-sysfonts-{}-service", std::process::id())).join(".29457") } -/// Fonts a font service activates after the first scan are picked up on the next check (#2576). +/// The fonts a font service (Adobe Fonts on Windows, through DirectWrite) loads in place are +/// cataloged: named by the platform's lister, read whatever their file is called (#579), and one it +/// activates after a scan is picked up by the next check (#2576). One test: the lister is set once +/// per process, so two tests changing what it lists would race. #[test] -fn the_database_tells_when_a_font_service_adds_a_font() { +fn fonts_the_platform_lists_outside_the_font_folders_are_cataloged() { use std::sync::Mutex; static FILES: Mutex> = Mutex::new(Vec::new()); + let list = |files: Vec| *FILES.lock().unwrap_or_else(|e| e.into_inner()) = files; set_platform_font_files(|| FILES.lock().unwrap_or_else(|e| e.into_inner()).clone()); let file = service_font(); std::fs::create_dir_all(file.parent().unwrap()).unwrap(); + let _ = std::fs::remove_file(&file); + // Nothing listed at the first scan; then a font service activates a font. let db = FontDb::for_system_scan_test(); assert!(!db.installed_fonts_changed()); db.load_system_fonts(); assert!(!db.has_family("Service Sans3")); std::fs::write(&file, renamed_to("SourceSans3-Regular.ttf", "Service Sans3")).unwrap(); - *FILES.lock().unwrap_or_else(|e| e.into_inner()) = vec![file.to_string_lossy().into_owned()]; + // A collection lists its file once per face. + list(vec![file.to_string_lossy().into_owned(); 2]); assert!(db.installed_fonts_changed(), "a font a font service activated since the scan"); db.load_system_fonts(); assert!(db.has_family("Service Sans3")); -} - -/// The fonts a font service (Adobe Fonts on Windows, through DirectWrite) loads in place are -/// cataloged: named by the platform's lister, read whatever their file is called (#579). -#[test] -fn fonts_the_platform_lists_outside_the_font_folders_are_cataloged() { - let file = service_font(); - std::fs::create_dir_all(file.parent().unwrap()).unwrap(); - std::fs::write(&file, renamed_to("SourceSans3-Regular.ttf", "Service Sans3")).unwrap(); - // The lister's files outside the font folders are scanned with them, each once (a collection - // lists its file once per face). - fn lister() -> Vec { - vec![service_font().to_string_lossy().into_owned(); 2] - } - set_platform_font_files(lister); + // The lister's files outside the font folders are scanned with them, each once. let dirs = system_font_dirs(); assert_eq!(dirs.iter().filter(|d| **d == file).count(), 1, "{dirs:?}"); // A file named by itself is read without a font's extension, one in a folder isn't. From 5906f53ca5a99a2f8545b922f42f518860a724f3 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sun, 18 Oct 2026 19:02:42 +0700 Subject: [PATCH 3/3] Fonts: the timed look for activated fonts doesn't wake an idle app; the next frame reads it (#2884) --- crates/ui-egui/src/lib.rs | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 1455f1f80..a6e268ab0 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -452,9 +452,10 @@ pub struct VectorcraftApp { pub(crate) picks: picks::Picks, /// The screen color sampler shown and not answered yet, and where its color goes. pub(crate) screen_sampler: Option, - /// The look for fonts installed or removed while the app was in the background, running - /// ([`Self::refresh_installed_fonts`]): whether they were. - font_check: Option>, + /// The look for fonts installed or removed meanwhile, running + /// ([`Self::refresh_installed_fonts`]): whether they were, and whether to wake the app for + /// the answer. + font_check: Option<(std::sync::mpsc::Receiver, bool)>, /// When installed fonts were last checked in line (app time, s). font_install_check_at: f64, /// Keyboard pastes of something other than text (see [`shortcuts::PasteChord`]). @@ -1206,7 +1207,8 @@ impl VectorcraftApp { #[cfg(not(target_arch = "wasm32"))] if !(0.0..FONT_INSTALL_POLL).contains(&(now - self.font_install_check_at)) { self.font_install_check_at = now; - self.refresh_installed_fonts(); + // Read on the next frame the app draws anyway: an idle app isn't woken for it. + self.refresh_installed_fonts(false); } self.poll_font_check(ctx); picks::poll(self, ctx); @@ -1306,9 +1308,11 @@ impl VectorcraftApp { /// at the font folders and the platform's font list on a timer and when the window comes back, /// a scan only when they changed. /// The look runs on another thread (asking DirectWrite for the fonts font services loaded - /// meanwhile takes tens of milliseconds on Windows, #579), [`Self::poll_font_check`] scans. - fn refresh_installed_fonts(&mut self) { - if self.font_check.is_some() { + /// meanwhile takes tens of milliseconds on Windows, #579), [`Self::poll_font_check`] scans; + /// `wake` wakes the app for the answer. + fn refresh_installed_fonts(&mut self, wake: bool) { + if let Some((_, w)) = &mut self.font_check { + *w |= wake; return; } let (tx, rx) = std::sync::mpsc::channel(); @@ -1321,12 +1325,12 @@ impl VectorcraftApp { if spawned.is_none() { look(); } - self.font_check = Some(rx); + self.font_check = Some((rx, wake)); } /// Rescan the fonts once [`Self::refresh_installed_fonts`]'s look says they changed. fn poll_font_check(&mut self, ctx: &egui::Context) { - let Some(check) = &self.font_check else { return }; + let Some((check, wake)) = &self.font_check else { return }; match check.try_recv() { Ok(changed) => { self.font_check = None; @@ -1335,7 +1339,11 @@ impl VectorcraftApp { let _ = self.run("text.rescanFonts", json!({})); } } - Err(std::sync::mpsc::TryRecvError::Empty) => ctx.request_repaint_after(std::time::Duration::from_millis(50)), + Err(std::sync::mpsc::TryRecvError::Empty) => { + if *wake { + ctx.request_repaint_after(std::time::Duration::from_millis(50)); + } + } Err(std::sync::mpsc::TryRecvError::Disconnected) => self.font_check = None, } } @@ -1357,7 +1365,7 @@ impl VectorcraftApp { match e { egui::Event::ModifiersChanged(m) => self.host_modifiers = *m, egui::Event::WindowFocused(false) => self.host_modifiers = egui::Modifiers::NONE, - egui::Event::WindowFocused(true) => self.refresh_installed_fonts(), + egui::Event::WindowFocused(true) => self.refresh_installed_fonts(true), _ => {} } }