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}