use std::fmt::Display; use std::io; use std::time::{Duration, Instant}; use tui::style::*; use crossterm::event; use crossterm::event::{Event, KeyCode}; use tui::backend::Backend; use tui::layout::*; use tui::widgets::*; use tui::{Frame, Terminal}; use crate::constants::*; use crate::ui_screens::utils::*; use crate::ui_screens::ScreenResult; use crate::ui_widgets::button_widget::ButtonWidget; use crate::ui_widgets::text_editor_widget::TextEditorWidget; pub struct InputScreen<'a> { title: &'a str, msg: &'a str, input_label: &'a str, value: String, can_cancel: bool, is_cancel_hovered: bool, value_required: bool, min_len: usize, max_len: usize, has_already_been_edited: bool, } impl<'a> InputScreen<'a> { pub fn new(msg: &'a str) -> Self { Self { title: "Input", msg, input_label: "", value: "".to_string(), can_cancel: true, is_cancel_hovered: false, value_required: true, min_len: 1, max_len: 10, has_already_been_edited: false, } } pub fn set_title(mut self, title: &'a str) -> Self { self.title = title; self } pub fn set_value(mut self, value: D) -> Self { self.value = value.to_string(); self } /// Get error contained in input fn error(&self) -> Option<&'static str> { if self.value.len() > self.max_len { Some("Input is too large!") } else if self.value.is_empty() && !self.value_required { None } else if self.value.len() < self.min_len { Some("Input is too small!") } else { None } } pub fn show( mut self, terminal: &mut Terminal, ) -> io::Result> { let mut last_tick = Instant::now(); loop { terminal.draw(|f| self.ui(f))?; let timeout = TICK_RATE .checked_sub(last_tick.elapsed()) .unwrap_or_else(|| Duration::from_secs(0)); if event::poll(timeout)? { if let Event::Key(key) = event::read()? { match key.code { KeyCode::Esc => return Ok(ScreenResult::Canceled), KeyCode::Tab if self.can_cancel => { self.is_cancel_hovered = !self.is_cancel_hovered; } KeyCode::Enter => { if self.is_cancel_hovered { return Ok(ScreenResult::Canceled); } else if self.error().is_none() { return Ok(ScreenResult::Ok(self.value)); } } KeyCode::Backspace => { self.value.pop(); } KeyCode::Char(c) => { if self.value.len() < self.max_len { self.has_already_been_edited = true; self.value.push(c); } } _ => {} } } } if last_tick.elapsed() >= TICK_RATE { last_tick = Instant::now(); } } } fn ui(&mut self, f: &mut Frame) { let area = centered_rect_size( (self.msg.len() + 4).max(self.max_len + 4) as u16, 7, &f.size(), ); let error = self.error(); let block = Block::default().borders(Borders::ALL).title(self.title); f.render_widget(block, area); // Create two chunks with equal horizontal screen space let chunks = Layout::default() .direction(Direction::Vertical) .constraints( [ Constraint::Length(1), Constraint::Length(3), Constraint::Length(3), ] .as_ref(), ) .split(area.inner(&Margin { horizontal: 2, vertical: 1, })); let paragraph = Paragraph::new(self.msg); f.render_widget(paragraph, chunks[0]); let input_widget = TextEditorWidget::new(self.input_label, &self.value, !self.is_cancel_hovered); f.render_widget(input_widget, chunks[1]); let buttons_area = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .split(*chunks.last().unwrap()); let cancel_button = ButtonWidget::new("Cancel", self.is_cancel_hovered) .set_disabled(!self.can_cancel) .set_min_width(8); f.render_widget(cancel_button, buttons_area[0]); let ok_button = ButtonWidget::new("OK", !self.is_cancel_hovered) .set_min_width(8) .set_disabled(error.is_some()); f.render_widget(ok_button, buttons_area[1]); // Render error (if any) if let (Some(e), true) = (error, self.has_already_been_edited) { let target_area = centered_text( e, &Rect::new(f.size().x, area.bottom() + 2, f.size().width, 1), ); let paragraph = Paragraph::new(e).style(Style::default().fg(Color::Red)); f.render_widget(paragraph, target_area) } } }