list: Add searchable, search_placeholder option to List and Select. (#1503)
## Break Change - Removed `set_query_input`, `query_input` and `no_query` method from List. Now use `List::new(&state).searchable(false)` to intead of `no_query`. - Removed `searchable` method from `SelectDelegate`, use `Select::searchable` instead.
This commit is contained in:
parent
27f1f960a0
commit
2870bc6e36
8 changed files with 130 additions and 96 deletions
|
|
@ -15,7 +15,7 @@ use gpui_component::{
|
|||
date_picker::{DatePicker, DatePickerState},
|
||||
h_flex,
|
||||
input::{Input, InputState},
|
||||
list::{ListDelegate, ListItem, ListState},
|
||||
list::{List, ListDelegate, ListItem, ListState},
|
||||
v_flex,
|
||||
webview::WebView,
|
||||
wry,
|
||||
|
|
@ -245,11 +245,6 @@ impl DrawerStory {
|
|||
let list = cx.new(|cx| {
|
||||
let mut list = ListState::new(delegate, window, cx);
|
||||
list.focus(window, cx);
|
||||
if let Some(query_input) = list.query_input() {
|
||||
query_input.update(cx, |input, cx| {
|
||||
input.set_placeholder("Pickup your country...", window, cx);
|
||||
})
|
||||
}
|
||||
list
|
||||
});
|
||||
|
||||
|
|
@ -308,14 +303,13 @@ impl DrawerStory {
|
|||
}),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
List::new(&list)
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.rounded(cx.theme().radius)
|
||||
.size_full()
|
||||
.flex_1()
|
||||
.h(list_h)
|
||||
.child(list.clone()),
|
||||
.h(list_h),
|
||||
)
|
||||
.footer(
|
||||
h_flex()
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::{rc::Rc, time::Duration};
|
|||
|
||||
use fake::Fake;
|
||||
use gpui::{
|
||||
App, AppContext, Context, Edges, ElementId, Entity, FocusHandle, Focusable, InteractiveElement,
|
||||
App, AppContext, Context, ElementId, Entity, FocusHandle, Focusable, InteractiveElement,
|
||||
IntoElement, ParentElement, Render, RenderOnce, ScrollStrategy, SharedString, Styled,
|
||||
Subscription, Task, Timer, Window, actions, div, prelude::FluentBuilder as _, px,
|
||||
};
|
||||
|
|
@ -535,13 +535,13 @@ impl Render for ListStory {
|
|||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
List::new(&self.company_list)
|
||||
.p(px(8.))
|
||||
.flex_1()
|
||||
.w_full()
|
||||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.rounded(cx.theme().radius)
|
||||
.child(List::new(&self.company_list).paddings(Edges::all(px(8.)))),
|
||||
.rounded(cx.theme().radius),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -191,6 +191,7 @@ impl Render for SelectStory {
|
|||
.child(
|
||||
section("Select").max_w_128().child(
|
||||
Select::new(&self.country_select)
|
||||
.search_placeholder("Search country by name or code")
|
||||
.cleanable()
|
||||
.disabled(self.disabled),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ impl CodeActionMenu {
|
|||
selected_ix: 0,
|
||||
};
|
||||
|
||||
let list = cx.new(|cx| ListState::new(menu, window, cx).no_query());
|
||||
let list = cx.new(|cx| ListState::new(menu, window, cx));
|
||||
|
||||
let _subscriptions =
|
||||
vec![
|
||||
|
|
@ -327,7 +327,11 @@ impl Render for CodeActionMenu {
|
|||
.top(pos.y)
|
||||
.max_w(max_width)
|
||||
.min_w(px(120.))
|
||||
.child(List::new(&self.list).max_h(MAX_MENU_HEIGHT))
|
||||
.child(
|
||||
List::new(&self.list)
|
||||
.searchable(false)
|
||||
.max_h(MAX_MENU_HEIGHT),
|
||||
)
|
||||
.child(
|
||||
canvas(
|
||||
move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds),
|
||||
|
|
|
|||
|
|
@ -195,7 +195,7 @@ impl CompletionMenu {
|
|||
selected_ix: 0,
|
||||
};
|
||||
|
||||
let list = cx.new(|cx| ListState::new(menu, window, cx).no_query());
|
||||
let list = cx.new(|cx| ListState::new(menu, window, cx));
|
||||
|
||||
let _subscriptions =
|
||||
vec![
|
||||
|
|
@ -428,7 +428,11 @@ impl Render for CompletionMenu {
|
|||
editor_popover("completion-menu", cx)
|
||||
.max_w(max_width)
|
||||
.min_w(px(120.))
|
||||
.child(List::new(&self.list).max_h(MAX_MENU_HEIGHT))
|
||||
.child(
|
||||
List::new(&self.list)
|
||||
.searchable(false)
|
||||
.max_h(MAX_MENU_HEIGHT),
|
||||
)
|
||||
.child(
|
||||
canvas(
|
||||
move |bounds, _, cx| view.update(cx, |r, _| r.bounds = bounds),
|
||||
|
|
|
|||
|
|
@ -16,8 +16,9 @@ use gpui::{
|
|||
IntoElement, KeyBinding, Length, MouseButton, ParentElement, Render, Styled, Task, Window,
|
||||
};
|
||||
use gpui::{
|
||||
px, size, App, AvailableSpace, ClickEvent, Context, Edges, EventEmitter, ListSizingBehavior,
|
||||
Pixels, RenderOnce, ScrollStrategy, SharedString, StatefulInteractiveElement, Subscription,
|
||||
px, size, App, AvailableSpace, ClickEvent, Context, DefiniteLength, EdgesRefinement,
|
||||
EventEmitter, ListSizingBehavior, RenderOnce, ScrollStrategy, SharedString,
|
||||
StatefulInteractiveElement, StyleRefinement, Subscription,
|
||||
};
|
||||
use rust_i18n::t;
|
||||
use smol::Timer;
|
||||
|
|
@ -46,8 +47,11 @@ pub enum ListEvent {
|
|||
struct ListOptions {
|
||||
size: Size,
|
||||
scrollbar_visible: bool,
|
||||
selectable: bool,
|
||||
searchable: bool,
|
||||
search_placeholder: Option<SharedString>,
|
||||
max_height: Option<Length>,
|
||||
paddings: Edges<Pixels>,
|
||||
paddings: EdgesRefinement<DefiniteLength>,
|
||||
}
|
||||
|
||||
impl Default for ListOptions {
|
||||
|
|
@ -56,7 +60,10 @@ impl Default for ListOptions {
|
|||
size: Size::default(),
|
||||
scrollbar_visible: true,
|
||||
max_height: None,
|
||||
paddings: Edges::default(),
|
||||
selectable: true,
|
||||
searchable: true,
|
||||
search_placeholder: None,
|
||||
paddings: EdgesRefinement::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -68,8 +75,6 @@ pub struct ListState<D: ListDelegate> {
|
|||
query_input: Option<Entity<InputState>>,
|
||||
delegate: D,
|
||||
last_query: Option<String>,
|
||||
selectable: bool,
|
||||
querying: bool,
|
||||
scroll_handle: VirtualListScrollHandle,
|
||||
scroll_state: ScrollbarState,
|
||||
rows_cache: RowsCache,
|
||||
|
|
@ -107,8 +112,6 @@ where
|
|||
mouse_right_clicked_index: None,
|
||||
scroll_handle: VirtualListScrollHandle::new(),
|
||||
scroll_state: ScrollbarState::default(),
|
||||
selectable: true,
|
||||
querying: false,
|
||||
reset_on_cancel: true,
|
||||
_search_task: Task::ready(()),
|
||||
_load_more_task: Task::ready(()),
|
||||
|
|
@ -116,33 +119,6 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
pub fn no_query(mut self) -> Self {
|
||||
self.query_input = None;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether the list is selectable, default is true.
|
||||
pub fn selectable(mut self, selectable: bool) -> Self {
|
||||
self.selectable = selectable;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_query_input(
|
||||
&mut self,
|
||||
query_input: Entity<InputState>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self._query_input_subscription =
|
||||
cx.subscribe_in(&query_input, window, Self::on_query_input_event);
|
||||
self.query_input = Some(query_input);
|
||||
}
|
||||
|
||||
/// Get the query input entity.
|
||||
pub fn query_input(&self) -> Option<&Entity<InputState>> {
|
||||
self.query_input.as_ref()
|
||||
}
|
||||
|
||||
pub fn delegate(&self) -> &D {
|
||||
&self.delegate
|
||||
}
|
||||
|
|
@ -242,7 +218,7 @@ where
|
|||
return;
|
||||
}
|
||||
|
||||
self.set_querying(true, window, cx);
|
||||
self.set_searching(true, window, cx);
|
||||
let search = self.delegate.perform_search(&text, window, cx);
|
||||
|
||||
if self.rows_cache.len() > 0 {
|
||||
|
|
@ -262,7 +238,7 @@ where
|
|||
// Always wait 100ms to avoid flicker
|
||||
Timer::after(Duration::from_millis(100)).await;
|
||||
_ = this.update_in(window, |this, window, cx| {
|
||||
this.set_querying(false, window, cx);
|
||||
this.set_searching(false, window, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -277,10 +253,9 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn set_querying(&mut self, querying: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.querying = querying;
|
||||
fn set_searching(&mut self, searching: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(input) = &self.query_input {
|
||||
input.update(cx, |input, cx| input.set_loading(querying, window, cx))
|
||||
input.update(cx, |input, cx| input.set_loading(searching, window, cx))
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
|
@ -424,8 +399,7 @@ where
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let selectable = self.selectable;
|
||||
|
||||
let selectable = self.options.selectable;
|
||||
let selected = self.selected_index.map(|s| s.eq_row(ix)).unwrap_or(false);
|
||||
let mouse_right_clicked = self
|
||||
.mouse_right_clicked_index
|
||||
|
|
@ -529,7 +503,7 @@ where
|
|||
.collect::<Vec<_>>()
|
||||
},
|
||||
)
|
||||
.paddings(self.options.paddings)
|
||||
.paddings(self.options.paddings.clone())
|
||||
.when(self.options.max_height.is_some(), |this| {
|
||||
this.with_sizing_behavior(ListSizingBehavior::Infer)
|
||||
})
|
||||
|
|
@ -549,6 +523,10 @@ where
|
|||
D: ListDelegate,
|
||||
{
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
if !self.options.searchable {
|
||||
return self.focus_handle.clone();
|
||||
}
|
||||
|
||||
if let Some(query_input) = &self.query_input {
|
||||
query_input.focus_handle(cx)
|
||||
} else {
|
||||
|
|
@ -572,7 +550,22 @@ where
|
|||
}
|
||||
|
||||
let loading = self.delegate().loading(cx);
|
||||
let query_input = self.query_input.clone();
|
||||
let query_input = if self.options.searchable {
|
||||
// sync placeholder
|
||||
if let Some(query_input) = &self.query_input {
|
||||
if let Some(placeholder) = &self.options.search_placeholder {
|
||||
query_input.update(cx, |input, cx| {
|
||||
input.set_placeholder(placeholder.clone(), window, cx);
|
||||
});
|
||||
}
|
||||
Some(query_input.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let loading_view = if loading {
|
||||
Some(self.delegate.render_loading(window, cx).into_any_element())
|
||||
} else {
|
||||
|
|
@ -648,6 +641,7 @@ where
|
|||
#[derive(IntoElement)]
|
||||
pub struct List<D: ListDelegate + 'static> {
|
||||
state: Entity<ListState<D>>,
|
||||
style: StyleRefinement,
|
||||
options: ListOptions,
|
||||
}
|
||||
|
||||
|
|
@ -659,25 +653,45 @@ where
|
|||
pub fn new(state: &Entity<ListState<D>>) -> Self {
|
||||
Self {
|
||||
state: state.clone(),
|
||||
style: StyleRefinement::default(),
|
||||
options: ListOptions::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set paddings for the list.
|
||||
pub fn paddings(mut self, paddings: Edges<Pixels>) -> Self {
|
||||
self.options.paddings = paddings;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn max_h(mut self, max_height: impl Into<Length>) -> Self {
|
||||
self.options.max_height = Some(max_height.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether the scrollbar is visible, default is `true`.
|
||||
pub fn scrollbar_visible(mut self, visible: bool) -> Self {
|
||||
self.options.scrollbar_visible = visible;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether the list is selectable, default is true.
|
||||
pub fn selectable(mut self, selectable: bool) -> Self {
|
||||
self.options.selectable = selectable;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets whether the list is searchable, default is `true`.
|
||||
///
|
||||
/// When `true`, there will be a search input at the top of the list.
|
||||
pub fn searchable(mut self, searchable: bool) -> Self {
|
||||
self.options.searchable = searchable;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the placeholder text for the search input.
|
||||
pub fn search_placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
|
||||
self.options.search_placeholder = Some(placeholder.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Styled for List<D>
|
||||
where
|
||||
D: ListDelegate + 'static,
|
||||
{
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
&mut self.style
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Sizable for List<D>
|
||||
|
|
@ -694,11 +708,22 @@ impl<D> RenderOnce for List<D>
|
|||
where
|
||||
D: ListDelegate + 'static,
|
||||
{
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
fn render(mut self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
// Take paddings, max_height to options, and clear them from style,
|
||||
// because they would be applied to the inner virtual list.
|
||||
self.options.paddings = self.style.padding.clone();
|
||||
self.options.max_height = self.style.max_size.height;
|
||||
self.style.padding = EdgesRefinement::default();
|
||||
self.style.max_size.height = None;
|
||||
|
||||
self.state.update(cx, |state, _| {
|
||||
state.options = self.options;
|
||||
});
|
||||
|
||||
div().id("list").size_full().child(self.state.clone())
|
||||
div()
|
||||
.id("list")
|
||||
.size_full()
|
||||
.refine_style(&self.style)
|
||||
.child(self.state.clone())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,10 +109,6 @@ pub trait SelectDelegate: Sized {
|
|||
Self::Item: SelectItem<Value = V>,
|
||||
V: PartialEq;
|
||||
|
||||
fn searchable(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn perform_search(
|
||||
&mut self,
|
||||
_query: &str,
|
||||
|
|
@ -307,6 +303,8 @@ struct SelectOptions {
|
|||
cleanable: bool,
|
||||
placeholder: Option<SharedString>,
|
||||
title_prefix: Option<SharedString>,
|
||||
searchable: bool,
|
||||
search_placeholder: Option<SharedString>,
|
||||
empty: Option<AnyElement>,
|
||||
menu_width: Length,
|
||||
disabled: bool,
|
||||
|
|
@ -326,6 +324,8 @@ impl Default for SelectOptions {
|
|||
menu_width: Length::Auto,
|
||||
disabled: false,
|
||||
appearance: true,
|
||||
searchable: true,
|
||||
search_placeholder: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -409,10 +409,6 @@ impl<I: SelectItem> SelectDelegate for SearchableVec<I> {
|
|||
None
|
||||
}
|
||||
|
||||
fn searchable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn perform_search(
|
||||
&mut self,
|
||||
query: &str,
|
||||
|
|
@ -475,10 +471,6 @@ impl<I: SelectItem> SelectDelegate for SearchableVec<SelectGroup<I>> {
|
|||
None
|
||||
}
|
||||
|
||||
fn searchable(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn perform_search(
|
||||
&mut self,
|
||||
query: &str,
|
||||
|
|
@ -566,15 +558,7 @@ where
|
|||
selected_index,
|
||||
};
|
||||
|
||||
let searchable = delegate.delegate.searchable();
|
||||
|
||||
let list = cx.new(|cx| {
|
||||
let mut list = ListState::new(delegate, window, cx).reset_on_cancel(false);
|
||||
if !searchable {
|
||||
list = list.no_query();
|
||||
}
|
||||
list
|
||||
});
|
||||
let list = cx.new(|cx| ListState::new(delegate, window, cx).reset_on_cancel(false));
|
||||
|
||||
let _subscriptions = vec![
|
||||
cx.on_blur(&list.focus_handle(cx), window, Self::on_blur),
|
||||
|
|
@ -901,6 +885,13 @@ where
|
|||
.shadow_md()
|
||||
.child(
|
||||
List::new(&self.list)
|
||||
.searchable(self.options.searchable)
|
||||
.when_some(
|
||||
self.options.search_placeholder.clone(),
|
||||
|this, placeholder| {
|
||||
this.search_placeholder(placeholder)
|
||||
},
|
||||
)
|
||||
.with_size(self.options.size)
|
||||
.max_h(rems(20.))
|
||||
.paddings(Edges::all(px(4.))),
|
||||
|
|
@ -963,6 +954,20 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
/// Sets whether the dropdown menu is searchable, default is `true`.
|
||||
///
|
||||
/// When `true`, there will be a search input at the top of the dropdown menu.
|
||||
pub fn searchable(mut self, searchable: bool) -> Self {
|
||||
self.options.searchable = searchable;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the placeholder text for the search input.
|
||||
pub fn search_placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
|
||||
self.options.search_placeholder = Some(placeholder.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the disable state for the select.
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.options.disabled = disabled;
|
||||
|
|
|
|||
|
|
@ -191,7 +191,8 @@ impl ListDelegate for MyListDelegate {
|
|||
}
|
||||
|
||||
// Create list without search input
|
||||
let state = cx.new(|cx| ListState::new(delegate, window, cx).no_query());
|
||||
let state = cx.new(|cx| ListState::new(delegate, window, cx));
|
||||
List::new(&state).searchable(false)
|
||||
```
|
||||
|
||||
### List with Loading State
|
||||
|
|
|
|||
Loading…
Reference in a new issue