generated from GarandPLG/rust-flake-template
- Document new Rust formatting standards in AGENTS, CODE_MAP, and PROGRESS. - Add rustfmt.toml with style 2024, 2‑space indents, 100‑char width, LF endings. - Apply consistent 2‑space indentation and ordering across source files. - Minor clean‑up of imports and whitespace to match the new style.
136 lines
4.3 KiB
Rust
136 lines
4.3 KiB
Rust
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<Paragraph<'static>>,
|
||
}
|
||
|
||
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<Option<&'static KeyBinding>> =
|
||
actions.iter().map(|a| binding_for(*a)).collect();
|
||
|
||
let used_groups: HashSet<Group> = keybindings
|
||
.iter()
|
||
.filter_map(|b| b.map(|binding| binding.group))
|
||
.collect();
|
||
|
||
let mut grouped_keybindings: Vec<Paragraph<'static>> = Vec::new();
|
||
|
||
for group in Group::value_variants().iter() {
|
||
if !used_groups.contains(group) {
|
||
continue;
|
||
}
|
||
|
||
let grouped_lines: Vec<Line<'_>> = 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<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);
|
||
}
|
||
}
|
||
}
|