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