Allow modals to override their dismissal (#3565)

Joseph T. Lyons created

Release Notes:

- N/A

Change summary

crates/collab_ui2/src/collab_panel/channel_modal.rs  |  2 
crates/collab_ui2/src/collab_panel/contact_finder.rs |  2 
crates/command_palette2/src/command_palette.rs       |  4 
crates/feedback2/src/feedback_modal.rs               | 69 +++++++------
crates/file_finder2/src/file_finder.rs               |  4 
crates/go_to_line2/src/go_to_line.rs                 |  3 
crates/language_selector2/src/language_selector.rs   |  3 
crates/outline2/src/outline.rs                       |  3 
crates/recent_projects2/src/recent_projects.rs       |  6 
crates/theme_selector2/src/theme_selector.rs         |  4 
crates/welcome2/src/base_keymap_picker.rs            |  3 
crates/workspace2/src/modal_layer.rs                 | 68 ++++++++++---
crates/workspace2/src/workspace2.rs                  |  2 
13 files changed, 115 insertions(+), 58 deletions(-)

Detailed changes

crates/collab_ui2/src/collab_panel/channel_modal.rs 🔗

@@ -13,6 +13,7 @@ use picker::{Picker, PickerDelegate};
 use std::sync::Arc;
 use ui::prelude::*;
 use util::TryFutureExt;
+use workspace::ModalView;
 
 actions!(
     SelectNextControl,
@@ -140,6 +141,7 @@ impl ChannelModal {
 }
 
 impl EventEmitter<DismissEvent> for ChannelModal {}
+impl ModalView for ChannelModal {}
 
 impl FocusableView for ChannelModal {
     fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {

crates/collab_ui2/src/collab_panel/contact_finder.rs 🔗

@@ -9,6 +9,7 @@ use std::sync::Arc;
 use theme::ActiveTheme as _;
 use ui::prelude::*;
 use util::{ResultExt as _, TryFutureExt};
+use workspace::ModalView;
 
 pub fn init(cx: &mut AppContext) {
     //Picker::<ContactFinderDelegate>::init(cx);
@@ -95,6 +96,7 @@ pub struct ContactFinderDelegate {
 }
 
 impl EventEmitter<DismissEvent> for ContactFinder {}
+impl ModalView for ContactFinder {}
 
 impl FocusableView for ContactFinder {
     fn focus_handle(&self, cx: &AppContext) -> FocusHandle {

crates/command_palette2/src/command_palette.rs 🔗

@@ -16,7 +16,7 @@ use util::{
     channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
     ResultExt,
 };
-use workspace::Workspace;
+use workspace::{ModalView, Workspace};
 use zed_actions::OpenZedURL;
 
 actions!(Toggle);
@@ -26,6 +26,8 @@ pub fn init(cx: &mut AppContext) {
     cx.observe_new_views(CommandPalette::register).detach();
 }
 
+impl ModalView for CommandPalette {}
+
 pub struct CommandPalette {
     picker: View<Picker<CommandPaletteDelegate>>,
 }

crates/feedback2/src/feedback_modal.rs 🔗

@@ -4,7 +4,7 @@ use anyhow::bail;
 use client::{Client, ZED_SECRET_CLIENT_TOKEN, ZED_SERVER_URL};
 use db::kvp::KEY_VALUE_STORE;
 use editor::{Editor, EditorEvent};
-use futures::AsyncReadExt;
+use futures::{AsyncReadExt, Future};
 use gpui::{
     div, rems, serde_json, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
     Model, PromptLevel, Render, Task, View, ViewContext,
@@ -16,7 +16,7 @@ use regex::Regex;
 use serde_derive::Serialize;
 use ui::{prelude::*, Button, ButtonStyle, IconPosition, Tooltip};
 use util::ResultExt;
-use workspace::Workspace;
+use workspace::{ModalView, Workspace};
 
 use crate::{system_specs::SystemSpecs, GiveFeedback, OpenZedCommunityRepo};
 
@@ -52,6 +52,13 @@ impl FocusableView for FeedbackModal {
 }
 impl EventEmitter<DismissEvent> for FeedbackModal {}
 
+impl ModalView for FeedbackModal {
+    fn dismiss(&mut self, cx: &mut ViewContext<Self>) -> Task<bool> {
+        let prompt = Self::prompt_dismiss(cx);
+        cx.spawn(|_, _| prompt)
+    }
+}
+
 impl FeedbackModal {
     pub fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
         let _handle = cx.view().downgrade();
@@ -105,7 +112,7 @@ impl FeedbackModal {
         let feedback_editor = cx.build_view(|cx| {
             let mut editor = Editor::for_buffer(buffer, Some(project.clone()), cx);
             editor.set_placeholder_text(
-                "You can use markdown to add links or organize feedback.",
+                "You can use markdown to organize your feedback wiht add code and links, or organize feedback.",
                 cx,
             );
             // editor.set_show_gutter(false, cx);
@@ -238,11 +245,29 @@ impl FeedbackModal {
     }
 
     // TODO: Escape button calls dismiss
-    // TODO: Should do same as hitting cancel / clicking outside of modal
-    //     Close immediately if no text in field
-    //     Ask to close if text in the field
     fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
-        cx.emit(DismissEvent);
+        self.dismiss_event(cx)
+    }
+
+    fn dismiss_event(&mut self, cx: &mut ViewContext<Self>) {
+        let has_feedback = self.feedback_editor.read(cx).text_option(cx).is_some();
+        let dismiss = Self::prompt_dismiss(cx);
+
+        cx.spawn(|this, mut cx| async move {
+            if !has_feedback || (has_feedback && dismiss.await) {
+                this.update(&mut cx, |_, cx| cx.emit(DismissEvent)).ok();
+            }
+        })
+        .detach()
+    }
+
+    fn prompt_dismiss(cx: &mut ViewContext<Self>) -> impl Future<Output = bool> {
+        let answer = cx.prompt(PromptLevel::Info, "Discard feedback?", &["Yes", "No"]);
+
+        async {
+            let answer = answer.await.ok();
+            answer == Some(0)
+        }
     }
 }
 
@@ -260,27 +285,12 @@ impl Render for FeedbackModal {
         let allow_submission =
             valid_character_count && valid_email_address && !self.pending_submission;
 
-        let has_feedback = self.feedback_editor.read(cx).text_option(cx).is_some();
-
         let submit_button_text = if self.pending_submission {
             "Submitting..."
         } else {
             "Submit"
         };
-        let dismiss = cx.listener(|_, _, cx| {
-            cx.emit(DismissEvent);
-        });
-        // TODO: get the "are you sure you want to dismiss?" prompt here working
-        let dismiss_prompt = cx.listener(|_, _, _| {
-            // let answer = cx.prompt(PromptLevel::Info, "Exit feedback?", &["Yes", "No"]);
-            // cx.spawn(|_, _| async move {
-            //     let answer = answer.await.ok();
-            //     if answer == Some(0) {
-            //         cx.emit(DismissEvent);
-            //     }
-            // })
-            // .detach();
-        });
+
         let open_community_repo =
             cx.listener(|_, _, cx| cx.dispatch_action(Box::new(OpenZedCommunityRepo)));
 
@@ -364,15 +374,9 @@ impl Render for FeedbackModal {
                                         Button::new("cancel_feedback", "Cancel")
                                             .style(ButtonStyle::Subtle)
                                             .color(Color::Muted)
-                                            // TODO: replicate this logic when clicking outside the modal
-                                            // TODO: Will require somehow overriding the modal dismal default behavior
-                                            .map(|this| {
-                                                if has_feedback {
-                                                    this.on_click(dismiss_prompt)
-                                                } else {
-                                                    this.on_click(dismiss)
-                                                }
-                                            }),
+                                            .on_click(cx.listener(move |this, _, cx| {
+                                                this.dismiss_event(cx)
+                                            })),
                                     )
                                     .child(
                                         Button::new("send_feedback", submit_button_text)
@@ -402,3 +406,4 @@ impl Render for FeedbackModal {
 
 // TODO: Add compilation flags to enable debug mode, where we can simulate sending feedback that both succeeds and fails, so we can test the UI
 // TODO: Maybe store email address whenever the modal is closed, versus just on submit, so users can remove it if they want without submitting
+// TODO: Fix bug of being asked twice to discard feedback when clicking cancel

crates/file_finder2/src/file_finder.rs 🔗

@@ -17,10 +17,12 @@ use std::{
 use text::Point;
 use ui::{prelude::*, HighlightedLabel, ListItem};
 use util::{paths::PathLikeWithPosition, post_inc, ResultExt};
-use workspace::Workspace;
+use workspace::{ModalView, Workspace};
 
 actions!(Toggle);
 
+impl ModalView for FileFinder {}
+
 pub struct FileFinder {
     picker: View<Picker<FileFinderDelegate>>,
 }

crates/go_to_line2/src/go_to_line.rs 🔗

@@ -8,6 +8,7 @@ use text::{Bias, Point};
 use theme::ActiveTheme;
 use ui::{h_stack, prelude::*, v_stack, Label};
 use util::paths::FILE_ROW_COLUMN_DELIMITER;
+use workspace::ModalView;
 
 actions!(Toggle);
 
@@ -23,6 +24,8 @@ pub struct GoToLine {
     _subscriptions: Vec<Subscription>,
 }
 
+impl ModalView for GoToLine {}
+
 impl FocusableView for GoToLine {
     fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
         self.line_editor.focus_handle(cx)

crates/language_selector2/src/language_selector.rs 🔗

@@ -14,7 +14,7 @@ use project::Project;
 use std::sync::Arc;
 use ui::{prelude::*, HighlightedLabel, ListItem};
 use util::ResultExt;
-use workspace::Workspace;
+use workspace::{ModalView, Workspace};
 
 actions!(Toggle);
 
@@ -81,6 +81,7 @@ impl FocusableView for LanguageSelector {
 }
 
 impl EventEmitter<DismissEvent> for LanguageSelector {}
+impl ModalView for LanguageSelector {}
 
 pub struct LanguageSelectorDelegate {
     language_selector: WeakView<LanguageSelector>,

crates/outline2/src/outline.rs 🔗

@@ -20,7 +20,7 @@ use std::{
 use theme::{color_alpha, ActiveTheme, ThemeSettings};
 use ui::{prelude::*, ListItem};
 use util::ResultExt;
-use workspace::Workspace;
+use workspace::{ModalView, Workspace};
 
 actions!(Toggle);
 
@@ -57,6 +57,7 @@ impl FocusableView for OutlineView {
 }
 
 impl EventEmitter<DismissEvent> for OutlineView {}
+impl ModalView for OutlineView {}
 
 impl Render for OutlineView {
     type Element = Div;

crates/recent_projects2/src/recent_projects.rs 🔗

@@ -13,8 +13,8 @@ use std::sync::Arc;
 use ui::{prelude::*, ListItem};
 use util::paths::PathExt;
 use workspace::{
-    notifications::simple_message_notification::MessageNotification, Workspace, WorkspaceLocation,
-    WORKSPACE_DB,
+    notifications::simple_message_notification::MessageNotification, ModalView, Workspace,
+    WorkspaceLocation, WORKSPACE_DB,
 };
 
 pub use projects::OpenRecent;
@@ -27,6 +27,8 @@ pub struct RecentProjects {
     picker: View<Picker<RecentProjectsDelegate>>,
 }
 
+impl ModalView for RecentProjects {}
+
 impl RecentProjects {
     fn new(delegate: RecentProjectsDelegate, cx: &mut ViewContext<Self>) -> Self {
         Self {

crates/theme_selector2/src/theme_selector.rs 🔗

@@ -11,7 +11,7 @@ use std::sync::Arc;
 use theme::{Theme, ThemeMeta, ThemeRegistry, ThemeSettings};
 use ui::{prelude::*, v_stack, ListItem};
 use util::ResultExt;
-use workspace::{ui::HighlightedLabel, Workspace};
+use workspace::{ui::HighlightedLabel, ModalView, Workspace};
 
 actions!(Toggle, Reload);
 
@@ -52,6 +52,8 @@ pub fn reload(cx: &mut AppContext) {
     }
 }
 
+impl ModalView for ThemeSelector {}
+
 pub struct ThemeSelector {
     picker: View<Picker<ThemeSelectorDelegate>>,
 }

crates/welcome2/src/base_keymap_picker.rs 🔗

@@ -10,7 +10,7 @@ use settings::{update_settings_file, Settings};
 use std::sync::Arc;
 use ui::{prelude::*, ListItem};
 use util::ResultExt;
-use workspace::{ui::HighlightedLabel, Workspace};
+use workspace::{ui::HighlightedLabel, ModalView, Workspace};
 
 actions!(ToggleBaseKeymapSelector);
 
@@ -47,6 +47,7 @@ impl FocusableView for BaseKeymapSelector {
 }
 
 impl EventEmitter<DismissEvent> for BaseKeymapSelector {}
+impl ModalView for BaseKeymapSelector {}
 
 impl BaseKeymapSelector {
     pub fn new(

crates/workspace2/src/modal_layer.rs 🔗

@@ -1,11 +1,32 @@
 use gpui::{
-    div, prelude::*, px, AnyView, Div, FocusHandle, ManagedView, Render, Subscription, View,
-    ViewContext,
+    div, prelude::*, px, AnyView, Div, FocusHandle, ManagedView, Render, Subscription, Task, View,
+    ViewContext, WindowContext,
 };
 use ui::{h_stack, v_stack};
 
+pub trait ModalView: ManagedView {
+    fn dismiss(&mut self, cx: &mut ViewContext<Self>) -> Task<bool> {
+        Task::ready(true)
+    }
+}
+
+trait ModalViewHandle {
+    fn should_dismiss(&mut self, cx: &mut WindowContext) -> Task<bool>;
+    fn view(&self) -> AnyView;
+}
+
+impl<V: ModalView> ModalViewHandle for View<V> {
+    fn should_dismiss(&mut self, cx: &mut WindowContext) -> Task<bool> {
+        self.update(cx, |this, cx| this.dismiss(cx))
+    }
+
+    fn view(&self) -> AnyView {
+        self.clone().into()
+    }
+}
+
 pub struct ActiveModal {
-    modal: AnyView,
+    modal: Box<dyn ModalViewHandle>,
     subscription: Subscription,
     previous_focus_handle: Option<FocusHandle>,
     focus_handle: FocusHandle,
@@ -22,11 +43,11 @@ impl ModalLayer {
 
     pub fn toggle_modal<V, B>(&mut self, cx: &mut ViewContext<Self>, build_view: B)
     where
-        V: ManagedView,
+        V: ModalView,
         B: FnOnce(&mut ViewContext<V>) -> V,
     {
         if let Some(active_modal) = &self.active_modal {
-            let is_close = active_modal.modal.clone().downcast::<V>().is_ok();
+            let is_close = active_modal.modal.view().downcast::<V>().is_ok();
             self.hide_modal(cx);
             if is_close {
                 return;
@@ -38,10 +59,10 @@ impl ModalLayer {
 
     pub fn show_modal<V>(&mut self, new_modal: View<V>, cx: &mut ViewContext<Self>)
     where
-        V: ManagedView,
+        V: ModalView,
     {
         self.active_modal = Some(ActiveModal {
-            modal: new_modal.clone().into(),
+            modal: Box::new(new_modal.clone()),
             subscription: cx.subscribe(&new_modal, |this, modal, e, cx| this.hide_modal(cx)),
             previous_focus_handle: cx.focused(),
             focus_handle: cx.focus_handle(),
@@ -51,15 +72,28 @@ impl ModalLayer {
     }
 
     pub fn hide_modal(&mut self, cx: &mut ViewContext<Self>) {
-        if let Some(active_modal) = self.active_modal.take() {
-            if let Some(previous_focus) = active_modal.previous_focus_handle {
-                if active_modal.focus_handle.contains_focused(cx) {
-                    previous_focus.focus(cx);
-                }
-            }
-        }
+        let Some(active_modal) = self.active_modal.as_mut() else {
+            return;
+        };
 
-        cx.notify();
+        let dismiss = active_modal.modal.should_dismiss(cx);
+
+        cx.spawn(|this, mut cx| async move {
+            if dismiss.await {
+                this.update(&mut cx, |this, cx| {
+                    if let Some(active_modal) = this.active_modal.take() {
+                        if let Some(previous_focus) = active_modal.previous_focus_handle {
+                            if active_modal.focus_handle.contains_focused(cx) {
+                                previous_focus.focus(cx);
+                            }
+                        }
+                        cx.notify();
+                    }
+                })
+                .ok();
+            }
+        })
+        .detach();
     }
 
     pub fn active_modal<V>(&self) -> Option<View<V>>
@@ -67,7 +101,7 @@ impl ModalLayer {
         V: 'static,
     {
         let active_modal = self.active_modal.as_ref()?;
-        active_modal.modal.clone().downcast::<V>().ok()
+        active_modal.modal.view().downcast::<V>().ok()
     }
 }
 
@@ -98,7 +132,7 @@ impl Render for ModalLayer {
                             .on_mouse_down_out(cx.listener(|this, _, cx| {
                                 this.hide_modal(cx);
                             }))
-                            .child(active_modal.modal.clone()),
+                            .child(active_modal.modal.view()),
                     ),
             )
     }

crates/workspace2/src/workspace2.rs 🔗

@@ -3414,7 +3414,7 @@ impl Workspace {
         self.modal_layer.read(cx).active_modal()
     }
 
-    pub fn toggle_modal<V: ManagedView, B>(&mut self, cx: &mut ViewContext<Self>, build: B)
+    pub fn toggle_modal<V: ModalView, B>(&mut self, cx: &mut ViewContext<Self>, build: B)
     where
         B: FnOnce(&mut ViewContext<V>) -> V,
     {