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 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 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 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 .shape(ui::IconButtonShape::Square)
141 .icon_size(IconSize::Small)
142 .icon_color(Color::Muted)
143 .tooltip(Tooltip::text("Dismiss"))
144 .on_click(move |_click_event, _window, cx| {
145 handle.update(cx, |_, cx| {
146 cx.emit(DismissEvent);
147 });
148 }),
149 )
150 })
151 }
152}
153
154impl ToastView for StatusToast {
155 fn action(&self) -> Option<ToastAction> {
156 self.action.clone()
157 }
158}
159
160impl Focusable for StatusToast {
161 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
162 self.focus_handle.clone()
163 }
164}
165
166impl EventEmitter<DismissEvent> for StatusToast {}
167
168impl Component for StatusToast {
169 fn scope() -> ComponentScope {
170 ComponentScope::Notification
171 }
172
173 fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
174 let text_example = StatusToast::new("Operation completed", cx, |this, _| this);
175
176 let action_example = StatusToast::new("Update ready to install", cx, |this, _cx| {
177 this.action("Restart", |_, _| {})
178 });
179
180 let dismiss_button_example =
181 StatusToast::new("Dismiss Button", cx, |this, _| this.dismiss_button(true));
182
183 let icon_example = StatusToast::new(
184 "Nathan Sobo accepted your contact request",
185 cx,
186 |this, _| this.icon(ToastIcon::new(IconName::Check).color(Color::Muted)),
187 );
188
189 let success_example = StatusToast::new("Pushed 4 changes to `zed/main`", cx, |this, _| {
190 this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
191 });
192
193 let error_example = StatusToast::new(
194 "git push: Couldn't find remote origin `iamnbutler/zed`",
195 cx,
196 |this, _cx| {
197 this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
198 .action("More Info", |_, _| {})
199 },
200 );
201
202 let warning_example = StatusToast::new("You have outdated settings", cx, |this, _cx| {
203 this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning))
204 .action("More Info", |_, _| {})
205 });
206
207 let pr_example =
208 StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| {
209 this.icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted))
210 .action("Open Pull Request", |_, cx| {
211 cx.open_url("https://github.com/")
212 })
213 });
214
215 Some(
216 v_flex()
217 .gap_6()
218 .p_4()
219 .children(vec![
220 example_group_with_title(
221 "Basic Toast",
222 vec![
223 single_example("Text", div().child(text_example).into_any_element()),
224 single_example(
225 "Action",
226 div().child(action_example).into_any_element(),
227 ),
228 single_example("Icon", div().child(icon_example).into_any_element()),
229 single_example(
230 "Dismiss Button",
231 div().child(dismiss_button_example).into_any_element(),
232 ),
233 ],
234 ),
235 example_group_with_title(
236 "Examples",
237 vec![
238 single_example(
239 "Success",
240 div().child(success_example).into_any_element(),
241 ),
242 single_example("Error", div().child(error_example).into_any_element()),
243 single_example(
244 "Warning",
245 div().child(warning_example).into_any_element(),
246 ),
247 single_example("Create PR", div().child(pr_example).into_any_element()),
248 ],
249 )
250 .vertical(),
251 ])
252 .into_any_element(),
253 )
254 }
255}