use crate::app::{ helpers::block_single_title_helper, keybindings::{Action, Group, KeyBinding, binding_for}, }; use clap::ValueEnum; use ratatui::{ buffer::Buffer, layout::{Alignment, Constraint, Layout, Rect}, style::{Color, Stylize}, text::Line, widgets::{Block, Borders, Padding, Paragraph, Widget}, }; use std::collections::HashSet; /// A widget that visualises the keybindings for a set of actions. /// /// The widget stores a vector of [`Paragraph`] objects, each representing one /// logical group of keybindings (e.g. navigation, editing, etc.). The paragraphs /// are rendered side‑by‑side inside a bordered container. pub struct KeybindingsWidget { /// Paragraphs, one per used `Group`, containing the formatted keybinding lines. grouped: Vec>, } impl KeybindingsWidget { /// Create a new widget from a list of actions. /// /// For each action, the corresponding static [`KeyBinding`] is looked up /// using [`binding_for`]. Only groups that have at least one binding are /// displayed. The resulting widget contains a paragraph for each group, /// formatted with the key symbol (red & bold) followed by its description. /// /// # Parameters /// /// * `actions` – A slice of actions for which keybindings should be shown. /// * `area_height` – The height of the area in which the widget will be rendered. /// This value is used to calculate vertical padding so that the keybinding rows /// are vertically centered when the widget's allocated height is larger than the /// number of lines needed to display the bindings. /// /// # Returns /// /// A fully‑initialised `KeybindingsWidget` ready for rendering. pub fn new(actions: &[Action], area_height: u16) -> Self { let keybindings: Vec> = actions.iter().map(|a| binding_for(*a)).collect(); let used_groups: HashSet = keybindings .iter() .filter_map(|b| b.map(|binding| binding.group)) .collect(); let mut grouped_keybindings: Vec> = Vec::new(); for group in Group::value_variants().iter() { if !used_groups.contains(group) { continue; } let grouped_lines: Vec> = keybindings .iter() .filter(|b| b.as_ref().map_or(false, |kb| kb.group == *group)) .filter_map(|b| { b.as_ref().map(|kb| { Line::from_iter([kb.symbol.red().bold(), " - ".into(), kb.description.into()]) }) }) .collect(); let height_diff: u16 = area_height.saturating_sub(grouped_lines.len().saturating_add(2) as u16); let padding_top: u16 = if height_diff <= 1 { height_diff } else { height_diff / 2 }; let mut block: Block<'_> = Block::default().padding(Padding::new(1, 1, padding_top, 0)); if !grouped_keybindings.is_empty() { block = block.borders(Borders::LEFT); } grouped_keybindings.push( Paragraph::new(grouped_lines) .alignment(Alignment::Center) .block(block), ); } Self { grouped: grouped_keybindings, } } } impl Widget for KeybindingsWidget { /// Render the widget inside the given rectangular area. /// /// The widget first draws a surrounding border with the title *Keybindings*. /// It then divides the inner area into equal horizontal slices—one per /// used group—and renders each group's paragraph into its slice. /// /// If no groups are present, the function returns early without drawing /// anything. 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(block_single_title_helper( "Keybindings".to_string(), Color::Magenta, )); let inner: Rect = block.inner(area); block.render(area, buf); let base: u16 = if count == 0 { 0 } else { 100u16 / count }; let constraints: Vec = vec![Constraint::Percentage(base); count as usize]; let chunks: Vec = Layout::horizontal(constraints).split(inner).to_vec(); for (paragraph, chunk) in self.grouped.into_iter().zip(chunks.into_iter()) { paragraph.render(chunk, buf); } } }