Fix Picker to search filter.

This commit is contained in:
Jason Lee 2024-06-27 18:07:08 +08:00
parent 659f00c9a4
commit 8489a3f7f7
7 changed files with 356 additions and 32 deletions

View file

@ -0,0 +1,152 @@
use std::{rc::Rc, vec};
use gpui::{
div, ClickEvent, IntoElement, ParentElement, Render, RenderOnce, SharedString, Styled,
ViewContext, WindowContext,
};
use ui::{
checkbox::Checkbox,
dropdown::{Dropdown, DropdownItem},
h_flex, v_flex, Disableable as _, Selection,
};
use super::story_case;
struct Country {
name: &'static str,
code: &'static str,
}
impl Country {
pub fn new(name: &'static str, code: &'static str) -> Self {
Self { name, code }
}
}
impl DropdownItem for Country {
fn title(&self) -> SharedString {
self.name.into()
}
fn value(&self) -> SharedString {
self.code.into()
}
}
#[derive(IntoElement)]
pub struct DropdownStory {
dropdown1: Dropdown,
dropdown2: Dropdown,
countries: Vec<Country>,
}
impl DropdownStory {
pub(crate) fn new(cx: &mut WindowContext) -> Self {
let countries = vec![
Country::new("United States", "US"),
Country::new("Canada", "CA"),
Country::new("Mexico", "MX"),
Country::new("Brazil", "BR"),
Country::new("Argentina", "AR"),
Country::new("Chile", "CL"),
Country::new("China", "CN"),
Country::new("Peru", "PE"),
Country::new("Colombia", "CO"),
Country::new("Venezuela", "VE"),
Country::new("Ecuador", "EC"),
];
let items2 = vec![
"Apple",
"Orange",
"Banana",
"Grape",
"Pineapple",
"Watermelon",
"Avocado",
];
Self {
countries,
dropdown1: Dropdown::new("dropdown-country", Rc::new(countries), cx),
dropdown2: Dropdown::new("dropdown-fruit", Rc::new(items2), cx),
}
}
#[allow(unused)]
fn on_click(sel: &Selection, cx: &mut WindowContext) {
println!("Check value changed: {}", sel);
}
}
impl RenderOnce for CheckboxStory {
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
story_case(
"Checkbox",
"A control that allows the user to toggle between checked and not checked.",
)
.child(
v_flex().items_start().justify_start().gap_4().child(
h_flex()
.items_center()
.gap_4()
.child(self.check1)
.child(self.check1_1)
.child(
Checkbox::new("check1_2", cx)
.checked(Selection::Selected)
.on_click(Self::on_click),
),
),
)
.child(
h_flex()
.items_center()
.gap_4()
.child(
Checkbox::new("check2", cx)
.checked(Selection::Unselected)
.label("With label (Unchecked)")
.on_click(Self::on_click),
)
.child(
Checkbox::new("check2_1", cx)
.label("With Label (Indeterminate)")
.checked(Selection::Indeterminate)
.on_click(Self::on_click),
)
.child(
Checkbox::new("check2_2", cx)
.label("With Label (Checked)")
.checked(Selection::Selected)
.on_click(Self::on_click),
),
)
.child(
h_flex().items_center().gap_4().child(
h_flex()
.items_center()
.gap_4()
.child(
Checkbox::new("check3", cx)
.label("Disabled Checked")
.checked(Selection::Selected)
.disabled(true),
)
.child(
Checkbox::new("check3_1", cx)
.label("Disabled Unchecked")
.checked(Selection::Unselected)
.disabled(true),
)
.child(
Checkbox::new("check3_2", cx)
.label("Disabled Indeterminate")
.checked(Selection::Indeterminate)
.disabled(true),
),
),
)
}
}

View file

