WIP: Make PickerDelegate a fully owned object instead of a view

This avoids issues with the parent view being on the stack when we want to
interact with the delegate from the picker. Still have several picker usages
to convert.
This commit is contained in:
Nathan Sobo 2023-04-19 22:05:29 -06:00
parent 5514349b6b
commit d70644618a
12 changed files with 309 additions and 393 deletions

View File

@ -1,49 +1,41 @@
use client::{ContactRequestStatus, User, UserStore}; use client::{ContactRequestStatus, User, UserStore};
use gpui::{ use gpui::{elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext};
elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MouseState, Task, View, use picker::{Picker, PickerDelegate, PickerEvent};
ViewContext, ViewHandle,
};
use picker::{Picker, PickerDelegate};
use settings::Settings; use settings::Settings;
use std::sync::Arc; use std::sync::Arc;
use util::TryFutureExt; use util::TryFutureExt;
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
Picker::<ContactFinder>::init(cx); Picker::<ContactFinderDelegate>::init(cx);
} }
pub struct ContactFinder { pub type ContactFinder = Picker<ContactFinderDelegate>;
picker: ViewHandle<Picker<Self>>,
pub fn build_contact_finder(
user_store: ModelHandle<UserStore>,
cx: &mut ViewContext<ContactFinder>,
) -> ContactFinder {
Picker::new(
ContactFinderDelegate {
user_store,
potential_contacts: Arc::from([]),
selected_index: 0,
},
cx,
)
}
pub struct ContactFinderDelegate {
potential_contacts: Arc<[Arc<User>]>, potential_contacts: Arc<[Arc<User>]>,
user_store: ModelHandle<UserStore>, user_store: ModelHandle<UserStore>,
selected_index: usize, selected_index: usize,
} }
pub enum Event { impl PickerDelegate for ContactFinderDelegate {
Dismissed, fn placeholder_text(&self) -> Arc<str> {
} "Search collaborator by username...".into()
impl Entity for ContactFinder {
type Event = Event;
}
impl View for ContactFinder {
fn ui_name() -> &'static str {
"ContactFinder"
} }
fn render(&mut self, cx: &mut ViewContext<Self>) -> Element<Self> {
ChildView::new(&self.picker, cx).boxed()
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.picker);
}
}
}
impl PickerDelegate for ContactFinder {
fn match_count(&self) -> usize { fn match_count(&self) -> usize {
self.potential_contacts.len() self.potential_contacts.len()
} }
@ -52,20 +44,20 @@ impl PickerDelegate for ContactFinder {
self.selected_index self.selected_index
} }
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Self>) { fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix; self.selected_index = ix;
} }
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> { fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let search_users = self let search_users = self
.user_store .user_store
.update(cx, |store, cx| store.fuzzy_search_users(query, cx)); .update(cx, |store, cx| store.fuzzy_search_users(query, cx));
cx.spawn(|this, mut cx| async move { cx.spawn(|picker, mut cx| async move {
async { async {
let potential_contacts = search_users.await?; let potential_contacts = search_users.await?;
this.update(&mut cx, |this, cx| { picker.update(&mut cx, |picker, cx| {
this.potential_contacts = potential_contacts.into(); picker.delegate_mut().potential_contacts = potential_contacts.into();
cx.notify(); cx.notify();
})?; })?;
anyhow::Ok(()) anyhow::Ok(())
@ -75,7 +67,7 @@ impl PickerDelegate for ContactFinder {
}) })
} }
fn confirm(&mut self, cx: &mut ViewContext<Self>) { fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
if let Some(user) = self.potential_contacts.get(self.selected_index) { if let Some(user) = self.potential_contacts.get(self.selected_index) {
let user_store = self.user_store.read(cx); let user_store = self.user_store.read(cx);
match user_store.contact_request_status(user) { match user_store.contact_request_status(user) {
@ -94,8 +86,8 @@ impl PickerDelegate for ContactFinder {
} }
} }
fn dismiss(&mut self, cx: &mut ViewContext<Self>) { fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
cx.emit(Event::Dismissed); cx.emit(PickerEvent::Dismiss);
} }
fn render_match( fn render_match(
@ -164,28 +156,3 @@ impl PickerDelegate for ContactFinder {
.boxed() .boxed()
} }
} }
impl ContactFinder {
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
let this = cx.weak_handle();
Self {
picker: cx.add_view(|cx| {
Picker::new("Search collaborator by username...", this, cx)
.with_theme(|theme| theme.contact_finder.picker.clone())
}),
potential_contacts: Arc::from([]),
user_store,
selected_index: 0,
}
}
pub fn editor_text(&self, cx: &AppContext) -> String {
self.picker.read(cx).query(cx)
}
pub fn with_editor_text(self, editor_text: String, cx: &mut ViewContext<Self>) -> Self {
self.picker
.update(cx, |picker, cx| picker.set_query(editor_text, cx));
self
}
}

View File

@ -1,9 +1,14 @@
use crate::{contact_finder::ContactFinder, contact_list::ContactList, ToggleContactsMenu}; use crate::{
contact_finder::{build_contact_finder, ContactFinder},
contact_list::ContactList,
ToggleContactsMenu,
};
use client::UserStore; use client::UserStore;
use gpui::{ use gpui::{
actions, elements::*, platform::MouseButton, AppContext, Entity, ModelHandle, View, actions, elements::*, platform::MouseButton, AppContext, Entity, ModelHandle, View,
ViewContext, ViewHandle, ViewContext, ViewHandle,
}; };
use picker::PickerEvent;
use project::Project; use project::Project;
use settings::Settings; use settings::Settings;
@ -50,19 +55,19 @@ impl ContactsPopover {
fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext<Self>) { fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext<Self>) {
match &self.child { match &self.child {
Child::ContactList(list) => self.show_contact_finder(list.read(cx).editor_text(cx), cx), Child::ContactList(list) => self.show_contact_finder(list.read(cx).editor_text(cx), cx),
Child::ContactFinder(finder) => { Child::ContactFinder(finder) => self.show_contact_list(finder.read(cx).query(cx), cx),
self.show_contact_list(finder.read(cx).editor_text(cx), cx)
}
} }
} }
fn show_contact_finder(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) { fn show_contact_finder(&mut self, editor_text: String, cx: &mut ViewContext<ContactsPopover>) {
let child = cx.add_view(|cx| { let child = cx.add_view(|cx| {
ContactFinder::new(self.user_store.clone(), cx).with_editor_text(editor_text, cx) let finder = build_contact_finder(self.user_store.clone(), cx);
finder.set_query(editor_text, cx);
finder
}); });
cx.focus(&child); cx.focus(&child);
self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event { self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event {
crate::contact_finder::Event::Dismissed => cx.emit(Event::Dismissed), PickerEvent::Dismiss => cx.emit(Event::Dismissed),
})); }));
self.child = Child::ContactFinder(child); self.child = Child::ContactFinder(child);
cx.notify(); cx.notify();

