notifications.rs

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