From 4bc0332e3b3b9130e28a288e4916bba2cfbcd9a5 Mon Sep 17 00:00:00 2001 From: MrSubidubi Date: Mon, 11 May 2026 21:58:55 +0200 Subject: [PATCH] Fix activity indicator --- .../src/activity_indicator.rs | 246 ++++++++---------- crates/ui/src/components/button/button.rs | 36 +-- 2 files changed, 122 insertions(+), 160 deletions(-) diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index dccf7cf6f3153d..95eab4d549ffe6 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -22,7 +22,7 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use ui::{CommonAnimationExt, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*}; +use ui::{ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*}; use util::truncate_and_trailoff; use workspace::{StatusItemView, Workspace, item::ItemHandle}; @@ -62,8 +62,13 @@ struct PendingWork<'a> { progress: &'a LanguageServerProgress, } +enum ActivityIcon { + LoadingSpinner, + Icon(IconName), +} + struct Content { - icon: Option, + icon: ActivityIcon, message: String, on_click: Option)>>, @@ -310,24 +315,19 @@ impl ActivityIndicator { .read(cx) .language_server_statuses(cx) .rev() - .filter_map(|(server_id, status)| { - if status.pending_work.is_empty() { - None - } else { - let mut pending_work = status - .pending_work - .iter() - .map(|(progress_token, progress)| PendingWork { - language_server_id: server_id, - progress_token, - progress, - }) - .collect::>(); - pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at)); - Some(pending_work) - } + .flat_map(|(server_id, status)| { + let mut pending_work = status + .pending_work + .iter() + .map(|(progress_token, progress)| PendingWork { + language_server_id: server_id, + progress_token, + progress, + }) + .collect::>(); + pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at)); + pending_work }) - .flatten() } fn pending_environment_error<'a>(&'a self, cx: &'a App) -> Option<&'a String> { @@ -338,11 +338,7 @@ impl ActivityIndicator { // Show if any direnv calls failed if let Some(message) = self.pending_environment_error(cx) { return Some(Content { - icon: Some( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .into_any_element(), - ), + icon: ActivityIcon::Icon(IconName::Warning), message: message.clone(), on_click: Some(Arc::new(move |this, window, cx| { this.project.update(cx, |project, cx| { @@ -379,12 +375,7 @@ impl ActivityIndicator { } return Some(Content { - icon: Some( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Small) - .with_rotate_animation(2) - .into_any_element(), - ), + icon: ActivityIcon::LoadingSpinner, message, on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)), tooltip_message: None, @@ -401,12 +392,7 @@ impl ActivityIndicator { .find(|s| !s.read(cx).is_started()) { return Some(Content { - icon: Some( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Small) - .with_rotate_animation(2) - .into_any_element(), - ), + icon: ActivityIcon::LoadingSpinner, message: format!("Debug: {}", session.read(cx).adapter()), tooltip_message: session.read(cx).label().map(|label| label.to_string()), on_click: None, @@ -424,12 +410,7 @@ impl ActivityIndicator { && Instant::now() - job_info.start >= GIT_OPERATION_DELAY { return Some(Content { - icon: Some( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Small) - .with_rotate_animation(2) - .into_any_element(), - ), + icon: ActivityIcon::LoadingSpinner, message: job_info.message.into(), on_click: None, tooltip_message: None, @@ -440,12 +421,7 @@ impl ActivityIndicator { for fs_job in &self.fs_jobs { if Instant::now().duration_since(fs_job.start) >= GIT_OPERATION_DELAY { return Some(Content { - icon: Some( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Small) - .with_rotate_animation(2) - .into_any_element(), - ), + icon: ActivityIcon::LoadingSpinner, message: fs_job.message.clone().into(), on_click: None, tooltip_message: None, @@ -498,11 +474,7 @@ impl ActivityIndicator { if !downloading.is_empty() { return Some(Content { - icon: Some( - Icon::new(IconName::Download) - .size(IconSize::Small) - .into_any_element(), - ), + icon: ActivityIcon::Icon(IconName::Download), message: format!( "Downloading {}...", downloading.iter().map(|name| name.as_ref()).fold( @@ -527,11 +499,7 @@ impl ActivityIndicator { if !checking_for_update.is_empty() { return Some(Content { - icon: Some( - Icon::new(IconName::Download) - .size(IconSize::Small) - .into_any_element(), - ), + icon: ActivityIcon::Icon(IconName::Download), message: format!( "Checking for updates to {}...", checking_for_update.iter().map(|name| name.as_ref()).fold( @@ -556,11 +524,7 @@ impl ActivityIndicator { if !failed.is_empty() { return Some(Content { - icon: Some( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .into_any_element(), - ), + icon: ActivityIcon::Icon(IconName::Warning), message: format!( "Failed to run {}. Click to show error.", failed @@ -584,11 +548,7 @@ impl ActivityIndicator { // Show any formatting failure if let Some(failure) = self.project.read(cx).last_formatting_failure(cx) { return Some(Content { - icon: Some( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .into_any_element(), - ), + icon: ActivityIcon::Icon(IconName::Warning), message: format!("Formatting failed: {failure}. Click to see logs."), on_click: Some(Arc::new(|indicator, window, cx| { indicator.project.update(cx, |project, cx| { @@ -630,11 +590,7 @@ impl ActivityIndicator { }; return Some(Content { - icon: Some( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .into_any_element(), - ), + icon: ActivityIcon::Icon(IconName::Warning), message: final_message, tooltip_message, on_click: Some(Arc::new(move |activity_indicator, window, cx| { @@ -656,32 +612,23 @@ impl ActivityIndicator { && let Some((extension_id, operation)) = extension_store.outstanding_operations().iter().next() { - let (message, icon, rotate) = match operation { + let (message, icon) = match operation { ExtensionOperation::Install => ( format!("Installing {extension_id} extension…"), - IconName::LoadCircle, - true, + ActivityIcon::LoadingSpinner, ), ExtensionOperation::Upgrade => ( format!("Updating {extension_id} extension…"), - IconName::Download, - false, + ActivityIcon::Icon(IconName::Download), ), ExtensionOperation::Remove => ( format!("Removing {extension_id} extension…"), - IconName::LoadCircle, - true, + ActivityIcon::LoadingSpinner, ), }; return Some(Content { - icon: Some(Icon::new(icon).size(IconSize::Small).map(|this| { - if rotate { - this.with_rotate_animation(3).into_any_element() - } else { - this.into_any_element() - } - })), + icon, message, on_click: Some(Arc::new(|this, window, cx| { this.dismiss_message(&Default::default(), window, cx) @@ -718,6 +665,8 @@ impl Render for ActivityIndicator { let activity_indicator = cx.entity().downgrade(); let truncate_content = content.message.len() > MAX_MESSAGE_LEN; + let has_click_handler = content.on_click.is_some(); + result.gap_2().child( PopoverMenu::new("activity-indicator-popover") .trigger( @@ -729,7 +678,14 @@ impl Render for ActivityIndicator { } }) .label_size(LabelSize::Small) - .loading(content.icon.is_some()) + .map(|this| match content.icon { + ActivityIcon::LoadingSpinner => this.loading(true), + ActivityIcon::Icon(icon_name) => this.start_icon( + Icon::new(icon_name) + .size(IconSize::Small) + .color(Color::Muted), + ), + }) .map(|button| { if truncate_content { button.tooltip(Tooltip::text(content.message)) @@ -746,64 +702,66 @@ impl Render for ActivityIndicator { }), ) .anchor(gpui::Anchor::BottomLeft) - .menu(move |window, cx| { - let strong_this = activity_indicator.upgrade()?; - let mut has_work = false; - let menu = ContextMenu::build(window, cx, |mut menu, _, cx| { - for work in strong_this.read(cx).pending_language_server_work(cx) { - has_work = true; - let activity_indicator = activity_indicator.clone(); - let mut title = work - .progress - .title - .clone() - .unwrap_or(work.progress_token.to_string()); - - if work.progress.is_cancellable { - let language_server_id = work.language_server_id; - let token = work.progress_token.clone(); - let title = SharedString::from(title); - menu = menu.custom_entry( - move |_, _| { - h_flex() - .w_full() - .justify_between() - .child(Label::new(title.clone())) - .child(Icon::new(IconName::XCircle)) - .into_any_element() - }, - move |_, cx| { - let token = token.clone(); - activity_indicator - .update(cx, |activity_indicator, cx| { - activity_indicator.project.update( - cx, - |project, cx| { - project.cancel_language_server_work( - language_server_id, - Some(token), - cx, - ); - }, - ); - activity_indicator.context_menu_handle.hide(cx); - cx.notify(); - }) - .ok(); - }, - ); - } else { - if let Some(progress_message) = work.progress.message.as_ref() { - title.push_str(": "); - title.push_str(progress_message); - } + .when(!has_click_handler, |this| { + this.menu(move |window, cx| { + let strong_this = activity_indicator.upgrade()?; + let mut has_work = false; + let menu = ContextMenu::build(window, cx, |mut menu, _, cx| { + for work in strong_this.read(cx).pending_language_server_work(cx) { + has_work = true; + let activity_indicator = activity_indicator.clone(); + let mut title = work + .progress + .title + .clone() + .unwrap_or(work.progress_token.to_string()); + + if work.progress.is_cancellable { + let language_server_id = work.language_server_id; + let token = work.progress_token.clone(); + let title = SharedString::from(title); + menu = menu.custom_entry( + move |_, _| { + h_flex() + .w_full() + .justify_between() + .child(Label::new(title.clone())) + .child(Icon::new(IconName::XCircle)) + .into_any_element() + }, + move |_, cx| { + let token = token.clone(); + activity_indicator + .update(cx, |activity_indicator, cx| { + activity_indicator.project.update( + cx, + |project, cx| { + project.cancel_language_server_work( + language_server_id, + Some(token), + cx, + ); + }, + ); + activity_indicator.context_menu_handle.hide(cx); + cx.notify(); + }) + .ok(); + }, + ); + } else { + if let Some(progress_message) = work.progress.message.as_ref() { + title.push_str(": "); + title.push_str(progress_message); + } - menu = menu.label(title); + menu = menu.label(title); + } } - } - menu - }); - has_work.then_some(menu) + menu + }); + has_work.then_some(menu) + }) }), ) } diff --git a/crates/ui/src/components/button/button.rs b/crates/ui/src/components/button/button.rs index a3636285999eab..61cb4a88556061 100644 --- a/crates/ui/src/components/button/button.rs +++ b/crates/ui/src/components/button/button.rs @@ -190,6 +190,7 @@ impl Button { /// Displays a rotating loading spinner in place of the `start_icon`. /// /// When `loading` is `true`, any `start_icon` is ignored. and a rotating + /// loading spinner is shown instead. pub fn loading(mut self, loading: bool) -> Self { self.loading = loading; self @@ -389,23 +390,26 @@ impl RenderOnce for Button { h_flex() .when(self.truncate, |this| this.min_w_0().overflow_hidden()) .gap(DynamicSpacing::Base04.rems(cx)) - .when(self.loading, |this| { - this.child( - Icon::new(IconName::LoadCircle) - .size(IconSize::Small) - .color(Color::Muted) - .with_rotate_animation(2), - ) - }) - .when(!self.loading, |this| { - this.when_some(self.start_icon, |this, icon| { - this.child(if is_disabled { - icon.color(Color::Disabled) - } else { - icon + .when_else( + self.loading, + |this| { + this.child( + Icon::new(IconName::LoadCircle) + .size(IconSize::Small) + .color(Color::Muted) + .with_rotate_animation(2), + ) + }, + |this| { + this.when_some(self.start_icon, |this, icon| { + this.child(if is_disabled { + icon.color(Color::Disabled) + } else { + icon + }) }) - }) - }) + }, + ) .child( h_flex() .when(self.truncate, |this| this.min_w_0().overflow_hidden())