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:
2026-03-16 21:42:08 +01:00
parent 1d8d1eed46
commit 53a713b5ea
6 changed files with 247 additions and 58 deletions
+1 -1
View File
@@ -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)]
+80 -5
View File
@@ -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
View File
@@ -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),
},
],
}, },
} }
} }
+2 -2
View File
@@ -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
View File
@@ -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,
); );
+5 -1
View File
@@ -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);