input, select, date_picker: Refactor cleanable to have argument and default to false. (#1506)

https://github.com/longbridge/gpui-component/pull/1502#issuecomment-3485354324

## Break Change

- The `DatePicker` and `Select` has changed `cleanable` default from
`true` to `false`.
- Updated `Input`, `DatePicker` and `Select` the `cleanable` to have a
argument.

```diff
- pub fn cleanable(mut self)
+ pub fn cleanable(mut self, cleanable: bool)
```

---------

Co-authored-by: Jason Lee <huacnlee@gmail.com>
This commit is contained in:
Moulberry 2025-11-05 09:33:31 +08:00 committed by GitHub
parent 522f3ced43
commit a4e93132f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 36 additions and 175 deletions

View file

@ -1,12 +1,12 @@
use chrono::{Datelike, Days, Duration, Utc}; use chrono::{Datelike, Days, Duration, Utc};
use gpui::{ use gpui::{
div, px, App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement as _, Render, App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement as _, Render,
Styled as _, Subscription, Window, Styled as _, Subscription, Window, div, px,
}; };
use gpui_component::{ use gpui_component::{
calendar, ActiveTheme as _, Sizable as _, calendar,
date_picker::{DatePicker, DatePickerEvent, DatePickerState, DateRangePreset}, date_picker::{DatePicker, DatePickerEvent, DatePickerState, DateRangePreset},
v_flex, ActiveTheme as _, Sizable as _, v_flex,
}; };
use crate::section; use crate::section;
@ -172,7 +172,7 @@ impl Render for DatePickerStory {
.child( .child(
section("Normal").max_w_128().child( section("Normal").max_w_128().child(
DatePicker::new(&self.date_picker) DatePicker::new(&self.date_picker)
.cleanable() .cleanable(true)
.presets(presets), .presets(presets),
), ),
) )
@ -195,7 +195,7 @@ impl Render for DatePickerStory {
section("Date Range").max_w_128().child( section("Date Range").max_w_128().child(
DatePicker::new(&self.date_range_picker) DatePicker::new(&self.date_range_picker)
.number_of_months(2) .number_of_months(2)
.cleanable() .cleanable(true)
.presets(range_presets.clone()), .presets(range_presets.clone()),
), ),
) )
@ -203,7 +203,7 @@ impl Render for DatePickerStory {
section("Default Range Mode").max_w_128().child( section("Default Range Mode").max_w_128().child(
DatePicker::new(&self.default_range_mode_picker) DatePicker::new(&self.default_range_mode_picker)
.placeholder("Range mode picker") .placeholder("Range mode picker")
.cleanable() .cleanable(true)
.presets(range_presets.clone()), .presets(range_presets.clone()),
), ),
) )
@ -217,8 +217,7 @@ impl Render for DatePickerStory {
div().w_full().bg(cx.theme().secondary).child( div().w_full().bg(cx.theme().secondary).child(
DatePicker::new(&self.without_appearance_picker) DatePicker::new(&self.without_appearance_picker)
.appearance(false) .appearance(false)
.placeholder("Without appearance") .placeholder("Without appearance"),
.cleanable(),
), ),
), ),
) )

View file

