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