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(¬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_stack, v_stack, Button, Icon, IconElement, 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_stack()
224 .elevation_3(cx)
225 .p_4()
226 .child(
227 h_stack()
228 .justify_between()
229 .child(div().max_w_80().child(Label::new(self.message.clone())))
230 .child(
231 div()
232 .id("cancel")
233 .child(IconElement::new(Icon::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 // todo!()
251 // impl View for MessageNotification {
252 // fn ui_name() -> &'static str {
253 // "MessageNotification"
254 // }
255
256 // fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
257 // let theme = theme::current(cx).clone();
258 // let theme = &theme.simple_message_notification;
259
260 // enum MessageNotificationTag {}
261
262 // let click_message = self.click_message.clone();
263 // let message = match &self.message {
264 // NotificationMessage::Text(text) => {
265 // Text::new(text.to_owned(), theme.message.text.clone()).into_any()
266 // }
267 // NotificationMessage::Element(e) => e(theme.message.text.clone(), cx),
268 // };
269 // let on_click = self.on_click.clone();
270 // let has_click_action = on_click.is_some();
271
272 // Flex::column()
273 // .with_child(
274 // Flex::row()
275 // .with_child(
276 // message
277 // .contained()
278 // .with_style(theme.message.container)
279 // .aligned()
280 // .top()
281 // .left()
282 // .flex(1., true),
283 // )
284 // .with_child(
285 // MouseEventHandler::new::<Cancel, _>(0, cx, |state, _| {
286 // let style = theme.dismiss_button.style_for(state);
287 // Svg::new("icons/x.svg")
288 // .with_color(style.color)
289 // .constrained()
290 // .with_width(style.icon_width)
291 // .aligned()
292 // .contained()
293 // .with_style(style.container)
294 // .constrained()
295 // .with_width(style.button_width)
296 // .with_height(style.button_width)
297 // })
298 // .with_padding(Padding::uniform(5.))
299 // .on_click(MouseButton::Left, move |_, this, cx| {
300 // this.dismiss(&Default::default(), cx);
301 // })
302 // .with_cursor_style(CursorStyle::PointingHand)
303 // .aligned()
304 // .constrained()
305 // .with_height(cx.font_cache().line_height(theme.message.text.font_size))
306 // .aligned()
307 // .top()
308 // .flex_float(),
309 // ),
310 // )
311 // .with_children({
312 // click_message
313 // .map(|click_message| {
314 // MouseEventHandler::new::<MessageNotificationTag, _>(
315 // 0,
316 // cx,
317 // |state, _| {
318 // let style = theme.action_message.style_for(state);
319
320 // Flex::row()
321 // .with_child(
322 // Text::new(click_message, style.text.clone())
323 // .contained()
324 // .with_style(style.container),
325 // )
326 // .contained()
327 // },
328 // )
329 // .on_click(MouseButton::Left, move |_, this, cx| {
330 // if let Some(on_click) = on_click.as_ref() {
331 // on_click(cx);
332 // this.dismiss(&Default::default(), cx);
333 // }
334 // })
335 // // Since we're not using a proper overlay, we have to capture these extra events
336 // .on_down(MouseButton::Left, |_, _, _| {})
337 // .on_up(MouseButton::Left, |_, _, _| {})
338 // .with_cursor_style(if has_click_action {
339 // CursorStyle::PointingHand
340 // } else {
341 // CursorStyle::Arrow
342 // })
343 // })
344 // .into_iter()
345 // })
346 // .into_any()
347 // }
348 // }
349}
350
351pub trait NotifyResultExt {
352 type Ok;
353
354 fn notify_err(
355 self,
356 workspace: &mut Workspace,
357 cx: &mut ViewContext<Workspace>,
358 ) -> Option<Self::Ok>;
359
360 fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<Self::Ok>;
361}
362
363impl<T, E> NotifyResultExt for Result<T, E>
364where
365 E: std::fmt::Debug,
366{
367 type Ok = T;
368
369 fn notify_err(self, workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<T> {
370 match self {
371 Ok(value) => Some(value),
372 Err(err) => {
373 log::error!("TODO {err:?}");
374 workspace.show_error(&err, cx);
375 None
376 }
377 }
378 }
379
380 fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<T> {
381 match self {
382 Ok(value) => Some(value),
383 Err(err) => {
384 log::error!("TODO {err:?}");
385 cx.update(|view, cx| {
386 if let Ok(workspace) = view.downcast::<Workspace>() {
387 workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
388 }
389 })
390 .ok();
391 None
392 }
393 }
394 }
395}