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 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,
};
use gpui_component::{
calendar,
date_picker::{DatePicker, DatePickerEvent, DatePickerState, DateRangePreset},
v_flex, Sizable as _,
v_flex, ActiveTheme as _, Sizable as _,
};
use crate::section;
@ -19,6 +19,7 @@ pub struct DatePickerStory {
date_picker_value: Option<String>,
date_range_picker: Entity<DatePickerState>,
default_range_mode_picker: Entity<DatePickerState>,
without_appearance_picker: Entity<DatePickerState>,
_subscriptions: Vec<Subscription>,
}
@ -95,6 +96,8 @@ impl DatePickerStory {
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![
cx.subscribe(&date_picker, |this, _, ev, _| match ev {
DatePickerEvent::Change(date) => {
@ -120,6 +123,7 @@ impl DatePickerStory {
data_picker_custom,
date_range_picker,
default_range_mode_picker,
without_appearance_picker,
date_picker_value: None,
_subscriptions,
}
@ -133,7 +137,7 @@ impl Focusable 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![
DateRangePreset::single(
"Yesterday",
@ -216,5 +220,15 @@ impl Render for DatePickerStory {
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::{
px, App, AppContext, Context, Entity, Focusable, InteractiveElement, IntoElement, KeyBinding,
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 gpui::*;
use gpui_component::{button::*, checkbox::*, divider::*, dropdown::*, input::*, *};
use crate::section;
use crate::{Tab, TabPrev};
@ -58,6 +50,8 @@ pub struct DropdownStory {
simple_dropdown2: Entity<DropdownState<SearchableVec<SharedString>>>,
simple_dropdown3: 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 {
@ -97,6 +91,21 @@ impl DropdownStory {
];
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![
"Apple".into(),
@ -156,6 +165,8 @@ impl DropdownStory {
.new(|cx| DropdownState::new(Vec::<SharedString>::new(), None, window, cx)),
disabled_dropdown: 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(
section("Selected Values").max_w_lg().child(
v_flex()

View file

@ -1,5 +1,5 @@
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,
};
use gpui_component::{
@ -8,14 +8,16 @@ use gpui_component::{
color_picker::{ColorPicker, ColorPickerState},
date_picker::{DatePicker, DatePickerState},
divider::Divider,
dropdown::{Dropdown, DropdownState},
form::{form_field, v_form},
h_flex,
input::{InputState, TextInput},
switch::Switch,
v_flex, AxisExt, FocusableCycle, Selectable, Sizable, Size,
v_flex, ActiveTheme, AxisExt, FocusableCycle, Selectable, Sizable, Size,
};
pub struct FormStory {
name_prefix_state: Entity<DropdownState<Vec<String>>>,
name_input: Entity<InputState>,
email_input: Entity<InputState>,
bio_input: Entity<InputState>,
@ -50,6 +52,20 @@ impl FormStory {
}
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 color_state = cx.new(|cx| ColorPickerState::new(window, cx));
@ -64,6 +80,7 @@ impl FormStory {
let date = cx.new(|cx| DatePickerState::new(window, cx));
Self {
name_prefix_state,
name_input,
email_input,
bio_input,
@ -158,9 +175,23 @@ impl Render for FormStory {
.layout(self.layout)
.with_size(self.size)
.child(
form_field()
.label_fn(|_, _| "Name")
.child(TextInput::new(&self.name_input)),
form_field().label_fn(|_, _| "Name").child(
h_flex()
.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(
form_field()

View file

@ -4,11 +4,7 @@ use gpui::{
};
use crate::{section, Tab, TabPrev};
use gpui_component::{
button::{Button, ButtonVariants as _},
input::{InputEvent, InputState, MaskPattern, TextInput},
v_flex, ContextModal, FocusableCycle, Icon, IconName, Sizable,
};
use gpui_component::{button::*, input::*, *};
const CONTEXT: &str = "InputStory";
@ -271,5 +267,26 @@ impl Render for InputStory {
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::{
App, AppContext as _, Context, Entity, FocusHandle, Focusable, InteractiveElement, IntoElement,
KeyBinding, ParentElement as _, Render, Styled, Subscription, Window,
div, App, AppContext as _, Context, Entity, FocusHandle, Focusable, InteractiveElement,
IntoElement, KeyBinding, ParentElement as _, Render, Styled, Subscription, Window,
};
use regex::Regex;
@ -8,7 +8,7 @@ use crate::{section, Tab, TabPrev};
use gpui_component::{
button::{Button, ButtonVariants},
input::{InputEvent, InputState, MaskPattern, NumberInput, NumberInputEvent, StepAction},
v_flex, FocusableCycle, IconName, Sizable,
v_flex, ActiveTheme, FocusableCycle, IconName, Sizable,
};
const CONTEXT: &str = "NumberInputStory";
@ -27,6 +27,8 @@ pub struct NumberInputStory {
number_input2_value: u64,
number_input3: Entity<InputState>,
number_input3_value: f64,
number_input4: Entity<InputState>,
number_input4_value: f64,
_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![
cx.subscribe_in(&number_input1, window, Self::on_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_input3, window, Self::on_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 {
@ -93,6 +106,8 @@ impl NumberInputStory {
number_input2_value: 0,
number_input3,
number_input3_value: 0.0,
number_input4,
number_input4_value: 0.0,
_subscriptions,
}
}
@ -162,6 +177,11 @@ impl NumberInputStory {
this.update(cx, |input, 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 => {
@ -180,6 +200,11 @@ impl NumberInputStory {
this.update(cx, |input, 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()
.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>,
menu_width: Length,
disabled: bool,
appearance: bool,
}
pub struct SearchableVec<T> {
@ -533,6 +534,7 @@ where
empty: None,
menu_width: Length::Auto,
disabled: false,
appearance: true,
}
}
@ -581,6 +583,12 @@ where
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.
fn display_title(&self, _: &Window, cx: &App) -> impl IntoElement {
let default_title = div()
@ -714,17 +722,25 @@ where
.flex()
.items_center()
.justify_between()
.bg(cx.theme().background)
.border_1()
.border_color(cx.theme().input)
.rounded(cx.theme().radius)
.when(cx.theme().shadow, |this| this.shadow_xs())
.map(|this| if self.disabled { this } else { this })
.when(self.appearance, |this| {
this.bg(cx.theme().background)
.border_1()
.border_color(cx.theme().input)
.rounded(cx.theme().radius)
.when(cx.theme().shadow, |this| this.shadow_xs())
})
.map(|this| {
if self.disabled {
this.shadow_none()
} else {
this
}
})
.overflow_hidden()
.input_text_size(self.size)
.when(outline_visible, |this| this.focused_border(cx))
.input_size(self.size)
.input_text_size(self.size)
.refine_style(&self.style)
.when(outline_visible, |this| this.focused_border(cx))
.when(allow_open, |this| {
this.on_click(window.listener_for(&self.state, DropdownState::toggle_menu))
})

View file

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

View file

@ -80,7 +80,7 @@ impl TextInput {
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 {
self.appearance = appearance;
self

View file

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