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}