status_toast.rs

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