Rename crate
This commit is contained in:
rust
Cargo.lockCargo.toml
sea_battle_backend
sea_battle_cli_player
63
rust/sea_battle_cli_player/src/ui_widgets/button_widget.rs
Normal file
63
rust/sea_battle_cli_player/src/ui_widgets/button_widget.rs
Normal file
@ -0,0 +1,63 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::constants::HIGHLIGHT_COLOR;
|
||||
use tui::buffer::Buffer;
|
||||
use tui::layout::Rect;
|
||||
use tui::style::{Color, Style};
|
||||
use tui::widgets::*;
|
||||
|
||||
use crate::ui_screens::utils::centered_rect_size;
|
||||
|
||||
pub struct ButtonWidget {
|
||||
is_hovered: bool,
|
||||
label: String,
|
||||
disabled: bool,
|
||||
min_width: usize,
|
||||
}
|
||||
|
||||
impl ButtonWidget {
|
||||
pub fn new<D: Display>(label: D, is_hovered: bool) -> Self {
|
||||
Self {
|
||||
label: label.to_string(),
|
||||
is_hovered,
|
||||
disabled: false,
|
||||
min_width: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_min_width(mut self, min_width: usize) -> Self {
|
||||
self.min_width = min_width;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn estimated_size(&self) -> (u16, u16) {
|
||||
((self.label.len() + 2).max(self.min_width) as u16, 1)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for ButtonWidget {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let expected_len = self.estimated_size().0;
|
||||
|
||||
let mut label = self.label.clone();
|
||||
while label.len() < expected_len as usize {
|
||||
label.insert(0, ' ');
|
||||
label.push(' ');
|
||||
}
|
||||
|
||||
let area = centered_rect_size(label.len() as u16, 1, &area);
|
||||
|
||||
let input = Paragraph::new(label.as_ref()).style(match (self.disabled, self.is_hovered) {
|
||||
(true, _) => Style::default(),
|
||||
(_, false) => Style::default().bg(Color::DarkGray),
|
||||
(_, true) => Style::default().fg(Color::White).bg(HIGHLIGHT_COLOR),
|
||||
});
|
||||
|
||||
input.render(area, buf);
|
||||
}
|
||||
}
|
57
rust/sea_battle_cli_player/src/ui_widgets/checkbox_widget.rs
Normal file
57
rust/sea_battle_cli_player/src/ui_widgets/checkbox_widget.rs
Normal file
@ -0,0 +1,57 @@
|
||||
use crate::constants::HIGHLIGHT_COLOR;
|
||||
use std::fmt::Display;
|
||||
use tui::buffer::Buffer;
|
||||
use tui::layout::Rect;
|
||||
use tui::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(HIGHLIGHT_COLOR),
|
||||
});
|
||||
|
||||
input.render(area, buf);
|
||||
}
|
||||
}
|
204
rust/sea_battle_cli_player/src/ui_widgets/game_map_widget.rs
Normal file
204
rust/sea_battle_cli_player/src/ui_widgets/game_map_widget.rs
Normal file
@ -0,0 +1,204 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
|
||||
use tui::buffer::Buffer;
|
||||
use tui::layout::Rect;
|
||||
use tui::style::{Color, Style};
|
||||
use tui::widgets::{BorderType, Widget};
|
||||
|
||||
use sea_battle_backend::data::{Coordinates, GameRules, PlayConfiguration};
|
||||
|
||||
use crate::ui_screens::utils::centered_rect_size;
|
||||
|
||||
pub struct ColoredCells {
|
||||
pub color: Color,
|
||||
pub cells: Vec<Coordinates>,
|
||||
}
|
||||
|
||||
pub struct GameMapWidget<'a> {
|
||||
rules: &'a GameRules,
|
||||
default_empty_character: char,
|
||||
colored_cells: Vec<ColoredCells>,
|
||||
title: Option<String>,
|
||||
legend: Option<String>,
|
||||
yield_coordinates: Option<Box<dyn 'a + FnMut(Coordinates, Rect)>>,
|
||||
chars: HashMap<Coordinates, char>,
|
||||
}
|
||||
|
||||
impl<'a> GameMapWidget<'a> {
|
||||
pub fn new(rules: &'a GameRules) -> Self {
|
||||
Self {
|
||||
rules,
|
||||
default_empty_character: '.',
|
||||
colored_cells: vec![],
|
||||
title: None,
|
||||
legend: None,
|
||||
yield_coordinates: None,
|
||||
chars: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_default_empty_char(mut self, c: char) -> Self {
|
||||
self.default_empty_character = c;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn add_colored_cells(mut self, c: ColoredCells) -> Self {
|
||||
self.colored_cells.push(c);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_title<D: Display>(mut self, title: D) -> Self {
|
||||
self.title = Some(title.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_legend<D: Display>(mut self, legend: D) -> Self {
|
||||
self.legend = Some(legend.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_yield_func<F>(mut self, func: F) -> Self
|
||||
where
|
||||
F: 'a + FnMut(Coordinates, Rect),
|
||||
{
|
||||
self.yield_coordinates = Some(Box::new(func));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_char(mut self, coordinates: Coordinates, c: char) -> Self {
|
||||
self.chars.insert(coordinates, c);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_char_no_overwrite(mut self, coordinates: Coordinates, c: char) -> Self {
|
||||
self.chars.entry(coordinates).or_insert(c);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn grid_size(&self) -> (u16, u16) {
|
||||
let w = (self.rules.map_width as u16 * 2) + 2;
|
||||
let h = (self.rules.map_height as u16 * 2) + 2;
|
||||
|
||||
(w, h)
|
||||
}
|
||||
|
||||
pub fn estimated_size(&self) -> (u16, u16) {
|
||||
let (w, mut h) = self.grid_size();
|
||||
|
||||
if self.title.is_some() {
|
||||
h += 2;
|
||||
}
|
||||
|
||||
if let Some(l) = &self.legend {
|
||||
h += 1 + l.split('\n').count() as u16;
|
||||
}
|
||||
|
||||
(w, h)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for GameMapWidget<'a> {
|
||||
fn render(mut self, area: Rect, buf: &mut Buffer) {
|
||||
let alphabet = PlayConfiguration::default().ordinate_alphabet;
|
||||
|
||||
let symbols = BorderType::line_symbols(BorderType::Plain);
|
||||
|
||||
let mut start_y = area.y;
|
||||
|
||||
// Render title
|
||||
if let Some(title) = &self.title {
|
||||
let x = centered_rect_size(title.len() as u16, 1, &area).x;
|
||||
buf.set_string(x, start_y, title, Style::default());
|
||||
start_y += 2;
|
||||
}
|
||||
|
||||
// Paint game grid
|
||||
for y in 0..(self.rules.map_height + 1) {
|
||||
// Header (ordinate)
|
||||
if y < self.rules.map_height {
|
||||
buf.get_mut(area.x, start_y + 2 + (y as u16 * 2))
|
||||
.set_char(alphabet.chars().nth(y).unwrap());
|
||||
}
|
||||
|
||||
for x in 0..(self.rules.map_width + 1) {
|
||||
let coordinates = Coordinates::new(x as i32, y as i32);
|
||||
|
||||
// Header (abscissa)
|
||||
if x < self.rules.map_width {
|
||||
buf.set_string(
|
||||
area.x + 2 + (x as u16 * 2) - (x as u16) / 10,
|
||||
start_y,
|
||||
x.to_string(),
|
||||
Style::default(),
|
||||
);
|
||||
}
|
||||
|
||||
let o_x = 1 + area.x + (x as u16 * 2);
|
||||
let o_y = 1 + start_y + (y as u16 * 2);
|
||||
|
||||
let color = self
|
||||
.colored_cells
|
||||
.iter()
|
||||
.find(|c| c.cells.contains(&coordinates));
|
||||
|
||||
buf.get_mut(o_x, o_y).set_symbol(match (x, y) {
|
||||
(0, 0) => symbols.top_left,
|
||||
|
||||
(x, 0) if x == self.rules.map_width => symbols.top_right,
|
||||
(0, y) if y == self.rules.map_height => symbols.bottom_left,
|
||||
|
||||
(0, _) => symbols.vertical_right,
|
||||
(_, 0) => symbols.horizontal_down,
|
||||
|
||||
(x, y) if x == self.rules.map_width && y == self.rules.map_height => {
|
||||
symbols.bottom_right
|
||||
}
|
||||
|
||||
(x, _) if x == self.rules.map_width => symbols.vertical_left,
|
||||
(_, y) if y == self.rules.map_height => symbols.horizontal_up,
|
||||
|
||||
_ => symbols.cross,
|
||||
});
|
||||
|
||||
if x < self.rules.map_width {
|
||||
buf.get_mut(o_x + 1, o_y).set_symbol(symbols.horizontal);
|
||||
}
|
||||
|
||||
if y < self.rules.map_height {
|
||||
buf.get_mut(o_x, o_y + 1).set_symbol(symbols.vertical);
|
||||
}
|
||||
|
||||
if x < self.rules.map_width && y < self.rules.map_height {
|
||||
let cell = buf.get_mut(o_x + 1, o_y + 1).set_char(
|
||||
*self
|
||||
.chars
|
||||
.get(&coordinates)
|
||||
.unwrap_or(&self.default_empty_character),
|
||||
);
|
||||
|
||||
if let Some(c) = color {
|
||||
cell.set_bg(c.color);
|
||||
}
|
||||
|
||||
if let Some(f) = self.yield_coordinates.as_mut() {
|
||||
f(coordinates, Rect::new(o_x + 1, o_y + 1, 1, 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start_y += self.grid_size().1;
|
||||
|
||||
// Paint legend (if any)
|
||||
if let Some(legend) = &self.legend {
|
||||
start_y += 1;
|
||||
for line in legend.split('\n') {
|
||||
let center_rect = centered_rect_size(line.len() as u16, 1, &area);
|
||||
let x = center_rect.x;
|
||||
buf.set_string(x, start_y, line, Style::default());
|
||||
start_y += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
4
rust/sea_battle_cli_player/src/ui_widgets/mod.rs
Normal file
4
rust/sea_battle_cli_player/src/ui_widgets/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
pub mod button_widget;
|
||||
pub mod checkbox_widget;
|
||||
pub mod game_map_widget;
|
||||
pub mod text_editor_widget;
|
@ -0,0 +1,65 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::constants::HIGHLIGHT_COLOR;
|
||||
use tui::buffer::Buffer;
|
||||
use tui::layout::Rect;
|
||||
use tui::style::*;
|
||||
use tui::text::*;
|
||||
use tui::widgets::{Block, Borders, Paragraph, Widget};
|
||||
|
||||
#[derive(Eq, PartialEq)]
|
||||
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 span = Span::styled(
|
||||
self.value.to_string(),
|
||||
match &self.input_mode {
|
||||
InputMode::Normal => Style::default(),
|
||||
InputMode::Editing => Style::default().fg(HIGHLIGHT_COLOR),
|
||||
},
|
||||
);
|
||||
|
||||
let mut spans = vec![span];
|
||||
|
||||
// Add cursor if field is highlighted
|
||||
if self.input_mode == InputMode::Editing {
|
||||
spans.push(Span::styled(" ", Style::default().bg(HIGHLIGHT_COLOR)))
|
||||
}
|
||||
|
||||
let text = Text::from(Spans::from(spans));
|
||||
let input = Paragraph::new(text).block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(match self.input_mode {
|
||||
InputMode::Normal => Style::default(),
|
||||
InputMode::Editing => Style::default().fg(HIGHLIGHT_COLOR),
|
||||
})
|
||||
.title(self.label.as_ref()),
|
||||
);
|
||||
|
||||
input.render(area, buf);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user