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