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