Add cleanable to Input (#36)

This commit is contained in:
Jason Lee 2024-07-16 19:44:16 +08:00 committed by GitHub
parent ce7c1a3a34
commit 07e1296916
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 220 additions and 59 deletions

View file

@ -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(

View file

@ -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.")
});

View file

@ -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) {

View file

@ -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 {

View file

@ -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(),
})
}

View file

@ -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))
}
}

View file

@ -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;

View file

@ -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)),
}
}