rolodex_tui/components/form/
mod.rs1use 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}