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(), + ) + } +}