use itertools::Itertools;
use ratatui::{
buffer::Buffer,
layout::{Constraint, HorizontalAlignment, Rect},
macros::horizontal,
style::Style,
text::{Line, Span, Text},
widgets::{Block, Cell, Paragraph, Row, Table, Widget},
};
use ratatui_input_manager::{Backend, KeyBind, KeyMap, KeyPress};
use crate::app::App;
const HELP_TIP: &str = " | Press '?' to view all keybinds";
impl App {
pub fn render_help_area(&self, area: Rect, buffer: &mut Buffer) {
#[allow(clippy::cast_possible_truncation)]
let [help_bar_area, tip_area] = horizontal![>= 1, == HELP_TIP.len() as u16].areas(area);
let keybinds = self
.get_area_subkeybinds()
.iter()
.chain(Self::KEYBINDS.iter())
.collect::<Vec<_>>();
let help = HelpBar::new(keybinds);
help.render(help_bar_area, buffer);
Paragraph::new(HELP_TIP)
.style(Style::default().fg(ratatui::style::Color::DarkGray))
.render(tip_area, buffer);
}
}
pub struct Help<'k, B: Backend + 'static> {
keybinds: Vec<&'k KeyBind<B>>,
block: Option<Block<'k>>,
style: Style,
key_style: Style,
description_style: Style,
}
impl<'k, B: Backend> Help<'k, B> {
pub fn new(keybinds: impl IntoIterator<Item = &'k KeyBind<B>>) -> Self {
Self {
keybinds: keybinds.into_iter().collect(),
block: None,
style: Style::default(),
key_style: Style::default().bold().white(),
description_style: Style::default(),
}
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn block(mut self, block: Block<'k>) -> Self {
self.block = Some(block);
self
}
}
impl<B: Backend + 'static> Widget for Help<'_, B>
where
KeyPress<B>: std::fmt::Display,
{
fn render(self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
#[allow(clippy::cast_possible_truncation)]
let max_key_width = self
.keybinds
.iter()
.map(|kb| {
let key_lens: Vec<usize> = kb.pressed.iter().map(|p| p.to_string().len()).collect();
key_lens.iter().sum::<usize>() + key_lens.len().saturating_sub(1) * 2
})
.max()
.unwrap_or_default()
.max(3) as u16;
let table = Table::new(
self.keybinds.iter().map(
|KeyBind {
pressed,
description,
..
}| {
let key_str = pressed
.iter()
.map(ToString::to_string)
.format(", ")
.to_string();
let mut text = Text::from(key_str);
text.alignment = Some(HorizontalAlignment::Right);
Row::new([
Cell::new(text).style(self.key_style),
Cell::new(*description).style(self.description_style),
])
},
),
[Constraint::Length(max_key_width), Constraint::Fill(1)],
)
.style(self.style);
let area = self.block.map_or(area, |block| {
block.clone().render(area, buf);
block.inner(area)
});
table.render(area, buf);
}
}
pub struct HelpBar<'k, B: Backend + 'static> {
keybinds: Vec<&'k KeyBind<B>>,
style: Style,
key_style: Style,
description_style: Style,
separator_style: Style,
}
impl<'k, B: Backend> HelpBar<'k, B> {
pub fn new(keybinds: impl IntoIterator<Item = &'k KeyBind<B>>) -> Self {
Self {
keybinds: keybinds.into_iter().collect(),
style: Style::default(),
key_style: Style::default().bold().white(),
description_style: Style::default(),
separator_style: Style::default().fg(ratatui::style::Color::DarkGray),
}
}
}
impl<B: Backend + 'static> Widget for HelpBar<'_, B>
where
KeyPress<B>: std::fmt::Display,
{
fn render(self, area: Rect, buf: &mut Buffer) {
self.keybinds
.iter()
.enumerate()
.flat_map(
|(
idx,
KeyBind {
pressed,
description,
..
},
)| {
[
Span::styled(if idx == 0 { "" } else { " | " }, self.separator_style),
Span::styled(format!("{description}: "), self.description_style),
Span::styled(
pressed
.iter()
.map(ToString::to_string)
.format(", ")
.to_string(),
self.key_style,
),
]
},
)
.collect::<Line>()
.style(self.style)
.render(area, buf);
}
}