gm_lib/tui/app/widgets/
input_box.rs1use 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 if text_input.is_empty() && char == ' ' {
95 }
97 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 else if char == 'a' && key_event.modifiers == KeyModifiers::CONTROL {
105 *text_cursor = 0;
106 }
107 else if char == 'e' && key_event.modifiers == KeyModifiers::CONTROL {
109 *text_cursor = text_input.len();
110 }
111 else if char == 'w' && key_event.modifiers == KeyModifiers::CONTROL {
113 option_delete(text_input, text_cursor);
114 }
115 else if char == 'b' && key_event.modifiers == KeyModifiers::ALT {
117 option_left(text_input, text_cursor);
118 }
119 else if char == 'f' && key_event.modifiers == KeyModifiers::ALT {
121 option_right(text_input, text_cursor);
122 }
123 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 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}