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