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 ScrollHandle, SharedString, Styled,
597 };
598 use ui::{WithScrollbar, 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 scroll_handle: ScrollHandle,
621 }
622
623 impl Focusable for MessageNotification {
624 fn focus_handle(&self, _: &App) -> FocusHandle {
625 self.focus_handle.clone()
626 }
627 }
628
629 impl EventEmitter<DismissEvent> for MessageNotification {}
630 impl EventEmitter<SuppressEvent> for MessageNotification {}
631
632 impl Notification for MessageNotification {}
633
634 impl MessageNotification {
635 pub fn new<S>(message: S, cx: &mut App) -> MessageNotification
636 where
637 S: Into<SharedString>,
638 {
639 let message = message.into();
640 Self::new_from_builder(cx, move |_, _| {
641 Label::new(message.clone()).into_any_element()
642 })
643 }
644
645 pub fn new_from_builder<F>(cx: &mut App, content: F) -> MessageNotification
646 where
647 F: 'static + Fn(&mut Window, &mut Context<Self>) -> AnyElement,
648 {
649 Self {
650 build_content: Box::new(content),
651 primary_message: None,
652 primary_icon: None,
653 primary_icon_color: None,
654 primary_on_click: None,
655 secondary_message: None,
656 secondary_icon: None,
657 secondary_icon_color: None,
658 secondary_on_click: None,
659 more_info_message: None,
660 more_info_url: None,
661 show_close_button: true,
662 show_suppress_button: true,
663 title: None,
664 focus_handle: cx.focus_handle(),
665 scroll_handle: ScrollHandle::new(),
666 }
667 }
668
669 pub fn primary_message<S>(mut self, message: S) -> Self
670 where
671 S: Into<SharedString>,
672 {
673 self.primary_message = Some(message.into());
674 self
675 }
676
677 pub fn primary_icon(mut self, icon: IconName) -> Self {
678 self.primary_icon = Some(icon);
679 self
680 }
681
682 pub fn primary_icon_color(mut self, color: Color) -> Self {
683 self.primary_icon_color = Some(color);
684 self
685 }
686
687 pub fn primary_on_click<F>(mut self, on_click: F) -> Self
688 where
689 F: 'static + Fn(&mut Window, &mut Context<Self>),
690 {
691 self.primary_on_click = Some(Arc::new(on_click));
692 self
693 }
694
695 pub fn primary_on_click_arc<F>(mut self, on_click: Arc<F>) -> Self
696 where
697 F: 'static + Fn(&mut Window, &mut Context<Self>),
698 {
699 self.primary_on_click = Some(on_click);
700 self
701 }
702
703 pub fn secondary_message<S>(mut self, message: S) -> Self
704 where
705 S: Into<SharedString>,
706 {
707 self.secondary_message = Some(message.into());
708 self
709 }
710
711 pub fn secondary_icon(mut self, icon: IconName) -> Self {
712 self.secondary_icon = Some(icon);
713 self
714 }
715
716 pub fn secondary_icon_color(mut self, color: Color) -> Self {
717 self.secondary_icon_color = Some(color);
718 self
719 }
720
721 pub fn secondary_on_click<F>(mut self, on_click: F) -> Self
722 where
723 F: 'static + Fn(&mut Window, &mut Context<Self>),
724 {
725 self.secondary_on_click = Some(Arc::new(on_click));
726 self
727 }
728
729 pub fn secondary_on_click_arc<F>(mut self, on_click: Arc<F>) -> Self
730 where
731 F: 'static + Fn(&mut Window, &mut Context<Self>),
732 {
733 self.secondary_on_click = Some(on_click);
734 self
735 }
736
737 pub fn more_info_message<S>(mut self, message: S) -> Self
738 where
739 S: Into<SharedString>,
740 {
741 self.more_info_message = Some(message.into());
742 self
743 }
744
745 pub fn more_info_url<S>(mut self, url: S) -> Self
746 where
747 S: Into<Arc<str>>,
748 {
749 self.more_info_url = Some(url.into());
750 self
751 }
752
753 pub fn dismiss(&mut self, cx: &mut Context<Self>) {
754 cx.emit(DismissEvent);
755 }
756
757 pub fn show_close_button(mut self, show: bool) -> Self {
758 self.show_close_button = show;
759 self
760 }
761
762 /// Determines whether the given notification ID should be suppressible
763 /// Suppressed notifications will not be shown anymor
764 pub fn show_suppress_button(mut self, show: bool) -> Self {
765 self.show_suppress_button = show;
766 self
767 }
768
769 pub fn with_title<S>(mut self, title: S) -> Self
770 where
771 S: Into<SharedString>,
772 {
773 self.title = Some(title.into());
774 self
775 }
776 }
777
778 impl Render for MessageNotification {
779 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
780 NotificationFrame::new()
781 .with_title(self.title.clone())
782 .with_content(
783 div()
784 .child(
785 div()
786 .id("message-notification-content")
787 .max_h(vh(0.6, window))
788 .overflow_y_scroll()
789 .track_scroll(&self.scroll_handle.clone())
790 .child((self.build_content)(window, cx)),
791 )
792 .vertical_scrollbar_for(&self.scroll_handle, window, cx),
793 )
794 .show_close_button(self.show_close_button)
795 .show_suppress_button(self.show_suppress_button)
796 .on_close(cx.listener(|_, suppress, _, cx| {
797 if *suppress {
798 cx.emit(SuppressEvent);
799 } else {
800 cx.emit(DismissEvent);
801 }
802 }))
803 .with_suffix(
804 h_flex()
805 .gap_1()
806 .children(self.primary_message.iter().map(|message| {
807 let mut button = Button::new(message.clone(), message.clone())
808 .label_size(LabelSize::Small)
809 .on_click(cx.listener(|this, _, window, cx| {
810 if let Some(on_click) = this.primary_on_click.as_ref() {
811 (on_click)(window, cx)
812 };
813 this.dismiss(cx)
814 }));
815
816 if let Some(icon) = self.primary_icon {
817 button = button
818 .icon(icon)
819 .icon_color(self.primary_icon_color.unwrap_or(Color::Muted))
820 .icon_position(IconPosition::Start)
821 .icon_size(IconSize::Small);
822 }
823
824 button
825 }))
826 .children(self.secondary_message.iter().map(|message| {
827 let mut button = Button::new(message.clone(), message.clone())
828 .label_size(LabelSize::Small)
829 .on_click(cx.listener(|this, _, window, cx| {
830 if let Some(on_click) = this.secondary_on_click.as_ref() {
831 (on_click)(window, cx)
832 };
833 this.dismiss(cx)
834 }));
835
836 if let Some(icon) = self.secondary_icon {
837 button = button
838 .icon(icon)
839 .icon_position(IconPosition::Start)
840 .icon_size(IconSize::Small)
841 .icon_color(self.secondary_icon_color.unwrap_or(Color::Muted));
842 }
843
844 button
845 }))
846 .child(
847 h_flex().w_full().justify_end().children(
848 self.more_info_message
849 .iter()
850 .zip(self.more_info_url.iter())
851 .map(|(message, url)| {
852 let url = url.clone();
853 Button::new(message.clone(), message.clone())
854 .label_size(LabelSize::Small)
855 .icon(IconName::ArrowUpRight)
856 .icon_size(IconSize::Indicator)
857 .icon_color(Color::Muted)
858 .on_click(cx.listener(move |_, _, _, cx| {
859 cx.open_url(&url);
860 }))
861 }),
862 ),
863 ),
864 )
865 }
866 }
867}
868
869static GLOBAL_APP_NOTIFICATIONS: LazyLock<Mutex<AppNotifications>> = LazyLock::new(|| {
870 Mutex::new(AppNotifications {
871 app_notifications: Vec::new(),
872 })
873});
874
875/// Stores app notifications so that they can be shown in new workspaces.
876struct AppNotifications {
877 app_notifications: Vec<(
878 NotificationId,
879 Arc<dyn Fn(&mut Context<Workspace>) -> AnyView + Send + Sync>,
880 )>,
881}
882
883impl AppNotifications {
884 pub fn insert(
885 &mut self,
886 id: NotificationId,
887 build_notification: Arc<dyn Fn(&mut Context<Workspace>) -> AnyView + Send + Sync>,
888 ) {
889 self.remove(&id);
890 self.app_notifications.push((id, build_notification))
891 }
892
893 pub fn remove(&mut self, id: &NotificationId) {
894 self.app_notifications
895 .retain(|(existing_id, _)| existing_id != id);
896 }
897}
898
899/// Shows a notification in all workspaces. New workspaces will also receive the notification - this
900/// is particularly to handle notifications that occur on initialization before any workspaces
901/// exist. If the notification is dismissed within any workspace, it will be removed from all.
902pub fn show_app_notification<V: Notification + 'static>(
903 id: NotificationId,
904 cx: &mut App,
905 build_notification: impl Fn(&mut Context<Workspace>) -> Entity<V> + 'static + Send + Sync,
906) {
907 // Defer notification creation so that windows on the stack can be returned to GPUI
908 cx.defer(move |cx| {
909 // Handle dismiss events by removing the notification from all workspaces.
910 let build_notification: Arc<dyn Fn(&mut Context<Workspace>) -> AnyView + Send + Sync> =
911 Arc::new({
912 let id = id.clone();
913 move |cx| {
914 let notification = build_notification(cx);
915 cx.subscribe(¬ification, {
916 let id = id.clone();
917 move |_, _, _: &DismissEvent, cx| {
918 dismiss_app_notification(&id, cx);
919 }
920 })
921 .detach();
922 cx.subscribe(¬ification, {
923 let id = id.clone();
924 move |workspace: &mut Workspace, _, _: &SuppressEvent, cx| {
925 workspace.suppress_notification(&id, cx);
926 }
927 })
928 .detach();
929 notification.into()
930 }
931 });
932
933 // Store the notification so that new workspaces also receive it.
934 GLOBAL_APP_NOTIFICATIONS
935 .lock()
936 .insert(id.clone(), build_notification.clone());
937
938 for window in cx.windows() {
939 if let Some(workspace_window) = window.downcast::<Workspace>() {
940 workspace_window
941 .update(cx, |workspace, _window, cx| {
942 workspace.show_notification_without_handling_dismiss_events(
943 &id,
944 cx,
945 |cx| build_notification(cx),
946 );
947 })
948 .ok(); // Doesn't matter if the windows are dropped
949 }
950 }
951 });
952}
953
954pub fn dismiss_app_notification(id: &NotificationId, cx: &mut App) {
955 let id = id.clone();
956 // Defer notification dismissal so that windows on the stack can be returned to GPUI
957 cx.defer(move |cx| {
958 GLOBAL_APP_NOTIFICATIONS.lock().remove(&id);
959 for window in cx.windows() {
960 if let Some(workspace_window) = window.downcast::<Workspace>() {
961 let id = id.clone();
962 workspace_window
963 .update(cx, |workspace, _window, cx| {
964 workspace.dismiss_notification(&id, cx)
965 })
966 .ok();
967 }
968 }
969 });
970}
971
972pub trait NotifyResultExt {
973 type Ok;
974
975 fn notify_err(self, workspace: &mut Workspace, cx: &mut Context<Workspace>)
976 -> Option<Self::Ok>;
977
978 fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<Self::Ok>;
979
980 /// Notifies the active workspace if there is one, otherwise notifies all workspaces.
981 fn notify_app_err(self, cx: &mut App) -> Option<Self::Ok>;
982}
983
984impl<T, E> NotifyResultExt for std::result::Result<T, E>
985where
986 E: std::fmt::Debug + std::fmt::Display,
987{
988 type Ok = T;
989
990 fn notify_err(self, workspace: &mut Workspace, cx: &mut Context<Workspace>) -> Option<T> {
991 match self {
992 Ok(value) => Some(value),
993 Err(err) => {
994 log::error!("Showing error notification in workspace: {err:?}");
995 workspace.show_error(&err, cx);
996 None
997 }
998 }
999 }
1000
1001 fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<T> {
1002 match self {
1003 Ok(value) => Some(value),
1004 Err(err) => {
1005 log::error!("{err:?}");
1006 cx.update_root(|view, _, cx| {
1007 if let Ok(workspace) = view.downcast::<Workspace>() {
1008 workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
1009 }
1010 })
1011 .ok();
1012 None
1013 }
1014 }
1015 }
1016
1017 fn notify_app_err(self, cx: &mut App) -> Option<T> {
1018 match self {
1019 Ok(value) => Some(value),
1020 Err(err) => {
1021 let message: SharedString = format!("Error: {err}").into();
1022 log::error!("Showing error notification in app: {message}");
1023 show_app_notification(workspace_error_notification_id(), cx, {
1024 move |cx| {
1025 cx.new({
1026 let message = message.clone();
1027 move |cx| ErrorMessagePrompt::new(message, cx)
1028 })
1029 }
1030 });
1031
1032 None
1033 }
1034 }
1035 }
1036}
1037
1038pub trait NotifyTaskExt {
1039 fn detach_and_notify_err(self, window: &mut Window, cx: &mut App);
1040}
1041
1042impl<R, E> NotifyTaskExt for Task<std::result::Result<R, E>>
1043where
1044 E: std::fmt::Debug + std::fmt::Display + Sized + 'static,
1045 R: 'static,
1046{
1047 fn detach_and_notify_err(self, window: &mut Window, cx: &mut App) {
1048 window
1049 .spawn(cx, async move |cx| self.await.notify_async_err(cx))
1050 .detach();
1051 }
1052}
1053
1054pub trait DetachAndPromptErr<R> {
1055 fn prompt_err(
1056 self,
1057 msg: &str,
1058 window: &Window,
1059 cx: &App,
1060 f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option<String> + 'static,
1061 ) -> Task<Option<R>>;
1062
1063 fn detach_and_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 );
1070}
1071
1072impl<R> DetachAndPromptErr<R> for Task<anyhow::Result<R>>
1073where
1074 R: 'static,
1075{
1076 fn prompt_err(
1077 self,
1078 msg: &str,
1079 window: &Window,
1080 cx: &App,
1081 f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option<String> + 'static,
1082 ) -> Task<Option<R>> {
1083 let msg = msg.to_owned();
1084 window.spawn(cx, async move |cx| {
1085 let result = self.await;
1086 if let Err(err) = result.as_ref() {
1087 log::error!("{err:#}");
1088 if let Ok(prompt) = cx.update(|window, cx| {
1089 let mut display = format!("{err:#}");
1090 if !display.ends_with('\n') {
1091 display.push('.');
1092 display.push(' ')
1093 }
1094 let detail =
1095 f(err, window, cx).unwrap_or_else(|| format!("{display}Please try again."));
1096 window.prompt(PromptLevel::Critical, &msg, Some(&detail), &["Ok"], cx)
1097 }) {
1098 prompt.await.ok();
1099 }
1100 return None;
1101 }
1102 Some(result.unwrap())
1103 })
1104 }
1105
1106 fn detach_and_prompt_err(
1107 self,
1108 msg: &str,
1109 window: &Window,
1110 cx: &App,
1111 f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option<String> + 'static,
1112 ) {
1113 self.prompt_err(msg, window, cx, f).detach();
1114 }
1115}