status_toast.rs

  1use std::rc::Rc;
  2
  3use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement};
  4use ui::{Tooltip, prelude::*};
  5use workspace::{ToastAction, ToastView};
  6use zed_actions::toast;
  7
  8#[derive(Clone, Copy)]
  9pub struct ToastIcon {
 10    icon: IconName,
 11    color: Color,
 12}
 13
 14impl ToastIcon {
 15    pub fn new(icon: IconName) -> Self {
 16        Self {
 17            icon,
 18            color: Color::default(),
 19        }
 20    }
 21
 22    pub fn color(mut self, color: Color) -> Self {
 23        self.color = color;
 24        self
 25    }
 26}
 27
 28impl From<IconName> for ToastIcon {
 29    fn from(icon: IconName) -> Self {
 30        Self {
 31            icon,
 32            color: Color::default(),
 33        }
 34    }
 35}
 36
 37#[derive(RegisterComponent)]
 38pub struct StatusToast {
 39    icon: Option<ToastIcon>,
 40    text: SharedString,
 41    action: Option<ToastAction>,
 42    this_handle: Entity<Self>,
 43    focus_handle: FocusHandle,
 44}
 45
 46impl StatusToast {
 47    pub fn new(
 48        text: impl Into<SharedString>,
 49        cx: &mut App,
 50        f: impl FnOnce(Self, &mut Context<Self>) -> Self,
 51    ) -> Entity<Self> {
 52        cx.new(|cx| {
 53            let focus_handle = cx.focus_handle();
 54
 55            f(
 56                Self {
 57                    text: text.into(),
 58                    icon: None,
 59                    action: None,
 60                    this_handle: cx.entity(),
 61                    focus_handle,
 62                },
 63                cx,
 64            )
 65        })
 66    }
 67
 68    pub fn icon(mut self, icon: ToastIcon) -> Self {
 69        self.icon = Some(icon);
 70        self
 71    }
 72
 73    pub fn action(
 74        mut self,
 75        label: impl Into<SharedString>,
 76        f: impl Fn(&mut Window, &mut App) + 'static,
 77    ) -> Self {
 78        let this_handle = self.this_handle.clone();
 79        self.action = Some(ToastAction::new(
 80            label.into(),
 81            Some(Rc::new(move |window, cx| {
 82                this_handle.update(cx, |_, cx| {
 83                    cx.emit(DismissEvent);
 84                });
 85                f(window, cx);
 86            })),
 87        ));
 88        self
 89    }
 90}
 91
 92impl Render for StatusToast {
 93    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 94        h_flex()
 95            .id("status-toast")
 96            .elevation_3(cx)
 97            .gap_2()
 98            .py_1p5()
 99            .px_2p5()
100            .flex_none()
101            .bg(cx.theme().colors().surface_background)
102            .shadow_lg()
103            .items_center()
104            .when_some(self.icon.as_ref(), |this, icon| {
105                this.child(Icon::new(icon.icon).color(icon.color))
106            })
107            .child(Label::new(self.text.clone()).color(Color::Default))
108            .when_some(self.action.as_ref(), |this, action| {
109                this.child(
110                    Button::new(action.id.clone(), action.label.clone())
111                        .tooltip(Tooltip::for_action_title(
112                            action.label.clone(),
113                            &toast::RunAction,
114                        ))
115                        .color(Color::Muted)
116                        .when_some(action.on_click.clone(), |el, handler| {
117                            el.on_click(move |_click_event, window, cx| handler(window, cx))
118                        }),
119                )
120            })
121    }
122}
123
124impl ToastView for StatusToast {
125    fn action(&self) -> Option<ToastAction> {
126        self.action.clone()
127    }
128}
129
130impl Focusable for StatusToast {
131    fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
132        self.focus_handle.clone()
133    }
134}
135
136impl EventEmitter<DismissEvent> for StatusToast {}
137
138impl Component for StatusToast {
139    fn scope() -> ComponentScope {
140        ComponentScope::Notification
141    }
142
143    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
144        let text_example = StatusToast::new("Operation completed", cx, |this, _| this);
145
146        let action_example = StatusToast::new("Update ready to install", cx, |this, _cx| {
147            this.action("Restart", |_, _| {})
148        });
149
150        let icon_example = StatusToast::new(
151            "Nathan Sobo accepted your contact request",
152            cx,
153            |this, _| this.icon(ToastIcon::new(IconName::Check).color(Color::Muted)),
154        );
155
156        let success_example = StatusToast::new("Pushed 4 changes to `zed/main`", cx, |this, _| {
157            this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
158        });
159
160        let error_example = StatusToast::new(
161            "git push: Couldn't find remote origin `iamnbutler/zed`",
162            cx,
163            |this, _cx| {
164                this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
165                    .action("More Info", |_, _| {})
166            },
167        );
168
169        let warning_example = StatusToast::new("You have outdated settings", cx, |this, _cx| {
170            this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning))
171                .action("More Info", |_, _| {})
172        });
173
174        let pr_example =
175            StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| {
176                this.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
177                    .action("Open Pull Request", |_, cx| {
178                        cx.open_url("https://github.com/")
179                    })
180            });
181
182        Some(
183            v_flex()
184                .gap_6()
185                .p_4()
186                .children(vec![
187                    example_group_with_title(
188                        "Basic Toast",
189                        vec![
190                            single_example("Text", div().child(text_example).into_any_element()),
191                            single_example(
192                                "Action",
193                                div().child(action_example).into_any_element(),
194                            ),
195                            single_example("Icon", div().child(icon_example).into_any_element()),
196                        ],
197                    ),
198                    example_group_with_title(
199                        "Examples",
200                        vec![
201                            single_example(
202                                "Success",
203                                div().child(success_example).into_any_element(),
204                            ),
205                            single_example("Error", div().child(error_example).into_any_element()),
206                            single_example(
207                                "Warning",
208                                div().child(warning_example).into_any_element(),
209                            ),
210                            single_example("Create PR", div().child(pr_example).into_any_element()),
211                        ],
212                    )
213                    .vertical(),
214                ])
215                .into_any_element(),
216        )
217    }
218}