Fix Picker to search filter.
This commit is contained in:
parent
659f00c9a4
commit
8489a3f7f7
7 changed files with 356 additions and 32 deletions
152
crates/ui-story/src/dropdown_story.rs
Normal file
152
crates/ui-story/src/dropdown_story.rs
Normal 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),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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| {
|
||||
|
|
|
|||
Loading…
Reference in a new issue