1use 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
12pub 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 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 pub fn title(mut self, title: impl Into<String>) -> Self {
41 self.title = Some(title.into());
42 self
43 }
44
45 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 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 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 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 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 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 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 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 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 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 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 pub fn submit_text(mut self, text: impl Into<String>) -> Self {
202 self.submit_text = text.into();
203 self
204 }
205
206 pub fn cancel_text(mut self, text: impl Into<String>) -> Self {
208 self.cancel_text = Some(text.into());
209 self
210 }
211
212 pub fn layout(mut self, layout: FormLayout) -> Self {
214 self.layout = layout;
215 self
216 }
217
218 pub fn width(mut self, width: Val) -> Self {
220 self.width = width;
221 self
222 }
223
224 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 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 for field in &self.fields {
267 spawn_form_field(form, field);
268 }
269
270 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 if let Some(cancel_text) = self.cancel_text {
284 crate::button::secondary_button(cancel_text).build(buttons);
285 }
286
287 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}