Skip to main content

rolodex_tui/components/input/
mod.rs

1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
2use ratatui::{prelude::*, widgets::*};
3use tracing::info;
4
5#[derive(Debug, Default)]
6pub enum InputMode {
7    #[default]
8    Regular,
9    Inline,
10}
11
12#[derive(Debug, Clone)]
13pub enum InputMsg {
14    Clear,
15    CursorLeft,
16    CursorRight,
17    CursorStart,
18    CursorEnd,
19    Backspace,
20    Delete,
21    TypeChar(char),
22}
23
24#[derive(Debug, Clone)]
25pub enum InputOutput {
26    Changed(String),
27}
28
29#[derive(Debug, Default)]
30pub struct Input {
31    label: String,
32    label_width: u16,
33    pub value: String,
34    cursor: usize,
35    focused: bool,
36    mode: InputMode,
37    max_len: usize,
38}
39
40impl Input {
41    pub fn new(
42        label: &str,
43        value: &str,
44        label_width: u16,
45        mode: InputMode,
46        max_len: usize,
47    ) -> Self {
48        Self {
49            label: label.to_string(),
50            label_width,
51            value: value.to_string(),
52            focused: false,
53            cursor: value.len(),
54            mode,
55            max_len,
56        }
57    }
58
59    pub fn set_focused(&mut self, focused: bool) {
60        self.focused = focused;
61        self.cursor = self.value.len();
62    }
63
64    pub fn set_label(&mut self, label: &str) {
65        self.label = label.to_string();
66    }
67
68    pub fn update<ParentMsg>(
69        &mut self,
70        msg: InputMsg,
71        map: impl Fn(InputOutput) -> ParentMsg,
72    ) -> Option<ParentMsg> {
73        match msg {
74            InputMsg::Clear => {
75                self.value.clear();
76                self.cursor = 0;
77                Some(map(InputOutput::Changed(self.value.clone())))
78            }
79            InputMsg::CursorLeft => {
80                if self.cursor > 0 {
81                    self.cursor -= 1;
82                }
83                None
84            }
85            InputMsg::CursorRight => {
86                if self.cursor < self.value.len() {
87                    self.cursor += 1;
88                }
89                None
90            }
91            InputMsg::CursorStart => {
92                self.cursor = 0;
93                None
94            }
95            InputMsg::CursorEnd => {
96                self.cursor = self.value.len();
97                None
98            }
99            InputMsg::Backspace => {
100                if self.cursor > 0 {
101                    self.cursor -= 1;
102                    self.value.remove(self.cursor);
103                    Some(map(InputOutput::Changed(self.value.clone())))
104                } else {
105                    None
106                }
107            }
108            InputMsg::Delete => {
109                if self.cursor < self.value.len() {
110                    self.value.remove(self.cursor);
111                    Some(map(InputOutput::Changed(self.value.clone())))
112                } else {
113                    None
114                }
115            }
116            InputMsg::TypeChar(c) => {
117                if self.value.len() < self.max_len {
118                    self.value.insert(self.cursor, c);
119                    self.cursor += 1;
120                    Some(map(InputOutput::Changed(self.value.clone())))
121                } else {
122                    None
123                }
124            }
125        }
126    }
127
128    fn draw_regular(&self, f: &mut Frame, area: Rect, focused: bool) {
129        let block = Block::default()
130            .title(self.label.clone())
131            .borders(Borders::ALL)
132            .border_style(if focused {
133                Style::default().fg(Color::Cyan)
134            } else {
135                Style::default()
136            });
137        f.render_widget(&block, area);
138
139        let text_style = if focused {
140            Style::default().fg(Color::Yellow)
141        } else {
142            Style::default()
143        };
144        let inner = block.inner(area);
145
146        let input = Paragraph::new(self.value.clone()).style(text_style);
147        f.render_widget(input, inner);
148
149        self.set_cursor_position(f, inner, focused);
150    }
151    fn set_cursor_position(&self, f: &mut Frame, area: Rect, focused: bool) {
152        if focused {
153            let clamped_cursor = self.cursor.min(self.value.len());
154            let cursor_x = area.x + clamped_cursor as u16;
155            let cursor_y = area.y;
156
157            f.set_cursor_position(Position {
158                x: cursor_x,
159                y: cursor_y,
160            });
161        }
162    }
163    fn draw_inline(&self, f: &mut Frame, area: Rect, focused: bool) {
164        let text_style = if focused {
165            Style::default().fg(Color::Yellow)
166        } else {
167            Style::default()
168        };
169        let label_text = format!(
170            "{:<width$}: ",
171            self.label.clone(),
172            width = self.label_width as usize
173        );
174
175        let label = Paragraph::new(label_text).style(Style::default().fg(Color::Cyan));
176        let input = Paragraph::new(self.value.clone()).style(text_style);
177        let layout = Layout::default()
178            .direction(Direction::Horizontal)
179            .constraints([Constraint::Length(self.label_width + 2), Constraint::Min(0)])
180            .split(area);
181
182        f.render_widget(label, layout[0]);
183        f.render_widget(input, layout[1]);
184
185        self.set_cursor_position(f, layout[1], focused);
186    }
187    pub fn draw(&self, f: &mut Frame, area: Rect, focused: bool) {
188        match self.mode {
189            InputMode::Regular => self.draw_regular(f, area, focused),
190            InputMode::Inline => self.draw_inline(f, area, focused),
191        }
192    }
193
194    pub fn handle_key(&self, event: KeyEvent) -> Option<InputMsg> {
195        match event.code {
196            KeyCode::Char('l') if event.modifiers.contains(KeyModifiers::CONTROL) => {
197                info!("Ctrl+L pressed - Clearing input");
198                Some(InputMsg::Clear)
199            }
200            KeyCode::Left => Some(InputMsg::CursorLeft),
201            KeyCode::Right => Some(InputMsg::CursorRight),
202            KeyCode::Home => Some(InputMsg::CursorStart),
203            KeyCode::End => Some(InputMsg::CursorEnd),
204            KeyCode::Backspace => Some(InputMsg::Backspace),
205            KeyCode::Delete => Some(InputMsg::Delete),
206            KeyCode::Char(c) => Some(InputMsg::TypeChar(c)),
207            _ => None,
208        }
209    }
210}
211
212impl crate::components::Component for Input {
213    type Msg = InputMsg;
214    type Output = InputOutput;
215
216    fn draw(&self, f: &mut Frame, area: Rect, focused: bool) {
217        self.draw(f, area, focused);
218    }
219    fn handle_key(&self, event: KeyEvent) -> Option<Self::Msg> {
220        self.handle_key(event)
221    }
222
223    fn update<ParentMsg>(
224        &mut self,
225        msg: Self::Msg,
226        map: impl Fn(Self::Output) -> ParentMsg,
227    ) -> Option<ParentMsg> {
228        self.update(msg, map)
229    }
230}