form: Add support for multi-column layout (#1250)

| Vertical | Horizontal |
| - | - |
| <img width="1642" height="1106" alt="SCR-20250916-nnry"
src="https://github.com/user-attachments/assets/279eb192-c660-449e-931e-1d7ff79ac69f"
/> | <img width="1642" height="1106" alt="image"
src="https://github.com/user-attachments/assets/289a6672-d82d-4f6f-a267-592874ccbe42"
/> |
This commit is contained in:
Floyd Wang 2025-09-16 16:28:43 +08:00 committed by GitHub
parent 717c83957d
commit 731ab958f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 111 additions and 46 deletions

View file

@ -26,6 +26,7 @@ pub struct FormStory {
date: Entity<DatePickerState>, date: Entity<DatePickerState>,
layout: Axis, layout: Axis,
size: Size, size: Size,
column: u16,
} }
impl super::Story for FormStory { impl super::Story for FormStory {
@ -89,6 +90,7 @@ impl FormStory {
subscribe_email: false, subscribe_email: false,
layout: Axis::Vertical, layout: Axis::Vertical,
size: Size::default(), size: Size::default(),
column: 1,
} }
} }
} }
@ -114,6 +116,9 @@ impl Focusable for FormStory {
impl Render for FormStory { impl Render for FormStory {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_multi_column = self.column > 1;
let is_horizontal = self.layout.is_horizontal();
v_flex() v_flex()
.id("form-story") .id("form-story")
.size_full() .size_full()
@ -126,17 +131,34 @@ impl Render for FormStory {
.flex_wrap() .flex_wrap()
.justify_between() .justify_between()
.child( .child(
Switch::new("layout") h_flex()
.checked(self.layout.is_horizontal()) .gap_x_3()
.label("Horizontal") .child(
.on_click(cx.listener(|this, checked: &bool, _, cx| { Switch::new("layout")
if *checked { .checked(self.layout.is_horizontal())
this.layout = Axis::Horizontal; .label("Horizontal")
} else { .on_click(cx.listener(|this, checked: &bool, _, cx| {
this.layout = Axis::Vertical; if *checked {
} this.layout = Axis::Horizontal;
cx.notify(); } else {
})), this.layout = Axis::Vertical;
}
cx.notify();
})),
)
.child(
Switch::new("column")
.checked(self.column > 1)
.label("Multi Column")
.on_click(cx.listener(|this, checked: &bool, _, cx| {
if *checked {
this.column = 2;
} else {
this.column = 1;
}
cx.notify();
})),
),
) )
.child( .child(
ButtonGroup::new("size") ButtonGroup::new("size")
@ -174,6 +196,8 @@ impl Render for FormStory {
v_form() v_form()
.layout(self.layout) .layout(self.layout)
.with_size(self.size) .with_size(self.size)
.column(self.column)
.label_width(px(if is_multi_column { 100. } else { 140. }))
.child( .child(
form_field().label_fn(|_, _| "Name").child( form_field().label_fn(|_, _| "Name").child(
h_flex() h_flex()
@ -211,46 +235,60 @@ impl Render for FormStory {
.child( .child(
form_field() form_field()
.no_label_indent() .no_label_indent()
.when(is_multi_column, |this| this.col_span(2))
.child("This is a full width form field."), .child("This is a full width form field."),
) )
.child( .child(
form_field() form_field()
.label("Please select your birthday") .label("Please select your birthday")
.child(DatePicker::new(&self.date)) .description("Select your birthday, we will send you a gift.")
.description("Select your birthday, we will send you a gift."), .when(is_multi_column, |this| this.col_span(2))
.child(DatePicker::new(&self.date)),
) )
.child( .child(
form_field().child( form_field()
Switch::new("subscribe-newsletter") .when(is_horizontal && is_multi_column, |this| {
.label("Subscribe our newsletter") this.no_label_indent()
.checked(self.subscribe_email) })
.on_click(cx.listener(|this, checked: &bool, _, cx| { .child(
this.subscribe_email = *checked; Switch::new("subscribe-newsletter")
cx.notify(); .label("Subscribe our newsletter")
})), .checked(self.subscribe_email)
), .on_click(cx.listener(|this, checked: &bool, _, cx| {
this.subscribe_email = *checked;
cx.notify();
})),
),
) )
.child( .child(
form_field().child( form_field()
ColorPicker::new(&self.color_state) .when(is_horizontal && is_multi_column, |this| {
.small() this.no_label_indent()
.label("Theme color"), })
), .child(
ColorPicker::new(&self.color_state)
.small()
.label("Theme color"),
),
) )
.child( .child(
form_field().child( form_field()
Checkbox::new("use-vertical-layout") .when(is_horizontal && is_multi_column, |this| {
.label("Vertical layout") this.no_label_indent()
.checked(self.layout.is_vertical()) })
.on_click(cx.listener(|this, checked: &bool, _, cx| { .child(
this.layout = if *checked { Checkbox::new("use-vertical-layout")
Axis::Vertical .label("Vertical layout")
} else { .checked(self.layout.is_vertical())
Axis::Horizontal .on_click(cx.listener(|this, checked: &bool, _, cx| {
}; this.layout = if *checked {
cx.notify(); Axis::Vertical
})), } else {
), Axis::Horizontal
};
cx.notify();
})),
),
), ),
) )
} }

View file

@ -37,6 +37,7 @@ struct FieldProps {
layout: Axis, layout: Axis,
/// Field gap /// Field gap
gap: Option<Pixels>, gap: Option<Pixels>,
column: u16,
} }
impl Default for FieldProps { impl Default for FieldProps {
@ -47,6 +48,7 @@ impl Default for FieldProps {
layout: Axis::Vertical, layout: Axis::Vertical,
size: Size::default(), size: Size::default(),
gap: None, gap: None,
column: 1,
} }
} }
} }
@ -104,6 +106,14 @@ impl Form {
self.fields.extend(fields); self.fields.extend(fields);
self self
} }
/// Set the column count for the form.
///
/// Default is 1.
pub fn column(mut self, column: u16) -> Self {
self.props.column = column;
self
}
} }
impl Sizable for Form { impl Sizable for Form {
@ -186,6 +196,7 @@ pub struct FormField {
/// Alignment of the form field. /// Alignment of the form field.
align_items: Option<AlignItems>, align_items: Option<AlignItems>,
props: FieldProps, props: FieldProps,
col_span: u16,
} }
impl FormField { impl FormField {
@ -202,6 +213,7 @@ impl FormField {
focus_handle: None, focus_handle: None,
align_items: None, align_items: None,
props: FieldProps::default(), props: FieldProps::default(),
col_span: 1,
} }
} }
@ -302,6 +314,14 @@ impl FormField {
self.align_items = Some(AlignItems::Center); self.align_items = Some(AlignItems::Center);
self self
} }
/// Set the column span for the form field.
///
/// Default is 1.
pub fn col_span(mut self, col_span: u16) -> Self {
self.col_span = col_span;
self
}
} }
impl ParentElement for FormField { impl ParentElement for FormField {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) { fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
@ -351,6 +371,7 @@ impl RenderOnce for FormField {
v_flex() v_flex()
.flex_1() .flex_1()
.gap(gap / 2.) .gap(gap / 2.)
.col_span(self.col_span)
.child( .child(
// This warp for aligning the Label + Input // This warp for aligning the Label + Input
wrap_div(layout) wrap_div(layout)
@ -433,11 +454,17 @@ impl RenderOnce for Form {
_ => px(8.), _ => px(8.),
}; };
v_flex().w_full().gap(gap).children( v_flex()
self.fields .w_full()
.into_iter() .gap_x(gap * 3.)
.enumerate() .gap_y(gap)
.map(|(ix, field)| field.props(ix, props)), .grid()
) .grid_cols(props.column)
.children(
self.fields
.into_iter()
.enumerate()
.map(|(ix, field)| field.props(ix, props)),
)
} }
} }