use ratatui::{
buffer::Buffer,
layout::Rect,
prelude::Widget,
text::{Line, Span},
widgets::{Block, Borders, Clear, Padding, Paragraph},
};
use crate::{BindingGroup, Key, WhichKey, WhichKeyState};
pub fn render_popup<K, S, A, C>(
config: &WhichKey,
buf: &mut Buffer,
state: &WhichKeyState<K, S, A, C>,
) where
K: Key + Clone + PartialEq,
S: Clone + Ord + PartialEq + Send + Sync,
A: Clone + Send + Sync,
C: Clone + std::fmt::Debug,
{
let groups = state.current_bindings();
if groups.is_empty() {
return;
}
let columns = build_columns(&groups, config.max_height);
let title = if state.current_sequence.is_empty() {
" Shortcuts ".to_string()
} else {
format!(" {} ", state.format_path())
};
let popup_area = calculate_popup_area(config, *buf.area(), &columns, &title);
Clear.render(popup_area, buf);
let block = Block::default()
.title(title.as_str())
.borders(Borders::ALL)
.border_style(config.border_style)
.padding(Padding::horizontal(1));
let inner_area = block.inner(popup_area);
block.render(popup_area, buf);
let column_areas = layout_columns(&columns, inner_area);
for (col_area, col_data) in column_areas.iter().zip(columns.iter()) {
render_column(buf, *col_area, col_data, config);
}
}
#[derive(Debug, Clone)]
struct ColumnData<K: Key> {
groups: Vec<(String, Vec<(K, String)>)>,
max_key_width: usize,
max_desc_width: usize,
}
impl<K: Key> ColumnData<K> {
const fn content_width(&self) -> usize {
self.max_key_width + 1 + self.max_desc_width
}
}
fn build_columns<K: Key>(groups: &[BindingGroup<K>], max_height: u16) -> Vec<ColumnData<K>> {
let rows_per_column = max_height.saturating_sub(2) as usize;
let mut columns: Vec<ColumnData<K>> = Vec::new();
let mut current_groups: Vec<(String, Vec<(K, String)>)> = Vec::new();
let mut current_rows = 0usize;
for group in groups {
let items: Vec<(K, String)> = group
.bindings
.iter()
.map(|b| (b.key.clone(), b.description.clone()))
.collect();
let group_rows = items.len() + 1;
if current_rows + group_rows > rows_per_column && current_rows > 0 {
columns.push(build_column_data(current_groups));
current_groups = Vec::new();
current_rows = 0;
}
current_groups.push((group.category.clone(), items));
current_rows += group_rows;
}
if !current_groups.is_empty() {
columns.push(build_column_data(current_groups));
}
columns
}
fn build_column_data<K: Key>(groups: Vec<(String, Vec<(K, String)>)>) -> ColumnData<K> {
let max_key_width = groups
.iter()
.flat_map(|(_, items)| items.iter())
.map(|(k, _)| k.display().len())
.max()
.unwrap_or(5);
let max_desc_width = groups
.iter()
.flat_map(|(_, items)| items.iter())
.map(|(_, d)| d.len())
.max()
.unwrap_or(10);
ColumnData {
groups,
max_key_width,
max_desc_width,
}
}
#[allow(clippy::cast_possible_truncation)]
fn calculate_popup_area<K: Key>(
config: &WhichKey,
frame_area: Rect,
columns: &[ColumnData<K>],
title: &str,
) -> Rect {
let column_gap = 1u16;
let total_content_width: u16 = columns.iter().map(|c| c.content_width() as u16).sum();
let total_gap = column_gap * columns.len().saturating_sub(1) as u16;
let title_width = title.len() as u16 + 2;
let min_width = total_content_width + total_gap + 4;
let popup_width = min_width
.max(title_width)
.min(frame_area.width.saturating_sub(2));
let popup_height = config.max_height.min(frame_area.height.saturating_sub(2));
let x = match config.position {
crate::PopupPosition::BottomLeft | crate::PopupPosition::TopLeft => 1,
crate::PopupPosition::BottomRight | crate::PopupPosition::TopRight => frame_area
.width
.saturating_sub(popup_width)
.saturating_sub(1),
};
let y = match config.position {
crate::PopupPosition::BottomLeft | crate::PopupPosition::BottomRight => frame_area
.height
.saturating_sub(popup_height)
.saturating_sub(1),
crate::PopupPosition::TopLeft | crate::PopupPosition::TopRight => 1,
};
Rect::new(x, y, popup_width, popup_height)
}
#[allow(clippy::cast_possible_truncation)]
fn layout_columns<K: Key>(columns: &[ColumnData<K>], inner_area: Rect) -> Vec<Rect> {
let column_gap = 1u16;
let mut result = Vec::with_capacity(columns.len());
let mut x = inner_area.x;
for column_data in columns {
let width = column_data.content_width() as u16;
result.push(Rect::new(x, inner_area.y, width, inner_area.height));
x += width + column_gap;
}
result
}
fn render_column<K: Key>(
buf: &mut Buffer,
area: Rect,
column_data: &ColumnData<K>,
config: &WhichKey,
) {
let mut y = area.y;
for (category, items) in &column_data.groups {
if y >= area.bottom() {
break;
}
if !category.is_empty() {
let header = Paragraph::new(category.clone()).style(config.category_style);
header.render(Rect::new(area.x, y, area.width, 1), buf);
y += 1;
}
for (key, description) in items {
if y >= area.bottom() {
break;
}
let key_display = key.display();
let key_span = Span::styled(
format!("{:>width$}", key_display, width = column_data.max_key_width),
config.key_style,
);
let desc_span = Span::styled(format!(" {description}"), config.description_style);
let line = Line::from(vec![key_span, desc_span]);
let para = Paragraph::new(line);
para.render(Rect::new(area.x, y, area.width, 1), buf);
y += 1;
}
}
}