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