diff --git a/crates/picker/src/head.rs b/crates/picker/src/head.rs new file mode 100644 index 0000000000..95de5acf24 --- /dev/null +++ b/crates/picker/src/head.rs @@ -0,0 +1,59 @@ +use std::sync::Arc; + +use editor::{Editor, EditorEvent}; +use gpui::{prelude::*, AppContext, FocusHandle, FocusableView, View}; +use ui::prelude::*; + +/// The head of a [`Picker`](crate::Picker). +pub(crate) enum Head { + /// Picker has an editor that allows the user to filter the list. + Editor(View), + + /// Picker has no head, it's just a list of items. + Empty(View), +} + +impl Head { + pub fn editor( + placeholder_text: Arc, + cx: &mut ViewContext, + edit_handler: impl FnMut(&mut V, View, &EditorEvent, &mut ViewContext<'_, V>) + 'static, + ) -> Self { + let editor = cx.new_view(|cx| { + let mut editor = Editor::single_line(cx); + editor.set_placeholder_text(placeholder_text, cx); + editor + }); + cx.subscribe(&editor, edit_handler).detach(); + Self::Editor(editor) + } + + pub fn empty(cx: &mut WindowContext) -> Self { + Self::Empty(cx.new_view(|cx| EmptyHead::new(cx))) + } +} + +/// An invisible element that can hold focus. +pub(crate) struct EmptyHead { + focus_handle: FocusHandle, +} + +impl EmptyHead { + fn new(cx: &mut ViewContext) -> Self { + Self { + focus_handle: cx.focus_handle(), + } + } +} + +impl Render for EmptyHead { + fn render(&mut self, _: &mut ViewContext) -> impl IntoElement { + div().track_focus(&self.focus_handle) + } +} + +impl FocusableView for EmptyHead { + fn focus_handle(&self, _: &AppContext) -> FocusHandle { + self.focus_handle.clone() + } +} diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index 8d6437c5dc..369192961f 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -5,10 +5,12 @@ use gpui::{ EventEmitter, FocusHandle, FocusableView, Length, ListState, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext, }; +use head::Head; use std::{sync::Arc, time::Duration}; use ui::{prelude::*, v_flex, Color, Divider, Label, ListItem, ListItemSpacing}; use workspace::ModalView; +mod head; pub mod highlighted_match_with_paths; enum ElementContainer { @@ -24,7 +26,7 @@ struct PendingUpdateMatches { pub struct Picker { pub delegate: D, element_container: ElementContainer, - editor: View, + head: Head, pending_update_matches: Option, confirm_on_update: Option, width: Option, @@ -84,37 +86,48 @@ pub trait PickerDelegate: Sized + 'static { impl FocusableView for Picker { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - self.editor.focus_handle(cx) + match &self.head { + Head::Editor(editor) => editor.focus_handle(cx), + Head::Empty(head) => head.focus_handle(cx), + } } } -fn create_editor(placeholder: Arc, cx: &mut WindowContext<'_>) -> View { - cx.new_view(|cx| { - let mut editor = Editor::single_line(cx); - editor.set_placeholder_text(placeholder, cx); - editor - }) -} - impl Picker { /// A picker, which displays its matches using `gpui::uniform_list`, all matches should have the same height. + /// The picker allows the user to perform search items by text. /// If `PickerDelegate::render_match` can return items with different heights, use `Picker::list`. pub fn uniform_list(delegate: D, cx: &mut ViewContext) -> Self { - Self::new(delegate, cx, true) + Self::new(delegate, cx, true, true) + } + + /// A picker, which displays its matches using `gpui::uniform_list`, all matches should have the same height. + /// If `PickerDelegate::render_match` can return items with different heights, use `Picker::list`. + pub fn nonsearchable_uniform_list(delegate: D, cx: &mut ViewContext) -> Self { + Self::new(delegate, cx, true, false) } /// A picker, which displays its matches using `gpui::list`, matches can have different heights. + /// The picker allows the user to perform search items by text. /// If `PickerDelegate::render_match` only returns items with the same height, use `Picker::uniform_list` as its implementation is optimized for that. pub fn list(delegate: D, cx: &mut ViewContext) -> Self { - Self::new(delegate, cx, false) + Self::new(delegate, cx, false, true) } - fn new(delegate: D, cx: &mut ViewContext, is_uniform: bool) -> Self { - let editor = create_editor(delegate.placeholder_text(cx), cx); - cx.subscribe(&editor, Self::on_input_editor_event).detach(); + fn new(delegate: D, cx: &mut ViewContext, is_uniform: bool, is_queryable: bool) -> Self { + let head = if is_queryable { + Head::editor( + delegate.placeholder_text(cx), + cx, + Self::on_input_editor_event, + ) + } else { + Head::empty(cx) + }; + let mut this = Self { delegate, - editor, + head, element_container: Self::create_element_container(is_uniform, cx), pending_update_matches: None, confirm_on_update: None, @@ -123,7 +136,7 @@ impl Picker { is_modal: true, }; this.update_matches("".to_string(), cx); - // give the delegate 4ms to renderthe first set of suggestions. + // give the delegate 4ms to render the first set of suggestions. this.delegate .finalize_update_matches("".to_string(), Duration::from_millis(4), cx); this @@ -167,7 +180,7 @@ impl Picker { } pub fn focus(&self, cx: &mut WindowContext) { - self.editor.update(cx, |editor, cx| editor.focus(cx)); + self.focus_handle(cx).focus(cx); } pub fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext) { @@ -269,9 +282,12 @@ impl Picker { event: &editor::EditorEvent, cx: &mut ViewContext, ) { + let Head::Editor(ref editor) = &self.head else { + panic!("unexpected call"); + }; match event { editor::EditorEvent::BufferEdited => { - let query = self.editor.read(cx).text(cx); + let query = editor.read(cx).text(cx); self.update_matches(query, cx); } editor::EditorEvent::Blurred => { @@ -282,7 +298,7 @@ impl Picker { } pub fn refresh(&mut self, cx: &mut ViewContext) { - let query = self.editor.read(cx).text(cx); + let query = self.query(cx); self.update_matches(query, cx); } @@ -330,17 +346,22 @@ impl Picker { } pub fn query(&self, cx: &AppContext) -> String { - self.editor.read(cx).text(cx) + match &self.head { + Head::Editor(editor) => editor.read(cx).text(cx), + Head::Empty(_) => "".to_string(), + } } pub fn set_query(&self, query: impl Into>, cx: &mut ViewContext) { - self.editor.update(cx, |editor, cx| { - editor.set_text(query, cx); - let editor_offset = editor.buffer().read(cx).len(cx); - editor.change_selections(Some(Autoscroll::Next), cx, |s| { - s.select_ranges(Some(editor_offset..editor_offset)) + if let Head::Editor(ref editor) = &self.head { + editor.update(cx, |editor, cx| { + editor.set_text(query, cx); + let editor_offset = editor.buffer().read(cx).len(cx); + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges(Some(editor_offset..editor_offset)) + }); }); - }); + } } fn scroll_to_item_index(&mut self, ix: usize) { @@ -400,13 +421,6 @@ impl ModalView for Picker {} impl Render for Picker { fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let picker_editor = h_flex() - .overflow_hidden() - .flex_none() - .h_9() - .px_4() - .child(self.editor.clone()); - div() .key_context("Picker") .size_full() @@ -425,8 +439,19 @@ impl Render for Picker { .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::secondary_confirm)) .on_action(cx.listener(Self::use_selected_query)) - .child(picker_editor) - .child(Divider::horizontal()) + .child(match &self.head { + Head::Editor(editor) => v_flex() + .child( + h_flex() + .overflow_hidden() + .flex_none() + .h_9() + .px_4() + .child(editor.clone()), + ) + .child(Divider::horizontal()), + Head::Empty(empty_head) => div().child(empty_head.clone()), + }) .when(self.delegate.match_count() > 0, |el| { el.child( v_flex()