Start work on adding a filter editor to the picker

Implement picker as a view instead of as a component

Co-authored-by: Mikayla <mikayla@zed.dev>
Co-authored-by: Marshall <marshall@zed.dev>
This commit is contained in:
Max Brunsfeld 2023-11-07 13:37:10 -08:00
parent 80e6427eec
commit b9d051eae7
5 changed files with 173 additions and 197 deletions

2
Cargo.lock generated
View file

@ -8557,8 +8557,10 @@ dependencies = [
"backtrace-on-stack-overflow",
"chrono",
"clap 4.4.4",
"editor2",
"gpui2",
"itertools 0.11.0",
"language2",
"log",
"menu2",
"picker2",

View file

@ -1,149 +1,121 @@
use editor::Editor;
use gpui::{
div, uniform_list, Component, ElementId, FocusHandle, ParentElement, StatelessInteractive,
Styled, UniformListScrollHandle, ViewContext,
div, uniform_list, Component, Div, ParentElement, Render, StatelessInteractive, Styled,
UniformListScrollHandle, View, ViewContext, VisualContext,
};
use std::cmp;
#[derive(Component)]
pub struct Picker<V: PickerDelegate> {
id: ElementId,
focus_handle: FocusHandle,
phantom: std::marker::PhantomData<V>,
pub struct Picker<D: PickerDelegate> {
pub delegate: D,
scroll_handle: UniformListScrollHandle,
editor: View<Editor>,
}
pub trait PickerDelegate: Sized + 'static {
type ListItem: Component<Self>;
type ListItem: Component<Picker<Self>>;
fn match_count(&self, picker_id: ElementId) -> usize;
fn selected_index(&self, picker_id: ElementId) -> usize;
fn set_selected_index(&mut self, ix: usize, picker_id: ElementId, cx: &mut ViewContext<Self>);
fn match_count(&self) -> usize;
fn selected_index(&self) -> usize;
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);
// fn placeholder_text(&self) -> Arc<str>;
// fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()>;
fn confirm(&mut self, secondary: bool, picker_id: ElementId, cx: &mut ViewContext<Self>);
fn dismissed(&mut self, picker_id: ElementId, cx: &mut ViewContext<Self>);
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
fn render_match(
&self,
ix: usize,
selected: bool,
picker_id: ElementId,
cx: &mut ViewContext<Self>,
cx: &mut ViewContext<Picker<Self>>,
) -> Self::ListItem;
}
impl<V: PickerDelegate> Picker<V> {
pub fn new(id: impl Into<ElementId>, focus_handle: FocusHandle) -> Self {
impl<D: PickerDelegate> Picker<D> {
pub fn new(delegate: D, cx: &mut ViewContext<Self>) -> Self {
Self {
id: id.into(),
focus_handle,
phantom: std::marker::PhantomData,
delegate,
scroll_handle: UniformListScrollHandle::new(),
editor: cx.build_view(|cx| Editor::single_line(cx)),
}
}
fn bind_actions<T: StatelessInteractive<V>>(
div: T,
id: ElementId,
scroll_handle: &UniformListScrollHandle,
) -> T {
div.on_action({
let id = id.clone();
let scroll_handle = scroll_handle.clone();
move |view: &mut V, _: &menu::SelectNext, cx| {
let count = view.match_count(id.clone());
if count > 0 {
let index = view.selected_index(id.clone());
let ix = cmp::min(index + 1, count - 1);
view.set_selected_index(ix, id.clone(), cx);
scroll_handle.scroll_to_item(ix);
}
}
})
.on_action({
let id = id.clone();
let scroll_handle = scroll_handle.clone();
move |view, _: &menu::SelectPrev, cx| {
let count = view.match_count(id.clone());
if count > 0 {
let index = view.selected_index(id.clone());
let ix = index.saturating_sub(1);
view.set_selected_index(ix, id.clone(), cx);
scroll_handle.scroll_to_item(ix);
}
}
})
.on_action({
let id = id.clone();
let scroll_handle = scroll_handle.clone();
move |view: &mut V, _: &menu::SelectFirst, cx| {
let count = view.match_count(id.clone());
if count > 0 {
view.set_selected_index(0, id.clone(), cx);
scroll_handle.scroll_to_item(0);
}
}
})
.on_action({
let id = id.clone();
let scroll_handle = scroll_handle.clone();
move |view: &mut V, _: &menu::SelectLast, cx| {
let count = view.match_count(id.clone());
if count > 0 {
view.set_selected_index(count - 1, id.clone(), cx);
scroll_handle.scroll_to_item(count - 1);
}
}
})
.on_action({
let id = id.clone();
move |view: &mut V, _: &menu::Cancel, cx| {
view.dismissed(id.clone(), cx);
}
})
.on_action({
let id = id.clone();
move |view: &mut V, _: &menu::Confirm, cx| {
view.confirm(false, id.clone(), cx);
}
})
.on_action({
let id = id.clone();
move |view: &mut V, _: &menu::SecondaryConfirm, cx| {
view.confirm(true, id.clone(), cx);
}
})
fn select_next(&mut self, _: &menu::SelectNext, cx: &mut ViewContext<Self>) {
let count = self.delegate.match_count();
if count > 0 {
let index = self.delegate.selected_index();
let ix = cmp::min(index + 1, count - 1);
self.delegate.set_selected_index(ix, cx);
self.scroll_handle.scroll_to_item(ix);
}
}
fn select_prev(&mut self, _: &menu::SelectPrev, cx: &mut ViewContext<Self>) {
let count = self.delegate.match_count();
if count > 0 {
let index = self.delegate.selected_index();
let ix = index.saturating_sub(1);
self.delegate.set_selected_index(ix, cx);
self.scroll_handle.scroll_to_item(ix);
}
}
fn select_first(&mut self, _: &menu::SelectFirst, cx: &mut ViewContext<Self>) {
let count = self.delegate.match_count();
if count > 0 {
self.delegate.set_selected_index(0, cx);
self.scroll_handle.scroll_to_item(0);
}
}
fn select_last(&mut self, _: &menu::SelectLast, cx: &mut ViewContext<Self>) {
let count = self.delegate.match_count();
if count > 0 {
self.delegate.set_selected_index(count - 1, cx);
self.scroll_handle.scroll_to_item(count - 1);
}
}
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
self.delegate.dismissed(cx);
}
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
self.delegate.confirm(false, cx);
}
fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
self.delegate.confirm(true, cx);
}
}
impl<V: 'static + PickerDelegate> Picker<V> {
pub fn render(self, view: &mut V, _cx: &mut ViewContext<V>) -> impl Component<V> {
let id = self.id.clone();
let scroll_handle = UniformListScrollHandle::new();
Self::bind_actions(
div()
.id(self.id.clone())
.size_full()
.track_focus(&self.focus_handle)
.context("picker")
.child(
uniform_list(
"candidates",
view.match_count(self.id.clone()),
move |view: &mut V, visible_range, cx| {
let selected_ix = view.selected_index(self.id.clone());
visible_range
.map(|ix| {
view.render_match(ix, ix == selected_ix, self.id.clone(), cx)
})
.collect()
},
)
.track_scroll(scroll_handle.clone())
.size_full(),
),
id,
&scroll_handle,
)
impl<D: PickerDelegate> Render for Picker<D> {
type Element = Div<Self>;
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
div()
.size_full()
.context("picker")
.on_action(Self::select_next)
.on_action(Self::select_prev)
.on_action(Self::select_first)
.on_action(Self::select_last)
.on_action(Self::cancel)
.on_action(Self::confirm)
.on_action(Self::secondary_confirm)
.child(self.editor.clone())
.child(
uniform_list("candidates", self.delegate.match_count(), {
move |this: &mut Self, visible_range, cx| {
let selected_ix = this.delegate.selected_index();
visible_range
.map(|ix| this.delegate.render_match(ix, ix == selected_ix, cx))
.collect()
}
})
.track_scroll(self.scroll_handle.clone())
.size_full(),
)
}
}

