Skip to main content

ez_tui/components/concrete/forms/
form.rs

1use 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/// A component responsible for rendering a form.
19#[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    /// Create a new [`FormCpt`] component with a given form.
43    #[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    /// Collect this form result as a [`FormResult`].
82    #[allow(clippy::missing_panics_doc)] // this except should never happen
83    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    /// Pass the validation event of this form to all its inputs so they can
101    /// act on it and return wether or not validation has been handled internally.
102    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}