From d90560d3306dc63939035effc733816cbfe41e2b Mon Sep 17 00:00:00 2001 From: Pierre Hubert Date: Mon, 10 Oct 2022 17:51:51 +0200 Subject: [PATCH] Improve cli screens structures --- rust/cli_player/src/cli_args.rs | 5 + rust/cli_player/src/main.rs | 40 +- .../src/ui_screens/configure_game_rules.rs | 344 ++++++++-------- .../src/ui_screens/confirm_dialog.rs | 178 ++++---- .../src/ui_screens/select_bot_type.rs | 139 ++++--- .../src/ui_screens/select_play_mode.rs | 104 ++--- .../src/ui_screens/set_boats_layout.rs | 384 +++++++++--------- 7 files changed, 620 insertions(+), 574 deletions(-) diff --git a/rust/cli_player/src/cli_args.rs b/rust/cli_player/src/cli_args.rs index 00b9285..95fb4cb 100644 --- a/rust/cli_player/src/cli_args.rs +++ b/rust/cli_player/src/cli_args.rs @@ -4,6 +4,11 @@ use clap::{Parser, ValueEnum}; pub enum TestDevScreen { Popup, Input, + Confirm, + SelectBotType, + SelectPlayMode, + SetBoatsLayout, + ConfigureGameRules, } #[derive(Parser, Debug)] diff --git a/rust/cli_player/src/main.rs b/rust/cli_player/src/main.rs index bf49cd6..80e5307 100644 --- a/rust/cli_player/src/main.rs +++ b/rust/cli_player/src/main.rs @@ -1,5 +1,4 @@ use std::error::Error; -use std::fmt::Debug; use std::io; use std::io::ErrorKind; @@ -15,31 +14,50 @@ use tui::backend::{Backend, CrosstermBackend}; use tui::Terminal; use cli_player::server::start_server_if_missing; -use cli_player::ui_screens::popup_screen::PopupScreen; use cli_player::ui_screens::*; use sea_battle_backend::data::GameRules; +/// Test code screens async fn run_dev( terminal: &mut Terminal, d: TestDevScreen, ) -> Result<(), Box> { let res = match d { - TestDevScreen::Popup => PopupScreen::new("Welcome there!!") + TestDevScreen::Popup => popup_screen::PopupScreen::new("Welcome there!!") .show(terminal)? .as_string(), - TestDevScreen::Input => input_screen::InputScreen::new("Whas it your name ?") + TestDevScreen::Input => input_screen::InputScreen::new("What it your name ?") .set_title("A custom title") .show(terminal)? .as_string(), + TestDevScreen::Confirm => { + confirm_dialog::ConfirmDialogScreen::new("Do you really want to quit game?") + .show(terminal)? + .as_string() + } + TestDevScreen::SelectBotType => select_bot_type::SelectBotTypeScreen::default() + .show(terminal)? + .as_string(), + TestDevScreen::SelectPlayMode => select_play_mode::SelectPlayModeScreen::default() + .show(terminal)? + .as_string(), + TestDevScreen::SetBoatsLayout => { + let rules = GameRules { + boats_can_touch: true, + ..Default::default() + }; + + set_boats_layout::SetBoatsLayoutScreen::new(&rules) + .show(terminal)? + .as_string() + } + TestDevScreen::ConfigureGameRules => { + configure_game_rules::GameRulesConfigurationScreen::new(GameRules::default()) + .show(terminal)? + .as_string() + } }; - // Temporary code - // let res = configure_game_rules::configure_play_rules(GameRules::default(), terminal)?; // select_bot_type::select_bot_type(terminal)?; - /*let mut rules = GameRules::default(); - rules.boats_can_touch = true; - let res = set_boats_layout::set_boat_layout(&rules, 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)?; - // select_bot_type::select_bot_type(terminal)?; Err(io::Error::new( ErrorKind::Other, format!("DEV result: {:?}", res), diff --git a/rust/cli_player/src/ui_screens/configure_game_rules.rs b/rust/cli_player/src/ui_screens/configure_game_rules.rs index 94bb344..e6aaa25 100644 --- a/rust/cli_player/src/ui_screens/configure_game_rules.rs +++ b/rust/cli_player/src/ui_screens/configure_game_rules.rs @@ -31,195 +31,199 @@ enum EditingField { OK, } -struct GameRulesConfigurationScreen { +pub struct GameRulesConfigurationScreen { rules: GameRules, curr_field: EditingField, } -pub fn configure_play_rules( - rules: GameRules, - terminal: &mut Terminal, -) -> io::Result> { - let mut model = GameRulesConfigurationScreen { - rules, - curr_field: EditingField::OK, - }; +impl GameRulesConfigurationScreen { + pub fn new(rules: GameRules) -> Self { + Self { + rules, + curr_field: EditingField::OK, + } + } - let mut last_tick = Instant::now(); - loop { - terminal.draw(|f| ui(f, &mut model))?; + 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)); + let timeout = TICK_RATE + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); - if crossterm::event::poll(timeout)? { - let mut cursor_pos = model.curr_field as i32; + if crossterm::event::poll(timeout)? { + let mut cursor_pos = self.curr_field as i32; - if let Event::Key(key) = event::read()? { - match key.code { - // Quit app - KeyCode::Char('q') => return Ok(ScreenResult::Canceled), + if let Event::Key(key) = event::read()? { + match key.code { + // Quit app + KeyCode::Char('q') => return Ok(ScreenResult::Canceled), - // Navigate between fields - KeyCode::Up | KeyCode::Left => cursor_pos -= 1, - KeyCode::Down | KeyCode::Right | KeyCode::Tab => cursor_pos += 1, + // Navigate between fields + KeyCode::Up | KeyCode::Left => cursor_pos -= 1, + KeyCode::Down | KeyCode::Right | KeyCode::Tab => cursor_pos += 1, - // Submit results - KeyCode::Enter => { - if model.curr_field == EditingField::Cancel { - return Ok(ScreenResult::Canceled); + // Submit results + KeyCode::Enter => { + if self.curr_field == EditingField::Cancel { + return Ok(ScreenResult::Canceled); + } + + if self.curr_field == EditingField::OK && self.rules.is_valid() { + return Ok(ScreenResult::Ok(self.rules)); + } } - if model.curr_field == EditingField::OK && model.rules.is_valid() { - return Ok(ScreenResult::Ok(model.rules)); + KeyCode::Char(' ') => { + if self.curr_field == EditingField::BoatsCanTouch { + self.rules.boats_can_touch = !self.rules.boats_can_touch; + } + + if self.curr_field == EditingField::PlayerContinueOnHit { + self.rules.player_continue_on_hit = + !self.rules.player_continue_on_hit; + } } + + KeyCode::Backspace => { + if self.curr_field == EditingField::MapWidth { + self.rules.map_width /= 10; + } + + if self.curr_field == EditingField::MapHeight { + self.rules.map_height /= 10; + } + + if self.curr_field == EditingField::BoatsList + && !self.rules.boats_list().is_empty() + { + self.rules.remove_last_boat(); + } + } + + KeyCode::Char(c) if ('0'..='9').contains(&c) => { + let val = c.to_string().parse::().unwrap_or_default(); + + if self.curr_field == EditingField::MapWidth { + self.rules.map_width *= 10; + self.rules.map_width += val; + } + + if self.curr_field == EditingField::MapHeight { + self.rules.map_height *= 10; + self.rules.map_height += val; + } + + if self.curr_field == EditingField::BoatsList { + self.rules.add_boat(val); + } + } + + _ => {} } + } - KeyCode::Char(' ') => { - if model.curr_field == EditingField::BoatsCanTouch { - model.rules.boats_can_touch = !model.rules.boats_can_touch; - } - - if model.curr_field == EditingField::PlayerContinueOnHit { - model.rules.player_continue_on_hit = - !model.rules.player_continue_on_hit; - } - } - - KeyCode::Backspace => { - if model.curr_field == EditingField::MapWidth { - model.rules.map_width /= 10; - } - - if model.curr_field == EditingField::MapHeight { - model.rules.map_height /= 10; - } - - if model.curr_field == EditingField::BoatsList - && !model.rules.boats_list().is_empty() - { - model.rules.remove_last_boat(); - } - } - - KeyCode::Char(c) if ('0'..='9').contains(&c) => { - let val = c.to_string().parse::().unwrap_or_default(); - - if model.curr_field == EditingField::MapWidth { - model.rules.map_width *= 10; - model.rules.map_width += val; - } - - if model.curr_field == EditingField::MapHeight { - model.rules.map_height *= 10; - model.rules.map_height += val; - } - - if model.curr_field == EditingField::BoatsList { - model.rules.add_boat(val); - } - } - - _ => {} + // Apply new cursor position + self.curr_field = if cursor_pos < 0 { + EditingField::OK + } else { + num_renamed::FromPrimitive::from_u64(cursor_pos as u64) + .unwrap_or(EditingField::MapWidth) } } - - // Apply new cursor position - model.curr_field = if cursor_pos < 0 { - EditingField::OK - } else { - num_renamed::FromPrimitive::from_u64(cursor_pos as u64) - .unwrap_or(EditingField::MapWidth) + if last_tick.elapsed() >= TICK_RATE { + last_tick = Instant::now(); } } - if last_tick.elapsed() >= TICK_RATE { - last_tick = Instant::now(); + } + + fn ui(&mut self, f: &mut Frame) { + let area = centered_rect_size(50, 16, &f.size()); + + let block = Block::default().title("Game rules").borders(Borders::ALL); + f.render_widget(block, area); + + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(3), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), + Constraint::Length(1), // Buttons + Constraint::Length(1), // Error message (if any) + ]) + .split(area.inner(&Margin { + horizontal: 2, + vertical: 1, + })); + + let editor = TextEditorWidget::new( + "Map width", + &self.rules.map_width.to_string(), + self.curr_field == EditingField::MapWidth, + ); + f.render_widget(editor, chunks[EditingField::MapWidth as usize]); + + let editor = TextEditorWidget::new( + "Map height", + &self.rules.map_height.to_string(), + self.curr_field == EditingField::MapHeight, + ); + f.render_widget(editor, chunks[EditingField::MapHeight as usize]); + + let editor = TextEditorWidget::new( + "Boats list", + &self + .rules + .boats_list() + .iter() + .map(usize::to_string) + .collect::>() + .join("; "), + self.curr_field == EditingField::BoatsList, + ); + f.render_widget(editor, chunks[EditingField::BoatsList as usize]); + + let editor = CheckboxWidget::new( + "Boats can touch", + self.rules.boats_can_touch, + self.curr_field == EditingField::BoatsCanTouch, + ); + f.render_widget(editor, chunks[EditingField::BoatsCanTouch as usize]); + + let editor = CheckboxWidget::new( + "Player continue on hit", + self.rules.player_continue_on_hit, + self.curr_field == EditingField::PlayerContinueOnHit, + ); + f.render_widget(editor, chunks[EditingField::PlayerContinueOnHit as usize]); + + // Buttons + let buttons_chunk = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(chunks[EditingField::OK as usize]); + + let button = ButtonWidget::new("Cancel", self.curr_field == EditingField::Cancel); + f.render_widget(button, buttons_chunk[0]); + + let button = ButtonWidget::new("OK", self.curr_field == EditingField::OK) + .set_disabled(!self.rules.is_valid()); + f.render_widget(button, buttons_chunk[1]); + + // Error message (if any) + if let Some(msg) = self.rules.get_errors().first() { + let area = centered_rect_size(msg.len() as u16, 1, chunks.last().unwrap()); + let err = Paragraph::new(*msg).style(Style::default().fg(Color::Red)); + f.render_widget(err, area); } } } - -fn ui(f: &mut Frame, model: &mut GameRulesConfigurationScreen) { - let area = centered_rect_size(50, 16, &f.size()); - - let block = Block::default().title("Game rules").borders(Borders::ALL); - f.render_widget(block, area); - - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(3), - Constraint::Length(3), - Constraint::Length(3), - Constraint::Length(1), - Constraint::Length(1), - Constraint::Length(1), - Constraint::Length(1), // Buttons - Constraint::Length(1), // Error message (if any) - ]) - .split(area.inner(&Margin { - horizontal: 2, - vertical: 1, - })); - - let editor = TextEditorWidget::new( - "Map width", - &model.rules.map_width.to_string(), - model.curr_field == EditingField::MapWidth, - ); - f.render_widget(editor, chunks[EditingField::MapWidth as usize]); - - let editor = TextEditorWidget::new( - "Map height", - &model.rules.map_height.to_string(), - model.curr_field == EditingField::MapHeight, - ); - f.render_widget(editor, chunks[EditingField::MapHeight as usize]); - - let editor = TextEditorWidget::new( - "Boats list", - &model - .rules - .boats_list() - .iter() - .map(usize::to_string) - .collect::>() - .join("; "), - model.curr_field == EditingField::BoatsList, - ); - f.render_widget(editor, chunks[EditingField::BoatsList as usize]); - - let editor = CheckboxWidget::new( - "Boats can touch", - model.rules.boats_can_touch, - model.curr_field == EditingField::BoatsCanTouch, - ); - f.render_widget(editor, chunks[EditingField::BoatsCanTouch as usize]); - - let editor = CheckboxWidget::new( - "Player continue on hit", - model.rules.player_continue_on_hit, - model.curr_field == EditingField::PlayerContinueOnHit, - ); - f.render_widget(editor, chunks[EditingField::PlayerContinueOnHit as usize]); - - // Buttons - let buttons_chunk = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) - .split(chunks[EditingField::OK as usize]); - - let button = ButtonWidget::new("Cancel", model.curr_field == EditingField::Cancel); - f.render_widget(button, buttons_chunk[0]); - - let button = ButtonWidget::new("OK", model.curr_field == EditingField::OK) - .set_disabled(!model.rules.is_valid()); - f.render_widget(button, buttons_chunk[1]); - - // Error message (if any) - if let Some(msg) = model.rules.get_errors().first() { - let area = centered_rect_size(msg.len() as u16, 1, chunks.last().unwrap()); - let err = Paragraph::new(*msg).style(Style::default().fg(Color::Red)); - f.render_widget(err, area); - } -} diff --git a/rust/cli_player/src/ui_screens/confirm_dialog.rs b/rust/cli_player/src/ui_screens/confirm_dialog.rs index 9164aad..0128fd6 100644 --- a/rust/cli_player/src/ui_screens/confirm_dialog.rs +++ b/rust/cli_player/src/ui_screens/confirm_dialog.rs @@ -14,99 +14,103 @@ use crate::ui_screens::utils::centered_rect_size; use crate::ui_screens::ScreenResult; use crate::ui_widgets::button_widget::ButtonWidget; -struct ConfirmDialogScreen<'a> { +pub struct ConfirmDialogScreen<'a> { title: &'a str, msg: &'a str, is_confirm: bool, can_cancel: bool, } -pub fn confirm_dialog( - msg: &str, - terminal: &mut Terminal, -) -> io::Result> { - let mut model = ConfirmDialogScreen { - title: "Confirmation Request", - msg, - is_confirm: true, - can_cancel: false, - }; - - let mut last_tick = Instant::now(); - loop { - terminal.draw(|f| ui(f, &mut model))?; - - 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 | KeyCode::Char('q') if model.can_cancel => { - return Ok(ScreenResult::Canceled) - } - - // Toggle selected choice - KeyCode::Left | KeyCode::Right | KeyCode::Tab => { - model.is_confirm = !model.is_confirm - } - - // Submit choice - KeyCode::Enter => { - return Ok(ScreenResult::Ok(model.is_confirm)); - } - _ => {} - } - } - } - if last_tick.elapsed() >= TICK_RATE { - last_tick = Instant::now(); +impl<'a> ConfirmDialogScreen<'a> { + pub fn new(msg: &'a str) -> Self { + Self { + title: "Confirmation Request", + msg, + is_confirm: true, + can_cancel: false, } } -} - -fn ui(f: &mut Frame, model: &mut ConfirmDialogScreen) { - // Preprocess message - let lines = textwrap::wrap(model.msg, f.size().width as usize - 20); - let line_max_len = lines.iter().map(|l| l.len()).max().unwrap(); - - let area = centered_rect_size(line_max_len as u16 + 4, 5 + lines.len() as u16, &f.size()); - - let block = Block::default().borders(Borders::ALL).title(model.title); - f.render_widget(block, area); - - // Create two chunks with equal horizontal screen space - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints( - [ - Constraint::Length(lines.len() as u16), - Constraint::Length(3), - ] - .as_ref(), - ) - .split(area.inner(&Margin { - horizontal: 2, - vertical: 1, - })); - - let text = lines - .iter() - .map(|s| Spans::from(s.as_ref())) - .collect::>(); - let paragraph = Paragraph::new(text); - f.render_widget(paragraph, chunks[0]); - - // Buttons - let buttons_area = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) - .split(chunks[1]); - - let cancel_button = ButtonWidget::new("Cancel", true).set_disabled(model.is_confirm); - f.render_widget(cancel_button, buttons_area[0]); - - let ok_button = ButtonWidget::new("Confirm", true).set_disabled(!model.is_confirm); - f.render_widget(ok_button, buttons_area[1]); + + 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 | KeyCode::Char('q') if self.can_cancel => { + return Ok(ScreenResult::Canceled) + } + + // Toggle selected choice + KeyCode::Left | KeyCode::Right | KeyCode::Tab => { + self.is_confirm = !self.is_confirm + } + + // Submit choice + KeyCode::Enter => { + return Ok(ScreenResult::Ok(self.is_confirm)); + } + _ => {} + } + } + } + if last_tick.elapsed() >= TICK_RATE { + last_tick = Instant::now(); + } + } + } + + fn ui(&mut self, f: &mut Frame) { + // Preprocess message + let lines = textwrap::wrap(self.msg, f.size().width as usize - 20); + let line_max_len = lines.iter().map(|l| l.len()).max().unwrap(); + + let area = centered_rect_size(line_max_len as u16 + 4, 5 + lines.len() as u16, &f.size()); + + 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(lines.len() as u16), + Constraint::Length(3), + ] + .as_ref(), + ) + .split(area.inner(&Margin { + horizontal: 2, + vertical: 1, + })); + + let text = lines + .iter() + .map(|s| Spans::from(s.as_ref())) + .collect::>(); + let paragraph = Paragraph::new(text); + f.render_widget(paragraph, chunks[0]); + + // Buttons + let buttons_area = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) + .split(chunks[1]); + + let cancel_button = ButtonWidget::new("Cancel", true).set_disabled(self.is_confirm); + f.render_widget(cancel_button, buttons_area[0]); + + let ok_button = ButtonWidget::new("Confirm", true).set_disabled(!self.is_confirm); + f.render_widget(ok_button, buttons_area[1]); + } } diff --git a/rust/cli_player/src/ui_screens/select_bot_type.rs b/rust/cli_player/src/ui_screens/select_bot_type.rs index 80dcdb9..02ccc75 100644 --- a/rust/cli_player/src/ui_screens/select_bot_type.rs +++ b/rust/cli_player/src/ui_screens/select_bot_type.rs @@ -15,81 +15,88 @@ use crate::constants::{HIGHLIGHT_COLOR, TICK_RATE}; use crate::ui_screens::utils::centered_rect_size; use crate::ui_screens::ScreenResult; -struct SelectPlayModeScreen { +pub struct SelectBotTypeScreen { state: ListState, curr_selection: usize, types: Vec, } -pub fn select_bot_type( - terminal: &mut Terminal, -) -> io::Result> { - let types = PlayConfiguration::default().bot_types; - let mut model = SelectPlayModeScreen { - state: Default::default(), - curr_selection: types.len() - 1, - types, - }; - - let mut last_tick = Instant::now(); - loop { - model.state.select(Some(model.curr_selection)); - terminal.draw(|f| ui(f, &mut model))?; - - 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::Char('q') => return Ok(ScreenResult::Canceled), - KeyCode::Enter => { - return Ok(ScreenResult::Ok(model.types[model.curr_selection].r#type)); - } - KeyCode::Down => model.curr_selection += 1, - KeyCode::Up => model.curr_selection += model.types.len() - 1, - _ => {} - } - - model.curr_selection %= model.types.len(); - } - } - if last_tick.elapsed() >= TICK_RATE { - last_tick = Instant::now(); +impl Default for SelectBotTypeScreen { + fn default() -> Self { + let types = PlayConfiguration::default().bot_types; + Self { + state: Default::default(), + curr_selection: types.len() - 1, + types, } } } -fn ui(f: &mut Frame, model: &mut SelectPlayModeScreen) { - let area = centered_rect_size(60, model.types.len() as u16 * 2 + 2, &f.size()); +impl SelectBotTypeScreen { + pub fn show( + mut self, + terminal: &mut Terminal, + ) -> io::Result> { + let mut last_tick = Instant::now(); + loop { + self.state.select(Some(self.curr_selection)); + terminal.draw(|f| self.ui(f))?; - // Create a List from all list items and highlight the currently selected one - let items = model - .types - .iter() - .map(|bot| { - ListItem::new(vec![ - Spans::from(bot.name), - Spans::from(Span::styled( - bot.description, - Style::default().add_modifier(Modifier::ITALIC), - )), - ]) - }) - .collect::>(); - let items = List::new(items) - .block( - Block::default() - .title("Select bot type") - .borders(Borders::ALL), - ) - .highlight_style( - Style::default() - .fg(HIGHLIGHT_COLOR) - .add_modifier(Modifier::BOLD), - ) - .highlight_symbol(">> "); + let timeout = TICK_RATE + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); - f.render_stateful_widget(items, area, &mut model.state); + if event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => return Ok(ScreenResult::Canceled), + KeyCode::Enter => { + return Ok(ScreenResult::Ok(self.types[self.curr_selection].r#type)); + } + KeyCode::Down => self.curr_selection += 1, + KeyCode::Up => self.curr_selection += self.types.len() - 1, + _ => {} + } + + self.curr_selection %= self.types.len(); + } + } + if last_tick.elapsed() >= TICK_RATE { + last_tick = Instant::now(); + } + } + } + + fn ui(&mut self, f: &mut Frame) { + let area = centered_rect_size(60, self.types.len() as u16 * 2 + 2, &f.size()); + + // Create a List from all list items and highlight the currently selected one + let items = self + .types + .iter() + .map(|bot| { + ListItem::new(vec![ + Spans::from(bot.name), + Spans::from(Span::styled( + bot.description, + Style::default().add_modifier(Modifier::ITALIC), + )), + ]) + }) + .collect::>(); + let items = List::new(items) + .block( + Block::default() + .title("Select bot type") + .borders(Borders::ALL), + ) + .highlight_style( + Style::default() + .fg(HIGHLIGHT_COLOR) + .add_modifier(Modifier::BOLD), + ) + .highlight_symbol(">> "); + + f.render_stateful_widget(items, area, &mut self.state); + } } diff --git a/rust/cli_player/src/ui_screens/select_play_mode.rs b/rust/cli_player/src/ui_screens/select_play_mode.rs index b9beb7e..257120d 100644 --- a/rust/cli_player/src/ui_screens/select_play_mode.rs +++ b/rust/cli_player/src/ui_screens/select_play_mode.rs @@ -3,6 +3,7 @@ use std::time::{Duration, Instant}; use crate::constants::{HIGHLIGHT_COLOR, TICK_RATE}; use crate::ui_screens::utils::centered_rect_size; +use crate::ui_screens::ScreenResult; use crossterm::event; use crossterm::event::{Event, KeyCode}; use tui::backend::Backend; @@ -41,64 +42,69 @@ const AVAILABLE_PLAY_MODES: [PlayModeDescription; 3] = [ ]; #[derive(Default)] -struct SelectPlayModeScreen { +pub struct SelectPlayModeScreen { state: ListState, curr_selection: usize, } -pub fn select_play_mode( - terminal: &mut Terminal, -) -> io::Result { - let mut model = SelectPlayModeScreen::default(); +impl SelectPlayModeScreen { + pub fn show( + mut self, + terminal: &mut Terminal, + ) -> io::Result> { + let mut last_tick = Instant::now(); + loop { + self.state.select(Some(self.curr_selection)); + terminal.draw(|f| self.ui(f))?; - let mut last_tick = Instant::now(); - loop { - model.state.select(Some(model.curr_selection)); - terminal.draw(|f| ui(f, &mut model))?; + let timeout = TICK_RATE + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); - let timeout = TICK_RATE - .checked_sub(last_tick.elapsed()) - .unwrap_or_else(|| Duration::from_secs(0)); + if crossterm::event::poll(timeout)? { + if let Event::Key(key) = event::read()? { + match key.code { + KeyCode::Char('q') => return Ok(ScreenResult::Canceled), + KeyCode::Enter => { + return Ok(ScreenResult::Ok( + AVAILABLE_PLAY_MODES[self.curr_selection].value, + )); + } + KeyCode::Down => self.curr_selection += 1, + KeyCode::Up => self.curr_selection += AVAILABLE_PLAY_MODES.len() - 1, + _ => {} + } - if crossterm::event::poll(timeout)? { - if let Event::Key(key) = event::read()? { - match key.code { - KeyCode::Char('q') => return Ok(SelectPlayModeResult::Exit), - KeyCode::Enter => return Ok(AVAILABLE_PLAY_MODES[model.curr_selection].value), - KeyCode::Down => model.curr_selection += 1, - KeyCode::Up => model.curr_selection += AVAILABLE_PLAY_MODES.len() - 1, - _ => {} + self.curr_selection %= AVAILABLE_PLAY_MODES.len(); } - - model.curr_selection %= AVAILABLE_PLAY_MODES.len(); + } + if last_tick.elapsed() >= TICK_RATE { + last_tick = Instant::now(); } } - if last_tick.elapsed() >= TICK_RATE { - last_tick = Instant::now(); - } + } + + fn ui(&mut self, f: &mut Frame) { + let area = centered_rect_size(50, 5, &f.size()); + + // Create a List from all list items and highlight the currently selected one + let items = AVAILABLE_PLAY_MODES + .iter() + .map(|mode| ListItem::new(Text::raw(mode.name))) + .collect::>(); + let items = List::new(items) + .block( + Block::default() + .title("Select play mode") + .borders(Borders::ALL), + ) + .highlight_style( + Style::default() + .fg(HIGHLIGHT_COLOR) + .add_modifier(Modifier::BOLD), + ) + .highlight_symbol(">> "); + + f.render_stateful_widget(items, area, &mut self.state); } } - -fn ui(f: &mut Frame, model: &mut SelectPlayModeScreen) { - let area = centered_rect_size(50, 5, &f.size()); - - // Create a List from all list items and highlight the currently selected one - let items = AVAILABLE_PLAY_MODES - .iter() - .map(|mode| ListItem::new(Text::raw(mode.name))) - .collect::>(); - let items = List::new(items) - .block( - Block::default() - .title("Select play mode") - .borders(Borders::ALL), - ) - .highlight_style( - Style::default() - .fg(HIGHLIGHT_COLOR) - .add_modifier(Modifier::BOLD), - ) - .highlight_symbol(">> "); - - f.render_stateful_widget(items, area, &mut model.state); -} diff --git a/rust/cli_player/src/ui_screens/set_boats_layout.rs b/rust/cli_player/src/ui_screens/set_boats_layout.rs index b0c1bea..1d85feb 100644 --- a/rust/cli_player/src/ui_screens/set_boats_layout.rs +++ b/rust/cli_player/src/ui_screens/set_boats_layout.rs @@ -17,216 +17,218 @@ use crate::ui_screens::utils::{centered_rect_size, centered_text}; use crate::ui_screens::ScreenResult; use crate::ui_widgets::game_map_widget::{ColoredCells, GameMapWidget}; -struct SetBotsLayoutScreen { - curr_boat: usize, - layout: BoatsLayout, -} - type CoordinatesMapper = HashMap; -pub fn set_boat_layout( - rules: &GameRules, - terminal: &mut Terminal, -) -> io::Result> { - let mut model = SetBotsLayoutScreen { - curr_boat: 0, - layout: BoatsLayout::gen_random_for_rules(rules) - .expect("Failed to generate initial boats layout"), - }; - - let mut coordinates_mapper = CoordinatesMapper::default(); - - let mut last_tick = Instant::now(); - let mut is_moving_boat = false; - loop { - terminal.draw(|f| coordinates_mapper = ui(f, &mut model, rules))?; - - let timeout = TICK_RATE - .checked_sub(last_tick.elapsed()) - .unwrap_or_else(|| Duration::from_secs(0)); - - if event::poll(timeout)? { - let mut move_boat = None; - - let event = event::read()?; - if let Event::Key(key) = &event { - match key.code { - KeyCode::Char('q') => return Ok(ScreenResult::Canceled), - - // Select next boat - KeyCode::Char('n') => model.curr_boat += model.layout.number_of_boats() - 1, - - // Rotate boat - KeyCode::Char('r') => { - model.layout.0[model.curr_boat].direction = - match model.layout.0[model.curr_boat].direction { - BoatDirection::Right => BoatDirection::Down, - _ => BoatDirection::Right, - } - } - - // Move boat - KeyCode::Left => move_boat = Some((-1, 0)), - KeyCode::Right => move_boat = Some((1, 0)), - KeyCode::Up => move_boat = Some((0, -1)), - KeyCode::Down => move_boat = Some((0, 1)), - - // Submit configuration - KeyCode::Enter => { - if model.layout.is_valid(rules) { - return Ok(ScreenResult::Ok(model.layout)); - } - } - - _ => {} - } - - model.curr_boat %= model.layout.number_of_boats(); - - // Apply boat move - if let Some((x, y)) = move_boat { - let new_pos = model.layout.0[model.curr_boat].start.add_x(x).add_y(y); - if new_pos.is_valid(rules) { - model.layout.0[model.curr_boat].start = new_pos; - } - } - } - // Mouse event - else if let Event::Mouse(mouse) = event { - let src_pos = Coordinates::new(mouse.column, mouse.row); - - // Start mouse action - if MouseEventKind::Down(MouseButton::Left) == mouse.kind { - is_moving_boat = if let Some(pos) = coordinates_mapper.get(&src_pos) { - if let Some(b) = model.layout.find_boat_at_position(*pos) { - model.curr_boat = model.layout.0.iter().position(|s| s == b).unwrap(); - } - - true - } else { - false - } - } - // Handle continue mouse action - else if is_moving_boat { - if let Some(pos) = coordinates_mapper.get(&src_pos) { - model.layout.0[model.curr_boat].start = *pos; - } - - if let MouseEventKind::Up(_) = mouse.kind { - is_moving_boat = false; - } - } - } - } - - if last_tick.elapsed() >= TICK_RATE { - last_tick = Instant::now(); - } - } +pub struct SetBoatsLayoutScreen<'a> { + curr_boat: usize, + layout: BoatsLayout, + rules: &'a GameRules, } -fn ui( - f: &mut Frame, - model: &mut SetBotsLayoutScreen, - rules: &GameRules, -) -> CoordinatesMapper { - let errors = model.layout.errors(rules); - - // Color of current boat - let current_boat = ColoredCells { - color: Color::Green, - cells: model.layout.0[model.curr_boat].all_coordinates(), - }; - - // Color of invalid boats - let mut invalid_coordinates = vec![]; - for (idx, pos) in model.layout.boats().iter().enumerate() { - if idx == model.curr_boat { - continue; - } - - if !model - .layout - .check_present_boat_position(idx, rules) - .is_empty() - { - invalid_coordinates.append(&mut pos.all_coordinates()); +impl<'a> SetBoatsLayoutScreen<'a> { + pub fn new(rules: &'a GameRules) -> Self { + Self { + curr_boat: 0, + layout: BoatsLayout::gen_random_for_rules(rules) + .expect("Failed to generate initial boats layout"), + rules, } } - let invalid_boats = ColoredCells { - color: Color::Red, - cells: invalid_coordinates, - }; - // Color of other boats - let mut other_boats_cells = vec![]; - for boat in &model.layout.0 { - other_boats_cells.append(&mut boat.all_coordinates()); + pub fn show( + mut self, + terminal: &mut Terminal, + ) -> io::Result> { + let mut coordinates_mapper = CoordinatesMapper::default(); + + let mut last_tick = Instant::now(); + let mut is_moving_boat = false; + loop { + terminal.draw(|f| coordinates_mapper = self.ui(f))?; + + let timeout = TICK_RATE + .checked_sub(last_tick.elapsed()) + .unwrap_or_else(|| Duration::from_secs(0)); + + if event::poll(timeout)? { + let mut move_boat = None; + + let event = event::read()?; + if let Event::Key(key) = &event { + match key.code { + KeyCode::Char('q') => return Ok(ScreenResult::Canceled), + + // Select next boat + KeyCode::Char('n') => self.curr_boat += self.layout.number_of_boats() - 1, + + // Rotate boat + KeyCode::Char('r') => { + self.layout.0[self.curr_boat].direction = + match self.layout.0[self.curr_boat].direction { + BoatDirection::Right => BoatDirection::Down, + _ => BoatDirection::Right, + } + } + + // Move boat + KeyCode::Left => move_boat = Some((-1, 0)), + KeyCode::Right => move_boat = Some((1, 0)), + KeyCode::Up => move_boat = Some((0, -1)), + KeyCode::Down => move_boat = Some((0, 1)), + + // Submit configuration + KeyCode::Enter => { + if self.layout.is_valid(self.rules) { + return Ok(ScreenResult::Ok(self.layout)); + } + } + + _ => {} + } + + self.curr_boat %= self.layout.number_of_boats(); + + // Apply boat move + if let Some((x, y)) = move_boat { + let new_pos = self.layout.0[self.curr_boat].start.add_x(x).add_y(y); + if new_pos.is_valid(self.rules) { + self.layout.0[self.curr_boat].start = new_pos; + } + } + } + // Mouse event + else if let Event::Mouse(mouse) = event { + let src_pos = Coordinates::new(mouse.column, mouse.row); + + // Start mouse action + if MouseEventKind::Down(MouseButton::Left) == mouse.kind { + is_moving_boat = if let Some(pos) = coordinates_mapper.get(&src_pos) { + if let Some(b) = self.layout.find_boat_at_position(*pos) { + self.curr_boat = self.layout.0.iter().position(|s| s == b).unwrap(); + } + + true + } else { + false + } + } + // Handle continue mouse action + else if is_moving_boat { + if let Some(pos) = coordinates_mapper.get(&src_pos) { + self.layout.0[self.curr_boat].start = *pos; + } + + if let MouseEventKind::Up(_) = mouse.kind { + is_moving_boat = false; + } + } + } + } + + if last_tick.elapsed() >= TICK_RATE { + last_tick = Instant::now(); + } + } } - let other_boats = ColoredCells { - color: Color::Gray, - cells: other_boats_cells, - }; + fn ui(&mut self, f: &mut Frame) -> CoordinatesMapper { + let errors = self.layout.errors(self.rules); - let mut coordinates_mapper = HashMap::new(); + // Color of current boat + let current_boat = ColoredCells { + color: Color::Green, + cells: self.layout.0[self.curr_boat].all_coordinates(), + }; - let mut legend = "n next boat \n\ + // Color of invalid boats + let mut invalid_coordinates = vec![]; + for (idx, pos) in self.layout.boats().iter().enumerate() { + if idx == self.curr_boat { + continue; + } + + if !self + .layout + .check_present_boat_position(idx, self.rules) + .is_empty() + { + invalid_coordinates.append(&mut pos.all_coordinates()); + } + } + let invalid_boats = ColoredCells { + color: Color::Red, + cells: invalid_coordinates, + }; + + // Color of other boats + let mut other_boats_cells = vec![]; + for boat in &self.layout.0 { + other_boats_cells.append(&mut boat.all_coordinates()); + } + + let other_boats = ColoredCells { + color: Color::Gray, + cells: other_boats_cells, + }; + + let mut coordinates_mapper = HashMap::new(); + + let mut legend = "n next boat \n\ r rotate boat \n\n\ ← ↓↑ → move boat \n\n" - .to_string(); - if errors.is_empty() { - legend.push_str("Enter confirm layout"); - } + .to_string(); + if errors.is_empty() { + legend.push_str("Enter confirm layout"); + } - let mut game_map_widget = GameMapWidget::new(rules) - .set_default_empty_char(' ') - .add_colored_cells(current_boat) - .add_colored_cells(invalid_boats) - .add_colored_cells(other_boats) - .set_title("Choose your boat layout") - .set_yield_func(|c, r| { - for i in 0..r.width { - for j in 0..r.height { - coordinates_mapper.insert(Coordinates::new(r.x + i, r.y + j), c); + let mut game_map_widget = GameMapWidget::new(self.rules) + .set_default_empty_char(' ') + .add_colored_cells(current_boat) + .add_colored_cells(invalid_boats) + .add_colored_cells(other_boats) + .set_title("Choose your boat layout") + .set_yield_func(|c, r| { + for i in 0..r.width { + for j in 0..r.height { + coordinates_mapper.insert(Coordinates::new(r.x + i, r.y + j), c); + } + } + }) + .set_legend(legend); + + // Color of neighbors if boats can not touch + if !self.rules.boats_can_touch { + let mut boats_neighbors_cells = vec![]; + for boat in &self.layout.0 { + for pos in boat.neighbor_coordinates(self.rules) { + boats_neighbors_cells.push(pos); } } - }) - .set_legend(legend); - // Color of neighbors if boats can not touch - if !rules.boats_can_touch { - let mut boats_neighbors_cells = vec![]; - for boat in &model.layout.0 { - for pos in boat.neighbor_coordinates(rules) { - boats_neighbors_cells.push(pos); + game_map_widget = game_map_widget.add_colored_cells(ColoredCells { + color: Color::Rgb(30, 30, 30), + cells: boats_neighbors_cells, + }); + } + + let (w, h) = game_map_widget.estimated_size(); + let area = centered_rect_size(w, h, &f.size()); + f.render_widget(game_map_widget, area); + + if !errors.is_empty() { + let messages = ["INVALID_LAYOUT", errors[0]]; + for (i, msg) in messages.iter().enumerate() { + let paragraph = Paragraph::new(*msg).style(Style::default().fg(Color::Red)); + f.render_widget( + paragraph, + centered_text( + msg, + &Rect::new(f.size().x, area.bottom() + i as u16, f.size().width, 1), + ), + ); } } - game_map_widget = game_map_widget.add_colored_cells(ColoredCells { - color: Color::Rgb(30, 30, 30), - cells: boats_neighbors_cells, - }); + coordinates_mapper } - - let (w, h) = game_map_widget.estimated_size(); - let area = centered_rect_size(w, h, &f.size()); - f.render_widget(game_map_widget, area); - - if !errors.is_empty() { - let messages = ["INVALID_LAYOUT", errors[0]]; - for (i, msg) in messages.iter().enumerate() { - let paragraph = Paragraph::new(*msg).style(Style::default().fg(Color::Red)); - f.render_widget( - paragraph, - centered_text( - msg, - &Rect::new(f.size().x, area.bottom() + i as u16, f.size().width, 1), - ), - ); - } - } - - coordinates_mapper }