View file

@ -13,9 +13,11 @@ anyhow.workspace = true
# TODO: Remove after diagnosing stack overflow.
backtrace-on-stack-overflow = "0.3.0"
clap = { version = "4.4", features = ["derive", "string"] }
editor = { package = "editor2", path = "../editor2" }
chrono = "0.4"
gpui = { package = "gpui2", path = "../gpui2" }
itertools = "0.11.0"
language = { package = "language2", path = "../language2" }
log.workspace = true
rust-embed.workspace = true
serde.workspace = true

View file

@ -6,15 +6,19 @@ use picker::{Picker, PickerDelegate};
use theme2::ActiveTheme;
pub struct PickerStory {
selected_ix: usize,
candidates: Vec<SharedString>,
picker: View<Picker<Delegate>>,
focus_handle: FocusHandle,
}
impl PickerDelegate for PickerStory {
type ListItem = Div<Self>;
struct Delegate {
candidates: Vec<SharedString>,
selected_ix: usize,
}
fn match_count(&self, _picker_id: gpui::ElementId) -> usize {
impl PickerDelegate for Delegate {
type ListItem = Div<Picker<Self>>;
fn match_count(&self) -> usize {
self.candidates.len()
}
@ -22,8 +26,7 @@ impl PickerDelegate for PickerStory {
&self,
ix: usize,
selected: bool,
_picker_id: gpui::ElementId,
cx: &mut gpui::ViewContext<Self>,
cx: &mut gpui::ViewContext<Picker<Self>>,
) -> Self::ListItem {
let colors = cx.theme().colors();
@ -40,26 +43,16 @@ impl PickerDelegate for PickerStory {
.child(self.candidates[ix].clone())
}
fn selected_index(&self, picker_id: gpui::ElementId) -> usize {
fn selected_index(&self) -> usize {
self.selected_ix
}
fn set_selected_index(
&mut self,
ix: usize,
_picker_id: gpui::ElementId,
cx: &mut gpui::ViewContext<Self>,
) {
fn set_selected_index(&mut self, ix: usize, cx: &mut gpui::ViewContext<Picker<Self>>) {
self.selected_ix = ix;
cx.notify();
}
fn confirm(
&mut self,
secondary: bool,
picker_id: gpui::ElementId,
cx: &mut gpui::ViewContext<Self>,
) {
fn confirm(&mut self, secondary: bool, cx: &mut gpui::ViewContext<Picker<Self>>) {
if secondary {
eprintln!("Secondary confirmed {}", self.candidates[self.selected_ix])
} else {
@ -67,7 +60,7 @@ impl PickerDelegate for PickerStory {
}
}
fn dismissed(&mut self, picker_id: gpui::ElementId, cx: &mut gpui::ViewContext<Self>) {
fn dismissed(&mut self, cx: &mut gpui::ViewContext<Picker<Self>>) {
cx.quit();
}
}
@ -98,58 +91,65 @@ impl PickerStory {
PickerStory {
focus_handle,
candidates: vec![
"Baguette (France)".into(),
"Baklava (Turkey)".into(),
"Beef Wellington (UK)".into(),
"Biryani (India)".into(),
"Borscht (Ukraine)".into(),
"Bratwurst (Germany)".into(),
"Bulgogi (Korea)".into(),
"Burrito (USA)".into(),
"Ceviche (Peru)".into(),
"Chicken Tikka Masala (India)".into(),
"Churrasco (Brazil)".into(),
"Couscous (North Africa)".into(),
"Croissant (France)".into(),
"Dim Sum (China)".into(),
"Empanada (Argentina)".into(),
"Fajitas (Mexico)".into(),
"Falafel (Middle East)".into(),
"Feijoada (Brazil)".into(),
"Fish and Chips (UK)".into(),
"Fondue (Switzerland)".into(),
"Goulash (Hungary)".into(),
"Haggis (Scotland)".into(),
"Kebab (Middle East)".into(),
"Kimchi (Korea)".into(),
"Lasagna (Italy)".into(),
"Maple Syrup Pancakes (Canada)".into(),
"Moussaka (Greece)".into(),
"Pad Thai (Thailand)".into(),
"Paella (Spain)".into(),
"Pancakes (USA)".into(),
"Pasta Carbonara (Italy)".into(),
"Pavlova (Australia)".into(),
"Peking Duck (China)".into(),
"Pho (Vietnam)".into(),
"Pierogi (Poland)".into(),
"Pizza (Italy)".into(),
"Poutine (Canada)".into(),
"Pretzel (Germany)".into(),
"Ramen (Japan)".into(),
"Rendang (Indonesia)".into(),
"Sashimi (Japan)".into(),
"Satay (Indonesia)".into(),
"Shepherd's Pie (Ireland)".into(),
"Sushi (Japan)".into(),
"Tacos (Mexico)".into(),
"Tandoori Chicken (India)".into(),
"Tortilla (Spain)".into(),
"Tzatziki (Greece)".into(),
"Wiener Schnitzel (Austria)".into(),
],
selected_ix: 0,
picker: cx.build_view(|cx| {
Picker::new(
Delegate {
candidates: vec![
"Baguette (France)".into(),
"Baklava (Turkey)".into(),
"Beef Wellington (UK)".into(),
"Biryani (India)".into(),
"Borscht (Ukraine)".into(),
"Bratwurst (Germany)".into(),
"Bulgogi (Korea)".into(),
"Burrito (USA)".into(),
"Ceviche (Peru)".into(),
"Chicken Tikka Masala (India)".into(),
"Churrasco (Brazil)".into(),
"Couscous (North Africa)".into(),
"Croissant (France)".into(),
"Dim Sum (China)".into(),
"Empanada (Argentina)".into(),
"Fajitas (Mexico)".into(),
"Falafel (Middle East)".into(),
"Feijoada (Brazil)".into(),
"Fish and Chips (UK)".into(),
"Fondue (Switzerland)".into(),
"Goulash (Hungary)".into(),
"Haggis (Scotland)".into(),
"Kebab (Middle East)".into(),
"Kimchi (Korea)".into(),
"Lasagna (Italy)".into(),
"Maple Syrup Pancakes (Canada)".into(),
"Moussaka (Greece)".into(),
"Pad Thai (Thailand)".into(),
"Paella (Spain)".into(),
"Pancakes (USA)".into(),
"Pasta Carbonara (Italy)".into(),
"Pavlova (Australia)".into(),
"Peking Duck (China)".into(),
"Pho (Vietnam)".into(),
"Pierogi (Poland)".into(),
"Pizza (Italy)".into(),
"Poutine (Canada)".into(),
"Pretzel (Germany)".into(),
"Ramen (Japan)".into(),
"Rendang (Indonesia)".into(),
"Sashimi (Japan)".into(),
"Satay (Indonesia)".into(),
"Shepherd's Pie (Ireland)".into(),
"Sushi (Japan)".into(),
"Tacos (Mexico)".into(),
"Tandoori Chicken (India)".into(),
"Tortilla (Spain)".into(),
"Tzatziki (Greece)".into(),
"Wiener Schnitzel (Austria)".into(),
],
selected_ix: 0,
},
cx,
)
}),
}
})
}
@ -159,11 +159,9 @@ impl Render for PickerStory {
type Element = Div<Self>;
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> Self::Element {
let theme = cx.theme();
div()
.bg(theme.styles.colors.background)
.bg(cx.theme().styles.colors.background)
.size_full()
.child(Picker::new("picker_story", self.focus_handle.clone()))
.child(self.picker.clone())
}
}

View file

@ -72,6 +72,8 @@ fn main() {
ThemeSettings::override_global(theme_settings, cx);
ui::settings::init(cx);
language::init(cx);
editor::init(cx);
let window = cx.open_window(
WindowOptions {