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:
Jason Lee 2024-07-22 15:30:07 +08:00 committed by GitHub
parent 4687a18ee4
commit 5034152f21
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 278 additions and 96 deletions

1
Cargo.lock generated
View file

@ -5699,6 +5699,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"smallvec", "smallvec",
"smol",
"taffy", "taffy",
"unicode-segmentation", "unicode-segmentation",
"usvg", "usvg",

1
assets/icons/inbox.svg Normal file
View 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

View file

@ -1,9 +1,9 @@
use gpui::*; use gpui::*;
use prelude::FluentBuilder as _; use prelude::FluentBuilder as _;
use story::{ use story::{
ButtonStory, CheckboxStory, DropdownStory, ImageStory, InputStory, ListStory, PickerStory, ButtonStory, CheckboxStory, DropdownStory, IconStory, ImageStory, InputStory, ListStory,
PopoverStory, ProgressStory, ResizableStory, ScrollableStory, StoryContainer, SwitchStory, PickerStory, PopoverStory, ProgressStory, ResizableStory, ScrollableStory, StoryContainer,
TableStory, TooltipStory, WebViewStory, SwitchStory, TableStory, TooltipStory, WebViewStory,
}; };
use workspace::{dock::DockPosition, TitleBar, Workspace}; use workspace::{dock::DockPosition, TitleBar, Workspace};
@ -121,6 +121,15 @@ impl StoryWorkspace {
cx, cx,
); );
StoryContainer::add_pane(
"Icon",
"Icon use examples",
IconStory::view(cx).into(),
workspace.clone(),
cx,
)
.detach();
StoryContainer::add_pane( StoryContainer::add_pane(
"Image", "Image",
"Render SVG image and Chart", "Render SVG image and Chart",

View 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.)),
),
)
}
}

View file

