From 113c7287df2ddab3fbba159447b23a8a4624852b Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 8 Dec 2023 15:26:06 -0500 Subject: [PATCH 1/3] Allow modals to override their dismissal Co-Authored-By: Mikayla Maki --- .../src/collab_panel/channel_modal.rs | 2 + .../src/collab_panel/contact_finder.rs | 2 + .../command_palette2/src/command_palette.rs | 4 +- crates/feedback2/src/feedback_modal.rs | 58 +++++++++------ crates/file_finder2/src/file_finder.rs | 4 +- crates/go_to_line2/src/go_to_line.rs | 3 + .../src/language_selector.rs | 3 +- crates/outline2/src/outline.rs | 3 +- .../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 | 70 ++++++++++++++----- crates/workspace2/src/workspace2.rs | 2 +- 13 files changed, 118 insertions(+), 46 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel/channel_modal.rs b/crates/collab_ui2/src/collab_panel/channel_modal.rs index af6343b98af37920f388eb042f27cbc281e6f4aa..3a630a1959146b58b2f3a2bdcbc57e35b0ef655c 100644 --- a/crates/collab_ui2/src/collab_panel/channel_modal.rs +++ b/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 for ChannelModal {} +impl ModalView for ChannelModal {} impl FocusableView for ChannelModal { fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { diff --git a/crates/collab_ui2/src/collab_panel/contact_finder.rs b/crates/collab_ui2/src/collab_panel/contact_finder.rs index 1623d10eab23a4473e1e89ea79c2f4fbcf86f1e4..742c25d148604dbbd195c1b17124f0e64b7ffc72 100644 --- a/crates/collab_ui2/src/collab_panel/contact_finder.rs +++ b/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::::init(cx); @@ -95,6 +96,7 @@ pub struct ContactFinderDelegate { } impl EventEmitter for ContactFinder {} +impl ModalView for ContactFinder {} impl FocusableView for ContactFinder { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 57c1fa5614016ba99331c1f0fe6f1945f4d243a2..306ccc2b5aa0f5aeb66fd55a7b05f6769d86bf3a 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/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>, } diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs index 23d8c38626e5bd076a57ff3d32d6f238d9333e60..ed8944be060d31667ab2f41d4c8d6bafcdf06dac 100644 --- a/crates/feedback2/src/feedback_modal.rs +++ b/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 for FeedbackModal {} +impl ModalView for FeedbackModal { + fn dismiss(&mut self, cx: &mut ViewContext) -> Task { + let prompt = Self::prompt_dismiss(cx); + cx.spawn(|_, _| prompt) + } +} + impl FeedbackModal { pub fn register(workspace: &mut Workspace, cx: &mut ViewContext) { 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); @@ -242,7 +249,27 @@ impl FeedbackModal { // Close immediately if no text in field // Ask to close if text in the field fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { - cx.emit(DismissEvent); + Self::dismiss_event(cx) + } + + fn dismiss_event(cx: &mut ViewContext) { + let dismiss = Self::prompt_dismiss(cx); + + cx.spawn(|this, mut cx| async move { + if dismiss.await { + this.update(&mut cx, |_, cx| cx.emit(DismissEvent)).ok(); + } + }) + .detach() + } + + fn prompt_dismiss(cx: &mut ViewContext) -> impl Future { + let answer = cx.prompt(PromptLevel::Info, "Discard feedback?", &["Yes", "No"]); + + async { + let answer = answer.await.ok(); + answer == Some(0) + } } } @@ -267,20 +294,7 @@ impl Render for FeedbackModal { } 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))); @@ -368,9 +382,13 @@ impl Render for FeedbackModal { // TODO: Will require somehow overriding the modal dismal default behavior .map(|this| { if has_feedback { - this.on_click(dismiss_prompt) + this.on_click(cx.listener(|_, _, cx| { + Self::dismiss_event(cx) + })) } else { - this.on_click(dismiss) + this.on_click(cx.listener(|_, _, cx| { + cx.emit(DismissEvent); + })) } }), ) diff --git a/crates/file_finder2/src/file_finder.rs b/crates/file_finder2/src/file_finder.rs index 63bf465a73d42d47063250a43908ec299b8db378..7324b3667a1dc0d1109510ea130da0d2591fba32 100644 --- a/crates/file_finder2/src/file_finder.rs +++ b/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>, } diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index aff9942c265da765431460d9f381abf131e3573a..1a64b29c4c0fdea85f1f7e4b06b65776d86002af 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/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, } +impl ModalView for GoToLine {} + impl FocusableView for GoToLine { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { self.line_editor.focus_handle(cx) diff --git a/crates/language_selector2/src/language_selector.rs b/crates/language_selector2/src/language_selector.rs index 5d6914d1e2822f235377be05ce9e36e45c674b85..1ba989ba5dd734a5c55fe2a2325fed4fb8211f25 100644 --- a/crates/language_selector2/src/language_selector.rs +++ b/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 for LanguageSelector {} +impl ModalView for LanguageSelector {} pub struct LanguageSelectorDelegate { language_selector: WeakView, diff --git a/crates/outline2/src/outline.rs b/crates/outline2/src/outline.rs index 45e167411e49065908dd7107f8d6260e622e46ba..bfa93d2099798f7b066d20be0a721cafd41fab63 100644 --- a/crates/outline2/src/outline.rs +++ b/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 for OutlineView {} +impl ModalView for OutlineView {} impl Render for OutlineView { type Element = Div; diff --git a/crates/recent_projects2/src/recent_projects.rs b/crates/recent_projects2/src/recent_projects.rs index d2c13b40a33645f14060cc421a8c43c2b19b7c8f..e0147836876bf47a431fae5344911abd65d96292 100644 --- a/crates/recent_projects2/src/recent_projects.rs +++ b/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>, } +impl ModalView for RecentProjects {} + impl RecentProjects { fn new(delegate: RecentProjectsDelegate, cx: &mut ViewContext) -> Self { Self { diff --git a/crates/theme_selector2/src/theme_selector.rs b/crates/theme_selector2/src/theme_selector.rs index 8430f0698baa1db1b1e18c3747b6ed5f473436e8..61f1cdf8fe0e91e67244198b86d86490cf5d88cb 100644 --- a/crates/theme_selector2/src/theme_selector.rs +++ b/crates/theme_selector2/src/theme_selector.rs @@ -11,7 +11,7 @@ use std::sync::Arc; use theme::{Theme, 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>, } diff --git a/crates/welcome2/src/base_keymap_picker.rs b/crates/welcome2/src/base_keymap_picker.rs index 4e829972f0cb5f004358680402efac3a3af37bad..9e672de69d0987139576a61d59a05502adb64d83 100644 --- a/crates/welcome2/src/base_keymap_picker.rs +++ b/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 for BaseKeymapSelector {} +impl ModalView for BaseKeymapSelector {} impl BaseKeymapSelector { pub fn new( diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index a9b6189fdc313394061f21abb1449d6664f3daed..a109352995a06c631c024a4bf36d7b95542d1ff5 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -1,11 +1,33 @@ +use futures::FutureExt; 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) -> Task { + Task::ready(true) + } +} + +trait ModalViewHandle { + fn should_dismiss(&mut self, cx: &mut WindowContext) -> Task; + fn view(&self) -> AnyView; +} + +impl ModalViewHandle for View { + fn should_dismiss(&mut self, cx: &mut WindowContext) -> Task { + self.update(cx, |this, cx| this.dismiss(cx)) + } + + fn view(&self) -> AnyView { + self.clone().into() + } +} + pub struct ActiveModal { - modal: AnyView, + modal: Box, subscription: Subscription, previous_focus_handle: Option, focus_handle: FocusHandle, @@ -22,11 +44,11 @@ impl ModalLayer { pub fn toggle_modal(&mut self, cx: &mut ViewContext, build_view: B) where - V: ManagedView, + V: ModalView, B: FnOnce(&mut ViewContext) -> V, { if let Some(active_modal) = &self.active_modal { - let is_close = active_modal.modal.clone().downcast::().is_ok(); + let is_close = active_modal.modal.view().downcast::().is_ok(); self.hide_modal(cx); if is_close { return; @@ -38,10 +60,10 @@ impl ModalLayer { pub fn show_modal(&mut self, new_modal: View, cx: &mut ViewContext) 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 +73,29 @@ impl ModalLayer { } pub fn hide_modal(&mut self, cx: &mut ViewContext) { - 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(); + } + }) + .shared() + .detach(); } pub fn active_modal(&self) -> Option> @@ -67,7 +103,7 @@ impl ModalLayer { V: 'static, { let active_modal = self.active_modal.as_ref()?; - active_modal.modal.clone().downcast::().ok() + active_modal.modal.view().downcast::().ok() } } @@ -98,7 +134,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()), ), ) } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 251f0685b0c93f7529984f2afabe582f1b481392..012c881e5b1514c9e806d77558d0284cb957a948 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -3414,7 +3414,7 @@ impl Workspace { self.modal_layer.read(cx).active_modal() } - pub fn toggle_modal(&mut self, cx: &mut ViewContext, build: B) + pub fn toggle_modal(&mut self, cx: &mut ViewContext, build: B) where B: FnOnce(&mut ViewContext) -> V, { From cd93ac1d2ac5b18a92ee1c9c11dbb22091d76520 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 8 Dec 2023 15:34:42 -0500 Subject: [PATCH 2/3] Fix error --- crates/workspace2/src/modal_layer.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index a109352995a06c631c024a4bf36d7b95542d1ff5..7a2f27df7262022780a9dcd405582dec7b2df8e3 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -1,4 +1,3 @@ -use futures::FutureExt; use gpui::{ div, prelude::*, px, AnyView, Div, FocusHandle, ManagedView, Render, Subscription, Task, View, ViewContext, WindowContext, @@ -94,7 +93,6 @@ impl ModalLayer { .ok(); } }) - .shared() .detach(); } From 13dad89a85687a8d04af80f3b386b38aed560649 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 8 Dec 2023 16:08:26 -0500 Subject: [PATCH 3/3] Move all dismissal logic into dismiss_event --- crates/feedback2/src/feedback_modal.rs | 29 +++++++------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/crates/feedback2/src/feedback_modal.rs b/crates/feedback2/src/feedback_modal.rs index ed8944be060d31667ab2f41d4c8d6bafcdf06dac..54d0705eb3fae2a33388553b9a9d3b291866a6f9 100644 --- a/crates/feedback2/src/feedback_modal.rs +++ b/crates/feedback2/src/feedback_modal.rs @@ -245,18 +245,16 @@ 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::dismiss_event(cx) + self.dismiss_event(cx) } - fn dismiss_event(cx: &mut ViewContext) { + fn dismiss_event(&mut self, cx: &mut ViewContext) { + 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 dismiss.await { + if !has_feedback || (has_feedback && dismiss.await) { this.update(&mut cx, |_, cx| cx.emit(DismissEvent)).ok(); } }) @@ -287,8 +285,6 @@ 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 { @@ -378,19 +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(cx.listener(|_, _, cx| { - Self::dismiss_event(cx) - })) - } else { - this.on_click(cx.listener(|_, _, cx| { - cx.emit(DismissEvent); - })) - } - }), + .on_click(cx.listener(move |this, _, cx| { + this.dismiss_event(cx) + })), ) .child( Button::new("send_feedback", submit_button_text) @@ -420,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