Add input screen

This commit is contained in:
Pierre HUBERT 2022-10-10 16:52:06 +02:00
parent 3e3169dd27
commit 5d4940adc6
4 changed files with 190 additions and 2 deletions

View File

@ -23,7 +23,10 @@ async fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> Result<(), Box<dyn E
rules.boats_can_touch = true; rules.boats_can_touch = true;
let res = set_boats_layout::set_boat_layout(&rules, terminal)?; // select_bot_type::select_bot_type(terminal)?;*/ let res = set_boats_layout::set_boat_layout(&rules, terminal)?; // select_bot_type::select_bot_type(terminal)?;*/
/*let res = popup_screen::popup_screen("Hi\nWelcome there", terminal);*/ /*let res = popup_screen::popup_screen("Hi\nWelcome there", terminal);*/
let res = confirm_dialog::confirm_dialog("Do you really want to interrupt game ?", terminal)?; // select_bot_type::select_bot_type(terminal)?; // let res = confirm_dialog::confirm_dialog("Do you really want to interrupt game ?", terminal)?; // select_bot_type::select_bot_type(terminal)?;
let res = input_screen::InputScreen::new("Whas it your name ?")
.set_title("custom title")
.show(terminal)?; // select_bot_type::select_bot_type(terminal)?;
Err(io::Error::new( Err(io::Error::new(
ErrorKind::Other, ErrorKind::Other,
format!("result: {:?}", res), format!("result: {:?}", res),

View File

@ -0,0 +1,171 @@
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
}
/// 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<B: Backend>(
mut self,
terminal: &mut Terminal<B>,
) -> io::Result<ScreenResult<String>> {
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<B: Backend>(&mut self, f: &mut Frame<B>) {
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)
}
}
}

View File

@ -1,5 +1,6 @@
pub mod configure_game_rules; pub mod configure_game_rules;
pub mod confirm_dialog; pub mod confirm_dialog;
pub mod input_screen;
pub mod popup_screen; pub mod popup_screen;
pub mod select_bot_type; pub mod select_bot_type;
pub mod select_play_mode; pub mod select_play_mode;

View File

@ -12,6 +12,7 @@ pub struct ButtonWidget {
is_hovered: bool, is_hovered: bool,
label: String, label: String,
disabled: bool, disabled: bool,
min_width: usize,
} }
impl ButtonWidget { impl ButtonWidget {
@ -20,6 +21,7 @@ impl ButtonWidget {
label: label.to_string(), label: label.to_string(),
is_hovered, is_hovered,
disabled: false, disabled: false,
min_width: 0,
} }
} }
@ -27,11 +29,22 @@ impl ButtonWidget {
self.disabled = disabled; self.disabled = disabled;
self self
} }
pub fn set_min_width(mut self, min_width: usize) -> Self {
self.min_width = min_width;
self
}
} }
impl Widget for ButtonWidget { impl Widget for ButtonWidget {
fn render(self, area: Rect, buf: &mut Buffer) { 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); let area = centered_rect_size(label.len() as u16, 1, &area);