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