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", "backtrace-on-stack-overflow",
"chrono", "chrono",
"clap 4.4.4", "clap 4.4.4",
"editor2",
"gpui2", "gpui2",
"itertools 0.11.0", "itertools 0.11.0",
"language2",
"log", "log",
"menu2", "menu2",
"picker2", "picker2",

View file

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

View file

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

View file

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