ez_tui/components/concrete/forms/
form.rs1use crate::components::concrete::forms::inputs::core::{InputBuilder, InputCpt};
2use crate::{
3 AttrValue, Attribute, BorderTheme, Component, EzArgs, EzCptIds, EzEvent, EzMsg, EzState, Form,
4 FormField, FormFieldValue, FormResult, Legend, Matcher, MockComponent, MockProps, Props, State,
5 Theme,
6};
7use crossterm::event::KeyCode;
8use eztui_derive::MockProps;
9use ratatui::buffer::Buffer;
10use ratatui::layout::Rect;
11use ratatui::prelude::Widget;
12use ratatui::style::Style;
13use ratatui::style::palette::tailwind::GRAY;
14use ratatui::widgets::{BorderType, Borders};
15use std::collections::HashMap;
16use std::fmt::Debug;
17
18#[derive(Debug, MockProps)]
20pub struct FormCpt<FID, CID, CA, CS, CM>
21where
22 FID: EzCptIds,
23 CID: EzCptIds,
24 CA: EzArgs,
25 CS: EzState,
26 CM: EzMsg,
27{
28 props: Props,
29 form: Form<FID>,
30 inputs: HashMap<FID, Box<dyn InputCpt<CID, CA, CS, CM>>>,
31 focus: Option<FID>,
32}
33
34impl<FID, CID, CA, CS, CM> FormCpt<FID, CID, CA, CS, CM>
35where
36 FID: EzCptIds,
37 CID: EzCptIds,
38 CA: EzArgs,
39 CS: EzState,
40 CM: EzMsg,
41{
42 #[must_use]
44 pub fn new(form: Form<FID>) -> Self {
45 let name = form.name().to_string();
46 let mut cpt = FormCpt {
47 props: Props::default(),
48 inputs: form
49 .flatten_fields()
50 .into_iter()
51 .map(|field| (field.id().clone(), InputBuilder::create(field)))
52 .collect(),
53 form,
54 focus: None,
55 };
56 if let Some(props) = cpt.props_mut() {
57 props.set(
58 Attribute::Title,
59 AttrValue::Title((name, ratatui::prelude::Alignment::Center)),
60 );
61 }
62 cpt
63 }
64
65 fn update_focus(&mut self, previous: Option<FID>) {
66 if let Some(fid) = previous
67 && let Some(cpt) = self.inputs.get_mut(&fid)
68 && let Some(props) = cpt.props_mut()
69 {
70 props.set(Attribute::Focus, AttrValue::Flag(false));
71 }
72
73 if let Some(fid) = &self.focus
74 && let Some(cpt) = self.inputs.get_mut(fid)
75 && let Some(props) = cpt.props_mut()
76 {
77 props.set(Attribute::Focus, AttrValue::Flag(true));
78 }
79 }
80
81 #[allow(clippy::missing_panics_doc)] pub fn collect(&self) -> FormResult<FID> {
84 let result: Vec<(FormField<FID>, FormFieldValue)> = self
85 .inputs
86 .iter()
87 .map(|(fid, v)| {
88 (
89 self.form
90 .get(fid)
91 .expect("form and inputs should have matching fields")
92 .clone(),
93 v.get_value(),
94 )
95 })
96 .collect();
97 result.into()
98 }
99
100 pub fn capture_validation(&mut self) -> bool {
103 for ref mut input in self.inputs.values_mut() {
104 if input.capture_validation() {
105 return true;
106 }
107 }
108 false
109 }
110}
111impl<FID, CID, CA, CS, CM> MockComponent for FormCpt<FID, CID, CA, CS, CM>
112where
113 FID: EzCptIds,
114 CID: EzCptIds,
115 CA: EzArgs,
116 CS: EzState,
117 CM: EzMsg,
118{
119 fn draw(&mut self, area: Rect, buf: &mut Buffer, theme: &Theme) {
120 let block = theme.block(self.props());
121 let block_area = block.inner(area);
122 block.render(area, buf);
123
124 let mut field_theme = theme.clone();
125 field_theme.default_border_theme =
126 BorderTheme::new(Borders::ALL, BorderType::Thick, Style::new().fg(GRAY.c500));
127
128 let mut callback = |(input_id, input_area, buf): (FID, Rect, &mut Buffer)| {
129 if let Some(field) = self.form.get(&input_id)
130 && let Some(input) = self.inputs.get_mut(field.id())
131 {
132 input.draw(input_area, buf, &field_theme);
133 }
134 };
135 self.form
136 .custom_layout()
137 .draw(block_area, buf, &mut callback);
138 }
139}
140
141impl<FID, CID, CA, CS, CM> Component<CID, CA, CS, CM> for FormCpt<FID, CID, CA, CS, CM>
142where
143 FID: EzCptIds,
144 CID: EzCptIds,
145 CA: EzArgs,
146 CS: EzState,
147 CM: EzMsg,
148{
149 fn on_event(
150 &mut self,
151 event: EzEvent<CID, CM>,
152 state: &mut State<CS>,
153 ) -> Vec<EzEvent<CID, CM>> {
154 if let Some(fid) = &self.focus
155 && let Some(cpt) = self.inputs.get_mut(fid)
156 {
157 cpt.on_event(event, state)
158 } else {
159 vec![]
160 }
161 }
162
163 fn focusable(&self) -> bool {
164 true
165 }
166
167 fn legend(&self) -> Option<Legend> {
168 Legend::new(
169 self.form.name.to_string(),
170 vec![
171 Matcher::new(vec![KeyCode::Enter.into()], "Validate form".into()),
172 Matcher::new(
173 vec![KeyCode::Char(' ').into()],
174 "Toggle (range target, check)".into(),
175 ),
176 Matcher::new(vec![KeyCode::PrintScreen.into()], "Toggle date tz)".into()),
177 ],
178 )
179 .into()
180 }
181
182 fn capture_focus(&mut self, forward: bool) -> bool {
183 let mut previous = None;
184 let new_focus = if let Some(focus) = &self.focus {
185 previous = Some(focus.clone());
186 if forward {
187 focus.after()
188 } else {
189 focus.before()
190 }
191 } else if forward {
192 FID::first()
193 } else {
194 FID::last()
195 };
196 self.focus = new_focus;
197 self.update_focus(previous);
198 self.focus.is_some()
199 }
200}
201impl<FID, CID, CA, CS, CM> FormCpt<FID, CID, CA, CS, CM>
202where
203 FID: EzCptIds,
204 CID: EzCptIds,
205 CA: EzArgs,
206 CS: EzState,
207 CM: EzMsg,
208{
209}