input: Add appearance option to NumberInput, Dropdown, DatePicker to render without style. (#1054)

<img width="1100" height="935" alt="image"
src="https://github.com/user-attachments/assets/b213c778-eef0-4d63-8d07-b68da9a078b1"
/>
This commit is contained in:
Jason Lee 2025-07-14 19:57:56 +08:00 committed by GitHub
parent 867b4ff3a6
commit 6508927e7f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 223 additions and 45 deletions

View file

@ -1,12 +1,12 @@
use chrono::{Datelike, Days, Duration, Utc}; use chrono::{Datelike, Days, Duration, Utc};
use gpui::{ use gpui::{
px, App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement as _, Render, div, px, App, AppContext, Context, Entity, Focusable, IntoElement, ParentElement as _, Render,
Styled as _, Subscription, Window, Styled as _, Subscription, Window,
}; };
use gpui_component::{ use gpui_component::{
calendar, calendar,
date_picker::{DatePicker, DatePickerEvent, DatePickerState, DateRangePreset}, date_picker::{DatePicker, DatePickerEvent, DatePickerState, DateRangePreset},
v_flex, Sizable as _, v_flex, ActiveTheme as _, Sizable as _,
}; };
use crate::section; use crate::section;
@ -19,6 +19,7 @@ pub struct DatePickerStory {
date_picker_value: Option<String>, date_picker_value: Option<String>,
date_range_picker: Entity<DatePickerState>, date_range_picker: Entity<DatePickerState>,
default_range_mode_picker: Entity<DatePickerState>, default_range_mode_picker: Entity<DatePickerState>,
without_appearance_picker: Entity<DatePickerState>,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
@ -95,6 +96,8 @@ impl DatePickerStory {
let default_range_mode_picker = cx.new(|cx| DatePickerState::range(window, cx)); let default_range_mode_picker = cx.new(|cx| DatePickerState::range(window, cx));
let without_appearance_picker = cx.new(|cx| DatePickerState::new(window, cx));
let _subscriptions = vec![ let _subscriptions = vec![
cx.subscribe(&date_picker, |this, _, ev, _| match ev { cx.subscribe(&date_picker, |this, _, ev, _| match ev {
DatePickerEvent::Change(date) => { DatePickerEvent::Change(date) => {
@ -120,6 +123,7 @@ impl DatePickerStory {
data_picker_custom, data_picker_custom,
date_range_picker, date_range_picker,
default_range_mode_picker, default_range_mode_picker,
without_appearance_picker,
date_picker_value: None, date_picker_value: None,
_subscriptions, _subscriptions,
} }
@ -133,7 +137,7 @@ impl Focusable for DatePickerStory {
} }
impl Render for DatePickerStory { impl Render for DatePickerStory {
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let presets = vec![ let presets = vec![
DateRangePreset::single( DateRangePreset::single(
"Yesterday", "Yesterday",
@ -216,5 +220,15 @@ impl Render for DatePickerStory {
format!("Date picker value: {:?}", self.date_picker_value).into_element(), format!("Date picker value: {:?}", self.date_picker_value).into_element(),
), ),
) )
.child(
section("Without Appearance").max_w_128().child(
div().w_full().bg(cx.theme().secondary).child(
DatePicker::new(&self.without_appearance_picker)
.appearance(false)
.placeholder("Without appearance")
.cleanable(),
),
),
)
} }
} }

View file

@ -1,13 +1,5 @@
use gpui::{ use gpui::*;
px, App, AppContext, Context, Entity, Focusable, InteractiveElement, IntoElement, KeyBinding, use gpui_component::{button::*, checkbox::*, divider::*, dropdown::*, input::*, *};
ParentElement, Render, SharedString, Styled, Window,
};
use gpui_component::{
checkbox::Checkbox,
dropdown::{Dropdown, DropdownEvent, DropdownItem, DropdownState, SearchableVec},
h_flex, v_flex, ActiveTheme, FocusableCycle, IconName, Sizable,
};
use crate::section; use crate::section;
use crate::{Tab, TabPrev}; use crate::{Tab, TabPrev};
@ -58,6 +50,8 @@ pub struct DropdownStory {
simple_dropdown2: Entity<DropdownState<SearchableVec<SharedString>>>, simple_dropdown2: Entity<DropdownState<SearchableVec<SharedString>>>,
simple_dropdown3: Entity<DropdownState<Vec<SharedString>>>, simple_dropdown3: Entity<DropdownState<Vec<SharedString>>>,
disabled_dropdown: Entity<DropdownState<Vec<SharedString>>>, disabled_dropdown: Entity<DropdownState<Vec<SharedString>>>,
appearance_dropdown: Entity<DropdownState<Vec<SharedString>>>,
input_state: Entity<InputState>,
} }
impl super::Story for DropdownStory { impl super::Story for DropdownStory {
@ -97,6 +91,21 @@ impl DropdownStory {
]; ];
let country_dropdown = cx.new(|cx| DropdownState::new(countries, Some(6), window, cx)); let country_dropdown = cx.new(|cx| DropdownState::new(countries, Some(6), window, cx));
let appearance_dropdown = cx.new(|cx| {
DropdownState::new(
vec![
"CN".into(),
"US".into(),
"HK".into(),
"JP".into(),
"KR".into(),
],
Some(0),
window,
cx,
)
});
let input_state = cx.new(|cx| InputState::new(window, cx).placeholder("Your phone number"));
let fruits = SearchableVec::new(vec![ let fruits = SearchableVec::new(vec![
"Apple".into(), "Apple".into(),
@ -156,6 +165,8 @@ impl DropdownStory {
.new(|cx| DropdownState::new(Vec::<SharedString>::new(), None, window, cx)), .new(|cx| DropdownState::new(Vec::<SharedString>::new(), None, window, cx)),
disabled_dropdown: cx disabled_dropdown: cx
.new(|cx| DropdownState::new(Vec::<SharedString>::new(), None, window, cx)), .new(|cx| DropdownState::new(Vec::<SharedString>::new(), None, window, cx)),
appearance_dropdown,
input_state,
} }
}) })
} }
@ -276,6 +287,41 @@ impl Render for DropdownStory {
), ),
), ),
) )
.child(
section("Appearance false with TextInput")
.max_w_128()
.child(
h_flex()
.border_1()
.border_color(cx.theme().input)
.rounded_lg()
.text_color(cx.theme().secondary_foreground)
.w_full()
.gap_1()
.child(
div().w(px(140.)).child(
Dropdown::new(&self.appearance_dropdown)
.appearance(false)
.py_2()
.pl_3(),
),
)
.child(Divider::vertical())
.child(
div().flex_1().child(
TextInput::new(&self.input_state)
.appearance(false)
.pr_3()
.py_2(),
),
)
.child(
div()
.p_2()
.child(Button::new("send").small().ghost().label("Send")),
),
),
)
.child( .child(
section("Selected Values").max_w_lg().child( section("Selected Values").max_w_lg().child(
v_flex() v_flex()

View file

@ -1,5 +1,5 @@
use gpui::{ use gpui::{
div, prelude::FluentBuilder as _, App, AppContext, Axis, Context, Entity, Focusable, div, prelude::FluentBuilder as _, px, App, AppContext, Axis, Context, Entity, Focusable,
InteractiveElement, IntoElement, ParentElement as _, Render, Styled, Window, InteractiveElement, IntoElement, ParentElement as _, Render, Styled, Window,
}; };
use gpui_component::{ use gpui_component::{
@ -8,14 +8,16 @@ use gpui_component::{
color_picker::{ColorPicker, ColorPickerState}, color_picker::{ColorPicker, ColorPickerState},
date_picker::{DatePicker, DatePickerState}, date_picker::{DatePicker, DatePickerState},
divider::Divider, divider::Divider,
dropdown::{Dropdown, DropdownState},
form::{form_field, v_form}, form::{form_field, v_form},
h_flex, h_flex,
input::{InputState, TextInput}, input::{InputState, TextInput},
switch::Switch, switch::Switch,
v_flex, AxisExt, FocusableCycle, Selectable, Sizable, Size, v_flex, ActiveTheme, AxisExt, FocusableCycle, Selectable, Sizable, Size,
}; };
pub struct FormStory { pub struct FormStory {
name_prefix_state: Entity<DropdownState<Vec<String>>>,
name_input: Entity<InputState>, name_input: Entity<InputState>,
email_input: Entity<InputState>, email_input: Entity<InputState>,
bio_input: Entity<InputState>, bio_input: Entity<InputState>,
@ -50,6 +52,20 @@ impl FormStory {
} }
fn new(window: &mut Window, cx: &mut Context<Self>) -> Self { fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
let name_prefix_state = cx.new(|cx| {
DropdownState::new(
vec![
"Mr.".to_string(),
"Mrs.".to_string(),
"Ms.".to_string(),
"Dr.".to_string(),
],
Some(0),
window,
cx,
)
});
let name_input = cx.new(|cx| InputState::new(window, cx).default_value("Jason Lee")); let name_input = cx.new(|cx| InputState::new(window, cx).default_value("Jason Lee"));
let color_state = cx.new(|cx| ColorPickerState::new(window, cx)); let color_state = cx.new(|cx| ColorPickerState::new(window, cx));
@ -64,6 +80,7 @@ impl FormStory {
let date = cx.new(|cx| DatePickerState::new(window, cx)); let date = cx.new(|cx| DatePickerState::new(window, cx));
Self { Self {
name_prefix_state,
name_input, name_input,
email_input, email_input,
bio_input, bio_input,
@ -158,9 +175,23 @@ impl Render for FormStory {
.layout(self.layout) .layout(self.layout)
.with_size(self.size) .with_size(self.size)
.child( .child(
form_field() form_field().label_fn(|_, _| "Name").child(
.label_fn(|_, _| "Name") h_flex()
.child(TextInput::new(&self.name_input)), .gap_2()
.border_1()
.border_color(cx.theme().border)
.rounded(cx.theme().radius)
.child(
div().w(px(90.)).child(
Dropdown::new(&self.name_prefix_state)
.pr_0()
.appearance(false),
),
)
.child(div().flex_1().child(
TextInput::new(&self.name_input).pl_0().appearance(false),
)),
),
) )
.child( .child(
form_field() form_field()

View file

@ -4,11 +4,7 @@ use gpui::{
}; };
use crate::{section, Tab, TabPrev}; use crate::{section, Tab, TabPrev};
use gpui_component::{ use gpui_component::{button::*, input::*, *};
button::{Button, ButtonVariants as _},
input::{InputEvent, InputState, MaskPattern, TextInput},
v_flex, ContextModal, FocusableCycle, Icon, IconName, Sizable,
};
const CONTEXT: &str = "InputStory"; const CONTEXT: &str = "InputStory";
@ -271,5 +267,26 @@ impl Render for InputStory {
window.focused_input(cx).map(|input| input.read(cx).value()) window.focused_input(cx).map(|input| input.read(cx).value())
))), ))),
) )
.child(
section("Appearance false").max_w_md().child(
div()
.border_1()
.px_6()
.py_5()
.rounded_lg()
.bg(if cx.theme().is_dark() {
yellow_950()
} else {
yellow_50()
})
.text_color(if cx.theme().is_dark() {
yellow_100()
} else {
yellow_900()
})
.w_full()
.child(TextInput::new(&self.input1).appearance(false)),
),
)
} }
} }

View file

@ -1,6 +1,6 @@
use gpui::{ use gpui::{
App, AppContext as _, Context, Entity, FocusHandle, Focusable, InteractiveElement, IntoElement, div, App, AppContext as _, Context, Entity, FocusHandle, Focusable, InteractiveElement,
KeyBinding, ParentElement as _, Render, Styled, Subscription, Window, IntoElement, KeyBinding, ParentElement as _, Render, Styled, Subscription, Window,
}; };
use regex::Regex; use regex::Regex;
@ -8,7 +8,7 @@ use crate::{section, Tab, TabPrev};
use gpui_component::{ use gpui_component::{
button::{Button, ButtonVariants}, button::{Button, ButtonVariants},
input::{InputEvent, InputState, MaskPattern, NumberInput, NumberInputEvent, StepAction}, input::{InputEvent, InputState, MaskPattern, NumberInput, NumberInputEvent, StepAction},
v_flex, FocusableCycle, IconName, Sizable, v_flex, ActiveTheme, FocusableCycle, IconName, Sizable,
}; };
const CONTEXT: &str = "NumberInputStory"; const CONTEXT: &str = "NumberInputStory";
@ -27,6 +27,8 @@ pub struct NumberInputStory {
number_input2_value: u64, number_input2_value: u64,
number_input3: Entity<InputState>, number_input3: Entity<InputState>,
number_input3_value: f64, number_input3_value: f64,
number_input4: Entity<InputState>,
number_input4_value: f64,
_subscriptions: Vec<Subscription>, _subscriptions: Vec<Subscription>,
} }
@ -77,6 +79,15 @@ impl NumberInputStory {
}) })
}); });
let number_input4 = cx.new(|cx| {
InputState::new(window, cx)
.placeholder("Number Input without appearance")
.mask_pattern(MaskPattern::Number {
separator: Some(','),
fraction: Some(2),
})
});
let _subscriptions = vec![ let _subscriptions = vec![
cx.subscribe_in(&number_input1, window, Self::on_input_event), cx.subscribe_in(&number_input1, window, Self::on_input_event),
cx.subscribe_in(&number_input1, window, Self::on_number_input_event), cx.subscribe_in(&number_input1, window, Self::on_number_input_event),
@ -84,6 +95,8 @@ impl NumberInputStory {
cx.subscribe_in(&number_input2, window, Self::on_number_input_event), cx.subscribe_in(&number_input2, window, Self::on_number_input_event),
cx.subscribe_in(&number_input3, window, Self::on_input_event), cx.subscribe_in(&number_input3, window, Self::on_input_event),
cx.subscribe_in(&number_input3, window, Self::on_number_input_event), 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),
]; ];
Self { Self {
@ -93,6 +106,8 @@ impl NumberInputStory {
number_input2_value: 0, number_input2_value: 0,
number_input3, number_input3,
number_input3_value: 0.0, number_input3_value: 0.0,
number_input4,
number_input4_value: 0.0,
_subscriptions, _subscriptions,
} }
} }
@ -162,6 +177,11 @@ impl NumberInputStory {
this.update(cx, |input, cx| { this.update(cx, |input, cx| {
input.set_value(self.number_input3_value.to_string(), window, cx); input.set_value(self.number_input3_value.to_string(), window, cx);
}); });
} else if this == &self.number_input4 {
self.number_input4_value = self.number_input4_value - 1.0;
this.update(cx, |input, cx| {
input.set_value(self.number_input4_value.to_string(), window, cx);
});
} }
} }
StepAction::Increment => { StepAction::Increment => {
@ -180,6 +200,11 @@ impl NumberInputStory {
this.update(cx, |input, cx| { this.update(cx, |input, cx| {
input.set_value(self.number_input3_value.to_string(), window, cx); input.set_value(self.number_input3_value.to_string(), window, cx);
}); });
} else if this == &self.number_input4 {
self.number_input4_value = self.number_input4_value + 1.0;
this.update(cx, |input, cx| {
input.set_value(self.number_input4_value.to_string(), window, cx);
});
} }
} }
}, },
@ -225,5 +250,14 @@ impl Render for NumberInputStory {
.max_w_md() .max_w_md()
.child(NumberInput::new(&self.number_input3)), .child(NumberInput::new(&self.number_input3)),
) )
.child(
section("Without appearance").max_w_md().child(
div()
.w_full()
.bg(cx.theme().secondary)
.rounded_md()
.child(NumberInput::new(&self.number_input4).appearance(false)),
),
)
} }
} }

