From 7487662343be77cd655d4bbf1c104db39c1b2ea1 Mon Sep 17 00:00:00 2001 From: obito <1255116997@qq.com> Date: Tue, 29 Jul 2025 17:31:52 +0800 Subject: [PATCH] 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 --- crates/story/src/calendar_story.rs | 9 ++++ crates/story/src/date_picker_story.rs | 28 ++++------- crates/story/src/number_input_story.rs | 17 ++++++- crates/story/src/otp_input_story.rs | 30 ++++++++---- crates/ui/src/input/number_input.rs | 15 +++++- crates/ui/src/input/otp_input.rs | 22 +++++++-- crates/ui/src/input/state.rs | 15 +----- crates/ui/src/time/calendar.rs | 45 +++++++++++------ crates/ui/src/time/date_picker.rs | 68 ++++++++++++++++++-------- 9 files changed, 167 insertions(+), 82 deletions(-) diff --git a/crates/story/src/calendar_story.rs b/crates/story/src/calendar_story.rs index 4884082f..cd5915cc 100644 --- a/crates/story/src/calendar_story.rs +++ b/crates/story/src/calendar_story.rs @@ -13,6 +13,7 @@ pub struct CalendarStory { focus_handle: FocusHandle, calendar: Entity, calendar_wide: Entity, + calendar_with_disabled_matcher: Entity, } impl super::Story for CalendarStory { @@ -37,10 +38,13 @@ impl CalendarStory { fn new(window: &mut Window, cx: &mut Context) -> 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)), + ) } } diff --git a/crates/story/src/date_picker_story.rs b/crates/story/src/date_picker_story.rs index 58b7b31c..a8be222c 100644 --- a/crates/story/src/date_picker_story.rs +++ b/crates/story/src/date_picker_story.rs @@ -45,18 +45,17 @@ impl DatePickerStory { fn new(window: &mut Window, cx: &mut Context) -> 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 }); diff --git a/crates/story/src/number_input_story.rs b/crates/story/src/number_input_story.rs index 72f6cc70..6934a0ee 100644 --- a/crates/story/src/number_input_story.rs +++ b/crates/story/src/number_input_story.rs @@ -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, number_input4_value: f64, + disabled_input: Entity, _subscriptions: Vec, } @@ -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) diff --git a/crates/story/src/otp_input_story.rs b/crates/story/src/otp_input_story.rs index f4459382..5eebd26b 100644 --- a/crates/story/src/otp_input_story.rs +++ b/crates/story/src/otp_input_story.rs @@ -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, otp_state_large: Entity, otp_state_sized: Entity, + otp_state_disabled: Entity, _subscriptions: Vec, } @@ -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.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)), + ) } } diff --git a/crates/ui/src/input/number_input.rs b/crates/ui/src/input/number_input.rs index a30d21d4..ed32afee 100644 --- a/crates/ui/src/input/number_input.rs +++ b/crates/ui/src/input/number_input.rs @@ -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, suffix: Option, 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.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| { diff --git a/crates/ui/src/input/otp_input.rs b/crates/ui/src/input/otp_input.rs index d326e73f..cc9c0606 100644 --- a/crates/ui/src/input/otp_input.rs +++ b/crates/ui/src/input/otp_input.rs @@ -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, 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) -> 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( diff --git a/crates/ui/src/input/state.rs b/crates/ui/src/input/state.rs index 411bf1e5..17b85608 100644 --- a/crates/ui/src/input/state.rs +++ b/crates/ui/src/input/state.rs @@ -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.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; diff --git a/crates/ui/src/time/calendar.rs b/crates/ui/src/time/calendar.rs index 72101d94..49262fc6 100644 --- a/crates/ui/src/time/calendar.rs +++ b/crates/ui/src/time/calendar.rs @@ -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, style: StyleRefinement, @@ -267,7 +268,7 @@ pub struct CalendarState { today: NaiveDate, /// Number of the months view to show. number_of_months: usize, - disabled: Option, + pub(crate) disabled_matcher: Option>, } 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) -> 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, + _: &mut Window, + _: &mut Context, + ) { + 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.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) -> 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) diff --git a/crates/ui/src/time/date_picker.rs b/crates/ui/src/time/date_picker.rs index 7db0f38b..92455c58 100644 --- a/crates/ui/src/time/date_picker.rs +++ b/crates/ui/src/time/date_picker.rs @@ -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, date_format: SharedString, number_of_months: usize, + disabled_matcher: Option>, _subscriptions: Vec, } @@ -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) -> 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, - window: &mut Window, - cx: &mut Context, - ) { - 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) { + 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>, 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) -> 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), + ) + }) }), ), )