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