From 191b4ccd4fce447bf3fb50030b84ee0308af7548 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:03:46 -0300 Subject: [PATCH] workspace: Move the update Zed button to the title bar (#48467) Currently, whenever Zed has a new version available for download, the update button shows up in the status bar. Problem is: that status bar slot can also display other buttons/information, such as problems with your language server or general errors. In case the two things exist (a problem and a new version), just one of them would be displayed, which is not great; you should be able to see both. Additionally, given we ship new versions pretty often, I've frequently saw feedback about wanting to hide away the new version button... at least temporarily while there's no immediate interest in upgrading. So, this PR tackles all of that. The button to update a new version is moved up to the title bar, nearby your avatar, and you have the ability to dismiss, which effectively just moves the button from the title bar to inside your user menu. https://github.com/user-attachments/assets/e3f1d76d-9b85-4bee-a70f-e22dd5e7fdb3 Release Notes: - Moved the update Zed button to the title bar and allowed it to be dismissed. --------- Co-authored-by: Conrad Irwin --- Cargo.lock | 3 +- crates/activity_indicator/Cargo.toml | 1 - .../src/activity_indicator.rs | 194 ++++------------- crates/auto_update/src/auto_update.rs | 14 ++ crates/title_bar/Cargo.toml | 3 + crates/title_bar/src/title_bar.rs | 70 +++++- crates/title_bar/src/update_version.rs | 148 +++++++++++++ crates/ui/src/components/collab.rs | 2 + .../ui/src/components/collab/update_button.rs | 202 ++++++++++++++++++ 9 files changed, 478 insertions(+), 159 deletions(-) create mode 100644 crates/title_bar/src/update_version.rs create mode 100644 crates/ui/src/components/collab/update_button.rs diff --git a/Cargo.lock b/Cargo.lock index 5425966d18678812a826b311c02a0e4db8b08ee3..d3ea0c1b6b1aed63d3059059672254edbc49b7ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -108,7 +108,6 @@ dependencies = [ "project", "proto", "release_channel", - "semver", "smallvec", "ui", "util", @@ -17247,9 +17246,11 @@ dependencies = [ "pretty_assertions", "project", "recent_projects", + "release_channel", "remote", "rpc", "schemars", + "semver", "serde", "settings", "smallvec", diff --git a/crates/activity_indicator/Cargo.toml b/crates/activity_indicator/Cargo.toml index 8587e52723b48d1495bbfbc5442bb8007aed1786..99ae5b5b077a14c0909737d64935220698a007c7 100644 --- a/crates/activity_indicator/Cargo.toml +++ b/crates/activity_indicator/Cargo.toml @@ -23,7 +23,6 @@ gpui.workspace = true language.workspace = true project.workspace = true proto.workspace = true -semver.workspace = true smallvec.workspace = true ui.workspace = true util.workspace = true diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index 8b67bfc3c9cdf85b436e5f197e9de21f4b33cd43..d2d8b6505a080cf2816f28b43c6f3c35406cce85 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -1,4 +1,4 @@ -use auto_update::{AutoUpdateStatus, AutoUpdater, DismissMessage, VersionCheckType}; +use auto_update::DismissMessage; use editor::Editor; use extension_host::{ExtensionOperation, ExtensionStore}; use futures::StreamExt; @@ -49,7 +49,6 @@ pub enum Event { pub struct ActivityIndicator { statuses: Vec, project: Entity, - auto_updater: Option>, context_menu_handle: PopoverMenuHandle, fs_jobs: Vec, } @@ -82,7 +81,6 @@ impl ActivityIndicator { cx: &mut Context, ) -> Entity { let project = workspace.project().clone(); - let auto_updater = AutoUpdater::get(cx); let this = cx.new(|cx| { let mut status_events = languages.language_server_binary_statuses(); cx.spawn(async move |this, cx| { @@ -215,14 +213,9 @@ impl ActivityIndicator { ) .detach(); - if let Some(auto_updater) = auto_updater.as_ref() { - cx.observe(auto_updater, |_, _, cx| cx.notify()).detach(); - } - Self { statuses: Vec::new(), project: project.clone(), - auto_updater, context_menu_handle: PopoverMenuHandle::default(), fs_jobs: Vec::new(), } @@ -302,15 +295,6 @@ impl ActivityIndicator { } fn dismiss_message(&mut self, _: &DismissMessage, _: &mut Window, cx: &mut Context) { - let dismissed = if let Some(updater) = &self.auto_updater { - updater.update(cx, |updater, cx| updater.dismiss(cx)) - } else { - false - }; - if dismissed { - return; - } - self.project.update(cx, |project, cx| { if project.last_formatting_failure(cx).is_some() { project.reset_last_formatting_failure(cx); @@ -669,122 +653,47 @@ impl ActivityIndicator { }); } - // Show any application auto-update info. - self.auto_updater - .as_ref() - .and_then(|updater| match &updater.read(cx).status() { - AutoUpdateStatus::Checking => Some(Content { - icon: Some( - Icon::new(IconName::LoadCircle) - .size(IconSize::Small) - .with_rotate_animation(3) - .into_any_element(), - ), - message: "Checking for Zed updates…".to_string(), - on_click: Some(Arc::new(|this, window, cx| { - this.dismiss_message(&DismissMessage, window, cx) - })), - tooltip_message: None, - }), - AutoUpdateStatus::Downloading { version } => Some(Content { - icon: Some( - Icon::new(IconName::Download) - .size(IconSize::Small) - .into_any_element(), - ), - message: "Downloading Zed update…".to_string(), - on_click: Some(Arc::new(|this, window, cx| { - this.dismiss_message(&DismissMessage, window, cx) - })), - tooltip_message: Some(Self::version_tooltip_message(version)), - }), - AutoUpdateStatus::Installing { version } => Some(Content { - icon: Some( - Icon::new(IconName::LoadCircle) - .size(IconSize::Small) - .with_rotate_animation(3) - .into_any_element(), - ), - message: "Installing Zed update…".to_string(), - on_click: Some(Arc::new(|this, window, cx| { - this.dismiss_message(&DismissMessage, window, cx) - })), - tooltip_message: Some(Self::version_tooltip_message(version)), - }), - AutoUpdateStatus::Updated { version } => Some(Content { - icon: None, - message: "Click to restart and update Zed".to_string(), - on_click: Some(Arc::new(move |_, _, cx| workspace::reload(cx))), - tooltip_message: Some(Self::version_tooltip_message(version)), - }), - AutoUpdateStatus::Errored { error } => Some(Content { - icon: Some( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .into_any_element(), - ), - message: "Failed to update Zed".to_string(), - on_click: Some(Arc::new(|this, window, cx| { - window.dispatch_action(Box::new(workspace::OpenLog), cx); - this.dismiss_message(&DismissMessage, window, cx); - })), - tooltip_message: Some(format!("{error}")), - }), - AutoUpdateStatus::Idle => 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, - ), - }; + // Show any extension installation info. + 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, + ), + }; - 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_message(&Default::default(), window, cx) - })), - tooltip_message: None, - }) - } else { - None - } - }) - } + 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() + } + })), + message, + on_click: Some(Arc::new(|this, window, cx| { + this.dismiss_message(&Default::default(), window, cx) + })), + tooltip_message: None, + }); + } - fn version_tooltip_message(version: &VersionCheckType) -> String { - format!("Version: {}", { - match version { - auto_update::VersionCheckType::Sha(sha) => format!("{}…", sha.short()), - auto_update::VersionCheckType::Semantic(semantic_version) => { - semantic_version.to_string() - } - } - }) + None } fn toggle_language_server_work_context_menu( @@ -922,26 +831,3 @@ impl StatusItemView for ActivityIndicator { ) { } } - -#[cfg(test)] -mod tests { - use release_channel::AppCommitSha; - use semver::Version; - - use super::*; - - #[test] - fn test_version_tooltip_message() { - let message = ActivityIndicator::version_tooltip_message(&VersionCheckType::Semantic( - Version::new(1, 0, 0), - )); - - assert_eq!(message, "Version: 1.0.0"); - - let message = ActivityIndicator::version_tooltip_message(&VersionCheckType::Sha( - AppCommitSha::new("14d9a4189f058d8736339b06ff2340101eaea5af".to_string()), - )); - - assert_eq!(message, "Version: 14d9a41…"); - } -} diff --git a/crates/auto_update/src/auto_update.rs b/crates/auto_update/src/auto_update.rs index a9243fa628020028412be88f180ac1ed5dc32ae9..4c20f8adb531e47262f1d4115737ec1ee211e547 100644 --- a/crates/auto_update/src/auto_update.rs +++ b/crates/auto_update/src/auto_update.rs @@ -108,6 +108,7 @@ pub struct AutoUpdater { client: Arc, pending_poll: Option>>, quit_subscription: Option, + update_check_type: UpdateCheckType, } #[derive(Deserialize, Serialize, Clone, Debug)] @@ -318,11 +319,18 @@ impl InstallerDir { } } +#[derive(Clone, Copy, Debug, PartialEq)] pub enum UpdateCheckType { Automatic, Manual, } +impl UpdateCheckType { + pub fn is_manual(self) -> bool { + self == Self::Manual + } +} + impl AutoUpdater { pub fn get(cx: &mut App) -> Option> { cx.default_global::().0.clone() @@ -352,6 +360,7 @@ impl AutoUpdater { client, pending_poll: None, quit_subscription, + update_check_type: UpdateCheckType::Automatic, } } @@ -373,10 +382,15 @@ impl AutoUpdater { }) } + pub fn update_check_type(&self) -> UpdateCheckType { + self.update_check_type + } + pub fn poll(&mut self, check_type: UpdateCheckType, cx: &mut Context) { if self.pending_poll.is_some() { return; } + self.update_check_type = check_type; cx.notify(); diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml index 40c6ba6ae60ef06cab84c8be35150f0bccc748f8..db735590716f0dbcf184155c9c6ab0860a80615a 100644 --- a/crates/title_bar/Cargo.toml +++ b/crates/title_bar/Cargo.toml @@ -46,6 +46,7 @@ project.workspace = true recent_projects.workspace = true remote.workspace = true rpc.workspace = true +semver.workspace = true schemars.workspace = true serde.workspace = true settings.workspace = true @@ -70,8 +71,10 @@ http_client = { workspace = true, features = ["test-support"] } notifications = { workspace = true, features = ["test-support"] } pretty_assertions.workspace = true project = { workspace = true, features = ["test-support"] } +release_channel.workspace = true remote = { workspace = true, features = ["test-support"] } rpc = { workspace = true, features = ["test-support"] } +semver.workspace = true settings = { workspace = true, features = ["test-support"] } tree-sitter-md.workspace = true util = { workspace = true, features = ["test-support"] } diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index 38fc531a76ee1bbdac2b821e7f0d270919219ac6..ff14c6fa25bfa3b52bfdd34433548431a042bc2b 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/crates/title_bar/src/title_bar.rs @@ -3,6 +3,7 @@ pub mod collab; mod onboarding_banner; mod project_dropdown; mod title_bar_settings; +mod update_version; #[cfg(feature = "stories")] mod stories; @@ -40,6 +41,7 @@ use ui::{ Avatar, ButtonLike, Chip, ContextMenu, IconWithIndicator, Indicator, PopoverMenu, PopoverMenuHandle, TintColor, Tooltip, prelude::*, }; +use update_version::UpdateVersion; use util::ResultExt; use workspace::{SwitchProject, ToggleWorktreeSecurity, Workspace, notifications::NotifyResultExt}; use zed_actions::OpenRemote; @@ -61,7 +63,9 @@ actions!( /// Toggles the project menu dropdown. ToggleProjectMenu, /// Switches to a different git branch. - SwitchBranch + SwitchBranch, + /// A debug action to simulate an update being available to test the update banner UI. + SimulateUpdateAvailable ] ); @@ -75,6 +79,17 @@ pub fn init(cx: &mut App) { let item = cx.new(|cx| TitleBar::new("title-bar", workspace, window, cx)); workspace.set_titlebar_item(item.into(), window, cx); + workspace.register_action(|workspace, _: &SimulateUpdateAvailable, _window, cx| { + if let Some(titlebar) = workspace + .titlebar_item() + .and_then(|item| item.downcast::().ok()) + { + titlebar.update(cx, |titlebar, cx| { + titlebar.toggle_update_simulation(cx); + }); + } + }); + workspace.register_action(|workspace, _: &SwitchProject, window, cx| { if let Some(titlebar) = workspace .titlebar_item() @@ -144,6 +159,7 @@ pub struct TitleBar { application_menu: Option>, _subscriptions: Vec, banner: Entity, + update_version: Entity, screen_share_popover_handle: PopoverMenuHandle, project_dropdown_handle: PopoverMenuHandle, } @@ -213,6 +229,7 @@ impl Render for TitleBar { .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) .children(self.render_call_controls(window, cx)) .children(self.render_connection_status(status, cx)) + .child(self.update_version.clone()) .when( user.is_none() && TitleBarSettings::get_global(cx).show_sign_in, |this| this.child(self.render_sign_in_button(cx)), @@ -338,6 +355,7 @@ impl TitleBar { .visible_when(|cx| !project::DisableAiSettings::get_global(cx).disable_ai) }); + let update_version = cx.new(|cx| UpdateVersion::new(cx)); let platform_titlebar = cx.new(|cx| PlatformTitleBar::new(id, cx)); Self { @@ -349,6 +367,7 @@ impl TitleBar { client, _subscriptions: subscriptions, banner, + update_version, screen_share_popover_handle: PopoverMenuHandle::default(), project_dropdown_handle: PopoverMenuHandle::default(), } @@ -358,6 +377,12 @@ impl TitleBar { self.project.read(cx).visible_worktrees(cx).count() } + fn toggle_update_simulation(&mut self, cx: &mut Context) { + self.update_version + .update(cx, |banner, cx| banner.update_simulation(cx)); + cx.notify(); + } + pub fn show_project_dropdown(&self, window: &mut Window, cx: &mut App) { if self.worktree_count(cx) > 1 { self.project_dropdown_handle.show(window, cx); @@ -927,6 +952,8 @@ impl TitleBar { } pub fn render_user_menu_button(&mut self, cx: &mut Context) -> impl Element { + let show_update_badge = self.update_version.read(cx).show_update_in_menu_bar(); + let user_store = self.user_store.read(cx); let user = user_store.current_user(); @@ -990,6 +1017,27 @@ impl TitleBar { ) .separator() }) + .when(show_update_badge, |this| { + this.custom_entry( + move |_window, _cx| { + h_flex() + .w_full() + .gap_1() + .justify_between() + .child(Label::new("Restart to update Zed").color(Color::Accent)) + .child( + Icon::new(IconName::Download) + .size(IconSize::Small) + .color(Color::Accent), + ) + .into_any_element() + }, + move |_, cx| { + workspace::reload(cx); + }, + ) + .separator() + }) .action("Settings", zed_actions::OpenSettings.boxed_clone()) .action("Keymap", Box::new(zed_actions::OpenKeymap)) .action( @@ -1013,9 +1061,25 @@ impl TitleBar { }) .map(|this| { if is_signed_in && TitleBarSettings::get_global(cx).show_user_picture { + let avatar = + user_avatar + .clone() + .map(|avatar| Avatar::new(avatar)) + .map(|avatar| { + if show_update_badge { + avatar.indicator( + div() + .absolute() + .bottom_0() + .right_0() + .child(Indicator::dot().color(Color::Accent)), + ) + } else { + avatar + } + }); this.trigger_with_tooltip( - ButtonLike::new("user-menu") - .children(user_avatar.clone().map(|avatar| Avatar::new(avatar))), + ButtonLike::new("user-menu").children(avatar), Tooltip::text("Toggle User Menu"), ) } else { diff --git a/crates/title_bar/src/update_version.rs b/crates/title_bar/src/update_version.rs new file mode 100644 index 0000000000000000000000000000000000000000..642187c3aba92a7366d75cb35d7875758097ab13 --- /dev/null +++ b/crates/title_bar/src/update_version.rs @@ -0,0 +1,148 @@ +use std::sync::Arc; + +use anyhow::anyhow; +use auto_update::{AutoUpdateStatus, AutoUpdater, UpdateCheckType, VersionCheckType}; +use gpui::{Empty, Render}; +use semver::Version; +use ui::{UpdateButton, prelude::*}; + +pub struct UpdateVersion { + status: AutoUpdateStatus, + update_check_type: UpdateCheckType, + dismissed: bool, +} + +impl UpdateVersion { + pub fn new(cx: &mut Context) -> Self { + if let Some(auto_updater) = AutoUpdater::get(cx) { + cx.observe(&auto_updater, |this, auto_update, cx| { + this.status = auto_update.read(cx).status(); + this.update_check_type = auto_update.read(cx).update_check_type(); + if this.status.is_updated() { + this.dismissed = false; + } + }) + .detach(); + Self { + status: auto_updater.read(cx).status(), + update_check_type: UpdateCheckType::Automatic, + dismissed: false, + } + } else { + Self { + status: AutoUpdateStatus::Idle, + update_check_type: UpdateCheckType::Automatic, + dismissed: false, + } + } + } + + pub fn update_simulation(&mut self, cx: &mut Context) { + let next_state = match self.status { + AutoUpdateStatus::Idle => AutoUpdateStatus::Checking, + AutoUpdateStatus::Checking => AutoUpdateStatus::Downloading { + version: VersionCheckType::Semantic(Version::new(1, 99, 0)), + }, + AutoUpdateStatus::Downloading { .. } => AutoUpdateStatus::Installing { + version: VersionCheckType::Semantic(Version::new(1, 99, 0)), + }, + AutoUpdateStatus::Installing { .. } => AutoUpdateStatus::Updated { + version: VersionCheckType::Semantic(Version::new(1, 99, 0)), + }, + AutoUpdateStatus::Updated { .. } => AutoUpdateStatus::Errored { + error: Arc::new(anyhow!("Network timeout")), + }, + AutoUpdateStatus::Errored { .. } => AutoUpdateStatus::Idle, + }; + + self.status = next_state; + self.update_check_type = UpdateCheckType::Manual; + self.dismissed = false; + cx.notify() + } + + pub fn show_update_in_menu_bar(&self) -> bool { + self.dismissed && self.status.is_updated() + } + + fn version_tooltip_message(version: &VersionCheckType) -> String { + format!("Version: {}", { + match version { + VersionCheckType::Sha(sha) => format!("{}…", sha.short()), + VersionCheckType::Semantic(semantic_version) => semantic_version.to_string(), + } + }) + } +} + +impl Render for UpdateVersion { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + if self.dismissed { + return Empty.into_any_element(); + } + match &self.status { + AutoUpdateStatus::Checking if self.update_check_type.is_manual() => { + UpdateButton::checking().into_any_element() + } + AutoUpdateStatus::Downloading { version } if self.update_check_type.is_manual() => { + let tooltip = Self::version_tooltip_message(&version); + UpdateButton::downloading(tooltip).into_any_element() + } + AutoUpdateStatus::Installing { version } if self.update_check_type.is_manual() => { + let tooltip = Self::version_tooltip_message(&version); + UpdateButton::installing(tooltip).into_any_element() + } + AutoUpdateStatus::Updated { version } => { + let tooltip = Self::version_tooltip_message(&version); + UpdateButton::updated(tooltip) + .on_click(|_, _, cx| { + workspace::reload(cx); + }) + .on_dismiss(cx.listener(|this, _, _window, cx| { + this.dismissed = true; + cx.notify() + })) + .into_any_element() + } + AutoUpdateStatus::Errored { error } => { + let error_str = error.to_string(); + UpdateButton::errored(error_str) + .on_click(|_, window, cx| { + window.dispatch_action(Box::new(workspace::OpenLog), cx); + }) + .on_dismiss(cx.listener(|this, _, _window, cx| { + this.dismissed = true; + cx.notify() + })) + .into_any_element() + } + AutoUpdateStatus::Idle + | AutoUpdateStatus::Checking { .. } + | AutoUpdateStatus::Downloading { .. } + | AutoUpdateStatus::Installing { .. } => Empty.into_any_element(), + } + } +} +#[cfg(test)] +mod tests { + use auto_update::VersionCheckType; + use release_channel::AppCommitSha; + use semver::Version; + + use super::*; + + #[test] + fn test_version_tooltip_message() { + let message = UpdateVersion::version_tooltip_message(&VersionCheckType::Semantic( + Version::new(1, 0, 0), + )); + + assert_eq!(message, "Version: 1.0.0"); + + let message = UpdateVersion::version_tooltip_message(&VersionCheckType::Sha( + AppCommitSha::new("14d9a4189f058d8736339b06ff2340101eaea5af".to_string()), + )); + + assert_eq!(message, "Version: 14d9a41…"); + } +} diff --git a/crates/ui/src/components/collab.rs b/crates/ui/src/components/collab.rs index 36518679c661627346e45357161ba117a6bfe5b3..d5508920b9dfe9146a77b6d7741a688692e8968e 100644 --- a/crates/ui/src/components/collab.rs +++ b/crates/ui/src/components/collab.rs @@ -1,3 +1,5 @@ mod collab_notification; +mod update_button; pub use collab_notification::*; +pub use update_button::*; diff --git a/crates/ui/src/components/collab/update_button.rs b/crates/ui/src/components/collab/update_button.rs new file mode 100644 index 0000000000000000000000000000000000000000..e65f40a167859d166c98dde3ea84ff3de2bb9959 --- /dev/null +++ b/crates/ui/src/components/collab/update_button.rs @@ -0,0 +1,202 @@ +use gpui::{AnyElement, ClickEvent, prelude::*}; + +use crate::{ButtonLike, CommonAnimationExt, Tooltip, prelude::*}; + +/// A button component displayed in the title bar to show auto-update status. +#[derive(IntoElement, RegisterComponent)] +pub struct UpdateButton { + icon: IconName, + icon_animate: bool, + icon_color: Option, + message: SharedString, + tooltip: Option, + show_dismiss: bool, + on_click: Option>, + on_dismiss: Option>, +} + +impl UpdateButton { + pub fn new(icon: IconName, message: impl Into) -> Self { + Self { + icon, + icon_animate: false, + icon_color: None, + message: message.into(), + tooltip: None, + show_dismiss: false, + on_click: None, + on_dismiss: None, + } + } + + /// Sets whether the icon should have a rotation animation (for progress states). + pub fn icon_animate(mut self, animate: bool) -> Self { + self.icon_animate = animate; + self + } + + /// Sets the icon color (e.g., for warning/error states). + pub fn icon_color(mut self, color: impl Into>) -> Self { + self.icon_color = color.into(); + self + } + + /// Sets the tooltip text shown on hover. + pub fn tooltip(mut self, tooltip: impl Into) -> Self { + self.tooltip = Some(tooltip.into()); + self + } + + /// Shows a dismiss button on the right side. + pub fn with_dismiss(mut self) -> Self { + self.show_dismiss = true; + self + } + + /// Sets the click handler for the main button area. + pub fn on_click( + mut self, + handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.on_click = Some(Box::new(handler)); + self + } + + /// Sets the click handler for the dismiss button. + pub fn on_dismiss( + mut self, + handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, + ) -> Self { + self.on_dismiss = Some(Box::new(handler)); + self + } + + pub fn checking() -> Self { + Self::new(IconName::ArrowCircle, "Checking for Zed updates…").icon_animate(true) + } + + pub fn downloading(version: impl Into) -> Self { + Self::new(IconName::Download, "Downloading Zed update…").tooltip(version) + } + + pub fn installing(version: impl Into) -> Self { + Self::new(IconName::ArrowCircle, "Installing Zed update…") + .icon_animate(true) + .tooltip(version) + } + + pub fn updated(version: impl Into) -> Self { + Self::new(IconName::Download, "Click to restart and update Zed") + .tooltip(version) + .with_dismiss() + } + + pub fn errored(error: impl Into) -> Self { + Self::new(IconName::Warning, "Failed to update Zed") + .icon_color(Color::Warning) + .tooltip(error) + .with_dismiss() + } +} + +impl RenderOnce for UpdateButton { + fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { + let border_color = cx.theme().colors().border; + + let icon = Icon::new(self.icon) + .size(IconSize::XSmall) + .when_some(self.icon_color, |this, color| this.color(color)); + let icon_element = if self.icon_animate { + icon.with_rotate_animation(3).into_any_element() + } else { + icon.into_any_element() + }; + + let tooltip = self.tooltip.clone(); + + h_flex() + .mr_2() + .rounded_sm() + .border_1() + .border_color(border_color) + .child( + ButtonLike::new("update-button") + .child( + h_flex() + .h_full() + .gap_1() + .child(icon_element) + .child(Label::new(self.message).size(LabelSize::Small)), + ) + .when_some(tooltip, |this, tooltip| { + this.tooltip(Tooltip::text(tooltip)) + }) + .when_some(self.on_click, |this, handler| this.on_click(handler)), + ) + .when(self.show_dismiss, |this| { + this.child( + div().border_l_1().border_color(border_color).child( + IconButton::new("dismiss-update-button", IconName::Close) + .icon_size(IconSize::Indicator) + .when_some(self.on_dismiss, |this, handler| this.on_click(handler)) + .tooltip(Tooltip::text("Dismiss")), + ), + ) + }) + } +} + +impl Component for UpdateButton { + fn scope() -> ComponentScope { + ComponentScope::Collaboration + } + + fn name() -> &'static str { + "UpdateButton" + } + + fn description() -> Option<&'static str> { + Some( + "A button component displayed in the title bar to show auto-update status and allow users to restart Zed.", + ) + } + + fn preview(_window: &mut Window, _cx: &mut App) -> Option { + let version = "1.99.0"; + + Some( + v_flex() + .gap_6() + .children(vec![ + example_group_with_title( + "Progress States", + vec![ + single_example("Checking", UpdateButton::checking().into_any_element()), + single_example( + "Downloading", + UpdateButton::downloading(version).into_any_element(), + ), + single_example( + "Installing", + UpdateButton::installing(version).into_any_element(), + ), + ], + ), + example_group_with_title( + "Actionable States", + vec![ + single_example( + "Ready to Update", + UpdateButton::updated(version).into_any_element(), + ), + single_example( + "Error", + UpdateButton::errored("Network timeout").into_any_element(), + ), + ], + ), + ]) + .into_any_element(), + ) + } +}