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