form: Add Form and FormField. (#565)
<img width="899" alt="image" src="https://github.com/user-attachments/assets/842dac65-36c3-4f3a-b55a-59d204d9a93c" /> <img width="901" alt="image" src="https://github.com/user-attachments/assets/616b7a3a-4a84-4d27-b554-63ebd5213ad7" />
This commit is contained in:
parent
4028f46cb9
commit
a89e2430d3
8 changed files with 651 additions and 16 deletions
|
|
@ -206,7 +206,7 @@ impl DropdownStory {
|
|||
}
|
||||
|
||||
impl FocusableCycle for DropdownStory {
|
||||
fn cycle_focus_handles(&self, cx: &mut ViewContext<Self>) -> Vec<gpui::FocusHandle>
|
||||
fn cycle_focus_handles(&self, cx: &mut WindowContext) -> Vec<gpui::FocusHandle>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
|
|
|
|||
202
crates/story/src/form_story.rs
Normal file
202
crates/story/src/form_story.rs
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
use gpui::{
|
||||
actions, div, Axis, InteractiveElement, IntoElement, ParentElement as _, Render, Styled, View,
|
||||
ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use ui::{
|
||||
button::{Button, ButtonGroup},
|
||||
date_picker::DatePicker,
|
||||
divider::Divider,
|
||||
form::{form_field, v_form},
|
||||
h_flex,
|
||||
input::TextInput,
|
||||
prelude::FluentBuilder as _,
|
||||
switch::Switch,
|
||||
v_flex, AxisExt, FocusableCycle, Selectable, Sizable, Size,
|
||||
};
|
||||
|
||||
actions!(input_story, [Tab, TabPrev]);
|
||||
|
||||
pub struct FormStory {
|
||||
name_input: View<TextInput>,
|
||||
email_input: View<TextInput>,
|
||||
bio_input: View<TextInput>,
|
||||
subscribe_email: bool,
|
||||
date_picker: View<DatePicker>,
|
||||
layout: Axis,
|
||||
size: Size,
|
||||
}
|
||||
|
||||
impl super::Story for FormStory {
|
||||
fn title() -> &'static str {
|
||||
"FormStory"
|
||||
}
|
||||
|
||||
fn closable() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn new_view(cx: &mut WindowContext) -> View<impl gpui::FocusableView> {
|
||||
Self::view(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl FormStory {
|
||||
pub fn view(cx: &mut WindowContext) -> View<Self> {
|
||||
cx.new_view(Self::new)
|
||||
}
|
||||
|
||||
fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
let name_input = cx.new_view(|cx| {
|
||||
let mut input = TextInput::new(cx).cleanable();
|
||||
input.set_text("Jason Lee", cx);
|
||||
input
|
||||
});
|
||||
|
||||
let email_input = cx.new_view(|cx| TextInput::new(cx).placeholder("Enter text here..."));
|
||||
let bio_input = cx.new_view(|cx| {
|
||||
let mut input = TextInput::new(cx)
|
||||
.multi_line()
|
||||
.rows(10)
|
||||
.placeholder("Enter text here...");
|
||||
input.set_text("Hello 世界,this is GPUI component.", cx);
|
||||
input
|
||||
});
|
||||
let date_picker = cx.new_view(|cx| DatePicker::new("birthday", cx));
|
||||
|
||||
Self {
|
||||
name_input,
|
||||
email_input,
|
||||
bio_input,
|
||||
date_picker,
|
||||
subscribe_email: false,
|
||||
layout: Axis::Vertical,
|
||||
size: Size::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableCycle for FormStory {
|
||||
fn cycle_focus_handles(&self, cx: &mut WindowContext) -> Vec<gpui::FocusHandle>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
vec![
|
||||
self.name_input.focus_handle(cx),
|
||||
self.email_input.focus_handle(cx),
|
||||
self.bio_input.focus_handle(cx),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
impl gpui::FocusableView for FormStory {
|
||||
fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
|
||||
self.name_input.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for FormStory {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.id("form-story")
|
||||
.size_full()
|
||||
.p_4()
|
||||
.justify_start()
|
||||
.gap_3()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_3()
|
||||
.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();
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
ButtonGroup::new("size")
|
||||
.small()
|
||||
.child(
|
||||
Button::new("large")
|
||||
.selected(self.size == Size::Large)
|
||||
.child("Large"),
|
||||
)
|
||||
.child(
|
||||
Button::new("medium")
|
||||
.child("Medium")
|
||||
.selected(self.size == Size::Medium),
|
||||
)
|
||||
.child(
|
||||
Button::new("small")
|
||||
.child("Small")
|
||||
.selected(self.size == Size::Small),
|
||||
)
|
||||
.on_click(cx.listener(|this, selecteds: &Vec<usize>, cx| {
|
||||
if selecteds.contains(&0) {
|
||||
this.size = Size::Large;
|
||||
} else if selecteds.contains(&1) {
|
||||
this.size = Size::Medium;
|
||||
} else if selecteds.contains(&2) {
|
||||
this.size = Size::Small;
|
||||
}
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(Divider::horizontal())
|
||||
.child(
|
||||
v_form()
|
||||
.layout(self.layout)
|
||||
.with_size(self.size)
|
||||
.child(
|
||||
form_field()
|
||||
.label_fn(|_| "Name")
|
||||
.child(self.name_input.clone()),
|
||||
)
|
||||
.child(
|
||||
form_field()
|
||||
.label("Email")
|
||||
.child(self.email_input.clone())
|
||||
.required(true),
|
||||
)
|
||||
.child(
|
||||
form_field()
|
||||
.label("Bio")
|
||||
.when(self.layout.is_vertical(), |this| this.items_start())
|
||||
.child(self.bio_input.clone())
|
||||
.description_fn(|_| {
|
||||
div().child("Use at most 100 words to describe yourself.")
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
form_field()
|
||||
.no_label_indent()
|
||||
.child("This is a full width form field."),
|
||||
)
|
||||
.child(
|
||||
form_field()
|
||||
.label("Birthday")
|
||||
.child(self.date_picker.clone())
|
||||
.description("Select your birthday, we will send you a gift."),
|
||||
)
|
||||
.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();
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -323,7 +323,7 @@ impl InputStory {
|
|||
}
|
||||
|
||||
impl FocusableCycle for InputStory {
|
||||
fn cycle_focus_handles(&self, cx: &mut ViewContext<Self>) -> Vec<FocusHandle> {
|
||||
fn cycle_focus_handles(&self, cx: &mut WindowContext) -> Vec<FocusHandle> {
|
||||
[
|
||||
self.input1.focus_handle(cx),
|
||||
self.input2.focus_handle(cx),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ mod assets;
|
|||
mod button_story;
|
||||
mod calendar_story;
|
||||
mod dropdown_story;
|
||||
mod form_story;
|
||||
mod icon_story;
|
||||
mod image_story;
|
||||
mod input_story;
|
||||
|
|
@ -25,6 +26,14 @@ pub use accordion_story::AccordionStory;
|
|||
pub use button_story::ButtonStory;
|
||||
pub use calendar_story::CalendarStory;
|
||||
pub use dropdown_story::DropdownStory;
|
||||
pub use form_story::FormStory;
|
||||
|
||||
use gpui::{
|
||||
actions, div, prelude::FluentBuilder as _, px, AnyElement, AnyView, AppContext, Context as _,
|
||||
Div, EventEmitter, FocusableView, Global, Hsla, InteractiveElement, IntoElement, Model,
|
||||
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled as _, View,
|
||||
ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
pub use icon_story::IconStory;
|
||||
pub use image_story::ImageStory;
|
||||
pub use input_story::InputStory;
|
||||
|
|
@ -42,13 +51,6 @@ pub use text_story::TextStory;
|
|||
pub use tooltip_story::TooltipStory;
|
||||
pub use webview_story::WebViewStory;
|
||||
|
||||
use gpui::{
|
||||
actions, div, prelude::FluentBuilder as _, px, AnyElement, AnyView, AppContext, Context as _,
|
||||
Div, EventEmitter, FocusableView, Global, Hsla, InteractiveElement, IntoElement, Model,
|
||||
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled as _, View,
|
||||
ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
|
||||
use ui::{
|
||||
button::Button,
|
||||
divider::Divider,
|
||||
|
|
@ -298,6 +300,7 @@ impl StoryState {
|
|||
"WebViewStory" => story!(WebViewStory),
|
||||
"AccordionStory" => story!(AccordionStory),
|
||||
"SidebarStory" => story!(SidebarStory),
|
||||
"FormStory" => story!(FormStory),
|
||||
_ => {
|
||||
unreachable!("Invalid story klass: {}", self.story_klass)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ use prelude::FluentBuilder as _;
|
|||
use serde::Deserialize;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use story::{
|
||||
AccordionStory, AppState, Assets, ButtonStory, CalendarStory, DropdownStory, IconStory,
|
||||
ImageStory, InputStory, ListStory, ModalStory, PopupStory, ProgressStory, ResizableStory,
|
||||
ScrollableStory, SidebarStory, StoryContainer, SwitchStory, TableStory, TextStory,
|
||||
TooltipStory,
|
||||
AccordionStory, AppState, Assets, ButtonStory, CalendarStory, DropdownStory, FormStory,
|
||||
IconStory, ImageStory, InputStory, ListStory, ModalStory, PopupStory, ProgressStory,
|
||||
ResizableStory, ScrollableStory, SidebarStory, StoryContainer, SwitchStory, TableStory,
|
||||
TextStory, TooltipStory,
|
||||
};
|
||||
use ui::{
|
||||
badge::Badge,
|
||||
|
|
@ -318,6 +318,7 @@ impl StoryWorkspace {
|
|||
Arc::new(StoryContainer::panel::<ScrollableStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<AccordionStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<SidebarStory>(cx)),
|
||||
Arc::new(StoryContainer::panel::<FormStory>(cx)),
|
||||
// Arc::new(StoryContainer::panel::<WebViewStory>(cx)),
|
||||
],
|
||||
None,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use gpui::{FocusHandle, ViewContext};
|
||||
use gpui::{FocusHandle, WindowContext};
|
||||
|
||||
/// A trait for views that can cycle focus between its children.
|
||||
///
|
||||
|
|
@ -8,13 +8,13 @@ use gpui::{FocusHandle, ViewContext};
|
|||
/// should be cycled, and the cycle will follow the order of the list.
|
||||
pub trait FocusableCycle {
|
||||
/// Returns a list of focus handles that should be cycled.
|
||||
fn cycle_focus_handles(&self, cx: &mut ViewContext<Self>) -> Vec<FocusHandle>
|
||||
fn cycle_focus_handles(&self, cx: &mut WindowContext) -> Vec<FocusHandle>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
/// Cycles focus between the focus handles returned by `cycle_focus_handles`.
|
||||
/// If `is_next` is `true`, it will cycle to the next focus handle, otherwise it will cycle to prev.
|
||||
fn cycle_focus(&self, is_next: bool, cx: &mut ViewContext<Self>)
|
||||
fn cycle_focus(&self, is_next: bool, cx: &mut WindowContext)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
|
|
|
|||
428
crates/ui/src/form.rs
Normal file
428
crates/ui/src/form.rs
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
use std::rc::{Rc, Weak};
|
||||
|
||||
use gpui::{
|
||||
div, prelude::FluentBuilder as _, px, AlignItems, AnyElement, AnyView, Axis, Div, Element,
|
||||
ElementId, FocusHandle, InteractiveElement as _, IntoElement, ParentElement, Pixels, Rems,
|
||||
RenderOnce, SharedString, Styled, WindowContext,
|
||||
};
|
||||
|
||||
use crate::{h_flex, v_flex, ActiveTheme as _, AxisExt, FocusableCycle, 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()
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Form {
|
||||
fields: Vec<FormField>,
|
||||
props: FieldProps,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct FieldProps {
|
||||
size: Size,
|
||||
label_width: Option<Pixels>,
|
||||
label_text_size: Option<Rems>,
|
||||
layout: Axis,
|
||||
/// Field gap
|
||||
gap: Option<Pixels>,
|
||||
}
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<FormField>) -> Self {
|
||||
self.fields.push(field.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Add multiple children to the form.
|
||||
pub fn children(mut self, fields: impl IntoIterator<Item = FormField>) -> Self {
|
||||
self.fields.extend(fields);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Sizable for Form {
|
||||
fn with_size(mut self, size: impl Into<Size>) -> Self {
|
||||
self.props.size = size.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableCycle for Form {
|
||||
fn cycle_focus_handles(&self, _: &mut WindowContext) -> Vec<FocusHandle>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.fields
|
||||
.iter()
|
||||
.filter_map(|item| item.focus_handle.clone())
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum FieldBuilder {
|
||||
String(SharedString),
|
||||
Element(Rc<dyn Fn(&mut WindowContext) -> AnyElement>),
|
||||
View(AnyView),
|
||||
}
|
||||
|
||||
impl Default for FieldBuilder {
|
||||
fn default() -> Self {
|
||||
Self::String(SharedString::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AnyView> for FieldBuilder {
|
||||
fn from(view: AnyView) -> Self {
|
||||
Self::View(view)
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for FieldBuilder {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
match self {
|
||||
FieldBuilder::String(value) => value.into_any_element(),
|
||||
FieldBuilder::Element(builder) => builder(cx),
|
||||
FieldBuilder::View(view) => view.into_any(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for FieldBuilder {
|
||||
fn from(value: &'static str) -> Self {
|
||||
Self::String(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for FieldBuilder {
|
||||
fn from(value: String) -> Self {
|
||||
Self::String(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SharedString> for FieldBuilder {
|
||||
fn from(value: SharedString) -> Self {
|
||||
Self::String(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct FormField {
|
||||
id: ElementId,
|
||||
form: Weak<Form>,
|
||||
label: Option<FieldBuilder>,
|
||||
no_label_indent: bool,
|
||||
focus_handle: Option<FocusHandle>,
|
||||
description: Option<FieldBuilder>,
|
||||
/// Used to render the actual form field, e.g.: TextInput, Switch...
|
||||
child: Div,
|
||||
visible: bool,
|
||||
required: bool,
|
||||
/// Alignment of the form field.
|
||||
align_items: Option<AlignItems>,
|
||||
props: FieldProps,
|
||||
}
|
||||
|
||||
impl FormField {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
id: 0.into(),
|
||||
form: Weak::new(),
|
||||
label: None,
|
||||
description: None,
|
||||
child: div(),
|
||||
visible: true,
|
||||
required: false,
|
||||
no_label_indent: false,
|
||||
focus_handle: None,
|
||||
align_items: None,
|
||||
props: FieldProps::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the label for the form field.
|
||||
pub fn label(mut self, label: impl Into<FieldBuilder>) -> Self {
|
||||
self.label = Some(label.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets not indent with the label width (in Horizontal layout).
|
||||
///
|
||||
/// 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;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the label for the form field using a function.
|
||||
pub fn label_fn<F, E>(mut self, label: F) -> Self
|
||||
where
|
||||
E: IntoElement,
|
||||
F: Fn(&mut WindowContext) -> E + 'static,
|
||||
{
|
||||
self.label = Some(FieldBuilder::Element(Rc::new(move |cx| {
|
||||
label(cx).into_any_element()
|
||||
})));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the description for the form field.
|
||||
pub fn description(mut self, description: impl Into<FieldBuilder>) -> Self {
|
||||
self.description = Some(description.into());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the description for the form field using a function.
|
||||
pub fn description_fn<F, E>(mut self, description: F) -> Self
|
||||
where
|
||||
E: IntoElement,
|
||||
F: Fn(&mut WindowContext) -> E + 'static,
|
||||
{
|
||||
self.description = Some(FieldBuilder::Element(Rc::new(move |cx| {
|
||||
description(cx).into_any_element()
|
||||
})));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the visibility of the form field, default is `true`.
|
||||
pub fn visible(mut self, visible: bool) -> Self {
|
||||
self.visible = visible;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the required status of the form field, default is `false`.
|
||||
pub fn required(mut self, required: bool) -> Self {
|
||||
self.required = required;
|
||||
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);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn parent(mut self, form: &Rc<Form>) -> 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 {
|
||||
self.id = ix.into();
|
||||
self.props = props;
|
||||
self
|
||||
}
|
||||
|
||||
/// Align the form field items to the start, this is the default.
|
||||
pub fn items_start(mut self) -> Self {
|
||||
self.align_items = Some(AlignItems::Start);
|
||||
self
|
||||
}
|
||||
|
||||
/// Align the form field items to the end.
|
||||
pub fn items_end(mut self) -> Self {
|
||||
self.align_items = Some(AlignItems::End);
|
||||
self
|
||||
}
|
||||
|
||||
/// Align the form field items to the center.
|
||||
pub fn items_center(mut self) -> Self {
|
||||
self.align_items = Some(AlignItems::Center);
|
||||
self
|
||||
}
|
||||
}
|
||||
impl ParentElement for FormField {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.child.extend(elements);
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for FormField {
|
||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||
let layout = self.props.layout;
|
||||
|
||||
let label_width = if layout.is_vertical() {
|
||||
None
|
||||
} else {
|
||||
self.props.label_width
|
||||
};
|
||||
let has_label = !self.no_label_indent;
|
||||
|
||||
#[inline]
|
||||
fn wrap_div(layout: Axis) -> Div {
|
||||
if layout.is_vertical() {
|
||||
v_flex()
|
||||
} else {
|
||||
h_flex()
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn wrap_label(label_width: Option<Pixels>) -> Div {
|
||||
h_flex()
|
||||
.truncate()
|
||||
.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 inner_gap = if layout.is_horizontal() {
|
||||
gap
|
||||
} else {
|
||||
gap / 2.
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.gap(gap / 2.)
|
||||
.child(
|
||||
// This warp for aligning the Label + Input
|
||||
wrap_div(layout)
|
||||
.id(self.id)
|
||||
.gap(inner_gap)
|
||||
.when_some(self.align_items, |this, align| {
|
||||
this.map(|this| match align {
|
||||
AlignItems::Start => this.items_start(),
|
||||
AlignItems::End => this.items_end(),
|
||||
AlignItems::Center => this.items_center(),
|
||||
AlignItems::Baseline => this.items_baseline(),
|
||||
_ => this,
|
||||
})
|
||||
})
|
||||
.when(has_label, |this| {
|
||||
// Label
|
||||
this.child(
|
||||
wrap_label(label_width)
|
||||
.text_sm()
|
||||
.when_some(self.props.label_text_size, |this, size| {
|
||||
this.text_size(size)
|
||||
})
|
||||
.font_medium()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.when_some(self.label, |this, builder| {
|
||||
this.child(builder.render(cx)).when(self.required, |this| {
|
||||
this.child(div().text_color(cx.theme().danger).child("*"))
|
||||
})
|
||||
}),
|
||||
)
|
||||
})
|
||||
.child(div().w_full().child(self.child)),
|
||||
)
|
||||
.child(
|
||||
// Other
|
||||
wrap_div(layout)
|
||||
.gap(inner_gap)
|
||||
.when(has_label && layout.is_horizontal(), |this| {
|
||||
this.child(
|
||||
// Empty for spacing to align with the input
|
||||
wrap_label(label_width),
|
||||
)
|
||||
})
|
||||
.when_some(self.description, |this, builder| {
|
||||
this.child(
|
||||
div()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(builder.render(cx)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
impl RenderOnce for Form {
|
||||
fn render(self, _: &mut WindowContext) -> 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(gap).children(
|
||||
self.fields
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(ix, field)| field.props(ix, props)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -23,6 +23,7 @@ pub mod divider;
|
|||
pub mod dock;
|
||||
pub mod drawer;
|
||||
pub mod dropdown;
|
||||
pub mod form;
|
||||
pub mod history;
|
||||
pub mod indicator;
|
||||
pub mod input;
|
||||
|
|
|
|||
Loading…
Reference in a new issue