extern crate num as num_renamed; use std::io; use std::time::{Duration, Instant}; use crossterm::event; use crossterm::event::{Event, KeyCode}; use tui::backend::Backend; use tui::layout::{Constraint, Direction, Layout, Margin}; use tui::style::{Color, Style}; use tui::widgets::*; use tui::{Frame, Terminal}; use sea_battle_backend::data::GameRules; use crate::constants::TICK_RATE; use crate::ui_screens::utils::centered_rect_size; use crate::ui_screens::ScreenResult; use crate::ui_widgets::button_widget::ButtonWidget; use crate::ui_widgets::checkbox_widget::CheckboxWidget; use crate::ui_widgets::text_editor_widget::TextEditorWidget; #[derive(num_derive::FromPrimitive, num_derive::ToPrimitive, Eq, PartialEq)] enum EditingField { MapWidth = 0, MapHeight, BoatsList, BoatsCanTouch, PlayerContinueOnHit, Cancel, OK, } 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, }; 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 crossterm::event::poll(timeout)? { let mut cursor_pos = model.curr_field as i32; 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, // Submit results KeyCode::Enter => { if model.curr_field == EditingField::Cancel { return Ok(ScreenResult::Canceled); } if model.curr_field == EditingField::OK && model.rules.is_valid() { return Ok(ScreenResult::Ok(model.rules)); } } 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 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(); } } } 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); } }