Improve cli screens structures

This commit is contained in:
Pierre HUBERT 2022-10-10 17:51:51 +02:00
parent 276f4508c0
commit d90560d330
7 changed files with 620 additions and 574 deletions

View File

@ -4,6 +4,11 @@ use clap::{Parser, ValueEnum};
pub enum TestDevScreen { pub enum TestDevScreen {
Popup, Popup,
Input, Input,
Confirm,
SelectBotType,
SelectPlayMode,
SetBoatsLayout,
ConfigureGameRules,
} }
#[derive(Parser, Debug)] #[derive(Parser, Debug)]

View File

@ -1,5 +1,4 @@
use std::error::Error; use std::error::Error;
use std::fmt::Debug;
use std::io; use std::io;
use std::io::ErrorKind; use std::io::ErrorKind;
@ -15,31 +14,50 @@ use tui::backend::{Backend, CrosstermBackend};
use tui::Terminal; use tui::Terminal;
use cli_player::server::start_server_if_missing; use cli_player::server::start_server_if_missing;
use cli_player::ui_screens::popup_screen::PopupScreen;
use cli_player::ui_screens::*; use cli_player::ui_screens::*;
use sea_battle_backend::data::GameRules; use sea_battle_backend::data::GameRules;
/// Test code screens
async fn run_dev<B: Backend>( async fn run_dev<B: Backend>(
terminal: &mut Terminal<B>, terminal: &mut Terminal<B>,
d: TestDevScreen, d: TestDevScreen,
) -> Result<(), Box<dyn Error>> { ) -> Result<(), Box<dyn Error>> {
let res = match d { let res = match d {
TestDevScreen::Popup => PopupScreen::new("Welcome there!!") TestDevScreen::Popup => popup_screen::PopupScreen::new("Welcome there!!")
.show(terminal)? .show(terminal)?
.as_string(), .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") .set_title("A custom title")
.show(terminal)? .show(terminal)?
.as_string(), .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( Err(io::Error::new(
ErrorKind::Other, ErrorKind::Other,
format!("DEV result: {:?}", res), format!("DEV result: {:?}", res),

View File

@ -31,30 +31,33 @@ enum EditingField {
OK, OK,
} }
struct GameRulesConfigurationScreen { pub struct GameRulesConfigurationScreen {
rules: GameRules, rules: GameRules,
curr_field: EditingField, curr_field: EditingField,
} }
pub fn configure_play_rules<B: Backend>( impl GameRulesConfigurationScreen {
rules: GameRules, pub fn new(rules: GameRules) -> Self {
terminal: &mut Terminal<B>, Self {
) -> io::Result<ScreenResult<GameRules>> {
let mut model = GameRulesConfigurationScreen {
rules, rules,
curr_field: EditingField::OK, curr_field: EditingField::OK,
}; }
}
pub fn show<B: Backend>(
mut self,
terminal: &mut Terminal<B>,
) -> io::Result<ScreenResult<GameRules>> {
let mut last_tick = Instant::now(); let mut last_tick = Instant::now();
loop { loop {
terminal.draw(|f| ui(f, &mut model))?; terminal.draw(|f| self.ui(f))?;
let timeout = TICK_RATE let timeout = TICK_RATE
.checked_sub(last_tick.elapsed()) .checked_sub(last_tick.elapsed())
.unwrap_or_else(|| Duration::from_secs(0)); .unwrap_or_else(|| Duration::from_secs(0));
if crossterm::event::poll(timeout)? { if crossterm::event::poll(timeout)? {
let mut cursor_pos = model.curr_field as i32; let mut cursor_pos = self.curr_field as i32;
if let Event::Key(key) = event::read()? { if let Event::Key(key) = event::read()? {
match key.code { match key.code {
@ -67,57 +70,57 @@ pub fn configure_play_rules<B: Backend>(
// Submit results // Submit results
KeyCode::Enter => { KeyCode::Enter => {
if model.curr_field == EditingField::Cancel { if self.curr_field == EditingField::Cancel {
return Ok(ScreenResult::Canceled); return Ok(ScreenResult::Canceled);
} }
if model.curr_field == EditingField::OK && model.rules.is_valid() { if self.curr_field == EditingField::OK && self.rules.is_valid() {
return Ok(ScreenResult::Ok(model.rules)); return Ok(ScreenResult::Ok(self.rules));
} }
} }
KeyCode::Char(' ') => { KeyCode::Char(' ') => {
if model.curr_field == EditingField::BoatsCanTouch { if self.curr_field == EditingField::BoatsCanTouch {
model.rules.boats_can_touch = !model.rules.boats_can_touch; self.rules.boats_can_touch = !self.rules.boats_can_touch;
} }
if model.curr_field == EditingField::PlayerContinueOnHit { if self.curr_field == EditingField::PlayerContinueOnHit {
model.rules.player_continue_on_hit = self.rules.player_continue_on_hit =
!model.rules.player_continue_on_hit; !self.rules.player_continue_on_hit;
} }
} }
KeyCode::Backspace => { KeyCode::Backspace => {
if model.curr_field == EditingField::MapWidth { if self.curr_field == EditingField::MapWidth {
model.rules.map_width /= 10; self.rules.map_width /= 10;
} }
if model.curr_field == EditingField::MapHeight { if self.curr_field == EditingField::MapHeight {
model.rules.map_height /= 10; self.rules.map_height /= 10;
} }
if model.curr_field == EditingField::BoatsList if self.curr_field == EditingField::BoatsList
&& !model.rules.boats_list().is_empty() && !self.rules.boats_list().is_empty()
{ {
model.rules.remove_last_boat(); self.rules.remove_last_boat();
} }
} }
KeyCode::Char(c) if ('0'..='9').contains(&c) => { KeyCode::Char(c) if ('0'..='9').contains(&c) => {
let val = c.to_string().parse::<usize>().unwrap_or_default(); let val = c.to_string().parse::<usize>().unwrap_or_default();
if model.curr_field == EditingField::MapWidth { if self.curr_field == EditingField::MapWidth {
model.rules.map_width *= 10; self.rules.map_width *= 10;
model.rules.map_width += val; self.rules.map_width += val;
} }
if model.curr_field == EditingField::MapHeight { if self.curr_field == EditingField::MapHeight {
model.rules.map_height *= 10; self.rules.map_height *= 10;
model.rules.map_height += val; self.rules.map_height += val;
} }
if model.curr_field == EditingField::BoatsList { if self.curr_field == EditingField::BoatsList {
model.rules.add_boat(val); self.rules.add_boat(val);
} }
} }
@ -126,7 +129,7 @@ pub fn configure_play_rules<B: Backend>(
} }
// Apply new cursor position // Apply new cursor position
model.curr_field = if cursor_pos < 0 { self.curr_field = if cursor_pos < 0 {
EditingField::OK EditingField::OK
} else { } else {
num_renamed::FromPrimitive::from_u64(cursor_pos as u64) num_renamed::FromPrimitive::from_u64(cursor_pos as u64)
@ -139,7 +142,7 @@ pub fn configure_play_rules<B: Backend>(
} }
} }
fn ui<B: Backend>(f: &mut Frame<B>, model: &mut GameRulesConfigurationScreen) { fn ui<B: Backend>(&mut self, f: &mut Frame<B>) {
let area = centered_rect_size(50, 16, &f.size()); let area = centered_rect_size(50, 16, &f.size());
let block = Block::default().title("Game rules").borders(Borders::ALL); let block = Block::default().title("Game rules").borders(Borders::ALL);
@ -164,42 +167,42 @@ fn ui<B: Backend>(f: &mut Frame<B>, model: &mut GameRulesConfigurationScreen) {
let editor = TextEditorWidget::new( let editor = TextEditorWidget::new(
"Map width", "Map width",
&model.rules.map_width.to_string(), &self.rules.map_width.to_string(),
model.curr_field == EditingField::MapWidth, self.curr_field == EditingField::MapWidth,
); );
f.render_widget(editor, chunks[EditingField::MapWidth as usize]); f.render_widget(editor, chunks[EditingField::MapWidth as usize]);
let editor = TextEditorWidget::new( let editor = TextEditorWidget::new(
"Map height", "Map height",
&model.rules.map_height.to_string(), &self.rules.map_height.to_string(),
model.curr_field == EditingField::MapHeight, self.curr_field == EditingField::MapHeight,
); );
f.render_widget(editor, chunks[EditingField::MapHeight as usize]); f.render_widget(editor, chunks[EditingField::MapHeight as usize]);
let editor = TextEditorWidget::new( let editor = TextEditorWidget::new(
"Boats list", "Boats list",
&model &self
.rules .rules
.boats_list() .boats_list()
.iter() .iter()
.map(usize::to_string) .map(usize::to_string)
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("; "), .join("; "),
model.curr_field == EditingField::BoatsList, self.curr_field == EditingField::BoatsList,
); );
f.render_widget(editor, chunks[EditingField::BoatsList as usize]); f.render_widget(editor, chunks[EditingField::BoatsList as usize]);
let editor = CheckboxWidget::new( let editor = CheckboxWidget::new(
"Boats can touch", "Boats can touch",
model.rules.boats_can_touch, self.rules.boats_can_touch,
model.curr_field == EditingField::BoatsCanTouch, self.curr_field == EditingField::BoatsCanTouch,
); );
f.render_widget(editor, chunks[EditingField::BoatsCanTouch as usize]); f.render_widget(editor, chunks[EditingField::BoatsCanTouch as usize]);
let editor = CheckboxWidget::new( let editor = CheckboxWidget::new(
"Player continue on hit", "Player continue on hit",
model.rules.player_continue_on_hit, self.rules.player_continue_on_hit,
model.curr_field == EditingField::PlayerContinueOnHit, self.curr_field == EditingField::PlayerContinueOnHit,
); );
f.render_widget(editor, chunks[EditingField::PlayerContinueOnHit as usize]); f.render_widget(editor, chunks[EditingField::PlayerContinueOnHit as usize]);
@ -209,17 +212,18 @@ fn ui<B: Backend>(f: &mut Frame<B>, model: &mut GameRulesConfigurationScreen) {
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(chunks[EditingField::OK as usize]); .split(chunks[EditingField::OK as usize]);
let button = ButtonWidget::new("Cancel", model.curr_field == EditingField::Cancel); let button = ButtonWidget::new("Cancel", self.curr_field == EditingField::Cancel);
f.render_widget(button, buttons_chunk[0]); f.render_widget(button, buttons_chunk[0]);
let button = ButtonWidget::new("OK", model.curr_field == EditingField::OK) let button = ButtonWidget::new("OK", self.curr_field == EditingField::OK)
.set_disabled(!model.rules.is_valid()); .set_disabled(!self.rules.is_valid());
f.render_widget(button, buttons_chunk[1]); f.render_widget(button, buttons_chunk[1]);
// Error message (if any) // Error message (if any)
if let Some(msg) = model.rules.get_errors().first() { if let Some(msg) = self.rules.get_errors().first() {
let area = centered_rect_size(msg.len() as u16, 1, chunks.last().unwrap()); let area = centered_rect_size(msg.len() as u16, 1, chunks.last().unwrap());
let err = Paragraph::new(*msg).style(Style::default().fg(Color::Red)); let err = Paragraph::new(*msg).style(Style::default().fg(Color::Red));
f.render_widget(err, area); f.render_widget(err, area);
} }
} }
}

View File

@ -14,27 +14,30 @@ use crate::ui_screens::utils::centered_rect_size;
use crate::ui_screens::ScreenResult; use crate::ui_screens::ScreenResult;
use crate::ui_widgets::button_widget::ButtonWidget; use crate::ui_widgets::button_widget::ButtonWidget;
struct ConfirmDialogScreen<'a> { pub struct ConfirmDialogScreen<'a> {
title: &'a str, title: &'a str,
msg: &'a str, msg: &'a str,
is_confirm: bool, is_confirm: bool,
can_cancel: bool, can_cancel: bool,
} }
pub fn confirm_dialog<B: Backend>( impl<'a> ConfirmDialogScreen<'a> {
msg: &str, pub fn new(msg: &'a str) -> Self {
terminal: &mut Terminal<B>, Self {
) -> io::Result<ScreenResult<bool>> {
let mut model = ConfirmDialogScreen {
title: "Confirmation Request", title: "Confirmation Request",
msg, msg,
is_confirm: true, is_confirm: true,
can_cancel: false, can_cancel: false,
}; }
}
pub fn show<B: Backend>(
mut self,
terminal: &mut Terminal<B>,
) -> io::Result<ScreenResult<bool>> {
let mut last_tick = Instant::now(); let mut last_tick = Instant::now();
loop { loop {
terminal.draw(|f| ui(f, &mut model))?; terminal.draw(|f| self.ui(f))?;
let timeout = TICK_RATE let timeout = TICK_RATE
.checked_sub(last_tick.elapsed()) .checked_sub(last_tick.elapsed())
@ -43,18 +46,18 @@ pub fn confirm_dialog<B: Backend>(
if event::poll(timeout)? { if event::poll(timeout)? {
if let Event::Key(key) = event::read()? { if let Event::Key(key) = event::read()? {
match key.code { match key.code {
KeyCode::Esc | KeyCode::Char('q') if model.can_cancel => { KeyCode::Esc | KeyCode::Char('q') if self.can_cancel => {
return Ok(ScreenResult::Canceled) return Ok(ScreenResult::Canceled)
} }
// Toggle selected choice // Toggle selected choice
KeyCode::Left | KeyCode::Right | KeyCode::Tab => { KeyCode::Left | KeyCode::Right | KeyCode::Tab => {
model.is_confirm = !model.is_confirm self.is_confirm = !self.is_confirm
} }
// Submit choice // Submit choice
KeyCode::Enter => { KeyCode::Enter => {
return Ok(ScreenResult::Ok(model.is_confirm)); return Ok(ScreenResult::Ok(self.is_confirm));
} }
_ => {} _ => {}
} }
@ -66,14 +69,14 @@ pub fn confirm_dialog<B: Backend>(
} }
} }
fn ui<B: Backend>(f: &mut Frame<B>, model: &mut ConfirmDialogScreen) { fn ui<B: Backend>(&mut self, f: &mut Frame<B>) {
// Preprocess message // Preprocess message
let lines = textwrap::wrap(model.msg, f.size().width as usize - 20); 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 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 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); let block = Block::default().borders(Borders::ALL).title(self.title);
f.render_widget(block, area); f.render_widget(block, area);
// Create two chunks with equal horizontal screen space // Create two chunks with equal horizontal screen space
@ -104,9 +107,10 @@ fn ui<B: Backend>(f: &mut Frame<B>, model: &mut ConfirmDialogScreen) {
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref())
.split(chunks[1]); .split(chunks[1]);
let cancel_button = ButtonWidget::new("Cancel", true).set_disabled(model.is_confirm); let cancel_button = ButtonWidget::new("Cancel", true).set_disabled(self.is_confirm);
f.render_widget(cancel_button, buttons_area[0]); f.render_widget(cancel_button, buttons_area[0]);
let ok_button = ButtonWidget::new("Confirm", true).set_disabled(!model.is_confirm); let ok_button = ButtonWidget::new("Confirm", true).set_disabled(!self.is_confirm);
f.render_widget(ok_button, buttons_area[1]); f.render_widget(ok_button, buttons_area[1]);
} }
}

View File

@ -15,26 +15,32 @@ use crate::constants::{HIGHLIGHT_COLOR, TICK_RATE};
use crate::ui_screens::utils::centered_rect_size; use crate::ui_screens::utils::centered_rect_size;
use crate::ui_screens::ScreenResult; use crate::ui_screens::ScreenResult;
struct SelectPlayModeScreen { pub struct SelectBotTypeScreen {
state: ListState, state: ListState,
curr_selection: usize, curr_selection: usize,
types: Vec<BotDescription>, types: Vec<BotDescription>,
} }
pub fn select_bot_type<B: Backend>( impl Default for SelectBotTypeScreen {
terminal: &mut Terminal<B>, fn default() -> Self {
) -> io::Result<ScreenResult<BotType>> {
let types = PlayConfiguration::default().bot_types; let types = PlayConfiguration::default().bot_types;
let mut model = SelectPlayModeScreen { Self {
state: Default::default(), state: Default::default(),
curr_selection: types.len() - 1, curr_selection: types.len() - 1,
types, types,
}; }
}
}
impl SelectBotTypeScreen {
pub fn show<B: Backend>(
mut self,
terminal: &mut Terminal<B>,
) -> io::Result<ScreenResult<BotType>> {
let mut last_tick = Instant::now(); let mut last_tick = Instant::now();
loop { loop {
model.state.select(Some(model.curr_selection)); self.state.select(Some(self.curr_selection));
terminal.draw(|f| ui(f, &mut model))?; terminal.draw(|f| self.ui(f))?;
let timeout = TICK_RATE let timeout = TICK_RATE
.checked_sub(last_tick.elapsed()) .checked_sub(last_tick.elapsed())
@ -45,14 +51,14 @@ pub fn select_bot_type<B: Backend>(
match key.code { match key.code {
KeyCode::Char('q') => return Ok(ScreenResult::Canceled), KeyCode::Char('q') => return Ok(ScreenResult::Canceled),
KeyCode::Enter => { KeyCode::Enter => {
return Ok(ScreenResult::Ok(model.types[model.curr_selection].r#type)); return Ok(ScreenResult::Ok(self.types[self.curr_selection].r#type));
} }
KeyCode::Down => model.curr_selection += 1, KeyCode::Down => self.curr_selection += 1,
KeyCode::Up => model.curr_selection += model.types.len() - 1, KeyCode::Up => self.curr_selection += self.types.len() - 1,
_ => {} _ => {}
} }
model.curr_selection %= model.types.len(); self.curr_selection %= self.types.len();
} }
} }
if last_tick.elapsed() >= TICK_RATE { if last_tick.elapsed() >= TICK_RATE {
@ -61,11 +67,11 @@ pub fn select_bot_type<B: Backend>(
} }
} }
fn ui<B: Backend>(f: &mut Frame<B>, model: &mut SelectPlayModeScreen) { fn ui<B: Backend>(&mut self, f: &mut Frame<B>) {
let area = centered_rect_size(60, model.types.len() as u16 * 2 + 2, &f.size()); 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 // Create a List from all list items and highlight the currently selected one
let items = model let items = self
.types .types
.iter() .iter()
.map(|bot| { .map(|bot| {
@ -91,5 +97,6 @@ fn ui<B: Backend>(f: &mut Frame<B>, model: &mut SelectPlayModeScreen) {
) )
.highlight_symbol(">> "); .highlight_symbol(">> ");
f.render_stateful_widget(items, area, &mut model.state); f.render_stateful_widget(items, area, &mut self.state);
}
} }

View File

@ -3,6 +3,7 @@ use std::time::{Duration, Instant};
use crate::constants::{HIGHLIGHT_COLOR, TICK_RATE}; use crate::constants::{HIGHLIGHT_COLOR, TICK_RATE};
use crate::ui_screens::utils::centered_rect_size; use crate::ui_screens::utils::centered_rect_size;
use crate::ui_screens::ScreenResult;
use crossterm::event; use crossterm::event;
use crossterm::event::{Event, KeyCode}; use crossterm::event::{Event, KeyCode};
use tui::backend::Backend; use tui::backend::Backend;
@ -41,20 +42,20 @@ const AVAILABLE_PLAY_MODES: [PlayModeDescription; 3] = [
]; ];
#[derive(Default)] #[derive(Default)]
struct SelectPlayModeScreen { pub struct SelectPlayModeScreen {
state: ListState, state: ListState,
curr_selection: usize, curr_selection: usize,
} }
pub fn select_play_mode<B: Backend>( impl SelectPlayModeScreen {
pub fn show<B: Backend>(
mut self,
terminal: &mut Terminal<B>, terminal: &mut Terminal<B>,
) -> io::Result<SelectPlayModeResult> { ) -> io::Result<ScreenResult<SelectPlayModeResult>> {
let mut model = SelectPlayModeScreen::default();
let mut last_tick = Instant::now(); let mut last_tick = Instant::now();
loop { loop {
model.state.select(Some(model.curr_selection)); self.state.select(Some(self.curr_selection));
terminal.draw(|f| ui(f, &mut model))?; terminal.draw(|f| self.ui(f))?;
let timeout = TICK_RATE let timeout = TICK_RATE
.checked_sub(last_tick.elapsed()) .checked_sub(last_tick.elapsed())
@ -63,14 +64,18 @@ pub fn select_play_mode<B: Backend>(
if crossterm::event::poll(timeout)? { if crossterm::event::poll(timeout)? {
if let Event::Key(key) = event::read()? { if let Event::Key(key) = event::read()? {
match key.code { match key.code {
KeyCode::Char('q') => return Ok(SelectPlayModeResult::Exit), KeyCode::Char('q') => return Ok(ScreenResult::Canceled),
KeyCode::Enter => return Ok(AVAILABLE_PLAY_MODES[model.curr_selection].value), KeyCode::Enter => {
KeyCode::Down => model.curr_selection += 1, return Ok(ScreenResult::Ok(
KeyCode::Up => model.curr_selection += AVAILABLE_PLAY_MODES.len() - 1, AVAILABLE_PLAY_MODES[self.curr_selection].value,
));
}
KeyCode::Down => self.curr_selection += 1,
KeyCode::Up => self.curr_selection += AVAILABLE_PLAY_MODES.len() - 1,
_ => {} _ => {}
} }
model.curr_selection %= AVAILABLE_PLAY_MODES.len(); self.curr_selection %= AVAILABLE_PLAY_MODES.len();
} }
} }
if last_tick.elapsed() >= TICK_RATE { if last_tick.elapsed() >= TICK_RATE {
@ -79,7 +84,7 @@ pub fn select_play_mode<B: Backend>(
} }
} }
fn ui<B: Backend>(f: &mut Frame<B>, model: &mut SelectPlayModeScreen) { fn ui<B: Backend>(&mut self, f: &mut Frame<B>) {
let area = centered_rect_size(50, 5, &f.size()); let area = centered_rect_size(50, 5, &f.size());
// Create a List from all list items and highlight the currently selected one // Create a List from all list items and highlight the currently selected one
@ -100,5 +105,6 @@ fn ui<B: Backend>(f: &mut Frame<B>, model: &mut SelectPlayModeScreen) {
) )
.highlight_symbol(">> "); .highlight_symbol(">> ");
f.render_stateful_widget(items, area, &mut model.state); f.render_stateful_widget(items, area, &mut self.state);
}
} }

View File

@ -17,29 +17,34 @@ use crate::ui_screens::utils::{centered_rect_size, centered_text};
use crate::ui_screens::ScreenResult; use crate::ui_screens::ScreenResult;
use crate::ui_widgets::game_map_widget::{ColoredCells, GameMapWidget}; use crate::ui_widgets::game_map_widget::{ColoredCells, GameMapWidget};
struct SetBotsLayoutScreen {
curr_boat: usize,
layout: BoatsLayout,
}
type CoordinatesMapper = HashMap<Coordinates, Coordinates>; type CoordinatesMapper = HashMap<Coordinates, Coordinates>;
pub fn set_boat_layout<B: Backend>( pub struct SetBoatsLayoutScreen<'a> {
rules: &GameRules, curr_boat: usize,
terminal: &mut Terminal<B>, layout: BoatsLayout,
) -> io::Result<ScreenResult<BoatsLayout>> { rules: &'a GameRules,
let mut model = SetBotsLayoutScreen { }
impl<'a> SetBoatsLayoutScreen<'a> {
pub fn new(rules: &'a GameRules) -> Self {
Self {
curr_boat: 0, curr_boat: 0,
layout: BoatsLayout::gen_random_for_rules(rules) layout: BoatsLayout::gen_random_for_rules(rules)
.expect("Failed to generate initial boats layout"), .expect("Failed to generate initial boats layout"),
}; rules,
}
}
pub fn show<B: Backend>(
mut self,
terminal: &mut Terminal<B>,
) -> io::Result<ScreenResult<BoatsLayout>> {
let mut coordinates_mapper = CoordinatesMapper::default(); let mut coordinates_mapper = CoordinatesMapper::default();
let mut last_tick = Instant::now(); let mut last_tick = Instant::now();
let mut is_moving_boat = false; let mut is_moving_boat = false;
loop { loop {
terminal.draw(|f| coordinates_mapper = ui(f, &mut model, rules))?; terminal.draw(|f| coordinates_mapper = self.ui(f))?;
let timeout = TICK_RATE let timeout = TICK_RATE
.checked_sub(last_tick.elapsed()) .checked_sub(last_tick.elapsed())
@ -54,12 +59,12 @@ pub fn set_boat_layout<B: Backend>(
KeyCode::Char('q') => return Ok(ScreenResult::Canceled), KeyCode::Char('q') => return Ok(ScreenResult::Canceled),
// Select next boat // Select next boat
KeyCode::Char('n') => model.curr_boat += model.layout.number_of_boats() - 1, KeyCode::Char('n') => self.curr_boat += self.layout.number_of_boats() - 1,
// Rotate boat // Rotate boat
KeyCode::Char('r') => { KeyCode::Char('r') => {
model.layout.0[model.curr_boat].direction = self.layout.0[self.curr_boat].direction =
match model.layout.0[model.curr_boat].direction { match self.layout.0[self.curr_boat].direction {
BoatDirection::Right => BoatDirection::Down, BoatDirection::Right => BoatDirection::Down,
_ => BoatDirection::Right, _ => BoatDirection::Right,
} }
@ -73,21 +78,21 @@ pub fn set_boat_layout<B: Backend>(
// Submit configuration // Submit configuration
KeyCode::Enter => { KeyCode::Enter => {
if model.layout.is_valid(rules) { if self.layout.is_valid(self.rules) {
return Ok(ScreenResult::Ok(model.layout)); return Ok(ScreenResult::Ok(self.layout));
} }
} }
_ => {} _ => {}
} }
model.curr_boat %= model.layout.number_of_boats(); self.curr_boat %= self.layout.number_of_boats();
// Apply boat move // Apply boat move
if let Some((x, y)) = move_boat { if let Some((x, y)) = move_boat {
let new_pos = model.layout.0[model.curr_boat].start.add_x(x).add_y(y); let new_pos = self.layout.0[self.curr_boat].start.add_x(x).add_y(y);
if new_pos.is_valid(rules) { if new_pos.is_valid(self.rules) {
model.layout.0[model.curr_boat].start = new_pos; self.layout.0[self.curr_boat].start = new_pos;
} }
} }
} }
@ -98,8 +103,8 @@ pub fn set_boat_layout<B: Backend>(
// Start mouse action // Start mouse action
if MouseEventKind::Down(MouseButton::Left) == mouse.kind { if MouseEventKind::Down(MouseButton::Left) == mouse.kind {
is_moving_boat = if let Some(pos) = coordinates_mapper.get(&src_pos) { is_moving_boat = if let Some(pos) = coordinates_mapper.get(&src_pos) {
if let Some(b) = model.layout.find_boat_at_position(*pos) { if let Some(b) = self.layout.find_boat_at_position(*pos) {
model.curr_boat = model.layout.0.iter().position(|s| s == b).unwrap(); self.curr_boat = self.layout.0.iter().position(|s| s == b).unwrap();
} }
true true
@ -110,7 +115,7 @@ pub fn set_boat_layout<B: Backend>(
// Handle continue mouse action // Handle continue mouse action
else if is_moving_boat { else if is_moving_boat {
if let Some(pos) = coordinates_mapper.get(&src_pos) { if let Some(pos) = coordinates_mapper.get(&src_pos) {
model.layout.0[model.curr_boat].start = *pos; self.layout.0[self.curr_boat].start = *pos;
} }
if let MouseEventKind::Up(_) = mouse.kind { if let MouseEventKind::Up(_) = mouse.kind {
@ -126,29 +131,25 @@ pub fn set_boat_layout<B: Backend>(
} }
} }
fn ui<B: Backend>( fn ui<B: Backend>(&mut self, f: &mut Frame<B>) -> CoordinatesMapper {
f: &mut Frame<B>, let errors = self.layout.errors(self.rules);
model: &mut SetBotsLayoutScreen,
rules: &GameRules,
) -> CoordinatesMapper {
let errors = model.layout.errors(rules);
// Color of current boat // Color of current boat
let current_boat = ColoredCells { let current_boat = ColoredCells {
color: Color::Green, color: Color::Green,
cells: model.layout.0[model.curr_boat].all_coordinates(), cells: self.layout.0[self.curr_boat].all_coordinates(),
}; };
// Color of invalid boats // Color of invalid boats
let mut invalid_coordinates = vec![]; let mut invalid_coordinates = vec![];
for (idx, pos) in model.layout.boats().iter().enumerate() { for (idx, pos) in self.layout.boats().iter().enumerate() {
if idx == model.curr_boat { if idx == self.curr_boat {
continue; continue;
} }
if !model if !self
.layout .layout
.check_present_boat_position(idx, rules) .check_present_boat_position(idx, self.rules)
.is_empty() .is_empty()
{ {
invalid_coordinates.append(&mut pos.all_coordinates()); invalid_coordinates.append(&mut pos.all_coordinates());
@ -161,7 +162,7 @@ fn ui<B: Backend>(
// Color of other boats // Color of other boats
let mut other_boats_cells = vec![]; let mut other_boats_cells = vec![];
for boat in &model.layout.0 { for boat in &self.layout.0 {
other_boats_cells.append(&mut boat.all_coordinates()); other_boats_cells.append(&mut boat.all_coordinates());
} }
@ -180,7 +181,7 @@ fn ui<B: Backend>(
legend.push_str("Enter confirm layout"); legend.push_str("Enter confirm layout");
} }
let mut game_map_widget = GameMapWidget::new(rules) let mut game_map_widget = GameMapWidget::new(self.rules)
.set_default_empty_char(' ') .set_default_empty_char(' ')
.add_colored_cells(current_boat) .add_colored_cells(current_boat)
.add_colored_cells(invalid_boats) .add_colored_cells(invalid_boats)
@ -196,10 +197,10 @@ fn ui<B: Backend>(
.set_legend(legend); .set_legend(legend);
// Color of neighbors if boats can not touch // Color of neighbors if boats can not touch
if !rules.boats_can_touch { if !self.rules.boats_can_touch {
let mut boats_neighbors_cells = vec![]; let mut boats_neighbors_cells = vec![];
for boat in &model.layout.0 { for boat in &self.layout.0 {
for pos in boat.neighbor_coordinates(rules) { for pos in boat.neighbor_coordinates(self.rules) {
boats_neighbors_cells.push(pos); boats_neighbors_cells.push(pos);
} }
} }
@ -230,3 +231,4 @@ fn ui<B: Backend>(
coordinates_mapper coordinates_mapper
} }
}