From 7512c9949ccba45bd91710c295b0d92470422784 Mon Sep 17 00:00:00 2001 From: Andrew Lygin Date: Mon, 18 Mar 2024 18:59:53 +0300 Subject: [PATCH] Allow `Picker` to be headless (#9099) At the moment, `Picker` always has an editor at the top, that allows the user to search list elements by text. Sometimes, the UI doesn't need such an editor. Like in the [tab switcher](https://github.com/zed-industries/zed/issues/7653) that will confirm selection on the modifier keys release, so there will be no searching capabilities. This PR adds support for a "headless picker" that doesn't display an editor. It only has an invisible element to hold input focus for preventing it from jumping back to the workspace. At the moment, none of the picker implementations is made headless. It's for the future implementations. But I'd like to make it a separate PR to keep it focused on this particular feature. Release Notes: - N/A Related Issues: - Part of #7653 --------- Co-authored-by: Marshall Bowers --- crates/picker/src/head.rs | 59 ++++++++++++++++++++++ crates/picker/src/picker.rs | 97 +++++++++++++++++++++++-------------- 2 files changed, 120 insertions(+), 36 deletions(-) create mode 100644 crates/picker/src/head.rs 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()