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