From 1d1dc0f2f76550d178b5c5f8da9f22c570d6e72a Mon Sep 17 00:00:00 2001 From: GarandPLG Date: Sun, 29 Mar 2026 14:12:03 +0200 Subject: [PATCH] Add zoom support and dynamic keybinding layout Introduce ZoomIn/ZoomOut actions and a Zoom group with corresponding keybindings. Add a CLI option to set the initial zoom level and a ZoomLevel enum. Expose a utility to compute the largest keybinding group size for layout sizing. Refactor board widget to adjust cell dimensions based on zoom level and simplify offset calculations. Update views to use the new layout sizing helper and integrate zoom handling in skirmish logic. --- src/app/keybindings/keybindings.rs | 34 ++++++++++++++++++++++++ src/app/keybindings/mod.rs | 4 ++- src/app/keybindings/skirmish.rs | 19 ++++++++++++++ src/app/state.rs | 1 + src/app/states/mod.rs | 2 +- src/app/states/skirmish.rs | 14 +++++++--- src/app/views/default.rs | 14 +++++++--- src/app/views/main_menu.rs | 27 ++++++++++++------- src/app/views/skirmish.rs | 26 +++++++++--------- src/app/widgets/board.rs | 42 ++++++++++++++++++------------ src/cli.rs | 11 +++++++- 11 files changed, 145 insertions(+), 49 deletions(-) diff --git a/src/app/keybindings/keybindings.rs b/src/app/keybindings/keybindings.rs index 4972b0c..d2ecc53 100644 --- a/src/app/keybindings/keybindings.rs +++ b/src/app/keybindings/keybindings.rs @@ -1,5 +1,6 @@ use clap::ValueEnum; use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers}; +use std::collections::HashMap; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Action { @@ -17,6 +18,8 @@ pub enum Action { Enter, Esc, Backspace, + ZoomIn, + ZoomOut, WildCard(char), } @@ -27,6 +30,7 @@ pub enum Group { Scroll, Select, Input, + Zoom, Quit, } @@ -168,6 +172,24 @@ pub static KEYBINDINGS: &[KeyBinding] = &[ symbol: "Backspace", description: "Delete character", }, + KeyBinding { + action: Action::ZoomIn, + code: KeyCode::Char(','), + kind: KeyEventKind::Press, + modifiers: KeyModifiers::NONE, + group: Group::Zoom, + symbol: ",", + description: "Zoom in", + }, + KeyBinding { + action: Action::ZoomOut, + code: KeyCode::Char('.'), + kind: KeyEventKind::Press, + modifiers: KeyModifiers::NONE, + group: Group::Zoom, + symbol: ".", + description: "Zoom out", + }, KeyBinding { action: Action::WildCard('_'), code: KeyCode::Char('_'), @@ -202,3 +224,15 @@ pub fn event_to_action(event: &KeyEvent) -> Option { None } + +pub fn count_largest_group(actions: &Vec) -> u16 { + let mut group_counts: HashMap = HashMap::new(); + + for action in actions { + if let Some(binding) = binding_for(*action) { + *group_counts.entry(binding.group).or_insert(0) += 1; + } + } + + group_counts.values().copied().max().map_or(0, |v| v) +} diff --git a/src/app/keybindings/mod.rs b/src/app/keybindings/mod.rs index 2696876..f6e45f7 100644 --- a/src/app/keybindings/mod.rs +++ b/src/app/keybindings/mod.rs @@ -4,6 +4,8 @@ pub mod main_menu; pub mod skirmish; pub use default::{common_keybindings, default_keybindings}; -pub use keybindings::{Action, Group, KEYBINDINGS, KeyBinding, binding_for, event_to_action}; +pub use keybindings::{ + Action, Group, KEYBINDINGS, KeyBinding, binding_for, count_largest_group, event_to_action, +}; pub use main_menu::main_menu_keybindings; pub use skirmish::skirmish_keybindings; diff --git a/src/app/keybindings/skirmish.rs b/src/app/keybindings/skirmish.rs index afa8755..8825d95 100644 --- a/src/app/keybindings/skirmish.rs +++ b/src/app/keybindings/skirmish.rs @@ -1,6 +1,7 @@ use crate::app::{ App, keybindings::{Action, common_keybindings, event_to_action}, + states::ZoomLevel, }; use ratatui::crossterm::event::KeyEvent; @@ -12,6 +13,24 @@ pub fn skirmish_keybindings(app: &mut App, key_event: &KeyEvent) { Action::ScrollDown => app.states.skirmish.vertical_offset.next(), Action::ScrollLeft => app.states.skirmish.horizontal_offset.prev(), Action::ScrollRight => app.states.skirmish.horizontal_offset.next(), + Action::ZoomIn => match app.states.skirmish.zoom_level { + ZoomLevel::ZoomedIn => {} + ZoomLevel::Default => { + app.states.skirmish.zoom_level = ZoomLevel::ZoomedIn; + } + ZoomLevel::ZoomedOut => { + app.states.skirmish.zoom_level = ZoomLevel::Default; + } + }, + Action::ZoomOut => match app.states.skirmish.zoom_level { + ZoomLevel::ZoomedIn => { + app.states.skirmish.zoom_level = ZoomLevel::Default; + } + ZoomLevel::Default => { + app.states.skirmish.zoom_level = ZoomLevel::ZoomedOut; + } + ZoomLevel::ZoomedOut => {} + }, _ => (), } } diff --git a/src/app/state.rs b/src/app/state.rs index 66c871f..c046b43 100644 --- a/src/app/state.rs +++ b/src/app/state.rs @@ -30,6 +30,7 @@ impl GameStates { map_width: args.map_width as usize, map_height: args.map_height as usize, board_cells: Vec::new(), + zoom_level: args.zoom_level, }, perk_decks: PerkDecksState { id: 2, diff --git a/src/app/states/mod.rs b/src/app/states/mod.rs index 900c2d3..6fb5fcb 100644 --- a/src/app/states/mod.rs +++ b/src/app/states/mod.rs @@ -8,4 +8,4 @@ pub use main_menu::MainMenuState; pub use perk_decks::{PerkDecks, PerkDecksState}; pub use settings::SettingsState; pub use skills_config::SkillsConfigState; -pub use skirmish::{GameMode, Offset, SkirmishState}; +pub use skirmish::{GameMode, Offset, SkirmishState, ZoomLevel}; diff --git a/src/app/states/skirmish.rs b/src/app/states/skirmish.rs index 5e98029..4414ff8 100644 --- a/src/app/states/skirmish.rs +++ b/src/app/states/skirmish.rs @@ -33,9 +33,9 @@ impl Offset { } pub fn set_max(&mut self, max: usize) { - if self.max_initiated { - return; - } + // if self.max_initiated { + // return; + // } self.max = max; self.max_initiated = true; @@ -59,6 +59,7 @@ pub struct SkirmishState { pub vertical_offset: Offset, pub horizontal_offset: Offset, pub board_cells: Vec, + pub zoom_level: ZoomLevel, } impl SkirmishState { @@ -80,3 +81,10 @@ pub enum GameMode { LastManStanding, FrontLines, } + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)] +pub enum ZoomLevel { + ZoomedIn, + Default, + ZoomedOut, +} diff --git a/src/app/views/default.rs b/src/app/views/default.rs index 20ecdf5..dd3fd44 100644 --- a/src/app/views/default.rs +++ b/src/app/views/default.rs @@ -1,4 +1,7 @@ -use crate::app::{keybindings::Action, widgets::KeybindingsWidget}; +use crate::app::{ + keybindings::{Action, count_largest_group}, + widgets::KeybindingsWidget, +}; use ratatui::{ buffer::Buffer, layout::{Alignment, Constraint, Layout, Rect}, @@ -7,7 +10,12 @@ use ratatui::{ }; pub fn default_view(area: Rect, buf: &mut Buffer) { - let vertical_layout: Layout = Layout::vertical([Constraint::Fill(1), Constraint::Length(4)]); + let actions: Vec = vec![Action::Quit, Action::Quit2, Action::Esc]; + + let vertical_layout: Layout = Layout::vertical([ + Constraint::Fill(1), + Constraint::Length(count_largest_group(&actions) + 2), + ]); let [main_area, keybindings_area] = vertical_layout.areas(area); @@ -24,8 +32,6 @@ pub fn default_view(area: Rect, buf: &mut Buffer) { } { - let actions: Vec = vec![Action::Quit, Action::Quit2, Action::Esc]; - KeybindingsWidget::new(actions).render(keybindings_area, buf); } } diff --git a/src/app/views/main_menu.rs b/src/app/views/main_menu.rs index 010602a..f674c90 100644 --- a/src/app/views/main_menu.rs +++ b/src/app/views/main_menu.rs @@ -1,4 +1,8 @@ -use crate::app::{App, View, keybindings::Action, widgets::KeybindingsWidget}; +use crate::app::{ + App, View, + keybindings::{Action, count_largest_group}, + widgets::KeybindingsWidget, +}; use clap::ValueEnum; use ratatui::{ buffer::Buffer, @@ -23,7 +27,18 @@ fn format_view_string(s: String) -> String { } pub fn main_menu_view(app: &App, area: Rect, buf: &mut Buffer) { - let vertical_layout: Layout = Layout::vertical([Constraint::Fill(1), Constraint::Length(4)]); + let actions: Vec = vec![ + Action::Up, + Action::Down, + Action::Space, + Action::Quit, + Action::Quit2, + ]; + + let vertical_layout: Layout = Layout::vertical([ + Constraint::Fill(1), + Constraint::Length(count_largest_group(&actions) + 2), + ]); let [main_menu_area, keybindings_area] = vertical_layout.areas(area); @@ -83,14 +98,6 @@ pub fn main_menu_view(app: &App, area: Rect, buf: &mut Buffer) { } { - let actions: Vec = vec![ - Action::Up, - Action::Down, - Action::Space, - Action::Quit, - Action::Quit2, - ]; - KeybindingsWidget::new(actions).render(keybindings_area, buf); } } diff --git a/src/app/views/skirmish.rs b/src/app/views/skirmish.rs index 79dcff9..fe4543a 100644 --- a/src/app/views/skirmish.rs +++ b/src/app/views/skirmish.rs @@ -1,6 +1,6 @@ use crate::app::{ App, - keybindings::Action, + keybindings::{Action, count_largest_group}, widgets::{BoardWidget, KeybindingsWidget}, }; use ratatui::{ @@ -12,10 +12,22 @@ use ratatui::{ }; pub fn skirmish_view(app: &mut App, area: Rect, buf: &mut Buffer) { + let actions: Vec = vec![ + Action::ScrollUp, + Action::ScrollDown, + Action::ScrollLeft, + Action::ScrollRight, + Action::ZoomIn, + Action::ZoomOut, + Action::Quit, + Action::Quit2, + Action::Esc, + ]; + let vertical_layout: Layout = Layout::vertical([ Constraint::Length(4), Constraint::Fill(1), - Constraint::Length(6), + Constraint::Length(count_largest_group(&actions) + 2), ]); let [title_area, main_area, keybindings_area] = vertical_layout.areas(area); @@ -78,16 +90,6 @@ pub fn skirmish_view(app: &mut App, area: Rect, buf: &mut Buffer) { } { - let actions: Vec = vec![ - Action::ScrollUp, - Action::ScrollDown, - Action::ScrollLeft, - Action::ScrollRight, - Action::Quit, - Action::Quit2, - Action::Esc, - ]; - KeybindingsWidget::new(actions).render(keybindings_area, buf); } } diff --git a/src/app/widgets/board.rs b/src/app/widgets/board.rs index 2f67a11..6496c36 100644 --- a/src/app/widgets/board.rs +++ b/src/app/widgets/board.rs @@ -1,4 +1,4 @@ -use crate::app::{App, widgets::CellWidget}; +use crate::app::{App, states::ZoomLevel, widgets::CellWidget}; use ratatui::{ buffer::Buffer, layout::{Constraint, Layout, Rect}, @@ -18,24 +18,32 @@ pub struct BoardWidget<'a> { } impl<'a> BoardWidget<'a> { + fn max_offset(map_size: u16, size: u16) -> usize { + if map_size > size { + (map_size - size) as usize + } else { + 0 + } + } + pub fn new(app: &'a mut App, area_width: u16, area_height: u16) -> Self { - const CELL_HIGHT: u16 = 5; - const CELL_WIDTH: u16 = 9; + let cell_height: u16 = match app.states.skirmish.zoom_level { + ZoomLevel::ZoomedIn => 7, + ZoomLevel::Default => 5, + ZoomLevel::ZoomedOut => 3, + }; - let rows: u16 = area_height / CELL_HIGHT; - let cols: u16 = area_width / CELL_WIDTH; + let cell_width: u16 = match app.states.skirmish.zoom_level { + ZoomLevel::ZoomedIn => 13, + ZoomLevel::Default => 9, + ZoomLevel::ZoomedOut => 5, + }; - let v_max_offset: usize = if app.states.skirmish.map_height as u16 > rows { - app.states.skirmish.map_height as u16 - rows - } else { - 0 - } as usize; + let rows: u16 = area_height / cell_height; + let cols: u16 = area_width / cell_width; - let h_max_offset: usize = if app.states.skirmish.map_width as u16 > cols { - app.states.skirmish.map_width as u16 - cols - } else { - 0 - } as usize; + let v_max_offset: usize = Self::max_offset(app.states.skirmish.map_height as u16, rows); + let h_max_offset: usize = Self::max_offset(app.states.skirmish.map_width as u16, cols); app.states.skirmish.horizontal_offset.set_max(h_max_offset); app.states.skirmish.vertical_offset.set_max(v_max_offset); @@ -49,8 +57,8 @@ impl<'a> BoardWidget<'a> { Self { map_width: app.states.skirmish.map_width as usize, - cell_width: CELL_WIDTH, - cell_height: CELL_HIGHT, + cell_width, + cell_height, cols, rows, h_offset: app.states.skirmish.horizontal_offset.get_value(), diff --git a/src/cli.rs b/src/cli.rs index 5b46828..2a6050d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,5 +1,5 @@ use crate::app::{ - states::{GameMode, PerkDecks}, + states::{GameMode, PerkDecks, ZoomLevel}, view::View, }; use clap::{Error, Parser, error::ErrorKind, value_parser}; @@ -52,6 +52,15 @@ pub struct Cli { )] pub map_height: u8, + #[arg( + long, + help = "Zoom level", + value_name = "...", + default_value_t = ZoomLevel::Default, + value_enum + )] + pub zoom_level: ZoomLevel, + #[arg( long, help = "Perk Deck",