chrore: Refactor the disabled API for NumberInput, DatePicker, OtpInput, Calandar. (#1102)

- Added `disabled` method to OtpInput.
- Added `disabled` method to DatePicker.
- Added `disabled_matcher` method to CalendarState.

## Break Changes

- The `InputState` has been removed `disabled` and `set_disabled`
method, the disabled state should assign from TextInput element.

```diff
- let state = InputState::new("input1").disabled(true)
+ let state = InputState::new("input1");
+ TextInput::new(&state).disabled(true)
```

- Renamed `set_disabled` method to `set_disabled_matcher` from
`CalendarState`.
- Removed `set_disabled` method from `DatePickerState`, use
`disabled_matcher` method in `DatePicker` element.

---------

Co-authored-by: Jason Lee <huacnlee@gmail.com>
This commit is contained in:
obito 2025-07-29 17:31:52 +08:00 committed by GitHub
parent c07bc4aaad
commit 7487662343
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 167 additions and 82 deletions

View file

@ -13,6 +13,7 @@ pub struct CalendarStory {
focus_handle: FocusHandle,
calendar: Entity<CalendarState>,
calendar_wide: Entity<CalendarState>,
calendar_with_disabled_matcher: Entity<CalendarState>,
}
impl super::Story for CalendarStory {
@ -37,10 +38,13 @@ impl CalendarStory {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let calendar = cx.new(|cx| CalendarState::new(window, cx));
let calendar_wide = cx.new(|cx| CalendarState::new(window, cx));
let calendar_with_disabled_matcher =
cx.new(|cx| CalendarState::new(window, cx).disabled_matcher(vec![0, 3, 6]));
Self {
calendar,
calendar_wide,
calendar_with_disabled_matcher,
focus_handle: cx.focus_handle(),
}
}
@ -66,5 +70,10 @@ impl Render for CalendarStory {
.max_w_md()
.child(Calendar::new(&self.calendar_wide).number_of_months(3)),
)
.child(
section("With Disabled matcher (Sundays, Wednesdays, Saturdays)")
.max_w_md()
.child(Calendar::new(&self.calendar_with_disabled_matcher)),
)
}
}

View file

