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:
parent
80e6427eec
commit
b9d051eae7
5 changed files with 173 additions and 197 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -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",
|
||||||
|
|
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue