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