View File

@ -1,24 +1,25 @@
use collections::CommandPaletteFilter; use collections::CommandPaletteFilter;
use fuzzy::{StringMatch, StringMatchCandidate}; use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{ use gpui::{
actions, elements::*, keymap_matcher::Keystroke, Action, AnyViewHandle, AppContext, Drawable, actions, elements::*, keymap_matcher::Keystroke, Action, AppContext, Drawable, MouseState,
Entity, MouseState, View, ViewContext, ViewHandle, ViewContext,
}; };
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate, PickerEvent};
use settings::Settings; use settings::Settings;
use std::cmp; use std::cmp;
use util::ResultExt; use util::ResultExt;
use workspace::Workspace; use workspace::Workspace;
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.add_action(CommandPalette::toggle); cx.add_action(toggle_command_palette);
Picker::<CommandPalette>::init(cx); Picker::<CommandPaletteDelegate>::init(cx);
} }
actions!(command_palette, [Toggle]); actions!(command_palette, [Toggle]);
pub struct CommandPalette { pub type CommandPalette = Picker<CommandPaletteDelegate>;
picker: ViewHandle<Picker<Self>>,
pub struct CommandPaletteDelegate {
actions: Vec<Command>, actions: Vec<Command>,
matches: Vec<StringMatch>, matches: Vec<StringMatch>,
selected_ix: usize, selected_ix: usize,
@ -40,9 +41,19 @@ struct Command {
keystrokes: Vec<Keystroke>, keystrokes: Vec<Keystroke>,
} }
impl CommandPalette { fn toggle_command_palette(_: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
pub fn new(focused_view_id: usize, cx: &mut ViewContext<Self>) -> Self { let workspace = cx.handle();
let this = cx.weak_handle(); let focused_view_id = cx.focused_view_id().unwrap_or_else(|| workspace.id());
cx.defer(move |workspace, cx| {
workspace.toggle_modal(cx, |_, cx| {
cx.add_view(|cx| Picker::new(CommandPaletteDelegate::new(focused_view_id, cx), cx))
});
});
}
impl CommandPaletteDelegate {
pub fn new(focused_view_id: usize, cx: &mut ViewContext<Picker<Self>>) -> Self {
let actions = cx let actions = cx
.available_actions(focused_view_id) .available_actions(focused_view_id)
.filter_map(|(name, action, bindings)| { .filter_map(|(name, action, bindings)| {
@ -65,73 +76,20 @@ impl CommandPalette {
}) })
.collect(); .collect();
let picker = cx.add_view(|cx| Picker::new("Execute a command...", this, cx));
Self { Self {
picker,
actions, actions,
matches: vec![], matches: vec![],
selected_ix: 0, selected_ix: 0,
focused_view_id, focused_view_id,
} }
} }
fn toggle(_: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
let workspace = cx.handle();
let focused_view_id = cx.focused_view_id().unwrap_or_else(|| workspace.id());
cx.defer(move |workspace, cx| {
let this = cx.add_view(|cx| Self::new(focused_view_id, cx));
workspace.toggle_modal(cx, |_, cx| {
cx.subscribe(&this, Self::on_event).detach();
this
});
});
}
fn on_event(
workspace: &mut Workspace,
_: ViewHandle<Self>,
event: &Event,
cx: &mut ViewContext<Workspace>,
) {
match event {
Event::Dismissed => workspace.dismiss_modal(cx),
Event::Confirmed {
window_id,
focused_view_id,
action,
} => {
let window_id = *window_id;
let focused_view_id = *focused_view_id;
let action = action.boxed_clone();
workspace.dismiss_modal(cx);
cx.defer(move |_, cx| cx.dispatch_any_action_at(window_id, focused_view_id, action))
}
}
}
} }
impl Entity for CommandPalette { impl PickerDelegate for CommandPaletteDelegate {
type Event = Event; fn placeholder_text(&self) -> std::sync::Arc<str> {
} "Execute a command...".into()
impl View for CommandPalette {
fn ui_name() -> &'static str {
"CommandPalette"
} }
fn render(&mut self, cx: &mut ViewContext<Self>) -> Element<Self> {
ChildView::new(&self.picker, cx).boxed()
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.picker);
}
}
}
impl PickerDelegate for CommandPalette {
fn match_count(&self) -> usize { fn match_count(&self) -> usize {
self.matches.len() self.matches.len()
} }
@ -140,14 +98,14 @@ impl PickerDelegate for CommandPalette {
self.selected_ix self.selected_ix
} }
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Self>) { fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
self.selected_ix = ix; self.selected_ix = ix;
} }
fn update_matches( fn update_matches(
&mut self, &mut self,
query: String, query: String,
cx: &mut gpui::ViewContext<Self>, cx: &mut ViewContext<Picker<Self>>,
) -> gpui::Task<()> { ) -> gpui::Task<()> {
let candidates = self let candidates = self
.actions .actions
@ -159,7 +117,7 @@ impl PickerDelegate for CommandPalette {
char_bag: command.name.chars().collect(), char_bag: command.name.chars().collect(),
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
cx.spawn(move |this, mut cx| async move { cx.spawn(move |picker, mut cx| async move {
let matches = if query.is_empty() { let matches = if query.is_empty() {
candidates candidates
.into_iter() .into_iter()
@ -182,33 +140,36 @@ impl PickerDelegate for CommandPalette {
) )
.await .await
}; };
this.update(&mut cx, |this, _| { picker
this.matches = matches; .update(&mut cx, |picker, _| {
if this.matches.is_empty() { let delegate = picker.delegate_mut();
this.selected_ix = 0; delegate.matches = matches;
} else { if delegate.matches.is_empty() {
this.selected_ix = cmp::min(this.selected_ix, this.matches.len() - 1); delegate.selected_ix = 0;
} } else {
}) delegate.selected_ix =
.log_err(); cmp::min(delegate.selected_ix, delegate.matches.len() - 1);
}
})
.log_err();
}) })
} }
fn dismiss(&mut self, cx: &mut ViewContext<Self>) { fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
cx.emit(Event::Dismissed); cx.emit(PickerEvent::Dismiss);
} }
fn confirm(&mut self, cx: &mut ViewContext<Self>) { fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
if !self.matches.is_empty() { if !self.matches.is_empty() {
let window_id = cx.window_id();
let focused_view_id = self.focused_view_id;
let action_ix = self.matches[self.selected_ix].candidate_id; let action_ix = self.matches[self.selected_ix].candidate_id;
cx.emit(Event::Confirmed { let action = self.actions.remove(action_ix).action;
window_id: cx.window_id(), cx.defer(move |_, cx| {
focused_view_id: self.focused_view_id, cx.dispatch_any_action_at(window_id, focused_view_id, action);
action: self.actions.remove(action_ix).action,
}); });
} else {
cx.emit(Event::Dismissed);
} }
cx.emit(PickerEvent::Dismiss);
} }
fn render_match( fn render_match(
@ -353,7 +314,7 @@ mod tests {
}); });
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
CommandPalette::toggle(workspace, &Toggle, cx) toggle_command_palette(workspace, &Toggle, cx);
}); });
let palette = workspace.read_with(cx, |workspace, _| { let palette = workspace.read_with(cx, |workspace, _| {
@ -362,7 +323,9 @@ mod tests {
palette palette
.update(cx, |palette, cx| { .update(cx, |palette, cx| {
palette.update_matches("bcksp".to_string(), cx) palette
.delegate_mut()
.update_matches("bcksp".to_string(), cx)
}) })
.await; .await;
@ -383,12 +346,12 @@ mod tests {
}); });
workspace.update(cx, |workspace, cx| { workspace.update(cx, |workspace, cx| {
CommandPalette::toggle(workspace, &Toggle, cx); CommandPaletteDelegate::toggle(workspace, &Toggle, cx);
}); });
// Assert editor command not present // Assert editor command not present
let palette = workspace.read_with(cx, |workspace, _| { let palette = workspace.read_with(cx, |workspace, _| {
workspace.modal::<CommandPalette>().unwrap() workspace.modal::<CommandPaletteDelegate>().unwrap()
}); });
palette palette

View File

@ -1,7 +1,6 @@
use fuzzy::PathMatch; use fuzzy::PathMatch;
use gpui::{ use gpui::{
actions, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MouseState, Task, View, actions, elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext, WeakViewHandle,
ViewContext, ViewHandle,
}; };
use picker::{Picker, PickerDelegate}; use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
@ -16,9 +15,11 @@ use std::{
use util::{post_inc, ResultExt}; use util::{post_inc, ResultExt};
use workspace::Workspace; use workspace::Workspace;
pub struct FileFinder { pub type FileFinder = Picker<FileFinderDelegate>;
pub struct FileFinderDelegate {
workspace: WeakViewHandle<Workspace>,
project: ModelHandle<Project>, project: ModelHandle<Project>,
picker: ViewHandle<Picker<Self>>,
search_count: usize, search_count: usize,
latest_search_id: usize, latest_search_id: usize,
latest_search_did_cancel: bool, latest_search_did_cancel: bool,
@ -32,8 +33,26 @@ pub struct FileFinder {
actions!(file_finder, [Toggle]); actions!(file_finder, [Toggle]);
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.add_action(FileFinder::toggle); cx.add_action(toggle_file_finder);
Picker::<FileFinder>::init(cx); FileFinder::init(cx);
}
fn toggle_file_finder(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
workspace.toggle_modal(cx, |workspace, cx| {
let relative_to = workspace
.active_item(cx)
.and_then(|item| item.project_path(cx))
.map(|project_path| project_path.path.clone());
let project = workspace.project().clone();
let workspace = cx.handle().downgrade();
let finder = cx.add_view(|cx| {
Picker::new(
FileFinderDelegate::new(workspace, project, relative_to, cx),
cx,
)
});
finder
});
} }
pub enum Event { pub enum Event {
@ -41,27 +60,7 @@ pub enum Event {
Dismissed, Dismissed,
} }
impl Entity for FileFinder { impl FileFinderDelegate {
type Event = Event;
}
impl View for FileFinder {
fn ui_name() -> &'static str {
"FileFinder"
}
fn render(&mut self, cx: &mut ViewContext<Self>) -> Element<Self> {
ChildView::new(&self.picker, cx).boxed()
}
fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
if cx.is_self_focused() {
cx.focus(&self.picker);
}
}
}
impl FileFinder {
fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec<usize>, String, Vec<usize>) { fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec<usize>, String, Vec<usize>) {
let path = &path_match.path; let path = &path_match.path;
let path_string = path.to_string_lossy(); let path_string = path.to_string_lossy();
@ -88,48 +87,20 @@ impl FileFinder {
(file_name, file_name_positions, full_path, path_positions) (file_name, file_name_positions, full_path, path_positions)
} }
fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
workspace.toggle_modal(cx, |workspace, cx| {
let project = workspace.project().clone();
let relative_to = workspace
.active_item(cx)
.and_then(|item| item.project_path(cx))
.map(|project_path| project_path.path.clone());
let finder = cx.add_view(|cx| Self::new(project, relative_to, cx));
cx.subscribe(&finder, Self::on_event).detach();
finder
});
}
fn on_event(
workspace: &mut Workspace,
_: ViewHandle<FileFinder>,
event: &Event,
cx: &mut ViewContext<Workspace>,
) {
match event {
Event::Selected(project_path) => {
workspace
.open_path(project_path.clone(), None, true, cx)
.detach_and_log_err(cx);
workspace.dismiss_modal(cx);
}
Event::Dismissed => {
workspace.dismiss_modal(cx);
}
}
}
pub fn new( pub fn new(
workspace: WeakViewHandle<Workspace>,
project: ModelHandle<Project>, project: ModelHandle<Project>,
relative_to: Option<Arc<Path>>, relative_to: Option<Arc<Path>>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<FileFinder>,
) -> Self { ) -> Self {
let handle = cx.weak_handle(); cx.observe(&project, |picker, _, cx| {
cx.observe(&project, Self::project_updated).detach(); let query = picker.query(cx);
picker.delegate_mut().spawn_search(query, cx).detach();
})
.detach();
Self { Self {
workspace,
project, project,
picker: cx.add_view(|cx| Picker::new("Search project files...", handle, cx)),
search_count: 0, search_count: 0,
latest_search_id: 0, latest_search_id: 0,
latest_search_did_cancel: false, latest_search_did_cancel: false,
@ -141,12 +112,7 @@ impl FileFinder {
} }
} }
fn project_updated(&mut self, _: ModelHandle<Project>, cx: &mut ViewContext<Self>) { fn spawn_search(&mut self, query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
self.spawn_search(self.picker.read(cx).query(cx), cx)
.detach();
}
fn spawn_search(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> {
let relative_to = self.relative_to.clone(); let relative_to = self.relative_to.clone();
let worktrees = self let worktrees = self
.project .project
@ -172,7 +138,7 @@ impl FileFinder {
self.cancel_flag.store(true, atomic::Ordering::Relaxed); self.cancel_flag.store(true, atomic::Ordering::Relaxed);
self.cancel_flag = Arc::new(AtomicBool::new(false)); self.cancel_flag = Arc::new(AtomicBool::new(false));
let cancel_flag = self.cancel_flag.clone(); let cancel_flag = self.cancel_flag.clone();
cx.spawn(|this, mut cx| async move { cx.spawn(|picker, mut cx| async move {
let matches = fuzzy::match_path_sets( let matches = fuzzy::match_path_sets(
candidate_sets.as_slice(), candidate_sets.as_slice(),
&query, &query,
@ -184,10 +150,13 @@ impl FileFinder {
) )
.await; .await;
let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed); let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
this.update(&mut cx, |this, cx| { picker
this.set_matches(search_id, did_cancel, query, matches, cx) .update(&mut cx, |picker, cx| {
}) picker
.log_err(); .delegate_mut()
.set_matches(search_id, did_cancel, query, matches, cx)
})
.log_err();
}) })
} }
@ -197,7 +166,7 @@ impl FileFinder {
did_cancel: bool, did_cancel: bool,
query: String, query: String,
matches: Vec<PathMatch>, matches: Vec<PathMatch>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<FileFinder>,
) { ) {
if search_id >= self.latest_search_id { if search_id >= self.latest_search_id {
self.latest_search_id = search_id; self.latest_search_id = search_id;
@ -209,12 +178,15 @@ impl FileFinder {
self.latest_search_query = query; self.latest_search_query = query;
self.latest_search_did_cancel = did_cancel; self.latest_search_did_cancel = did_cancel;
cx.notify(); cx.notify();
self.picker.update(cx, |_, cx| cx.notify());
} }
} }
} }
impl PickerDelegate for FileFinder { impl PickerDelegate for FileFinderDelegate {
fn placeholder_text(&self) -> Arc<str> {
"Search project files...".into()
}
fn match_count(&self) -> usize { fn match_count(&self) -> usize {
self.matches.len() self.matches.len()
} }
@ -232,13 +204,13 @@ impl PickerDelegate for FileFinder {
0 0
} }
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>) { fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<FileFinder>) {
let mat = &self.matches[ix]; let mat = &self.matches[ix];
self.selected = Some((mat.worktree_id, mat.path.clone())); self.selected = Some((mat.worktree_id, mat.path.clone()));
cx.notify(); cx.notify();
} }
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()> { fn update_matches(&mut self, query: String, cx: &mut ViewContext<FileFinder>) -> Task<()> {
if query.is_empty() { if query.is_empty() {
self.latest_search_id = post_inc(&mut self.search_count); self.latest_search_id = post_inc(&mut self.search_count);
self.matches.clear(); self.matches.clear();
@ -249,18 +221,25 @@ impl PickerDelegate for FileFinder {
} }
} }
fn confirm(&mut self, cx: &mut ViewContext<Self>) { fn confirm(&mut self, cx: &mut ViewContext<FileFinder>) {
if let Some(m) = self.matches.get(self.selected_index()) { if let Some(m) = self.matches.get(self.selected_index()) {
cx.emit(Event::Selected(ProjectPath { if let Some(workspace) = self.workspace.upgrade(cx) {
worktree_id: WorktreeId::from_usize(m.worktree_id), let project_path = ProjectPath {
path: m.path.clone(), worktree_id: WorktreeId::from_usize(m.worktree_id),
})); path: m.path.clone(),
};
workspace.update(cx, |workspace, cx| {
workspace
.open_path(project_path.clone(), None, true, cx)
.detach_and_log_err(cx);
workspace.dismiss_modal(cx);
})
}
} }
} }
fn dismiss(&mut self, cx: &mut ViewContext<Self>) { fn dismissed(&mut self, _: &mut ViewContext<FileFinder>) {}
cx.emit(Event::Dismissed);
}
fn render_match( fn render_match(
&self, &self,
@ -336,11 +315,11 @@ mod tests {
let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap()); let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
finder finder
.update(cx, |finder, cx| { .update(cx, |finder, cx| {
finder.update_matches("bna".to_string(), cx) finder.delegate_mut().update_matches("bna".to_string(), cx)
}) })
.await; .await;
finder.read_with(cx, |finder, _| { finder.read_with(cx, |finder, _| {
assert_eq!(finder.matches.len(), 2); assert_eq!(finder.delegate().matches.len(), 2);
}); });
let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone());
@ -385,8 +364,12 @@ mod tests {
let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let (_, finder) = let (_, finder) = cx.add_window(|cx| {
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx)); Picker::new(
FileFinderDelegate::new(workspace.read(cx).project().clone(), None, cx),
cx,
)
});
let query = "hi".to_string(); let query = "hi".to_string();
finder finder
@ -395,13 +378,14 @@ mod tests {
finder.read_with(cx, |f, _| assert_eq!(f.matches.len(), 5)); finder.read_with(cx, |f, _| assert_eq!(f.matches.len(), 5));
finder.update(cx, |finder, cx| { finder.update(cx, |finder, cx| {
let matches = finder.matches.clone(); let delegate = finder.delegate_mut();
let matches = delegate.matches.clone();
// Simulate a search being cancelled after the time limit, // Simulate a search being cancelled after the time limit,
// returning only a subset of the matches that would have been found. // returning only a subset of the matches that would have been found.
drop(finder.spawn_search(query.clone(), cx)); drop(delegate.spawn_search(query.clone(), cx));
finder.set_matches( delegate.set_matches(
finder.latest_search_id, finder.delegate().latest_search_id,
true, // did-cancel true, // did-cancel
query.clone(), query.clone(),
vec![matches[1].clone(), matches[3].clone()], vec![matches[1].clone(), matches[3].clone()],
@ -409,16 +393,16 @@ mod tests {
); );
// Simulate another cancellation. // Simulate another cancellation.
drop(finder.spawn_search(query.clone(), cx)); drop(delegate.spawn_search(query.clone(), cx));
finder.set_matches( delegate.set_matches(
finder.latest_search_id, delegate.latest_search_id,
true, // did-cancel true, // did-cancel
query.clone(), query.clone(),
vec![matches[0].clone(), matches[2].clone(), matches[3].clone()], vec![matches[0].clone(), matches[2].clone(), matches[3].clone()],
cx, cx,
); );
assert_eq!(finder.matches, matches[0..4]) assert_eq!(delegate.matches, matches[0..4])
}); });
} }
@ -459,8 +443,12 @@ mod tests {
) )
.await; .await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let (_, finder) = let (_, finder) = cx.add_window(|cx| {
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx)); Picker::new(
FileFinderDelegate::new(workspace.read(cx).project().clone(), None, cx),
cx,
)
});
finder finder
.update(cx, |f, cx| f.spawn_search("hi".into(), cx)) .update(cx, |f, cx| f.spawn_search("hi".into(), cx))
.await; .await;
@ -483,8 +471,9 @@ mod tests {
) )
.await; .await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let (_, finder) = let (_, finder) = cx.add_window(|cx| {
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx)); FileFinderDelegate::new(workspace.read(cx).project().clone(), None, cx)
});
// Even though there is only one worktree, that worktree's filename // Even though there is only one worktree, that worktree's filename
// is included in the matching, because the worktree is a single file. // is included in the matching, because the worktree is a single file.
@ -536,8 +525,9 @@ mod tests {
.await; .await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let (_, finder) = let (_, finder) = cx.add_window(|cx| {
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx)); FileFinderDelegate::new(workspace.read(cx).project().clone(), None, cx)
});
// Run a search that matches two files with the same relative path. // Run a search that matches two files with the same relative path.
finder finder
@ -582,8 +572,9 @@ mod tests {
// first when they have the same name. In this case, b.txt is closer to dir2's a.txt // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
// so that one should be sorted earlier // so that one should be sorted earlier
let b_path = Some(Arc::from(Path::new("/root/dir2/b.txt"))); let b_path = Some(Arc::from(Path::new("/root/dir2/b.txt")));
let (_, finder) = let (_, finder) = cx.add_window(|cx| {
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), b_path, cx)); FileFinderDelegate::new(workspace.read(cx).project().clone(), b_path, cx)
});
finder finder
.update(cx, |f, cx| f.spawn_search("a.txt".into(), cx)) .update(cx, |f, cx| f.spawn_search("a.txt".into(), cx))
@ -614,8 +605,9 @@ mod tests {
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
let (_, finder) = let (_, finder) = cx.add_window(|cx| {
cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx)); FileFinderDelegate::new(workspace.read(cx).project().clone(), None, cx)
});
finder finder
.update(cx, |f, cx| f.spawn_search("dir".into(), cx)) .update(cx, |f, cx| f.spawn_search("dir".into(), cx))
.await; .await;

