Start to build edit game rules screen

This commit is contained in:
Pierre HUBERT 2022-10-02 15:50:54 +02:00
parent 72af5df56f
commit 075b9e33e4
11 changed files with 392 additions and 8 deletions

71
rust/Cargo.lock generated
View File

@ -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"

View File

@ -14,3 +14,6 @@ tui = "0.19.0"
crossterm = "0.25.0"
lazy_static = "1.4.0"
tokio = "1.21.2"
num = "0.4.0"
num-traits = "0.2.15"
num-derive = "0.3.3"

View File

@ -2,3 +2,4 @@ pub mod cli_args;
pub mod constants;
pub mod server;
pub mod ui_screens;
pub mod ui_widgets;

View File

@ -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(())
}

View 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]);
}

View File

@ -1,2 +1,3 @@
pub mod configure_game_rules;
pub mod select_play_mode;
pub mod utils;

View File

@ -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,
}

View 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);
}
}

View 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);
}
}

View File

@ -0,0 +1,3 @@
pub mod button_widget;
pub mod checkbox_widget;
pub mod text_editor_widget;

View 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);
}
}