From a96015b3c5593368730cb5a97956d1e8960b4578 Mon Sep 17 00:00:00 2001 From: Finn Evers Date: Tue, 2 Sep 2025 16:51:13 +0200 Subject: [PATCH] activity_indicator: Show extension installation and updates (#37374) This PR fixes an issue where extension operations would never show in the activity indicator despite this being implemented for ages. This happened because we were always returning `None` whenever the app has a global auto updater, which is always the case, so the code path for showing extension updates in the indicator could never be hit despite existing prior. Also slightly improves the messages shown for ongoing extension operations, as these were previously context unaware. While I was at this, I also quickly took a stab at cleaning up some remotely related stuff, namely: - The `AnimationExt` trait is now by default only implemented for anything that also implements `IntoElement`. This prevents `with_animation` from showing up for e.g. `u32` within the suggestions (finally). - Commonly used animations are now implemented in the `CommonAnimationExt` trait within the `ui` crate so the needed code does not always need to be copied and element IDs for the animations are truly unique. Relevant change here regarding the original issue is the change from the `return match` to just a `match` within the activitiy indicator, which solved the issue at hand. If we find this to be too noisy at some point, we can easily revisit, but I think this holds important enough information to be shown in the activity indicator, especially whilst developing extensions. Release Notes: - Extension installation and updates will now be shown in the activity indicator. --- .../src/activity_indicator.rs | 100 ++++++++++-------- crates/agent_ui/src/acp/thread_view.rs | 43 ++------ crates/agent_ui/src/active_thread.rs | 45 ++------ crates/agent_ui/src/agent_configuration.rs | 19 ++-- .../configure_context_server_modal.rs | 16 ++- crates/agent_ui/src/agent_diff.rs | 14 +-- crates/agent_ui/src/text_thread_editor.rs | 24 ++--- crates/ai_onboarding/src/ai_upsell_card.rs | 15 +-- crates/assistant_tools/src/edit_file_tool.rs | 10 +- crates/assistant_tools/src/terminal_tool.rs | 12 +-- crates/debugger_ui/src/dropdown_menus.rs | 12 +-- crates/git_ui/src/git_panel.rs | 22 ++-- crates/gpui/src/elements/animation.rs | 2 +- .../src/provider/copilot_chat.rs | 14 +-- .../recent_projects/src/remote_connections.rs | 20 ++-- crates/repl/src/outputs.rs | 17 +-- crates/ui/src/components/icon.rs | 5 +- crates/ui/src/components/image.rs | 5 +- crates/ui/src/traits.rs | 2 + crates/ui/src/traits/animation_ext.rs | 42 ++++++++ crates/ui/src/traits/transformable.rs | 7 ++ crates/ui/src/ui.rs | 1 + .../zed/src/zed/quick_action_bar/repl_menu.rs | 14 +-- 23 files changed, 203 insertions(+), 258 deletions(-) create mode 100644 crates/ui/src/traits/animation_ext.rs create mode 100644 crates/ui/src/traits/transformable.rs diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 6641db0805fed2fbade1e66cde143f58123dd3d4..b65d1472a7552d56ec319e12295088a2973796d5 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -1,11 +1,10 @@ use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage, VersionCheckType}; use editor::Editor; -use extension_host::ExtensionStore; +use extension_host::{ExtensionOperation, ExtensionStore}; use futures::StreamExt; use gpui::{ - Animation, AnimationExt as _, App, Context, CursorStyle, Entity, EventEmitter, - InteractiveElement as _, ParentElement as _, Render, SharedString, StatefulInteractiveElement, - Styled, Transformation, Window, actions, percentage, + App, Context, CursorStyle, Entity, EventEmitter, InteractiveElement as _, ParentElement as _, + Render, SharedString, StatefulInteractiveElement, Styled, Window, actions, }; use language::{ BinaryStatus, LanguageRegistry, LanguageServerId, LanguageServerName, @@ -25,7 +24,10 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use ui::{ButtonLike, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*}; +use ui::{ + ButtonLike, CommonAnimationExt, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, + prelude::*, +}; use util::truncate_and_trailoff; use workspace::{StatusItemView, Workspace, item::ItemHandle}; @@ -405,13 +407,7 @@ impl ActivityIndicator { icon: Some( Icon::new(IconName::ArrowCircle) .size(IconSize::Small) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage(delta))) - }, - ) + .with_rotate_animation(2) .into_any_element(), ), message, @@ -433,11 +429,7 @@ impl ActivityIndicator { icon: Some( Icon::new(IconName::ArrowCircle) .size(IconSize::Small) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ) + .with_rotate_animation(2) .into_any_element(), ), message: format!("Debug: {}", session.read(cx).adapter()), @@ -460,11 +452,7 @@ impl ActivityIndicator { icon: Some( Icon::new(IconName::ArrowCircle) .size(IconSize::Small) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ) + .with_rotate_animation(2) .into_any_element(), ), message: job_info.message.into(), @@ -671,8 +659,9 @@ impl ActivityIndicator { } // Show any application auto-update info. - if let Some(updater) = &self.auto_updater { - return match &updater.read(cx).status() { + self.auto_updater + .as_ref() + .and_then(|updater| match &updater.read(cx).status() { AutoUpdateStatus::Checking => Some(Content { icon: Some( Icon::new(IconName::Download) @@ -728,28 +717,49 @@ impl ActivityIndicator { tooltip_message: None, }), AutoUpdateStatus::Idle => None, - }; - } - - if let Some(extension_store) = - ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx)) - && let Some(extension_id) = extension_store.outstanding_operations().keys().next() - { - return Some(Content { - icon: Some( - Icon::new(IconName::Download) - .size(IconSize::Small) - .into_any_element(), - ), - message: format!("Updating {extension_id} extension…"), - on_click: Some(Arc::new(|this, window, cx| { - this.dismiss_error_message(&DismissErrorMessage, window, cx) - })), - tooltip_message: None, - }); - } + }) + .or_else(|| { + if let Some(extension_store) = + ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx)) + && let Some((extension_id, operation)) = + extension_store.outstanding_operations().iter().next() + { + let (message, icon, rotate) = match operation { + ExtensionOperation::Install => ( + format!("Installing {extension_id} extension…"), + IconName::LoadCircle, + true, + ), + ExtensionOperation::Upgrade => ( + format!("Updating {extension_id} extension…"), + IconName::Download, + false, + ), + ExtensionOperation::Remove => ( + format!("Removing {extension_id} extension…"), + IconName::LoadCircle, + true, + ), + }; - None + 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() + } + })), + message, + on_click: Some(Arc::new(|this, window, cx| { + this.dismiss_error_message(&Default::default(), window, cx) + })), + tooltip_message: None, + }) + } else { + None + } + }) } fn version_tooltip_message(version: &VersionCheckType) -> String { diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index c3bf7219b4919c7e479f22f327b73d90b0b1dad7..a9421723d125d27f3eb13c43fb17936f9078dae8 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -23,9 +23,9 @@ use gpui::{ Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem, CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, - Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, - Window, WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, - point, prelude::*, pulsating_between, + Subscription, Task, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, Window, + WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, point, prelude::*, + pulsating_between, }; use language::Buffer; @@ -45,8 +45,8 @@ use terminal_view::terminal_panel::TerminalPanel; use text::Anchor; use theme::ThemeSettings; use ui::{ - Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle, - Scrollbar, ScrollbarState, SpinnerLabel, Tooltip, prelude::*, + Callout, CommonAnimationExt, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, + PopoverMenuHandle, Scrollbar, ScrollbarState, SpinnerLabel, Tooltip, prelude::*, }; use util::{ResultExt, size::format_file_size, time::duration_alt_display}; use workspace::{CollaboratorId, Workspace}; @@ -2515,13 +2515,7 @@ impl AcpThreadView { Icon::new(IconName::ArrowCircle) .size(IconSize::XSmall) .color(Color::Info) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage(delta))) - }, - ), + .with_rotate_animation(2) ) }) .child( @@ -2948,16 +2942,7 @@ impl AcpThreadView { Icon::new(IconName::ArrowCircle) .size(IconSize::Small) .color(Color::Muted) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage( - delta, - ))) - }, - ) - .into_any_element(), + .with_rotate_animation(2) ) .child(Label::new("Authenticating…").size(LabelSize::Small)), ) @@ -3270,13 +3255,7 @@ impl AcpThreadView { acp::PlanEntryStatus::InProgress => Icon::new(IconName::TodoProgress) .size(IconSize::Small) .color(Color::Accent) - .with_animation( - "running", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage(delta))) - }, - ) + .with_rotate_animation(2) .into_any_element(), acp::PlanEntryStatus::Completed => Icon::new(IconName::TodoComplete) .size(IconSize::Small) @@ -5000,11 +4979,7 @@ fn loading_contents_spinner(size: IconSize) -> AnyElement { Icon::new(IconName::LoadCircle) .size(size) .color(Color::Accent) - .with_animation( - "load_context_circle", - Animation::new(Duration::from_secs(3)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ) + .with_rotate_animation(3) .into_any_element() } diff --git a/crates/agent_ui/src/active_thread.rs b/crates/agent_ui/src/active_thread.rs index e0cecad6e2e8b37d649a9dbc0d91268096670365..371a59e7eb9eb88dc5200251f971ef851162b630 100644 --- a/crates/agent_ui/src/active_thread.rs +++ b/crates/agent_ui/src/active_thread.rs @@ -23,9 +23,8 @@ use gpui::{ AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardEntry, ClipboardItem, DefiniteLength, EdgesRefinement, Empty, Entity, EventEmitter, Focusable, Hsla, ListAlignment, ListOffset, ListState, MouseButton, PlatformDisplay, ScrollHandle, Stateful, - StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, Transformation, - UnderlineStyle, WeakEntity, WindowHandle, linear_color_stop, linear_gradient, list, percentage, - pulsating_between, + StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement, UnderlineStyle, + WeakEntity, WindowHandle, linear_color_stop, linear_gradient, list, pulsating_between, }; use language::{Buffer, Language, LanguageRegistry}; use language_model::{ @@ -46,8 +45,8 @@ use std::time::Duration; use text::ToPoint; use theme::ThemeSettings; use ui::{ - Banner, Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize, - Tooltip, prelude::*, + Banner, CommonAnimationExt, Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar, + ScrollbarState, TextSize, Tooltip, prelude::*, }; use util::ResultExt as _; use util::markdown::MarkdownCodeBlock; @@ -2647,15 +2646,7 @@ impl ActiveThread { Icon::new(IconName::ArrowCircle) .color(Color::Accent) .size(IconSize::Small) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate( - percentage(delta), - )) - }, - ) + .with_rotate_animation(2) }), ), ) @@ -2831,17 +2822,11 @@ impl ActiveThread { } ToolUseStatus::Pending | ToolUseStatus::InputStillStreaming - | ToolUseStatus::Running => { - let icon = Icon::new(IconName::ArrowCircle) - .color(Color::Accent) - .size(IconSize::Small); - icon.with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ) - .into_any_element() - } + | ToolUseStatus::Running => Icon::new(IconName::ArrowCircle) + .color(Color::Accent) + .size(IconSize::Small) + .with_rotate_animation(2) + .into_any_element(), ToolUseStatus::Finished(_) => div().w_0().into_any_element(), ToolUseStatus::Error(_) => { let icon = Icon::new(IconName::Close) @@ -2930,15 +2915,7 @@ impl ActiveThread { Icon::new(IconName::ArrowCircle) .size(IconSize::Small) .color(Color::Accent) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage( - delta, - ))) - }, - ), + .with_rotate_animation(2), ) .child( Label::new("Running…") diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 5f0b6f33c38b0b064fcb8b287a901a33e9e7186b..5981a3c52bf52ff4549b2f73a6322e308725750d 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -3,7 +3,7 @@ mod configure_context_server_modal; mod manage_profiles_modal; mod tool_picker; -use std::{ops::Range, sync::Arc, time::Duration}; +use std::{ops::Range, sync::Arc}; use agent_servers::{AgentServerCommand, AllAgentServersSettings, CustomAgentServerSettings}; use agent_settings::AgentSettings; @@ -17,9 +17,8 @@ use extension::ExtensionManifest; use extension_host::ExtensionStore; use fs::Fs; use gpui::{ - Action, Animation, AnimationExt as _, AnyView, App, AsyncWindowContext, Corner, Entity, - EventEmitter, FocusHandle, Focusable, Hsla, ScrollHandle, Subscription, Task, Transformation, - WeakEntity, percentage, + Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable, + Hsla, ScrollHandle, Subscription, Task, WeakEntity, }; use language::LanguageRegistry; use language_model::{ @@ -32,8 +31,9 @@ use project::{ }; use settings::{Settings, SettingsStore, update_settings_file}; use ui::{ - Chip, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, Indicator, PopoverMenu, - Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, prelude::*, + Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor, ElevationIndex, + Indicator, PopoverMenu, Scrollbar, ScrollbarState, Switch, SwitchColor, SwitchField, Tooltip, + prelude::*, }; use util::ResultExt as _; use workspace::{Workspace, create_and_open_local_file}; @@ -670,10 +670,9 @@ impl AgentConfiguration { Icon::new(IconName::LoadCircle) .size(IconSize::XSmall) .color(Color::Accent) - .with_animation( - SharedString::from(format!("{}-starting", context_server_id.0,)), - Animation::new(Duration::from_secs(3)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), + .with_keyed_rotate_animation( + SharedString::from(format!("{}-starting", context_server_id.0)), + 3, ) .into_any_element(), "Server is starting.", diff --git a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs index c898a5acb5b8d0a45780efb383ece19b4cfe289d..e5027b876ac0f996e1f4df2a61af1477c6490c10 100644 --- a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs +++ b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs @@ -1,16 +1,14 @@ use std::{ path::PathBuf, sync::{Arc, Mutex}, - time::Duration, }; use anyhow::{Context as _, Result}; use context_server::{ContextServerCommand, ContextServerId}; use editor::{Editor, EditorElement, EditorStyle}; use gpui::{ - Animation, AnimationExt as _, AsyncWindowContext, DismissEvent, Entity, EventEmitter, - FocusHandle, Focusable, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, - WeakEntity, percentage, prelude::*, + AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task, + TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*, }; use language::{Language, LanguageRegistry}; use markdown::{Markdown, MarkdownElement, MarkdownStyle}; @@ -24,7 +22,9 @@ use project::{ }; use settings::{Settings as _, update_settings_file}; use theme::ThemeSettings; -use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*}; +use ui::{ + CommonAnimationExt, KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*, +}; use util::ResultExt as _; use workspace::{ModalView, Workspace}; @@ -638,11 +638,7 @@ impl ConfigureContextServerModal { Icon::new(IconName::ArrowCircle) .size(IconSize::XSmall) .color(Color::Info) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ) + .with_rotate_animation(2) .into_any_element(), ) .child( diff --git a/crates/agent_ui/src/agent_diff.rs b/crates/agent_ui/src/agent_diff.rs index 4bd525e9d0461a7a180cccc1748e7f8983c0b665..74bcb266d52ac25c91f3243c3e76f1e1f25d770e 100644 --- a/crates/agent_ui/src/agent_diff.rs +++ b/crates/agent_ui/src/agent_diff.rs @@ -14,9 +14,8 @@ use editor::{ scroll::Autoscroll, }; use gpui::{ - Action, Animation, AnimationExt, AnyElement, AnyView, App, AppContext, Empty, Entity, - EventEmitter, FocusHandle, Focusable, Global, SharedString, Subscription, Task, Transformation, - WeakEntity, Window, percentage, prelude::*, + Action, AnyElement, AnyView, App, AppContext, Empty, Entity, EventEmitter, FocusHandle, + Focusable, Global, SharedString, Subscription, Task, WeakEntity, Window, prelude::*, }; use language::{Buffer, Capability, DiskState, OffsetRangeExt, Point}; @@ -29,9 +28,8 @@ use std::{ collections::hash_map::Entry, ops::Range, sync::Arc, - time::Duration, }; -use ui::{IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider}; +use ui::{CommonAnimationExt, IconButtonShape, KeyBinding, Tooltip, prelude::*, vertical_divider}; use util::ResultExt; use workspace::{ Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, @@ -1084,11 +1082,7 @@ impl Render for AgentDiffToolbar { Icon::new(IconName::LoadCircle) .size(IconSize::Small) .color(Color::Accent) - .with_animation( - "load_circle", - Animation::new(Duration::from_secs(3)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ), + .with_rotate_animation(3), ) .into_any(); diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs index 70ec94beeadb1ae84839bab6747715223f2540c9..d979db5e0468b696d32ed755aec1ef47e2fd3df3 100644 --- a/crates/agent_ui/src/text_thread_editor.rs +++ b/crates/agent_ui/src/text_thread_editor.rs @@ -25,8 +25,8 @@ use gpui::{ Action, Animation, AnimationExt, AnyElement, AnyView, App, ClipboardEntry, ClipboardItem, Empty, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, Global, InteractiveElement, IntoElement, ParentElement, Pixels, Render, RenderImage, SharedString, Size, - StatefulInteractiveElement, Styled, Subscription, Task, Transformation, WeakEntity, actions, - div, img, percentage, point, prelude::*, pulsating_between, size, + StatefulInteractiveElement, Styled, Subscription, Task, WeakEntity, actions, div, img, point, + prelude::*, pulsating_between, size, }; use language::{ BufferSnapshot, LspAdapterDelegate, ToOffset, @@ -53,8 +53,8 @@ use std::{ }; use text::SelectionGoal; use ui::{ - ButtonLike, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle, TintColor, Tooltip, - prelude::*, + ButtonLike, CommonAnimationExt, Disclosure, ElevationIndex, KeyBinding, PopoverMenuHandle, + TintColor, Tooltip, prelude::*, }; use util::{ResultExt, maybe}; use workspace::{ @@ -1061,15 +1061,7 @@ impl TextThreadEditor { Icon::new(IconName::ArrowCircle) .size(IconSize::XSmall) .color(Color::Info) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate( - percentage(delta), - )) - }, - ) + .with_rotate_animation(2) .into_any_element(), ); note = Some(Self::esc_kbd(cx).into_any_element()); @@ -2790,11 +2782,7 @@ fn invoked_slash_command_fold_placeholder( .child(Label::new(format!("/{}", command.name))) .map(|parent| match &command.status { InvokedSlashCommandStatus::Running(_) => { - parent.child(Icon::new(IconName::ArrowCircle).with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(4)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - )) + parent.child(Icon::new(IconName::ArrowCircle).with_rotate_animation(4)) } InvokedSlashCommandStatus::Error(message) => parent.child( Label::new(format!("error: {message}")) diff --git a/crates/ai_onboarding/src/ai_upsell_card.rs b/crates/ai_onboarding/src/ai_upsell_card.rs index 106dcb0aef0ee35836b2c7c576d7c68799ea988a..efe6e4165e445c4cd92f4d08dfc0c1e1947acd55 100644 --- a/crates/ai_onboarding/src/ai_upsell_card.rs +++ b/crates/ai_onboarding/src/ai_upsell_card.rs @@ -1,12 +1,9 @@ -use std::{sync::Arc, time::Duration}; +use std::sync::Arc; use client::{Client, UserStore, zed_urls}; use cloud_llm_client::Plan; -use gpui::{ - Animation, AnimationExt, AnyElement, App, Entity, IntoElement, RenderOnce, Transformation, - Window, percentage, -}; -use ui::{Divider, Vector, VectorName, prelude::*}; +use gpui::{AnyElement, App, Entity, IntoElement, RenderOnce, Window}; +use ui::{CommonAnimationExt, Divider, Vector, VectorName, prelude::*}; use crate::{SignInStatus, YoungAccountBanner, plan_definitions::PlanDefinitions}; @@ -147,11 +144,7 @@ impl RenderOnce for AiUpsellCard { rems_from_px(72.), ) .color(Color::Custom(cx.theme().colors().text_accent.alpha(0.3))) - .with_animation( - "loading_stamp", - Animation::new(Duration::from_secs(10)).repeat(), - |this, delta| this.transform(Transformation::rotate(percentage(delta))), - ), + .with_rotate_animation(10), ); let pro_trial_stamp = div() diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index 7b208ccc7768c9c0df2904573e2d47504a8eb61f..d13f9891c3af1933ee49428c223d3e6737871047 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -17,7 +17,7 @@ use editor::{ use futures::StreamExt; use gpui::{ Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task, - TextStyleRefinement, Transformation, WeakEntity, percentage, pulsating_between, px, + TextStyleRefinement, WeakEntity, pulsating_between, px, }; use indoc::formatdoc; use language::{ @@ -44,7 +44,7 @@ use std::{ time::Duration, }; use theme::ThemeSettings; -use ui::{Disclosure, Tooltip, prelude::*}; +use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*}; use util::ResultExt; use workspace::Workspace; @@ -939,11 +939,7 @@ impl ToolCard for EditFileToolCard { Icon::new(IconName::ArrowCircle) .size(IconSize::XSmall) .color(Color::Info) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ), + .with_rotate_animation(2), ) }) .when_some(error_message, |header, error_message| { diff --git a/crates/assistant_tools/src/terminal_tool.rs b/crates/assistant_tools/src/terminal_tool.rs index 774f32426540e077e5bde72081db789329f86262..1605003671621b90e58a5f62e521c0aba2c990c6 100644 --- a/crates/assistant_tools/src/terminal_tool.rs +++ b/crates/assistant_tools/src/terminal_tool.rs @@ -8,8 +8,8 @@ use anyhow::{Context as _, Result, anyhow}; use assistant_tool::{Tool, ToolCard, ToolResult, ToolUseStatus}; use futures::{FutureExt as _, future::Shared}; use gpui::{ - Animation, AnimationExt, AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, - TextStyleRefinement, Transformation, WeakEntity, Window, percentage, + AnyWindowHandle, App, AppContext, Empty, Entity, EntityId, Task, TextStyleRefinement, + WeakEntity, Window, }; use language::LineEnding; use language_model::{LanguageModel, LanguageModelRequest, LanguageModelToolSchemaFormat}; @@ -28,7 +28,7 @@ use std::{ }; use terminal_view::TerminalView; use theme::ThemeSettings; -use ui::{Disclosure, Tooltip, prelude::*}; +use ui::{CommonAnimationExt, Disclosure, Tooltip, prelude::*}; use util::{ ResultExt, get_system_shell, markdown::MarkdownInlineCode, size::format_file_size, time::duration_alt_display, @@ -522,11 +522,7 @@ impl ToolCard for TerminalToolCard { Icon::new(IconName::ArrowCircle) .size(IconSize::XSmall) .color(Color::Info) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ), + .with_rotate_animation(2), ) }) .when(tool_failed || command_failed, |header| { diff --git a/crates/debugger_ui/src/dropdown_menus.rs b/crates/debugger_ui/src/dropdown_menus.rs index c5399f6f69648dcfa775a6dd6da62bd637124f2c..c611d5d44f36b4eafb578a400da615bbd96b4cd2 100644 --- a/crates/debugger_ui/src/dropdown_menus.rs +++ b/crates/debugger_ui/src/dropdown_menus.rs @@ -1,9 +1,9 @@ -use std::{rc::Rc, time::Duration}; +use std::rc::Rc; use collections::HashMap; -use gpui::{Animation, AnimationExt as _, Entity, Transformation, WeakEntity, percentage}; +use gpui::{Entity, WeakEntity}; use project::debugger::session::{ThreadId, ThreadStatus}; -use ui::{ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*}; +use ui::{CommonAnimationExt, ContextMenu, DropdownMenu, DropdownStyle, Indicator, prelude::*}; use util::{maybe, truncate_and_trailoff}; use crate::{ @@ -152,11 +152,7 @@ impl DebugPanel { Icon::new(IconName::ArrowCircle) .size(IconSize::Small) .color(Color::Muted) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ) + .with_rotate_animation(2) .into_any_element() } else { match running_state.thread_status(cx).unwrap_or_default() { diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 4ecb4a8829659ca9a25152db8d1eff529cfff2b1..64163b0ebc33f908de5c5cd8c97a24418bf4ba43 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -31,11 +31,11 @@ use git::{ UnstageAll, }; use gpui::{ - Action, Animation, AnimationExt as _, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner, - DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, - ListHorizontalSizingBehavior, ListSizingBehavior, MouseButton, MouseDownEvent, Point, - PromptLevel, ScrollStrategy, Subscription, Task, Transformation, UniformListScrollHandle, - WeakEntity, actions, anchored, deferred, percentage, uniform_list, + Action, AsyncApp, AsyncWindowContext, Axis, ClickEvent, Corner, DismissEvent, Entity, + EventEmitter, FocusHandle, Focusable, KeyContext, ListHorizontalSizingBehavior, + ListSizingBehavior, MouseButton, MouseDownEvent, Point, PromptLevel, ScrollStrategy, + Subscription, Task, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, + uniform_list, }; use itertools::Itertools; use language::{Buffer, File}; @@ -63,8 +63,8 @@ use std::{collections::HashSet, sync::Arc, time::Duration, usize}; use strum::{IntoEnumIterator, VariantNames}; use time::OffsetDateTime; use ui::{ - Checkbox, ContextMenu, ElevationIndex, IconPosition, Label, LabelSize, PopoverMenu, Scrollbar, - ScrollbarState, SplitButton, Tooltip, prelude::*, + Checkbox, CommonAnimationExt, ContextMenu, ElevationIndex, IconPosition, Label, LabelSize, + PopoverMenu, Scrollbar, ScrollbarState, SplitButton, Tooltip, prelude::*, }; use util::{ResultExt, TryFutureExt, maybe}; use workspace::SERIALIZATION_THROTTLE_TIME; @@ -3088,13 +3088,7 @@ impl GitPanel { Icon::new(IconName::ArrowCircle) .size(IconSize::XSmall) .color(Color::Info) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage(delta))) - }, - ), + .with_rotate_animation(2), ) .child( Label::new("Generating Commit...") diff --git a/crates/gpui/src/elements/animation.rs b/crates/gpui/src/elements/animation.rs index 11dd19e260c20e49b87e05137771be73a3f816ea..e72fb00456d14dec74ffc56e040511c189af1d18 100644 --- a/crates/gpui/src/elements/animation.rs +++ b/crates/gpui/src/elements/animation.rs @@ -87,7 +87,7 @@ pub trait AnimationExt { } } -impl AnimationExt for E {} +impl AnimationExt for E {} /// A GPUI element that applies an animation to another element pub struct AnimationElement { diff --git a/crates/language_models/src/provider/copilot_chat.rs b/crates/language_models/src/provider/copilot_chat.rs index bd284eb72b207dee90048f06dc44a8e21ae8d34f..071424eabe3c1ad3436de201860d6220ab664a06 100644 --- a/crates/language_models/src/provider/copilot_chat.rs +++ b/crates/language_models/src/provider/copilot_chat.rs @@ -14,10 +14,7 @@ use copilot::{Copilot, Status}; use futures::future::BoxFuture; use futures::stream::BoxStream; use futures::{FutureExt, Stream, StreamExt}; -use gpui::{ - Action, Animation, AnimationExt, AnyView, App, AsyncApp, Entity, Render, Subscription, Task, - Transformation, percentage, svg, -}; +use gpui::{Action, AnyView, App, AsyncApp, Entity, Render, Subscription, Task, svg}; use language::language_settings::all_language_settings; use language_model::{ AuthenticateError, LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, @@ -28,8 +25,7 @@ use language_model::{ StopReason, TokenUsage, }; use settings::SettingsStore; -use std::time::Duration; -use ui::prelude::*; +use ui::{CommonAnimationExt, prelude::*}; use util::debug_panic; use crate::provider::x_ai::count_xai_tokens; @@ -672,11 +668,7 @@ impl Render for ConfigurationView { }), ) } else { - let loading_icon = Icon::new(IconName::ArrowCircle).with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(4)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ); + let loading_icon = Icon::new(IconName::ArrowCircle).with_rotate_animation(4); const ERROR_LABEL: &str = "Copilot Chat requires an active GitHub Copilot subscription. Please ensure Copilot is configured and try again, or use a different Assistant provider."; diff --git a/crates/recent_projects/src/remote_connections.rs b/crates/recent_projects/src/remote_connections.rs index e543bf219ff0bc8226e819798a9ea74a098d0f98..a7f915301f42850b03be951f596a8542842a6877 100644 --- a/crates/recent_projects/src/remote_connections.rs +++ b/crates/recent_projects/src/remote_connections.rs @@ -1,5 +1,5 @@ use std::collections::BTreeSet; -use std::{path::PathBuf, sync::Arc, time::Duration}; +use std::{path::PathBuf, sync::Arc}; use anyhow::{Context as _, Result}; use auto_update::AutoUpdater; @@ -7,9 +7,9 @@ use editor::Editor; use extension_host::ExtensionStore; use futures::channel::oneshot; use gpui::{ - Animation, AnimationExt, AnyWindowHandle, App, AsyncApp, DismissEvent, Entity, EventEmitter, - Focusable, FontFeatures, ParentElement as _, PromptLevel, Render, SemanticVersion, - SharedString, Task, TextStyleRefinement, Transformation, WeakEntity, percentage, + AnyWindowHandle, App, AsyncApp, DismissEvent, Entity, EventEmitter, Focusable, FontFeatures, + ParentElement as _, PromptLevel, Render, SemanticVersion, SharedString, Task, + TextStyleRefinement, WeakEntity, }; use language::CursorShape; @@ -24,8 +24,8 @@ use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsSources, SettingsUi}; use theme::ThemeSettings; use ui::{ - ActiveTheme, Color, Context, Icon, IconName, IconSize, InteractiveElement, IntoElement, Label, - LabelCommon, Styled, Window, prelude::*, + ActiveTheme, Color, CommonAnimationExt, Context, Icon, IconName, IconSize, InteractiveElement, + IntoElement, Label, LabelCommon, Styled, Window, prelude::*, }; use util::serde::default_true; use workspace::{AppState, ModalView, Workspace}; @@ -268,13 +268,7 @@ impl Render for RemoteConnectionPrompt { .child( Icon::new(IconName::ArrowCircle) .size(IconSize::Medium) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage(delta))) - }, - ), + .with_rotate_animation(2), ) .child( div() diff --git a/crates/repl/src/outputs.rs b/crates/repl/src/outputs.rs index 767b103435e1f80b2b6802bdc2525fcd992931bc..2cd6494d66be1b615e10e537c139e4b2e22af863 100644 --- a/crates/repl/src/outputs.rs +++ b/crates/repl/src/outputs.rs @@ -33,16 +33,13 @@ //! This module is designed to work with Jupyter message protocols, //! interpreting and displaying various types of Jupyter output. -use std::time::Duration; - use editor::{Editor, MultiBuffer}; -use gpui::{ - Animation, AnimationExt, AnyElement, ClipboardItem, Entity, Render, Transformation, WeakEntity, - percentage, -}; +use gpui::{AnyElement, ClipboardItem, Entity, Render, WeakEntity}; use language::Buffer; use runtimelib::{ExecutionState, JupyterMessageContent, MimeBundle, MimeType}; -use ui::{Context, IntoElement, Styled, Tooltip, Window, div, prelude::*, v_flex}; +use ui::{ + CommonAnimationExt, Context, IntoElement, Styled, Tooltip, Window, div, prelude::*, v_flex, +}; mod image; use image::ImageView; @@ -481,11 +478,7 @@ impl Render for ExecutionView { Icon::new(IconName::ArrowCircle) .size(IconSize::Small) .color(Color::Muted) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(3)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ), + .with_rotate_animation(3), ) .child(Label::new("Executing...").color(Color::Muted)) .into_any_element(), diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index bc0ec462e9fbb964abc1e305933ce759ddde0ebc..8f7ef41108afd22a7f932e8ab6ed1b74078244ec 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -9,6 +9,7 @@ use gpui::{AnimationElement, AnyElement, Hsla, IntoElement, Rems, Transformation pub use icon_decoration::*; pub use icons::*; +use crate::traits::transformable::Transformable; use crate::{Indicator, prelude::*}; #[derive(IntoElement)] @@ -180,8 +181,10 @@ impl Icon { self.size = size; self } +} - pub fn transform(mut self, transformation: Transformation) -> Self { +impl Transformable for Icon { + fn transform(mut self, transformation: Transformation) -> Self { self.transformation = transformation; self } diff --git a/crates/ui/src/components/image.rs b/crates/ui/src/components/image.rs index 6e552ddcee83e20d3812f78c67270c0291c2c0e7..8a14cffd3b2de2e184fd87a9212775c470e3118d 100644 --- a/crates/ui/src/components/image.rs +++ b/crates/ui/src/components/image.rs @@ -7,6 +7,7 @@ use strum::{EnumIter, EnumString, IntoStaticStr}; use crate::Color; use crate::prelude::*; +use crate::traits::transformable::Transformable; #[derive( Debug, PartialEq, Eq, Copy, Clone, EnumIter, EnumString, IntoStaticStr, Serialize, Deserialize, @@ -74,8 +75,10 @@ impl Vector { self.size = size; self } +} - pub fn transform(mut self, transformation: Transformation) -> Self { +impl Transformable for Vector { + fn transform(mut self, transformation: Transformation) -> Self { self.transformation = transformation; self } diff --git a/crates/ui/src/traits.rs b/crates/ui/src/traits.rs index 628c76aaddecaa291b3cfad2e6d16ccd6478c767..9627f6d6ad275dbcd4281cd6a85741d932b688fe 100644 --- a/crates/ui/src/traits.rs +++ b/crates/ui/src/traits.rs @@ -1,6 +1,8 @@ +pub mod animation_ext; pub mod clickable; pub mod disableable; pub mod fixed; pub mod styled_ext; pub mod toggleable; +pub mod transformable; pub mod visible_on_hover; diff --git a/crates/ui/src/traits/animation_ext.rs b/crates/ui/src/traits/animation_ext.rs new file mode 100644 index 0000000000000000000000000000000000000000..4907c71ff2ad29104e3e9a3f408c1c9b69af8d44 --- /dev/null +++ b/crates/ui/src/traits/animation_ext.rs @@ -0,0 +1,42 @@ +use std::time::Duration; + +use gpui::{Animation, AnimationElement, AnimationExt, Transformation, percentage}; + +use crate::{prelude::*, traits::transformable::Transformable}; + +/// An extension trait for adding common animations to animatable components. +pub trait CommonAnimationExt: AnimationExt { + /// Render this component as rotating over the given duration. + /// + /// NOTE: This method uses the location of the caller to generate an ID for this state. + /// If this is not sufficient to identify your state (e.g. you're rendering a list item), + /// you can provide a custom ElementID using the `use_keyed_rotate_animation` method. + #[track_caller] + fn with_rotate_animation(self, duration: u64) -> AnimationElement + where + Self: Transformable + Sized, + { + self.with_keyed_rotate_animation( + ElementId::CodeLocation(*std::panic::Location::caller()), + duration, + ) + } + + /// Render this component as rotating with the given element ID over the given duration. + fn with_keyed_rotate_animation( + self, + id: impl Into, + duration: u64, + ) -> AnimationElement + where + Self: Transformable + Sized, + { + self.with_animation( + id, + Animation::new(Duration::from_secs(duration)).repeat(), + |component, delta| component.transform(Transformation::rotate(percentage(delta))), + ) + } +} + +impl CommonAnimationExt for T {} diff --git a/crates/ui/src/traits/transformable.rs b/crates/ui/src/traits/transformable.rs new file mode 100644 index 0000000000000000000000000000000000000000..f52141f304d51807e45f97d5091f5a5087467679 --- /dev/null +++ b/crates/ui/src/traits/transformable.rs @@ -0,0 +1,7 @@ +use gpui::Transformation; + +/// A trait for components that can be transformed. +pub trait Transformable { + /// Sets the transformation for the element. + fn transform(self, transformation: Transformation) -> Self; +} diff --git a/crates/ui/src/ui.rs b/crates/ui/src/ui.rs index dadc5ecdd12d6c3b7e3431977f1606d56c456cfa..17e707f11b3a43392bff755f1ba7904b61b02f92 100644 --- a/crates/ui/src/ui.rs +++ b/crates/ui/src/ui.rs @@ -17,3 +17,4 @@ pub mod utils; pub use components::*; pub use prelude::*; pub use styles::*; +pub use traits::animation_ext::*; diff --git a/crates/zed/src/zed/quick_action_bar/repl_menu.rs b/crates/zed/src/zed/quick_action_bar/repl_menu.rs index eaa989f88dc9e3e3e969841f02fa334a8f6f594e..82eb82de1e2807346eb3ade2ced8a7946413f0a4 100644 --- a/crates/zed/src/zed/quick_action_bar/repl_menu.rs +++ b/crates/zed/src/zed/quick_action_bar/repl_menu.rs @@ -1,7 +1,5 @@ -use std::time::Duration; - use gpui::ElementId; -use gpui::{Animation, AnimationExt, AnyElement, Entity, Transformation, percentage}; +use gpui::{AnyElement, Entity}; use picker::Picker; use repl::{ ExecutionState, JupyterSettings, Kernel, KernelSpecification, KernelStatus, Session, @@ -10,8 +8,8 @@ use repl::{ worktree_id_for_editor, }; use ui::{ - ButtonLike, ContextMenu, IconWithIndicator, Indicator, IntoElement, PopoverMenu, - PopoverMenuHandle, Tooltip, prelude::*, + ButtonLike, CommonAnimationExt, ContextMenu, IconWithIndicator, Indicator, IntoElement, + PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*, }; use util::ResultExt; @@ -224,11 +222,7 @@ impl QuickActionBar { .child(if menu_state.icon_is_animating { Icon::new(menu_state.icon) .color(menu_state.icon_color) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(5)).repeat(), - |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), - ) + .with_rotate_animation(5) .into_any_element() } else { IconWithIndicator::new(