View File

@ -163,7 +163,7 @@ impl PickerDelegate for LanguageSelector {
cx.emit(Event::Dismissed); cx.emit(Event::Dismissed);
} }
fn dismiss(&mut self, cx: &mut ViewContext<Self>) { fn dismissed(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(Event::Dismissed); cx.emit(Event::Dismissed);
} }

View File

@ -229,7 +229,7 @@ impl PickerDelegate for OutlineView {
cx.emit(Event::Dismissed); cx.emit(Event::Dismissed);
} }
fn dismiss(&mut self, cx: &mut ViewContext<Self>) { fn dismissed(&mut self, cx: &mut ViewContext<Self>) {
self.restore_active_editor(cx); self.restore_active_editor(cx);
cx.emit(Event::Dismissed); cx.emit(Event::Dismissed);
} }

View File

@ -5,15 +5,20 @@ use gpui::{
keymap_matcher::KeymapContext, keymap_matcher::KeymapContext,
platform::{CursorStyle, MouseButton}, platform::{CursorStyle, MouseButton},
AnyViewHandle, AppContext, Axis, Element, Entity, MouseState, Task, View, ViewContext, AnyViewHandle, AppContext, Axis, Element, Entity, MouseState, Task, View, ViewContext,
ViewHandle, WeakViewHandle, ViewHandle,
}; };
use menu::{Cancel, Confirm, SelectFirst, SelectIndex, SelectLast, SelectNext, SelectPrev}; use menu::{Cancel, Confirm, SelectFirst, SelectIndex, SelectLast, SelectNext, SelectPrev};
use parking_lot::Mutex; use parking_lot::Mutex;
use std::{cmp, sync::Arc}; use std::{cmp, sync::Arc};
use util::ResultExt; use util::ResultExt;
use workspace::Modal;
pub enum PickerEvent {
Dismiss,
}
pub struct Picker<D: PickerDelegate> { pub struct Picker<D: PickerDelegate> {
delegate: WeakViewHandle<D>, delegate: D,
query_editor: ViewHandle<Editor>, query_editor: ViewHandle<Editor>,
list_state: UniformListState, list_state: UniformListState,
max_size: Vector2F, max_size: Vector2F,
@ -21,13 +26,14 @@ pub struct Picker<D: PickerDelegate> {
confirmed: bool, confirmed: bool,
} }
pub trait PickerDelegate: View { pub trait PickerDelegate: Sized + 'static {
fn placeholder_text(&self) -> Arc<str>;
fn match_count(&self) -> usize; fn match_count(&self) -> usize;
fn selected_index(&self) -> usize; fn selected_index(&self) -> usize;
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Self>); fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) -> Task<()>; fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()>;
fn confirm(&mut self, cx: &mut ViewContext<Self>); fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>);
fn dismiss(&mut self, cx: &mut ViewContext<Self>); fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
fn render_match( fn render_match(
&self, &self,
ix: usize, ix: usize,
@ -41,7 +47,7 @@ pub trait PickerDelegate: View {
} }
impl<D: PickerDelegate> Entity for Picker<D> { impl<D: PickerDelegate> Entity for Picker<D> {
type Event = (); type Event = PickerEvent;
} }
impl<D: PickerDelegate> View for Picker<D> { impl<D: PickerDelegate> View for Picker<D> {
@ -52,12 +58,7 @@ impl<D: PickerDelegate> View for Picker<D> {
fn render(&mut self, cx: &mut ViewContext<Self>) -> Element<Self> { fn render(&mut self, cx: &mut ViewContext<Self>) -> Element<Self> {
let theme = (self.theme.lock())(&cx.global::<settings::Settings>().theme); let theme = (self.theme.lock())(&cx.global::<settings::Settings>().theme);
let query = self.query(cx); let query = self.query(cx);
let delegate = self.delegate.clone(); let match_count = self.delegate.match_count();
let match_count = if let Some(delegate) = delegate.upgrade(cx) {
delegate.read(cx).match_count()
} else {
0
};
let container_style; let container_style;
let editor_style; let editor_style;
@ -94,14 +95,11 @@ impl<D: PickerDelegate> View for Picker<D> {
match_count, match_count,
cx, cx,
move |this, mut range, items, cx| { move |this, mut range, items, cx| {
let delegate = this.delegate.upgrade(cx).unwrap(); let selected_ix = this.delegate.selected_index();
let selected_ix = delegate.read(cx).selected_index(); range.end = cmp::min(range.end, this.delegate.match_count());
range.end = cmp::min(range.end, delegate.read(cx).match_count());
items.extend(range.map(move |ix| { items.extend(range.map(move |ix| {
MouseEventHandler::<D, _>::new(ix, cx, |state, cx| { MouseEventHandler::<D, _>::new(ix, cx, |state, cx| {
delegate this.delegate.render_match(ix, state, ix == selected_ix, cx)
.read(cx)
.render_match(ix, state, ix == selected_ix, cx)
}) })
// Capture mouse events // Capture mouse events
.on_down(MouseButton::Left, |_, _, _| {}) .on_down(MouseButton::Left, |_, _, _| {})
@ -141,6 +139,12 @@ impl<D: PickerDelegate> View for Picker<D> {
} }
} }
impl<D: PickerDelegate> Modal for Picker<D> {
fn dismiss_on_event(event: &Self::Event) -> bool {
matches!(event, PickerEvent::Dismiss)
}
}
impl<D: PickerDelegate> Picker<D> { impl<D: PickerDelegate> Picker<D> {
pub fn init(cx: &mut AppContext) { pub fn init(cx: &mut AppContext) {
cx.add_action(Self::select_first); cx.add_action(Self::select_first);
@ -152,14 +156,12 @@ impl<D: PickerDelegate> Picker<D> {
cx.add_action(Self::cancel); cx.add_action(Self::cancel);
} }
pub fn new<P>(placeholder: P, delegate: WeakViewHandle<D>, cx: &mut ViewContext<Self>) -> Self pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
where
P: Into<Arc<str>>,
{
let theme = Arc::new(Mutex::new( let theme = Arc::new(Mutex::new(
Box::new(|theme: &theme::Theme| theme.picker.clone()) Box::new(|theme: &theme::Theme| theme.picker.clone())
as Box<dyn Fn(&theme::Theme) -> theme::Picker>, as Box<dyn Fn(&theme::Theme) -> theme::Picker>,
)); ));
let placeholder_text = delegate.placeholder_text();
let query_editor = cx.add_view({ let query_editor = cx.add_view({
let picker_theme = theme.clone(); let picker_theme = theme.clone();
|cx| { |cx| {
@ -169,13 +171,13 @@ impl<D: PickerDelegate> Picker<D> {
})), })),
cx, cx,
); );
editor.set_placeholder_text(placeholder, cx); editor.set_placeholder_text(placeholder_text, cx);
editor editor
} }
}); });
cx.subscribe(&query_editor, Self::on_query_editor_event) cx.subscribe(&query_editor, Self::on_query_editor_event)
.detach(); .detach();
let this = Self { let mut this = Self {
query_editor, query_editor,
list_state: Default::default(), list_state: Default::default(),
delegate, delegate,
@ -183,12 +185,9 @@ impl<D: PickerDelegate> Picker<D> {
theme, theme,
confirmed: false, confirmed: false,
}; };
cx.defer(|this, cx| { // TODO! How can the delegate notify the picker to update?
if let Some(delegate) = this.delegate.upgrade(cx) { // cx.observe(&delegate, |_, _, cx| cx.notify()).detach();
cx.observe(&delegate, |_, _, cx| cx.notify()).detach(); this.update_matches(String::new(), cx);
this.update_matches(String::new(), cx)
}
});
this this
} }
@ -205,6 +204,14 @@ impl<D: PickerDelegate> Picker<D> {
self self
} }
pub fn delegate(&self) -> &D {
&self.delegate
}
pub fn delegate_mut(&mut self) -> &mut D {
&mut self.delegate
}
pub fn query(&self, cx: &AppContext) -> String { pub fn query(&self, cx: &AppContext) -> String {
self.query_editor.read(cx).text(cx) self.query_editor.read(cx).text(cx)
} }
@ -223,121 +230,93 @@ impl<D: PickerDelegate> Picker<D> {
match event { match event {
editor::Event::BufferEdited { .. } => self.update_matches(self.query(cx), cx), editor::Event::BufferEdited { .. } => self.update_matches(self.query(cx), cx),
editor::Event::Blurred if !self.confirmed => { editor::Event::Blurred if !self.confirmed => {
if let Some(delegate) = self.delegate.upgrade(cx) { self.dismiss(cx);
delegate.update(cx, |delegate, cx| {
delegate.dismiss(cx);
})
}
} }
_ => {} _ => {}
} }
} }
pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) { pub fn update_matches(&mut self, query: String, cx: &mut ViewContext<Self>) {
if let Some(delegate) = self.delegate.upgrade(cx) { let update = self.delegate.update_matches(query, cx);
let update = delegate.update(cx, |d, cx| d.update_matches(query, cx)); cx.spawn_weak(|this, mut cx| async move {
cx.spawn_weak(|this, mut cx| async move { update.await;
update.await; this.upgrade(&cx)?
this.upgrade(&cx)? .update(&mut cx, |this, cx| {
.update(&mut cx, |this, cx| { let index = this.delegate.selected_index();
if let Some(delegate) = this.delegate.upgrade(cx) { let target = if this.delegate.center_selection_after_match_updates() {
let delegate = delegate.read(cx); ScrollTarget::Center(index)
let index = delegate.selected_index(); } else {
let target = if delegate.center_selection_after_match_updates() { ScrollTarget::Show(index)
ScrollTarget::Center(index) };
} else { this.list_state.scroll_to(target);
ScrollTarget::Show(index) cx.notify();
}; })
this.list_state.scroll_to(target); .log_err()
cx.notify(); })
} .detach()
})
.log_err()
})
.detach()
}
} }
pub fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) { pub fn select_first(&mut self, _: &SelectFirst, cx: &mut ViewContext<Self>) {
if let Some(delegate) = self.delegate.upgrade(cx) { if self.delegate.match_count() > 0 {
delegate.update(cx, |delegate, cx| { self.delegate.set_selected_index(0, cx);
if delegate.match_count() > 0 { self.list_state.scroll_to(ScrollTarget::Show(0));
delegate.set_selected_index(0, cx);
self.list_state.scroll_to(ScrollTarget::Show(0));
}
});
cx.notify();
} }
cx.notify();
} }
pub fn select_index(&mut self, action: &SelectIndex, cx: &mut ViewContext<Self>) { pub fn select_index(&mut self, action: &SelectIndex, cx: &mut ViewContext<Self>) {
if let Some(delegate) = self.delegate.upgrade(cx) { let index = action.0;
let index = action.0; if self.delegate.match_count() > 0 {
delegate.update(cx, |delegate, cx| { self.confirmed = true;
if delegate.match_count() > 0 { self.delegate.set_selected_index(index, cx);
self.confirmed = true; self.delegate.confirm(cx);
delegate.set_selected_index(index, cx);
delegate.confirm(cx);
}
});
} }
} }
pub fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) { pub fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext<Self>) {
if let Some(delegate) = self.delegate.upgrade(cx) { let match_count = self.delegate.match_count();
delegate.update(cx, |delegate, cx| { if match_count > 0 {
let match_count = delegate.match_count(); let index = match_count - 1;
if match_count > 0 { self.delegate.set_selected_index(index, cx);
let index = match_count - 1; self.list_state.scroll_to(ScrollTarget::Show(index));
delegate.set_selected_index(index, cx);
self.list_state.scroll_to(ScrollTarget::Show(index));
}
});
cx.notify();
} }
cx.notify();
} }
pub fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) { pub fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
if let Some(delegate) = self.delegate.upgrade(cx) { let next_index = self.delegate.selected_index() + 1;
delegate.update(cx, |delegate, cx| { if next_index < self.delegate.match_count() {
let next_index = delegate.selected_index() + 1; self.delegate.set_selected_index(next_index, cx);
if next_index < delegate.match_count() { self.list_state.scroll_to(ScrollTarget::Show(next_index));
delegate.set_selected_index(next_index, cx);
self.list_state.scroll_to(ScrollTarget::Show(next_index));
}
});
cx.notify();
} }
cx.notify();
} }
pub fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) { pub fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
if let Some(delegate) = self.delegate.upgrade(cx) { let mut selected_index = self.delegate.selected_index();
delegate.update(cx, |delegate, cx| { if selected_index > 0 {
let mut selected_index = delegate.selected_index(); selected_index -= 1;
if selected_index > 0 { self.delegate.set_selected_index(selected_index, cx);
selected_index -= 1; self.list_state
delegate.set_selected_index(selected_index, cx); .scroll_to(ScrollTarget::Show(selected_index));
self.list_state
.scroll_to(ScrollTarget::Show(selected_index));
}
});
cx.notify();
} }
cx.notify();
} }
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) { fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
if let Some(delegate) = self.delegate.upgrade(cx) { self.confirmed = true;
self.confirmed = true; self.delegate.confirm(cx);
delegate.update(cx, |delegate, cx| delegate.confirm(cx));
}
} }
fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) { fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
if let Some(delegate) = self.delegate.upgrade(cx) { self.dismiss(cx);
delegate.update(cx, |delegate, cx| delegate.dismiss(cx)); }
}
fn dismiss(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(PickerEvent::Dismiss);
self.delegate.dismissed(cx);
} }
} }