@ -45,18 +45,17 @@ impl DatePickerStory {
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let now = chrono::Local::now().naive_local().date();
let date_picker = cx.new(|cx| {
let mut picker = DatePickerState::new(window, cx);
let mut picker = DatePickerState::new(window, cx).disabled_matcher(vec![0, 6]);
picker.set_date(now, window, cx);
picker.set_disabled(vec![0, 6], window, cx);
picker
});
let date_picker_large = cx.new(|cx| {
let mut picker = DatePickerState::new(window, cx).date_format("%Y-%m-%d");
picker.set_disabled(
calendar::Matcher::range(Some(now), now.checked_add_days(Days::new(7))),
window,
cx,
);
let mut picker = DatePickerState::new(window, cx)
.date_format("%Y-%m-%d")
.disabled_matcher(calendar::Matcher::range(
Some(now),
now.checked_add_days(Days::new(7)),
));
picker.set_date(
now.checked_sub_days(Days::new(1)).unwrap_or_default(),
window,
@ -65,22 +64,15 @@ impl DatePickerStory {
picker
});
let date_picker_small = cx.new(|cx| {
let mut picker = DatePickerState::new(window, cx);
picker.set_disabled(
let mut picker = DatePickerState::new(window, cx).disabled_matcher(
calendar::Matcher::interval(Some(now), now.checked_add_days(Days::new(5))),
window,
cx,
);
picker.set_date(now, window, cx);
picker
});
let data_picker_custom = cx.new(|cx| {
let mut picker = DatePickerState::new(window, cx);
picker.set_disabled(
calendar::Matcher::custom(|date| date.day0() < 5),
window,
cx,
);
let mut picker = DatePickerState::new(window, cx)
.disabled_matcher(calendar::Matcher::custom(|date| date.day0() < 5));
picker.set_date(now, window, cx);
picker
});

View file

@ -8,7 +8,7 @@ use crate::{section, Tab, TabPrev};
use gpui_component::{
button::{Button, ButtonVariants},
input::{InputEvent, InputState, MaskPattern, NumberInput, NumberInputEvent, StepAction},
v_flex, ActiveTheme, FocusableCycle, IconName, Sizable,
v_flex, ActiveTheme, Disableable, FocusableCycle, IconName, Sizable,
};
const CONTEXT: &str = "NumberInputStory";
@ -29,6 +29,7 @@ pub struct NumberInputStory {
number_input3_value: f64,
number_input4: Entity<InputState>,
number_input4_value: f64,
disabled_input: Entity<InputState>,
_subscriptions: Vec<Subscription>,
}
@ -88,6 +89,12 @@ impl NumberInputStory {
})
});
let disabled_input = cx.new(|cx| {
InputState::new(window, cx)
.default_value("100")
.placeholder("Disabled input")
});
let _subscriptions = vec![
cx.subscribe_in(&number_input1, window, Self::on_input_event),
cx.subscribe_in(&number_input1, window, Self::on_number_input_event),
@ -97,6 +104,8 @@ impl NumberInputStory {
cx.subscribe_in(&number_input3, window, Self::on_number_input_event),
cx.subscribe_in(&number_input4, window, Self::on_input_event),
cx.subscribe_in(&number_input4, window, Self::on_number_input_event),
cx.subscribe_in(&disabled_input, window, Self::on_input_event),
cx.subscribe_in(&disabled_input, window, Self::on_number_input_event),
];
Self {
@ -108,6 +117,7 @@ impl NumberInputStory {
number_input3_value: 0.0,
number_input4,
number_input4_value: 0.0,
disabled_input,
_subscriptions,
}
}
@ -238,6 +248,11 @@ impl Render for NumberInputStory {
.max_w_md()
.child(NumberInput::new(&self.number_input1)),
)
.child(
section("Disabled")
.max_w_md()
.child(NumberInput::new(&self.disabled_input).disabled(true)),
)
.child(
section("Small Size with suffix").max_w_md().child(
NumberInput::new(&self.number_input2)

View file

@ -7,7 +7,7 @@ use gpui_component::{
checkbox::Checkbox,
h_flex,
input::{InputEvent, OtpInput, OtpState},
v_flex, FocusableCycle, Sizable, StyledExt,
v_flex, Disableable as _, FocusableCycle, Sizable, StyledExt,
};
use crate::{section, Tab, TabPrev};
@ -28,6 +28,7 @@ pub struct OtpInputStory {
otp_state_small: Entity<OtpState>,
otp_state_large: Entity<OtpState>,
otp_state_sized: Entity<OtpState>,
otp_state_disabled: Entity<OtpState>,
_subscriptions: Vec<Subscription>,
}
@ -88,6 +89,11 @@ impl OtpInputStory {
.masked(true)
.default_value("654321")
}),
otp_state_disabled: cx.new(|cx| {
OtpState::new(6, window, cx)
.masked(true)
.default_value("123456")
}),
_subscriptions,
}
}
@ -102,17 +108,20 @@ impl OtpInputStory {
fn toggle_opt_masked(&mut self, _: &bool, window: &mut Window, cx: &mut Context<Self>) {
self.otp_masked = !self.otp_masked;
self.otp_state.update(cx, |input, cx| {
input.set_masked(self.otp_masked, window, cx)
self.otp_state.update(cx, |state, cx| {
state.set_masked(self.otp_masked, window, cx)
});
self.otp_state_small.update(cx, |input, cx| {
input.set_masked(self.otp_masked, window, cx)
self.otp_state_small.update(cx, |state, cx| {
state.set_masked(self.otp_masked, window, cx)
});
self.otp_state_large.update(cx, |input, cx| {
input.set_masked(self.otp_masked, window, cx)
self.otp_state_large.update(cx, |state, cx| {
state.set_masked(self.otp_masked, window, cx)
});
self.otp_state_sized.update(cx, |input, cx| {
input.set_masked(self.otp_masked, window, cx)
self.otp_state_sized.update(cx, |state, cx| {
state.set_masked(self.otp_masked, window, cx)
});
self.otp_state_disabled.update(cx, |state, cx| {
state.set_masked(self.otp_masked, window, cx)
});
}
}
@ -162,5 +171,8 @@ impl Render for OtpInputStory {
.with_size(px(55.)),
),
)
.child(
section("Disabled").child(OtpInput::new(&self.otp_state_disabled).disabled(true)),
)
}
}

View file

@ -6,7 +6,7 @@ use gpui::{
use crate::{
button::{Button, ButtonVariants as _},
h_flex, ActiveTheme, IconName, Sizable, Size, StyleSized, StyledExt as _,
h_flex, ActiveTheme, Disableable, IconName, Sizable, Size, StyleSized, StyledExt as _,
};
use super::{InputState, TextInput};
@ -30,6 +30,7 @@ pub struct NumberInput {
prefix: Option<AnyElement>,
suffix: Option<AnyElement>,
appearance: bool,
disabled: bool,
}
impl NumberInput {
@ -42,6 +43,7 @@ impl NumberInput {
prefix: None,
suffix: None,
appearance: true,
disabled: false,
}
}
@ -84,6 +86,13 @@ impl NumberInput {
}
}
impl Disableable for NumberInput {
fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
}
impl InputState {
fn on_action_increment(&mut self, _: &Increment, window: &mut Window, cx: &mut Context<Self>) {
self.on_number_input_step(StepAction::Increment, window, cx);
@ -142,6 +151,7 @@ impl RenderOnce for NumberInput {
.border_1()
.rounded(cx.theme().radius)
})
.when(self.disabled, |this| this.bg(cx.theme().muted))
.when(focused, |this| this.focused_border(cx))
.child(
Button::new("minus")
@ -149,6 +159,7 @@ impl RenderOnce for NumberInput {
.with_size(self.size.smaller())
.icon(IconName::Minus)
.compact()
.disabled(self.disabled)
.on_click({
let state = self.state.clone();
move |_, window, cx| {
@ -159,6 +170,7 @@ impl RenderOnce for NumberInput {
.child(
TextInput::new(&self.state)
.appearance(false)
.disabled(self.disabled)
.px(px(2.))
.gap_0()
.when_some(self.prefix, |this, prefix| this.prefix(prefix))
@ -170,6 +182,7 @@ impl RenderOnce for NumberInput {
.with_size(self.size.smaller())
.icon(IconName::Plus)
.compact()
.disabled(self.disabled)
.on_click({
let state = self.state.clone();
move |_, window, cx| {

View file

@ -6,7 +6,7 @@ use gpui::{
};
use super::{blink_cursor::BlinkCursor, InputEvent};
use crate::{h_flex, v_flex, ActiveTheme, Icon, IconName, Sizable, Size};
use crate::{h_flex, v_flex, ActiveTheme, Disableable, Icon, IconName, Sizable, Size};
pub struct OtpState {
focus_handle: FocusHandle,
@ -183,6 +183,7 @@ pub struct OtpInput {
state: Entity<OtpState>,
number_of_groups: usize,
size: Size,
disabled: bool,
}
impl OtpInput {
@ -192,6 +193,7 @@ impl OtpInput {
state: state.clone(),
number_of_groups: 2,
size: Size::Medium,
disabled: false,
}
}
@ -201,7 +203,12 @@ impl OtpInput {
self
}
}
impl Disableable for OtpInput {
fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
}
impl Sizable for OtpInput {
fn with_size(mut self, size: impl Into<crate::Size>) -> Self {
self.size = size.into();
@ -248,6 +255,10 @@ impl RenderOnce for OtpInput {
.border_1()
.border_color(cx.theme().input)
.bg(cx.theme().background)
.when(self.disabled, |this| {
this.bg(cx.theme().muted)
.text_color(cx.theme().muted_foreground)
})
.when(is_input_focused, |this| this.border_color(cx.theme().ring))
.when(cx.theme().shadow, |this| this.shadow_xs())
.items_center()
@ -271,6 +282,9 @@ impl RenderOnce for OtpInput {
this.child(
Icon::new(IconName::Asterisk)
.text_color(cx.theme().secondary_foreground)
.when(self.disabled, |this| {
this.text_color(cx.theme().muted_foreground)
})
.with_size(text_size),
)
} else {
@ -294,7 +308,9 @@ impl RenderOnce for OtpInput {
v_flex()
.id(("otp-input", self.state.entity_id()))
.track_focus(&self.state.read(cx).focus_handle)
.on_key_down(window.listener_for(&self.state, OtpState::on_key_down))
.when(!self.disabled, |this| {
this.on_key_down(window.listener_for(&self.state, OtpState::on_key_down))
})
.items_center()
.child(
h_flex().items_center().gap_5().children(

View file

@ -723,24 +723,11 @@ impl InputState {
/// Set with disabled mode.
///
/// See also: [`Self::set_disabled`], [`Self::is_disabled`].
pub fn disabled(mut self, disabled: bool) -> Self {
pub(crate) fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
/// Set the disabled state of the input field.
///
/// See also: [`Self::disabled`], [`Self::is_disabled`].
pub fn set_disabled(&mut self, disabled: bool, _: &mut Window, cx: &mut Context<Self>) {
self.disabled = disabled;
cx.notify();
}
/// Return is the input field is disabled.
pub fn is_disabled(&self) -> bool {
self.disabled
}
/// Set with password masked state.
pub fn masked(mut self, masked: bool) -> Self {
self.masked = masked;

View file

@ -1,4 +1,4 @@
use std::borrow::Cow;
use std::{borrow::Cow, rc::Rc};
use chrono::{Datelike, Local, NaiveDate};
use gpui::{
@ -248,6 +248,7 @@ impl Matcher {
#[derive(IntoElement)]
pub struct Calendar {
id: ElementId,
size: Size,
state: Entity<CalendarState>,
style: StyleRefinement,
@ -267,7 +268,7 @@ pub struct CalendarState {
today: NaiveDate,
/// Number of the months view to show.
number_of_months: usize,
disabled: Option<Matcher>,
pub(crate) disabled_matcher: Option<Rc<Matcher>>,
}
impl CalendarState {
@ -283,11 +284,29 @@ impl CalendarState {
year_page: 0,
today,
number_of_months: 1,
disabled: None,
disabled_matcher: None,
}
.year_range((today.year() - 50, today.year() + 50))
}
/// Set the disabled matcher of the calendar state.
pub fn disabled_matcher(mut self, matcher: impl Into<Matcher>) -> Self {
self.disabled_matcher = Some(Rc::new(matcher.into()));
self
}
/// Set the disabled matcher of the calendar.
///
/// The disabled matcher will be used to disable the days that match the matcher.
pub fn set_disabled_matcher(
&mut self,
disabled: impl Into<Matcher>,
_: &mut Window,
_: &mut Context<Self>,
) {
self.disabled_matcher = Some(Rc::new(disabled.into()));
}
/// Set the date of the calendar.
///
/// When you set a range date, the mode will be automatically set to `Mode::Range`.
@ -295,9 +314,9 @@ impl CalendarState {
let date = date.into();
let invalid = self
.disabled
.disabled_matcher
.as_ref()
.map_or(false, |disabled| disabled.date_matched(&date));
.map_or(false, |matcher| matcher.date_matched(&date));
if invalid {
return;
@ -356,13 +375,6 @@ impl CalendarState {
self
}
/// Set the disabled matcher of the calendar.
///
/// The disabled matcher will be used to disable the days that match the matcher.
pub fn set_disabled(&mut self, disabled: Matcher, _: &mut Window, _: &mut Context<Self>) {
self.disabled = Some(disabled);
}
/// Get year and month by offset month.
fn offset_year_month(&self, offset_month: usize) -> (i32, u32) {
let mut month = self.current_month as i32 + offset_month as i32;
@ -497,6 +509,7 @@ impl Render for CalendarState {
impl Calendar {
pub fn new(state: &Entity<CalendarState>) -> Self {
Self {
id: ("calendar", state.entity_id()).into(),
size: Size::default(),
state: state.clone(),
style: StyleRefinement::default(),
@ -527,7 +540,7 @@ impl Calendar {
let date = *d;
let is_today = *d == state.today;
let disabled = state
.disabled
.disabled_matcher
.as_ref()
.map_or(false, |disabled| disabled.matched(&date));
@ -914,10 +927,12 @@ impl RenderOnce for Calendar {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let view_mode = self.state.read(cx).view_mode;
let number_of_months = self.number_of_months;
self.state
.update(cx, |state, _| state.number_of_months = number_of_months);
self.state.update(cx, |state, _| {
state.number_of_months = number_of_months;
});
v_flex()
.id(self.id.clone())
.track_focus(&self.state.read(cx).focus_handle)
.border_1()
.border_color(cx.theme().border)

View file

@ -1,3 +1,5 @@
use std::rc::Rc;
use chrono::NaiveDate;
use gpui::{
anchored, deferred, div, prelude::FluentBuilder as _, px, App, AppContext, Context, ElementId,
@ -12,7 +14,8 @@ use crate::{
button::{Button, ButtonVariants as _},
h_flex,
input::clear_button,
v_flex, ActiveTheme, Icon, IconName, Sizable, Size, StyleSized as _, StyledExt as _,
v_flex, ActiveTheme, Disableable, Icon, IconName, Sizable, Size, StyleSized as _,
StyledExt as _,
};
use super::calendar::{Calendar, CalendarEvent, CalendarState, Date, Matcher};
@ -64,6 +67,7 @@ pub struct DatePickerState {
calendar: Entity<CalendarState>,
date_format: SharedString,
number_of_months: usize,
disabled_matcher: Option<Rc<Matcher>>,
_subscriptions: Vec<Subscription>,
}
@ -116,6 +120,7 @@ impl DatePickerState {
open: false,
date_format: "%Y/%m/%d".into(),
number_of_months: 1,
disabled_matcher: None,
_subscriptions,
}
}
@ -154,15 +159,17 @@ impl DatePickerState {
cx.notify();
}
/// Set the disabled match for the calendar.
pub fn disabled_matcher(mut self, disabled: impl Into<Matcher>) -> Self {
self.disabled_matcher = Some(Rc::new(disabled.into()));
self
}
/// Set the disabled matcher of the date picker.
pub fn set_disabled(
&mut self,
disabled: impl Into<Matcher>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.calendar.update(cx, |view, cx| {
view.set_disabled(disabled.into(), window, cx);
fn set_canlendar_disabled_matcher(&mut self, _: &mut Window, cx: &mut Context<Self>) {
let matcher = self.disabled_matcher.clone();
self.calendar.update(cx, |state, _| {
state.disabled_matcher = matcher;
});
}
@ -239,6 +246,7 @@ pub struct DatePicker {
number_of_months: usize,
presets: Option<Vec<DateRangePreset>>,
appearance: bool,
disabled: bool,
}
impl Sizable for DatePicker {
@ -259,6 +267,13 @@ impl Styled for DatePicker {
}
}
impl Disableable for DatePicker {
fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
}
impl Render for DatePickerState {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl gpui::IntoElement {
Empty
@ -277,6 +292,7 @@ impl DatePicker {
number_of_months: 2,
presets: None,
appearance: true,
disabled: false,
}
}
@ -313,6 +329,10 @@ impl DatePicker {
impl RenderOnce for DatePicker {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
self.state.update(cx, |state, cx| {
state.set_canlendar_disabled_matcher(window, cx);
});
// This for keep focus border style, when click on the popup.
let is_focused = self.focus_handle(cx).contains_focused(window, cx);
let state = self.state.read(cx);
@ -352,11 +372,15 @@ impl RenderOnce for DatePicker {
.rounded(cx.theme().radius)
.when(cx.theme().shadow, |this| this.shadow_xs())
.when(is_focused, |this| this.focused_border(cx))
.when(self.disabled, |this| {
this.bg(cx.theme().muted)
.text_color(cx.theme().muted_foreground)
})
})
.overflow_hidden()
.input_text_size(self.size)
.input_size(self.size)
.when(!state.open, |this| {
.when(!state.open && !self.disabled, |this| {
this.on_click(
window.listener_for(&self.state, DatePickerState::toggle_calendar),
)
@ -368,17 +392,19 @@ impl RenderOnce for DatePicker {
.justify_between()
.gap_1()
.child(div().w_full().overflow_hidden().child(display_title))
.when(show_clean, |this| {
this.child(clear_button(cx).on_click(
window.listener_for(&self.state, DatePickerState::clean),
))
})
.when(!show_clean, |this| {
this.child(
Icon::new(IconName::Calendar)
.xsmall()
.text_color(cx.theme().muted_foreground),
)
.when(!self.disabled, |this| {
this.when(show_clean, |this| {
this.child(clear_button(cx).on_click(
window.listener_for(&self.state, DatePickerState::clean),
))
})
.when(!show_clean, |this| {
this.child(
Icon::new(IconName::Calendar)
.xsmall()
.text_color(cx.theme().muted_foreground),
)
})
}),
),
)