From 5d4940adc6cb22d87616ce5833925e26eb1a2129 Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Mon, 10 Oct 2022 16:52:06 +0200 Subject: [PATCH] Add input screen --- rust/cli_player/src/main.rs | 5 +- .../cli_player/src/ui_screens/input_screen.rs | 171 ++++++++++++++++++ rust/cli_player/src/ui_screens/mod.rs | 1 + .../src/ui_widgets/button_widget.rs | 15 +- 4 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 rust/cli_player/src/ui_screens/input_screen.rs diff --git a/rust/cli_player/src/main.rs b/rust/cli_player/src/main.rs index e7bc837..d2a144f 100644 --- a/rust/cli_player/src/main.rs +++ b/rust/cli_player/src/main.rs @@ -23,7 +23,10 @@ async fn run_app(terminal: &mut Terminal) -> Result<(), Box { + 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 + } + + /// 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) + } + } +} diff --git a/rust/cli_player/src/ui_screens/mod.rs b/rust/cli_player/src/ui_screens/mod.rs index 24aa98d..460d17e 100644 --- a/rust/cli_player/src/ui_screens/mod.rs +++ b/rust/cli_player/src/ui_screens/mod.rs @@ -1,5 +1,6 @@ pub mod configure_game_rules; pub mod confirm_dialog; +pub mod input_screen; pub mod popup_screen; pub mod select_bot_type; pub mod select_play_mode; diff --git a/rust/cli_player/src/ui_widgets/button_widget.rs b/rust/cli_player/src/ui_widgets/button_widget.rs index bc7b90a..4fa10d7 100644 --- a/rust/cli_player/src/ui_widgets/button_widget.rs +++ b/rust/cli_player/src/ui_widgets/button_widget.rs @@ -12,6 +12,7 @@ pub struct ButtonWidget { is_hovered: bool, label: String, disabled: bool, + min_width: usize, } impl ButtonWidget { @@ -20,6 +21,7 @@ impl ButtonWidget { label: label.to_string(), is_hovered, disabled: false, + min_width: 0, } } @@ -27,11 +29,22 @@ impl ButtonWidget { self.disabled = disabled; self } + + pub fn set_min_width(mut self, min_width: usize) -> Self { + self.min_width = min_width; + self + } } impl Widget for ButtonWidget { fn render(self, area: Rect, buf: &mut Buffer) { - let label = format!(" {} ", self.label); + let expected_len = (self.label.len() + 2).max(self.min_width); + + let mut label = self.label.clone(); + while label.len() < expected_len { + label.insert(0, ' '); + label.push(' '); + } let area = centered_rect_size(label.len() as u16, 1, &area);