1use crate::{Toast, Workspace};
2use collections::HashMap;
3use gpui::{
4 AnyView, AppContext, AsyncWindowContext, DismissEvent, Entity, EntityId, EventEmitter, Render,
5 View, ViewContext, VisualContext,
6};
7use std::{any::TypeId, ops::DerefMut};
8
9pub fn init(cx: &mut AppContext) {
10 cx.set_global(NotificationTracker::new());
11 // todo!()
12 // simple_message_notification::init(cx);
13}
14
15pub trait Notification: EventEmitter<DismissEvent> + Render {}
16
17impl<V: EventEmitter<DismissEvent> + Render> Notification for V {}
18
19pub trait NotificationHandle: Send {
20 fn id(&self) -> EntityId;
21 fn to_any(&self) -> AnyView;
22}
23
24impl<T: Notification> NotificationHandle for View<T> {
25 fn id(&self) -> EntityId {
26 self.entity_id()
27 }
28
29 fn to_any(&self) -> AnyView {
30 self.clone().into()
31 }
32}
33
34impl From<&dyn NotificationHandle> for AnyView {
35 fn from(val: &dyn NotificationHandle) -> Self {
36 val.to_any()
37 }
38}
39
40pub(crate) struct NotificationTracker {
41 notifications_sent: HashMap<TypeId, Vec<usize>>,
42}
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(
108 ¬ification,
109 move |this, handle, event: &DismissEvent, cx| match event {
110 DismissEvent::Dismiss => {
111 this.dismiss_notification_internal(type_id, id, cx);
112 }
113 },
114 )
115 .detach();
116 self.notifications
117 .push((type_id, id, Box::new(notification)));
118 cx.notify();
119 }
120 }
121
122 pub fn show_error<E>(&mut self, err: &E, cx: &mut ViewContext<Self>)
123 where
124 E: std::fmt::Debug,
125 {
126 self.show_notification(0, cx, |cx| {
127 cx.build_view(|_cx| {
128 simple_message_notification::MessageNotification::new(format!("Error: {err:?}"))
129 })
130 });
131 }
132
133 pub fn dismiss_notification<V: Notification>(&mut self, id: usize, cx: &mut ViewContext<Self>) {
134 let type_id = TypeId::of::<V>();
135
136 self.dismiss_notification_internal(type_id, id, cx)
137 }
138
139 pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
140 todo!()
141 // self.dismiss_notification::<simple_message_notification::MessageNotification>(toast.id, cx);
142 // self.show_notification(toast.id, cx, |cx| {
143 // cx.add_view(|_cx| match toast.on_click.as_ref() {
144 // Some((click_msg, on_click)) => {
145 // let on_click = on_click.clone();
146 // simple_message_notification::MessageNotification::new(toast.msg.clone())
147 // .with_click_message(click_msg.clone())
148 // .on_click(move |cx| on_click(cx))
149 // }
150 // None => simple_message_notification::MessageNotification::new(toast.msg.clone()),
151 // })
152 // })
153 }
154
155 pub fn dismiss_toast(&mut self, id: usize, cx: &mut ViewContext<Self>) {
156 todo!()
157 // self.dismiss_notification::<simple_message_notification::MessageNotification>(id, cx);
158 }
159
160 fn dismiss_notification_internal(
161 &mut self,
162 type_id: TypeId,
163 id: usize,
164 cx: &mut ViewContext<Self>,
165 ) {
166 self.notifications
167 .retain(|(existing_type_id, existing_id, _)| {
168 if (*existing_type_id, *existing_id) == (type_id, id) {
169 cx.notify();
170 false
171 } else {
172 true
173 }
174 });
175 }
176}
177
178pub mod simple_message_notification {
179 use gpui::{
180 div, AnyElement, AppContext, DismissEvent, Div, EventEmitter, InteractiveElement,
181 ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, TextStyle,
182 ViewContext,
183 };
184 use serde::Deserialize;
185 use std::{borrow::Cow, sync::Arc};
186 use ui::{h_stack, v_stack, Button, Icon, IconElement, Label, StyledExt};
187
188 #[derive(Clone, Default, Deserialize, PartialEq)]
189 pub struct OsOpen(pub Cow<'static, str>);
190
191 impl OsOpen {
192 pub fn new<I: Into<Cow<'static, str>>>(url: I) -> Self {
193 OsOpen(url.into())
194 }
195 }
196
197 // todo!()
198 // impl_actions!(message_notifications, [OsOpen]);
199 //
200 // todo!()
201 // pub fn init(cx: &mut AppContext) {
202 // cx.add_action(MessageNotification::dismiss);
203 // cx.add_action(
204 // |_workspace: &mut Workspace, open_action: &OsOpen, cx: &mut ViewContext<Workspace>| {
205 // cx.platform().open_url(open_action.0.as_ref());
206 // },
207 // )
208 // }
209
210 enum NotificationMessage {
211 Text(SharedString),
212 Element(fn(TextStyle, &AppContext) -> AnyElement),
213 }
214
215 pub struct MessageNotification {
216 message: NotificationMessage,
217 on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>) + Send + Sync>>,
218 click_message: Option<SharedString>,
219 }
220
221 impl EventEmitter<DismissEvent> for MessageNotification {}
222
223 impl MessageNotification {
224 pub fn new<S>(message: S) -> MessageNotification
225 where
226 S: Into<SharedString>,
227 {
228 Self {
229 message: NotificationMessage::Text(message.into()),
230 on_click: None,
231 click_message: None,
232 }
233 }
234
235 // not needed I think (only for the "new panel" toast, which is outdated now)
236 // pub fn new_element(
237 // message: fn(TextStyle, &AppContext) -> AnyElement,
238 // ) -> MessageNotification {
239 // Self {
240 // message: NotificationMessage::Element(message),
241 // on_click: None,
242 // click_message: None,
243 // }
244 // }
245
246 pub fn with_click_message<S>(mut self, message: S) -> Self
247 where
248 S: Into<SharedString>,
249 {
250 self.click_message = Some(message.into());
251 self
252 }
253
254 pub fn on_click<F>(mut self, on_click: F) -> Self
255 where
256 F: 'static + Send + Sync + Fn(&mut ViewContext<Self>),
257 {
258 self.on_click = Some(Arc::new(on_click));
259 self
260 }
261
262 pub fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
263 cx.emit(DismissEvent::Dismiss);
264 }
265 }
266
267 impl Render for MessageNotification {
268 type Element = Div;
269
270 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
271 v_stack()
272 .elevation_3(cx)
273 .p_4()
274 .child(
275 h_stack()
276 .justify_between()
277 .child(div().max_w_80().child(match &self.message {
278 NotificationMessage::Text(text) => Label::new(text.clone()),
279 NotificationMessage::Element(element) => {
280 todo!()
281 }
282 }))
283 .child(
284 div()
285 .id("cancel")
286 .child(IconElement::new(Icon::Close))
287 .cursor_pointer()
288 .on_click(cx.listener(|this, event, cx| this.dismiss(cx))),
289 ),
290 )
291 .children(self.click_message.iter().map(|message| {
292 Button::new(message.clone()).on_click(cx.listener(|this, _, cx| {
293 if let Some(on_click) = this.on_click.as_ref() {
294 (on_click)(cx)
295 };
296 this.dismiss(cx)
297 }))
298 }))
299 }
300 }
301 // todo!()
302 // impl View for MessageNotification {
303 // fn ui_name() -> &'static str {
304 // "MessageNotification"
305 // }
306
307 // fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
308 // let theme = theme2::current(cx).clone();
309 // let theme = &theme.simple_message_notification;
310
311 // enum MessageNotificationTag {}
312
313 // let click_message = self.click_message.clone();
314 // let message = match &self.message {
315 // NotificationMessage::Text(text) => {
316 // Text::new(text.to_owned(), theme.message.text.clone()).into_any()
317 // }
318 // NotificationMessage::Element(e) => e(theme.message.text.clone(), cx),
319 // };
320 // let on_click = self.on_click.clone();
321 // let has_click_action = on_click.is_some();
322
323 // Flex::column()
324 // .with_child(
325 // Flex::row()
326 // .with_child(
327 // message
328 // .contained()
329 // .with_style(theme.message.container)
330 // .aligned()
331 // .top()
332 // .left()
333 // .flex(1., true),
334 // )
335 // .with_child(
336 // MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| {
337 // let style = theme.dismiss_button.style_for(state);
338 // Svg::new("icons/x.svg")
339 // .with_color(style.color)
340 // .constrained()
341 // .with_width(style.icon_width)
342 // .aligned()
343 // .contained()
344 // .with_style(style.container)
345 // .constrained()
346 // .with_width(style.button_width)
347 // .with_height(style.button_width)
348 // })
349 // .with_padding(Padding::uniform(5.))
350 // .on_click(MouseButton::Left, move |_, this, cx| {
351 // this.dismiss(&Default::default(), cx);
352 // })
353 // .with_cursor_style(CursorStyle::PointingHand)
354 // .aligned()
355 // .constrained()
356 // .with_height(cx.font_cache().line_height(theme.message.text.font_size))
357 // .aligned()
358 // .top()
359 // .flex_float(),
360 // ),
361 // )
362 // .with_children({
363 // click_message
364 // .map(|click_message| {
365 // MouseEventHandler::new::<MessageNotificationTag, _>(
366 // 0,
367 // cx,
368 // |state, _| {
369 // let style = theme.action_message.style_for(state);
370
371 // Flex::row()
372 // .with_child(
373 // Text::new(click_message, style.text.clone())
374 // .contained()
375 // .with_style(style.container),
376 // )
377 // .contained()
378 // },
379 // )
380 // .on_click(MouseButton::Left, move |_, this, cx| {
381 // if let Some(on_click) = on_click.as_ref() {
382 // on_click(cx);
383 // this.dismiss(&Default::default(), cx);
384 // }
385 // })
386 // // Since we're not using a proper overlay, we have to capture these extra events
387 // .on_down(MouseButton::Left, |_, _, _| {})
388 // .on_up(MouseButton::Left, |_, _, _| {})
389 // .with_cursor_style(if has_click_action {
390 // CursorStyle::PointingHand
391 // } else {
392 // CursorStyle::Arrow
393 // })
394 // })
395 // .into_iter()
396 // })
397 // .into_any()
398 // }
399 // }
400}
401
402pub trait NotifyResultExt {
403 type Ok;
404
405 fn notify_err(
406 self,
407 workspace: &mut Workspace,
408 cx: &mut ViewContext<Workspace>,
409 ) -> Option<Self::Ok>;
410
411 fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<Self::Ok>;
412}
413
414impl<T, E> NotifyResultExt for Result<T, E>
415where
416 E: std::fmt::Debug,
417{
418 type Ok = T;
419
420 fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<T> {
421 match self {
422 Ok(value) => Some(value),
423 Err(err) => {
424 log::error!("TODO {err:?}");
425 workspace.show_error(&err, cx);
426 None
427 }
428 }
429 }
430
431 fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<T> {
432 match self {
433 Ok(value) => Some(value),
434 Err(err) => {
435 log::error!("TODO {err:?}");
436 cx.update(|view, cx| {
437 if let Ok(workspace) = view.downcast::<Workspace>() {
438 workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
439 }
440 })
441 .ok();
442 None
443 }
444 }
445 }
446}