status_toast.rs

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