Start to build edit game rules screen
This commit is contained in:
parent
72af5df56f
commit
075b9e33e4
71
rust/Cargo.lock
generated
71
rust/Cargo.lock
generated
@ -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"
|
||||
|
@ -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"
|
||||
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 server;
|
||||
pub mod ui_screens;
|
||||
pub mod ui_widgets;
|
||||
|
@ -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<B: Backend>(terminal: &mut Terminal<B>) -> Result<(), Box<dyn Error>> {
|
||||
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(())
|
||||
}
|
||||
|
||||
|
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 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 {
|
||||
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,
|
||||
}
|
||||
|
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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user