@ -6,11 +6,10 @@ use gpui::{
RenderOnce, SharedString, StatefulInteractiveElement as _, Styled as _, View, ViewContext,
VisualContext, WindowContext,
};
use picker_story::PickerStory;
use switch_story::SwitchStory;
mod button_story;
mod checkbox_story;
// mod dropdown_story;
mod input_story;
mod picker_story;
mod switch_story;
@ -23,6 +22,8 @@ use ui::{
use button_story::ButtonStory;
use input_story::InputStory;
use picker_story::PickerStory;
use switch_story::SwitchStory;
pub fn story_case(name: &'static str, description: &'static str) -> StoryContainer {
StoryContainer::new(name, description)

View file

@ -22,7 +22,6 @@ actions!(picker_story, [DismissPicker]);
pub struct ListItemDeletegate {
story: WeakView<PickerStory>,
selected_index: usize,
items: Vec<String>,
matches: Vec<String>,
}
@ -48,7 +47,7 @@ impl PickerDelegate for ListItemDeletegate {
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
if let Some(item) = self.matches.get(ix) {
let list_item = ListItem::new(format!("item-{}", ix))
let list_item = ListItem::new(("item", ix))
.check_icon(ui::IconName::Check)
.selected(selected)
.py_1()
@ -65,14 +64,18 @@ impl PickerDelegate for ListItemDeletegate {
query: &str,
cx: &mut ViewContext<Picker<Self>>,
) -> gpui::Task<()> {
let matched_items = self
.items
.iter()
.filter(|item| item.contains(query))
.cloned()
.collect();
if let Some(story) = self.story.upgrade() {
let matched_items = story
.read(cx)
.items
.iter()
.filter(|item| item.contains(query))
.cloned()
.collect();
self.matches = matched_items;
self.matches = matched_items;
cx.notify();
}
Task::ready(())
}
@ -102,12 +105,13 @@ impl PickerDelegate for ListItemDeletegate {
pub struct PickerStory {
picker: View<Picker<ListItemDeletegate>>,
open: bool,
items: Vec<String>,
selected_value: Option<String>,
}
impl PickerStory {
pub(crate) fn new(cx: &mut ViewContext<Self>) -> Self {
let items = [
let items: Vec<String> = [
"Baguette (France)",
"Baklava (Turkey)",
"Beef Wellington (UK)",
@ -157,18 +161,18 @@ impl PickerStory {
"Tortilla (Spain)",
"Tzatziki (Greece)",
"Wiener Schnitzel (Austria)",
];
]
.iter()
.map(|s| s.to_string())
.collect();
let story = cx.view().downgrade();
let picker = cx.new_view(|cx| {
let items: Vec<String> = items.iter().map(|s| s.to_string()).collect();
let mut picker = Picker::uniform_list(
ListItemDeletegate {
story,
selected_index: 0,
matches: items.clone(),
items,
},
cx,
)
@ -180,6 +184,7 @@ impl PickerStory {
});
Self {
items,
picker,
open: false,
selected_value: None,

View file

@ -1,9 +1,123 @@
use std::rc::Rc;
use gpui::{
div, prelude::FluentBuilder as _, Div, ElementId, InteractiveElement, IntoElement,
ParentElement as _, Render, RenderOnce, SharedString, Stateful, Styled as _, View, ViewContext,
VisualContext as _, WeakView,
};
pub struct DropdownItemDelegate {
items: Vec<ListItem>,
use crate::{
list::ListItem,
picker::{Picker, PickerDelegate},
IconName,
};
/// A trait for items that can be displayed in a dropdown.
pub trait DropdownItem {
fn title(&self) -> &str;
fn value(&self) -> &str;
}
pub struct Dropdown {
picker:
}
pub trait DropdownDelegate {
type Item: DropdownItem;
fn len(&self) -> usize;
fn get(&self, index: usize) -> Option<&Self::Item>;
}
struct DropdownPickerDelegate<D: DropdownDelegate> {
dropdown: WeakView<Dropdown<D>>,
selected_index: usize,
}
impl<D> PickerDelegate for DropdownPickerDelegate<D: PickerDelegate> {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.delegate.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, index: usize, _cx: &mut gpui::ViewContext<Picker<Self>>) {
self.selected_index = index;
}
fn render_match(
&self,
ix: usize,
selected: bool,
_cx: &mut gpui::ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
if let Some(item) = self.delegate.get(ix) {
let list_item = ListItem::new(("list-item", ix))
.check_icon(IconName::Check)
.selected(selected)
.py_1()
.px_3()
.child(item.title());
Some(list_item)
} else {
None
}
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
if let Some(view) = self.dropdown.upgrade() {
cx.update_view(&view, |view, cx| {
view.open = false;
cx.notify();
});
}
}
fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
if let Some(view) = self.dropdown.upgrade() {
cx.update_view(&view, |view, cx| {
if let Some(item) = self.delegate.get(self.selected_index) {
view.value = Some(item.value());
}
view.open = false;
cx.notify();
});
}
}
}
pub struct Dropdown<D: DropdownDelegate> {
base: Stateful<Div>,
delegate: D,
picker: View<Picker<DropdownPickerDelegate>>,
open: bool,
/// The value of the selected item.
value: Option<SharedString>,
}
impl<D: DropdownDelegate> Dropdown<D> {
pub fn new(id: impl Into<ElementId>, delegate: D, cx: &mut ViewContext<Self>) -> Self {
let delegate = DropdownPickerDelegate {
// delegate,
dropdown: cx.view().downgrade(),
selected_index: 0,
};
let picker = cx.new_view(|cx| Picker::uniform_list(delegate, cx));
Self {
delegate,
base: div().id(id.into()),
picker,
open: false,
value: None,
}
}
}
impl RenderOnce for Dropdown {
fn render(self, cx: &mut gpui::WindowContext) -> impl IntoElement {
self.base
.child(self.value.unwrap_or_else(|| "Select...".into()))
.when(self.open, |this| this.child(self.picker))
}
}

View file

@ -7,7 +7,7 @@ use prelude::FluentBuilder as _;
use unicode_segmentation::*;
actions!(
text_input,
input,
[
Backspace,
Delete,
@ -24,9 +24,14 @@ actions!(
Paste,
MoveToStartOfLine,
MoveToEndOfLine,
TextChanged,
]
);
pub enum TextEvent {
Input { text: SharedString },
}
pub fn init(cx: &mut AppContext) {
cx.bind_keys([
KeyBinding::new("backspace", Backspace, None),
@ -60,6 +65,8 @@ pub struct TextInput {
appearance: bool,
}
impl EventEmitter<TextEvent> for TextInput {}
impl TextInput {
pub fn new(cx: &mut ViewContext<Self>) -> Self {
Self {
@ -316,6 +323,9 @@ impl ViewInputHandler for TextInput {
(self.text[0..range.start].to_owned() + new_text + &self.text[range.end..]).into();
self.selected_range = range.start + new_text.len()..range.start + new_text.len();
self.marked_range.take();
cx.emit(TextEvent::Input {
text: self.text.clone(),
});
cx.notify();
}
@ -340,7 +350,9 @@ impl ViewInputHandler for TextInput {
.map(|range_utf16| self.range_from_utf16(range_utf16))
.map(|new_range| new_range.start + range.start..new_range.end + range.end)
.unwrap_or_else(|| range.start + new_text.len()..range.start + new_text.len());
cx.emit(TextEvent::Input {
text: self.text.clone(),
});
cx.notify();
}

View file

@ -1,5 +1,5 @@
use gpui::{
div, prelude::FluentBuilder as _, ClickEvent, Div, InteractiveElement, IntoElement,
div, prelude::FluentBuilder as _, ClickEvent, Div, ElementId, InteractiveElement, IntoElement,
MouseButton, MouseDownEvent, ParentElement, RenderOnce, SharedString, Stateful,
StatefulInteractiveElement as _, Style, Styled, WindowContext,
};
@ -17,7 +17,7 @@ pub struct ListItem {
}
impl ListItem {
pub fn new(id: impl Into<SharedString>) -> Self {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
base: h_flex().id(id.into()),
disabled: false,
@ -33,6 +33,16 @@ impl ListItem {
self
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
self.on_click = Some(Box::new(handler));
self

View file

@ -24,7 +24,12 @@ actions!(
);
use crate::{
divider::Divider, empty::Empty, input::TextInput, label::Label, stock::*, theme::ActiveTheme,
divider::Divider,
empty::Empty,
input::{TextEvent, TextInput},
label::Label,
stock::*,
theme::ActiveTheme,
StyledExt as _,
};
@ -80,7 +85,9 @@ pub trait PickerDelegate: Sized + 'static {
fn separators_after_indices(&self) -> Vec<usize> {
Vec::new()
}
fn update_matches(&mut self, query: &str, cx: &mut ViewContext<Picker<Self>>) -> Task<()>;
fn update_matches(&mut self, query: &str, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
Task::ready(())
}
fn confirm_update_query(&mut self, _cx: &mut ViewContext<Picker<Self>>) -> Option<String> {
None
}
@ -173,11 +180,13 @@ impl<D: PickerDelegate> Picker<D> {
placehoder: impl Into<SharedString>,
cx: &mut ViewContext<Self>,
) -> View<TextInput> {
cx.new_view(|cx| {
let input = cx.new_view(|cx| {
let mut input = TextInput::new(cx).appearance(false);
input.set_placeholder(placehoder, cx);
input
})
});
cx.subscribe(&input, Self::on_query_input_event).detach();
input
}
pub fn list(delegate: D, cx: &mut ViewContext<Self>) -> Self {
@ -444,6 +453,22 @@ impl<D: PickerDelegate> Picker<D> {
// }
cx.notify();
}
fn on_query_input_event(
&mut self,
_: View<TextInput>,
event: &TextEvent,
cx: &mut ViewContext<Self>,
) {
#[allow(clippy::single_match)]
match event {
TextEvent::Input { text } => {
self.set_query(text, cx);
self.refresh(cx);
}
_ => {}
}
}
}
impl<D: PickerDelegate> Render for Picker<D> {
@ -481,9 +506,14 @@ impl<D: PickerDelegate> Render for Picker<D> {
.when(self.delegate.match_count() == 0, |el| {
el.child(
v_flex()
.flex_grow()
.py_2()
.child(div().child(Label::new("No matched.").text_color(cx.theme().muted))),
.h_full()
.size_full()
.h_16()
.items_center()
.content_center()
.justify_center()
.text_color(cx.theme().muted_foreground)
.child("No matched."),
)
})
.on_key_down(cx.listener(|this, ev: &gpui::KeyDownEvent, cx| {