diff --git a/crates/story/src/form_story.rs b/crates/story/src/form_story.rs index fb602dde..90201524 100644 --- a/crates/story/src/form_story.rs +++ b/crates/story/src/form_story.rs @@ -9,7 +9,7 @@ use gpui_component::{ color_picker::{ColorPicker, ColorPickerState}, date_picker::{DatePicker, DatePickerState}, divider::Divider, - form::{form_field, v_form}, + form::{field, v_form}, h_flex, input::{Input, InputState}, select::{Select, SelectState}, @@ -189,7 +189,7 @@ impl Render for FormStory { .columns(self.columns) .label_width(px(if is_multi_column { 100. } else { 140. })) .child( - form_field().label_fn(|_, _| "Name").child( + field().label_fn(|_, _| "Name").child( h_flex() .gap_2() .border_1() @@ -210,13 +210,13 @@ impl Render for FormStory { ), ) .child( - form_field() + field() .label("Email") .child(Input::new(&self.email_input)) .required(true), ) .child( - form_field() + field() .label("Bio") .when(self.layout.is_vertical(), |this| this.items_start()) .child(Input::new(&self.bio_input)) @@ -225,21 +225,21 @@ impl Render for FormStory { }), ) .child( - form_field() - .no_label_indent() + field() + .label_indent(false) .when(is_multi_column, |this| this.col_span(2)) .child("This is a full width form field."), ) .child( - form_field() + field() .label("Please select your birthday") .description("Select your birthday, we will send you a gift.") .child(DatePicker::new(&self.date)), ) .child( - form_field() + field() .when(is_horizontal && is_multi_column, |this| { - this.no_label_indent() + this.label_indent(false) }) .when(is_multi_column, |this| this.col_start(1)) .child( @@ -253,9 +253,9 @@ impl Render for FormStory { ), ) .child( - form_field() + field() .when(is_horizontal && is_multi_column, |this| { - this.no_label_indent() + this.label_indent(false) }) .child( ColorPicker::new(&self.color_state) @@ -264,9 +264,9 @@ impl Render for FormStory { ), ) .child( - form_field() + field() .when(is_horizontal && is_multi_column, |this| { - this.no_label_indent() + this.label_indent(false) }) .child( Checkbox::new("use-vertical-layout") diff --git a/crates/ui/src/form.rs b/crates/ui/src/form/field.rs similarity index 67% rename from crates/ui/src/form.rs rename to crates/ui/src/form/field.rs index e367eb6f..1d7ac37f 100644 --- a/crates/ui/src/form.rs +++ b/crates/ui/src/form/field.rs @@ -1,129 +1,35 @@ -use std::rc::{Rc, Weak}; +use std::rc::Rc; use gpui::{ div, prelude::FluentBuilder as _, px, AlignItems, AnyElement, AnyView, App, Axis, Div, Element, - ElementId, FocusHandle, InteractiveElement as _, IntoElement, ParentElement, Pixels, Rems, - RenderOnce, SharedString, Styled, Window, + ElementId, InteractiveElement as _, IntoElement, ParentElement, Pixels, Rems, RenderOnce, + SharedString, Styled, Window, }; -use crate::{h_flex, v_flex, ActiveTheme as _, AxisExt, Sizable, Size, StyledExt}; - -/// Create a new form with a vertical layout. -pub fn v_form() -> Form { - Form::vertical() -} - -/// Create a new form with a horizontal layout. -pub fn h_form() -> Form { - Form::horizontal() -} - -/// Create a new form field. -pub fn form_field() -> FormField { - FormField::new() -} - -/// A form element that contains multiple form fields. -#[derive(IntoElement)] -pub struct Form { - fields: Vec, - props: FieldProps, -} +use crate::{h_flex, v_flex, ActiveTheme as _, AxisExt, Size, StyledExt}; #[derive(Clone, Copy)] -struct FieldProps { - size: Size, - label_width: Option, - label_text_size: Option, - layout: Axis, - /// Field gap - gap: Option, - columns: usize, +pub(super) struct FieldProps { + pub(super) size: Size, + pub(super) layout: Axis, + pub(super) columns: usize, + + pub(super) label_width: Option, + pub(super) label_text_size: Option, } impl Default for FieldProps { fn default() -> Self { Self { - label_width: Some(px(140.)), - label_text_size: None, layout: Axis::Vertical, size: Size::default(), - gap: None, columns: 1, + label_width: Some(px(140.)), + label_text_size: None, } } } -impl Form { - fn new() -> Self { - Self { - props: FieldProps::default(), - fields: Vec::new(), - } - } - - /// Creates a new form with a horizontal layout. - pub fn horizontal() -> Self { - Self::new().layout(Axis::Horizontal) - } - - /// Creates a new form with a vertical layout. - pub fn vertical() -> Self { - Self::new().layout(Axis::Vertical) - } - - /// Set the layout for the form, default is `Axis::Vertical`. - pub fn layout(mut self, layout: Axis) -> Self { - self.props.layout = layout; - self - } - - /// Set the width of the labels in the form. Default is `px(100.)`. - pub fn label_width(mut self, width: Pixels) -> Self { - self.props.label_width = Some(width); - self - } - - /// Set the text size of the labels in the form. Default is `None`. - pub fn label_text_size(mut self, size: Rems) -> Self { - self.props.label_text_size = Some(size); - self - } - - /// Set the gap between the form fields. - pub fn gap(mut self, gap: Pixels) -> Self { - self.props.gap = Some(gap); - self - } - - /// Add a child to the form. - pub fn child(mut self, field: impl Into) -> Self { - self.fields.push(field.into()); - self - } - - /// Add multiple children to the form. - pub fn children(mut self, fields: impl IntoIterator) -> Self { - self.fields.extend(fields); - self - } - - /// Set the column count for the form. - /// - /// Default is 1. - pub fn columns(mut self, columns: usize) -> Self { - self.props.columns = columns; - self - } -} - -impl Sizable for Form { - fn with_size(mut self, size: impl Into) -> Self { - self.props.size = size.into(); - self - } -} - pub enum FieldBuilder { String(SharedString), Element(Rc AnyElement>), @@ -170,38 +76,35 @@ impl From for FieldBuilder { } } +/// Form field element. #[derive(IntoElement)] -pub struct FormField { +pub struct Field { id: ElementId, - form: Weak
, + props: FieldProps, label: Option, - no_label_indent: bool, - focus_handle: Option, + label_indent: bool, description: Option, /// Used to render the actual form field, e.g.: Input, Switch... - child: Div, + children: Vec, visible: bool, required: bool, /// Alignment of the form field. align_items: Option, - props: FieldProps, col_span: u16, col_start: Option, col_end: Option, } -impl FormField { +impl Field { pub fn new() -> Self { Self { id: 0.into(), - form: Weak::new(), label: None, description: None, - child: div(), + children: Vec::new(), visible: true, required: false, - no_label_indent: false, - focus_handle: None, + label_indent: true, align_items: None, props: FieldProps::default(), col_span: 1, @@ -216,13 +119,13 @@ impl FormField { self } - /// Sets not indent with the label width (in Horizontal layout). + /// Sets indent with the label width (in Horizontal layout), default is `true`. /// /// Sometimes you want to align the input form left (Default is align after the label width in Horizontal layout). /// /// This is only work when the `label` is not set. - pub fn no_label_indent(mut self) -> Self { - self.no_label_indent = true; + pub fn label_indent(mut self, indent: bool) -> Self { + self.label_indent = indent; self } @@ -268,23 +171,10 @@ impl FormField { self } - /// Set the focus handle for the form field. - /// - /// If not set, the form field will not be focusable. - pub fn track_focus(mut self, focus_handle: &FocusHandle) -> Self { - self.focus_handle = Some(focus_handle.clone()); - self - } - - pub fn parent(mut self, form: &Rc) -> Self { - self.form = Rc::downgrade(form); - self - } - /// Set the properties for the form field. /// /// This is internal API for sync props from From. - fn props(mut self, ix: usize, props: FieldProps) -> Self { + pub(super) fn props(mut self, ix: usize, props: FieldProps) -> Self { self.id = ix.into(); self.props = props; self @@ -328,13 +218,14 @@ impl FormField { self } } -impl ParentElement for FormField { + +impl ParentElement for Field { fn extend(&mut self, elements: impl IntoIterator) { - self.child.extend(elements); + self.children.extend(elements); } } -impl RenderOnce for FormField { +impl RenderOnce for Field { fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let layout = self.props.layout; @@ -343,7 +234,7 @@ impl RenderOnce for FormField { } else { self.props.label_width }; - let has_label = !self.no_label_indent; + let has_label = self.label_indent; #[inline] fn wrap_div(layout: Axis) -> Div { @@ -359,13 +250,10 @@ impl RenderOnce for FormField { div().when_some(label_width, |this, width| this.w(width).flex_shrink_0()) } - let gap = match self.props.gap { - Some(v) => v, - None => match self.props.size { - Size::Large => px(8.), - Size::XSmall | Size::Small => px(4.), - _ => px(4.), - }, + let gap = match self.props.size { + Size::Large => px(8.), + Size::XSmall | Size::Small => px(4.), + _ => px(4.), }; let inner_gap = if layout.is_horizontal() { gap @@ -429,7 +317,7 @@ impl RenderOnce for FormField { .w_full() .flex_1() .overflow_x_hidden() - .child(self.child), + .children(self.children), ), ) .child( @@ -453,27 +341,3 @@ impl RenderOnce for FormField { ) } } -impl RenderOnce for Form { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let props = self.props; - - let gap = match props.size { - Size::XSmall | Size::Small => px(6.), - Size::Large => px(12.), - _ => px(8.), - }; - - v_flex() - .w_full() - .gap_x(gap * 3.) - .gap_y(gap) - .grid() - .grid_cols(props.columns as u16) - .children( - self.fields - .into_iter() - .enumerate() - .map(|(ix, field)| field.props(ix, props)), - ) - } -} diff --git a/crates/ui/src/form/form.rs b/crates/ui/src/form/form.rs new file mode 100644 index 00000000..b1310ee4 --- /dev/null +++ b/crates/ui/src/form/form.rs @@ -0,0 +1,113 @@ +use gpui::{ + px, App, Axis, IntoElement, ParentElement, Pixels, Rems, RenderOnce, StyleRefinement, Styled, + Window, +}; + +use crate::{ + form::{Field, FieldProps}, + v_flex, Sizable, Size, +}; + +/// A form element that contains multiple form fields. +#[derive(IntoElement)] +pub struct Form { + style: StyleRefinement, + fields: Vec, + props: FieldProps, +} + +impl Form { + fn new() -> Self { + Self { + style: StyleRefinement::default(), + props: FieldProps::default(), + fields: Vec::new(), + } + } + + /// Creates a new form with a horizontal layout. + pub fn horizontal() -> Self { + Self::new().layout(Axis::Horizontal) + } + + /// Creates a new form with a vertical layout. + pub fn vertical() -> Self { + Self::new().layout(Axis::Vertical) + } + + /// Set the layout for the form, default is `Axis::Vertical`. + pub fn layout(mut self, layout: Axis) -> Self { + self.props.layout = layout; + self + } + + /// Set the width of the labels in the form. Default is `px(100.)`. + pub fn label_width(mut self, width: Pixels) -> Self { + self.props.label_width = Some(width); + self + } + + /// Set the text size of the labels in the form. Default is `None`. + pub fn label_text_size(mut self, size: Rems) -> Self { + self.props.label_text_size = Some(size); + self + } + + /// Add a child to the form. + pub fn child(mut self, field: impl Into) -> Self { + self.fields.push(field.into()); + self + } + + /// Add multiple children to the form. + pub fn children(mut self, fields: impl IntoIterator) -> Self { + self.fields.extend(fields); + self + } + + /// Set the column count for the form. + /// + /// Default is 1. + pub fn columns(mut self, columns: usize) -> Self { + self.props.columns = columns; + self + } +} + +impl Styled for Form { + fn style(&mut self) -> &mut StyleRefinement { + &mut self.style + } +} + +impl Sizable for Form { + fn with_size(mut self, size: impl Into) -> Self { + self.props.size = size.into(); + self + } +} + +impl RenderOnce for Form { + fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { + let props = self.props; + + let gap = match props.size { + Size::XSmall | Size::Small => px(6.), + Size::Large => px(12.), + _ => px(8.), + }; + + v_flex() + .w_full() + .gap_x(gap * 3.) + .gap_y(gap) + .grid() + .grid_cols(props.columns as u16) + .children( + self.fields + .into_iter() + .enumerate() + .map(|(ix, field)| field.props(ix, props)), + ) + } +} diff --git a/crates/ui/src/form/mod.rs b/crates/ui/src/form/mod.rs new file mode 100644 index 00000000..b3962605 --- /dev/null +++ b/crates/ui/src/form/mod.rs @@ -0,0 +1,20 @@ +mod field; +mod form; + +pub use field::*; +pub use form::*; + +/// Create a new [`Form`] with a vertical layout. +pub fn v_form() -> Form { + Form::vertical() +} + +/// Create a new [`Form`] with a horizontal layout. +pub fn h_form() -> Form { + Form::horizontal() +} + +/// Create a new [`Field`]. +pub fn field() -> Field { + Field::new() +} diff --git a/docs/docs/components/form.md b/docs/docs/components/form.md index 93edc78f..fec7bc69 100644 --- a/docs/docs/components/form.md +++ b/docs/docs/components/form.md @@ -10,7 +10,7 @@ A comprehensive form component that provides structured layout for form fields w ## Import ```rust -use gpui_component::form::{form_field, v_form, h_form, Form, FormField}; +use gpui_component::form::{field, v_form, h_form, Form, Field}; ``` ## Usage @@ -20,12 +20,12 @@ use gpui_component::form::{form_field, v_form, h_form, Form, FormField}; ```rust v_form() .child( - form_field() + field() .label("Name") .child(Input::new(&name_input)) ) .child( - form_field() + field() .label("Email") .child(Input::new(&email_input)) .required(true) @@ -38,12 +38,12 @@ v_form() h_form() .label_width(px(120.)) .child( - form_field() + field() .label("First Name") .child(Input::new(&first_name)) ) .child( - form_field() + field() .label("Last Name") .child(Input::new(&last_name)) ) @@ -55,17 +55,17 @@ h_form() v_form() .columns(2) // Two-column layout .child( - form_field() + field() .label("First Name") .child(Input::new(&first_name)) ) .child( - form_field() + field() .label("Last Name") .child(Input::new(&last_name)) ) .child( - form_field() + field() .label("Bio") .col_span(2) // Span across both columns .child(Input::new(&bio_input)) @@ -79,8 +79,8 @@ v_form() ```rust v_form() .gap(px(12.)) - .child(form_field().label("Name").child(input)) - .child(form_field().label("Email").child(email_input)) + .child(field().label("Name").child(input)) + .child(field().label("Email").child(email_input)) ``` ### Horizontal Layout @@ -88,8 +88,8 @@ v_form() ```rust h_form() .label_width(px(100.)) - .child(form_field().label("Name").child(input)) - .child(form_field().label("Email").child(email_input)) + .child(field().label("Name").child(input)) + .child(field().label("Email").child(email_input)) ``` ### Custom Sizing @@ -98,11 +98,11 @@ h_form() v_form() .large() // Large form size .label_text_size(rems(1.2)) - .child(form_field().label("Title").child(input)) + .child(field().label("Title").child(input)) v_form() .small() // Small form size - .child(form_field().label("Code").child(input)) + .child(field().label("Code").child(input)) ``` ## Form Validation @@ -110,7 +110,7 @@ v_form() ### Required Fields ```rust -form_field() +field() .label("Email") .required(true) // Shows asterisk (*) next to label .child(Input::new(&email_input)) @@ -119,7 +119,7 @@ form_field() ### Field Descriptions ```rust -form_field() +field() .label("Password") .description("Must be at least 8 characters long") .child(Input::new(&password_input)) @@ -128,7 +128,7 @@ form_field() ### Dynamic Descriptions ```rust -form_field() +field() .label("Bio") .description_fn(|_, _| { div().child("Use at most 100 words to describe yourself.") @@ -139,7 +139,7 @@ form_field() ### Field Visibility ```rust -form_field() +field() .label("Admin Settings") .visible(user.is_admin()) // Conditionally show field .child(Switch::new("admin-mode")) @@ -173,11 +173,11 @@ impl FormView { // Form with submit button v_form() - .child(form_field().label("Name").child(Input::new(&self.name_input))) - .child(form_field().label("Email").child(Input::new(&self.email_input))) + .child(field().label("Name").child(Input::new(&self.name_input))) + .child(field().label("Email").child(Input::new(&self.email_input))) .child( - form_field() - .no_label_indent() + field() + .label_indent(false) .child( Button::new("submit") .primary() @@ -191,11 +191,11 @@ v_form() ```rust v_form() - .child(form_field().label("Title").child(Input::new(&title))) - .child(form_field().label("Content").child(Input::new(&content))) + .child(field().label("Title").child(Input::new(&title))) + .child(field().label("Content").child(Input::new(&content))) .child( - form_field() - .no_label_indent() + field() + .label_indent(false) .child( h_flex() .gap_2() @@ -213,7 +213,7 @@ v_form() ```rust v_form() .child( - form_field() + field() .label("Name") .child( h_flex() @@ -223,7 +223,7 @@ v_form() ) ) .child( - form_field() + field() .label("Address") .items_start() // Align to start for multi-line content .child( @@ -243,16 +243,16 @@ v_form() ### Custom Field Components ```rust -form_field() +field() .label("Theme Color") .child(ColorPicker::new(&color_state).small()) -form_field() +field() .label("Birth Date") .description("We'll send you a birthday gift!") .child(DatePicker::new(&date_state)) -form_field() +field() .label("Notifications") .child( v_flex() @@ -268,18 +268,18 @@ form_field() ```rust v_form() .child( - form_field() + field() .label("Account Type") .child(Select::new(&account_type)) ) .child( - form_field() + field() .label("Company Name") .visible(is_business_account) // Show only for business accounts .child(Input::new(&company_name)) ) .child( - form_field() + field() .label("Tax ID") .visible(is_business_account) .required(is_business_account) @@ -294,11 +294,11 @@ v_form() ```rust v_form() .columns(3) // Three-column grid - .child(form_field().label("First").child(input1)) - .child(form_field().label("Second").child(input2)) - .child(form_field().label("Third").child(input3)) + .child(field().label("First").child(input1)) + .child(field().label("Second").child(input2)) + .child(field().label("Third").child(input3)) .child( - form_field() + field() .label("Full Width") .col_span(3) // Spans all three columns .child(Input::new(&full_width)) @@ -310,10 +310,10 @@ v_form() ```rust v_form() .columns(4) - .child(form_field().label("A").child(input_a)) - .child(form_field().label("B").child(input_b)) + .child(field().label("A").child(input_a)) + .child(field().label("B").child(input_b)) .child( - form_field() + field() .label("Positioned") .col_start(1) // Start at column 1 .col_span(2) // Span 2 columns @@ -326,10 +326,10 @@ v_form() ```rust v_form() .columns(if is_mobile { 1 } else { 2 }) - .child(form_field().label("Name").child(name_input)) - .child(form_field().label("Email").child(email_input)) + .child(field().label("Name").child(name_input)) + .child(field().label("Email").child(email_input)) .child( - form_field() + field() .label("Bio") .when(!is_mobile, |field| field.col_span(2)) .child(bio_input) @@ -355,9 +355,9 @@ impl Render for RegistrationForm { v_form() .large() .child( - form_field() + field() .label("Personal Information") - .no_label_indent() + .label_indent(false) .child( h_flex() .gap_3() @@ -376,27 +376,27 @@ impl Render for RegistrationForm { ) ) .child( - form_field() + field() .label("Email") .required(true) .child(Input::new(&self.email)) ) .child( - form_field() + field() .label("Password") .required(true) .description("Must be at least 8 characters") .child(Input::new(&self.password)) ) .child( - form_field() + field() .label("Confirm Password") .required(true) .child(Input::new(&self.confirm_password)) ) .child( - form_field() - .no_label_indent() + field() + .label_indent(false) .child( Checkbox::new("terms") .label("I agree to the Terms of Service") @@ -408,8 +408,8 @@ impl Render for RegistrationForm { ) ) .child( - form_field() - .no_label_indent() + field() + .label_indent(false) .child( Button::new("register") .primary() @@ -428,54 +428,54 @@ impl Render for RegistrationForm { v_form() .column(2) .child( - form_field() + field() .label("Profile") - .no_label_indent() + .label_indent(false) .col_span(2) .child(Divider::horizontal()) ) .child( - form_field() + field() .label("Display Name") .child(Input::new(&display_name)) ) .child( - form_field() + field() .label("Email") .child(Input::new(&email)) ) .child( - form_field() + field() .label("Bio") .col_span(2) .items_start() .child(Input::new(&bio)) ) .child( - form_field() + field() .label("Preferences") - .no_label_indent() + .label_indent(false) .col_span(2) .child(Divider::horizontal()) ) .child( - form_field() + field() .label("Theme") .child(Select::new(&theme_state)) ) .child( - form_field() + field() .label("Language") .child(Select::new(&language_state)) ) .child( - form_field() - .no_label_indent() + field() + .label_indent(false) .child(Switch::new("notifications").label("Enable notifications")) ) .child( - form_field() - .no_label_indent() + field() + .label_indent(false) .child(Switch::new("marketing").label("Marketing emails")) ) ``` @@ -485,7 +485,7 @@ v_form() ```rust v_form() .child( - form_field() + field() .label("Contact Information") .child( h_flex() @@ -503,18 +503,18 @@ v_form() ) ) .child( - form_field() + field() .label("Email") .required(true) .child(Input::new(&email_input)) ) .child( - form_field() + field() .label("Subject") .child(Select::new(&subject_state)) ) .child( - form_field() + field() .label("Message") .required(true) .items_start() @@ -522,8 +522,8 @@ v_form() .child(Input::new(&message_input)) ) .child( - form_field() - .no_label_indent() + field() + .label_indent(false) .child( h_flex() .gap_2()