From 92912915fa564e218af58e91c2f1963e73821262 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Mon, 8 Sep 2025 10:09:54 -0500 Subject: [PATCH] onboarding: Fix font loading frame delay (#37668) Closes #ISSUE Fixed an issue where the first frame of the `Editing` page in onboarding would have a slight delay before rendering the first time it was navigated to. This was caused by listing the OS fonts on the main thread, blocking rendering. This PR fixes the issue by adding a new method to the font family cache to prefill the cache on a background thread. Release Notes: - N/A *or* Added/Fixed/Improved ... --------- Co-authored-by: Mikayla Maki Co-authored-by: Anthony Eid Co-authored-by: Anthony --- crates/onboarding/src/editing_page.rs | 32 +++++++++---------- crates/onboarding/src/onboarding.rs | 25 +++++++++++---- crates/settings/src/settings_ui_core.rs | 1 + crates/theme/src/font_family_cache.rs | 42 ++++++++++++++++++++++++- 4 files changed, 77 insertions(+), 23 deletions(-) diff --git a/crates/onboarding/src/editing_page.rs b/crates/onboarding/src/editing_page.rs index 47dfd84894bf0ca5e7fd4a5a9ad0785d80b07ac5..297016abd4a1499feb6f637d028056ca0b412d31 100644 --- a/crates/onboarding/src/editing_page.rs +++ b/crates/onboarding/src/editing_page.rs @@ -449,28 +449,28 @@ impl FontPickerDelegate { ) -> Self { let font_family_cache = FontFamilyCache::global(cx); - let fonts: Vec = font_family_cache - .list_font_families(cx) - .into_iter() - .collect(); - + let fonts = font_family_cache + .try_list_font_families() + .unwrap_or_else(|| vec![current_font.clone()]); let selected_index = fonts .iter() .position(|font| *font == current_font) .unwrap_or(0); + let filtered_fonts = fonts + .iter() + .enumerate() + .map(|(index, font)| StringMatch { + candidate_id: index, + string: font.to_string(), + positions: Vec::new(), + score: 0.0, + }) + .collect(); + Self { - fonts: fonts.clone(), - filtered_fonts: fonts - .iter() - .enumerate() - .map(|(index, font)| StringMatch { - candidate_id: index, - string: font.to_string(), - positions: Vec::new(), - score: 0.0, - }) - .collect(), + fonts, + filtered_fonts, selected_index, current_font, on_font_changed: Arc::new(on_font_changed), diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs index 873dd63201423bba8995136e2fde82551966b3dd..5cb8718db53d5a2c41d878119910c9032bdd9921 100644 --- a/crates/onboarding/src/onboarding.rs +++ b/crates/onboarding/src/onboarding.rs @@ -242,12 +242,25 @@ struct Onboarding { impl Onboarding { fn new(workspace: &Workspace, cx: &mut App) -> Entity { - cx.new(|cx| Self { - workspace: workspace.weak_handle(), - focus_handle: cx.focus_handle(), - selected_page: SelectedPage::Basics, - user_store: workspace.user_store().clone(), - _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), + let font_family_cache = theme::FontFamilyCache::global(cx); + + cx.new(|cx| { + cx.spawn(async move |this, cx| { + font_family_cache.prefetch(cx).await; + this.update(cx, |_, cx| { + cx.notify(); + }) + }) + .detach(); + + Self { + workspace: workspace.weak_handle(), + focus_handle: cx.focus_handle(), + selected_page: SelectedPage::Basics, + user_store: workspace.user_store().clone(), + _settings_subscription: cx + .observe_global::(move |_, cx| cx.notify()), + } }) } diff --git a/crates/settings/src/settings_ui_core.rs b/crates/settings/src/settings_ui_core.rs index 8ab744f5a8244057e0b87a66cc3e4c7dcf02527f..a4aefefb0acddb7606060a69fba80aa51d4462da 100644 --- a/crates/settings/src/settings_ui_core.rs +++ b/crates/settings/src/settings_ui_core.rs @@ -73,6 +73,7 @@ impl SettingsValue { let fs = ::global(cx); let rx = settings_store.update_settings_file_at_path(fs.clone(), path.as_slice(), value); + let path = path.clone(); cx.background_spawn(async move { rx.await? diff --git a/crates/theme/src/font_family_cache.rs b/crates/theme/src/font_family_cache.rs index fecaf5b360a91beca3fceb564c87973d6f676384..411cf9b4d41359f4da4520061ac46c984bdd08f2 100644 --- a/crates/theme/src/font_family_cache.rs +++ b/crates/theme/src/font_family_cache.rs @@ -16,7 +16,7 @@ struct FontFamilyCacheState { /// so we do it once and then use the cached values each render. #[derive(Default)] pub struct FontFamilyCache { - state: RwLock, + state: Arc>, } #[derive(Default)] @@ -52,4 +52,44 @@ impl FontFamilyCache { lock.font_families.clone() } + + /// Returns the list of font families if they have been loaded + pub fn try_list_font_families(&self) -> Option> { + self.state + .try_read() + .filter(|state| state.loaded_at.is_some()) + .map(|state| state.font_families.clone()) + } + + /// Prefetch all font names in the background + pub async fn prefetch(&self, cx: &gpui::AsyncApp) { + if self + .state + .try_read() + .is_none_or(|state| state.loaded_at.is_some()) + { + return; + } + + let Ok(text_system) = cx.update(|cx| App::text_system(cx).clone()) else { + return; + }; + + let state = self.state.clone(); + + cx.background_executor() + .spawn(async move { + // We take this lock in the background executor to ensure that synchronous calls to `list_font_families` are blocked while we are prefetching, + // while not blocking the main thread and risking deadlocks + let mut lock = state.write(); + let all_font_names = text_system + .all_font_names() + .into_iter() + .map(SharedString::from) + .collect(); + lock.font_families = all_font_names; + lock.loaded_at = Some(Instant::now()); + }) + .await; + } }