Improved Input prefix, suffix with a builder to support Element. (#54)
- Add loading to List query input and changed perform_search now return a Task.
This commit is contained in:
parent
4687a18ee4
commit
5034152f21
12 changed files with 278 additions and 96 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -5699,6 +5699,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
"smol",
|
||||
"taffy",
|
||||
"unicode-segmentation",
|
||||
"usvg",
|
||||
|
|
|
|||
1
assets/icons/inbox.svg
Normal file
1
assets/icons/inbox.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-inbox"><polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/></svg>
|
||||
|
After Width: | Height: | Size: 388 B |
|
|
@ -1,9 +1,9 @@
|
|||
use gpui::*;
|
||||
use prelude::FluentBuilder as _;
|
||||
use story::{
|
||||
ButtonStory, CheckboxStory, DropdownStory, ImageStory, InputStory, ListStory, PickerStory,
|
||||
PopoverStory, ProgressStory, ResizableStory, ScrollableStory, StoryContainer, SwitchStory,
|
||||
TableStory, TooltipStory, WebViewStory,
|
||||
ButtonStory, CheckboxStory, DropdownStory, IconStory, ImageStory, InputStory, ListStory,
|
||||
PickerStory, PopoverStory, ProgressStory, ResizableStory, ScrollableStory, StoryContainer,
|
||||
SwitchStory, TableStory, TooltipStory, WebViewStory,
|
||||
};
|
||||
use workspace::{dock::DockPosition, TitleBar, Workspace};
|
||||
|
||||
|
|
@ -121,6 +121,15 @@ impl StoryWorkspace {
|
|||
cx,
|
||||
);
|
||||
|
||||
StoryContainer::add_pane(
|
||||
"Icon",
|
||||
"Icon use examples",
|
||||
IconStory::view(cx).into(),
|
||||
workspace.clone(),
|
||||
cx,
|
||||
)
|
||||
.detach();
|
||||
|
||||
StoryContainer::add_pane(
|
||||
"Image",
|
||||
"Render SVG image and Chart",
|
||||
|
|
|
|||
38
crates/story/src/icon_story.rs
Normal file
38
crates/story/src/icon_story.rs
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
use gpui::{px, rems, ParentElement, Render, Styled, View, VisualContext as _, WindowContext};
|
||||
use ui::{h_flex, theme::ActiveTheme as _, v_flex, Icon, IconName};
|
||||
|
||||
pub struct IconStory {}
|
||||
|
||||
impl IconStory {
|
||||
pub fn new(_: &WindowContext) -> Self {
|
||||
Self {}
|
||||
}
|
||||
|
||||
pub fn view(cx: &mut WindowContext) -> View<Self> {
|
||||
cx.new_view(|cx| Self::new(cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for IconStory {
|
||||
fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> impl gpui::IntoElement {
|
||||
v_flex().gap_3().child(
|
||||
h_flex()
|
||||
.gap_4()
|
||||
.child(IconName::Info)
|
||||
.child(
|
||||
Icon::new(IconName::Maximize)
|
||||
.size_6()
|
||||
.text_color(ui::green_500()),
|
||||
)
|
||||
.child(Icon::new(IconName::Maximize).size(px(55.)))
|
||||
.child(
|
||||
Icon::new(IconName::Plus)
|
||||
.w(rems(3.))
|
||||
.h(rems(3.))
|
||||
.bg(cx.theme().primary)
|
||||
.text_color(cx.theme().primary_foreground)
|
||||
.rounded(px(32.)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -68,20 +68,20 @@ impl InputStory {
|
|||
|
||||
let prefix_input1 = cx.new_view(|cx| {
|
||||
TextInput::new(cx)
|
||||
.prefix(IconName::Search.view(cx))
|
||||
.prefix(|_| IconName::Search)
|
||||
.placeholder("Search some thing...")
|
||||
.cleanable(true)
|
||||
});
|
||||
let suffix_input1 = cx.new_view(|cx| {
|
||||
TextInput::new(cx)
|
||||
.suffix(IconName::Info.view(cx))
|
||||
.suffix(|_| IconName::Info)
|
||||
.placeholder("Info here...")
|
||||
.cleanable(true)
|
||||
});
|
||||
let both_input1 = cx.new_view(|cx| {
|
||||
TextInput::new(cx)
|
||||
.prefix(IconName::Search.view(cx))
|
||||
.suffix(IconName::Info.view(cx))
|
||||
.prefix(|_| IconName::Search)
|
||||
.suffix(|_| IconName::Info)
|
||||
.cleanable(true)
|
||||
.placeholder("This input have prefix and suffix.")
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
mod button_story;
|
||||
mod checkbox_story;
|
||||
mod dropdown_story;
|
||||
mod icon_story;
|
||||
mod image_story;
|
||||
mod input_story;
|
||||
mod list_story;
|
||||
|
|
@ -17,6 +18,7 @@ mod webview_story;
|
|||
pub use button_story::ButtonStory;
|
||||
pub use checkbox_story::CheckboxStory;
|
||||
pub use dropdown_story::DropdownStory;
|
||||
pub use icon_story::IconStory;
|
||||
pub use image_story::ImageStory;
|
||||
pub use input_story::InputStory;
|
||||
pub use list_story::ListStory;
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
use std::sync::Arc;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
|
||||
use fake::Fake;
|
||||
use gpui::{
|
||||
deferred, div, prelude::FluentBuilder as _, px, FocusHandle, FocusableView,
|
||||
InteractiveElement as _, IntoElement, ParentElement, Render, Styled, View, ViewContext,
|
||||
VisualContext as _, WeakView, WindowContext,
|
||||
InteractiveElement as _, IntoElement, ParentElement, Render, Styled, Task, Timer, View,
|
||||
ViewContext, VisualContext as _, WeakView, WindowContext,
|
||||
};
|
||||
|
||||
use ui::{
|
||||
button::Button,
|
||||
h_flex,
|
||||
list::{List, ListDelegate, ListItem},
|
||||
v_flex, Clickable as _, IconName, StyledExt,
|
||||
theme::ActiveTheme as _,
|
||||
v_flex, Clickable as _, Icon, IconName, StyledExt,
|
||||
};
|
||||
|
||||
pub struct ListItemDeletegate {
|
||||
|
|
@ -31,14 +33,25 @@ impl ListDelegate for ListItemDeletegate {
|
|||
Some(self.selected_index)
|
||||
}
|
||||
|
||||
fn perform_search(&mut self, query: &str, cx: &mut ViewContext<List<Self>>) {
|
||||
self.matches = self
|
||||
.items
|
||||
.iter()
|
||||
.filter(|item| item.to_lowercase().contains(&query.to_lowercase()))
|
||||
.cloned()
|
||||
.collect();
|
||||
cx.notify();
|
||||
fn perform_search(&mut self, query: &str, cx: &mut ViewContext<List<Self>>) -> Task<()> {
|
||||
let query = query.to_string();
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
// Simulate a slow search.
|
||||
let sleep = (0.15..0.3).fake();
|
||||
Timer::after(Duration::from_secs_f64(sleep)).await;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate_mut().matches = this
|
||||
.delegate()
|
||||
.items
|
||||
.iter()
|
||||
.filter(|item| item.to_lowercase().contains(&query.to_lowercase()))
|
||||
.cloned()
|
||||
.collect();
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
fn render_item(&self, ix: usize, _cx: &mut ViewContext<List<Self>>) -> Option<Self::Item> {
|
||||
|
|
@ -56,6 +69,22 @@ impl ListDelegate for ListItemDeletegate {
|
|||
}
|
||||
}
|
||||
|
||||
fn render_empty(&self, cx: &mut ViewContext<List<Self>>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.child(
|
||||
Icon::new(IconName::Inbox)
|
||||
.size(px(50.))
|
||||
.text_color(cx.theme().muted_foreground),
|
||||
)
|
||||
.child("No matches found")
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.p_3()
|
||||
.bg(cx.theme().muted)
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
}
|
||||
|
||||
fn cancel(&mut self, cx: &mut ViewContext<List<Self>>) {
|
||||
if let Some(story) = self.story.upgrade() {
|
||||
cx.update_view(&story, |story, cx| {
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ once_cell = "1.19.0"
|
|||
raw-window-handle = "0.6.2"
|
||||
winit = "0.30.3"
|
||||
wry = "0"
|
||||
smol = "1"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ pub enum IconName {
|
|||
ChevronRight,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Inbox,
|
||||
}
|
||||
|
||||
impl IconName {
|
||||
|
|
@ -57,6 +58,7 @@ impl IconName {
|
|||
IconName::ChevronRight => "icons/chevron-right.svg",
|
||||
IconName::Eye => "icons/eye.svg",
|
||||
IconName::EyeOff => "icons/eye-off.svg",
|
||||
IconName::Inbox => "icons/inbox.svg",
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
|
@ -90,7 +92,7 @@ pub struct Icon {
|
|||
base: Svg,
|
||||
path: SharedString,
|
||||
text_color: Option<Hsla>,
|
||||
size: Size,
|
||||
size: Option<Size>,
|
||||
}
|
||||
|
||||
impl Default for Icon {
|
||||
|
|
@ -99,14 +101,18 @@ impl Default for Icon {
|
|||
base: svg().flex_none().size_4(),
|
||||
path: "".into(),
|
||||
text_color: None,
|
||||
size: Size::Medium,
|
||||
size: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Icon {
|
||||
fn clone(&self) -> Self {
|
||||
Self::default().path(self.path.clone()).size(self.size)
|
||||
let mut this = Self::default().path(self.path.clone());
|
||||
if let Some(size) = self.size {
|
||||
this = this.size(size);
|
||||
}
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -128,7 +134,7 @@ impl Icon {
|
|||
/// Also can receive a `ButtonSize` to convert to `IconSize`,
|
||||
/// Or a `Pixels` to set a custom size: `px(30.)`
|
||||
pub fn size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.size = size.into();
|
||||
self.size = Some(size.into());
|
||||
|
||||
self
|
||||
}
|
||||
|
|
@ -161,7 +167,7 @@ impl RenderOnce for Icon {
|
|||
|
||||
self.base
|
||||
.text_color(text_color)
|
||||
.map(|this| match self.size {
|
||||
.when_some(self.size, |this, size| match size {
|
||||
Size::Size(px) => this.size(px),
|
||||
Size::XSmall => this.size_3(),
|
||||
Size::Small => this.size_3p5(),
|
||||
|
|
@ -184,9 +190,8 @@ impl Render for Icon {
|
|||
|
||||
svg()
|
||||
.flex_none()
|
||||
.size_4()
|
||||
.text_color(text_color)
|
||||
.map(|this| match self.size {
|
||||
.when_some(self.size, |this, size| match size {
|
||||
Size::Size(px) => this.size(px),
|
||||
Size::XSmall => this.size_3(),
|
||||
Size::Small => this.size_3p5(),
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ impl RenderOnce for Indicator {
|
|||
let color = self.color.unwrap_or_else(|| cx.theme().indicator);
|
||||
div()
|
||||
.child(
|
||||
Icon::new(self.icon)
|
||||
Icon::new(self.icon.clone())
|
||||
.size(self.size)
|
||||
.text_color(color)
|
||||
.with_animation(
|
||||
|
|
|
|||
|
|
@ -6,16 +6,17 @@
|
|||
use std::ops::Range;
|
||||
|
||||
use super::blink_cursor::BlinkCursor;
|
||||
use super::history::{Change, History};
|
||||
use super::history::History;
|
||||
use crate::button::{Button, ButtonStyle};
|
||||
use crate::indicator::Indicator;
|
||||
use crate::styled_ext::Sizeful;
|
||||
use crate::theme::ActiveTheme;
|
||||
use crate::{event::InterativeElementExt as _, Size};
|
||||
use crate::{Clickable, IconName, StyledExt as _};
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
use gpui::{
|
||||
actions, div, fill, point, px, relative, rems, size, AnyView, AppContext, Bounds, ClickEvent,
|
||||
ClipboardItem, Context as _, Element, ElementId, ElementInputHandler, EventEmitter,
|
||||
actions, div, fill, point, px, relative, rems, size, AnyElement, AppContext, Bounds,
|
||||
ClickEvent, ClipboardItem, Context as _, Element, ElementId, ElementInputHandler, EventEmitter,
|
||||
FocusHandle, FocusableView, GlobalElementId, InteractiveElement as _, IntoElement, KeyBinding,
|
||||
KeyDownEvent, LayoutId, Model, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
||||
PaintQuad, ParentElement as _, Pixels, Point, Render, ShapedLine, SharedString, Style,
|
||||
|
|
@ -110,8 +111,9 @@ pub struct TextInput {
|
|||
text: SharedString,
|
||||
history: History,
|
||||
blink_cursor: Model<BlinkCursor>,
|
||||
prefix: Option<AnyView>,
|
||||
suffix: Option<AnyView>,
|
||||
prefix: Option<Box<dyn Fn(&WindowContext) -> AnyElement + 'static>>,
|
||||
suffix: Option<Box<dyn Fn(&WindowContext) -> AnyElement + 'static>>,
|
||||
loading: bool,
|
||||
placeholder: SharedString,
|
||||
selected_range: Range<usize>,
|
||||
selection_reversed: bool,
|
||||
|
|
@ -151,6 +153,7 @@ impl TextInput {
|
|||
masked: false,
|
||||
appearance: true,
|
||||
cleanable: false,
|
||||
loading: false,
|
||||
prefix: None,
|
||||
suffix: None,
|
||||
size: Size::Medium,
|
||||
|
|
@ -201,6 +204,26 @@ impl TextInput {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
/// Set the prefix element of the input field.
|
||||
pub fn set_prefix<F, E>(&mut self, builder: F, cx: &mut ViewContext<Self>)
|
||||
where
|
||||
F: Fn(&WindowContext) -> E + 'static,
|
||||
E: IntoElement,
|
||||
{
|
||||
self.prefix = Some(Box::new(move |cx| builder(cx).into_any_element()));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Set the suffix element of the input field.
|
||||
pub fn set_suffix<F, E>(&mut self, builder: F, cx: &mut ViewContext<Self>)
|
||||
where
|
||||
F: Fn(&WindowContext) -> E + 'static,
|
||||
E: IntoElement,
|
||||
{
|
||||
self.suffix = Some(Box::new(move |cx| builder(cx).into_any_element()));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Set the appearance of the input field.
|
||||
pub fn appearance(mut self, appearance: bool) -> Self {
|
||||
self.appearance = appearance;
|
||||
|
|
@ -208,8 +231,22 @@ impl TextInput {
|
|||
}
|
||||
|
||||
/// Set the prefix element of the input field, for example a search Icon.
|
||||
pub fn prefix(mut self, prefix: impl Into<AnyView>) -> Self {
|
||||
self.prefix = Some(prefix.into());
|
||||
pub fn prefix<F, E>(mut self, builder: F) -> Self
|
||||
where
|
||||
F: Fn(&WindowContext) -> E + 'static,
|
||||
E: IntoElement,
|
||||
{
|
||||
self.prefix = Some(Box::new(move |cx| builder(cx).into_any_element()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the suffix element of the input field, for example a clear button.
|
||||
pub fn suffix<F, E>(mut self, builder: F) -> Self
|
||||
where
|
||||
F: Fn(&WindowContext) -> E + 'static,
|
||||
E: IntoElement,
|
||||
{
|
||||
self.suffix = Some(Box::new(move |cx| builder(cx).into_any_element()));
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -219,12 +256,6 @@ impl TextInput {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set the suffix element of the input field, for example a clear button.
|
||||
pub fn suffix(mut self, suffix: impl Into<AnyView>) -> Self {
|
||||
self.suffix = Some(suffix.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the size of the input field.
|
||||
pub fn size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.size = size.into();
|
||||
|
|
@ -237,6 +268,12 @@ impl TextInput {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set true to show indicator at the input right.
|
||||
pub fn set_loading(&mut self, loading: bool, cx: &mut ViewContext<Self>) {
|
||||
self.loading = loading;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// Return the text of the input field.
|
||||
pub fn text(&self) -> SharedString {
|
||||
self.text.clone()
|
||||
|
|
@ -908,6 +945,9 @@ impl Render for TextInput {
|
|||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let focused = self.focus_handle.is_focused(cx);
|
||||
|
||||
let prefix = self.prefix.as_ref().map(|build| build(cx));
|
||||
let suffix = self.suffix.as_ref().map(|build| build(cx));
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.key_context(CONTEXT)
|
||||
|
|
@ -961,7 +1001,7 @@ impl Render for TextInput {
|
|||
cx.theme().background
|
||||
})
|
||||
})
|
||||
.when_some(self.prefix.clone(), |this, prefix| this.child(prefix))
|
||||
.children(prefix)
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.child(
|
||||
|
|
@ -974,16 +1014,20 @@ impl Render for TextInput {
|
|||
input: cx.view().clone(),
|
||||
}),
|
||||
)
|
||||
.when(self.cleanable && !self.text.is_empty(), |this| {
|
||||
this.child(
|
||||
Button::new("clean-text", cx)
|
||||
.icon(IconName::Close)
|
||||
.style(ButtonStyle::Ghost)
|
||||
.size(px(15.))
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(Self::clean)),
|
||||
)
|
||||
})
|
||||
.when_some(self.suffix.clone(), |this, suffix| this.child(suffix))
|
||||
.when(self.loading, |this| this.child(Indicator::new()))
|
||||
.when(
|
||||
self.cleanable && !self.loading && !self.text.is_empty(),
|
||||
|this| {
|
||||
this.child(
|
||||
Button::new("clean-text", cx)
|
||||
.icon(IconName::Close)
|
||||
.style(ButtonStyle::Ghost)
|
||||
.size(px(15.))
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(Self::clean)),
|
||||
)
|
||||
},
|
||||
)
|
||||
.children(suffix)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
use std::time::Duration;
|
||||
use std::{cell::Cell, rc::Rc};
|
||||
|
||||
use gpui::prelude::FluentBuilder as _;
|
||||
|
||||
use crate::input::{InputEvent, TextInput};
|
||||
use crate::scroll::ScrollbarState;
|
||||
use crate::theme::{ActiveTheme, Colorize as _};
|
||||
use crate::IconName;
|
||||
use crate::{scroll::Scrollbar, v_flex};
|
||||
use crate::{Icon, IconName};
|
||||
use gpui::{
|
||||
actions, div, px, uniform_list, AppContext, FocusHandle, FocusableView,
|
||||
InteractiveElement as _, IntoElement, KeyBinding, Length, ListSizingBehavior, MouseButton,
|
||||
ParentElement as _, Render, Styled as _, UniformListScrollHandle, View, ViewContext,
|
||||
VisualContext as _,
|
||||
actions, div, prelude::FluentBuilder as _, px, uniform_list, AppContext, FocusHandle,
|
||||
FocusableView, InteractiveElement as _, IntoElement, KeyBinding, Length, ListSizingBehavior,
|
||||
MouseButton, ParentElement as _, Render, Styled as _, Task, UniformListScrollHandle, View,
|
||||
ViewContext, VisualContext as _,
|
||||
};
|
||||
use smol::Timer;
|
||||
|
||||
actions!(list, [Cancel, Confirm, SelectPrev, SelectNext]);
|
||||
|
||||
|
|
@ -33,7 +33,9 @@ pub trait ListDelegate: Sized + 'static {
|
|||
|
||||
/// When Query Input change, this method will be called.
|
||||
/// You can perform search here.
|
||||
fn perform_search(&mut self, query: &str, cx: &mut ViewContext<List<Self>>) {}
|
||||
fn perform_search(&mut self, query: &str, cx: &mut ViewContext<List<Self>>) -> Task<()> {
|
||||
Task::Ready(Some(()))
|
||||
}
|
||||
|
||||
/// Return the number of items in the list.
|
||||
fn items_count(&self) -> usize;
|
||||
|
|
@ -43,6 +45,11 @@ pub trait ListDelegate: Sized + 'static {
|
|||
/// Return None will skip the item.
|
||||
fn render_item(&self, ix: usize, cx: &mut ViewContext<List<Self>>) -> Option<Self::Item>;
|
||||
|
||||
/// Return a Element to show when list is empty.
|
||||
fn render_empty(&self, cx: &mut ViewContext<List<Self>>) -> impl IntoElement {
|
||||
div()
|
||||
}
|
||||
|
||||
/// Return the confirmed index of the selected item.
|
||||
fn confirmed_index(&self) -> Option<usize> {
|
||||
None
|
||||
|
|
@ -63,6 +70,8 @@ pub struct List<D: ListDelegate> {
|
|||
delegate: D,
|
||||
max_height: Option<Length>,
|
||||
query_input: Option<View<TextInput>>,
|
||||
last_query: Option<String>,
|
||||
loading: bool,
|
||||
|
||||
enable_scrollbar: bool,
|
||||
vertical_scroll_handle: UniformListScrollHandle,
|
||||
|
|
@ -79,7 +88,7 @@ where
|
|||
let query_input = cx.new_view(|cx| {
|
||||
TextInput::new(cx)
|
||||
.appearance(false)
|
||||
.prefix(Icon::new(IconName::Search).view(cx))
|
||||
.prefix(|_| IconName::Search)
|
||||
.placeholder("Search...")
|
||||
.cleanable(true)
|
||||
});
|
||||
|
|
@ -91,11 +100,13 @@ where
|
|||
focus_handle: cx.focus_handle(),
|
||||
delegate,
|
||||
query_input: Some(query_input),
|
||||
last_query: None,
|
||||
selected_index: None,
|
||||
vertical_scroll_handle: UniformListScrollHandle::new(),
|
||||
scrollbar_state: Rc::new(Cell::new(ScrollbarState::new())),
|
||||
max_height: None,
|
||||
enable_scrollbar: true,
|
||||
loading: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -162,25 +173,57 @@ where
|
|||
) {
|
||||
match event {
|
||||
InputEvent::Change(text) => {
|
||||
self.delegate.perform_search(&text.trim(), cx);
|
||||
cx.notify()
|
||||
let text = text.trim().to_string();
|
||||
if Some(&text) == self.last_query.as_ref() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.set_loading(true, cx);
|
||||
let search = self.delegate.perform_search(&text, cx);
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
search.await;
|
||||
// Always wait 100ms to avoid flicker
|
||||
Timer::after(Duration::from_millis(100)).await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.last_query = Some(text);
|
||||
this.set_loading(false, cx);
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
InputEvent::PressEnter => self.action_confirm(&Confirm, cx),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_loading(&mut self, loading: bool, cx: &mut ViewContext<Self>) {
|
||||
self.loading = loading;
|
||||
if let Some(input) = &self.query_input {
|
||||
input.update(cx, |input, cx| input.set_loading(loading, cx))
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn action_cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
self.delegate.cancel(cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn action_confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
|
||||
if self.delegate.items_count() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.delegate.confirm(self.selected_index, cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn action_select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
|
||||
if self.delegate.items_count() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let selected_index = self.selected_index.unwrap_or(0);
|
||||
if selected_index > 0 {
|
||||
self.selected_index = Some(selected_index - 1);
|
||||
|
|
@ -193,6 +236,10 @@ where
|
|||
}
|
||||
|
||||
fn action_select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
|
||||
if self.delegate.items_count() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let selected_index = self.selected_index.unwrap_or(0);
|
||||
if selected_index < self.delegate.items_count() - 1 {
|
||||
self.selected_index = Some(selected_index + 1);
|
||||
|
|
@ -261,39 +308,44 @@ where
|
|||
.min_h(px(100.))
|
||||
.when_some(self.max_height, |this, h| this.max_h(h))
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
uniform_list(view, "uniform-list", items_count, {
|
||||
move |list, visible_range, cx| {
|
||||
visible_range
|
||||
.map(|ix| {
|
||||
div()
|
||||
.id("list-item")
|
||||
.w_full()
|
||||
.children(list.delegate.render_item(ix, cx))
|
||||
.when_some(
|
||||
list.selected_index,
|
||||
|this, selected_index| {
|
||||
this.when(ix == selected_index, |this| {
|
||||
this.bg(selected_bg)
|
||||
})
|
||||
},
|
||||
)
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(move |this, _, cx| {
|
||||
this.selected_index = Some(ix);
|
||||
this.action_confirm(&Confirm, cx);
|
||||
}),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
})
|
||||
.flex_grow()
|
||||
.with_sizing_behavior(sizing_behavior)
|
||||
.track_scroll(vertical_scroll_handle)
|
||||
.into_any_element(),
|
||||
)
|
||||
.when(items_count == 0, |this| {
|
||||
this.child(self.delegate().render_empty(cx))
|
||||
})
|
||||
.when(items_count > 0, |this| {
|
||||
this.child(
|
||||
uniform_list(view, "uniform-list", items_count, {
|
||||
move |list, visible_range, cx| {
|
||||
visible_range
|
||||
.map(|ix| {
|
||||
div()
|
||||
.id("list-item")
|
||||
.w_full()
|
||||
.children(list.delegate.render_item(ix, cx))
|
||||
.when_some(
|
||||
list.selected_index,
|
||||
|this, selected_index| {
|
||||
this.when(ix == selected_index, |this| {
|
||||
this.bg(selected_bg)
|
||||
})
|
||||
},
|
||||
)
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(move |this, _, cx| {
|
||||
this.selected_index = Some(ix);
|
||||
this.action_confirm(&Confirm, cx);
|
||||
}),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
})
|
||||
.flex_grow()
|
||||
.with_sizing_behavior(sizing_behavior)
|
||||
.track_scroll(vertical_scroll_handle)
|
||||
.into_any_element(),
|
||||
)
|
||||
})
|
||||
.children(self.render_scrollbar(cx)),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue