1use crate::{SuppressNotification, Toast, Workspace};
2use anyhow::Context as _;
3use gpui::{
4 AnyView, App, AppContext as _, AsyncWindowContext, ClickEvent, ClipboardItem, Context,
5 DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, PromptLevel, Render, ScrollHandle,
6 Task, svg,
7};
8use parking_lot::Mutex;
9
10use std::ops::Deref;
11use std::sync::{Arc, LazyLock};
12use std::{any::TypeId, time::Duration};
13use ui::{Tooltip, prelude::*};
14use util::ResultExt;
15
16#[derive(Default)]
17pub struct Notifications {
18 notifications: Vec<(NotificationId, AnyView)>,
19}
20
21impl Deref for Notifications {
22 type Target = Vec<(NotificationId, AnyView)>;
23
24 fn deref(&self) -> &Self::Target {
25 &self.notifications
26 }
27}
28
29impl std::ops::DerefMut for Notifications {
30 fn deref_mut(&mut self) -> &mut Self::Target {
31 &mut self.notifications
32 }
33}
34
35#[derive(Debug, Eq, PartialEq, Clone, Hash)]
36pub enum NotificationId {
37 Unique(TypeId),
38 Composite(TypeId, ElementId),
39 Named(SharedString),
40}
41
42impl NotificationId {
43 /// Returns a unique [`NotificationId`] for the given type.
44 pub fn unique<T: 'static>() -> Self {
45 Self::Unique(TypeId::of::<T>())
46 }
47
48 /// Returns a [`NotificationId`] for the given type that is also identified
49 /// by the provided ID.
50 pub fn composite<T: 'static>(id: impl Into<ElementId>) -> Self {
51 Self::Composite(TypeId::of::<T>(), id.into())
52 }
53
54 /// Builds a `NotificationId` out of the given string.
55 pub fn named(id: SharedString) -> Self {
56 Self::Named(id)
57 }
58}
59
60pub trait Notification:
61 EventEmitter<DismissEvent> + EventEmitter<SuppressEvent> + Focusable + Render
62{
63}
64
65pub struct SuppressEvent;
66
67impl Workspace {
68 #[cfg(any(test, feature = "test-support"))]
69 pub fn notification_ids(&self) -> Vec<NotificationId> {
70 self.notifications
71 .iter()
72 .map(|(id, _)| id)
73 .cloned()
74 .collect()
75 }
76
77 pub fn show_notification<V: Notification>(
78 &mut self,
79 id: NotificationId,
80 cx: &mut Context<Self>,
81 build_notification: impl FnOnce(&mut Context<Self>) -> Entity<V>,
82 ) {
83 self.show_notification_without_handling_dismiss_events(&id, cx, |cx| {
84 let notification = build_notification(cx);
85 cx.subscribe(¬ification, {
86 let id = id.clone();
87 move |this, _, _: &DismissEvent, cx| {
88 this.dismiss_notification(&id, cx);
89 }
90 })
91 .detach();
92 cx.subscribe(¬ification, {
93 let id = id.clone();
94 move |workspace: &mut Workspace, _, _: &SuppressEvent, cx| {
95 workspace.suppress_notification(&id, cx);
96 }
97 })
98 .detach();
99 notification.into()
100 });
101 }
102
103 /// Shows a notification in this workspace's window. Caller must handle dismiss.
104 ///
105 /// This exists so that the `build_notification` closures stored for app notifications can
106 /// return `AnyView`. Subscribing to events from an `AnyView` is not supported, so instead that
107 /// responsibility is pushed to the caller where the `V` type is known.
108 pub(crate) fn show_notification_without_handling_dismiss_events(
109 &mut self,
110 id: &NotificationId,
111 cx: &mut Context<Self>,
112 build_notification: impl FnOnce(&mut Context<Self>) -> AnyView,
113 ) {
114 if self.suppressed_notifications.contains(id) {
115 return;
116 }
117 self.dismiss_notification(id, cx);
118 self.notifications
119 .push((id.clone(), build_notification(cx)));
120 cx.notify();
121 }
122
123 pub fn show_error<E>(&mut self, err: &E, cx: &mut Context<Self>)
124 where
125 E: std::fmt::Debug + std::fmt::Display,
126 {
127 self.show_notification(workspace_error_notification_id(), cx, |cx| {
128 cx.new(|cx| ErrorMessagePrompt::new(format!("Error: {err}"), cx))
129 });
130 }
131
132 pub fn show_portal_error(&mut self, err: String, cx: &mut Context<Self>) {
133 struct PortalError;
134
135 self.show_notification(NotificationId::unique::<PortalError>(), cx, |cx| {
136 cx.new(|cx| {
137 ErrorMessagePrompt::new(err.to_string(), cx).with_link_button(
138 "See docs",
139 "https://zed.dev/docs/linux#i-cant-open-any-files",
140 )
141 })
142 });
143 }
144
145 pub fn dismiss_notification(&mut self, id: &NotificationId, cx: &mut Context<Self>) {
146 self.notifications.retain(|(existing_id, _)| {
147 if existing_id == id {
148 cx.notify();
149 false
150 } else {
151 true
152 }
153 });
154 }
155
156 pub fn show_toast(&mut self, toast: Toast, cx: &mut Context<Self>) {
157 self.dismiss_notification(&toast.id, cx);
158 self.show_notification(toast.id.clone(), cx, |cx| {
159 cx.new(|cx| match toast.on_click.as_ref() {
160 Some((click_msg, on_click)) => {
161 let on_click = on_click.clone();
162 simple_message_notification::MessageNotification::new(toast.msg.clone(), cx)
163 .primary_message(click_msg.clone())
164 .primary_on_click(move |window, cx| on_click(window, cx))
165 }
166 None => {
167 simple_message_notification::MessageNotification::new(toast.msg.clone(), cx)
168 }
169 })
170 });
171 if toast.autohide {
172 cx.spawn(async move |workspace, cx| {
173 cx.background_executor()
174 .timer(Duration::from_millis(5000))
175 .await;
176 workspace
177 .update(cx, |workspace, cx| workspace.dismiss_toast(&toast.id, cx))
178 .ok();
179 })
180 .detach();
181 }
182 }
183
184 pub fn dismiss_toast(&mut self, id: &NotificationId, cx: &mut Context<Self>) {
185 self.dismiss_notification(id, cx);
186 }
187
188 pub fn clear_all_notifications(&mut self, cx: &mut Context<Self>) {
189 self.notifications.clear();
190 cx.notify();
191 }
192
193 /// Hide all notifications matching the given ID
194 pub fn suppress_notification(&mut self, id: &NotificationId, cx: &mut Context<Self>) {
195 self.dismiss_notification(id, cx);
196 self.suppressed_notifications.insert(id.clone());
197 }
198
199 pub fn show_initial_notifications(&mut self, cx: &mut Context<Self>) {
200 // Allow absence of the global so that tests don't need to initialize it.
201 let app_notifications = GLOBAL_APP_NOTIFICATIONS
202 .lock()
203 .app_notifications
204 .iter()
205 .cloned()
206 .collect::<Vec<_>>();
207 for (id, build_notification) in app_notifications {
208 self.show_notification_without_handling_dismiss_events(&id, cx, |cx| {
209 build_notification(cx)
210 });
211 }
212 }
213}
214
215pub struct LanguageServerPrompt {
216 focus_handle: FocusHandle,
217 request: Option<project::LanguageServerPromptRequest>,
218 scroll_handle: ScrollHandle,
219}
220
221impl Focusable for LanguageServerPrompt {
222 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
223 self.focus_handle.clone()
224 }
225}
226
227impl Notification for LanguageServerPrompt {}
228
229impl LanguageServerPrompt {
230 pub fn new(request: project::LanguageServerPromptRequest, cx: &mut App) -> Self {
231 Self {
232 focus_handle: cx.focus_handle(),
233 request: Some(request),
234 scroll_handle: ScrollHandle::new(),
235 }
236 }
237
238 async fn select_option(this: Entity<Self>, ix: usize, cx: &mut AsyncWindowContext) {
239 util::maybe!(async move {
240 let potential_future = this.update(cx, |this, _| {
241 this.request.take().map(|request| request.respond(ix))
242 });
243
244 potential_future? // App Closed
245 .context("Response already sent")?
246 .await
247 .context("Stream already closed")?;
248
249 this.update(cx, |_, cx| cx.emit(DismissEvent))?;
250
251 anyhow::Ok(())
252 })
253 .await
254 .log_err();
255 }
256}
257
258impl Render for LanguageServerPrompt {
259 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
260 let Some(request) = &self.request else {
261 return div().id("language_server_prompt_notification");
262 };
263
264 let (icon, color) = match request.level {
265 PromptLevel::Info => (IconName::Info, Color::Accent),
266 PromptLevel::Warning => (IconName::Warning, Color::Warning),
267 PromptLevel::Critical => (IconName::XCircle, Color::Error),
268 };
269
270 let suppress = window.modifiers().shift;
271 let (close_id, close_icon) = if suppress {
272 ("suppress", IconName::Minimize)
273 } else {
274 ("close", IconName::Close)
275 };
276
277 div()
278 .id("language_server_prompt_notification")
279 .group("language_server_prompt_notification")
280 .occlude()
281 .w_full()
282 .max_h(vh(0.8, window))
283 .elevation_3(cx)
284 .overflow_y_scroll()
285 .track_scroll(&self.scroll_handle)
286 .on_modifiers_changed(cx.listener(|_, _, _, cx| cx.notify()))
287 .child(
288 v_flex()
289 .p_3()
290 .overflow_hidden()
291 .child(
292 h_flex()
293 .justify_between()
294 .items_start()
295 .child(
296 h_flex()
297 .gap_2()
298 .child(Icon::new(icon).color(color))
299 .child(Label::new(request.lsp_name.clone())),
300 )
301 .child(
302 h_flex()
303 .gap_2()
304 .child(
305 IconButton::new("copy", IconName::Copy)
306 .on_click({
307 let message = request.message.clone();
308 move |_, _, cx| {
309 cx.write_to_clipboard(
310 ClipboardItem::new_string(message.clone()),
311 )
312 }
313 })
314 .tooltip(Tooltip::text("Copy Description")),
315 )
316 .child(
317 IconButton::new(close_id, close_icon)
318 .tooltip(move |window, cx| {
319 if suppress {
320 Tooltip::for_action(
321 "Suppress.\nClose with click.",
322 &SuppressNotification,
323 window,
324 cx,
325 )
326 } else {
327 Tooltip::for_action(
328 "Close.\nSuppress with shift-click.",
329 &menu::Cancel,
330 window,
331 cx,
332 )
333 }
334 })
335 .on_click(cx.listener(
336 move |_, _: &ClickEvent, _, cx| {
337 if suppress {
338 cx.emit(SuppressEvent);
339 } else {
340 cx.emit(DismissEvent);
341 }
342 },
343 )),
344 ),
345 ),
346 )
347 .child(Label::new(request.message.to_string()).size(LabelSize::Small))
348 .children(request.actions.iter().enumerate().map(|(ix, action)| {
349 let this_handle = cx.entity();
350 Button::new(ix, action.title.clone())
351 .size(ButtonSize::Large)
352 .on_click(move |_, window, cx| {
353 let this_handle = this_handle.clone();
354 window
355 .spawn(cx, async move |cx| {
356 LanguageServerPrompt::select_option(this_handle, ix, cx)
357 .await
358 })
359 .detach()
360 })
361 })),
362 )
363 }
364}
365
366impl EventEmitter<DismissEvent> for LanguageServerPrompt {}
367impl EventEmitter<SuppressEvent> for LanguageServerPrompt {}
368
369fn workspace_error_notification_id() -> NotificationId {
370 struct WorkspaceErrorNotification;
371 NotificationId::unique::<WorkspaceErrorNotification>()
372}
373
374#[derive(Debug, Clone)]
375pub struct ErrorMessagePrompt {
376 message: SharedString,
377 focus_handle: gpui::FocusHandle,
378 label_and_url_button: Option<(SharedString, SharedString)>,
379}
380
381impl ErrorMessagePrompt {
382 pub fn new<S>(message: S, cx: &mut App) -> Self
383 where
384 S: Into<SharedString>,
385 {
386 Self {
387 message: message.into(),
388 focus_handle: cx.focus_handle(),
389 label_and_url_button: None,
390 }
391 }
392
393 pub fn with_link_button<S>(mut self, label: S, url: S) -> Self
394 where
395 S: Into<SharedString>,
396 {
397 self.label_and_url_button = Some((label.into(), url.into()));
398 self
399 }
400}
401
402impl Render for ErrorMessagePrompt {
403 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
404 h_flex()
405 .id("error_message_prompt_notification")
406 .occlude()
407 .elevation_3(cx)
408 .items_start()
409 .justify_between()
410 .p_2()
411 .gap_2()
412 .w_full()
413 .child(
414 v_flex()
415 .w_full()
416 .child(
417 h_flex()
418 .w_full()
419 .justify_between()
420 .child(
421 svg()
422 .size(window.text_style().font_size)
423 .flex_none()
424 .mr_2()
425 .mt(px(-2.0))
426 .map(|icon| {
427 icon.path(IconName::Warning.path())
428 .text_color(Color::Error.color(cx))
429 }),
430 )
431 .child(
432 ui::IconButton::new("close", ui::IconName::Close)
433 .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
434 ),
435 )
436 .child(
437 div()
438 .id("error_message")
439 .max_w_96()
440 .max_h_40()
441 .overflow_y_scroll()
442 .child(Label::new(self.message.clone()).size(LabelSize::Small)),
443 )
444 .when_some(self.label_and_url_button.clone(), |elm, (label, url)| {
445 elm.child(
446 div().mt_2().child(
447 ui::Button::new("error_message_prompt_notification_button", label)
448 .on_click(move |_, _, cx| cx.open_url(&url)),
449 ),
450 )
451 }),
452 )
453 }
454}
455
456impl Focusable for ErrorMessagePrompt {
457 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
458 self.focus_handle.clone()
459 }
460}
461
462impl EventEmitter<DismissEvent> for ErrorMessagePrompt {}
463impl EventEmitter<SuppressEvent> for ErrorMessagePrompt {}
464
465impl Notification for ErrorMessagePrompt {}
466
467#[derive(IntoElement, RegisterComponent)]
468pub struct NotificationFrame {
469 title: Option<SharedString>,
470 show_suppress_button: bool,
471 show_close_button: bool,
472 close: Option<Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
473 contents: Option<AnyElement>,
474 suffix: Option<AnyElement>,
475}
476
477impl NotificationFrame {
478 pub fn new() -> Self {
479 Self {
480 title: None,
481 contents: None,
482 suffix: None,
483 show_suppress_button: true,
484 show_close_button: true,
485 close: None,
486 }
487 }
488
489 pub fn with_title(mut self, title: Option<impl Into<SharedString>>) -> Self {
490 self.title = title.map(Into::into);
491 self
492 }
493
494 pub fn with_content(self, content: impl IntoElement) -> Self {
495 Self {
496 contents: Some(content.into_any_element()),
497 ..self
498 }
499 }
500
501 /// Determines whether the given notification ID should be suppressible
502 /// Suppressed notifications will not be shown anymore
503 pub fn show_suppress_button(mut self, show: bool) -> Self {
504 self.show_suppress_button = show;
505 self
506 }
507
508 pub fn show_close_button(mut self, show: bool) -> Self {
509 self.show_close_button = show;
510 self
511 }
512
513 pub fn on_close(self, on_close: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
514 Self {
515 close: Some(Box::new(on_close)),
516 ..self
517 }
518 }
519
520 pub fn with_suffix(mut self, suffix: impl IntoElement) -> Self {
521 self.suffix = Some(suffix.into_any_element());
522 self
523 }
524}
525
526impl RenderOnce for NotificationFrame {
527 fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
528 let entity = window.current_view();
529 let show_suppress_button = self.show_suppress_button;
530 let suppress = show_suppress_button && window.modifiers().shift;
531 let (close_id, close_icon) = if suppress {
532 ("suppress", IconName::Minimize)
533 } else {
534 ("close", IconName::Close)
535 };
536
537 v_flex()
538 .occlude()
539 .p_3()
540 .gap_2()
541 .elevation_3(cx)
542 .child(
543 h_flex()
544 .gap_4()
545 .justify_between()
546 .items_start()
547 .child(
548 v_flex()
549 .gap_0p5()
550 .when_some(self.title.clone(), |div, title| {
551 div.child(Label::new(title))
552 })
553 .child(div().max_w_96().children(self.contents)),
554 )
555 .when(self.show_close_button, |this| {
556 this.on_modifiers_changed(move |_, _, cx| cx.notify(entity))
557 .child(
558 IconButton::new(close_id, close_icon)
559 .tooltip(move |window, cx| {
560 if suppress {
561 Tooltip::for_action(
562 "Suppress.\nClose with click.",
563 &SuppressNotification,
564 window,
565 cx,
566 )
567 } else if show_suppress_button {
568 Tooltip::for_action(
569 "Close.\nSuppress with shift-click.",
570 &menu::Cancel,
571 window,
572 cx,
573 )
574 } else {
575 Tooltip::for_action("Close", &menu::Cancel, window, cx)
576 }
577 })
578 .on_click({
579 let close = self.close.take();
580 move |_, window, cx| {
581 if let Some(close) = &close {
582 close(&suppress, window, cx)
583 }
584 }
585 }),
586 )
587 }),
588 )
589 .children(self.suffix)
590 }
591}
592
593impl Component for NotificationFrame {}
594
595pub mod simple_message_notification {
596 use std::sync::Arc;
597
598 use gpui::{
599 AnyElement, DismissEvent, EventEmitter, FocusHandle, Focusable, ParentElement, Render,
600 SharedString, Styled,
601 };
602 use ui::prelude::*;
603
604 use crate::notifications::NotificationFrame;
605
606 use super::{Notification, SuppressEvent};
607
608 pub struct MessageNotification {
609 focus_handle: FocusHandle,
610 build_content: Box<dyn Fn(&mut Window, &mut Context<Self>) -> AnyElement>,
611 primary_message: Option<SharedString>,
612 primary_icon: Option<IconName>,
613 primary_icon_color: Option<Color>,
614 primary_on_click: Option<Arc<dyn Fn(&mut Window, &mut Context<Self>)>>,
615 secondary_message: Option<SharedString>,
616 secondary_icon: Option<IconName>,
617 secondary_icon_color: Option<Color>,
618 secondary_on_click: Option<Arc<dyn Fn(&mut Window, &mut Context<Self>)>>,
619 more_info_message: Option<SharedString>,
620 more_info_url: Option<Arc<str>>,
621 show_close_button: bool,
622 show_suppress_button: bool,
623 title: Option<SharedString>,
624 }
625
626 impl Focusable for MessageNotification {
627 fn focus_handle(&self, _: &App) -> FocusHandle {
628 self.focus_handle.clone()
629 }
630 }
631
632 impl EventEmitter<DismissEvent> for MessageNotification {}
633 impl EventEmitter<SuppressEvent> for MessageNotification {}
634
635 impl Notification for MessageNotification {}
636
637 impl MessageNotification {
638 pub fn new<S>(message: S, cx: &mut App) -> MessageNotification
639 where
640 S: Into<SharedString>,
641 {
642 let message = message.into();
643 Self::new_from_builder(cx, move |_, _| {
644 Label::new(message.clone()).into_any_element()
645 })
646 }
647
648 pub fn new_from_builder<F>(cx: &mut App, content: F) -> MessageNotification
649 where
650 F: 'static + Fn(&mut Window, &mut Context<Self>) -> AnyElement,
651 {
652 Self {
653 build_content: Box::new(content),
654 primary_message: None,
655 primary_icon: None,
656 primary_icon_color: None,
657 primary_on_click: None,
658 secondary_message: None,
659 secondary_icon: None,
660 secondary_icon_color: None,
661 secondary_on_click: None,
662 more_info_message: None,
663 more_info_url: None,
664 show_close_button: true,
665 show_suppress_button: true,
666 title: None,
667 focus_handle: cx.focus_handle(),
668 }
669 }
670
671 pub fn primary_message<S>(mut self, message: S) -> Self
672 where
673 S: Into<SharedString>,
674 {
675 self.primary_message = Some(message.into());
676 self
677 }
678
679 pub fn primary_icon(mut self, icon: IconName) -> Self {
680 self.primary_icon = Some(icon);
681 self
682 }
683
684 pub fn primary_icon_color(mut self, color: Color) -> Self {
685 self.primary_icon_color = Some(color);
686 self
687 }
688
689 pub fn primary_on_click<F>(mut self, on_click: F) -> Self
690 where
691 F: 'static + Fn(&mut Window, &mut Context<Self>),
692 {
693 self.primary_on_click = Some(Arc::new(on_click));
694 self
695 }
696
697 pub fn primary_on_click_arc<F>(mut self, on_click: Arc<F>) -> Self
698 where
699 F: 'static + Fn(&mut Window, &mut Context<Self>),
700 {
701 self.primary_on_click = Some(on_click);
702 self
703 }
704
705 pub fn secondary_message<S>(mut self, message: S) -> Self
706 where
707 S: Into<SharedString>,
708 {
709 self.secondary_message = Some(message.into());
710 self
711 }
712
713 pub fn secondary_icon(mut self, icon: IconName) -> Self {
714 self.secondary_icon = Some(icon);
715 self
716 }
717
718 pub fn secondary_icon_color(mut self, color: Color) -> Self {
719 self.secondary_icon_color = Some(color);
720 self
721 }
722
723 pub fn secondary_on_click<F>(mut self, on_click: F) -> Self
724 where
725 F: 'static + Fn(&mut Window, &mut Context<Self>),
726 {
727 self.secondary_on_click = Some(Arc::new(on_click));
728 self
729 }
730
731 pub fn secondary_on_click_arc<F>(mut self, on_click: Arc<F>) -> Self
732 where
733 F: 'static + Fn(&mut Window, &mut Context<Self>),
734 {
735 self.secondary_on_click = Some(on_click);
736 self
737 }
738
739 pub fn more_info_message<S>(mut self, message: S) -> Self
740 where
741 S: Into<SharedString>,
742 {
743 self.more_info_message = Some(message.into());
744 self
745 }
746
747 pub fn more_info_url<S>(mut self, url: S) -> Self
748 where
749 S: Into<Arc<str>>,
750 {
751 self.more_info_url = Some(url.into());
752 self
753 }
754
755 pub fn dismiss(&mut self, cx: &mut Context<Self>) {
756 cx.emit(DismissEvent);
757 }
758
759 pub fn show_close_button(mut self, show: bool) -> Self {
760 self.show_close_button = show;
761 self
762 }
763
764 /// Determines whether the given notification ID should be suppressible
765 /// Suppressed notifications will not be shown anymor
766 pub fn show_suppress_button(mut self, show: bool) -> Self {
767 self.show_suppress_button = show;
768 self
769 }
770
771 pub fn with_title<S>(mut self, title: S) -> Self
772 where
773 S: Into<SharedString>,
774 {
775 self.title = Some(title.into());
776 self
777 }
778 }
779
780 impl Render for MessageNotification {
781 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
782 NotificationFrame::new()
783 .with_title(self.title.clone())
784 .with_content((self.build_content)(window, cx))
785 .show_close_button(self.show_close_button)
786 .show_suppress_button(self.show_suppress_button)
787 .on_close(cx.listener(|_, suppress, _, cx| {
788 if *suppress {
789 cx.emit(SuppressEvent);
790 } else {
791 cx.emit(DismissEvent);
792 }
793 }))
794 .with_suffix(
795 h_flex()
796 .gap_1()
797 .children(self.primary_message.iter().map(|message| {
798 let mut button = Button::new(message.clone(), message.clone())
799 .label_size(LabelSize::Small)
800 .on_click(cx.listener(|this, _, window, cx| {
801 if let Some(on_click) = this.primary_on_click.as_ref() {
802 (on_click)(window, cx)
803 };
804 this.dismiss(cx)
805 }));
806
807 if let Some(icon) = self.primary_icon {
808 button = button
809 .icon(icon)
810 .icon_color(self.primary_icon_color.unwrap_or(Color::Muted))
811 .icon_position(IconPosition::Start)
812 .icon_size(IconSize::Small);
813 }
814
815 button
816 }))
817 .children(self.secondary_message.iter().map(|message| {
818 let mut button = Button::new(message.clone(), message.clone())
819 .label_size(LabelSize::Small)
820 .on_click(cx.listener(|this, _, window, cx| {
821 if let Some(on_click) = this.secondary_on_click.as_ref() {
822 (on_click)(window, cx)
823 };
824 this.dismiss(cx)
825 }));
826
827 if let Some(icon) = self.secondary_icon {
828 button = button
829 .icon(icon)
830 .icon_position(IconPosition::Start)
831 .icon_size(IconSize::Small)
832 .icon_color(self.secondary_icon_color.unwrap_or(Color::Muted));
833 }
834
835 button
836 }))
837 .child(
838 h_flex().w_full().justify_end().children(
839 self.more_info_message
840 .iter()
841 .zip(self.more_info_url.iter())
842 .map(|(message, url)| {
843 let url = url.clone();
844 Button::new(message.clone(), message.clone())
845 .label_size(LabelSize::Small)
846 .icon(IconName::ArrowUpRight)
847 .icon_size(IconSize::Indicator)
848 .icon_color(Color::Muted)
849 .on_click(cx.listener(move |_, _, _, cx| {
850 cx.open_url(&url);
851 }))
852 }),
853 ),
854 ),
855 )
856 }
857 }
858}
859
860static GLOBAL_APP_NOTIFICATIONS: LazyLock<Mutex<AppNotifications>> = LazyLock::new(|| {
861 Mutex::new(AppNotifications {
862 app_notifications: Vec::new(),
863 })
864});
865
866/// Stores app notifications so that they can be shown in new workspaces.
867struct AppNotifications {
868 app_notifications: Vec<(
869 NotificationId,
870 Arc<dyn Fn(&mut Context<Workspace>) -> AnyView + Send + Sync>,
871 )>,
872}
873
874impl AppNotifications {
875 pub fn insert(
876 &mut self,
877 id: NotificationId,
878 build_notification: Arc<dyn Fn(&mut Context<Workspace>) -> AnyView + Send + Sync>,
879 ) {
880 self.remove(&id);
881 self.app_notifications.push((id, build_notification))
882 }
883
884 pub fn remove(&mut self, id: &NotificationId) {
885 self.app_notifications
886 .retain(|(existing_id, _)| existing_id != id);
887 }
888}
889
890/// Shows a notification in all workspaces. New workspaces will also receive the notification - this
891/// is particularly to handle notifications that occur on initialization before any workspaces
892/// exist. If the notification is dismissed within any workspace, it will be removed from all.
893pub fn show_app_notification<V: Notification + 'static>(
894 id: NotificationId,
895 cx: &mut App,
896 build_notification: impl Fn(&mut Context<Workspace>) -> Entity<V> + 'static + Send + Sync,
897) {
898 // Defer notification creation so that windows on the stack can be returned to GPUI
899 cx.defer(move |cx| {
900 // Handle dismiss events by removing the notification from all workspaces.
901 let build_notification: Arc<dyn Fn(&mut Context<Workspace>) -> AnyView + Send + Sync> =
902 Arc::new({
903 let id = id.clone();
904 move |cx| {
905 let notification = build_notification(cx);
906 cx.subscribe(¬ification, {
907 let id = id.clone();
908 move |_, _, _: &DismissEvent, cx| {
909 dismiss_app_notification(&id, cx);
910 }
911 })
912 .detach();
913 cx.subscribe(¬ification, {
914 let id = id.clone();
915 move |workspace: &mut Workspace, _, _: &SuppressEvent, cx| {
916 workspace.suppress_notification(&id, cx);
917 }
918 })
919 .detach();
920 notification.into()
921 }
922 });
923
924 // Store the notification so that new workspaces also receive it.
925 GLOBAL_APP_NOTIFICATIONS
926 .lock()
927 .insert(id.clone(), build_notification.clone());
928
929 for window in cx.windows() {
930 if let Some(workspace_window) = window.downcast::<Workspace>() {
931 workspace_window
932 .update(cx, |workspace, _window, cx| {
933 workspace.show_notification_without_handling_dismiss_events(
934 &id,
935 cx,
936 |cx| build_notification(cx),
937 );
938 })
939 .ok(); // Doesn't matter if the windows are dropped
940 }
941 }
942 });
943}
944
945pub fn dismiss_app_notification(id: &NotificationId, cx: &mut App) {
946 let id = id.clone();
947 // Defer notification dismissal so that windows on the stack can be returned to GPUI
948 cx.defer(move |cx| {
949 GLOBAL_APP_NOTIFICATIONS.lock().remove(&id);
950 for window in cx.windows() {
951 if let Some(workspace_window) = window.downcast::<Workspace>() {
952 let id = id.clone();
953 workspace_window
954 .update(cx, |workspace, _window, cx| {
955 workspace.dismiss_notification(&id, cx)
956 })
957 .ok();
958 }
959 }
960 });
961}
962
963pub trait NotifyResultExt {
964 type Ok;
965
966 fn notify_err(self, workspace: &mut Workspace, cx: &mut Context<Workspace>)
967 -> Option<Self::Ok>;
968
969 fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<Self::Ok>;
970
971 /// Notifies the active workspace if there is one, otherwise notifies all workspaces.
972 fn notify_app_err(self, cx: &mut App) -> Option<Self::Ok>;
973}
974
975impl<T, E> NotifyResultExt for std::result::Result<T, E>
976where
977 E: std::fmt::Debug + std::fmt::Display,
978{
979 type Ok = T;
980
981 fn notify_err(self, workspace: &mut Workspace, cx: &mut Context<Workspace>) -> Option<T> {
982 match self {
983 Ok(value) => Some(value),
984 Err(err) => {
985 log::error!("Showing error notification in workspace: {err:?}");
986 workspace.show_error(&err, cx);
987 None
988 }
989 }
990 }
991
992 fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<T> {
993 match self {
994 Ok(value) => Some(value),
995 Err(err) => {
996 log::error!("{err:?}");
997 cx.update_root(|view, _, cx| {
998 if let Ok(workspace) = view.downcast::<Workspace>() {
999 workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
1000 }
1001 })
1002 .ok();
1003 None
1004 }
1005 }
1006 }
1007
1008 fn notify_app_err(self, cx: &mut App) -> Option<T> {
1009 match self {
1010 Ok(value) => Some(value),
1011 Err(err) => {
1012 let message: SharedString = format!("Error: {err}").into();
1013 log::error!("Showing error notification in app: {message}");
1014 show_app_notification(workspace_error_notification_id(), cx, {
1015 move |cx| {
1016 cx.new({
1017 let message = message.clone();
1018 move |cx| ErrorMessagePrompt::new(message, cx)
1019 })
1020 }
1021 });
1022
1023 None
1024 }
1025 }
1026 }
1027}
1028
1029pub trait NotifyTaskExt {
1030 fn detach_and_notify_err(self, window: &mut Window, cx: &mut App);
1031}
1032
1033impl<R, E> NotifyTaskExt for Task<std::result::Result<R, E>>
1034where
1035 E: std::fmt::Debug + std::fmt::Display + Sized + 'static,
1036 R: 'static,
1037{
1038 fn detach_and_notify_err(self, window: &mut Window, cx: &mut App) {
1039 window
1040 .spawn(cx, async move |cx| self.await.notify_async_err(cx))
1041 .detach();
1042 }
1043}
1044
1045pub trait DetachAndPromptErr<R> {
1046 fn prompt_err(
1047 self,
1048 msg: &str,
1049 window: &Window,
1050 cx: &App,
1051 f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option<String> + 'static,
1052 ) -> Task<Option<R>>;
1053
1054 fn detach_and_prompt_err(
1055 self,
1056 msg: &str,
1057 window: &Window,
1058 cx: &App,
1059 f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option<String> + 'static,
1060 );
1061}
1062
1063impl<R> DetachAndPromptErr<R> for Task<anyhow::Result<R>>
1064where
1065 R: 'static,
1066{
1067 fn prompt_err(
1068 self,
1069 msg: &str,
1070 window: &Window,
1071 cx: &App,
1072 f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option<String> + 'static,
1073 ) -> Task<Option<R>> {
1074 let msg = msg.to_owned();
1075 window.spawn(cx, async move |cx| {
1076 let result = self.await;
1077 if let Err(err) = result.as_ref() {
1078 log::error!("{err:?}");
1079 if let Ok(prompt) = cx.update(|window, cx| {
1080 let mut display = format!("{err}");
1081 if !display.ends_with('\n') {
1082 display.push('.');
1083 display.push(' ')
1084 }
1085 let detail =
1086 f(err, window, cx).unwrap_or_else(|| format!("{display}Please try again."));
1087 window.prompt(PromptLevel::Critical, &msg, Some(&detail), &["Ok"], cx)
1088 }) {
1089 prompt.await.ok();
1090 }
1091 return None;
1092 }
1093 Some(result.unwrap())
1094 })
1095 }
1096
1097 fn detach_and_prompt_err(
1098 self,
1099 msg: &str,
1100 window: &Window,
1101 cx: &App,
1102 f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option<String> + 'static,
1103 ) {
1104 self.prompt_err(msg, window, cx, f).detach();
1105 }
1106}