@@ -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<Editor>),
+
+ /// Picker has no head, it's just a list of items.
+ Empty(View<EmptyHead>),
+}
+
+impl Head {
+ pub fn editor<V: 'static>(
+ placeholder_text: Arc<str>,
+ cx: &mut ViewContext<V>,
+ edit_handler: impl FnMut(&mut V, View<Editor>, &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 {
+ Self {
+ focus_handle: cx.focus_handle(),
+ }
+ }
+}
+
+impl Render for EmptyHead {
+ fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
+ div().track_focus(&self.focus_handle)
+ }
+}
+
+impl FocusableView for EmptyHead {
+ fn focus_handle(&self, _: &AppContext) -> FocusHandle {
+ self.focus_handle.clone()
+ }
+}
@@ -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<D: PickerDelegate> {
pub delegate: D,
element_container: ElementContainer,
- editor: View<Editor>,
+ head: Head,
pending_update_matches: Option<PendingUpdateMatches>,
confirm_on_update: Option<bool>,
width: Option<Length>,
@@ -84,37 +86,48 @@ pub trait PickerDelegate: Sized + 'static {
impl<D: PickerDelegate> FocusableView for Picker<D> {
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<str>, cx: &mut WindowContext<'_>) -> View<Editor> {
- cx.new_view(|cx| {
- let mut editor = Editor::single_line(cx);
- editor.set_placeholder_text(placeholder, cx);
- editor
- })
-}
-
impl<D: PickerDelegate> Picker<D> {
/// 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 {
- 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 {
+ 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 {
- Self::new(delegate, cx, false)
+ Self::new(delegate, cx, false, true)
}
- fn new(delegate: D, cx: &mut ViewContext<Self>, 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<Self>, 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<D: PickerDelegate> Picker<D> {
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<D: PickerDelegate> Picker<D> {
}
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<Self>) {
@@ -269,9 +282,12 @@ impl<D: PickerDelegate> Picker<D> {
event: &editor::EditorEvent,
cx: &mut ViewContext<Self>,
) {
+ 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<D: PickerDelegate> Picker<D> {
}
pub fn refresh(&mut self, cx: &mut ViewContext<Self>) {
- let query = self.editor.read(cx).text(cx);
+ let query = self.query(cx);
self.update_matches(query, cx);
}
@@ -330,17 +346,22 @@ impl<D: PickerDelegate> Picker<D> {
}
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<Arc<str>>, cx: &mut ViewContext<Self>) {
- 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<D: PickerDelegate> ModalView for Picker<D> {}
impl<D: PickerDelegate> Render for Picker<D> {
fn render(&mut self, cx: &mut ViewContext<Self>) -> 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<D: PickerDelegate> Render for Picker<D> {
.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()