1use ratatui::{
2 layout::Rect,
3 style::{Color, Modifier, Style},
4 text::{Line, Span},
5 widgets::Paragraph,
6 Frame,
7};
8
9use crate::input::{Key, Modifiers};
10use crate::theme::Theme;
11
12pub fn key_to_op(key: crate::input::KeyEvent) -> Option<InputFieldOp> {
18 match (key.modifiers, key.key) {
19 (m, Key::Char(c)) if !m.contains(Modifiers::CTRL) && !m.contains(Modifiers::ALT) => {
20 Some(InputFieldOp::InsertChar(c))
21 }
22 (_, Key::Backspace) => Some(InputFieldOp::Backspace),
23 (_, Key::Delete) => Some(InputFieldOp::DeleteForward),
24 (_, Key::ArrowLeft) => Some(InputFieldOp::CursorLeft),
25 (_, Key::ArrowRight) => Some(InputFieldOp::CursorRight),
26 (_, Key::Home) => Some(InputFieldOp::CursorHome),
27 (_, Key::End) => Some(InputFieldOp::CursorEnd),
28 _ => None,
29 }
30}
31
32#[derive(Debug, Default, Clone)]
38pub struct InputField {
39 text: String,
40 cursor: usize,
42 label: String,
44}
45
46#[derive(Debug, Clone)]
48pub enum InputFieldOp {
49 InsertChar(char),
50 Backspace,
51 DeleteForward,
52 CursorLeft,
53 CursorRight,
54 CursorHome,
55 CursorEnd,
56 SetText(String),
58}
59
60impl InputField {
61 pub fn new(label: impl Into<String>) -> Self {
62 Self {
63 text: String::new(),
64 cursor: 0,
65 label: label.into(),
66 }
67 }
68
69 pub fn with_text(mut self, text: impl Into<String>) -> Self {
70 self.text = text.into();
71 self.cursor = self.text.len();
72 self
73 }
74
75 pub fn text(&self) -> &str {
76 &self.text
77 }
78
79 pub fn cursor(&self) -> usize {
80 self.cursor
81 }
82
83 pub fn label(&self) -> &str {
84 &self.label
85 }
86
87 pub fn set_label(&mut self, label: impl Into<String>) {
88 self.label = label.into();
89 }
90
91 pub fn is_empty(&self) -> bool {
92 self.text.is_empty()
93 }
94
95 pub fn insert_char(&mut self, c: char) {
98 self.text.insert(self.cursor, c);
99 self.cursor += c.len_utf8();
100 }
101
102 pub fn backspace(&mut self) {
103 if self.cursor == 0 {
104 return;
105 }
106 let prev = self.text[..self.cursor]
107 .char_indices()
108 .next_back()
109 .map(|(i, _)| i)
110 .unwrap_or(0);
111 self.text.drain(prev..self.cursor);
112 self.cursor = prev;
113 }
114
115 pub fn delete_forward(&mut self) {
116 if self.cursor >= self.text.len() {
117 return;
118 }
119 let next = self.text[self.cursor..]
120 .char_indices()
121 .nth(1)
122 .map(|(i, _)| self.cursor + i)
123 .unwrap_or(self.text.len());
124 self.text.drain(self.cursor..next);
125 }
126
127 pub fn cursor_left(&mut self) {
128 if self.cursor == 0 {
129 return;
130 }
131 self.cursor = self.text[..self.cursor]
132 .char_indices()
133 .next_back()
134 .map(|(i, _)| i)
135 .unwrap_or(0);
136 }
137
138 pub fn cursor_right(&mut self) {
139 if self.cursor >= self.text.len() {
140 return;
141 }
142 self.cursor = self.text[self.cursor..]
143 .char_indices()
144 .nth(1)
145 .map(|(i, _)| self.cursor + i)
146 .unwrap_or(self.text.len());
147 }
148
149 pub fn cursor_home(&mut self) {
150 self.cursor = 0;
151 }
152
153 pub fn cursor_end(&mut self) {
154 self.cursor = self.text.len();
155 }
156
157 pub fn set_text(&mut self, text: String) {
158 self.text = text;
159 self.cursor = self.text.len();
160 }
161
162 pub fn apply(&mut self, op: &InputFieldOp) {
164 match op {
165 InputFieldOp::InsertChar(c) => self.insert_char(*c),
166 InputFieldOp::Backspace => self.backspace(),
167 InputFieldOp::DeleteForward => self.delete_forward(),
168 InputFieldOp::CursorLeft => self.cursor_left(),
169 InputFieldOp::CursorRight => self.cursor_right(),
170 InputFieldOp::CursorHome => self.cursor_home(),
171 InputFieldOp::CursorEnd => self.cursor_end(),
172 InputFieldOp::SetText(t) => self.set_text(t.clone()),
173 }
174 }
175
176 pub fn render(&self, frame: &mut Frame, area: Rect, focused: bool, theme: &Theme) {
180 if area.height == 0 || area.width == 0 {
181 return;
182 }
183
184 let bg = if focused {
185 theme.bg_active()
186 } else {
187 theme.bg_inactive()
188 };
189 let fg = if focused {
190 theme.fg_active()
191 } else {
192 theme.fg_dim()
193 };
194 let accent = theme.accent();
195
196 let mut spans = Vec::new();
197
198 if !self.label.is_empty() {
200 spans.push(Span::styled(
201 format!(" {}: ", self.label),
202 Style::new().fg(accent).add_modifier(Modifier::BOLD),
203 ));
204 }
205
206 if focused {
207 let (before, after) = self.text.split_at(self.cursor);
209 spans.push(Span::styled(before.to_string(), Style::new().fg(fg).bg(bg)));
210
211 let cursor_char = after.chars().next().unwrap_or(' ');
213 spans.push(Span::styled(
214 cursor_char.to_string(),
215 Style::new()
216 .fg(Color::Black)
217 .bg(Color::White)
218 .add_modifier(Modifier::BOLD),
219 ));
220
221 if !after.is_empty() {
223 let rest_start = cursor_char.len_utf8();
224 if rest_start < after.len() {
225 spans.push(Span::styled(
226 after[rest_start..].to_string(),
227 Style::new().fg(fg).bg(bg),
228 ));
229 }
230 }
231 } else {
232 spans.push(Span::styled(
233 self.text.clone(),
234 Style::new().fg(fg).bg(bg),
235 ));
236 }
237
238 frame.render_widget(
239 Paragraph::new(Line::from(spans)).style(Style::new().bg(bg)),
240 area,
241 );
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248
249 #[test]
250 fn test_new_empty() {
251 let field = InputField::new("Filter");
252 assert_eq!(field.text(), "");
253 assert_eq!(field.cursor(), 0);
254 assert_eq!(field.label(), "Filter");
255 assert!(field.is_empty());
256 }
257
258 #[test]
259 fn test_with_text() {
260 let field = InputField::new("").with_text("hello");
261 assert_eq!(field.text(), "hello");
262 assert_eq!(field.cursor(), 5);
263 }
264
265 #[test]
266 fn test_insert_char() {
267 let mut field = InputField::new("");
268 field.insert_char('a');
269 field.insert_char('b');
270 assert_eq!(field.text(), "ab");
271 assert_eq!(field.cursor(), 2);
272 }
273
274 #[test]
275 fn test_insert_char_middle() {
276 let mut field = InputField::new("").with_text("ac");
277 field.cursor_left();
278 field.insert_char('b');
279 assert_eq!(field.text(), "abc");
280 assert_eq!(field.cursor(), 2);
281 }
282
283 #[test]
284 fn test_backspace() {
285 let mut field = InputField::new("").with_text("abc");
286 field.backspace();
287 assert_eq!(field.text(), "ab");
288 assert_eq!(field.cursor(), 2);
289 }
290
291 #[test]
292 fn test_backspace_at_start() {
293 let mut field = InputField::new("").with_text("abc");
294 field.cursor_home();
295 field.backspace();
296 assert_eq!(field.text(), "abc");
297 assert_eq!(field.cursor(), 0);
298 }
299
300 #[test]
301 fn test_backspace_empty() {
302 let mut field = InputField::new("");
303 field.backspace();
304 assert_eq!(field.text(), "");
305 assert_eq!(field.cursor(), 0);
306 }
307
308 #[test]
309 fn test_delete_forward() {
310 let mut field = InputField::new("").with_text("abc");
311 field.cursor_home();
312 field.delete_forward();
313 assert_eq!(field.text(), "bc");
314 assert_eq!(field.cursor(), 0);
315 }
316
317 #[test]
318 fn test_delete_forward_at_end() {
319 let mut field = InputField::new("").with_text("abc");
320 field.delete_forward();
321 assert_eq!(field.text(), "abc");
322 assert_eq!(field.cursor(), 3);
323 }
324
325 #[test]
326 fn test_cursor_left_right() {
327 let mut field = InputField::new("").with_text("abc");
328 assert_eq!(field.cursor(), 3);
329 field.cursor_left();
330 assert_eq!(field.cursor(), 2);
331 field.cursor_left();
332 assert_eq!(field.cursor(), 1);
333 field.cursor_right();
334 assert_eq!(field.cursor(), 2);
335 }
336
337 #[test]
338 fn test_cursor_left_at_start() {
339 let mut field = InputField::new("").with_text("a");
340 field.cursor_home();
341 field.cursor_left();
342 assert_eq!(field.cursor(), 0);
343 }
344
345 #[test]
346 fn test_cursor_right_at_end() {
347 let mut field = InputField::new("").with_text("a");
348 field.cursor_right();
349 assert_eq!(field.cursor(), 1);
350 }
351
352 #[test]
353 fn test_cursor_home_end() {
354 let mut field = InputField::new("").with_text("hello");
355 field.cursor_home();
356 assert_eq!(field.cursor(), 0);
357 field.cursor_end();
358 assert_eq!(field.cursor(), 5);
359 }
360
361 #[test]
362 fn test_set_text() {
363 let mut field = InputField::new("");
364 field.set_text("new value".to_string());
365 assert_eq!(field.text(), "new value");
366 assert_eq!(field.cursor(), 9);
367 }
368
369 #[test]
370 fn test_utf8_multibyte() {
371 let mut field = InputField::new("");
372 field.insert_char('é');
373 field.insert_char('ñ');
374 assert_eq!(field.text(), "éñ");
375 assert_eq!(field.cursor(), 4);
377 field.cursor_left();
378 assert_eq!(field.cursor(), 2);
379 field.backspace();
380 assert_eq!(field.text(), "ñ");
381 assert_eq!(field.cursor(), 0);
382 }
383
384 #[test]
385 fn test_utf8_emoji() {
386 let mut field = InputField::new("");
387 field.insert_char('🎉');
388 assert_eq!(field.text(), "🎉");
389 assert_eq!(field.cursor(), 4); field.backspace();
391 assert_eq!(field.text(), "");
392 assert_eq!(field.cursor(), 0);
393 }
394
395 #[test]
396 fn test_apply_op() {
397 let mut field = InputField::new("");
398 field.apply(&InputFieldOp::InsertChar('h'));
399 field.apply(&InputFieldOp::InsertChar('i'));
400 assert_eq!(field.text(), "hi");
401 field.apply(&InputFieldOp::CursorLeft);
402 field.apply(&InputFieldOp::Backspace);
403 assert_eq!(field.text(), "i");
404 field.apply(&InputFieldOp::CursorHome);
405 field.apply(&InputFieldOp::DeleteForward);
406 assert_eq!(field.text(), "");
407 }
408
409 #[test]
410 fn test_apply_set_text() {
411 let mut field = InputField::new("");
412 field.apply(&InputFieldOp::SetText("replaced".to_string()));
413 assert_eq!(field.text(), "replaced");
414 assert_eq!(field.cursor(), 8);
415 }
416
417 #[test]
418 fn test_delete_forward_middle() {
419 let mut field = InputField::new("").with_text("abc");
420 field.cursor_home();
421 field.cursor_right();
422 field.delete_forward();
423 assert_eq!(field.text(), "ac");
424 assert_eq!(field.cursor(), 1);
425 }
426
427 #[test]
428 fn test_backspace_middle() {
429 let mut field = InputField::new("").with_text("abc");
430 field.cursor_left();
431 field.backspace();
432 assert_eq!(field.text(), "ac");
433 assert_eq!(field.cursor(), 1);
434 }
435}