Refactor keybindings with groups and symbols

Introduce a `Group` enum to categorize actions and add `group` and
`symbol`
fields to `KeyBinding`. Extend `Action` with `Up`, `Down`, `Space`, and
`Esc`, updating all keybinding definitions accordingly.

Update `binding_for_view` to return the new actions per view and handle
`Esc` by returning to the main menu.

Rewrite the keybindings widget as a custom `Widget` that renders grouped
paragraphs with proper layout.

Adjust the main menu layout percentages to accommodate the new widget.
This commit is contained in:
2026-03-12 16:00:45 +01:00
parent f481f5dc87
commit 71b9d44a77
6 changed files with 126 additions and 43 deletions
+1
View File
@@ -103,6 +103,7 @@ impl App {
if let Some(action) = event_to_action(&key_event) { if let Some(action) = event_to_action(&key_event) {
match action { match action {
Action::Quit => self.exit = true, Action::Quit => self.exit = true,
Action::Esc => self.view = View::MainMenu,
_ => (), _ => (),
} }
} }
+51 -9
View File
@@ -4,17 +4,39 @@ use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Action { pub enum Action {
Quit, Quit,
ScrollUp, Up,
ScrollDown, Down,
Space,
Esc,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Group {
Quit,
Movement,
Select, Select,
} }
impl Group {
pub fn iter() -> impl Iterator<Item = Group> {
[Group::Quit, Group::Movement, Group::Select]
.iter()
.copied()
}
pub fn len() -> usize {
Self::iter().count()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct KeyBinding { pub struct KeyBinding {
pub action: Action, pub action: Action,
pub code: KeyCode, pub code: KeyCode,
pub kind: KeyEventKind, pub kind: KeyEventKind,
pub modifiers: KeyModifiers, pub modifiers: KeyModifiers,
pub group: Group,
pub symbol: &'static str,
pub description: &'static str, pub description: &'static str,
} }
@@ -24,29 +46,46 @@ pub static KEYBINDINGS: &[KeyBinding] = &[
code: KeyCode::Char('q'), code: KeyCode::Char('q'),
kind: KeyEventKind::Press, kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
group: Group::Quit,
symbol: "q",
description: "Quit", description: "Quit",
}, },
KeyBinding { KeyBinding {
action: Action::ScrollUp, action: Action::Up,
code: KeyCode::Up, code: KeyCode::Up,
kind: KeyEventKind::Press, kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
group: Group::Movement,
symbol: "",
description: "Up", description: "Up",
}, },
KeyBinding { KeyBinding {
action: Action::ScrollDown, action: Action::Down,
code: KeyCode::Down, code: KeyCode::Down,
kind: KeyEventKind::Press, kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
group: Group::Movement,
symbol: "",
description: "Down", description: "Down",
}, },
KeyBinding { KeyBinding {
action: Action::Select, action: Action::Space,
code: KeyCode::Char(' '), code: KeyCode::Char(' '),
kind: KeyEventKind::Press, kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
group: Group::Select,
symbol: "Space",
description: "Select", description: "Select",
}, },
KeyBinding {
action: Action::Esc,
code: KeyCode::Esc,
kind: KeyEventKind::Press,
modifiers: KeyModifiers::NONE,
group: Group::Movement,
symbol: "Esc",
description: "Go back",
},
]; ];
pub fn binding_for(action: Action) -> Option<&'static KeyBinding> { pub fn binding_for(action: Action) -> Option<&'static KeyBinding> {
@@ -63,11 +102,14 @@ pub fn event_to_action(event: &KeyEvent) -> Option<Action> {
pub fn binding_for_view(view: View) -> Vec<&'static KeyBinding> { pub fn binding_for_view(view: View) -> Vec<&'static KeyBinding> {
match view { match view {
View::MainMenu => vec![ View::MainMenu => vec![
binding_for(Action::ScrollUp).unwrap(), binding_for(Action::Up).unwrap(),
binding_for(Action::ScrollDown).unwrap(), binding_for(Action::Down).unwrap(),
binding_for(Action::Select).unwrap(), binding_for(Action::Space).unwrap(),
binding_for(Action::Quit).unwrap(), binding_for(Action::Quit).unwrap(),
], ],
_ => vec![], _ => vec![
binding_for(Action::Quit).unwrap(),
binding_for(Action::Esc).unwrap(),
],
} }
} }
+5 -4
View File
@@ -8,7 +8,7 @@ pub fn main_menu_keybindings(app: &mut App, event: &KeyEvent) {
if let Some(action) = event_to_action(&event) { if let Some(action) = event_to_action(&event) {
match action { match action {
Action::Quit => app.exit = true, Action::Quit => app.exit = true,
Action::ScrollUp => { Action::Up => {
app.game_states.main_menu_state.selected_view = app app.game_states.main_menu_state.selected_view = app
.game_states .game_states
.main_menu_state .main_menu_state
@@ -16,7 +16,7 @@ pub fn main_menu_keybindings(app: &mut App, event: &KeyEvent) {
.saturating_sub(1) .saturating_sub(1)
.max(1); .max(1);
} }
Action::ScrollDown => { Action::Down => {
app.game_states.main_menu_state.selected_view = app app.game_states.main_menu_state.selected_view = app
.game_states .game_states
.main_menu_state .main_menu_state
@@ -24,7 +24,7 @@ pub fn main_menu_keybindings(app: &mut App, event: &KeyEvent) {
.saturating_add(1) .saturating_add(1)
.min(4); .min(4);
} }
Action::Select => { Action::Space => {
let selected_view: usize = app.game_states.main_menu_state.selected_view; let selected_view: usize = app.game_states.main_menu_state.selected_view;
if selected_view == 1 { if selected_view == 1 {
@@ -36,7 +36,8 @@ pub fn main_menu_keybindings(app: &mut App, event: &KeyEvent) {
} else if selected_view == 4 { } else if selected_view == 4 {
app.view = View::Settings; app.view = View::Settings;
} }
} // _ => (), }
_ => (),
} }
} }
} }
+1 -1
View File
@@ -2,6 +2,6 @@ pub mod keybindings;
pub mod main_menu; pub mod main_menu;
pub use keybindings::{ pub use keybindings::{
Action, KEYBINDINGS, KeyBinding, binding_for, binding_for_view, event_to_action, Action, Group, KEYBINDINGS, KeyBinding, binding_for, binding_for_view, event_to_action,
}; };
pub use main_menu::main_menu_keybindings; pub use main_menu::main_menu_keybindings;
+63 -24
View File
@@ -1,33 +1,72 @@
use crate::app::{View, keybindings::binding_for_view}; use crate::app::{
View,
keybindings::{Group, KeyBinding, binding_for_view},
};
use ratatui::{ use ratatui::{
crossterm::event::KeyCode, buffer::Buffer,
layout::Alignment, layout::{Alignment, Constraint, Layout, Rect},
style::Stylize, style::Stylize,
text::{Line, Span}, text::Line,
widgets::{Block, Borders, Paragraph}, widgets::{Block, Borders, Padding, Paragraph, Widget},
}; };
pub fn keybindings_widget(view: View) -> Paragraph<'static> { pub struct KeybindingsWidget {
let lines: Vec<Line> = binding_for_view(view) grouped: Vec<Paragraph<'static>>,
.iter()
.map(|b| {
let key_span = match b.code {
KeyCode::Up => Span::raw("".to_string()),
KeyCode::Down => Span::raw("".to_string()),
KeyCode::Char(' ') => Span::raw("Space".to_string()),
KeyCode::Char(c) => Span::raw(c.to_string()),
other => Span::raw(format!("{:?}", other)),
} }
.bold()
.red();
Line::from_iter([key_span, "\t - ".into(), b.description.into()]) impl Widget for KeybindingsWidget {
}) fn render(self, area: Rect, buf: &mut Buffer) {
let count: u16 = self.grouped.len() as u16;
if count == 0 {
return;
}
let block: Block<'_> = Block::default()
.borders(Borders::ALL)
.title("[ Keybindings ]");
let inner: Rect = block.inner(area);
block.render(area, buf);
let base: u16 = if count == 0 { 0 } else { 10 };
let constraints: Vec<Constraint> = vec![Constraint::Percentage(base); count as usize];
let chunks: Vec<Rect> = Layout::horizontal(constraints).split(inner).to_vec();
for (paragraph, chunk) in self.grouped.into_iter().zip(chunks.into_iter()) {
paragraph.render(chunk, buf);
}
}
}
pub fn keybindings_widget(view: View) -> KeybindingsWidget {
let keybindings: Vec<&'static KeyBinding> = binding_for_view(view);
let mut grouped_keybindings: Vec<Paragraph<'static>> = Vec::new();
for (i, group) in Group::iter().enumerate() {
let grouped_lines: Vec<Line<'_>> = keybindings
.iter()
.filter(|b| b.group == group)
.map(|b| Line::from_iter([b.symbol.red().bold(), " - ".into(), b.description.into()]))
.collect(); .collect();
Paragraph::new(lines).alignment(Alignment::Left).block( let mut block: Block<'_> = Block::default().padding(Padding::new(1, 1, 0, 0));
Block::default()
.borders(Borders::ALL) if i != Group::len() - 1 {
.title("[ Keybindings ]"), block = block.borders(Borders::RIGHT);
) }
grouped_keybindings.push(
Paragraph::new(grouped_lines)
.alignment(Alignment::Center)
.block(block),
);
}
KeybindingsWidget {
grouped: grouped_keybindings,
}
} }
+1 -1
View File
@@ -28,7 +28,7 @@ fn view_options() -> Vec<(usize, String)> {
pub fn main_menu_widget(app: &App, area: Rect, buf: &mut Buffer) { pub fn main_menu_widget(app: &App, area: Rect, buf: &mut Buffer) {
let vertical_layout: Layout = let vertical_layout: Layout =
Layout::vertical([Constraint::Percentage(88), Constraint::Percentage(12)]); Layout::vertical([Constraint::Percentage(92), Constraint::Percentage(8)]);
let [main_menu_area, keybindings_area] = vertical_layout.areas(area); let [main_menu_area, keybindings_area] = vertical_layout.areas(area);