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",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
"smol",
|
||||||
"taffy",
|
"taffy",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"usvg",
|
"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 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",
|
||||||
|
|
|
||||||
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| {
|
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.")
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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| {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue