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>,
layout: Axis,
size: Size,
column: u16,
}
impl super::Story for FormStory {
@ -89,6 +90,7 @@ impl FormStory {
subscribe_email: false,
layout: Axis::Vertical,
size: Size::default(),
column: 1,
}
}
}
@ -114,6 +116,9 @@ impl Focusable for FormStory {
impl Render for FormStory {
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()
.id("form-story")
.size_full()
@ -126,17 +131,34 @@ impl Render for FormStory {
.flex_wrap()
.justify_between()
.child(
Switch::new("layout")
.checked(self.layout.is_horizontal())
.label("Horizontal")
.on_click(cx.listener(|this, checked: &bool, _, cx| {
if *checked {
this.layout = Axis::Horizontal;
} else {
this.layout = Axis::Vertical;
}
cx.notify();
})),
h_flex()
.gap_x_3()
.child(
Switch::new("layout")
.checked(self.layout.is_horizontal())
.label("Horizontal")
.on_click(cx.listener(|this, checked: &bool, _, cx| {
if *checked {
this.layout = Axis::Horizontal;
} 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(
ButtonGroup::new("size")
@ -174,6 +196,8 @@ impl Render for FormStory {
v_form()
.layout(self.layout)
.with_size(self.size)
.column(self.column)
.label_width(px(if is_multi_column { 100. } else { 140. }))
.child(
form_field().label_fn(|_, _| "Name").child(
h_flex()
@ -211,46 +235,60 @@ impl Render for FormStory {
.child(
form_field()
.no_label_indent()
.when(is_multi_column, |this| this.col_span(2))
.child("This is a full width form field."),
)
.child(
form_field()
.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(
form_field().child(
Switch::new("subscribe-newsletter")
.label("Subscribe our newsletter")
.checked(self.subscribe_email)
.on_click(cx.listener(|this, checked: &bool, _, cx| {
this.subscribe_email = *checked;
cx.notify();
})),
),
form_field()
.when(is_horizontal && is_multi_column, |this| {
this.no_label_indent()
})
.child(
Switch::new("subscribe-newsletter")
.label("Subscribe our newsletter")
.checked(self.subscribe_email)
.on_click(cx.listener(|this, checked: &bool, _, cx| {
this.subscribe_email = *checked;
cx.notify();
})),
),
)
.child(
form_field().child(
ColorPicker::new(&self.color_state)
.small()
.label("Theme color"),
),
form_field()
.when(is_horizontal && is_multi_column, |this| {
this.no_label_indent()
})
.child(
ColorPicker::new(&self.color_state)
.small()
.label("Theme color"),
),
)
.child(
form_field().child(
Checkbox::new("use-vertical-layout")
.label("Vertical layout")
.checked(self.layout.is_vertical())
.on_click(cx.listener(|this, checked: &bool, _, cx| {
this.layout = if *checked {
Axis::Vertical
} else {
Axis::Horizontal
};
cx.notify();
})),
),
form_field()
.when(is_horizontal && is_multi_column, |this| {
this.no_label_indent()
})
.child(
Checkbox::new("use-vertical-layout")
.label("Vertical layout")
.checked(self.layout.is_vertical())
.on_click(cx.listener(|this, checked: &bool, _, cx| {
this.layout = if *checked {
Axis::Vertical
} else {
Axis::Horizontal
};
cx.notify();
})),
),
),
)
}

View file

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