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