1use std::{collections::HashMap, marker::PhantomData};
2
3use crossterm::event::{KeyCode, KeyEventKind};
4use ratatui::layout::{Constraint, Layout};
5use ratatui::{
6 buffer::Buffer,
7 layout::Rect,
8 style::Stylize,
9 text::Text,
10 widgets::{Paragraph, Widget, Wrap},
11};
12use strum::IntoEnumIterator;
13
14use super::{button::Button, input_box::InputBox};
15use crate::tui::app::widgets::scroll_bar::CustomScrollBar;
16use crate::tui::traits::{HandleResult, RectUtil};
17use crate::tui::{
18 app::widgets::filter_select_popup::FilterSelectPopup, theme::Theme, traits::WidgetHeight, Event,
19};
20use crate::utils::text::split_string;
21
22pub trait FormItemIndex {
23 fn index(self) -> usize;
24}
25
26#[derive(Clone, Debug)]
27pub enum FormWidget {
28 Heading(&'static str),
29 StaticText(&'static str),
30 InputBox {
31 label: &'static str,
32 text: String,
33 empty_text: Option<&'static str>,
34 currency: Option<String>,
35 },
36 BooleanInput {
37 label: &'static str,
38 value: bool,
39 },
40 DisplayBox {
41 label: &'static str,
42 text: String,
43 empty_text: Option<&'static str>,
44 },
45 SelectInput {
46 label: &'static str,
47 text: String,
48 empty_text: Option<&'static str>,
49 popup: FilterSelectPopup<String>,
50 },
51 Button {
52 label: &'static str,
53 },
54 DisplayText(String),
55 ErrorText(String),
56}
57
58impl FormWidget {
59 pub fn label(&self) -> Option<&'static str> {
60 match self {
61 FormWidget::InputBox { label, .. } => Some(label),
62 FormWidget::DisplayBox { label, .. } => Some(label),
63 FormWidget::BooleanInput { label, .. } => Some(label),
64 FormWidget::Button { label } => Some(label),
65 FormWidget::SelectInput { label, .. } => Some(label),
66 FormWidget::Heading(_)
67 | FormWidget::StaticText(_)
68 | FormWidget::DisplayText(_)
69 | FormWidget::ErrorText(_) => None,
70 }
71 }
72
73 pub fn max_cursor(&self) -> usize {
74 match self {
75 FormWidget::InputBox { text, .. }
76 | FormWidget::DisplayBox { text, .. }
77 | FormWidget::SelectInput { text, .. } => text.len(),
78 FormWidget::BooleanInput { value, .. } => value.to_string().len(),
79 FormWidget::Button { .. }
80 | FormWidget::Heading(_)
81 | FormWidget::StaticText(_)
82 | FormWidget::DisplayText(_)
83 | FormWidget::ErrorText(_) => 0,
84 }
85 }
86
87 pub fn to_value(&self) -> Option<String> {
88 match self {
89 FormWidget::InputBox { text, .. }
90 | FormWidget::DisplayBox { text, .. }
91 | FormWidget::SelectInput { text, .. } => Some(text.clone()),
92 FormWidget::BooleanInput { value, .. } => Some(value.to_string()),
93 FormWidget::Button { .. }
94 | FormWidget::Heading(_)
95 | FormWidget::StaticText(_)
96 | FormWidget::DisplayText(_)
97 | FormWidget::ErrorText(_) => None,
98 }
99 }
100
101 pub fn height(&self, area: Rect) -> u16 {
102 match self {
103 FormWidget::InputBox { text, .. }
104 | FormWidget::DisplayBox { text, .. }
105 | FormWidget::SelectInput { text, .. } => {
106 let lines = split_string(text, (area.width - 2) as usize);
107 (3 + lines.len()) as u16
108 }
109
110 FormWidget::BooleanInput { value, .. } => {
111 let value = value.to_string();
112 let lines = split_string(&value, (area.width - 2) as usize);
113 (2 + lines.len()) as u16
114 }
115 FormWidget::Button { .. } => 4,
116 FormWidget::Heading(text) | FormWidget::StaticText(text) => {
117 (text.len() as u16).div_ceil(area.width) + 1
118 }
119 FormWidget::DisplayText(text) | FormWidget::ErrorText(text) => {
120 (text.len() as u16).div_ceil(area.width) + 3
121 }
122 }
123 }
124}
125
126#[derive(Debug)]
127pub struct Form<E: IntoEnumIterator + FormItemIndex + TryInto<FormWidget, Error = crate::Error>> {
128 pub cursor: usize,
129 pub text_cursor: usize,
130 pub form_focus: bool,
131 pub items: Vec<FormWidget>,
132 pub hide: HashMap<usize, bool>,
133 pub everything_empty: bool,
134 pub _phantom: PhantomData<E>,
135}
136
137impl<E: IntoEnumIterator + FormItemIndex + TryInto<FormWidget, Error = crate::Error>> Form<E> {
138 pub fn init<F>(set_values_closure: F) -> crate::Result<Self>
141 where
142 F: FnOnce(&mut Self) -> crate::Result<()>,
143 {
144 let mut form = Self {
145 cursor: 0,
146 text_cursor: 0,
147 form_focus: true,
148 items: E::iter()
149 .map(|item| item.try_into())
150 .collect::<Result<Vec<FormWidget>, _>>()?,
151 hide: HashMap::new(),
152 everything_empty: false,
153 _phantom: PhantomData,
154 };
155 for i in 0..form.items.len() {
156 if form.is_valid_cursor(i) {
157 break;
158 } else {
159 form.cursor += 1;
160 }
161 }
162 set_values_closure(&mut form)?;
163 form.text_cursor = form.items[form.cursor].max_cursor();
164
165 Ok(form)
166 }
167
168 pub fn set_form_focus(&mut self, focus: bool) {
169 self.form_focus = focus;
170 }
171
172 pub fn show_everything_empty(&mut self, empty: bool) {
173 self.everything_empty = empty;
174 }
175
176 pub fn hide_item(&mut self, idx: E) {
177 self.hide.insert(idx.index(), true);
178 }
179
180 pub fn show_item(&mut self, idx: E) {
181 self.hide.remove(&idx.index());
182 }
183
184 pub fn hidden_count(&self) -> usize {
185 self.hide.len()
186 }
187
188 pub fn visible_count(&self) -> usize {
189 self.items.len() - self.hidden_count()
190 }
191
192 pub fn advance_cursor(&mut self) {
193 loop {
194 self.cursor = (self.cursor + 1) % self.items.len();
195 self.update_text_cursor();
196
197 if self.is_valid_cursor(self.cursor) {
198 break;
199 }
200 }
201 }
202
203 pub fn retreat_cursor(&mut self) {
204 loop {
205 self.cursor = (self.cursor + self.items.len() - 1) % self.items.len();
206 self.update_text_cursor();
207
208 if self.is_valid_cursor(self.cursor) {
209 break;
210 }
211 }
212 }
213
214 pub fn update_text_cursor(&mut self) {
215 self.text_cursor = self.items[self.cursor].max_cursor();
216 }
217
218 pub fn is_valid_cursor(&self, idx: usize) -> bool {
219 if self.hide.contains_key(&idx) {
220 return false;
221 }
222
223 match &self.items[idx] {
224 FormWidget::Heading(_)
225 | FormWidget::StaticText(_)
226 | FormWidget::DisplayText(_)
227 | FormWidget::ErrorText(_) => false,
228
229 FormWidget::InputBox { .. }
230 | FormWidget::DisplayBox { .. }
231 | FormWidget::BooleanInput { .. }
232 | FormWidget::SelectInput { .. }
233 | FormWidget::Button { .. } => true,
234 }
235 }
236
237 pub fn get_text(&self, idx: E) -> &String {
238 match &self.items[idx.index()] {
239 FormWidget::InputBox { text, .. } => text,
240 FormWidget::DisplayBox { text, .. } => text,
241 FormWidget::DisplayText(text) => text,
242 FormWidget::ErrorText(text) => text,
243 FormWidget::SelectInput { text, .. } => text,
244 _ => unreachable!(),
245 }
246 }
247
248 pub fn get_text_mut(&mut self, idx: E) -> &mut String {
249 match &mut self.items[idx.index()] {
250 FormWidget::InputBox { text, .. } => text,
251 FormWidget::DisplayBox { text, .. } => text,
252 FormWidget::DisplayText(text) => text,
253 FormWidget::ErrorText(text) => text,
254 FormWidget::SelectInput { text, .. } => text,
255 _ => unreachable!(),
256 }
257 }
258
259 pub fn get_boolean(&self, idx: E) -> bool {
260 match &self.items[idx.index()] {
261 FormWidget::BooleanInput { value, .. } => *value,
262 _ => unreachable!(),
263 }
264 }
265
266 pub fn get_boolean_mut(&mut self, idx: E) -> &mut bool {
267 match &mut self.items[idx.index()] {
268 FormWidget::BooleanInput { value, .. } => value,
269 _ => unreachable!(),
270 }
271 }
272
273 pub fn get_currency_mut(&mut self, idx: E) -> Option<&mut Option<String>> {
274 match &mut self.items[idx.index()] {
275 FormWidget::InputBox { currency, .. } => Some(currency),
276 _ => None,
277 }
278 }
279
280 pub fn get_popup_mut(&mut self, idx: E) -> &mut FilterSelectPopup<String> {
281 match &mut self.items[idx.index()] {
282 FormWidget::SelectInput { popup, .. } => popup,
283 _ => unreachable!(),
284 }
285 }
286
287 pub fn is_focused(&self, idx: E) -> bool {
288 self.cursor == idx.index()
289 }
290
291 pub fn is_button_focused(&self) -> bool {
292 matches!(self.items[self.cursor], FormWidget::Button { .. })
293 }
294
295 pub fn is_some_popup_open(&self) -> bool {
296 self.items
297 .iter()
298 .any(|item| matches!(item, FormWidget::SelectInput { popup, .. } if popup.is_open()))
299 }
300
301 pub fn is_select_focused(&self) -> bool {
302 matches!(self.items[self.cursor], FormWidget::SelectInput { .. })
303 }
304
305 pub fn current_label_enum(&self) -> crate::Result<E> {
306 E::iter()
307 .nth(self.cursor)
308 .ok_or(crate::Error::InternalErrorStr("form index out of bounds"))
309 }
310
311 pub fn handle_event<F1, F2>(
312 &mut self,
313 event: &Event,
314 mut on_value_change: F2,
315 mut on_button_press: F1,
316 ) -> crate::Result<HandleResult>
317 where
318 F1: FnMut(E, &mut Self) -> crate::Result<()>,
319 F2: FnMut(E, &mut Self) -> crate::Result<()>,
320 {
321 let mut result = HandleResult::default();
322
323 if let Event::Input(key_event) = event {
324 if key_event.kind == KeyEventKind::Press {
325 if !self.is_some_popup_open() {
326 match key_event.code {
327 KeyCode::Up => {
328 self.retreat_cursor();
329 }
330 KeyCode::Down | KeyCode::Tab => {
331 self.advance_cursor();
332 }
333 KeyCode::Enter => {
334 if !self.is_button_focused() && !self.is_select_focused() {
335 self.advance_cursor();
336 }
337 }
338
339 _ => {}
340 }
341 }
342
343 let value_before = self.items[self.cursor].to_value();
344
345 match &mut self.items[self.cursor] {
346 FormWidget::InputBox { text, .. } => {
347 InputBox::handle_events(text, &mut self.text_cursor, event)?;
348 }
349 FormWidget::DisplayBox { .. } => {
350 }
352 FormWidget::BooleanInput { value, .. } => {
353 if matches!(
354 key_event.code,
355 KeyCode::Char(_) | KeyCode::Left | KeyCode::Right | KeyCode::Backspace
356 ) {
357 *value = !*value;
358 self.text_cursor = value.to_string().len();
359 }
360 }
361 FormWidget::SelectInput { text, popup, .. } => {
362 let is_open = popup.is_open();
363
364 let popup_result = popup.handle_event(event, |selected| {
365 *text = selected.clone();
366 self.text_cursor = selected.len();
367 Ok(())
368 })?;
369 result.merge(popup_result);
370
371 if !is_open {
372 match key_event.code {
373 KeyCode::Backspace | KeyCode::Char(_) | KeyCode::Enter => {
375 popup.open();
376 }
377 _ => {}
378 }
379 }
380 }
381 FormWidget::Button { .. } => {
382 if matches!(key_event.code, KeyCode::Enter) {
383 on_button_press(self.current_label_enum()?, self)?
384 }
385 }
386 _ => {}
387 }
388
389 let value_after = self.items[self.cursor].to_value();
390 if value_after != value_before {
391 on_value_change(self.current_label_enum()?, self)?;
392 }
393 }
394 }
395 Ok(result)
396 }
397
398 pub fn render(&self, mut area: Rect, buf: &mut Buffer, theme: &Theme)
399 where
400 Self: Sized,
401 {
402 let full_area = area;
403 let form_height: u16 = std::cmp::max(
404 self.items.iter().fold(0, |acc, i| acc + i.height(area)),
405 full_area.height,
406 );
407 let mut virtual_buf = Buffer::empty(Rect::new(0, 0, buf.area.width, form_height));
408 let mut scroll_cursor: u16 = 0;
409 let mut scroll_cursor_item_height: u16 = 0;
410 let horizontal_layout = Layout::horizontal([Constraint::Min(3), Constraint::Length(1)]);
411 let [form_area, scroll_area] = horizontal_layout.areas(area);
412 if full_area.height < form_height {
413 area = form_area;
414 }
415
416 for (i, item) in self.items.iter().enumerate() {
417 if self.hide.contains_key(&i) {
418 continue; }
420 if self.form_focus && self.cursor == i {
421 scroll_cursor = area.y;
422 scroll_cursor_item_height = item.height(area);
423 }
424
425 match item {
426 FormWidget::Heading(heading) => {
427 heading.bold().render(area, &mut virtual_buf);
428 area.y += 2;
429 }
430 FormWidget::StaticText(text) => {
431 text.render(area, &mut virtual_buf);
432 area.y += 2;
433 }
434 FormWidget::InputBox {
435 label,
436 text,
437 empty_text,
438 currency,
439 } => {
440 let widget = InputBox {
441 focus: self.form_focus && self.cursor == i,
442 label,
443 text: if !self.everything_empty {
444 text
445 } else {
446 &"".to_string()
447 },
448 empty_text: if !self.everything_empty {
449 *empty_text
450 } else {
451 Some("")
452 },
453 currency: currency.as_ref(),
454 };
455 let height_used = widget.height_used(area); widget.render(area, &mut virtual_buf, &self.text_cursor, theme);
458 area.y += height_used;
459 }
460 FormWidget::DisplayBox {
461 label,
462 text,
463 empty_text,
464 } => {
465 let widget = InputBox {
466 focus: self.form_focus && self.cursor == i,
467 label,
468 text: if !self.everything_empty {
469 text
470 } else {
471 &"".to_string()
472 },
473 empty_text: if !self.everything_empty {
474 *empty_text
475 } else {
476 Some("")
477 },
478 currency: None,
479 };
480 let height_used = widget.height_used(area); widget.render(area, &mut virtual_buf, &self.text_cursor, theme);
483 area.y += height_used;
484 }
485 FormWidget::BooleanInput { label, value } => {
486 let widget = InputBox {
487 focus: self.form_focus && self.cursor == i,
488 label,
489 text: if !self.everything_empty {
490 &value.to_string()
491 } else {
492 &"".to_string()
493 },
494 empty_text: None,
495 currency: None,
496 };
497 let height_used = widget.height_used(area); widget.render(area, &mut virtual_buf, &self.text_cursor, theme);
500 area.y += height_used;
501 }
502 FormWidget::SelectInput {
503 label,
504 text,
505 empty_text,
506 ..
507 } => {
508 let widget = InputBox {
509 focus: self.form_focus && self.cursor == i,
510 label,
511 text,
512 empty_text: *empty_text,
513 currency: None,
514 };
515 let height_used = widget.height_used(area); widget.render(area, &mut virtual_buf, &self.text_cursor, theme);
518 area.y += height_used;
519 }
520 FormWidget::Button { label } => {
521 Button {
522 focus: self.form_focus && self.cursor == i,
523 label,
524 }
525 .render(area, &mut virtual_buf, theme);
526
527 area.y += 4;
528 }
529 FormWidget::DisplayText(text) | FormWidget::ErrorText(text) => {
530 if !text.is_empty() {
531 area.y += 1;
532 Paragraph::new(Text::raw(text))
533 .wrap(Wrap { trim: false })
534 .render(area.margin_h(1), &mut virtual_buf);
535 area.y += (text.len() as u16).div_ceil(area.width) + 1;
536 }
537 }
538 }
539 }
540
541 if full_area.height < form_height {
542 CustomScrollBar {
544 cursor: scroll_cursor as usize,
545 total: form_height as usize,
546 }
547 .render(scroll_area, buf);
548 }
549
550 let mut page = area;
551 page.x = full_area.x;
552 page.height = full_area.height;
553 page.y = full_area.y;
554 let item_overflow_top = (page.y).saturating_sub(scroll_cursor - 1);
555 let item_overflow_bottom =
556 (scroll_cursor + scroll_cursor_item_height + 1).saturating_sub(page.y + page.height);
557 page.y = page.y.saturating_sub(item_overflow_top);
558 page.y = page.y.saturating_add(item_overflow_bottom);
559
560 let visible_area = page.intersection(virtual_buf.area);
561
562 for (src_row, dst_row) in visible_area.rows().zip(full_area.rows()) {
564 for (src_col, dst_col) in src_row.columns().zip(dst_row.columns()) {
565 if let Some(dst) = buf.cell_mut((dst_col.x, dst_col.y)) {
566 if let Some(src) = virtual_buf.cell((src_col.x, src_col.y)) {
567 *dst = src.clone();
568 }
569 };
570 }
571 }
572 for item in &self.items {
574 #[allow(clippy::single_match)]
575 match item {
576 FormWidget::SelectInput { popup, .. } => {
577 popup.render(full_area, buf, theme);
578 }
579 _ => {}
580 }
581 }
582 }
583}