1use crate::{Toast, Workspace};
2use collections::HashMap;
3use gpui::{
4 AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Render,
5 Task, View, ViewContext, VisualContext, WindowContext,
6};
7use std::{any::TypeId, ops::DerefMut};
8
9pub fn init(cx: &mut AppContext) {
10 cx.set_global(NotificationTracker::new());
11}
12
13pub trait Notification: EventEmitter<DismissEvent> + Render {}
14
15impl<V: EventEmitter<DismissEvent> + Render> Notification for V {}
16
17pub trait NotificationHandle: Send {
18 fn id(&self) -> EntityId;
19 fn to_any(&self) -> AnyView;
20}
21
22impl<T: Notification> NotificationHandle for View<T> {
23 fn id(&self) -> EntityId {
24 self.entity_id()
25 }
26
27 fn to_any(&self) -> AnyView {
28 self.clone().into()
29 }
30}
31
32impl From<&dyn NotificationHandle> for AnyView {
33 fn from(val: &dyn NotificationHandle) -> Self {
34 val.to_any()
35 }
36}
37
38pub(crate) struct NotificationTracker {
39 notifications_sent: HashMap<TypeId, Vec<usize>>,
40}
41
42impl std::ops::Deref for NotificationTracker {
43 type Target = HashMap<TypeId, Vec<usize>>;
44
45 fn deref(&self) -> &Self::Target {
46 &self.notifications_sent
47 }
48}
49
50impl DerefMut for NotificationTracker {
51 fn deref_mut(&mut self) -> &mut Self::Target {
52 &mut self.notifications_sent
53 }
54}
55
56impl NotificationTracker {
57 fn new() -> Self {
58 Self {
59 notifications_sent: Default::default(),
60 }
61 }
62}
63
64impl Workspace {
65 pub fn has_shown_notification_once<V: Notification>(
66 &self,
67 id: usize,
68 cx: &ViewContext<Self>,
69 ) -> bool {
70 cx.global::<NotificationTracker>()
71 .get(&TypeId::of::<V>())
72 .map(|ids| ids.contains(&id))
73 .unwrap_or(false)
74 }
75
76 pub fn show_notification_once<V: Notification>(
77 &mut self,
78 id: usize,
79 cx: &mut ViewContext<Self>,
80 build_notification: impl FnOnce(&mut ViewContext<Self>) -> View<V>,
81 ) {
82 if !self.has_shown_notification_once::<V>(id, cx) {
83 let tracker = cx.global_mut::<NotificationTracker>();
84 let entry = tracker.entry(TypeId::of::<V>()).or_default();
85 entry.push(id);
86 self.show_notification::<V>(id, cx, build_notification)
87 }
88 }
89
90 pub fn show_notification<V: Notification>(
91 &mut self,
92 id: usize,
93 cx: &mut ViewContext<Self>,
94 build_notification: impl FnOnce(&mut ViewContext<Self>) -> View<V>,
95 ) {
96 let type_id = TypeId::of::<V>();
97 if self
98 .notifications
99 .iter()
100 .all(|(existing_type_id, existing_id, _)| {
101 (*existing_type_id, *existing_id) != (type_id, id)
102 })
103 {
104 let notification = build_notification(cx);
105 cx.subscribe(¬ification, move |this, _, _: &DismissEvent, cx| {
106 this.dismiss_notification_internal(type_id, id, cx);
107 })
108 .detach();
109 self.notifications
110 .push((type_id, id, Box::new(notification)));
111 cx.notify();
112 }
113 }
114
115 pub fn show_error<E>(&mut self, err: &E, cx: &mut ViewContext<Self>)
116 where
117 E: std::fmt::Debug,
118 {
119 self.show_notification(0, cx, |cx| {
120 cx.new_view(|_cx| {
121 simple_message_notification::MessageNotification::new(format!("Error: {err:?}"))
122 })
123 });
124 }
125
126 pub fn dismiss_notification<V: Notification>(&mut self, id: usize, cx: &mut ViewContext<Self>) {
127 let type_id = TypeId::of::<V>();
128
129 self.dismiss_notification_internal(type_id, id, cx)
130 }
131
132 pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
133 self.dismiss_notification::<simple_message_notification::MessageNotification>(toast.id, cx);
134 self.show_notification(toast.id, cx, |cx| {
135 cx.new_view(|_cx| match toast.on_click.as_ref() {
136 Some((click_msg, on_click)) => {
137 let on_click = on_click.clone();
138 simple_message_notification::MessageNotification::new(toast.msg.clone())
139 .with_click_message(click_msg.clone())
140 .on_click(move |cx| on_click(cx))
141 }
142 None => simple_message_notification::MessageNotification::new(toast.msg.clone()),
143 })
144 })
145 }
146
147 pub fn dismiss_toast(&mut self, id: usize, cx: &mut ViewContext<Self>) {
148 self.dismiss_notification::<simple_message_notification::MessageNotification>(id, cx);
149 }
150
151 fn dismiss_notification_internal(
152 &mut self,
153 type_id: TypeId,
154 id: usize,
155 cx: &mut ViewContext<Self>,
156 ) {
157 self.notifications
158 .retain(|(existing_type_id, existing_id, _)| {
159 if (*existing_type_id, *existing_id) == (type_id, id) {
160 cx.notify();
161 false
162 } else {
163 true
164 }
165 });
166 }
167}
168
169pub mod simple_message_notification {
170 use gpui::{
171 div, DismissEvent, EventEmitter, InteractiveElement, ParentElement, Render, SharedString,
172 StatefulInteractiveElement, Styled, ViewContext,
173 };
174 use std::sync::Arc;
175 use ui::prelude::*;
176 use ui::{h_stack, v_stack, Button, Icon, IconName, Label, StyledExt};
177
178 pub struct MessageNotification {
179 message: SharedString,
180 on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
181 click_message: Option<SharedString>,
182 }
183
184 impl EventEmitter<DismissEvent> for MessageNotification {}
185
186 impl MessageNotification {
187 pub fn new<S>(message: S) -> MessageNotification
188 where
189 S: Into<SharedString>,
190 {
191 Self {
192 message: message.into(),
193 on_click: None,
194 click_message: None,
195 }
196 }
197
198 pub fn with_click_message<S>(mut self, message: S) -> Self
199 where
200 S: Into<SharedString>,
201 {
202 self.click_message = Some(message.into());
203 self
204 }
205
206 pub fn on_click<F>(mut self, on_click: F) -> Self
207 where
208 F: 'static + Fn(&mut ViewContext<Self>),
209 {
210 self.on_click = Some(Arc::new(on_click));
211 self
212 }
213
214 pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
215 cx.emit(DismissEvent);
216 }
217 }
218
219 impl Render for MessageNotification {
220 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
221 v_stack()
222 .elevation_3(cx)
223 .p_4()
224 .child(
225 h_stack()
226 .justify_between()
227 .child(div().max_w_80().child(Label::new(self.message.clone())))
228 .child(
229 div()
230 .id("cancel")
231 .child(Icon::new(IconName::Close))
232 .cursor_pointer()
233 .on_click(cx.listener(|this, _, cx| this.dismiss(cx))),
234 ),
235 )
236 .children(self.click_message.iter().map(|message| {
237 Button::new(message.clone(), message.clone()).on_click(cx.listener(
238 |this, _, cx| {
239 if let Some(on_click) = this.on_click.as_ref() {
240 (on_click)(cx)
241 };
242 this.dismiss(cx)
243 },
244 ))
245 }))
246 }
247 }
248}
249
250pub trait NotifyResultExt {
251 type Ok;
252
253 fn notify_err(
254 self,
255 workspace: &mut Workspace,
256 cx: &mut ViewContext<Workspace>,
257 ) -> Option<Self::Ok>;
258
259 fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<Self::Ok>;
260}
261
262impl<T, E> NotifyResultExt for Result<T, E>
263where
264 E: std::fmt::Debug,
265{
266 type Ok = T;
267
268 fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<T> {
269 match self {
270 Ok(value) => Some(value),
271 Err(err) => {
272 log::error!("TODO {err:?}");
273 workspace.show_error(&err, cx);
274 None
275 }
276 }
277 }
278
279 fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<T> {
280 match self {
281 Ok(value) => Some(value),
282 Err(err) => {
283 log::error!("TODO {err:?}");
284 cx.update(|view, cx| {
285 if let Ok(workspace) = view.downcast::<Workspace>() {
286 workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
287 }
288 })
289 .ok();
290 None
291 }
292 }
293 }
294}
295
296pub trait NotifyTaskExt {
297 fn detach_and_notify_err(self, cx: &mut WindowContext);
298}
299
300impl<R, E> NotifyTaskExt for Task<Result<R, E>>
301where
302 E: std::fmt::Debug + 'static,
303 R: 'static,
304{
305 fn detach_and_notify_err(self, cx: &mut WindowContext) {
306 cx.spawn(|mut cx| async move { self.await.notify_async_err(&mut cx) })
307 .detach();
308 }
309}