1use crate::{Toast, Workspace};
2use collections::HashSet;
3use gpui::{AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle};
4use std::{any::TypeId, ops::DerefMut};
5
6pub fn init(cx: &mut AppContext) {
7 cx.set_global(NotificationTracker::new());
8 simple_message_notification::init(cx);
9}
10
11pub trait Notification: View {
12 fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool;
13}
14
15pub trait NotificationHandle {
16 fn id(&self) -> usize;
17 fn as_any(&self) -> &AnyViewHandle;
18}
19
20impl<T: Notification> NotificationHandle for ViewHandle<T> {
21 fn id(&self) -> usize {
22 self.id()
23 }
24
25 fn as_any(&self) -> &AnyViewHandle {
26 self
27 }
28}
29
30impl From<&dyn NotificationHandle> for AnyViewHandle {
31 fn from(val: &dyn NotificationHandle) -> Self {
32 val.as_any().clone()
33 }
34}
35
36struct NotificationTracker {
37 notifications_sent: HashSet<TypeId>,
38}
39
40impl std::ops::Deref for NotificationTracker {
41 type Target = HashSet<TypeId>;
42
43 fn deref(&self) -> &Self::Target {
44 &self.notifications_sent
45 }
46}
47
48impl DerefMut for NotificationTracker {
49 fn deref_mut(&mut self) -> &mut Self::Target {
50 &mut self.notifications_sent
51 }
52}
53
54impl NotificationTracker {
55 fn new() -> Self {
56 Self {
57 notifications_sent: HashSet::default(),
58 }
59 }
60}
61
62impl Workspace {
63 pub fn show_notification_once<V: Notification>(
64 &mut self,
65 id: usize,
66 cx: &mut ViewContext<Self>,
67 build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
68 ) {
69 if !cx
70 .global::<NotificationTracker>()
71 .contains(&TypeId::of::<V>())
72 {
73 cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
74 tracker.insert(TypeId::of::<V>())
75 });
76
77 self.show_notification::<V>(id, cx, build_notification)
78 }
79 }
80
81 pub fn show_notification<V: Notification>(
82 &mut self,
83 id: usize,
84 cx: &mut ViewContext<Self>,
85 build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
86 ) {
87 let type_id = TypeId::of::<V>();
88 if self
89 .notifications
90 .iter()
91 .all(|(existing_type_id, existing_id, _)| {
92 (*existing_type_id, *existing_id) != (type_id, id)
93 })
94 {
95 let notification = build_notification(cx);
96 cx.subscribe(¬ification, move |this, handle, event, cx| {
97 if handle.read(cx).should_dismiss_notification_on_event(event) {
98 this.dismiss_notification_internal(type_id, id, cx);
99 }
100 })
101 .detach();
102 self.notifications
103 .push((type_id, id, Box::new(notification)));
104 cx.notify();
105 }
106 }
107
108 pub fn dismiss_notification<V: Notification>(&mut self, id: usize, cx: &mut ViewContext<Self>) {
109 let type_id = TypeId::of::<V>();
110
111 self.dismiss_notification_internal(type_id, id, cx)
112 }
113
114 pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
115 self.dismiss_notification::<simple_message_notification::MessageNotification>(toast.id, cx);
116 self.show_notification(toast.id, cx, |cx| {
117 cx.add_view(|_cx| match &toast.click {
118 Some((click_msg, action)) => {
119 simple_message_notification::MessageNotification::new_boxed_action(
120 toast.msg.clone(),
121 action.boxed_clone(),
122 click_msg.clone(),
123 )
124 }
125 None => {
126 simple_message_notification::MessageNotification::new_message(toast.msg.clone())
127 }
128 })
129 })
130 }
131
132 pub fn dismiss_toast(&mut self, id: usize, cx: &mut ViewContext<Self>) {
133 self.dismiss_notification::<simple_message_notification::MessageNotification>(id, cx);
134 }
135
136 fn dismiss_notification_internal(
137 &mut self,
138 type_id: TypeId,
139 id: usize,
140 cx: &mut ViewContext<Self>,
141 ) {
142 self.notifications
143 .retain(|(existing_type_id, existing_id, _)| {
144 if (*existing_type_id, *existing_id) == (type_id, id) {
145 cx.notify();
146 false
147 } else {
148 true
149 }
150 });
151 }
152}
153
154pub mod simple_message_notification {
155
156 use std::borrow::Cow;
157
158 use gpui::{
159 actions,
160 elements::{Flex, MouseEventHandler, Padding, ParentElement, Svg, Text},
161 impl_actions,
162 platform::{CursorStyle, MouseButton},
163 Action, AppContext, Element, Entity, View, ViewContext,
164 };
165 use menu::Cancel;
166 use serde::Deserialize;
167 use settings::Settings;
168
169 use crate::Workspace;
170
171 use super::Notification;
172
173 actions!(message_notifications, [CancelMessageNotification]);
174
175 #[derive(Clone, Default, Deserialize, PartialEq)]
176 pub struct OsOpen(pub Cow<'static, str>);
177
178 impl OsOpen {
179 pub fn new<I: Into<Cow<'static, str>>>(url: I) -> Self {
180 OsOpen(url.into())
181 }
182 }
183
184 impl_actions!(message_notifications, [OsOpen]);
185
186 pub fn init(cx: &mut AppContext) {
187 cx.add_action(MessageNotification::dismiss);
188 cx.add_action(
189 |_workspace: &mut Workspace, open_action: &OsOpen, cx: &mut ViewContext<Workspace>| {
190 cx.platform().open_url(open_action.0.as_ref());
191 },
192 )
193 }
194
195 pub struct MessageNotification {
196 message: Cow<'static, str>,
197 click_action: Option<Box<dyn Action>>,
198 click_message: Option<Cow<'static, str>>,
199 }
200
201 pub enum MessageNotificationEvent {
202 Dismiss,
203 }
204
205 impl Entity for MessageNotification {
206 type Event = MessageNotificationEvent;
207 }
208
209 impl MessageNotification {
210 pub fn new_message<S: Into<Cow<'static, str>>>(message: S) -> MessageNotification {
211 Self {
212 message: message.into(),
213 click_action: None,
214 click_message: None,
215 }
216 }
217
218 pub fn new_boxed_action<S1: Into<Cow<'static, str>>, S2: Into<Cow<'static, str>>>(
219 message: S1,
220 click_action: Box<dyn Action>,
221 click_message: S2,
222 ) -> Self {
223 Self {
224 message: message.into(),
225 click_action: Some(click_action),
226 click_message: Some(click_message.into()),
227 }
228 }
229
230 pub fn new<S1: Into<Cow<'static, str>>, A: Action, S2: Into<Cow<'static, str>>>(
231 message: S1,
232 click_action: A,
233 click_message: S2,
234 ) -> Self {
235 Self {
236 message: message.into(),
237 click_action: Some(Box::new(click_action) as Box<dyn Action>),
238 click_message: Some(click_message.into()),
239 }
240 }
241
242 pub fn dismiss(&mut self, _: &CancelMessageNotification, cx: &mut ViewContext<Self>) {
243 cx.emit(MessageNotificationEvent::Dismiss);
244 }
245 }
246
247 impl View for MessageNotification {
248 fn ui_name() -> &'static str {
249 "MessageNotification"
250 }
251
252 fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> gpui::AnyElement<Self> {
253 let theme = cx.global::<Settings>().theme.clone();
254 let theme = &theme.simple_message_notification;
255
256 enum MessageNotificationTag {}
257
258 let click_action = self
259 .click_action
260 .as_ref()
261 .map(|action| action.boxed_clone());
262 let click_message = self.click_message.as_ref().map(|message| message.clone());
263 let message = self.message.clone();
264
265 let has_click_action = click_action.is_some();
266
267 MouseEventHandler::<MessageNotificationTag, _>::new(0, cx, |state, cx| {
268 Flex::column()
269 .with_child(
270 Flex::row()
271 .with_child(
272 Text::new(message, theme.message.text.clone())
273 .contained()
274 .with_style(theme.message.container)
275 .aligned()
276 .top()
277 .left()
278 .flex(1., true),
279 )
280 .with_child(
281 MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
282 let style = theme.dismiss_button.style_for(state, false);
283 Svg::new("icons/x_mark_8.svg")
284 .with_color(style.color)
285 .constrained()
286 .with_width(style.icon_width)
287 .aligned()
288 .contained()
289 .with_style(style.container)
290 .constrained()
291 .with_width(style.button_width)
292 .with_height(style.button_width)
293 })
294 .with_padding(Padding::uniform(5.))
295 .on_click(MouseButton::Left, move |_, _, cx| {
296 cx.dispatch_action(CancelMessageNotification)
297 })
298 .with_cursor_style(CursorStyle::PointingHand)
299 .aligned()
300 .constrained()
301 .with_height(
302 cx.font_cache().line_height(theme.message.text.font_size),
303 )
304 .aligned()
305 .top()
306 .flex_float(),
307 ),
308 )
309 .with_children({
310 let style = theme.action_message.style_for(state, false);
311 if let Some(click_message) = click_message {
312 Some(
313 Flex::row().with_child(
314 Text::new(click_message, style.text.clone())
315 .contained()
316 .with_style(style.container),
317 ),
318 )
319 } else {
320 None
321 }
322 .into_iter()
323 })
324 .contained()
325 })
326 // Since we're not using a proper overlay, we have to capture these extra events
327 .on_down(MouseButton::Left, |_, _, _| {})
328 .on_up(MouseButton::Left, |_, _, _| {})
329 .on_click(MouseButton::Left, move |_, _, cx| {
330 if let Some(click_action) = click_action.as_ref() {
331 cx.dispatch_any_action(click_action.boxed_clone());
332 cx.dispatch_action(CancelMessageNotification)
333 }
334 })
335 .with_cursor_style(if has_click_action {
336 CursorStyle::PointingHand
337 } else {
338 CursorStyle::Arrow
339 })
340 .into_any()
341 }
342 }
343
344 impl Notification for MessageNotification {
345 fn should_dismiss_notification_on_event(&self, event: &<Self as Entity>::Event) -> bool {
346 match event {
347 MessageNotificationEvent::Dismiss => true,
348 }
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
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 workspace.show_notification(0, cx, |cx| {
374 cx.add_view(|_cx| {
375 simple_message_notification::MessageNotification::new_message(format!(
376 "Error: {:?}",
377 err,
378 ))
379 })
380 });
381
382 None
383 }
384 }
385 }
386}