@ -153,32 +153,32 @@ impl Render for InputStory {
.child( .child(
section("Normal Input") section("Normal Input")
.max_w_md() .max_w_md()
.child(Input::new(&self.input1).cleanable()) .child(Input::new(&self.input1).cleanable(true))
.child(Input::new(&self.input2)), .child(Input::new(&self.input2)),
) )
.child( .child(
section("Input State") section("Input State")
.max_w_md() .max_w_md()
.child(Input::new(&self.disabled_input).disabled(true)) .child(Input::new(&self.disabled_input).disabled(true))
.child(Input::new(&self.mask_input).mask_toggle().cleanable()), .child(Input::new(&self.mask_input).mask_toggle().cleanable(true)),
) )
.child( .child(
section("Prefix and Suffix") section("Prefix and Suffix")
.max_w_md() .max_w_md()
.child( .child(
Input::new(&self.prefix_input1) Input::new(&self.prefix_input1)
.cleanable() .cleanable(true)
.prefix(Icon::new(IconName::Search).small()), .prefix(Icon::new(IconName::Search).small()),
) )
.child( .child(
Input::new(&self.both_input1) Input::new(&self.both_input1)
.cleanable() .cleanable(true)
.prefix(div().child(Icon::new(IconName::Search).small())) .prefix(div().child(Icon::new(IconName::Search).small()))
.suffix(Button::new("info").ghost().icon(IconName::Info).xsmall()), .suffix(Button::new("info").ghost().icon(IconName::Info).xsmall()),
) )
.child( .child(
Input::new(&self.suffix_input1) Input::new(&self.suffix_input1)
.cleanable() .cleanable(true)
.suffix(Button::new("info").ghost().icon(IconName::Info).xsmall()), .suffix(Button::new("info").ghost().icon(IconName::Info).xsmall()),
), ),
) )
@ -219,13 +219,13 @@ impl Render for InputStory {
.child( .child(
section("Input Size") section("Input Size")
.max_w_md() .max_w_md()
.child(Input::new(&self.large_input).large().cleanable()) .child(Input::new(&self.large_input).large())
.child(Input::new(&self.small_input).small().cleanable()), .child(Input::new(&self.small_input).small()),
) )
.child( .child(
section("Cleanable and ESC to clean") section("Cleanable and ESC to clean")
.max_w_md() .max_w_md()
.child(Input::new(&self.input_esc).cleanable()), .child(Input::new(&self.input_esc).cleanable(true)),
) )
.child( .child(
section("Focused Input") section("Focused Input")

View file

@ -97,7 +97,7 @@ impl Render for LabelStory {
.child( .child(
h_flex() h_flex()
.gap_x_3() .gap_x_3()
.child(Input::new(&self.highlights_input).cleanable().w_1_3()) .child(Input::new(&self.highlights_input).w_1_3())
.child( .child(
Checkbox::new("prefix") Checkbox::new("prefix")
.label("Prefix") .label("Prefix")

View file

@ -221,7 +221,7 @@ impl Render for Gallery {
.child( .child(
Input::new(&self.search_input) Input::new(&self.search_input)
.appearance(false) .appearance(false)
.cleanable(), .cleanable(true),
), ),
), ),
) )

View file

@ -193,7 +193,7 @@ impl Render for SelectStory {
section("Select").max_w_128().child( section("Select").max_w_128().child(
Select::new(&self.country_select) Select::new(&self.country_select)
.search_placeholder("Search country by name or code") .search_placeholder("Search country by name or code")
.cleanable() .cleanable(true)
.disabled(self.disabled), .disabled(self.disabled),
), ),
) )

View file

@ -115,9 +115,9 @@ impl Input {
self self
} }
/// Set true to show the clear button when the input field is not empty. /// Set whether to show the clear button when the input field is not empty, default is false.
pub fn cleanable(mut self) -> Self { pub fn cleanable(mut self, cleanable: bool) -> Self {
self.cleanable = true; self.cleanable = cleanable;
self self
} }

View file

@ -635,7 +635,7 @@ where
Icon::new(IconName::Search) Icon::new(IconName::Search)
.text_color(cx.theme().muted_foreground), .text_color(cx.theme().muted_foreground),
) )
.cleanable() .cleanable(true)
.p_0() .p_0()
.appearance(false), .appearance(false),
), ),

View file

@ -316,7 +316,7 @@ impl Default for SelectOptions {
style: StyleRefinement::default(), style: StyleRefinement::default(),
size: Size::default(), size: Size::default(),
icon: None, icon: None,
cleanable: true, cleanable: false,
placeholder: None, placeholder: None,
title_prefix: None, title_prefix: None,
empty: None, empty: None,
@ -962,9 +962,9 @@ where
self self
} }
/// Set true to show the clear button when the input field is not empty. /// Set whether to show the clear button when the input field is not empty, default is false.
pub fn cleanable(mut self) -> Self { pub fn cleanable(mut self, cleanable: bool) -> Self {
self.options.cleanable = true; self.options.cleanable = cleanable;
self self
} }

View file

