Track window area and refactor keybinding APIs

- Track terminal size in `App::window_area` and update on resize
- Accept action slices in `count_largest_group` and
  `KeybindingsWidget::new`
- Add ACTIONS slices and layout helpers for default, main_menu, skirmish
- Add `main_menu_option_helper` for formatting menu strings and export
  it
This commit is contained in:
2026-04-01 20:27:50 +02:00
parent bf8883934d
commit f405c3cde1
8 changed files with 82 additions and 60 deletions
+12 -1
View File
@@ -5,6 +5,7 @@ use crate::{
use ratatui::{ use ratatui::{
DefaultTerminal, Frame, DefaultTerminal, Frame,
crossterm::event::{self, KeyEvent}, crossterm::event::{self, KeyEvent},
layout::Rect,
}; };
use std::{ use std::{
io::Result, io::Result,
@@ -20,6 +21,7 @@ pub enum Event {
pub struct App { pub struct App {
pub exit: bool, pub exit: bool,
pub view: View, pub view: View,
pub window_area: Rect,
pub states: GameStates, pub states: GameStates,
} }
@@ -31,6 +33,7 @@ impl App {
Self { Self {
exit: false, exit: false,
view: args.view, view: args.view,
window_area: Rect::default(),
states, states,
} }
} }
@@ -56,7 +59,15 @@ impl App {
} }
fn draw(&mut self, frame: &mut Frame<'_>) { fn draw(&mut self, frame: &mut Frame<'_>) {
frame.render_widget(self, frame.area()); let window_area: Rect = frame.area();
if window_area.width != self.window_area.width
|| window_area.height != self.window_area.height
{
self.window_area = window_area;
}
frame.render_widget(self, window_area);
} }
fn handle_key_event(&mut self, key_event: KeyEvent) -> Result<()> { fn handle_key_event(&mut self, key_event: KeyEvent) -> Result<()> {
+13
View File
@@ -0,0 +1,13 @@
pub fn main_menu_option_helper(s: String) -> String {
if let Some(pos) = s
.char_indices()
.filter(|(_, c)| c.is_ascii_uppercase())
.nth(1)
.map(|(i, _)| i)
{
let (first, second) = s.split_at(pos);
format!("{first} {second}")
} else {
s
}
}
+2
View File
@@ -1,5 +1,7 @@
pub mod block_title; pub mod block_title;
pub mod main_menu_option;
pub mod zoom_level; pub mod zoom_level;
pub use block_title::{block_single_title_helper, block_title_helper}; pub use block_title::{block_single_title_helper, block_title_helper};
pub use main_menu_option::main_menu_option_helper;
pub use zoom_level::{CellSizes, cell_size_helper}; pub use zoom_level::{CellSizes, cell_size_helper};
+1 -1
View File
@@ -284,7 +284,7 @@ pub fn event_to_action(event: &KeyEvent) -> Option<Action> {
/// It counts how many times each group appears in the supplied actions and /// It counts how many times each group appears in the supplied actions and
/// returns the highest count. This can be used, for example, to size UI /// returns the highest count. This can be used, for example, to size UI
/// elements that display groups of bindings. /// elements that display groups of bindings.
pub fn count_largest_group(actions: &Vec<Action>) -> u16 { pub fn count_largest_group(actions: &[Action]) -> u16 {
let mut group_counts: HashMap<Group, u16> = HashMap::new(); let mut group_counts: HashMap<Group, u16> = HashMap::new();
for action in actions { for action in actions {
+10 -7
View File
@@ -9,15 +9,18 @@ use ratatui::{
widgets::{Block, Borders, Paragraph, Widget}, widgets::{Block, Borders, Paragraph, Widget},
}; };
pub fn default_view(area: Rect, buf: &mut Buffer) { const ACTIONS: &[Action] = &[Action::Quit, Action::Quit2, Action::Esc];
let actions: Vec<Action> = vec![Action::Quit, Action::Quit2, Action::Esc];
let vertical_layout: Layout = Layout::vertical([ fn default_view_layout(area: Rect) -> [Rect; 2] {
Layout::vertical([
Constraint::Fill(1), Constraint::Fill(1),
Constraint::Length(count_largest_group(&actions) + 2), Constraint::Length(count_largest_group(&ACTIONS) + 2),
]); ])
.areas(area)
}
let [main_area, keybindings_area] = vertical_layout.areas(area); pub fn default_view(area: Rect, buf: &mut Buffer) {
let [main_area, keybindings_area] = default_view_layout(area);
{ {
Paragraph::new("Work in progress") Paragraph::new("Work in progress")
@@ -32,6 +35,6 @@ pub fn default_view(area: Rect, buf: &mut Buffer) {
} }
{ {
KeybindingsWidget::new(actions).render(keybindings_area, buf); KeybindingsWidget::new(ACTIONS).render(keybindings_area, buf);
} }
} }
+18 -28
View File
@@ -1,5 +1,6 @@
use crate::app::{ use crate::app::{
App, View, App, View,
helpers::main_menu_option_helper,
keybindings::{Action, count_largest_group}, keybindings::{Action, count_largest_group},
widgets::KeybindingsWidget, widgets::KeybindingsWidget,
}; };
@@ -12,35 +13,24 @@ use ratatui::{
widgets::{Block, Borders, Paragraph, Widget}, widgets::{Block, Borders, Paragraph, Widget},
}; };
fn format_view_string(s: String) -> String { const ACTIONS: &[Action] = &[
if let Some(pos) = s Action::Up,
.char_indices() Action::Down,
.filter(|(_, c)| c.is_ascii_uppercase()) Action::Space,
.nth(1) Action::Quit,
.map(|(i, _)| i) Action::Quit2,
{ ];
let (first, second) = s.split_at(pos);
format!("{first} {second}") fn main_menu_layout(area: Rect) -> [Rect; 2] {
} else { Layout::vertical([
s Constraint::Fill(1),
} Constraint::Length(count_largest_group(&ACTIONS) + 2),
])
.areas(area)
} }
pub fn main_menu_view(app: &App, area: Rect, buf: &mut Buffer) { pub fn main_menu_view(app: &App, area: Rect, buf: &mut Buffer) {
let actions: Vec<Action> = vec![ let [main_menu_area, keybindings_area] = main_menu_layout(area);
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);
Block::new() Block::new()
.borders(Borders::LEFT | Borders::TOP | Borders::RIGHT) .borders(Borders::LEFT | Borders::TOP | Borders::RIGHT)
@@ -78,7 +68,7 @@ pub fn main_menu_view(app: &App, area: Rect, buf: &mut Buffer) {
.enumerate() .enumerate()
.filter(|(_, v)| v != &&View::MainMenu) .filter(|(_, v)| v != &&View::MainMenu)
.map(|(i, view)| { .map(|(i, view)| {
let view_string: String = format_view_string(format!("{:?}", view)); let view_string: String = main_menu_option_helper(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_iter(["> ".cyan(), view_string.yellow()]).yellow() Line::from_iter(["> ".cyan(), view_string.yellow()]).yellow()
@@ -98,6 +88,6 @@ pub fn main_menu_view(app: &App, area: Rect, buf: &mut Buffer) {
} }
{ {
KeybindingsWidget::new(actions).render(keybindings_area, buf); KeybindingsWidget::new(ACTIONS).render(keybindings_area, buf);
} }
} }
+24 -21
View File
@@ -12,30 +12,33 @@ use ratatui::{
widgets::{Block, Borders, Padding, Paragraph, Widget}, widgets::{Block, Borders, Padding, Paragraph, Widget},
}; };
pub fn skirmish_view(app: &mut App, area: Rect, buf: &mut Buffer) { const ACTIONS: &[Action] = &[
let actions: Vec<Action> = vec![ Action::Up,
Action::Up, Action::Down,
Action::Down, Action::Left,
Action::Left, Action::Right,
Action::Right, Action::ScrollUp,
Action::ScrollUp, Action::ScrollDown,
Action::ScrollDown, Action::ScrollLeft,
Action::ScrollLeft, Action::ScrollRight,
Action::ScrollRight, Action::ZoomIn,
Action::ZoomIn, Action::ZoomOut,
Action::ZoomOut, Action::Quit,
Action::Quit, Action::Quit2,
Action::Quit2, Action::Esc,
Action::Esc, ];
];
let vertical_layout: Layout = Layout::vertical([ fn skirmish_layout(area: Rect) -> [Rect; 3] {
Layout::vertical([
Constraint::Length(4), Constraint::Length(4),
Constraint::Fill(1), Constraint::Fill(1),
Constraint::Length(count_largest_group(&actions) + 2), Constraint::Length(count_largest_group(&ACTIONS) + 2),
]); ])
.areas(area)
}
let [title_area, main_area, keybindings_area] = vertical_layout.areas(area); pub fn skirmish_view(app: &mut App, area: Rect, buf: &mut Buffer) {
let [title_area, main_area, keybindings_area] = skirmish_layout(area);
{ {
let lines: Vec<Line<'_>> = Vec::from_iter([ let lines: Vec<Line<'_>> = Vec::from_iter([
@@ -92,6 +95,6 @@ pub fn skirmish_view(app: &mut App, area: Rect, buf: &mut Buffer) {
} }
{ {
KeybindingsWidget::new(actions).render(keybindings_area, buf); KeybindingsWidget::new(ACTIONS).render(keybindings_area, buf);
} }
} }
+2 -2
View File
@@ -32,12 +32,12 @@ impl KeybindingsWidget {
/// ///
/// # Parameters /// # Parameters
/// ///
/// * `actions` A vector of actions for which keybindings should be shown. /// * `actions` A slice of actions for which keybindings should be shown.
/// ///
/// # Returns /// # Returns
/// ///
/// A fullyinitialised `KeybindingsWidget` ready for rendering. /// A fullyinitialised `KeybindingsWidget` ready for rendering.
pub fn new(actions: Vec<Action>) -> Self { pub fn new(actions: &[Action]) -> Self {
let keybindings: Vec<Option<&'static KeyBinding>> = let keybindings: Vec<Option<&'static KeyBinding>> =
actions.iter().map(|a| binding_for(*a)).collect(); actions.iter().map(|a| binding_for(*a)).collect();