Skip to main content

rolodex_tui/components/form/
mod.rs

1use std::iter::repeat_n;
2
3use crossterm::event::{KeyCode, KeyEvent};
4use ratatui::{prelude::*, widgets::*};
5use tracing::info;
6
7use crate::{
8    components::{
9        component::opt,
10        input::{Input, InputMode, InputMsg, InputOutput},
11    },
12    model::Contact,
13};
14
15#[derive(Debug, Clone)]
16pub enum FormMsg {
17    Input(InputMsg),
18    Next,
19    Previous,
20    Submit,
21    Cancel,
22}
23
24pub enum FormOutput {
25    Submitted(Contact),
26    Cancelled,
27}
28
29#[derive(Debug, Clone)]
30pub enum FormField {
31    Name,
32    Company,
33    Email,
34    Phone,
35}
36
37const FIELD_ORDER: [FormField; 4] = [
38    FormField::Name,
39    FormField::Company,
40    FormField::Email,
41    FormField::Phone,
42];
43
44#[derive(Debug, Default)]
45pub struct Form {
46    fields: Vec<Input>,
47    contact: Contact,
48    focused: usize,
49    editing_id: Option<i64>,
50}
51
52impl Form {
53    pub fn new() -> Self {
54        let contact = Contact::default();
55        let contact_clone = contact.clone();
56
57        Self {
58            fields: vec![
59                Input::new("Name", &contact.name, 10, InputMode::Inline, 30),
60                Input::new(
61                    "Company",
62                    &contact.company.unwrap_or_default(),
63                    10,
64                    InputMode::Inline,
65                    30,
66                ),
67                Input::new(
68                    "Email",
69                    &contact.email.unwrap_or_default(),
70                    10,
71                    InputMode::Inline,
72                    30,
73                ),
74                Input::new(
75                    "Phone",
76                    &contact.phone.unwrap_or_default(),
77                    10,
78                    InputMode::Inline,
79                    20,
80                ),
81            ],
82            contact: contact_clone,
83            focused: 0,
84            editing_id: None,
85        }
86    }
87    pub fn set_contact(&mut self, contact: Contact) {
88        self.editing_id = Some(contact.id);
89        self.contact = contact.clone();
90        self.fields[0].value = contact.name;
91        self.fields[1].value = contact.company.unwrap_or_default();
92        self.fields[2].value = contact.email.unwrap_or_default();
93        self.fields[3].value = contact.phone.unwrap_or_default();
94        self.focused = 0;
95        self.fields[0].set_focused(true);
96    }
97
98    pub fn update<ParentMsg>(
99        &mut self,
100        msg: FormMsg,
101        map: impl Fn(FormOutput) -> ParentMsg,
102    ) -> Option<ParentMsg> {
103        info!("Form update: {:?}", msg);
104        info!("Contact: {:?}", self.contact);
105        match msg {
106            FormMsg::Input(input_msg) => {
107                info!("Form input: {:?}", input_msg);
108                if let Some(field) = self.fields.get_mut(self.focused) {
109                    let field_key = &FIELD_ORDER[self.focused];
110                    info!("Field key: {:?}", field_key);
111
112                    if let Some(InputOutput::Changed(val)) = field.update(input_msg, |out| out) {
113                        info!("Field value: {:?}", val);
114                        match field_key {
115                            FormField::Name => self.contact.name = val,
116                            FormField::Company => self.contact.company = opt(val),
117                            FormField::Email => self.contact.email = opt(val),
118                            FormField::Phone => self.contact.phone = opt(val),
119                        }
120                    }
121                }
122                None
123            }
124            FormMsg::Next => {
125                if !self.fields.is_empty() {
126                    self.fields[self.focused].set_focused(false);
127                    self.focused = (self.focused + 1) % self.fields.len();
128                    self.fields[self.focused].set_focused(true);
129                }
130                None
131            }
132
133            FormMsg::Previous => {
134                if !self.fields.is_empty() {
135                    self.fields[self.focused].set_focused(false);
136                    self.focused = (self.focused + self.fields.len() - 1) % self.fields.len();
137                    self.fields[self.focused].set_focused(true);
138                }
139                None
140            }
141            FormMsg::Submit => Some(map(FormOutput::Submitted(self.contact.clone()))),
142            FormMsg::Cancel => Some(map(FormOutput::Cancelled)),
143        }
144    }
145
146    pub fn draw(&self, f: &mut Frame, area: Rect, _focused: bool) {
147        f.render_widget(Clear, area);
148
149        let block = Block::default()
150            .borders(Borders::ALL)
151            .title(" Add Contact ")
152            .border_type(BorderType::Rounded)
153            .padding(Padding {
154                left: 2,
155                right: 2,
156                top: 1,
157                bottom: 1,
158            });
159
160        f.render_widget(block.clone(), area);
161
162        let inner = block.inner(area);
163
164        let num_fields = self.fields.len();
165        let chunks = Layout::default()
166            .direction(Direction::Vertical)
167            .constraints(
168                repeat_n(Constraint::Length(1), num_fields)
169                    .chain([Constraint::Length(1), Constraint::Length(1)])
170                    .collect::<Vec<_>>(),
171            )
172            .split(inner);
173
174        for (i, field) in self.fields.iter().enumerate() {
175            let is_focused = self.focused == i;
176            field.draw(f, chunks[i], is_focused);
177        }
178
179        let button_area = chunks[num_fields + 1];
180        let text = Span::styled(
181            "[Enter] = Save / [Esc] = Cancel",
182            Style::default().fg(Color::DarkGray),
183        );
184        let paragraph = Paragraph::new(text).alignment(Alignment::Center);
185        f.render_widget(paragraph, button_area);
186    }
187    pub fn handle_key(&self, event: KeyEvent) -> Option<FormMsg> {
188        match event.code {
189            KeyCode::Tab => Some(FormMsg::Next),
190            KeyCode::BackTab => Some(FormMsg::Previous),
191            KeyCode::Enter => Some(FormMsg::Submit),
192            KeyCode::Esc => Some(FormMsg::Cancel),
193            _ => self.fields[self.focused]
194                .handle_key(event)
195                .map(FormMsg::Input),
196        }
197    }
198}
199
200impl crate::components::Component for Form {
201    type Msg = FormMsg;
202    type Output = FormOutput;
203
204    fn draw(&self, f: &mut ratatui::Frame, area: Rect, focused: bool) {
205        self.draw(f, area, focused);
206    }
207    fn handle_key(&self, event: KeyEvent) -> Option<Self::Msg> {
208        self.handle_key(event)
209    }
210
211    fn update<ParentMsg>(
212        &mut self,
213        msg: Self::Msg,
214        map: impl Fn(Self::Output) -> ParentMsg,
215    ) -> Option<ParentMsg> {
216        self.update(msg, map)
217    }
218}