View File

@ -176,7 +176,7 @@ impl PickerDelegate for ProjectSymbolsView {
} }
} }
fn dismiss(&mut self, cx: &mut ViewContext<Self>) { fn dismissed(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(Event::Dismissed); cx.emit(Event::Dismissed);
} }

View File

@ -176,7 +176,7 @@ impl PickerDelegate for RecentProjectsView {
} }
} }
fn dismiss(&mut self, cx: &mut ViewContext<Self>) { fn dismissed(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(Event::Dismissed); cx.emit(Event::Dismissed);
} }

View File

@ -156,7 +156,7 @@ impl PickerDelegate for ThemeSelector {
cx.emit(Event::Dismissed); cx.emit(Event::Dismissed);
} }
fn dismiss(&mut self, cx: &mut ViewContext<Self>) { fn dismissed(&mut self, cx: &mut ViewContext<Self>) {
if !self.selection_completed { if !self.selection_completed {
Self::set_theme(self.original_theme.clone(), cx); Self::set_theme(self.original_theme.clone(), cx);
self.selection_completed = true; self.selection_completed = true;

View File

@ -155,7 +155,7 @@ impl PickerDelegate for BaseKeymapSelector {
cx.emit(Event::Dismissed); cx.emit(Event::Dismissed);
} }
fn dismiss(&mut self, cx: &mut ViewContext<Self>) { fn dismissed(&mut self, cx: &mut ViewContext<Self>) {
cx.emit(Event::Dismissed) cx.emit(Event::Dismissed)
} }

View File

@ -96,6 +96,10 @@ lazy_static! {
.and_then(parse_pixel_position_env_var); .and_then(parse_pixel_position_env_var);
} }
pub trait Modal: View {
fn dismiss_on_event(event: &Self::Event) -> bool;
}
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]
pub struct RemoveWorktreeFromProject(pub WorktreeId); pub struct RemoveWorktreeFromProject(pub WorktreeId);
@ -1335,7 +1339,7 @@ impl Workspace {
add_view: F, add_view: F,
) -> Option<ViewHandle<V>> ) -> Option<ViewHandle<V>>
where where
V: 'static + View, V: 'static + Modal,
F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>, F: FnOnce(&mut Self, &mut ViewContext<Self>) -> ViewHandle<V>,
{ {
cx.notify(); cx.notify();
@ -1347,6 +1351,12 @@ impl Workspace {
Some(already_open_modal) Some(already_open_modal)
} else { } else {
let modal = add_view(self, cx); let modal = add_view(self, cx);
cx.subscribe(&modal, |this, _, event, cx| {
if V::dismiss_on_event(event) {
this.dismiss_modal(cx);
}
})
.detach();
cx.focus(&modal); cx.focus(&modal);
self.modal = Some(modal.into_any()); self.modal = Some(modal.into_any());
None None