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(), cx)
 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(
 446                                    "error_message_prompt_notification_button",
 447                                    label,
 448                                    cx,
 449                                )
 450                                .on_click(move |_, _, cx| cx.open_url(&url)),
 451                            ),
 452                        )
 453                    }),
 454            )
 455    }
 456}
 457
 458impl Focusable for ErrorMessagePrompt {
 459    fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
 460        self.focus_handle.clone()
 461    }
 462}
 463
 464impl EventEmitter<DismissEvent> for ErrorMessagePrompt {}
 465impl EventEmitter<SuppressEvent> for ErrorMessagePrompt {}
 466
 467impl Notification for ErrorMessagePrompt {}
 468
 469pub mod simple_message_notification {
 470    use std::sync::Arc;
 471
 472    use gpui::{
 473        AnyElement, ClickEvent, DismissEvent, EventEmitter, FocusHandle, Focusable, ParentElement,
 474        Render, SharedString, Styled, div,
 475    };
 476    use ui::{Tooltip, prelude::*};
 477
 478    use crate::SuppressNotification;
 479
 480    use super::{Notification, SuppressEvent};
 481
 482    pub struct MessageNotification {
 483        focus_handle: FocusHandle,
 484        build_content: Box<dyn Fn(&mut Window, &mut Context<Self>) -> AnyElement>,
 485        primary_message: Option<SharedString>,
 486        primary_icon: Option<IconName>,
 487        primary_icon_color: Option<Color>,
 488        primary_on_click: Option<Arc<dyn Fn(&mut Window, &mut Context<Self>)>>,
 489        secondary_message: Option<SharedString>,
 490        secondary_icon: Option<IconName>,
 491        secondary_icon_color: Option<Color>,
 492        secondary_on_click: Option<Arc<dyn Fn(&mut Window, &mut Context<Self>)>>,
 493        more_info_message: Option<SharedString>,
 494        more_info_url: Option<Arc<str>>,
 495        show_close_button: bool,
 496        show_suppress_button: bool,
 497        title: Option<SharedString>,
 498    }
 499
 500    impl Focusable for MessageNotification {
 501        fn focus_handle(&self, _: &App) -> FocusHandle {
 502            self.focus_handle.clone()
 503        }
 504    }
 505
 506    impl EventEmitter<DismissEvent> for MessageNotification {}
 507    impl EventEmitter<SuppressEvent> for MessageNotification {}
 508
 509    impl Notification for MessageNotification {}
 510
 511    impl MessageNotification {
 512        pub fn new<S>(message: S, cx: &mut App) -> MessageNotification
 513        where
 514            S: Into<SharedString>,
 515        {
 516            let message = message.into();
 517            Self::new_from_builder(cx, move |_, _| {
 518                Label::new(message.clone()).into_any_element()
 519            })
 520        }
 521
 522        pub fn new_from_builder<F>(cx: &mut App, content: F) -> MessageNotification
 523        where
 524            F: 'static + Fn(&mut Window, &mut Context<Self>) -> AnyElement,
 525        {
 526            Self {
 527                build_content: Box::new(content),
 528                primary_message: None,
 529                primary_icon: None,
 530                primary_icon_color: None,
 531                primary_on_click: None,
 532                secondary_message: None,
 533                secondary_icon: None,
 534                secondary_icon_color: None,
 535                secondary_on_click: None,
 536                more_info_message: None,
 537                more_info_url: None,
 538                show_close_button: true,
 539                show_suppress_button: true,
 540                title: None,
 541                focus_handle: cx.focus_handle(),
 542            }
 543        }
 544
 545        pub fn primary_message<S>(mut self, message: S) -> Self
 546        where
 547            S: Into<SharedString>,
 548        {
 549            self.primary_message = Some(message.into());
 550            self
 551        }
 552
 553        pub fn primary_icon(mut self, icon: IconName) -> Self {
 554            self.primary_icon = Some(icon);
 555            self
 556        }
 557
 558        pub fn primary_icon_color(mut self, color: Color) -> Self {
 559            self.primary_icon_color = Some(color);
 560            self
 561        }
 562
 563        pub fn primary_on_click<F>(mut self, on_click: F) -> Self
 564        where
 565            F: 'static + Fn(&mut Window, &mut Context<Self>),
 566        {
 567            self.primary_on_click = Some(Arc::new(on_click));
 568            self
 569        }
 570
 571        pub fn primary_on_click_arc<F>(mut self, on_click: Arc<F>) -> Self
 572        where
 573            F: 'static + Fn(&mut Window, &mut Context<Self>),
 574        {
 575            self.primary_on_click = Some(on_click);
 576            self
 577        }
 578
 579        pub fn secondary_message<S>(mut self, message: S) -> Self
 580        where
 581            S: Into<SharedString>,
 582        {
 583            self.secondary_message = Some(message.into());
 584            self
 585        }
 586
 587        pub fn secondary_icon(mut self, icon: IconName) -> Self {
 588            self.secondary_icon = Some(icon);
 589            self
 590        }
 591
 592        pub fn secondary_icon_color(mut self, color: Color) -> Self {
 593            self.secondary_icon_color = Some(color);
 594            self
 595        }
 596
 597        pub fn secondary_on_click<F>(mut self, on_click: F) -> Self
 598        where
 599            F: 'static + Fn(&mut Window, &mut Context<Self>),
 600        {
 601            self.secondary_on_click = Some(Arc::new(on_click));
 602            self
 603        }
 604
 605        pub fn secondary_on_click_arc<F>(mut self, on_click: Arc<F>) -> Self
 606        where
 607            F: 'static + Fn(&mut Window, &mut Context<Self>),
 608        {
 609            self.secondary_on_click = Some(on_click);
 610            self
 611        }
 612
 613        pub fn more_info_message<S>(mut self, message: S) -> Self
 614        where
 615            S: Into<SharedString>,
 616        {
 617            self.more_info_message = Some(message.into());
 618            self
 619        }
 620
 621        pub fn more_info_url<S>(mut self, url: S) -> Self
 622        where
 623            S: Into<Arc<str>>,
 624        {
 625            self.more_info_url = Some(url.into());
 626            self
 627        }
 628
 629        pub fn dismiss(&mut self, cx: &mut Context<Self>) {
 630            cx.emit(DismissEvent);
 631        }
 632
 633        pub fn show_close_button(mut self, show: bool) -> Self {
 634            self.show_close_button = show;
 635            self
 636        }
 637
 638        pub fn show_suppress_button(mut self, show: bool) -> Self {
 639            self.show_suppress_button = show;
 640            self
 641        }
 642
 643        pub fn with_title<S>(mut self, title: S) -> Self
 644        where
 645            S: Into<SharedString>,
 646        {
 647            self.title = Some(title.into());
 648            self
 649        }
 650    }
 651
 652    impl Render for MessageNotification {
 653        fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 654            let show_suppress_button = self.show_suppress_button;
 655            let suppress = show_suppress_button && window.modifiers().shift;
 656            let (close_id, close_icon) = if suppress {
 657                ("suppress", IconName::Minimize)
 658            } else {
 659                ("close", IconName::Close)
 660            };
 661
 662            v_flex()
 663                .occlude()
 664                .p_3()
 665                .gap_2()
 666                .elevation_3(cx)
 667                .child(
 668                    h_flex()
 669                        .gap_4()
 670                        .justify_between()
 671                        .items_start()
 672                        .child(
 673                            v_flex()
 674                                .gap_0p5()
 675                                .when_some(self.title.clone(), |element, title| {
 676                                    element.child(Label::new(title))
 677                                })
 678                                .child(div().max_w_96().child((self.build_content)(window, cx))),
 679                        )
 680                        .when(self.show_close_button, |this| {
 681                            this.on_modifiers_changed(cx.listener(|_, _, _, cx| cx.notify()))
 682                                .child(
 683                                    IconButton::new(close_id, close_icon)
 684                                        .tooltip(move |window, cx| {
 685                                            if suppress {
 686                                                Tooltip::for_action(
 687                                                    "Suppress.\nClose with click.",
 688                                                    &SuppressNotification,
 689                                                    window,
 690                                                    cx,
 691                                                )
 692                                            } else if show_suppress_button {
 693                                                Tooltip::for_action(
 694                                                    "Close.\nSuppress with shift-click.",
 695                                                    &menu::Cancel,
 696                                                    window,
 697                                                    cx,
 698                                                )
 699                                            } else {
 700                                                Tooltip::for_action(
 701                                                    "Close",
 702                                                    &menu::Cancel,
 703                                                    window,
 704                                                    cx,
 705                                                )
 706                                            }
 707                                        })
 708                                        .on_click(cx.listener(move |_, _: &ClickEvent, _, cx| {
 709                                            if suppress {
 710                                                cx.emit(SuppressEvent);
 711                                            } else {
 712                                                cx.emit(DismissEvent);
 713                                            }
 714                                        })),
 715                                )
 716                        }),
 717                )
 718                .child(
 719                    h_flex()
 720                        .gap_1()
 721                        .children(self.primary_message.iter().map(|message| {
 722                            let mut button = Button::new(message.clone(), message.clone(), cx)
 723                                .label_size(LabelSize::Small)
 724                                .on_click(cx.listener(|this, _, window, cx| {
 725                                    if let Some(on_click) = this.primary_on_click.as_ref() {
 726                                        (on_click)(window, cx)
 727                                    };
 728                                    this.dismiss(cx)
 729                                }));
 730
 731                            if let Some(icon) = self.primary_icon {
 732                                button = button
 733                                    .icon(icon)
 734                                    .icon_color(self.primary_icon_color.unwrap_or(Color::Muted))
 735                                    .icon_position(IconPosition::Start)
 736                                    .icon_size(IconSize::Small);
 737                            }
 738
 739                            button
 740                        }))
 741                        .children(self.secondary_message.iter().map(|message| {
 742                            let mut button = Button::new(message.clone(), message.clone(), cx)
 743                                .label_size(LabelSize::Small)
 744                                .on_click(cx.listener(|this, _, window, cx| {
 745                                    if let Some(on_click) = this.secondary_on_click.as_ref() {
 746                                        (on_click)(window, cx)
 747                                    };
 748                                    this.dismiss(cx)
 749                                }));
 750
 751                            if let Some(icon) = self.secondary_icon {
 752                                button = button
 753                                    .icon(icon)
 754                                    .icon_position(IconPosition::Start)
 755                                    .icon_size(IconSize::Small)
 756                                    .icon_color(self.secondary_icon_color.unwrap_or(Color::Muted));
 757                            }
 758
 759                            button
 760                        }))
 761                        .child(
 762                            h_flex().w_full().justify_end().children(
 763                                self.more_info_message
 764                                    .iter()
 765                                    .zip(self.more_info_url.iter())
 766                                    .map(|(message, url)| {
 767                                        let url = url.clone();
 768                                        Button::new(message.clone(), message.clone(), cx)
 769                                            .label_size(LabelSize::Small)
 770                                            .icon(IconName::ArrowUpRight)
 771                                            .icon_size(IconSize::Indicator)
 772                                            .icon_color(Color::Muted)
 773                                            .on_click(cx.listener(move |_, _, _, cx| {
 774                                                cx.open_url(&url);
 775                                            }))
 776                                    }),
 777                            ),
 778                        ),
 779                )
 780        }
 781    }
 782}
 783
 784static GLOBAL_APP_NOTIFICATIONS: LazyLock<Mutex<AppNotifications>> = LazyLock::new(|| {
 785    Mutex::new(AppNotifications {
 786        app_notifications: Vec::new(),
 787    })
 788});
 789
 790/// Stores app notifications so that they can be shown in new workspaces.
 791struct AppNotifications {
 792    app_notifications: Vec<(
 793        NotificationId,
 794        Arc<dyn Fn(&mut Context<Workspace>) -> AnyView + Send + Sync>,
 795    )>,
 796}
 797
 798impl AppNotifications {
 799    pub fn insert(
 800        &mut self,
 801        id: NotificationId,
 802        build_notification: Arc<dyn Fn(&mut Context<Workspace>) -> AnyView + Send + Sync>,
 803    ) {
 804        self.remove(&id);
 805        self.app_notifications.push((id, build_notification))
 806    }
 807
 808    pub fn remove(&mut self, id: &NotificationId) {
 809        self.app_notifications
 810            .retain(|(existing_id, _)| existing_id != id);
 811    }
 812}
 813
 814/// Shows a notification in all workspaces. New workspaces will also receive the notification - this
 815/// is particularly to handle notifications that occur on initialization before any workspaces
 816/// exist. If the notification is dismissed within any workspace, it will be removed from all.
 817pub fn show_app_notification<V: Notification + 'static>(
 818    id: NotificationId,
 819    cx: &mut App,
 820    build_notification: impl Fn(&mut Context<Workspace>) -> Entity<V> + 'static + Send + Sync,
 821) {
 822    // Defer notification creation so that windows on the stack can be returned to GPUI
 823    cx.defer(move |cx| {
 824        // Handle dismiss events by removing the notification from all workspaces.
 825        let build_notification: Arc<dyn Fn(&mut Context<Workspace>) -> AnyView + Send + Sync> =
 826            Arc::new({
 827                let id = id.clone();
 828                move |cx| {
 829                    let notification = build_notification(cx);
 830                    cx.subscribe(&notification, {
 831                        let id = id.clone();
 832                        move |_, _, _: &DismissEvent, cx| {
 833                            dismiss_app_notification(&id, cx);
 834                        }
 835                    })
 836                    .detach();
 837                    cx.subscribe(&notification, {
 838                        let id = id.clone();
 839                        move |workspace: &mut Workspace, _, _: &SuppressEvent, cx| {
 840                            workspace.suppress_notification(&id, cx);
 841                        }
 842                    })
 843                    .detach();
 844                    notification.into()
 845                }
 846            });
 847
 848        // Store the notification so that new workspaces also receive it.
 849        GLOBAL_APP_NOTIFICATIONS
 850            .lock()
 851            .insert(id.clone(), build_notification.clone());
 852
 853        for window in cx.windows() {
 854            if let Some(workspace_window) = window.downcast::<Workspace>() {
 855                workspace_window
 856                    .update(cx, |workspace, _window, cx| {
 857                        workspace.show_notification_without_handling_dismiss_events(
 858                            &id,
 859                            cx,
 860                            |cx| build_notification(cx),
 861                        );
 862                    })
 863                    .ok(); // Doesn't matter if the windows are dropped
 864            }
 865        }
 866    });
 867}
 868
 869pub fn dismiss_app_notification(id: &NotificationId, cx: &mut App) {
 870    let id = id.clone();
 871    // Defer notification dismissal so that windows on the stack can be returned to GPUI
 872    cx.defer(move |cx| {
 873        GLOBAL_APP_NOTIFICATIONS.lock().remove(&id);
 874        for window in cx.windows() {
 875            if let Some(workspace_window) = window.downcast::<Workspace>() {
 876                let id = id.clone();
 877                workspace_window
 878                    .update(cx, |workspace, _window, cx| {
 879                        workspace.dismiss_notification(&id, cx)
 880                    })
 881                    .ok();
 882            }
 883        }
 884    });
 885}
 886
 887pub trait NotifyResultExt {
 888    type Ok;
 889
 890    fn notify_err(self, workspace: &mut Workspace, cx: &mut Context<Workspace>)
 891    -> Option<Self::Ok>;
 892
 893    fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<Self::Ok>;
 894
 895    /// Notifies the active workspace if there is one, otherwise notifies all workspaces.
 896    fn notify_app_err(self, cx: &mut App) -> Option<Self::Ok>;
 897}
 898
 899impl<T, E> NotifyResultExt for std::result::Result<T, E>
 900where
 901    E: std::fmt::Debug + std::fmt::Display,
 902{
 903    type Ok = T;
 904
 905    fn notify_err(self, workspace: &mut Workspace, cx: &mut Context<Workspace>) -> Option<T> {
 906        match self {
 907            Ok(value) => Some(value),
 908            Err(err) => {
 909                log::error!("Showing error notification in workspace: {err:?}");
 910                workspace.show_error(&err, cx);
 911                None
 912            }
 913        }
 914    }
 915
 916    fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option<T> {
 917        match self {
 918            Ok(value) => Some(value),
 919            Err(err) => {
 920                log::error!("{err:?}");
 921                cx.update_root(|view, _, cx| {
 922                    if let Ok(workspace) = view.downcast::<Workspace>() {
 923                        workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx))
 924                    }
 925                })
 926                .ok();
 927                None
 928            }
 929        }
 930    }
 931
 932    fn notify_app_err(self, cx: &mut App) -> Option<T> {
 933        match self {
 934            Ok(value) => Some(value),
 935            Err(err) => {
 936                let message: SharedString = format!("Error: {err}").into();
 937                log::error!("Showing error notification in app: {message}");
 938                show_app_notification(workspace_error_notification_id(), cx, {
 939                    let message = message.clone();
 940                    move |cx| {
 941                        cx.new({
 942                            let message = message.clone();
 943                            move |cx| ErrorMessagePrompt::new(message, cx)
 944                        })
 945                    }
 946                });
 947
 948                None
 949            }
 950        }
 951    }
 952}
 953
 954pub trait NotifyTaskExt {
 955    fn detach_and_notify_err(self, window: &mut Window, cx: &mut App);
 956}
 957
 958impl<R, E> NotifyTaskExt for Task<std::result::Result<R, E>>
 959where
 960    E: std::fmt::Debug + std::fmt::Display + Sized + 'static,
 961    R: 'static,
 962{
 963    fn detach_and_notify_err(self, window: &mut Window, cx: &mut App) {
 964        window
 965            .spawn(cx, async move |mut cx| self.await.notify_async_err(&mut cx))
 966            .detach();
 967    }
 968}
 969
 970pub trait DetachAndPromptErr<R> {
 971    fn prompt_err(
 972        self,
 973        msg: &str,
 974        window: &Window,
 975        cx: &App,
 976        f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option<String> + 'static,
 977    ) -> Task<Option<R>>;
 978
 979    fn detach_and_prompt_err(
 980        self,
 981        msg: &str,
 982        window: &Window,
 983        cx: &App,
 984        f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option<String> + 'static,
 985    );
 986}
 987
 988impl<R> DetachAndPromptErr<R> for Task<anyhow::Result<R>>
 989where
 990    R: 'static,
 991{
 992    fn prompt_err(
 993        self,
 994        msg: &str,
 995        window: &Window,
 996        cx: &App,
 997        f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option<String> + 'static,
 998    ) -> Task<Option<R>> {
 999        let msg = msg.to_owned();
1000        window.spawn(cx, async move |cx| {
1001            let result = self.await;
1002            if let Err(err) = result.as_ref() {
1003                log::error!("{err:?}");
1004                if let Ok(prompt) = cx.update(|window, cx| {
1005                    let detail =
1006                        f(err, window, cx).unwrap_or_else(|| format!("{err}. Please try again."));
1007                    window.prompt(PromptLevel::Critical, &msg, Some(&detail), &["Ok"], cx)
1008                }) {
1009                    prompt.await.ok();
1010                }
1011                return None;
1012            }
1013            Some(result.unwrap())
1014        })
1015    }
1016
1017    fn detach_and_prompt_err(
1018        self,
1019        msg: &str,
1020        window: &Window,
1021        cx: &App,
1022        f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option<String> + 'static,
1023    ) {
1024        self.prompt_err(msg, window, cx, f).detach();
1025    }
1026}