From fe2aa3f4cb07d836249aa57891e6c391191d4d36 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 7f1bb81d4d7486ea85e23dedcb763e238d53b2f3..9dcf27c7cbebf6621bbeb558619944c768e63fb6 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 896a8bc038bdd8e495cdb6161212f9c722d54f14..e056dfbd5af486e19e2b3e8cfc038dd85c0a2549 100644 --- a/crates/settings/src/settings_ui_core.rs +++ b/crates/settings/src/settings_ui_core.rs @@ -78,6 +78,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; + } }