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)]
pub enum GameMode {
LastManStanding,
Frontlines,
FrontLines,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
+80 -5
View File
@@ -1,18 +1,91 @@
use crate::app::{
App, View,
keybindings::{Action, event_to_action},
states::SettingsValue,
};
use ratatui::crossterm::event::KeyEvent;
use std::any::type_name_of_val;
pub fn settings_keybindings(app: &mut App, key_event: &KeyEvent) {
if let Some(action) = event_to_action(&key_event) {
match action {
// Action::Up,
// Action::Down,
Action::Quit => app.exit = true,
Action::Quit2 => app.exit = true,
Action::Up => {
app.states.settings.selected_setting = app
.states
.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::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 => {
if app.states.settings.show_popup {
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);
}
}
_ => (),
Action::Quit => app.exit = true,
Action::Quit2 => app.exit = true,
// _ => (),
}
}
}
+94 -26
View File
@@ -1,3 +1,5 @@
use std::fmt::Display;
use crate::{
app::{GameMode, PerkDecks},
cli::Cli,
@@ -39,28 +41,63 @@ pub struct SkillsConfigState {
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)]
pub struct SettingsState {
pub id: usize,
pub name: &'static str,
pub options: SettingsOptions,
pub selected_setting: usize,
pub show_popup: bool,
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 {
@@ -91,18 +128,49 @@ impl GameStates {
selected_setting: 0,
show_popup: false,
selected_setting_new_value: String::new(),
options: SettingsOptions {
username: args.username,
game_mode: args.game_mode,
map_width: args.map_width,
map_height: args.map_height,
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,
error_message: String::new(),
options: vec![
SettingsOption {
name: "Username",
value: SettingsValue::Text(args.username),
},
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 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 {
Line::from(view_string).white()
Line::from(format!(" {view_string}")).white()
};
styled
+64 -22
View File
@@ -3,14 +3,15 @@ use ratatui::{
buffer::Buffer,
layout::{Alignment, Constraint, Layout, Rect},
style::{Style, Stylize},
text::Line,
widgets::{Block, Borders, Padding, Paragraph, Widget, Wrap},
};
pub fn settings_view(app: &App, area: Rect, buf: &mut Buffer) {
let vertical_layout: Layout = Layout::vertical([
Constraint::Length(10),
Constraint::Fill(1),
Constraint::Length(4),
Constraint::Fill(1),
Constraint::Length(5),
]);
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()
.gray()
.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);
Paragraph::new("Settings")
.alignment(Alignment::Center)
.yellow()
.block(
Block::new()
.gray()
.borders(Borders::LEFT | Borders::RIGHT)
.padding(Padding::new(1, 1, 1, 1)),
)
.render(main_area, buf);
let lines: Vec<Line<'_>> = app
.states
.settings
.options
.iter()
.enumerate()
.map(|(i, o)| {
if i == app.states.settings.selected_setting {
Line::from_iter([
"> ".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 {
Paragraph::new(app.states.settings.selected_setting_new_value.clone())
Paragraph::new(popup_text)
.wrap(Wrap { trim: true })
.style(Style::default().yellow())
.style(Style::default())
.block(
Block::new()
.title("Insert value")
.title_style(Style::default().green())
.title("Insert new value")
.title_style(Style::default().yellow())
.borders(Borders::ALL)
.border_style(Style::default().blue())
.border_style(Style::default().gray())
.padding(Padding {
left: 1,
right: 1,
@@ -57,10 +95,14 @@ pub fn settings_view(app: &App, area: Rect, buf: &mut Buffer) {
)
.render(
Rect {
x: main_area.width / 3,
y: main_area.height / 2,
width: main_area.width / 3,
height: main_area.height / 7,
x: 2,
y: main_area.height - 1,
width: main_area.width - 3,
height: if app.states.settings.error_message.is_empty() {
5
} else {
6
},
},
buf,
);
+5 -1
View File
@@ -72,7 +72,11 @@ impl Widget for KeybindingsWidget {
let block: Block<'_> = Block::default()
.borders(Borders::ALL)
.title("[ Keybindings ]");
.title(Line::from_iter([
"[ ".gray(),
"Keybindings".magenta(),
" ]".gray(),
]));
let inner: Rect = block.inner(area);