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 cx,
324 )
325 } else {
326 Tooltip::for_action(
327 "Close.\nSuppress with shift-click.",
328 &menu::Cancel,
329 cx,
330 )
331 }
332 })
333 .on_click(cx.listener(
334 move |_, _: &ClickEvent, _, cx| {
335 if suppress {
336 cx.emit(SuppressEvent);
337 } else {
338 cx.emit(DismissEvent);
339 }
340 },
341 )),
342 ),
343 ),
344 )
345 .child(Label::new(request.message.to_string()).size(LabelSize::Small))
346 .children(request.actions.iter().enumerate().map(|(ix, action)| {
347 let this_handle = cx.entity();
348 Button::new(ix, action.title.clone())
349 .size(ButtonSize::Large)
350 .on_click(move |_, window, cx| {
351 let this_handle = this_handle.clone();
352 window
353 .spawn(cx, async move |cx| {
354 LanguageServerPrompt::select_option(this_handle, ix, cx)
355 .await
356 })
357 .detach()
358 })
359 })),
360 )
361 }
362}
363
364impl EventEmitter<DismissEvent> for LanguageServerPrompt {}
365impl EventEmitter<SuppressEvent> for LanguageServerPrompt {}
366
367fn workspace_error_notification_id() -> NotificationId {
368 struct WorkspaceErrorNotification;
369 NotificationId::unique::<WorkspaceErrorNotification>()
370}
371
372#[derive(Debug, Clone)]
373pub struct ErrorMessagePrompt {
374 message: SharedString,
375 focus_handle: gpui::FocusHandle,
376 label_and_url_button: Option<(SharedString, SharedString)>,
377}
378
379impl ErrorMessagePrompt {
380 pub fn new<S>(message: S, cx: &mut App) -> Self
381 where
382 S: Into<SharedString>,
383 {
384 Self {
385 message: message.into(),
386 focus_handle: cx.focus_handle(),
387 label_and_url_button: None,
388 }
389 }
390
391 pub fn with_link_button<S>(mut self, label: S, url: S) -> Self
392 where
393 S: Into<SharedString>,
394 {
395 self.label_and_url_button = Some((label.into(), url.into()));
396 self
397 }
398}
399
400impl Render for ErrorMessagePrompt {
401 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
402 h_flex()
403 .id("error_message_prompt_notification")
404 .occlude()
405 .elevation_3(cx)
406 .items_start()
407 .justify_between()
408 .p_2()
409 .gap_2()
410 .w_full()
411 .child(
412 v_flex()
413 .w_full()
414 .child(
415 h_flex()
416 .w_full()
417 .justify_between()
418 .child(
419 svg()
420 .size(window.text_style().font_size)
421 .flex_none()
422 .mr_2()
423 .mt(px(-2.0))
424 .map(|icon| {
425 icon.path(IconName::Warning.path())
426 .text_color(Color::Error.color(cx))
427 }),
428 )
429 .child(
430 ui::IconButton::new("close", ui::IconName::Close)
431 .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))),
432 ),
433 )
434 .child(
435 div()
436 .id("error_message")
437 .max_w_96()
438 .max_h_40()
439 .overflow_y_scroll()
440 .child(Label::new(self.message.clone()).size(LabelSize::Small)),
441 )
442 .when_some(self.label_and_url_button.clone(), |elm, (label, url)| {
443 elm.child(
444 div().mt_2().child(
445 ui::Button::new("error_message_prompt_notification_button", label)
446 .on_click(move |_, _, cx| cx.open_url(&url)),
447 ),
448 )
449 }),
450 )
451 }
452}
453
454impl Focusable for ErrorMessagePrompt {
455 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
456 self.focus_handle.clone()
457 }
458}
459
460impl EventEmitter<DismissEvent> for ErrorMessagePrompt {}
461impl EventEmitter<SuppressEvent> for ErrorMessagePrompt {}
462
463impl Notification for ErrorMessagePrompt {}
464
465#[derive(IntoElement, RegisterComponent)]
466pub struct NotificationFrame {
467 title: Option<SharedString>,
468 show_suppress_button: bool,
469 show_close_button: bool,
470 close: Option<Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
471 contents: Option<AnyElement>,
472 suffix: Option<AnyElement>,
473}
474
475impl NotificationFrame {
476 pub fn new() -> Self {
477 Self {
478 title: None,
479 contents: None,
480 suffix: None,
481 show_suppress_button: true,
482 show_close_button: true,
483 close: None,
484 }
485 }
486
487 pub fn with_title(mut self, title: Option<impl Into<SharedString>>) -> Self {
488 self.title = title.map(Into::into);
489 self
490 }
491
492 pub fn with_content(self, content: impl IntoElement) -> Self {
493 Self {
494 contents: Some(content.into_any_element()),
495 ..self
496 }
497 }
498
499 /// Determines whether the given notification ID should be suppressible
500 /// Suppressed notifications will not be shown anymore
501 pub fn show_suppress_button(mut self, show: bool) -> Self {
502 self.show_suppress_button = show;
503 self
504 }
505
506 pub fn show_close_button(mut self, show: bool) -> Self {
507 self.show_close_button = show;
508 self
509 }
510
511 pub fn on_close(self, on_close: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
512 Self {
513 close: Some(Box::new(on_close)),
514 ..self
515 }
516 }
517
518 pub fn with_suffix(mut self, suffix: impl IntoElement) -> Self {
519 self.suffix = Some(suffix.into_any_element());
520 self
521 }
522}
523
524impl RenderOnce for NotificationFrame {
525 fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
526 let entity = window.current_view();
527 let show_suppress_button = self.show_suppress_button;
528 let suppress = show_suppress_button && window.modifiers().shift;
529 let (close_id, close_icon) = if suppress {
530 ("suppress", IconName::Minimize)
531 } else {
532 ("close", IconName::Close)
533 };
534
535 v_flex()
536 .occlude()
537 .p_3()
538 .gap_2()
539 .elevation_3(cx)
540 .child(
541 h_flex()
542 .gap_4()
543 .justify_between()
544 .items_start()
545 .child(
546 v_flex()
547 .gap_0p5()
548 .when_some(self.title.clone(), |div, title| {
549 div.child(Label::new(title))
550 })
551 .child(div().max_w_96().children(self.contents)),
552 )
553 .when(self.show_close_button, |this| {
554 this.on_modifiers_changed(move |_, _, cx| cx.notify(entity))
555 .child(
556 IconButton::new(close_id, close_icon)
557 .tooltip(move |_window, cx| {
558 if suppress {
559 Tooltip::for_action(
560 "Suppress.\nClose with click.",
561 &SuppressNotification,
562 cx,
563 )
564 } else if show_suppress_button {
565 Tooltip::for_action(
566 "Close.\nSuppress with shift-click.",
567 &menu::Cancel,
568 cx,
569 )
570 } else {
571 Tooltip::for_action("Close", &menu::Cancel, cx)
572 }
573 })
574 .on_click({
575 let close = self.close.take();
576 move |_, window, cx| {
577 if let Some(close) = &close {
578 close(&suppress, window, cx)
579 }
580 }
581 }),
582 )
583 }),
584 )
585 .children(self.suffix)
586 }
587}
588
589impl Component for NotificationFrame {}
590
591pub mod simple_message_notification {
592 use std::sync::Arc;
593
594 use gpui::{
595 AnyElement, DismissEvent, EventEmitter, FocusHandle, Focusable, ParentElement, Render,
596 SharedString, Styled,
597 };
598 use ui::prelude::*;
599
600 use crate::notifications::NotificationFrame;
601
602 use super::{Notification, SuppressEvent};
603
604 pub struct MessageNotification {
605 focus_handle: FocusHandle,
606 build_content: Box<dyn Fn(&mut Window, &mut Context<Self>) -> AnyElement>,
607 primary_message: Option<SharedString>,
608 primary_icon: Option<IconName>,
609 primary_icon_color: Option<Color>,
610 primary_on_click: Option<Arc<dyn Fn(&mut Window, &mut Context<Self>)>>,
611 secondary_message: Option<SharedString>,
612 secondary_icon: Option<IconName>,
613 secondary_icon_color: Option<Color>,
614 secondary_on_click: Option<Arc<dyn Fn(&mut Window, &mut Context<Self>)>>,
615 more_info_message: Option<SharedString>,
616 more_info_url: Option<Arc<str>>,
617 show_close_button: bool,
618 show_suppress_button: bool,
619 title: Option<SharedString>,
620 }
621
622 impl Focusable for MessageNotification {
623 fn focus_handle(&self, _: &App) -> FocusHandle {
624 self.focus_handle.clone()
625 }
626 }
627
628 impl EventEmitter<DismissEvent> for MessageNotification {}
629 impl EventEmitter<SuppressEvent> for MessageNotification {}
630
631 impl Notification for MessageNotification {}
632
633 impl MessageNotification {
634 pub fn new<S>(message: S, cx: &mut App) -> MessageNotification
635 where
636 S: Into<SharedString>,
637 {
638 let message = message.into();
639 Self::new_from_builder(cx, move |_, _| {
640 Label::new(message.clone()).into_any_element()
641 })
642 }
643
644 pub fn new_from_builder<F>(cx: &mut App, content: F) -> MessageNotification
645 where
646 F: 'static + Fn(&mut Window, &mut Context<Self>) -> AnyElement,
647 {
648 Self {
649 build_content: Box::new(content),
650 primary_message: None,
651 primary_icon: None,
652 primary_icon_color: None,
653 primary_on_click: None,
654 secondary_message: None,
655 secondary_icon: None,
656 secondary_icon_color: None,
657 secondary_on_click: None,
658 more_info_message: None,
659 more_info_url: None,
660 show_close_button: true,
661 show_suppress_button: true,
662 title: None,
663 focus_handle: cx.focus_handle(),
664 }
665 }
666
667 pub fn primary_message<S>(mut self, message: S) -> Self
668 where
669 S: Into<SharedString>,
670 {
671 self.primary_message = Some(message.into());
672 self
673 }
674
675 pub fn primary_icon(mut self, icon: IconName) -> Self {
676 self.primary_icon = Some(icon);
677 self
678 }
679
680 pub fn primary_icon_color(mut self, color: Color) -> Self {
681 self.primary_icon_color = Some(color);
682 self
683 }
684
685 pub fn primary_on_click<F>(mut self, on_click: F) -> Self
686 where
687 F: 'static + Fn(&mut Window, &mut Context<Self>),
688 {
689 self.primary_on_click = Some(Arc::new(on_click));
690 self
691 }
692
693 pub fn primary_on_click_arc<F>(mut self, on_click: Arc<F>) -> Self
694 where
695 F: 'static + Fn(&mut Window, &mut Context<Self>),
696 {
697 self.primary_on_click = Some(on_click);
698 self
699 }
700
701 pub fn secondary_message<S>(mut self, message: S) -> Self
702 where
703 S: Into<SharedString>,
704 {
705 self.secondary_message = Some(message.into());
706 self
707 }
708
709 pub fn secondary_icon(mut self, icon: IconName) -> Self {
710 self.secondary_icon = Some(icon);
711 self
712 }
713
714 pub fn secondary_icon_color(mut self, color: Color) -> Self {
715 self.secondary_icon_color = Some(color);
716 self
717 }
718
719 pub fn secondary_on_click<F>(mut self, on_click: F) -> Self
720 where
721 F: 'static + Fn(&mut Window, &mut Context<Self>),
722 {
723 self.secondary_on_click = Some(Arc::new(on_click));
724 self
725 }
726
727 pub fn secondary_on_click_arc<F>(mut self, on_click: Arc<F>) -> Self
728 where
729 F: 'static + Fn(&mut Window, &mut Context<Self>),
730 {
731 self.secondary_on_click = Some(on_click);
732 self
733 }
734
735 pub fn more_info_message<S>(mut self, message: S) -> Self
736 where
737 S: Into<SharedString>,
738 {
739 self.more_info_message = Some(message.into());
740 self
741 }
742
743 pub fn more_info_url<S>(mut self, url: S) -> Self
744 where
745 S: Into<Arc<str>>,
746 {
747 self.more_info_url = Some(url.into());
748 self
749 }
750
751 pub fn dismiss(&mut self, cx: &mut Context<Self>) {
752 cx.emit(DismissEvent);
753 }
754
755 pub fn show_close_button(mut self, show: bool) -> Self {
756 self.show_close_button = show;
757 self
758 }
759
760 /// Determines whether the given notification ID should be suppressible
761 /// Suppressed notifications will not be shown anymor
762 pub fn show_suppress_button(mut self, show: bool) -> Self {
763 self.show_suppress_button = show;
764 self
765 }
766
767 pub fn with_title<S>(mut self, title: S) -> Self
768 where
769 S: Into<SharedString>,
770 {
771 self.title = Some(title.into());
772 self
773 }
774 }
775
776 impl Render for MessageNotification {
777 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
778 NotificationFrame::new()
779 .with_title(self.title.clone())
780 .with_content((self.build_content)(window, cx))
781 .show_close_button(self.show_close_button)
782 .show_suppress_button(self.show_suppress_button)
783 .on_close(cx.listener(|_, suppress, _, cx| {
784 if *suppress {
785 cx.emit(SuppressEvent);
786 } else {
787 cx.emit(DismissEvent);
788 }
789 }))
790 .with_suffix(
791 h_flex()
792 .gap_1()
793 .children(self.primary_message.iter().map(|message| {
794 let mut button = Button::new(message.clone(), message.clone())
795 .label_size(LabelSize::Small)
796 .on_click(cx.listener(|this, _, window, cx| {
797 if let Some(on_click) = this.primary_on_click.as_ref() {
798 (on_click)(window, cx)
799 };
800 this.dismiss(cx)
801 }));
802
803 if let Some(icon) = self.primary_icon {
804 button = button
805 .icon(icon)
806 .icon_color(self.primary_icon_color.unwrap_or(Color::Muted))
807 .icon_position(IconPosition::Start)
808 .icon_size(IconSize::Small);
809 }
810
811 button
812 }))
813 .children(self.secondary_message.iter().map(|message| {
814 let mut button = Button::new(message.clone(), message.clone())
815 .label_size(LabelSize::Small)
816 .on_click(cx.listener(|this, _, window, cx| {
817 if let Some(on_click) = this.secondary_on_click.as_ref() {
818 (on_click)(window, cx)
819 };
820 this.dismiss(cx)
821 }));
822
823 if let Some(icon) = self.secondary_icon {
824 button = button
825 .icon(icon)
826 .icon_position(IconPosition::Start)
827 .icon_size(IconSize::Small)
828 .icon_color(self.secondary_icon_color.unwrap_or(Color::Muted));
829 }
830
831 button
832 }))
833 .child(
834 h_flex().w_full().justify_end().children(
835 self.more_info_message
836 .iter()
837 .zip(self.more_info_url.iter())
838 .map(|(message, url)| {
839 let url = url.clone();
840 Button::new(message.clone(), message.clone())
841 .label_size(LabelSize::Small)
842 .icon(IconName::ArrowUpRight)
843 .icon_size(IconSize::Indicator)
844 .icon_color(Color::Muted)
845 .on_click(cx.listener(move |_, _, _, cx| {
846 cx.open_url(&url);
847 }))
848 }),
849 ),
850 ),
851 )
852 }
853 }
854}
855
856static GLOBAL_APP_NOTIFICATIONS: LazyLock<Mutex<AppNotifications>> = LazyLock::new(|| {
857 Mutex::new(AppNotifications {
858 app_notifications: Vec::new(),
859 })
860});
861
862/// Stores app notifications so that they can be shown in new workspaces.
863struct AppNotifications {
864 app_notifications: Vec<(
865 NotificationId,
866 Arc<dyn Fn(&mut Context<Workspace>) -> AnyView + Send + Sync>,
867 )>,
868}
869
870impl AppNotifications {
871 pub fn insert(
872 &mut self,
873 id: NotificationId,
874 build_notification: Arc<dyn Fn(&mut Context<Workspace>) -> AnyView + Send + Sync>,
875 ) {
876 self.remove(&id);
877 self.app_notifications.push((id, build_notification))
878 }
879
880 pub fn remove(&mut self, id: &NotificationId) {
881 self.app_notifications
882 .retain(|(existing_id, _)| existing_id != id);
883 }
884}
885
886/// Shows a notification in all workspaces. New workspaces will also receive the notification - this
887/// is particularly to handle notifications that occur on initialization before any workspaces
888/// exist. If the notification is dismissed within any workspace, it will be removed from all.
889pub fn show_app_notification<V: Notification + 'static>(
890 id: NotificationId,
891 cx: &mut App,
892 build_notification: impl Fn(&mut Context<Workspace>) -> Entity<V> + 'static + Send + Sync,
893) {
894 // Defer notification creation so that windows on the stack can be returned to GPUI
895 cx.defer(move |cx| {
896 // Handle dismiss events by removing the notification from all workspaces.
897 let build_notification: Arc<dyn Fn(&mut Context<Workspace>) -> AnyView + Send + Sync> =
898 Arc::new({
899 let id = id.clone();
900 move |cx| {
901 let notification = build_notification(cx);
902 cx.subscribe(¬ification, {
903 let id = id.clone();
904 move |_, _, _: &DismissEvent, cx| {
905 dismiss_app_notification(&id, cx);
906 }
907 })
908 .detach();
909 cx.subscribe(¬ification, {
910 let id = id.clone();
911 move |workspace: &mut Workspace, _, _: &SuppressEvent, cx| {
912 workspace.suppress_notification(&id, cx);
913 }
914 })
915 .detach();
916 notification.into()
917 }
918 });
919
920 // Store the notification so that new workspaces also receive it.
921 GLOBAL_APP_NOTIFICATIONS
922 .lock()
923 .insert(id.clone(), build_notification.clone());
924
925 for window in cx.windows() {
926 if let Some(workspace_window) = window.downcast::<Workspace>() {
927 workspace_window
928 .update(cx, |workspace, _window, cx| {
929 workspace.show_notification_without_handling_dismiss_events(
930 &id,
931 cx,
932 |cx| build_notification(cx),
933 );
934 })
935 .ok(); // Doesn't matter if the windows are dropped
936 }
937 }
938 });
939}
940
941pub fn dismiss_app_notification(id: &NotificationId, cx: &mut App) {
942 let id = id.clone();
943 // Defer notification dismissal so that windows on the stack can be returned to GPUI
944 cx.defer(move |cx| {
945 GLOBAL_APP_NOTIFICATIONS.lock().remove(&id);
946 for window in cx.windows() {
947 if let Some(workspace_window) = window.downcast::<Workspace>() {
948 let id = id.clone();
949 workspace_window
950 .update(cx, |workspace, _window, cx| {
951 workspace.dismiss_notification(&id, cx)
952 })
953 .ok();
954 }
955 }
956 });
957}
958
959pub trait NotifyResultExt {
960 type Ok;
961
962 fn notify_err(self, workspace: &mut Workspace, cx: &mut Context<Workspace>)
963 -> Option<Self::Ok>;
964
965 fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<Self::Ok>;
966
967 /// Notifies the active workspace if there is one, otherwise notifies all workspaces.
968 fn notify_app_err(self, cx: &mut App) -> Option<Self::Ok>;
969}
970
971impl<T, E> NotifyResultExt for std::result::Result<T, E>
972where
973 E: std::fmt::Debug + std::fmt::Display,
974{
975 type Ok = T;
976
977 fn notify_err(self, workspace: &mut Workspace, cx: &mut Context<Workspace>) -> Option<T> {
978 match self {
979 Ok(value) => Some(value),
980 Err(err) => {
981 log::error!("Showing error notification in workspace: {err:?}");
982 workspace.show_error(&err, cx);
983 None
984 }
985 }
986 }
987
988 fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<T> {
989 match self {
990 Ok(value) => Some(value),
991 Err(err) => {
992 log::error!("{err:?}");
993 cx.update_root(|view, _, cx| {
994 if let Ok(workspace) = view.downcast::<Workspace>() {
995 workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
996 }
997 })
998 .ok();
999 None
1000 }
1001 }
1002 }
1003
1004 fn notify_app_err(self, cx: &mut App) -> Option<T> {
1005 match self {
1006 Ok(value) => Some(value),
1007 Err(err) => {
1008 let message: SharedString = format!("Error: {err}").into();
1009 log::error!("Showing error notification in app: {message}");
1010 show_app_notification(workspace_error_notification_id(), cx, {
1011 move |cx| {
1012 cx.new({
1013 let message = message.clone();
1014 move |cx| ErrorMessagePrompt::new(message, cx)
1015 })
1016 }
1017 });
1018
1019 None
1020 }
1021 }
1022 }
1023}
1024
1025pub trait NotifyTaskExt {
1026 fn detach_and_notify_err(self, window: &mut Window, cx: &mut App);
1027}
1028
1029impl<R, E> NotifyTaskExt for Task<std::result::Result<R, E>>
1030where
1031 E: std::fmt::Debug + std::fmt::Display + Sized + 'static,
1032 R: 'static,
1033{
1034 fn detach_and_notify_err(self, window: &mut Window, cx: &mut App) {
1035 window
1036 .spawn(cx, async move |cx| self.await.notify_async_err(cx))
1037 .detach();
1038 }
1039}
1040
1041pub trait DetachAndPromptErr<R> {
1042 fn prompt_err(
1043 self,
1044 msg: &str,
1045 window: &Window,
1046 cx: &App,
1047 f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option<String> + 'static,
1048 ) -> Task<Option<R>>;
1049
1050 fn detach_and_prompt_err(
1051 self,
1052 msg: &str,
1053 window: &Window,
1054 cx: &App,
1055 f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option<String> + 'static,
1056 );
1057}
1058
1059impl<R> DetachAndPromptErr<R> for Task<anyhow::Result<R>>
1060where
1061 R: 'static,
1062{
1063 fn prompt_err(
1064 self,
1065 msg: &str,
1066 window: &Window,
1067 cx: &App,
1068 f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option<String> + 'static,
1069 ) -> Task<Option<R>> {
1070 let msg = msg.to_owned();
1071 window.spawn(cx, async move |cx| {
1072 let result = self.await;
1073 if let Err(err) = result.as_ref() {
1074 log::error!("{err:?}");
1075 if let Ok(prompt) = cx.update(|window, cx| {
1076 let mut display = format!("{err}");
1077 if !display.ends_with('\n') {
1078 display.push('.');
1079 display.push(' ')
1080 }
1081 let detail =
1082 f(err, window, cx).unwrap_or_else(|| format!("{display}Please try again."));
1083 window.prompt(PromptLevel::Critical, &msg, Some(&detail), &["Ok"], cx)
1084 }) {
1085 prompt.await.ok();
1086 }
1087 return None;
1088 }
1089 Some(result.unwrap())
1090 })
1091 }
1092
1093 fn detach_and_prompt_err(
1094 self,
1095 msg: &str,
1096 window: &Window,
1097 cx: &App,
1098 f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option<String> + 'static,
1099 ) {
1100 self.prompt_err(msg, window, cx, f).detach();
1101 }
1102}