Start to build edit game rules screen
This commit is contained in:
		
							
								
								
									
										71
									
								
								rust/Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										71
									
								
								rust/Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -467,6 +467,9 @@ dependencies = [ | |||||||
|  "env_logger", |  "env_logger", | ||||||
|  "lazy_static", |  "lazy_static", | ||||||
|  "log", |  "log", | ||||||
|  |  "num", | ||||||
|  |  "num-derive", | ||||||
|  |  "num-traits", | ||||||
|  "sea_battle_backend", |  "sea_battle_backend", | ||||||
|  "tokio", |  "tokio", | ||||||
|  "tui", |  "tui", | ||||||
| @@ -1009,6 +1012,51 @@ dependencies = [ | |||||||
|  "windows-sys", |  "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]] | [[package]] | ||||||
| name = "num-integer" | name = "num-integer" | ||||||
| version = "0.1.45" | version = "0.1.45" | ||||||
| @@ -1019,6 +1067,29 @@ dependencies = [ | |||||||
|  "num-traits", |  "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]] | [[package]] | ||||||
| name = "num-traits" | name = "num-traits" | ||||||
| version = "0.2.15" | version = "0.2.15" | ||||||
|   | |||||||
| @@ -13,4 +13,7 @@ env_logger = "0.9.0" | |||||||
| tui = "0.19.0" | tui = "0.19.0" | ||||||
| crossterm = "0.25.0" | crossterm = "0.25.0" | ||||||
| lazy_static = "1.4.0" | lazy_static = "1.4.0" | ||||||
| tokio = "1.21.2" | tokio = "1.21.2" | ||||||
|  | num = "0.4.0" | ||||||
|  | num-traits = "0.2.15" | ||||||
|  | num-derive = "0.3.3" | ||||||
| @@ -2,3 +2,4 @@ pub mod cli_args; | |||||||
| pub mod constants; | pub mod constants; | ||||||
| pub mod server; | pub mod server; | ||||||
| pub mod ui_screens; | pub mod ui_screens; | ||||||
|  | pub mod ui_widgets; | ||||||
|   | |||||||
| @@ -12,11 +12,17 @@ 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::select_play_mode; | use cli_player::ui_screens::*; | ||||||
|  | use sea_battle_backend::data::{GameRules, PlayConfiguration}; | ||||||
|  |  | ||||||
| async fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> Result<(), Box<dyn Error>> { | async fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> Result<(), Box<dyn Error>> { | ||||||
|     let res = select_play_mode::select_play_mode(terminal)?; |     // Temporary code | ||||||
|     println!("selected play mode: {:?}", res); |     let res = configure_game_rules::configure_play_rules( | ||||||
|  |         GameRules::default(), | ||||||
|  |         PlayConfiguration::default(), | ||||||
|  |         terminal, | ||||||
|  |     )?; | ||||||
|  |     println!("configured rules: {:?}", res); | ||||||
|     Ok(()) |     Ok(()) | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										162
									
								
								rust/cli_player/src/ui_screens/configure_game_rules.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								rust/cli_player/src/ui_screens/configure_game_rules.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<B: Backend>( | ||||||
|  |     rules: GameRules, | ||||||
|  |     config: PlayConfiguration, | ||||||
|  |     terminal: &mut Terminal<B>, | ||||||
|  | ) -> io::Result<ConfigureGameRulesResult> { | ||||||
|  |     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<B: Backend>(f: &mut Frame<B>, 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::<Vec<_>>() | ||||||
|  |             .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]); | ||||||
|  | } | ||||||
| @@ -1,2 +1,3 @@ | |||||||
|  | pub mod configure_game_rules; | ||||||
| pub mod select_play_mode; | pub mod select_play_mode; | ||||||
| pub mod utils; | pub mod utils; | ||||||
|   | |||||||
| @@ -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 { | pub fn centered_rect_size(width: u16, height: u16, parent: Rect) -> Rect { | ||||||
|     if parent.width < width || parent.height < height { |     if parent.width < width || parent.height < height { | ||||||
|         return Rect { |         return Rect { | ||||||
|             x: 0, |             x: parent.x, | ||||||
|             y: 0, |             y: parent.y, | ||||||
|             width: parent.width, |             width: parent.width, | ||||||
|             height: parent.height, |             height: parent.height, | ||||||
|         }; |         }; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     Rect { |     Rect { | ||||||
|         x: (parent.width - width) / 2, |         x: parent.x + (parent.width - width) / 2, | ||||||
|         y: (parent.height - height) / 2, |         y: parent.y + (parent.height - height) / 2, | ||||||
|         width, |         width, | ||||||
|         height, |         height, | ||||||
|     } |     } | ||||||
|   | |||||||
							
								
								
									
										35
									
								
								rust/cli_player/src/ui_widgets/button_widget.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								rust/cli_player/src/ui_widgets/button_widget.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<D: Display>(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); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										56
									
								
								rust/cli_player/src/ui_widgets/checkbox_widget.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								rust/cli_player/src/ui_widgets/checkbox_widget.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<D: Display>(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); | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										3
									
								
								rust/cli_player/src/ui_widgets/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								rust/cli_player/src/ui_widgets/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | pub mod button_widget; | ||||||
|  | pub mod checkbox_widget; | ||||||
|  | pub mod text_editor_widget; | ||||||
							
								
								
									
										46
									
								
								rust/cli_player/src/ui_widgets/text_editor_widget.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								rust/cli_player/src/ui_widgets/text_editor_widget.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -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<D: Display>(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); | ||||||
|  |     } | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user