use ratatui::{
Frame,
layout::Rect,
style::{Modifier, Style},
text::{Line, Span},
widgets::Paragraph,
};
use unicode_width::UnicodeWidthStr;
use super::{HintGroup, HintStyle, state::visible, style};
use crate::theme::Color;
const SEPARATOR: &str = " \u{00b7} ";
const LABEL_GAP: usize = 2;
pub fn footer_height(rows: u16) -> u16 {
rows
}
pub fn lines<S: AsRef<str>>(
items: &[(S, S)],
key_color: Color,
width: usize,
) -> Vec<Line<'static>> {
let key_style = style::fg(key_color).add_modifier(Modifier::BOLD);
wrap(items, key_style, style::dim(), width)
.into_iter()
.map(Line::from)
.collect()
}
pub fn group_lines<S: AsRef<str>>(
groups: &[HintGroup<'_, S>],
opts: &HintStyle,
width: usize,
) -> Vec<Line<'static>> {
if !visible() {
return Vec::new();
}
let label_col = label_column_width(groups);
let hint_width = width.saturating_sub(label_col).max(1);
let mut lines: Vec<Line<'static>> = Vec::new();
for group in groups {
let rows = wrap(group.hints, opts.key, opts.description, hint_width);
for (row_index, mut hint_spans) in rows.into_iter().enumerate() {
let mut spans: Vec<Span<'static>> = Vec::new();
if label_col > 0 {
let is_label_row = row_index == 0 && !group.label.is_empty();
let cell = if is_label_row {
pad(&format!("{}:", group.label), label_col)
} else {
" ".repeat(label_col)
};
let cell_style = if is_label_row {
opts.label
} else {
Style::default()
};
spans.push(Span::styled(cell, cell_style));
}
spans.append(&mut hint_spans);
lines.push(Line::from(spans));
}
}
lines
}
pub fn height<S: AsRef<str>>(
groups: &[HintGroup<'_, S>],
width: usize,
top_margin: u16,
) -> u16 {
if !visible() {
return 0;
}
let count = group_lines(groups, &HintStyle::default(), width).len() as u16;
(count + top_margin).max(1)
}
pub fn render<S: AsRef<str>>(
frame: &mut Frame,
area: Rect,
groups: &[HintGroup<'_, S>],
opts: &HintStyle,
) {
if !visible() {
return;
}
let width = area.width as usize;
let lines = group_lines(groups, opts, width);
let margin = opts.top_margin.min(area.height);
let hint_area = Rect {
x: area.x,
y: area.y + margin,
width: area.width,
height: area.height.saturating_sub(margin),
};
let mut paragraph = Paragraph::new(lines);
if let Some(bg) = opts.background {
paragraph = paragraph.style(style::bg(bg));
}
frame.render_widget(paragraph, hint_area);
}
fn wrap<S: AsRef<str>>(
items: &[(S, S)],
key_style: Style,
desc_style: Style,
width: usize,
) -> Vec<Vec<Span<'static>>> {
let mut rows: Vec<Vec<Span<'static>>> = Vec::new();
let mut spans: Vec<Span<'static>> = Vec::new();
let mut used = 0usize;
for (key, description) in items {
let (key, description) = (key.as_ref(), description.as_ref());
let token_width = format!("{key} {description}").width();
let separator_width = if spans.is_empty() {
0
} else {
SEPARATOR.width()
};
if !spans.is_empty() && used + separator_width + token_width > width {
rows.push(std::mem::take(&mut spans));
used = 0;
}
if !spans.is_empty() {
spans.push(Span::styled(SEPARATOR, desc_style));
used += SEPARATOR.width();
}
spans.push(Span::styled(format!("{key} "), key_style));
spans.push(Span::styled(description.to_string(), desc_style));
used += token_width;
}
if !spans.is_empty() {
rows.push(spans);
}
rows
}
fn label_column_width<S: AsRef<str>>(groups: &[HintGroup<'_, S>]) -> usize {
let widest = groups
.iter()
.filter(|group| !group.label.is_empty())
.map(|group| group.label.width() + 1) .max()
.unwrap_or(0);
if widest == 0 { 0 } else { widest + LABEL_GAP }
}
fn pad(text: &str, width: usize) -> String {
let current = text.width();
if current >= width {
text.to_string()
} else {
format!("{text}{}", " ".repeat(width - current))
}
}