use ratatui::{
style::{Color, Modifier, Style},
text::{Line, Span, Text},
widgets::{BlockExt, Clear, Widget},
};
use tracing::trace;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HelpElementKind {
Keybind(&'static str, &'static str),
Text(&'static str),
}
#[macro_export]
macro_rules! help_keybind {
($key:expr, $description:expr) => {
$crate::ui::components::help::HelpElementKind::Keybind($key, $description)
};
}
#[macro_export]
macro_rules! help_text {
($text:expr) => {
$crate::ui::components::help::HelpElementKind::Text($text)
};
}
pub fn help_elements_to_text(elements: &[HelpElementKind], width: u16) -> Text<'static> {
let mut lines = Vec::with_capacity(elements.len());
for element in elements {
match element {
HelpElementKind::Keybind(key, description) => {
let total_length = (key.len() + description.len()) as u16; let padding = if total_length < width {
width - total_length
} else {
1 };
lines.push(Line::from(vec![
Span::styled(
*key,
Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD),
),
Span::raw(" ".repeat(padding as usize)),
Span::raw(*description),
]));
}
HelpElementKind::Text(text) => {
let wrapped = textwrap::wrap(text, width as usize);
lines.extend(wrapped.into_iter().map(|line| Line::from(line).centered()));
}
}
}
Text::from(lines)
}
pub struct HelpComponent<'a> {
constraint: u16,
content: &'a [HelpElementKind],
block: Option<ratatui::widgets::Block<'a>>,
width: u16,
}
impl<'a> HelpComponent<'a> {
pub fn new(content: &'a [HelpElementKind]) -> Self {
Self {
content,
width: 0,
constraint: 0,
block: None,
}
}
pub fn set_constraint(self, constraint: u16) -> Self {
Self { constraint, ..self }
}
pub fn block(self, block: ratatui::widgets::Block<'a>) -> Self {
Self {
block: Some(block),
..self
}
}
}
impl<'a> Widget for HelpComponent<'a> {
fn render(mut self, area: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) {
use ratatui::layout::Constraint::{Length, Percentage};
trace!(content = ?self.content, "Rendering HelpComponent");
trace!(content_length = ?self.content.len(), "Content length");
let mut centered_area = if self.constraint != 0 {
area.centered(Percentage(self.constraint), Length(self.constraint))
} else {
area
};
let mut inner = self.block.inner_if_some(centered_area);
self.width = inner.width;
let text = help_elements_to_text(self.content, self.width);
let text_height = text.height() as u16;
let y_offset = |h: u16| {
if text_height < h {
(h - text_height) / 2
} else {
0
}
};
inner.y += y_offset(inner.height) + 1;
inner.height = text.height() as u16;
let inner_height = inner.height;
centered_area.y += y_offset(centered_area.height);
centered_area.height = inner_height + 2;
Clear.render(centered_area, buf);
self.block.render(centered_area, buf);
text.render(inner, buf);
}
}