1use crate::patterns::form::FormState;
28use crate::render::{Cell, Modifier};
29use crate::style::Color;
30use crate::widget::traits::{RenderContext, View, WidgetProps};
31use std::collections::HashMap;
32use std::sync::Arc;
33
34type SubmitCallback = Arc<dyn Fn(HashMap<String, String>)>;
36
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
39pub enum InputType {
40 #[default]
42 Text,
43 Password,
45 Email,
47 Number,
49}
50
51pub struct Form {
53 form_state: FormState,
55 on_submit: Option<SubmitCallback>,
57 props: WidgetProps,
59 submit_text: Option<String>,
61 show_errors: bool,
63 error_style: ErrorDisplayStyle,
65}
66
67#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
69pub enum ErrorDisplayStyle {
70 #[default]
72 Inline,
73 Summary,
75 Both,
77}
78
79impl Form {
80 pub fn new(form_state: FormState) -> Self {
82 Self {
83 form_state,
84 on_submit: None,
85 props: WidgetProps::default(),
86 submit_text: None,
87 show_errors: true,
88 error_style: ErrorDisplayStyle::default(),
89 }
90 }
91
92 pub fn on_submit(mut self, callback: SubmitCallback) -> Self {
94 self.on_submit = Some(callback);
95 self
96 }
97
98 pub fn submit_text(mut self, text: impl Into<String>) -> Self {
100 self.submit_text = Some(text.into());
101 self
102 }
103
104 pub fn show_errors(mut self, show: bool) -> Self {
106 self.show_errors = show;
107 self
108 }
109
110 pub fn error_style(mut self, style: ErrorDisplayStyle) -> Self {
112 self.error_style = style;
113 self
114 }
115
116 pub fn form_state(&self) -> &FormState {
118 &self.form_state
119 }
120
121 pub fn is_valid(&self) -> bool {
123 self.form_state.is_valid()
124 }
125
126 pub fn error_count(&self) -> usize {
128 self.form_state.errors().len()
129 }
130
131 pub fn submit(&self) {
133 if self.is_valid() {
134 if let Some(ref callback) = self.on_submit {
135 let data = self.form_state.values();
136 callback(data);
137 }
138 }
139 }
140
141 fn render_border(&self, ctx: &mut RenderContext) {
143 let area = ctx.area;
144 if area.width < 2 || area.height < 2 {
145 return;
146 }
147
148 let border_color = if self.is_valid() {
149 Color::rgb(100, 100, 100)
150 } else {
151 Color::rgb(200, 80, 80) };
153
154 for x in area.x..area.x + area.width {
156 let mut top_cell = Cell::new('─');
157 top_cell.fg = Some(border_color);
158 ctx.buffer.set(x, area.y, top_cell);
159
160 let mut bottom_cell = Cell::new('─');
161 bottom_cell.fg = Some(border_color);
162 ctx.buffer.set(x, area.y + area.height - 1, bottom_cell);
163 }
164
165 for y in area.y..area.y + area.height {
167 let mut left_cell = Cell::new('│');
168 left_cell.fg = Some(border_color);
169 ctx.buffer.set(area.x, y, left_cell);
170
171 let mut right_cell = Cell::new('│');
172 right_cell.fg = Some(border_color);
173 ctx.buffer.set(area.x + area.width - 1, y, right_cell);
174 }
175
176 let corners = [
178 ('┌', area.x, area.y),
179 ('┐', area.x + area.width - 1, area.y),
180 ('└', area.x, area.y + area.height - 1),
181 ('┘', area.x + area.width - 1, area.y + area.height - 1),
182 ];
183
184 for &(ch, x, y) in &corners {
185 let mut cell = Cell::new(ch);
186 cell.fg = Some(border_color);
187 ctx.buffer.set(x, y, cell);
188 }
189 }
190
191 fn render_title(&self, ctx: &mut RenderContext) {
193 let area = ctx.area;
194 if area.width < 4 {
195 return;
196 }
197
198 let title = "Form";
199 let title_x = area.x + 2;
200
201 for (i, ch) in title.chars().enumerate() {
202 if title_x + (i as u16) < area.x + area.width - 1 {
203 let mut cell = Cell::new(ch);
204 cell.fg = Some(Color::WHITE);
205 cell.bg = Some(Color::BLACK);
206 ctx.buffer.set(title_x + i as u16, area.y, cell);
207 }
208 }
209 }
210}
211
212impl Default for Form {
213 fn default() -> Self {
214 Self::new(FormState::new().build())
215 }
216}
217
218impl View for Form {
219 crate::impl_view_meta!("Form");
220
221 fn render(&self, ctx: &mut RenderContext) {
222 let area = ctx.area;
223
224 self.render_border(ctx);
226
227 self.render_title(ctx);
229
230 let status_y = area.y + area.height - 2;
232 if status_y > area.y && area.width > 4 {
233 let status_text = if self.is_valid() {
234 "✓ Valid".to_string()
235 } else {
236 let error_count = self.error_count();
237 format!("✗ {} error(s)", error_count)
238 };
239
240 let status_color = if self.is_valid() {
241 Color::rgb(80, 200, 80)
242 } else {
243 Color::rgb(200, 80, 80)
244 };
245
246 for (i, ch) in status_text.chars().enumerate() {
247 let x = area.x + 2 + i as u16;
248 if x < area.x + area.width - 2 {
249 let mut cell = Cell::new(ch);
250 cell.fg = Some(status_color);
251 ctx.buffer.set(x, status_y, cell);
252 }
253 }
254 }
255 }
256}
257
258pub struct FormFieldWidget {
260 name: String,
262 placeholder: String,
264 input_type: InputType,
266 props: WidgetProps,
268 show_label: bool,
270 show_errors: bool,
272}
273
274impl FormFieldWidget {
275 pub fn new(name: impl Into<String>) -> Self {
277 Self {
278 name: name.into(),
279 placeholder: String::new(),
280 input_type: InputType::Text,
281 props: WidgetProps::default(),
282 show_label: true,
283 show_errors: true,
284 }
285 }
286
287 pub fn placeholder(mut self, text: impl Into<String>) -> Self {
289 self.placeholder = text.into();
290 self
291 }
292
293 pub fn input_type(mut self, input_type: InputType) -> Self {
295 self.input_type = input_type;
296 self
297 }
298
299 pub fn show_label(mut self, show: bool) -> Self {
301 self.show_label = show;
302 self
303 }
304
305 pub fn show_errors(mut self, show: bool) -> Self {
307 self.show_errors = show;
308 self
309 }
310
311 pub fn name(&self) -> &str {
313 &self.name
314 }
315
316 #[allow(dead_code)]
318 fn render_label(&self, form_state: &FormState, ctx: &mut RenderContext) {
319 let area = ctx.area;
320 if let Some(field) = form_state.get(&self.name) {
321 let label = &field.label;
322
323 for (i, ch) in label.chars().enumerate() {
324 if area.x + (i as u16) < area.x + area.width {
325 let mut cell = Cell::new(ch);
326 cell.fg = Some(Color::rgb(200, 200, 200));
327 ctx.buffer.set(area.x + i as u16, area.y, cell);
328 }
329 }
330 }
331 }
332
333 #[allow(dead_code)]
335 fn render_value(&self, form_state: &FormState, ctx: &mut RenderContext) {
336 let area = ctx.area;
337 let value = form_state.value(&self.name).unwrap_or_default();
338
339 let display_text = if value.is_empty() {
341 self.placeholder.clone()
342 } else {
343 match self.input_type {
344 InputType::Password => "•".repeat(value.len().min(20)),
345 _ => value.clone(),
346 }
347 };
348
349 let text_color = if value.is_empty() {
350 Color::rgb(120, 120, 120) } else {
352 Color::WHITE
353 };
354
355 for (i, ch) in display_text.chars().enumerate() {
356 let x = area.x + i as u16;
357 if x < area.x + area.width {
358 let mut cell = Cell::new(ch);
359 cell.fg = Some(text_color);
360 ctx.buffer.set(x, area.y, cell);
361 }
362 }
363 }
364
365 #[allow(dead_code)]
367 fn render_errors(&self, form_state: &FormState, ctx: &mut RenderContext) {
368 if !self.show_errors {
369 return;
370 }
371
372 let field = match form_state.get(&self.name) {
373 Some(f) => f,
374 None => return,
375 };
376
377 let error_msg = match field.first_error() {
378 Some(err) => err,
379 None => return,
380 };
381
382 let area = ctx.area;
383 let error_color = Color::rgb(200, 80, 80);
384
385 for (i, ch) in error_msg.chars().enumerate() {
386 let x = area.x + i as u16;
387 if x < area.x + area.width {
388 let mut cell = Cell::new(ch);
389 cell.fg = Some(error_color);
390 cell.modifier |= Modifier::DIM;
391 ctx.buffer.set(x, area.y, cell);
392 }
393 }
394 }
395}
396
397impl Default for FormFieldWidget {
398 fn default() -> Self {
399 Self::new("")
400 }
401}
402
403impl View for FormFieldWidget {
404 crate::impl_view_meta!("FormField");
405
406 fn render(&self, ctx: &mut RenderContext) {
407 let area = ctx.area;
410
411 if area.width > self.name.len() as u16 {
412 for (i, ch) in self.name.chars().enumerate() {
413 let mut cell = Cell::new(ch);
414 cell.fg = Some(Color::rgb(150, 150, 150));
415 ctx.buffer.set(area.x + i as u16, area.y, cell);
416 }
417 }
418 }
419}
420
421pub fn form(form_state: FormState) -> Form {
423 Form::new(form_state)
424}
425
426pub fn form_field(name: impl Into<String>) -> FormFieldWidget {
428 FormFieldWidget::new(name)
429}
430
431pub use crate::patterns::form::FormField;
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437
438 #[test]
439 fn test_form_new() {
440 let form_state = FormState::new().build();
441 let form = Form::new(form_state);
442 assert!(form.is_valid());
443 }
444
445 #[test]
446 fn test_form_builder() {
447 let form_state = FormState::new().build();
448 let form = Form::new(form_state)
449 .submit_text("Send")
450 .show_errors(true)
451 .error_style(ErrorDisplayStyle::Summary);
452
453 assert_eq!(form.submit_text, Some("Send".to_string()));
454 assert!(form.show_errors);
455 assert_eq!(form.error_style, ErrorDisplayStyle::Summary);
456 }
457
458 #[test]
459 fn test_input_type_default() {
460 let input_type = InputType::default();
461 assert_eq!(input_type, InputType::Text);
462 }
463
464 #[test]
465 fn test_form_field_new() {
466 let field = FormFieldWidget::new("username");
467 assert_eq!(field.name, "username");
468 assert_eq!(field.input_type, InputType::Text);
469 }
470
471 #[test]
472 fn test_form_field_builder() {
473 let field = FormFieldWidget::new("email")
474 .placeholder("Enter email")
475 .input_type(InputType::Email)
476 .show_label(false)
477 .show_errors(false);
478
479 assert_eq!(field.placeholder, "Enter email");
480 assert_eq!(field.input_type, InputType::Email);
481 assert!(!field.show_label);
482 assert!(!field.show_errors);
483 }
484
485 #[test]
486 fn test_error_display_style_default() {
487 let style = ErrorDisplayStyle::default();
488 assert_eq!(style, ErrorDisplayStyle::Inline);
489 }
490
491 #[test]
492 fn test_convenience_functions() {
493 let form_state = FormState::new().build();
494 let form = form(form_state);
495 assert_eq!(form.submit_text, None);
496
497 let field = form_field("password");
498 assert_eq!(field.name, "password");
499 }
500}