View file

@ -264,6 +264,7 @@ pub struct Dropdown<D: DropdownDelegate + 'static> {
empty: Option<AnyElement>, empty: Option<AnyElement>,
menu_width: Length, menu_width: Length,
disabled: bool, disabled: bool,
appearance: bool,
} }
pub struct SearchableVec<T> { pub struct SearchableVec<T> {
@ -533,6 +534,7 @@ where
empty: None, empty: None,
menu_width: Length::Auto, menu_width: Length::Auto,
disabled: false, disabled: false,
appearance: true,
} }
} }
@ -581,6 +583,12 @@ where
self self
} }
/// Set the appearance of the dropdown, if false the dropdown input will no border, background.
pub fn appearance(mut self, appearance: bool) -> Self {
self.appearance = appearance;
self
}
/// Returns the title element for the dropdown input. /// Returns the title element for the dropdown input.
fn display_title(&self, _: &Window, cx: &App) -> impl IntoElement { fn display_title(&self, _: &Window, cx: &App) -> impl IntoElement {
let default_title = div() let default_title = div()
@ -714,17 +722,25 @@ where
.flex() .flex()
.items_center() .items_center()
.justify_between() .justify_between()
.bg(cx.theme().background) .when(self.appearance, |this| {
.border_1() this.bg(cx.theme().background)
.border_color(cx.theme().input) .border_1()
.rounded(cx.theme().radius) .border_color(cx.theme().input)
.when(cx.theme().shadow, |this| this.shadow_xs()) .rounded(cx.theme().radius)
.map(|this| if self.disabled { this } else { this }) .when(cx.theme().shadow, |this| this.shadow_xs())
})
.map(|this| {
if self.disabled {
this.shadow_none()
} else {
this
}
})
.overflow_hidden() .overflow_hidden()
.input_text_size(self.size)
.when(outline_visible, |this| this.focused_border(cx))
.input_size(self.size) .input_size(self.size)
.input_text_size(self.size)
.refine_style(&self.style) .refine_style(&self.style)
.when(outline_visible, |this| this.focused_border(cx))
.when(allow_open, |this| { .when(allow_open, |this| {
this.on_click(window.listener_for(&self.state, DropdownState::toggle_menu)) this.on_click(window.listener_for(&self.state, DropdownState::toggle_menu))
}) })

View file

@ -29,6 +29,7 @@ pub struct NumberInput {
size: Size, size: Size,
prefix: Option<AnyElement>, prefix: Option<AnyElement>,
suffix: Option<AnyElement>, suffix: Option<AnyElement>,
appearance: bool,
} }
impl NumberInput { impl NumberInput {
@ -40,6 +41,7 @@ impl NumberInput {
placeholder: SharedString::default(), placeholder: SharedString::default(),
prefix: None, prefix: None,
suffix: None, suffix: None,
appearance: true,
} }
} }
@ -74,6 +76,12 @@ impl NumberInput {
self.suffix = Some(suffix.into_any_element()); self.suffix = Some(suffix.into_any_element());
self self
} }
/// Set the appearance of the number input, if false will no border and background.
pub fn appearance(mut self, appearance: bool) -> Self {
self.appearance = appearance;
self
}
} }
impl InputState { impl InputState {
@ -128,10 +136,12 @@ impl RenderOnce for NumberInput {
.flex_1() .flex_1()
.input_size(self.size) .input_size(self.size)
.px(self.size.input_px() / 2.) .px(self.size.input_px() / 2.)
.bg(cx.theme().background) .when(self.appearance, |this| {
.border_color(cx.theme().input) this.bg(cx.theme().background)
.border_1() .border_color(cx.theme().input)
.rounded(cx.theme().radius) .border_1()
.rounded(cx.theme().radius)
})
.when(focused, |this| this.focused_border(cx)) .when(focused, |this| this.focused_border(cx))
.child( .child(
Button::new("minus") Button::new("minus")

View file

@ -80,7 +80,7 @@ impl TextInput {
self self
} }
/// Set the appearance of the input field. /// Set the appearance of the input field, if false the input field will no border, background.
pub fn appearance(mut self, appearance: bool) -> Self { pub fn appearance(mut self, appearance: bool) -> Self {
self.appearance = appearance; self.appearance = appearance;
self self

View file

@ -238,6 +238,7 @@ pub struct DatePicker {
size: Size, size: Size,
number_of_months: usize, number_of_months: usize,
presets: Option<Vec<DateRangePreset>>, presets: Option<Vec<DateRangePreset>>,
appearance: bool,
} }
impl Sizable for DatePicker { impl Sizable for DatePicker {
@ -275,6 +276,7 @@ impl DatePicker {
style: StyleRefinement::default(), style: StyleRefinement::default(),
number_of_months: 2, number_of_months: 2,
presets: None, presets: None,
appearance: true,
} }
} }
@ -301,6 +303,12 @@ impl DatePicker {
self.number_of_months = number_of_months; self.number_of_months = number_of_months;
self self
} }
/// Set appearance of the date picker, if false, the date picker will be in a minimal style.
pub fn appearance(mut self, appearance: bool) -> Self {
self.appearance = appearance;
self
}
} }
impl RenderOnce for DatePicker { impl RenderOnce for DatePicker {
@ -337,14 +345,16 @@ impl RenderOnce for DatePicker {
.flex() .flex()
.items_center() .items_center()
.justify_between() .justify_between()
.bg(cx.theme().background) .when(self.appearance, |this| {
.border_1() this.bg(cx.theme().background)
.border_color(cx.theme().input) .border_1()
.rounded(cx.theme().radius) .border_color(cx.theme().input)
.when(cx.theme().shadow, |this| this.shadow_xs()) .rounded(cx.theme().radius)
.when(cx.theme().shadow, |this| this.shadow_xs())
.when(is_focused, |this| this.focused_border(cx))
})
.overflow_hidden() .overflow_hidden()
.input_text_size(self.size) .input_text_size(self.size)
.when(is_focused, |this| this.focused_border(cx))
.input_size(self.size) .input_size(self.size)
.when(!state.open, |this| { .when(!state.open, |this| {
this.on_click( this.on_click(