bevy_ui_builders/form/
builder.rs

1//! FormBuilder implementation
2
3use bevy::prelude::*;
4use std::collections::HashMap;
5use crate::styles::{colors, dimensions};
6use super::types::{
7    FormField, FieldType, ValidationRule, FormLayout, ValidationTrigger,
8    FormRoot, FormSubmitButton
9};
10use super::field::spawn_form_field;
11
12/// Builder for creating complete forms
13pub struct FormBuilder {
14    id: String,
15    title: Option<String>,
16    fields: Vec<FormField>,
17    submit_text: String,
18    cancel_text: Option<String>,
19    layout: FormLayout,
20    width: Val,
21    validation_on: ValidationTrigger,
22}
23
24impl FormBuilder {
25    /// Create a new form builder
26    pub fn new(id: impl Into<String>) -> Self {
27        Self {
28            id: id.into(),
29            title: None,
30            fields: Vec::new(),
31            submit_text: "Submit".to_string(),
32            cancel_text: None,
33            layout: FormLayout::Vertical,
34            width: Val::Px(400.0),
35            validation_on: ValidationTrigger::OnBlur,
36        }
37    }
38
39    /// Set the form title
40    pub fn title(mut self, title: impl Into<String>) -> Self {
41        self.title = Some(title.into());
42        self
43    }
44
45    /// Add a text field
46    pub fn text_field(mut self, name: impl Into<String>, label: impl Into<String>) -> Self {
47        self.fields.push(FormField {
48            name: name.into(),
49            label: label.into(),
50            field_type: FieldType::Text,
51            validations: Vec::new(),
52            placeholder: None,
53            help_text: None,
54            disabled: false,
55            default_value: None,
56        });
57        self
58    }
59
60    /// Add a password field
61    pub fn password_field(mut self, name: impl Into<String>, label: impl Into<String>) -> Self {
62        self.fields.push(FormField {
63            name: name.into(),
64            label: label.into(),
65            field_type: FieldType::Password,
66            validations: Vec::new(),
67            placeholder: None,
68            help_text: None,
69            disabled: false,
70            default_value: None,
71        });
72        self
73    }
74
75    /// Add an email field
76    pub fn email_field(mut self, name: impl Into<String>, label: impl Into<String>) -> Self {
77        let field = FormField {
78            name: name.into(),
79            label: label.into(),
80            field_type: FieldType::Email,
81            validations: vec![ValidationRule::Email],
82            placeholder: Some("email@example.com".to_string()),
83            help_text: None,
84            disabled: false,
85            default_value: None,
86        };
87        self.fields.push(field);
88        self
89    }
90
91    /// Add a number field
92    pub fn number_field(
93        mut self,
94        name: impl Into<String>,
95        label: impl Into<String>,
96        min: Option<f32>,
97        max: Option<f32>,
98    ) -> Self {
99        self.fields.push(FormField {
100            name: name.into(),
101            label: label.into(),
102            field_type: FieldType::Number { min, max },
103            validations: Vec::new(),
104            placeholder: None,
105            help_text: None,
106            disabled: false,
107            default_value: None,
108        });
109        self
110    }
111
112    /// Add a slider field
113    pub fn slider_field(
114        mut self,
115        name: impl Into<String>,
116        label: impl Into<String>,
117        min: f32,
118        max: f32,
119    ) -> Self {
120        self.fields.push(FormField {
121            name: name.into(),
122            label: label.into(),
123            field_type: FieldType::Slider { min, max, step: None },
124            validations: Vec::new(),
125            placeholder: None,
126            help_text: None,
127            disabled: false,
128            default_value: Some(min.to_string()),
129        });
130        self
131    }
132
133    /// Add a dropdown field
134    pub fn dropdown_field(
135        mut self,
136        name: impl Into<String>,
137        label: impl Into<String>,
138        options: Vec<String>,
139    ) -> Self {
140        self.fields.push(FormField {
141            name: name.into(),
142            label: label.into(),
143            field_type: FieldType::Dropdown { options },
144            validations: Vec::new(),
145            placeholder: Some("Select an option".to_string()),
146            help_text: None,
147            disabled: false,
148            default_value: None,
149        });
150        self
151    }
152
153    /// Add a checkbox field
154    pub fn checkbox_field(mut self, name: impl Into<String>, label: impl Into<String>) -> Self {
155        self.fields.push(FormField {
156            name: name.into(),
157            label: label.into(),
158            field_type: FieldType::Checkbox,
159            validations: Vec::new(),
160            placeholder: None,
161            help_text: None,
162            disabled: false,
163            default_value: Some("false".to_string()),
164        });
165        self
166    }
167
168    /// Make the last added field required
169    pub fn required(mut self) -> Self {
170        if let Some(field) = self.fields.last_mut() {
171            field.validations.push(ValidationRule::Required);
172        }
173        self
174    }
175
176    /// Add validation to the last field
177    pub fn validate(mut self, rule: ValidationRule) -> Self {
178        if let Some(field) = self.fields.last_mut() {
179            field.validations.push(rule);
180        }
181        self
182    }
183
184    /// Add placeholder to the last field
185    pub fn placeholder(mut self, text: impl Into<String>) -> Self {
186        if let Some(field) = self.fields.last_mut() {
187            field.placeholder = Some(text.into());
188        }
189        self
190    }
191
192    /// Add help text to the last field
193    pub fn help_text(mut self, text: impl Into<String>) -> Self {
194        if let Some(field) = self.fields.last_mut() {
195            field.help_text = Some(text.into());
196        }
197        self
198    }
199
200    /// Set submit button text
201    pub fn submit_text(mut self, text: impl Into<String>) -> Self {
202        self.submit_text = text.into();
203        self
204    }
205
206    /// Add cancel button
207    pub fn cancel_text(mut self, text: impl Into<String>) -> Self {
208        self.cancel_text = Some(text.into());
209        self
210    }
211
212    /// Set form layout
213    pub fn layout(mut self, layout: FormLayout) -> Self {
214        self.layout = layout;
215        self
216    }
217
218    /// Set form width
219    pub fn width(mut self, width: Val) -> Self {
220        self.width = width;
221        self
222    }
223
224    /// Build the form
225    pub fn build(self, parent: &mut ChildSpawnerCommands) -> Entity {
226        let form_entity = parent
227            .spawn((
228                FormRoot {
229                    id: self.id.clone(),
230                    fields: self.fields.clone(),
231                    is_valid: false,
232                    values: HashMap::new(),
233                },
234                Node {
235                    flex_direction: FlexDirection::Column,
236                    width: self.width,
237                    row_gap: Val::Px(dimensions::SPACING_MEDIUM),
238                    padding: UiRect::all(Val::Px(dimensions::PADDING_LARGE)),
239                    ..default()
240                },
241                BackgroundColor(colors::BACKGROUND_SECONDARY),
242                BorderRadius::all(Val::Px(dimensions::BORDER_RADIUS_MEDIUM)),
243            ))
244            .id();
245
246        let form_entity_copy = form_entity;
247
248        parent.commands().entity(form_entity).with_children(|form| {
249                // Add title if provided
250                if let Some(title) = self.title {
251                    form.spawn((
252                        Text::new(title),
253                        TextFont {
254                            font_size: dimensions::FONT_SIZE_HEADING,
255                            ..default()
256                        },
257                        TextColor(colors::TEXT_PRIMARY),
258                        Node {
259                            margin: UiRect::bottom(Val::Px(dimensions::SPACING_LARGE)),
260                            ..default()
261                        },
262                    ));
263                }
264
265                // Add fields
266                for field in &self.fields {
267                    spawn_form_field(form, field);
268                }
269
270                // Add buttons
271                form.spawn((
272                    Node {
273                        flex_direction: FlexDirection::Row,
274                        justify_content: JustifyContent::End,
275                        column_gap: Val::Px(dimensions::SPACING_MEDIUM),
276                        margin: UiRect::top(Val::Px(dimensions::SPACING_LARGE)),
277                        ..default()
278                    },
279                    BackgroundColor(Color::NONE),
280                ))
281                .with_children(|buttons| {
282                    // Cancel button if specified
283                    if let Some(cancel_text) = self.cancel_text {
284                        crate::button::secondary_button(cancel_text).build(buttons);
285                    }
286
287                    // Submit button
288                    let submit_button = crate::button::primary_button(self.submit_text)
289                        .build(buttons);
290
291                    buttons.commands()
292                        .entity(submit_button)
293                        .insert(FormSubmitButton { form_entity: form_entity_copy });
294                });
295            });
296
297        form_entity
298    }
299}