Skip to main content

gm_lib/tui/app/widgets/
input_box.rs

1use crossterm::event::{KeyCode, KeyModifiers};
2use ratatui::{
3    layout::{Offset, Rect},
4    text::Span,
5    widgets::{Block, Widget},
6};
7
8use crate::tui::theme::Theme;
9use crate::{
10    tui::{
11        traits::{HandleResult, WidgetHeight},
12        Event,
13    },
14    utils::text::split_string,
15};
16
17fn option_delete(text_input: &mut String, text_cursor: &mut usize) {
18    loop {
19        if *text_cursor == 0 {
20            break;
21        }
22        text_input.remove(*text_cursor - 1);
23        *text_cursor -= 1;
24        if *text_cursor == 0 {
25            break;
26        }
27        let next_char = text_input.chars().nth(*text_cursor - 1).unwrap_or(' ');
28        if next_char == ' ' {
29            break;
30        }
31    }
32}
33
34fn option_left(text_input: &str, text_cursor: &mut usize) {
35    loop {
36        if *text_cursor == 0 {
37            break;
38        }
39        *text_cursor -= 1;
40        let cur_char = text_input.chars().nth(*text_cursor).unwrap_or(' ');
41        if cur_char == ' ' {
42            break;
43        }
44    }
45}
46
47fn option_right(text_input: &str, text_cursor: &mut usize) {
48    loop {
49        if *text_cursor == text_input.len() {
50            break;
51        }
52        *text_cursor += 1;
53        let cur_char = text_input.chars().nth(*text_cursor).unwrap_or(' ');
54        if cur_char == ' ' {
55            break;
56        }
57    }
58}
59
60pub struct InputBox<'a> {
61    pub focus: bool,
62    pub label: &'static str,
63    pub text: &'a String,
64    pub empty_text: Option<&'static str>,
65    pub currency: Option<&'a String>,
66}
67
68impl InputBox<'_> {
69    pub fn handle_events(
70        text_input: &mut String,
71        text_cursor: &mut usize,
72        event: &Event,
73    ) -> crate::Result<HandleResult> {
74        let result = HandleResult::default();
75
76        if let Event::Input(key_event) = event {
77            match key_event.code {
78                KeyCode::Left => {
79                    if key_event.modifiers == KeyModifiers::ALT {
80                        option_left(text_input, text_cursor);
81                    } else if *text_cursor > 0 {
82                        *text_cursor -= 1
83                    }
84                }
85                KeyCode::Right => {
86                    if key_event.modifiers == KeyModifiers::ALT {
87                        option_right(text_input, text_cursor);
88                    } else if *text_cursor < text_input.len() {
89                        *text_cursor += 1
90                    }
91                }
92                KeyCode::Char(char) => {
93                    // Handle space key on empty state
94                    if text_input.is_empty() && char == ' ' {
95                        // Ignore leading spaces
96                    }
97                    // Handle command + delete on macOS
98                    else if char == 'u' && key_event.modifiers == KeyModifiers::CONTROL {
99                        let (_, right) = text_input.split_at(*text_cursor);
100                        *text_input = right.to_string();
101                        *text_cursor = 0;
102                    }
103                    // Handle command + left on macOS
104                    else if char == 'a' && key_event.modifiers == KeyModifiers::CONTROL {
105                        *text_cursor = 0;
106                    }
107                    // Handle command + right on macOS
108                    else if char == 'e' && key_event.modifiers == KeyModifiers::CONTROL {
109                        *text_cursor = text_input.len();
110                    }
111                    // Handle option + delete on macOS
112                    else if char == 'w' && key_event.modifiers == KeyModifiers::CONTROL {
113                        option_delete(text_input, text_cursor);
114                    }
115                    // option + Left
116                    else if char == 'b' && key_event.modifiers == KeyModifiers::ALT {
117                        option_left(text_input, text_cursor);
118                    }
119                    // option + Right
120                    else if char == 'f' && key_event.modifiers == KeyModifiers::ALT {
121                        option_right(text_input, text_cursor);
122                    }
123                    // Simple char press
124                    else {
125                        text_input.insert(*text_cursor, char);
126                        *text_cursor += 1;
127                    }
128                }
129                KeyCode::Backspace => {
130                    if key_event.modifiers == KeyModifiers::ALT {
131                        option_delete(text_input, text_cursor);
132                    } else if *text_cursor > 0 {
133                        *text_cursor -= 1;
134                        text_input.remove(*text_cursor);
135                    }
136                }
137                _ => {}
138            }
139        }
140
141        // *text_input = event.fmt();
142
143        Ok(result)
144    }
145
146    pub fn render(
147        self,
148        area: ratatui::prelude::Rect,
149        buf: &mut ratatui::prelude::Buffer,
150        text_cursor: &usize,
151        theme: &Theme,
152    ) where
153        Self: Sized,
154    {
155        let lines = split_string(self.text, (area.width - 2) as usize);
156        let area_used = Rect {
157            x: area.x,
158            y: area.y,
159            width: area.width,
160            height: (2 + lines.len()) as u16,
161        };
162
163        let block = Block::bordered()
164            .border_type(theme.into())
165            .title(self.label);
166        let inner_area = block.inner(area_used);
167        block.render(area_used, buf);
168
169        if lines.len() == 1 && !lines.last().unwrap().is_empty() && self.currency.is_some() {
170            let currency = self.currency.unwrap();
171            Span::from(currency).render(
172                inner_area.offset(Offset {
173                    x: lines.last().unwrap().len() as i32 + 1,
174                    y: 0,
175                }),
176                buf,
177            );
178        }
179
180        for (idx, line) in lines.iter().enumerate() {
181            line.render(
182                inner_area.offset(Offset {
183                    x: 0,
184                    y: idx as i32,
185                }),
186                buf,
187            );
188        }
189
190        if self.focus {
191            if self.text.is_empty() && self.empty_text.is_some() {
192                self.empty_text.unwrap().render(inner_area, buf);
193            } else {
194                Span::from("|").render(
195                    Rect {
196                        x: inner_area.x + (*text_cursor as u16) % (area.width - 2),
197                        y: inner_area.y + (*text_cursor as u16) / (area.width - 2),
198                        width: 1,
199                        height: 1,
200                    },
201                    buf,
202                );
203            }
204        }
205    }
206}
207
208impl WidgetHeight for InputBox<'_> {
209    fn height_used(&self, area: ratatui::prelude::Rect) -> u16 {
210        let lines = split_string(self.text, (area.width - 2) as usize);
211        (2 + lines.len()) as u16
212    }
213}