notifications.rs

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