generated from GarandPLG/rust-flake-template
Refactor settings UI and add typed option handling
- Rename `GameMode::Frontlines` to `FrontLines`. - Introduce `SettingsValue` enum and `SettingsOption` vector for typed settings. - Implement `Display` for `SettingsValue`, `GameMode`, and `PerkDecks`. - Enhance settings keybindings: navigation with Up/Down, value parsing on Enter, and error messages. - Update settings view to render options list, selection marker, and popup with error feedback. - Restyle main menu selector and keybindings widget title. - Add required imports and minor layout adjustments.
This commit is contained in:
+1
-1
@@ -27,7 +27,7 @@ pub struct App {
|
|||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
|
||||||
pub enum GameMode {
|
pub enum GameMode {
|
||||||
LastManStanding,
|
LastManStanding,
|
||||||
Frontlines,
|
FrontLines,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
|
||||||
|
|||||||
@@ -1,18 +1,91 @@
|
|||||||
use crate::app::{
|
use crate::app::{
|
||||||
App, View,
|
App, View,
|
||||||
keybindings::{Action, event_to_action},
|
keybindings::{Action, event_to_action},
|
||||||
|
states::SettingsValue,
|
||||||
};
|
};
|
||||||
use ratatui::crossterm::event::KeyEvent;
|
use ratatui::crossterm::event::KeyEvent;
|
||||||
|
use std::any::type_name_of_val;
|
||||||
|
|
||||||
pub fn settings_keybindings(app: &mut App, key_event: &KeyEvent) {
|
pub fn settings_keybindings(app: &mut App, key_event: &KeyEvent) {
|
||||||
if let Some(action) = event_to_action(&key_event) {
|
if let Some(action) = event_to_action(&key_event) {
|
||||||
match action {
|
match action {
|
||||||
// Action::Up,
|
Action::Up => {
|
||||||
// Action::Down,
|
app.states.settings.selected_setting = app
|
||||||
Action::Quit => app.exit = true,
|
.states
|
||||||
Action::Quit2 => app.exit = true,
|
.settings
|
||||||
|
.selected_setting
|
||||||
|
.saturating_sub(1)
|
||||||
|
.max(0)
|
||||||
|
}
|
||||||
|
Action::Down => {
|
||||||
|
app.states.settings.selected_setting = app
|
||||||
|
.states
|
||||||
|
.settings
|
||||||
|
.selected_setting
|
||||||
|
.saturating_add(1)
|
||||||
|
.min(9)
|
||||||
|
}
|
||||||
Action::Esc => app.view = View::MainMenu,
|
Action::Esc => app.view = View::MainMenu,
|
||||||
Action::Space => app.states.settings.show_popup = !app.states.settings.show_popup,
|
Action::Space => app.states.settings.show_popup = !app.states.settings.show_popup,
|
||||||
|
Action::Enter => {
|
||||||
|
// FIXME: No feedback
|
||||||
|
if app.states.settings.show_popup {
|
||||||
|
let option =
|
||||||
|
&app.states.settings.options[app.states.settings.selected_setting].value;
|
||||||
|
|
||||||
|
let option_type: &'static str = type_name_of_val(option);
|
||||||
|
|
||||||
|
let new_value: String = app.states.settings.selected_setting_new_value.clone();
|
||||||
|
|
||||||
|
match option_type {
|
||||||
|
"u8" => {
|
||||||
|
if let Ok(value) = new_value.parse::<u8>() {
|
||||||
|
app.states.settings.options[app.states.settings.selected_setting]
|
||||||
|
.value = SettingsValue::U8(value);
|
||||||
|
|
||||||
|
app.states.settings.selected_setting_new_value = "".to_string();
|
||||||
|
app.states.settings.error_message = "".to_string();
|
||||||
|
app.states.settings.show_popup = false;
|
||||||
|
} else {
|
||||||
|
app.states.settings.error_message = "Invalid value".to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"f32" => {
|
||||||
|
if let Ok(value) = new_value.parse::<f32>() {
|
||||||
|
app.states.settings.options[app.states.settings.selected_setting]
|
||||||
|
.value = SettingsValue::F32(value);
|
||||||
|
|
||||||
|
app.states.settings.selected_setting_new_value = "".to_string();
|
||||||
|
app.states.settings.error_message = "".to_string();
|
||||||
|
app.states.settings.show_popup = false;
|
||||||
|
} else {
|
||||||
|
app.states.settings.error_message = "Invalid value".to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"u16" => {
|
||||||
|
if let Ok(value) = new_value.parse::<u16>() {
|
||||||
|
app.states.settings.options[app.states.settings.selected_setting]
|
||||||
|
.value = SettingsValue::U16(value);
|
||||||
|
|
||||||
|
app.states.settings.selected_setting_new_value = "".to_string();
|
||||||
|
app.states.settings.error_message = "".to_string();
|
||||||
|
app.states.settings.show_popup = false;
|
||||||
|
} else {
|
||||||
|
app.states.settings.error_message = "Invalid value".to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"std::string::String" => {
|
||||||
|
app.states.settings.options[app.states.settings.selected_setting]
|
||||||
|
.value = SettingsValue::Text(new_value);
|
||||||
|
|
||||||
|
app.states.settings.selected_setting_new_value = "".to_string();
|
||||||
|
app.states.settings.error_message = "".to_string();
|
||||||
|
app.states.settings.show_popup = false;
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Action::Backspace => {
|
Action::Backspace => {
|
||||||
if app.states.settings.show_popup {
|
if app.states.settings.show_popup {
|
||||||
app.states.settings.selected_setting_new_value.pop();
|
app.states.settings.selected_setting_new_value.pop();
|
||||||
@@ -23,7 +96,9 @@ pub fn settings_keybindings(app: &mut App, key_event: &KeyEvent) {
|
|||||||
app.states.settings.selected_setting_new_value.push(c);
|
app.states.settings.selected_setting_new_value.push(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => (),
|
Action::Quit => app.exit = true,
|
||||||
|
Action::Quit2 => app.exit = true,
|
||||||
|
// _ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+94
-26
@@ -1,3 +1,5 @@
|
|||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
app::{GameMode, PerkDecks},
|
app::{GameMode, PerkDecks},
|
||||||
cli::Cli,
|
cli::Cli,
|
||||||
@@ -39,28 +41,63 @@ pub struct SkillsConfigState {
|
|||||||
pub selected_skill: usize,
|
pub selected_skill: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct SettingsOptions {
|
|
||||||
pub username: String,
|
|
||||||
pub game_mode: GameMode,
|
|
||||||
pub map_width: u8,
|
|
||||||
pub map_height: u8,
|
|
||||||
pub perk_deck: PerkDecks,
|
|
||||||
pub starting_wood: u8,
|
|
||||||
pub starting_iron: u8,
|
|
||||||
pub supply_limit: u8,
|
|
||||||
pub xp_modifier: f32,
|
|
||||||
pub skill_points_limit: u16,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct SettingsState {
|
pub struct SettingsState {
|
||||||
pub id: usize,
|
pub id: usize,
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
pub options: SettingsOptions,
|
|
||||||
pub selected_setting: usize,
|
pub selected_setting: usize,
|
||||||
pub show_popup: bool,
|
pub show_popup: bool,
|
||||||
pub selected_setting_new_value: String,
|
pub selected_setting_new_value: String,
|
||||||
|
pub error_message: String,
|
||||||
|
pub options: Vec<SettingsOption>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum SettingsValue {
|
||||||
|
U8(u8),
|
||||||
|
F32(f32),
|
||||||
|
U16(u16),
|
||||||
|
Text(String),
|
||||||
|
GameMode(GameMode),
|
||||||
|
PerkDeck(PerkDecks),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct SettingsOption {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub value: SettingsValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for SettingsValue {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
SettingsValue::U8(v) => write!(f, "{}", v),
|
||||||
|
SettingsValue::F32(v) => write!(f, "{}", v),
|
||||||
|
SettingsValue::U16(v) => write!(f, "{}", v),
|
||||||
|
SettingsValue::Text(v) => write!(f, "{}", v),
|
||||||
|
SettingsValue::GameMode(v) => write!(f, "{}", v),
|
||||||
|
SettingsValue::PerkDeck(v) => write!(f, "{}", v),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for GameMode {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
GameMode::FrontLines => write!(f, "Front Lines"),
|
||||||
|
GameMode::LastManStanding => write!(f, "Last Man Standing"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for PerkDecks {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
PerkDecks::Silesian => write!(f, "Silesian"),
|
||||||
|
PerkDecks::BogeyMan => write!(f, "Bogey Man"),
|
||||||
|
PerkDecks::Anteater => write!(f, "Anteater"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameStates {
|
impl GameStates {
|
||||||
@@ -91,18 +128,49 @@ impl GameStates {
|
|||||||
selected_setting: 0,
|
selected_setting: 0,
|
||||||
show_popup: false,
|
show_popup: false,
|
||||||
selected_setting_new_value: String::new(),
|
selected_setting_new_value: String::new(),
|
||||||
options: SettingsOptions {
|
error_message: String::new(),
|
||||||
username: args.username,
|
options: vec![
|
||||||
game_mode: args.game_mode,
|
SettingsOption {
|
||||||
map_width: args.map_width,
|
name: "Username",
|
||||||
map_height: args.map_height,
|
value: SettingsValue::Text(args.username),
|
||||||
perk_deck: args.perk_deck,
|
|
||||||
starting_wood: args.starting_wood,
|
|
||||||
starting_iron: args.starting_iron,
|
|
||||||
supply_limit: args.supply_limit,
|
|
||||||
xp_modifier: args.xp_modifier,
|
|
||||||
skill_points_limit: args.skill_points_limit,
|
|
||||||
},
|
},
|
||||||
|
SettingsOption {
|
||||||
|
name: "Game Mode",
|
||||||
|
value: SettingsValue::GameMode(args.game_mode),
|
||||||
|
},
|
||||||
|
SettingsOption {
|
||||||
|
name: "Map Width",
|
||||||
|
value: SettingsValue::U8(args.map_width),
|
||||||
|
},
|
||||||
|
SettingsOption {
|
||||||
|
name: "Map Height",
|
||||||
|
value: SettingsValue::U8(args.map_height),
|
||||||
|
},
|
||||||
|
SettingsOption {
|
||||||
|
name: "Perk Deck",
|
||||||
|
value: SettingsValue::PerkDeck(args.perk_deck),
|
||||||
|
},
|
||||||
|
SettingsOption {
|
||||||
|
name: "Starting Wood",
|
||||||
|
value: SettingsValue::U8(args.starting_wood),
|
||||||
|
},
|
||||||
|
SettingsOption {
|
||||||
|
name: "Starting Iron",
|
||||||
|
value: SettingsValue::U8(args.starting_iron),
|
||||||
|
},
|
||||||
|
SettingsOption {
|
||||||
|
name: "Supply Limit",
|
||||||
|
value: SettingsValue::U8(args.supply_limit),
|
||||||
|
},
|
||||||
|
SettingsOption {
|
||||||
|
name: "XP Modifier",
|
||||||
|
value: SettingsValue::F32(args.xp_modifier),
|
||||||
|
},
|
||||||
|
SettingsOption {
|
||||||
|
name: "Skill Points Limit",
|
||||||
|
value: SettingsValue::U16(args.skill_points_limit),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,9 +66,9 @@ pub fn main_menu_view(app: &App, area: Rect, buf: &mut Buffer) {
|
|||||||
let view_string: String = format_view_string(format!("{:?}", view));
|
let view_string: String = format_view_string(format!("{:?}", view));
|
||||||
|
|
||||||
let styled: Line<'_> = if app.states.main_menu.selected_view == i {
|
let styled: Line<'_> = if app.states.main_menu.selected_view == i {
|
||||||
Line::from(format!("> {view_string}")).yellow()
|
Line::from_iter(["> ".cyan(), view_string.yellow()]).yellow()
|
||||||
} else {
|
} else {
|
||||||
Line::from(view_string).white()
|
Line::from(format!(" {view_string}")).white()
|
||||||
};
|
};
|
||||||
|
|
||||||
styled
|
styled
|
||||||
|
|||||||
+64
-22
@@ -3,14 +3,15 @@ use ratatui::{
|
|||||||
buffer::Buffer,
|
buffer::Buffer,
|
||||||
layout::{Alignment, Constraint, Layout, Rect},
|
layout::{Alignment, Constraint, Layout, Rect},
|
||||||
style::{Style, Stylize},
|
style::{Style, Stylize},
|
||||||
|
text::Line,
|
||||||
widgets::{Block, Borders, Padding, Paragraph, Widget, Wrap},
|
widgets::{Block, Borders, Padding, Paragraph, Widget, Wrap},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn settings_view(app: &App, area: Rect, buf: &mut Buffer) {
|
pub fn settings_view(app: &App, area: Rect, buf: &mut Buffer) {
|
||||||
let vertical_layout: Layout = Layout::vertical([
|
let vertical_layout: Layout = Layout::vertical([
|
||||||
Constraint::Length(10),
|
|
||||||
Constraint::Fill(1),
|
|
||||||
Constraint::Length(4),
|
Constraint::Length(4),
|
||||||
|
Constraint::Fill(1),
|
||||||
|
Constraint::Length(5),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
let [title_area, main_area, keybindings_area] = vertical_layout.areas(area);
|
let [title_area, main_area, keybindings_area] = vertical_layout.areas(area);
|
||||||
@@ -23,31 +24,68 @@ pub fn settings_view(app: &App, area: Rect, buf: &mut Buffer) {
|
|||||||
Block::new()
|
Block::new()
|
||||||
.gray()
|
.gray()
|
||||||
.borders(Borders::LEFT | Borders::TOP | Borders::RIGHT)
|
.borders(Borders::LEFT | Borders::TOP | Borders::RIGHT)
|
||||||
.padding(Padding::new(1, 1, 1, 1)),
|
.padding(Padding::new(5, 1, 1, 1)),
|
||||||
)
|
)
|
||||||
.render(title_area, buf);
|
.render(title_area, buf);
|
||||||
|
|
||||||
Paragraph::new("Settings")
|
let lines: Vec<Line<'_>> = app
|
||||||
.alignment(Alignment::Center)
|
.states
|
||||||
.yellow()
|
.settings
|
||||||
.block(
|
.options
|
||||||
Block::new()
|
.iter()
|
||||||
.gray()
|
.enumerate()
|
||||||
.borders(Borders::LEFT | Borders::RIGHT)
|
.map(|(i, o)| {
|
||||||
.padding(Padding::new(1, 1, 1, 1)),
|
if i == app.states.settings.selected_setting {
|
||||||
)
|
Line::from_iter([
|
||||||
.render(main_area, buf);
|
"> ".cyan(),
|
||||||
|
o.name.yellow(),
|
||||||
|
": ".gray(),
|
||||||
|
o.value.to_string().light_green(),
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
Line::from(format!(" {}: {}", o.name, o.value)).white()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let settings_block: Block<'_> = Block::new()
|
||||||
|
.borders(Borders::LEFT | Borders::TOP | Borders::RIGHT)
|
||||||
|
.padding(Padding::new(1, 1, 1, 1))
|
||||||
|
.title(Line::from_iter([
|
||||||
|
"[ ".gray(),
|
||||||
|
"Settings".magenta(),
|
||||||
|
" ]".gray(),
|
||||||
|
]))
|
||||||
|
.gray();
|
||||||
|
|
||||||
|
let settings_area: Rect = settings_block.inner(main_area);
|
||||||
|
|
||||||
|
settings_block.render(main_area, buf);
|
||||||
|
|
||||||
|
Paragraph::new(lines).alignment(Alignment::Center).render(
|
||||||
|
settings_area.centered_vertically(Constraint::Ratio(1, 2)),
|
||||||
|
buf,
|
||||||
|
);
|
||||||
|
|
||||||
|
let popup_text: Vec<Line<'_>> = Vec::from_iter([
|
||||||
|
app.states
|
||||||
|
.settings
|
||||||
|
.selected_setting_new_value
|
||||||
|
.clone()
|
||||||
|
.into(),
|
||||||
|
app.states.settings.error_message.clone().into(),
|
||||||
|
]);
|
||||||
|
|
||||||
if app.states.settings.show_popup {
|
if app.states.settings.show_popup {
|
||||||
Paragraph::new(app.states.settings.selected_setting_new_value.clone())
|
Paragraph::new(popup_text)
|
||||||
.wrap(Wrap { trim: true })
|
.wrap(Wrap { trim: true })
|
||||||
.style(Style::default().yellow())
|
.style(Style::default())
|
||||||
.block(
|
.block(
|
||||||
Block::new()
|
Block::new()
|
||||||
.title("Insert value")
|
.title("Insert new value")
|
||||||
.title_style(Style::default().green())
|
.title_style(Style::default().yellow())
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.border_style(Style::default().blue())
|
.border_style(Style::default().gray())
|
||||||
.padding(Padding {
|
.padding(Padding {
|
||||||
left: 1,
|
left: 1,
|
||||||
right: 1,
|
right: 1,
|
||||||
@@ -57,10 +95,14 @@ pub fn settings_view(app: &App, area: Rect, buf: &mut Buffer) {
|
|||||||
)
|
)
|
||||||
.render(
|
.render(
|
||||||
Rect {
|
Rect {
|
||||||
x: main_area.width / 3,
|
x: 2,
|
||||||
y: main_area.height / 2,
|
y: main_area.height - 1,
|
||||||
width: main_area.width / 3,
|
width: main_area.width - 3,
|
||||||
height: main_area.height / 7,
|
height: if app.states.settings.error_message.is_empty() {
|
||||||
|
5
|
||||||
|
} else {
|
||||||
|
6
|
||||||
|
},
|
||||||
},
|
},
|
||||||
buf,
|
buf,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -72,7 +72,11 @@ impl Widget for KeybindingsWidget {
|
|||||||
|
|
||||||
let block: Block<'_> = Block::default()
|
let block: Block<'_> = Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.title("[ Keybindings ]");
|
.title(Line::from_iter([
|
||||||
|
"[ ".gray(),
|
||||||
|
"Keybindings".magenta(),
|
||||||
|
" ]".gray(),
|
||||||
|
]));
|
||||||
|
|
||||||
let inner: Rect = block.inner(area);
|
let inner: Rect = block.inner(area);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user