@ -68,20 +68,20 @@ impl InputStory {
let prefix_input1 = cx.new_view(|cx| { let prefix_input1 = cx.new_view(|cx| {
TextInput::new(cx) TextInput::new(cx)
.prefix(IconName::Search.view(cx)) .prefix(|_| IconName::Search)
.placeholder("Search some thing...") .placeholder("Search some thing...")
.cleanable(true) .cleanable(true)
}); });
let suffix_input1 = cx.new_view(|cx| { let suffix_input1 = cx.new_view(|cx| {
TextInput::new(cx) TextInput::new(cx)
.suffix(IconName::Info.view(cx)) .suffix(|_| IconName::Info)
.placeholder("Info here...") .placeholder("Info here...")
.cleanable(true) .cleanable(true)
}); });
let both_input1 = cx.new_view(|cx| { let both_input1 = cx.new_view(|cx| {
TextInput::new(cx) TextInput::new(cx)
.prefix(IconName::Search.view(cx)) .prefix(|_| IconName::Search)
.suffix(IconName::Info.view(cx)) .suffix(|_| IconName::Info)
.cleanable(true) .cleanable(true)
.placeholder("This input have prefix and suffix.") .placeholder("This input have prefix and suffix.")
}); });

View file

@ -1,6 +1,7 @@
mod button_story; mod button_story;
mod checkbox_story; mod checkbox_story;
mod dropdown_story; mod dropdown_story;
mod icon_story;
mod image_story; mod image_story;
mod input_story; mod input_story;
mod list_story; mod list_story;
@ -17,6 +18,7 @@ mod webview_story;
pub use button_story::ButtonStory; pub use button_story::ButtonStory;
pub use checkbox_story::CheckboxStory; pub use checkbox_story::CheckboxStory;
pub use dropdown_story::DropdownStory; pub use dropdown_story::DropdownStory;
pub use icon_story::IconStory;
pub use image_story::ImageStory; pub use image_story::ImageStory;
pub use input_story::InputStory; pub use input_story::InputStory;
pub use list_story::ListStory; pub use list_story::ListStory;

View file

@ -1,16 +1,18 @@
use std::sync::Arc; use std::{sync::Arc, time::Duration};
use fake::Fake;
use gpui::{ use gpui::{
deferred, div, prelude::FluentBuilder as _, px, FocusHandle, FocusableView, deferred, div, prelude::FluentBuilder as _, px, FocusHandle, FocusableView,
InteractiveElement as _, IntoElement, ParentElement, Render, Styled, View, ViewContext, InteractiveElement as _, IntoElement, ParentElement, Render, Styled, Task, Timer, View,
VisualContext as _, WeakView, WindowContext, ViewContext, VisualContext as _, WeakView, WindowContext,
}; };
use ui::{ use ui::{
button::Button, button::Button,
h_flex, h_flex,
list::{List, ListDelegate, ListItem}, list::{List, ListDelegate, ListItem},
v_flex, Clickable as _, IconName, StyledExt, theme::ActiveTheme as _,
v_flex, Clickable as _, Icon, IconName, StyledExt,
}; };
pub struct ListItemDeletegate { pub struct ListItemDeletegate {
@ -31,14 +33,25 @@ impl ListDelegate for ListItemDeletegate {
Some(self.selected_index) Some(self.selected_index)
} }
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<()> {
self.matches = self let query = query.to_string();
.items cx.spawn(move |this, mut cx| async move {
.iter() // Simulate a slow search.
.filter(|item| item.to_lowercase().contains(&query.to_lowercase())) let sleep = (0.15..0.3).fake();
.cloned() Timer::after(Duration::from_secs_f64(sleep)).await;
.collect();
cx.notify(); 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> { 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>>) { fn cancel(&mut self, cx: &mut ViewContext<List<Self>>) {
if let Some(story) = self.story.upgrade() { if let Some(story) = self.story.upgrade() {
cx.update_view(&story, |story, cx| { cx.update_view(&story, |story, cx| {

View file

@ -27,6 +27,7 @@ once_cell = "1.19.0"
raw-window-handle = "0.6.2" raw-window-handle = "0.6.2"
winit = "0.30.3" winit = "0.30.3"
wry = "0" wry = "0"
smol = "1"
[lints] [lints]
workspace = true workspace = true

View file

@ -29,6 +29,7 @@ pub enum IconName {
ChevronRight, ChevronRight,
Eye, Eye,
EyeOff, EyeOff,
Inbox,
} }
impl IconName { impl IconName {
@ -57,6 +58,7 @@ impl IconName {
IconName::ChevronRight => "icons/chevron-right.svg", IconName::ChevronRight => "icons/chevron-right.svg",
IconName::Eye => "icons/eye.svg", IconName::Eye => "icons/eye.svg",
IconName::EyeOff => "icons/eye-off.svg", IconName::EyeOff => "icons/eye-off.svg",
IconName::Inbox => "icons/inbox.svg",
} }
.into() .into()
} }
@ -90,7 +92,7 @@ pub struct Icon {
base: Svg, base: Svg,
path: SharedString, path: SharedString,
text_color: Option<Hsla>, text_color: Option<Hsla>,
size: Size, size: Option<Size>,
} }
impl Default for Icon { impl Default for Icon {
@ -99,14 +101,18 @@ impl Default for Icon {
base: svg().flex_none().size_4(), base: svg().flex_none().size_4(),
path: "".into(), path: "".into(),
text_color: None, text_color: None,
size: Size::Medium, size: None,
} }
} }
} }
impl Clone for Icon { impl Clone for Icon {
fn clone(&self) -> Self { 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`, /// Also can receive a `ButtonSize` to convert to `IconSize`,
/// Or a `Pixels` to set a custom size: `px(30.)` /// Or a `Pixels` to set a custom size: `px(30.)`
pub fn size(mut self, size: impl Into<Size>) -> Self { pub fn size(mut self, size: impl Into<Size>) -> Self {
self.size = size.into(); self.size = Some(size.into());
self self
} }
@ -161,7 +167,7 @@ impl RenderOnce for Icon {
self.base self.base
.text_color(text_color) .text_color(text_color)
.map(|this| match self.size { .when_some(self.size, |this, size| match size {
Size::Size(px) => this.size(px), Size::Size(px) => this.size(px),
Size::XSmall => this.size_3(), Size::XSmall => this.size_3(),
Size::Small => this.size_3p5(), Size::Small => this.size_3p5(),
@ -184,9 +190,8 @@ impl Render for Icon {
svg() svg()
.flex_none() .flex_none()
.size_4()
.text_color(text_color) .text_color(text_color)
.map(|this| match self.size { .when_some(self.size, |this, size| match size {
Size::Size(px) => this.size(px), Size::Size(px) => this.size(px),
Size::XSmall => this.size_3(), Size::XSmall => this.size_3(),
Size::Small => this.size_3p5(), Size::Small => this.size_3p5(),

View file

@ -45,7 +45,7 @@ impl RenderOnce for Indicator {
let color = self.color.unwrap_or_else(|| cx.theme().indicator); let color = self.color.unwrap_or_else(|| cx.theme().indicator);
div() div()
.child( .child(
Icon::new(self.icon) Icon::new(self.icon.clone())
.size(self.size) .size(self.size)
.text_color(color) .text_color(color)
.with_animation( .with_animation(

View file

@ -6,16 +6,17 @@
use std::ops::Range; use std::ops::Range;
use super::blink_cursor::BlinkCursor; use super::blink_cursor::BlinkCursor;
use super::history::{Change, History}; use super::history::History;
use crate::button::{Button, ButtonStyle}; use crate::button::{Button, ButtonStyle};
use crate::indicator::Indicator;
use crate::styled_ext::Sizeful; use crate::styled_ext::Sizeful;
use crate::theme::ActiveTheme; use crate::theme::ActiveTheme;
use crate::{event::InterativeElementExt as _, Size}; use crate::{event::InterativeElementExt as _, Size};
use crate::{Clickable, IconName, StyledExt as _}; use crate::{Clickable, IconName, StyledExt as _};
use gpui::prelude::FluentBuilder as _; use gpui::prelude::FluentBuilder as _;
use gpui::{ use gpui::{
actions, div, fill, point, px, relative, rems, size, AnyView, AppContext, Bounds, ClickEvent, actions, div, fill, point, px, relative, rems, size, AnyElement, AppContext, Bounds,
ClipboardItem, Context as _, Element, ElementId, ElementInputHandler, EventEmitter, ClickEvent, ClipboardItem, Context as _, Element, ElementId, ElementInputHandler, EventEmitter,
FocusHandle, FocusableView, GlobalElementId, InteractiveElement as _, IntoElement, KeyBinding, FocusHandle, FocusableView, GlobalElementId, InteractiveElement as _, IntoElement, KeyBinding,
KeyDownEvent, LayoutId, Model, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, KeyDownEvent, LayoutId, Model, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
PaintQuad, ParentElement as _, Pixels, Point, Render, ShapedLine, SharedString, Style, PaintQuad, ParentElement as _, Pixels, Point, Render, ShapedLine, SharedString, Style,
@ -110,8 +111,9 @@ pub struct TextInput {
text: SharedString, text: SharedString,
history: History, history: History,
blink_cursor: Model<BlinkCursor>, blink_cursor: Model<BlinkCursor>,
prefix: Option<AnyView>, prefix: Option<Box<dyn Fn(&WindowContext) -> AnyElement + 'static>>,
suffix: Option<AnyView>, suffix: Option<Box<dyn Fn(&WindowContext) -> AnyElement + 'static>>,
loading: bool,
placeholder: SharedString, placeholder: SharedString,
selected_range: Range<usize>, selected_range: Range<usize>,
selection_reversed: bool, selection_reversed: bool,
@ -151,6 +153,7 @@ impl TextInput {
masked: false, masked: false,
appearance: true, appearance: true,
cleanable: false, cleanable: false,
loading: false,
prefix: None, prefix: None,
suffix: None, suffix: None,
size: Size::Medium, size: Size::Medium,
@ -201,6 +204,26 @@ impl TextInput {
cx.notify(); 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. /// Set the appearance of the input field.
pub fn appearance(mut self, appearance: bool) -> Self { pub fn appearance(mut self, appearance: bool) -> Self {
self.appearance = appearance; self.appearance = appearance;
@ -208,8 +231,22 @@ impl TextInput {
} }
/// Set the prefix element of the input field, for example a search Icon. /// Set the prefix element of the input field, for example a search Icon.
pub fn prefix(mut self, prefix: impl Into<AnyView>) -> Self { pub fn prefix<F, E>(mut self, builder: F) -> Self
self.prefix = Some(prefix.into()); 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 self
} }
@ -219,12 +256,6 @@ impl TextInput {
self 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. /// Set the size of the input field.
pub fn size(mut self, size: impl Into<Size>) -> Self { pub fn size(mut self, size: impl Into<Size>) -> Self {
self.size = size.into(); self.size = size.into();
@ -237,6 +268,12 @@ impl TextInput {
self 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. /// Return the text of the input field.
pub fn text(&self) -> SharedString { pub fn text(&self) -> SharedString {
self.text.clone() self.text.clone()
@ -908,6 +945,9 @@ impl Render for TextInput {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement { fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let focused = self.focus_handle.is_focused(cx); 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() div()
.flex() .flex()
.key_context(CONTEXT) .key_context(CONTEXT)
@ -961,7 +1001,7 @@ impl Render for TextInput {
cx.theme().background cx.theme().background
}) })
}) })
.when_some(self.prefix.clone(), |this, prefix| this.child(prefix)) .children(prefix)
.gap_1() .gap_1()
.items_center() .items_center()
.child( .child(
@ -974,16 +1014,20 @@ impl Render for TextInput {
input: cx.view().clone(), input: cx.view().clone(),
}), }),
) )
.when(self.cleanable && !self.text.is_empty(), |this| { .when(self.loading, |this| this.child(Indicator::new()))
this.child( .when(
Button::new("clean-text", cx) self.cleanable && !self.loading && !self.text.is_empty(),
.icon(IconName::Close) |this| {
.style(ButtonStyle::Ghost) this.child(
.size(px(15.)) Button::new("clean-text", cx)
.cursor_pointer() .icon(IconName::Close)
.on_click(cx.listener(Self::clean)), .style(ButtonStyle::Ghost)
) .size(px(15.))
}) .cursor_pointer()
.when_some(self.suffix.clone(), |this, suffix| this.child(suffix)) .on_click(cx.listener(Self::clean)),
)
},
)
.children(suffix)
} }
} }

View file

@ -1,18 +1,18 @@
use std::time::Duration;
use std::{cell::Cell, rc::Rc}; use std::{cell::Cell, rc::Rc};
use gpui::prelude::FluentBuilder as _;
use crate::input::{InputEvent, TextInput}; use crate::input::{InputEvent, TextInput};
use crate::scroll::ScrollbarState; use crate::scroll::ScrollbarState;
use crate::theme::{ActiveTheme, Colorize as _}; use crate::theme::{ActiveTheme, Colorize as _};
use crate::IconName;
use crate::{scroll::Scrollbar, v_flex}; use crate::{scroll::Scrollbar, v_flex};
use crate::{Icon, IconName};
use gpui::{ use gpui::{
actions, div, px, uniform_list, AppContext, FocusHandle, FocusableView, actions, div, prelude::FluentBuilder as _, px, uniform_list, AppContext, FocusHandle,
InteractiveElement as _, IntoElement, KeyBinding, Length, ListSizingBehavior, MouseButton, FocusableView, InteractiveElement as _, IntoElement, KeyBinding, Length, ListSizingBehavior,
ParentElement as _, Render, Styled as _, UniformListScrollHandle, View, ViewContext, MouseButton, ParentElement as _, Render, Styled as _, Task, UniformListScrollHandle, View,
VisualContext as _, ViewContext, VisualContext as _,
}; };
use smol::Timer;
actions!(list, [Cancel, Confirm, SelectPrev, SelectNext]); actions!(list, [Cancel, Confirm, SelectPrev, SelectNext]);
@ -33,7 +33,9 @@ pub trait ListDelegate: Sized + 'static {
/// When Query Input change, this method will be called. /// When Query Input change, this method will be called.
/// You can perform search here. /// 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. /// Return the number of items in the list.
fn items_count(&self) -> usize; fn items_count(&self) -> usize;
@ -43,6 +45,11 @@ pub trait ListDelegate: Sized + 'static {
/// Return None will skip the item. /// Return None will skip the item.
fn render_item(&self, ix: usize, cx: &mut ViewContext<List<Self>>) -> Option<Self::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. /// Return the confirmed index of the selected item.
fn confirmed_index(&self) -> Option<usize> { fn confirmed_index(&self) -> Option<usize> {
None None
@ -63,6 +70,8 @@ pub struct List<D: ListDelegate> {
delegate: D, delegate: D,
max_height: Option<Length>, max_height: Option<Length>,
query_input: Option<View<TextInput>>, query_input: Option<View<TextInput>>,
last_query: Option<String>,
loading: bool,
enable_scrollbar: bool, enable_scrollbar: bool,
vertical_scroll_handle: UniformListScrollHandle, vertical_scroll_handle: UniformListScrollHandle,
@ -79,7 +88,7 @@ where
let query_input = cx.new_view(|cx| { let query_input = cx.new_view(|cx| {
TextInput::new(cx) TextInput::new(cx)
.appearance(false) .appearance(false)
.prefix(Icon::new(IconName::Search).view(cx)) .prefix(|_| IconName::Search)
.placeholder("Search...") .placeholder("Search...")
.cleanable(true) .cleanable(true)
}); });
@ -91,11 +100,13 @@ where
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
delegate, delegate,
query_input: Some(query_input), query_input: Some(query_input),
last_query: None,
selected_index: None, selected_index: None,
vertical_scroll_handle: UniformListScrollHandle::new(), vertical_scroll_handle: UniformListScrollHandle::new(),
scrollbar_state: Rc::new(Cell::new(ScrollbarState::new())), scrollbar_state: Rc::new(Cell::new(ScrollbarState::new())),
max_height: None, max_height: None,
enable_scrollbar: true, enable_scrollbar: true,
loading: false,
} }
} }
@ -162,25 +173,57 @@ where
) { ) {
match event { match event {
InputEvent::Change(text) => { InputEvent::Change(text) => {
self.delegate.perform_search(&text.trim(), cx); let text = text.trim().to_string();
cx.notify() 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), 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>) { fn action_cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
self.delegate.cancel(cx); self.delegate.cancel(cx);
cx.notify(); cx.notify();
} }
fn action_confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) { fn action_confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
if self.delegate.items_count() == 0 {
return;
}
self.delegate.confirm(self.selected_index, cx); self.delegate.confirm(self.selected_index, cx);
cx.notify(); cx.notify();
} }
fn action_select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) { 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); let selected_index = self.selected_index.unwrap_or(0);
if selected_index > 0 { if selected_index > 0 {
self.selected_index = Some(selected_index - 1); self.selected_index = Some(selected_index - 1);
@ -193,6 +236,10 @@ where
} }
fn action_select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) { 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); let selected_index = self.selected_index.unwrap_or(0);
if selected_index < self.delegate.items_count() - 1 { if selected_index < self.delegate.items_count() - 1 {
self.selected_index = Some(selected_index + 1); self.selected_index = Some(selected_index + 1);
@ -261,39 +308,44 @@ where
.min_h(px(100.)) .min_h(px(100.))
.when_some(self.max_height, |this, h| this.max_h(h)) .when_some(self.max_height, |this, h| this.max_h(h))
.overflow_hidden() .overflow_hidden()
.child( .when(items_count == 0, |this| {
uniform_list(view, "uniform-list", items_count, { this.child(self.delegate().render_empty(cx))
move |list, visible_range, cx| { })
visible_range .when(items_count > 0, |this| {
.map(|ix| { this.child(
div() uniform_list(view, "uniform-list", items_count, {
.id("list-item") move |list, visible_range, cx| {
.w_full() visible_range
.children(list.delegate.render_item(ix, cx)) .map(|ix| {
.when_some( div()
list.selected_index, .id("list-item")
|this, selected_index| { .w_full()
this.when(ix == selected_index, |this| { .children(list.delegate.render_item(ix, cx))
this.bg(selected_bg) .when_some(
}) list.selected_index,
}, |this, selected_index| {
) this.when(ix == selected_index, |this| {
.on_mouse_down( this.bg(selected_bg)
MouseButton::Left, })
cx.listener(move |this, _, cx| { },
this.selected_index = Some(ix); )
this.action_confirm(&Confirm, cx); .on_mouse_down(
}), MouseButton::Left,
) cx.listener(move |this, _, cx| {
}) this.selected_index = Some(ix);
.collect::<Vec<_>>() this.action_confirm(&Confirm, cx);
} }),
}) )
.flex_grow() })
.with_sizing_behavior(sizing_behavior) .collect::<Vec<_>>()
.track_scroll(vertical_scroll_handle) }
.into_any_element(), })
) .flex_grow()
.with_sizing_behavior(sizing_behavior)
.track_scroll(vertical_scroll_handle)
.into_any_element(),
)
})
.children(self.render_scrollbar(cx)), .children(self.render_scrollbar(cx)),
) )
} }