use std::rc::Rc; use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement}; use ui::{Tooltip, prelude::*}; use workspace::{ToastAction, ToastView}; use zed_actions::toast; #[derive(Clone, Copy)] pub struct ToastIcon { icon: IconName, color: Color, } impl ToastIcon { pub fn new(icon: IconName) -> Self { Self { icon, color: Color::default(), } } pub fn color(mut self, color: Color) -> Self { self.color = color; self } } impl From for ToastIcon { fn from(icon: IconName) -> Self { Self { icon, color: Color::default(), } } } #[derive(RegisterComponent)] pub struct StatusToast { icon: Option, text: SharedString, action: Option, show_dismiss: bool, this_handle: Entity, focus_handle: FocusHandle, } impl StatusToast { pub fn new( text: impl Into, cx: &mut App, f: impl FnOnce(Self, &mut Context) -> Self, ) -> Entity { cx.new(|cx| { let focus_handle = cx.focus_handle(); f( Self { text: text.into(), icon: None, action: None, show_dismiss: false, this_handle: cx.entity(), focus_handle, }, cx, ) }) } pub fn icon(mut self, icon: ToastIcon) -> Self { self.icon = Some(icon); self } pub fn action( mut self, label: impl Into, f: impl Fn(&mut Window, &mut App) + 'static, ) -> Self { let this_handle = self.this_handle.clone(); self.action = Some(ToastAction::new( label.into(), Some(Rc::new(move |window, cx| { this_handle.update(cx, |_, cx| { cx.emit(DismissEvent); }); f(window, cx); })), )); self } pub fn dismiss_button(mut self, show: bool) -> Self { self.show_dismiss = show; self } } impl Render for StatusToast { fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { let has_action_or_dismiss = self.action.is_some() || self.show_dismiss; h_flex() .id("status-toast") .elevation_3(cx) .gap_2() .py_1p5() .pl_2p5() .map(|this| { if has_action_or_dismiss { this.pr_1p5() } else { this.pr_2p5() } }) .flex_none() .bg(cx.theme().colors().surface_background) .shadow_lg() .when_some(self.icon.as_ref(), |this, icon| { this.child(Icon::new(icon.icon).color(icon.color)) }) .child(Label::new(self.text.clone()).color(Color::Default)) .when_some(self.action.as_ref(), |this, action| { this.child( Button::new(action.id.clone(), action.label.clone(), cx) .tooltip(Tooltip::for_action_title( action.label.clone(), &toast::RunAction, )) .color(Color::Muted) .when_some(action.on_click.clone(), |el, handler| { el.on_click(move |_click_event, window, cx| handler(window, cx)) }), ) }) .when(self.show_dismiss, |this| { let handle = self.this_handle.clone(); this.child( IconButton::new("dismiss", IconName::Close) .icon_size(IconSize::XSmall) .icon_color(Color::Muted) .tooltip(Tooltip::text("Dismiss")) .on_click(move |_click_event, _window, cx| { handle.update(cx, |_, cx| { cx.emit(DismissEvent); }); }), ) }) } } impl ToastView for StatusToast { fn action(&self) -> Option { self.action.clone() } } impl Focusable for StatusToast { fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle { self.focus_handle.clone() } } impl EventEmitter for StatusToast {} impl Component for StatusToast { fn scope() -> ComponentScope { ComponentScope::Notification } fn preview(_window: &mut Window, cx: &mut App) -> Option { let text_example = StatusToast::new("Operation completed", cx, |this, _| this); let action_example = StatusToast::new("Update ready to install", cx, |this, _cx| { this.action("Restart", |_, _| {}) }); let dismiss_button_example = StatusToast::new("Dismiss Button", cx, |this, _| this.dismiss_button(true)); let icon_example = StatusToast::new( "Nathan Sobo accepted your contact request", cx, |this, _| this.icon(ToastIcon::new(IconName::Check).color(Color::Muted)), ); let success_example = StatusToast::new("Pushed 4 changes to `zed/main`", cx, |this, _| { this.icon(ToastIcon::new(IconName::Check).color(Color::Success)) }); let error_example = StatusToast::new( "git push: Couldn't find remote origin `iamnbutler/zed`", cx, |this, _cx| { this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error)) .action("More Info", |_, _| {}) }, ); let warning_example = StatusToast::new("You have outdated settings", cx, |this, _cx| { this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning)) .action("More Info", |_, _| {}) }); let pr_example = StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| { this.icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)) .action("Open Pull Request", |_, cx| { cx.open_url("https://github.com/") }) }); Some( v_flex() .gap_6() .p_4() .children(vec![ example_group_with_title( "Basic Toast", vec![ single_example("Text", div().child(text_example).into_any_element()), single_example( "Action", div().child(action_example).into_any_element(), ), single_example("Icon", div().child(icon_example).into_any_element()), single_example( "Dismiss Button", div().child(dismiss_button_example).into_any_element(), ), ], ), example_group_with_title( "Examples", vec![ single_example( "Success", div().child(success_example).into_any_element(), ), single_example("Error", div().child(error_example).into_any_element()), single_example( "Warning", div().child(warning_example).into_any_element(), ), single_example("Create PR", div().child(pr_example).into_any_element()), ], ) .vertical(), ]) .into_any_element(), ) } }