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::{
DefaultTerminal, Frame,
crossterm::event::{self, KeyEvent},
layout::Rect,
};
use std::{
io::Result,
@@ -20,6 +21,7 @@ pub enum Event {
pub struct App {
pub exit: bool,
pub view: View,
pub window_area: Rect,
pub states: GameStates,
}
@@ -31,6 +33,7 @@ impl App {
Self {
exit: false,
view: args.view,
window_area: Rect::default(),
states,
}
}
@@ -56,7 +59,15 @@ impl App {
}
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<()> {
+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 main_menu_option;
pub mod zoom_level;
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};
+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
/// returns the highest count. This can be used, for example, to size UI
/// 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();
for action in actions {
+10 -7
View File
@@ -9,15 +9,18 @@ use ratatui::{
widgets::{Block, Borders, Paragraph, Widget},
};
pub fn default_view(area: Rect, buf: &mut Buffer) {
let actions: Vec<Action> = vec![Action::Quit, Action::Quit2, Action::Esc];
const ACTIONS: &[Action] = &[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::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")
@@ -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::{
App, View,
helpers::main_menu_option_helper,
keybindings::{Action, count_largest_group},
widgets::KeybindingsWidget,
};
@@ -12,35 +13,24 @@ use ratatui::{
widgets::{Block, Borders, Paragraph, Widget},
};
fn format_view_string(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
}
const ACTIONS: &[Action] = &[
Action::Up,
Action::Down,
Action::Space,
Action::Quit,
Action::Quit2,
];
fn main_menu_layout(area: Rect) -> [Rect; 2] {
Layout::vertical([
Constraint::Fill(1),
Constraint::Length(count_largest_group(&ACTIONS) + 2),
])
.areas(area)
}
pub fn main_menu_view(app: &App, area: Rect, buf: &mut Buffer) {
let actions: Vec<Action> = 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);
let [main_menu_area, keybindings_area] = main_menu_layout(area);
Block::new()
.borders(Borders::LEFT | Borders::TOP | Borders::RIGHT)
@@ -78,7 +68,7 @@ pub fn main_menu_view(app: &App, area: Rect, buf: &mut Buffer) {
.enumerate()
.filter(|(_, v)| v != &&View::MainMenu)
.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 {
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},
};
pub fn skirmish_view(app: &mut App, area: Rect, buf: &mut Buffer) {
let actions: Vec<Action> = vec![
Action::Up,
Action::Down,
Action::Left,
Action::Right,
Action::ScrollUp,
Action::ScrollDown,
Action::ScrollLeft,
Action::ScrollRight,
Action::ZoomIn,
Action::ZoomOut,
Action::Quit,
Action::Quit2,
Action::Esc,
];
const ACTIONS: &[Action] = &[
Action::Up,
Action::Down,
Action::Left,
Action::Right,
Action::ScrollUp,
Action::ScrollDown,
Action::ScrollLeft,
Action::ScrollRight,
Action::ZoomIn,
Action::ZoomOut,
Action::Quit,
Action::Quit2,
Action::Esc,
];
let vertical_layout: Layout = Layout::vertical([
fn skirmish_layout(area: Rect) -> [Rect; 3] {
Layout::vertical([
Constraint::Length(4),
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([
@@ -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
///
/// * `actions` A vector of actions for which keybindings should be shown.
/// * `actions` A slice of actions for which keybindings should be shown.
///
/// # Returns
///
/// 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>> =
actions.iter().map(|a| binding_for(*a)).collect();