@ -301,7 +301,7 @@ impl DatePicker {
Self { Self {
id: ("date-picker", state.entity_id()).into(), id: ("date-picker", state.entity_id()).into(),
state: state.clone(), state: state.clone(),
cleanable: true, cleanable: false,
placeholder: None, placeholder: None,
size: Size::default(), size: Size::default(),
style: StyleRefinement::default(), style: StyleRefinement::default(),
@ -318,9 +318,9 @@ impl DatePicker {
self self
} }
/// Set true to show the clear button when the input field is not empty. /// Set whether to show the clear button when the input field is not empty, default is false.
pub fn cleanable(mut self) -> Self { pub fn cleanable(mut self, cleanable: bool) -> Self {
self.cleanable = true; self.cleanable = cleanable;
self self
} }

View file

@ -94,7 +94,7 @@ DatePicker::new(&date_picker)
```rust ```rust
DatePicker::new(&date_picker) DatePicker::new(&date_picker)
.cleanable() // Shows clear button when date is selected .cleanable(true) // Show clear button when date is selected
``` ```
### Different Sizes ### Different Sizes
@ -360,91 +360,6 @@ DatePicker::new(&date_picker)
.presets(quarterly_presets) .presets(quarterly_presets)
``` ```
## API Reference
### DatePickerState
| Method | Description |
| ---------------------------- | ------------------------------------------- |
| `new(window, cx)` | Create a single date picker state |
| `range(window, cx)` | Create a date range picker state |
| `date_format(format)` | Set display format (default: "%Y/%m/%d") |
| `number_of_months(count)` | Set calendar months to display (default: 1) |
| `disabled_matcher(matcher)` | Set date disabling rules |
| `date()` | Get current selected date |
| `set_date(date, window, cx)` | Set selected date programmatically |
| `focus_handle(cx)` | Get focus handle |
### DatePicker
| Method | Description |
| ------------------------- | --------------------------------------- |
| `new(state)` | Create date picker with state entity |
| `placeholder(text)` | Set placeholder text |
| `cleanable()` | Show clear button when date selected |
| `presets(presets)` | Set quick selection presets |
| `number_of_months(count)` | Set months to display (overrides state) |
| `appearance(bool)` | Enable/disable default styling |
| `disabled(bool)` | Set disabled state |
| `large()` | Large size |
| `small()` | Small size |
### DateRangePreset
| Method | Description |
| -------------------------- | ------------------------- |
| `single(label, date)` | Create single date preset |
| `range(label, start, end)` | Create date range preset |
### Date
| Variant | Description |
| --------------------------------------------- | --------------------- |
| `Single(Option<NaiveDate>)` | Single date selection |
| `Range(Option<NaiveDate>, Option<NaiveDate>)` | Date range selection |
| Method | Description |
| -------------------- | ----------------------------------------- |
| `format(format_str)` | Format date(s) using chrono format string |
| `is_some()` | Check if date is selected |
| `is_complete()` | Check if selection is complete |
### Matcher
| Variant | Description |
| --------------------------- | ------------------------------------------------ |
| `DayOfWeek(Vec<u32>)` | Disable specific weekdays (0=Sunday, 6=Saturday) |
| `Interval(IntervalMatcher)` | Disable dates outside interval |
| `Range(RangeMatcher)` | Disable dates within range |
| `Custom(fn)` | Custom disable function |
| Method | Description |
| ------------------------- | ----------------------- |
| `interval(before, after)` | Create interval matcher |
| `range(from, to)` | Create range matcher |
| `custom(fn)` | Create custom matcher |
### DatePickerEvent
| Event | Description |
| -------------- | ---------------------- |
| `Change(Date)` | Date selection changed |
## Format Strings
Common date format patterns using chrono format specifiers:
| Format | Example Output |
| ------------------- | ------------------------- |
| `%Y-%m-%d` | 2023-12-25 |
| `%m/%d/%Y` | 12/25/2023 |
| `%d/%m/%Y` | 25/12/2023 |
| `%B %d, %Y` | December 25, 2023 |
| `%b %d, %Y` | Dec 25, 2023 |
| `%d %b %Y` | 25 Dec 2023 |
| `%A, %B %d, %Y` | Monday, December 25, 2023 |
| `%Y 年 %m 月 %d 日` | 2023 年 12 月 25 日 |
## Examples ## Examples
### Event Date Picker ### Event Date Picker
@ -462,7 +377,7 @@ let event_date = cx.new(|cx| {
DatePicker::new(&event_date) DatePicker::new(&event_date)
.placeholder("Choose event date") .placeholder("Choose event date")
.cleanable() .cleanable(true)
``` ```
### Booking System Date Range ### Booking System Date Range

View file

@ -49,7 +49,7 @@ Input::new(&input)
```rust ```rust
Input::new(&input) Input::new(&input)
.cleanable() // Shows clear button when input has value .cleanable(true) // Show clear button when input has value
``` ```
### With Prefix and Suffix ### With Prefix and Suffix
@ -60,7 +60,6 @@ use gpui_component::{Icon, IconName};
// With prefix icon // With prefix icon
Input::new(&input) Input::new(&input)
.prefix(Icon::new(IconName::Search).small()) .prefix(Icon::new(IconName::Search).small())
.cleanable()
// With suffix button // With suffix button
Input::new(&input) Input::new(&input)
@ -75,7 +74,6 @@ Input::new(&input)
Input::new(&input) Input::new(&input)
.prefix(Icon::new(IconName::Search).small()) .prefix(Icon::new(IconName::Search).small())
.suffix(Button::new("btn").ghost().icon(IconName::Info).xsmall()) .suffix(Button::new("btn").ghost().icon(IconName::Info).xsmall())
.cleanable()
``` ```
### Password Input (Masked) ### Password Input (Masked)
@ -113,7 +111,7 @@ let input = cx.new(|cx|
.clean_on_escape() // Clear input when ESC is pressed .clean_on_escape() // Clear input when ESC is pressed
); );
Input::new(&input).cleanable() Input::new(&input)
``` ```
### Input Validation ### Input Validation
@ -195,55 +193,6 @@ div()
.child(Input::new(&input).appearance(false)) .child(Input::new(&input).appearance(false))
``` ```
## API Reference
### InputState
| Method | Description |
| ---------------------------- | -------------------------------------- |
| `new(window, cx)` | Create a new input state |
| `placeholder(str)` | Set placeholder text |
| `default_value(str)` | Set initial value |
| `masked(bool)` | Enable password masking |
| `clean_on_escape()` | Clear on ESC key |
| `validate(fn)` | Set validation function |
| `pattern(regex)` | Set regex pattern for validation |
| `mask_pattern(pattern)` | Set input mask pattern |
| `value()` | Get current value |
| `unmask_value()` | Get unmasked value (for masked inputs) |
| `set_value(str, window, cx)` | Set input value programmatically |
| `focus_handle(cx)` | Get focus handle |
### Input
| Method | Description |
| ------------------ | ---------------------------------------- |
| `new(state)` | Create input with state entity |
| `prefix(el)` | Add prefix element |
| `suffix(el)` | Add suffix element |
| `cleanable()` | Show clear button |
| `mask_toggle()` | Show password toggle (for masked inputs) |
| `disabled(bool)` | Set disabled state |
| `appearance(bool)` | Enable/disable default styling |
| `large()` | Large size |
| `small()` | Small size |
### InputEvent
| Event | Description |
| -------------------------- | ----------------- |
| `Change` | Value changed |
| `PressEnter { secondary }` | Enter key pressed |
| `Focus` | Input focused |
| `Blur` | Input lost focus |
### MaskPattern
| Pattern | Description |
| -------------------------------- | ---------------------------------------------------- |
| String | Pattern string (9=digit, A=letter, #=digit required) |
| `Number { separator, fraction }` | Number with thousands separator |
## Examples ## Examples
### Search Input ### Search Input
@ -256,7 +205,6 @@ let search = cx.new(|cx|
Input::new(&search) Input::new(&search)
.prefix(Icon::new(IconName::Search).small()) .prefix(Icon::new(IconName::Search).small())
.cleanable()
``` ```
### Currency Input ### Currency Input

View file

@ -165,7 +165,7 @@ Select::new(&state).disabled(true)
```rust ```rust
Select::new(&state) Select::new(&state)
.cleanable() // Shows clear button when item is selected .cleanable(true) // Show clear button when item is selected
``` ```
### Custom Appearance ### Custom Appearance
@ -257,7 +257,6 @@ let state = cx.new(|cx| {
Select::new(&state) Select::new(&state)
.placeholder("Select language...") .placeholder("Select language...")
.title_prefix("Language: ") .title_prefix("Language: ")
.cleanable()
``` ```
### Country/Region Selector ### Country/Region Selector