diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 809808b..cc429e9 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -467,6 +467,9 @@ dependencies = [ "env_logger", "lazy_static", "log", + "num", + "num-derive", + "num-traits", "sea_battle_backend", "tokio", "tui", @@ -1009,6 +1012,51 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "num" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43db66d1170d347f9a065114077f7dccb00c1b9478c89384490a3425279a4606" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ae39348c8bc5fbd7f40c727a9925f03517afd2ab27d46702108b6a7e5414c19" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1019,6 +1067,29 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.15" diff --git a/rust/cli_player/Cargo.toml b/rust/cli_player/Cargo.toml index 81d3d61..81c3ca9 100644 --- a/rust/cli_player/Cargo.toml +++ b/rust/cli_player/Cargo.toml @@ -13,4 +13,7 @@ env_logger = "0.9.0" tui = "0.19.0" crossterm = "0.25.0" lazy_static = "1.4.0" -tokio = "1.21.2" \ No newline at end of file +tokio = "1.21.2" +num = "0.4.0" +num-traits = "0.2.15" +num-derive = "0.3.3" \ No newline at end of file diff --git a/rust/cli_player/src/lib.rs b/rust/cli_player/src/lib.rs index c53eb9f..849b5ab 100644 --- a/rust/cli_player/src/lib.rs +++ b/rust/cli_player/src/lib.rs @@ -2,3 +2,4 @@ pub mod cli_args; pub mod constants; pub mod server; pub mod ui_screens; +pub mod ui_widgets; diff --git a/rust/cli_player/src/main.rs b/rust/cli_player/src/main.rs index 95be3b2..9e76740 100644 --- a/rust/cli_player/src/main.rs +++ b/rust/cli_player/src/main.rs @@ -12,11 +12,17 @@ use tui::backend::{Backend, CrosstermBackend}; use tui::Terminal; use cli_player::server::start_server_if_missing; -use cli_player::ui_screens::select_play_mode; +use cli_player::ui_screens::*; +use sea_battle_backend::data::{GameRules, PlayConfiguration}; async fn run_app(terminal: &mut Terminal) -> Result<(), Box> { - let res = select_play_mode::select_play_mode(terminal)?; - println!("selected play mode: {:?}", res); + // Temporary code + let res = configure_game_rules::configure_play_rules( + GameRules::default(), + PlayConfiguration::default(), + terminal, + )?; + println!("configured rules: {:?}", res); Ok(()) } diff --git a/rust/cli_player/src/ui_screens/configure_game_rules.rs b/rust/cli_player/src/ui_screens/configure_game_rules.rs new file mode 100644 index 0000000..a7c36d4 --- /dev/null +++ b/rust/cli_player/src/ui_screens/configure_game_rules.rs @@ -0,0 +1,162 @@ +extern crate num as num_renamed; + +use std::cmp::max; +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::*; +use tui::text::Text; +use tui::widgets::*; +use tui::{Frame, Terminal}; + +use sea_battle_backend::data::{GameRules, PlayConfiguration}; + +use crate::constants::TICK_RATE; +use crate::ui_screens::utils::centered_rect_size; +use crate::ui_widgets::button_widget::ButtonWidget; +use crate::ui_widgets::checkbox_widget::CheckboxWidget; +use crate::ui_widgets::text_editor_widget::TextEditorWidget; + +#[derive(Debug)] +pub enum ConfigureGameRulesResult { + Ok(GameRules), + Canceled, +} + +#[derive(num_derive::FromPrimitive, num_derive::ToPrimitive, Eq, PartialEq)] +enum EditingField { + MapWidth = 0, + MapHeight, + BoatsList, + BoatsCanTouch, + PlayerContinueOnHit, + Cancel, + OK, +} + +struct GameRulesConfigurationScreen { + config: PlayConfiguration, + rules: GameRules, + curr_field: EditingField, +} + +pub fn configure_play_rules( + rules: GameRules, + config: PlayConfiguration, + terminal: &mut Terminal, +) -> io::Result { + let mut model = GameRulesConfigurationScreen { + config, + 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 { + KeyCode::Char('q') => return Ok(ConfigureGameRulesResult::Canceled), + KeyCode::Up => cursor_pos -= 1, + KeyCode::Down => cursor_pos += 1, + _ => {} + } + } + + // Apply new cursor position + cursor_pos = max(0, cursor_pos); + if let Some(val) = num_renamed::FromPrimitive::from_u64(cursor_pos as u64) { + model.curr_field = val; + } + } + 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(3), + ]) + .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]); + + let buttons_chunk = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Percentage(50), Constraint::Percentage(50)]) + .split(*chunks.last().unwrap()); + + 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); + f.render_widget(button, buttons_chunk[1]); +} diff --git a/rust/cli_player/src/ui_screens/mod.rs b/rust/cli_player/src/ui_screens/mod.rs index ae73570..f08c34b 100644 --- a/rust/cli_player/src/ui_screens/mod.rs +++ b/rust/cli_player/src/ui_screens/mod.rs @@ -1,2 +1,3 @@ +pub mod configure_game_rules; pub mod select_play_mode; pub mod utils; diff --git a/rust/cli_player/src/ui_screens/utils.rs b/rust/cli_player/src/ui_screens/utils.rs index 5eef9b8..94ab278 100644 --- a/rust/cli_player/src/ui_screens/utils.rs +++ b/rust/cli_player/src/ui_screens/utils.rs @@ -31,16 +31,16 @@ pub fn centered_rect_percentage(percent_x: u16, percent_y: u16, r: Rect) -> Rect pub fn centered_rect_size(width: u16, height: u16, parent: Rect) -> Rect { if parent.width < width || parent.height < height { return Rect { - x: 0, - y: 0, + x: parent.x, + y: parent.y, width: parent.width, height: parent.height, }; } Rect { - x: (parent.width - width) / 2, - y: (parent.height - height) / 2, + x: parent.x + (parent.width - width) / 2, + y: parent.y + (parent.height - height) / 2, width, height, } diff --git a/rust/cli_player/src/ui_widgets/button_widget.rs b/rust/cli_player/src/ui_widgets/button_widget.rs new file mode 100644 index 0000000..0bfd68c --- /dev/null +++ b/rust/cli_player/src/ui_widgets/button_widget.rs @@ -0,0 +1,35 @@ +use crate::ui_screens::utils::centered_rect_size; +use std::fmt::Display; +use tui::buffer::Buffer; +use tui::layout::Rect; +use tui::style::{Color, Style}; +use tui::widgets::*; + +pub struct ButtonWidget { + is_hovered: bool, + label: String, +} + +impl ButtonWidget { + pub fn new(label: D, is_hovered: bool) -> Self { + Self { + label: label.to_string(), + is_hovered, + } + } +} + +impl Widget for ButtonWidget { + fn render(self, area: Rect, buf: &mut Buffer) { + let label = format!(" {} ", self.label); + + let area = centered_rect_size(label.len() as u16, 1, area); + + let input = Paragraph::new(label.as_ref()).style(match &self.is_hovered { + false => Style::default().bg(Color::DarkGray), + true => Style::default().fg(Color::White).bg(Color::Yellow), + }); + + input.render(area, buf); + } +} diff --git a/rust/cli_player/src/ui_widgets/checkbox_widget.rs b/rust/cli_player/src/ui_widgets/checkbox_widget.rs new file mode 100644 index 0000000..f40e648 --- /dev/null +++ b/rust/cli_player/src/ui_widgets/checkbox_widget.rs @@ -0,0 +1,56 @@ +use std::fmt::Display; +use tui::buffer::Buffer; +use tui::layout::Rect; +use tui::style::{Color, Style}; +use tui::widgets::*; + +pub struct CheckboxWidget { + is_editing: bool, + checked: bool, + label: String, + is_radio: bool, +} + +impl CheckboxWidget { + pub fn new(label: D, checked: bool, is_editing: bool) -> Self { + Self { + is_editing, + checked, + label: label.to_string(), + is_radio: false, + } + } + + pub fn set_radio(mut self) -> Self { + self.is_radio = true; + self + } +} + +impl Widget for CheckboxWidget { + fn render(self, area: Rect, buf: &mut Buffer) { + let paragraph = format!( + "{}{}{} {}", + match self.is_radio { + true => "(", + false => "[", + }, + match self.checked { + true => "X", + false => " ", + }, + match self.is_radio { + true => ")", + false => "]", + }, + self.label + ); + + let input = Paragraph::new(paragraph.as_ref()).style(match &self.is_editing { + false => Style::default(), + true => Style::default().fg(Color::Yellow), + }); + + input.render(area, buf); + } +} diff --git a/rust/cli_player/src/ui_widgets/mod.rs b/rust/cli_player/src/ui_widgets/mod.rs new file mode 100644 index 0000000..0bfdfdc --- /dev/null +++ b/rust/cli_player/src/ui_widgets/mod.rs @@ -0,0 +1,3 @@ +pub mod button_widget; +pub mod checkbox_widget; +pub mod text_editor_widget; diff --git a/rust/cli_player/src/ui_widgets/text_editor_widget.rs b/rust/cli_player/src/ui_widgets/text_editor_widget.rs new file mode 100644 index 0000000..bfb2089 --- /dev/null +++ b/rust/cli_player/src/ui_widgets/text_editor_widget.rs @@ -0,0 +1,46 @@ +use std::fmt::Display; +use tui::buffer::Buffer; +use tui::layout::Rect; +use tui::style::{Color, Style}; +use tui::widgets::{Block, Borders, Paragraph, Widget}; + +pub enum InputMode { + Normal, + Editing, +} + +pub struct TextEditorWidget { + input_mode: InputMode, + value: String, + label: String, +} + +impl TextEditorWidget { + pub fn new(label: D, value: D, is_editing: bool) -> Self { + Self { + input_mode: match is_editing { + true => InputMode::Editing, + false => InputMode::Normal, + }, + value: value.to_string(), + label: label.to_string(), + } + } +} + +impl Widget for TextEditorWidget { + fn render(self, area: Rect, buf: &mut Buffer) { + let input = Paragraph::new(self.value.as_ref()) + .style(match &self.input_mode { + InputMode::Normal => Style::default(), + InputMode::Editing => Style::default().fg(Color::Yellow), + }) + .block( + Block::default() + .borders(Borders::ALL) + .title(self.label.as_ref()), + ); + + input.render(area, buf); + } +}