Add cleanable to Input (#36)
This commit is contained in:
parent
ce7c1a3a34
commit
07e1296916
8 changed files with 220 additions and 59 deletions
|
|
@ -113,6 +113,8 @@ impl DropdownStory {
|
|||
cx,
|
||||
)
|
||||
.size(ui::Size::Small)
|
||||
.placeholder("UI")
|
||||
.title_prefix("UI: ")
|
||||
}),
|
||||
simple_dropdown2: cx.new_view(|cx| {
|
||||
Dropdown::string_list(
|
||||
|
|
@ -127,6 +129,8 @@ impl DropdownStory {
|
|||
cx,
|
||||
)
|
||||
.size(ui::Size::Small)
|
||||
.placeholder("Language")
|
||||
.title_prefix("Language: ")
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
|
@ -152,7 +156,7 @@ impl Render for DropdownStory {
|
|||
.child(self.furit_dropdown.clone()),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
v_flex()
|
||||
.w_full()
|
||||
.items_center()
|
||||
.p_10()
|
||||
|
|
@ -161,6 +165,22 @@ impl Render for DropdownStory {
|
|||
.border_1()
|
||||
.border_color(cx.theme().border)
|
||||
.gap_4()
|
||||
.child(format!(
|
||||
"Country: {:?}",
|
||||
self.country_dropdown.read(cx).selected_value()
|
||||
))
|
||||
.child(format!(
|
||||
"Furit: {:?}",
|
||||
self.furit_dropdown.read(cx).selected_value()
|
||||
))
|
||||
.child(format!(
|
||||
"UI: {:?}",
|
||||
self.simple_dropdown1.read(cx).selected_value()
|
||||
))
|
||||
.child(format!(
|
||||
"Language: {:?}",
|
||||
self.simple_dropdown2.read(cx).selected_value()
|
||||
))
|
||||
.child("This is other text."),
|
||||
)
|
||||
.child(
|
||||
|
|
|
|||
|
|
@ -37,13 +37,13 @@ impl InputStory {
|
|||
|
||||
fn new(cx: &mut WindowContext) -> Self {
|
||||
let input1 = cx.new_view(|cx| {
|
||||
let mut input = TextInput::new(cx);
|
||||
let mut input = TextInput::new(cx).cleanable(true);
|
||||
input.set_text("Hello 世界", cx);
|
||||
input
|
||||
});
|
||||
|
||||
let mask_input = cx.new_view(|cx| {
|
||||
let mut input = TextInput::new(cx);
|
||||
let mut input = TextInput::new(cx).cleanable(true);
|
||||
input.set_masked(true, cx);
|
||||
input.set_text("this-is-password", cx);
|
||||
input
|
||||
|
|
@ -53,16 +53,19 @@ impl InputStory {
|
|||
TextInput::new(cx)
|
||||
.prefix(IconName::Search.view(cx))
|
||||
.placeholder("Search some thing...")
|
||||
.cleanable(true)
|
||||
});
|
||||
let suffix_input1 = cx.new_view(|cx| {
|
||||
TextInput::new(cx)
|
||||
.suffix(IconName::Info.view(cx))
|
||||
.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))
|
||||
.cleanable(true)
|
||||
.placeholder("This input have prefix and suffix.")
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -154,6 +154,13 @@ impl ListDelegate for CompanyListDelegate {
|
|||
cx.dispatch_action(Box::new(SelectedCompany));
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: Option<usize>, cx: &mut ViewContext<List<Self>>) {
|
||||
if let Some(ix) = ix {
|
||||
self.selected_index = ix;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn render_item(&self, ix: usize, _cx: &mut ViewContext<List<Self>>) -> Option<Self::Item> {
|
||||
let selected = ix == self.selected_index;
|
||||
if let Some(company) = self.companies.get(ix) {
|
||||
|
|
|
|||
|
|
@ -79,6 +79,13 @@ impl ListDelegate for ListItemDeletegate {
|
|||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: Option<usize>, cx: &mut ViewContext<List<Self>>) {
|
||||
if let Some(ix) = ix {
|
||||
self.selected_index = ix;
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PickerStory {
|
||||
|
|
|
|||
|
|
@ -2,10 +2,10 @@ use std::rc::Rc;
|
|||
|
||||
use gpui::{
|
||||
actions, deferred, div, prelude::FluentBuilder as _, px, rems, AnyElement, AppContext,
|
||||
DismissEvent, Element, ElementId, EventEmitter, FocusHandle, FocusableView, InteractiveElement,
|
||||
IntoElement, KeyBinding, LayoutId, ParentElement as _, Render, SharedString,
|
||||
StatefulInteractiveElement as _, Styled as _, View, ViewContext, VisualContext as _, WeakView,
|
||||
WindowContext,
|
||||
ClickEvent, DismissEvent, Element, ElementId, EventEmitter, FocusHandle, FocusableView,
|
||||
InteractiveElement, IntoElement, KeyBinding, LayoutId, ParentElement as _, Render,
|
||||
SharedString, StatefulInteractiveElement as _, Styled as _, View, ViewContext,
|
||||
VisualContext as _, WeakView, WindowContext,
|
||||
};
|
||||
|
||||
actions!(dropdown, [Up, Down, Enter, Escape]);
|
||||
|
|
@ -21,11 +21,12 @@ pub fn init(cx: &mut AppContext) {
|
|||
}
|
||||
|
||||
use crate::{
|
||||
button::{Button, ButtonStyle},
|
||||
h_flex,
|
||||
list::{self, List, ListDelegate, ListItem},
|
||||
styled_ext::Sizeful,
|
||||
theme::ActiveTheme,
|
||||
Icon, IconName, Size, StyledExt,
|
||||
Clickable as _, Icon, IconName, Size, StyledExt,
|
||||
};
|
||||
|
||||
/// A trait for items that can be displayed in a dropdown.
|
||||
|
|
@ -116,19 +117,19 @@ where
|
|||
self.selected_index = ix;
|
||||
|
||||
if let Some(view) = self.dropdown.upgrade() {
|
||||
cx.update_view(&view, |view, cx| {
|
||||
if let Some(ix) = self.selected_index {
|
||||
if let Some(item) = self.delegate.get(ix) {
|
||||
view.title = Some(item.title().to_string().into());
|
||||
view.value = Some(item.value().to_string().into());
|
||||
}
|
||||
}
|
||||
|
||||
cx.update_view(&view, |view, _| {
|
||||
view.selected_value = self
|
||||
.selected_index
|
||||
.and_then(|ix| self.delegate.get(ix))
|
||||
.map(|item| item.value().to_string().into());
|
||||
view.open = false;
|
||||
view.focus_handle.focus(cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: Option<usize>, _: &mut ViewContext<List<Self>>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StringDropdownDelegate {
|
||||
|
|
@ -151,9 +152,10 @@ pub struct Dropdown<D: DropdownDelegate + 'static> {
|
|||
list: View<List<DropdownListDelegate<D>>>,
|
||||
size: Size,
|
||||
open: bool,
|
||||
/// The value of the selected item.
|
||||
value: Option<SharedString>,
|
||||
title: Option<SharedString>,
|
||||
cleanable: bool,
|
||||
placeholder: SharedString,
|
||||
title_prefix: Option<SharedString>,
|
||||
selected_value: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl<D> Dropdown<D>
|
||||
|
|
@ -172,30 +174,20 @@ where
|
|||
selected_index,
|
||||
};
|
||||
|
||||
let (title, value) = if let Some(selected_index) = selected_index {
|
||||
let title: Option<SharedString> = delegate
|
||||
.delegate
|
||||
.get(selected_index)
|
||||
.map(|item| item.title().to_string().into());
|
||||
let value: Option<SharedString> = delegate
|
||||
.delegate
|
||||
.get(selected_index)
|
||||
.map(|item| item.value().to_string().into());
|
||||
(title, value)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let list = cx.new_view(|cx| List::new(delegate, cx).no_query().max_h(rems(20.)));
|
||||
Self {
|
||||
let mut this = Self {
|
||||
id: id.into(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
placeholder: "Select...".into(),
|
||||
list,
|
||||
size: Size::Medium,
|
||||
selected_value: None,
|
||||
open: false,
|
||||
title,
|
||||
value,
|
||||
}
|
||||
cleanable: true,
|
||||
title_prefix: None,
|
||||
};
|
||||
this.update_selected_value(cx);
|
||||
this
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: Size) -> Self {
|
||||
|
|
@ -203,13 +195,52 @@ where
|
|||
self
|
||||
}
|
||||
|
||||
pub fn set_value(&mut self, value: impl Into<SharedString>, cx: &mut ViewContext<Self>) {
|
||||
self.value = Some(value.into());
|
||||
cx.notify();
|
||||
/// Set the placeholder for display when dropdown value is empty.
|
||||
pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
|
||||
self.placeholder = placeholder.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn value(&self) -> Option<SharedString> {
|
||||
self.value.clone()
|
||||
/// Set title prefix for the dropdown.
|
||||
///
|
||||
/// e.g.: Country: United States
|
||||
///
|
||||
/// You should set the label is `Country: `
|
||||
pub fn title_prefix(mut self, prefix: impl Into<SharedString>) -> Self {
|
||||
self.title_prefix = Some(prefix.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set true to show the clear button when the input field is not empty.
|
||||
pub fn cleanable(mut self, cleanable: bool) -> Self {
|
||||
self.cleanable = cleanable;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_selected_index(
|
||||
&mut self,
|
||||
selected_index: Option<usize>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.list.update(cx, |list, cx| {
|
||||
list.set_selected_index(selected_index, cx);
|
||||
});
|
||||
self.update_selected_value(cx);
|
||||
}
|
||||
|
||||
pub fn selected_index(&self, cx: &WindowContext) -> Option<usize> {
|
||||
self.list.read(cx).selected_index()
|
||||
}
|
||||
|
||||
fn update_selected_value(&mut self, cx: &WindowContext) {
|
||||
self.selected_value = self
|
||||
.selected_index(cx)
|
||||
.and_then(|ix| self.list.read(cx).delegate().delegate.get(ix))
|
||||
.map(|item| item.value().to_string().into());
|
||||
}
|
||||
|
||||
pub fn selected_value(&self) -> Option<SharedString> {
|
||||
self.selected_value.clone()
|
||||
}
|
||||
|
||||
fn up(&mut self, _: &Up, cx: &mut ViewContext<Self>) {
|
||||
|
|
@ -239,11 +270,20 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
fn toggle_menu(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
|
||||
self.open = !self.open;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn escape(&mut self, _: &Escape, cx: &mut ViewContext<Self>) {
|
||||
self.open = false;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn clean(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
|
||||
self.set_selected_index(None, cx)
|
||||
}
|
||||
|
||||
fn render_menu_content(&self, cx: &WindowContext) -> impl IntoElement {
|
||||
div()
|
||||
.absolute()
|
||||
|
|
@ -259,6 +299,31 @@ where
|
|||
cx.dispatch_action(Box::new(Escape));
|
||||
})
|
||||
}
|
||||
|
||||
fn display_title(&self, cx: &WindowContext) -> impl IntoElement {
|
||||
if let Some(selected_index) = &self.selected_index(cx) {
|
||||
let title = self
|
||||
.list
|
||||
.read(cx)
|
||||
.delegate()
|
||||
.delegate
|
||||
.get(*selected_index)
|
||||
.map(|item| item.title().to_string())
|
||||
.unwrap();
|
||||
|
||||
h_flex()
|
||||
.children(self.title_prefix.clone().map(|prefix| {
|
||||
div()
|
||||
.text_color(cx.theme().accent_foreground)
|
||||
.child(prefix.clone())
|
||||
}))
|
||||
.child(title.clone())
|
||||
} else {
|
||||
div()
|
||||
.text_color(cx.theme().accent_foreground)
|
||||
.child(self.placeholder.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Dropdown<StringDropdownDelegate> {
|
||||
|
|
@ -291,10 +356,11 @@ where
|
|||
D: DropdownDelegate + 'static,
|
||||
{
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let title = self.title.clone().unwrap_or_else(|| "Select...".into());
|
||||
let focused = self.focus_handle.is_focused(cx);
|
||||
let show_clean = self.cleanable && self.selected_index(cx).is_some();
|
||||
|
||||
div()
|
||||
.id(self.id.clone())
|
||||
.key_context("Dropdown")
|
||||
.group(format!("dropdown-group:{}", self.id))
|
||||
.track_focus(&self.focus_handle)
|
||||
|
|
@ -321,24 +387,33 @@ where
|
|||
.input_px(self.size)
|
||||
.input_py(self.size)
|
||||
.input_h(self.size)
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.open = !this.open;
|
||||
cx.notify();
|
||||
}))
|
||||
.on_click(cx.listener(Self::toggle_menu))
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(div().flex_1().child(title))
|
||||
.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.text_color(cx.theme().muted_foreground),
|
||||
),
|
||||
.child(div().flex_1().child(self.display_title(cx)))
|
||||
.when(show_clean, |this| {
|
||||
this.child(
|
||||
Button::new("clean-text", cx)
|
||||
.icon(IconName::Close)
|
||||
.style(ButtonStyle::Ghost)
|
||||
.size(px(14.))
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(Self::clean)),
|
||||
)
|
||||
})
|
||||
.when(!show_clean, |this| {
|
||||
this.child(
|
||||
Icon::new(IconName::ChevronDown)
|
||||
.text_color(cx.theme().muted_foreground),
|
||||
)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(DropdownMenuElement {
|
||||
id: "dropdown-menu".into(),
|
||||
id: ElementId::Name(format!("dropdown-menu:{}", self.id).into()),
|
||||
dropdown: cx.view().clone(),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,14 +5,15 @@
|
|||
|
||||
use std::ops::Range;
|
||||
|
||||
use crate::button::{Button, ButtonStyle};
|
||||
use crate::styled_ext::Sizeful;
|
||||
use crate::theme::ActiveTheme;
|
||||
use crate::StyledExt as _;
|
||||
use crate::{event::InterativeElementExt as _, Size};
|
||||
use crate::{Clickable, IconName, StyledExt as _};
|
||||
use blink_cursor::BlinkCursor;
|
||||
use gpui::{
|
||||
actions, div, fill, point, prelude, px, relative, rems, size, AnyView, AppContext, Bounds,
|
||||
ClipboardItem, Context as _, Element, ElementId, ElementInputHandler, EventEmitter,
|
||||
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,
|
||||
|
|
@ -103,6 +104,7 @@ pub struct TextInput {
|
|||
disabled: bool,
|
||||
masked: bool,
|
||||
appearance: bool,
|
||||
cleanable: bool,
|
||||
size: Size,
|
||||
}
|
||||
|
||||
|
|
@ -126,6 +128,7 @@ impl TextInput {
|
|||
disabled: false,
|
||||
masked: false,
|
||||
appearance: true,
|
||||
cleanable: false,
|
||||
prefix: None,
|
||||
suffix: None,
|
||||
size: Size::Medium,
|
||||
|
|
@ -202,12 +205,19 @@ impl TextInput {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set true to show the clear button when the input field is not empty.
|
||||
pub fn cleanable(mut self, cleanable: bool) -> Self {
|
||||
self.cleanable = cleanable;
|
||||
self
|
||||
}
|
||||
|
||||
/// Return the text of the input field.
|
||||
pub fn text(&self) -> SharedString {
|
||||
self.text.clone()
|
||||
}
|
||||
|
||||
fn left(&mut self, _: &Left, cx: &mut ViewContext<Self>) {
|
||||
self.pause_blink_cursor(cx);
|
||||
if self.selected_range.is_empty() {
|
||||
self.move_to(self.previous_boundary(self.cursor_offset()), cx);
|
||||
} else {
|
||||
|
|
@ -216,6 +226,7 @@ impl TextInput {
|
|||
}
|
||||
|
||||
fn right(&mut self, _: &Right, cx: &mut ViewContext<Self>) {
|
||||
self.pause_blink_cursor(cx);
|
||||
if self.selected_range.is_empty() {
|
||||
self.move_to(self.next_boundary(self.selected_range.end), cx);
|
||||
} else {
|
||||
|
|
@ -237,10 +248,12 @@ impl TextInput {
|
|||
}
|
||||
|
||||
fn home(&mut self, _: &Home, cx: &mut ViewContext<Self>) {
|
||||
self.pause_blink_cursor(cx);
|
||||
self.move_to(0, cx);
|
||||
}
|
||||
|
||||
fn end(&mut self, _: &End, cx: &mut ViewContext<Self>) {
|
||||
self.pause_blink_cursor(cx);
|
||||
self.move_to(self.text.len(), cx);
|
||||
}
|
||||
|
||||
|
|
@ -256,13 +269,18 @@ impl TextInput {
|
|||
if self.selected_range.is_empty() {
|
||||
self.select_to(self.next_boundary(self.cursor_offset()), cx)
|
||||
}
|
||||
self.replace_text_in_range(None, "", cx)
|
||||
self.replace_text_in_range(None, "", cx);
|
||||
self.pause_blink_cursor(cx);
|
||||
}
|
||||
|
||||
fn enter(&mut self, _: &Enter, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(TextEvent::PressEnter);
|
||||
}
|
||||
|
||||
fn clean(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
|
||||
self.set_text("", cx);
|
||||
}
|
||||
|
||||
fn on_mouse_down(&mut self, event: &MouseDownEvent, cx: &mut ViewContext<Self>) {
|
||||
self.is_selecting = true;
|
||||
|
||||
|
|
@ -784,6 +802,16 @@ 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(14.))
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(Self::clean)),
|
||||
)
|
||||
})
|
||||
.when_some(self.suffix.clone(), |this, suffix| this.child(suffix))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,14 +26,21 @@ pub fn init(cx: &mut AppContext) {
|
|||
]);
|
||||
}
|
||||
|
||||
/// A delegate for the List.
|
||||
#[allow(unused)]
|
||||
pub trait ListDelegate: Sized + 'static {
|
||||
type Item: IntoElement;
|
||||
|
||||
/// 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>>) {}
|
||||
|
||||
/// Return the number of items in the list.
|
||||
fn items_count(&self) -> usize;
|
||||
|
||||
/// Render the item at the given index.
|
||||
///
|
||||
/// Return None will skip the item.
|
||||
fn render_item(&self, ix: usize, cx: &mut ViewContext<List<Self>>) -> Option<Self::Item>;
|
||||
|
||||
/// Return the confirmed index of the selected item.
|
||||
|
|
@ -41,8 +48,13 @@ pub trait ListDelegate: Sized + 'static {
|
|||
None
|
||||
}
|
||||
|
||||
/// Set the confirm and give the selected index.
|
||||
/// Set the selected index, just store the ix, don't confirm.
|
||||
fn set_selected_index(&mut self, ix: Option<usize>, cx: &mut ViewContext<List<Self>>);
|
||||
|
||||
/// Set the confirm and give the selected index, this is means user have clicked the item or pressed Enter.
|
||||
fn confirm(&mut self, ix: Option<usize>, cx: &mut ViewContext<List<Self>>) {}
|
||||
|
||||
/// Cancel the selection, e.g.: Pressed ESC.
|
||||
fn cancel(&mut self, cx: &mut ViewContext<List<Self>>) {}
|
||||
}
|
||||
|
||||
|
|
@ -113,6 +125,15 @@ where
|
|||
cx.focus(&self.focus_handle);
|
||||
}
|
||||
|
||||
pub fn set_selected_index(&mut self, ix: Option<usize>, cx: &mut ViewContext<Self>) {
|
||||
self.selected_index = ix;
|
||||
self.delegate.set_selected_index(ix, cx);
|
||||
}
|
||||
|
||||
pub fn selected_index(&self) -> Option<usize> {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn render_scrollbar(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
|
||||
if !self.enable_scrollbar {
|
||||
return None;
|
||||
|
|
|
|||
|
|
@ -182,7 +182,7 @@ impl<T: Styled> Sizeful<T> for T {
|
|||
fn input_h(self, size: Size) -> Self {
|
||||
match size {
|
||||
Size::Large => self.h_11().text_size(rems(1.)),
|
||||
Size::Medium => self.h_8().text_size(rems(0.85)),
|
||||
Size::Medium => self.h_8().text_size(rems(0.875)),
|
||||
_ => self.h(px(26.)).text_size(rems(0.8)),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue