use ratatui::layout::{Constraint, Rect};
use ratatui::text::Span;
use ratatui::widgets::{Block, Padding};
use ratatui::Frame;
use crate::view::fitted::{columns, cover, indent, Fitted, CUT, GAP};
use crate::view::palette;
const CLOSE_BINDINGS: &str = "Key bindings · press any key to close";
pub fn bindings_window(area: Rect, bindings: &[(String, &str)]) -> Rect {
area.centered(
Constraint::Length(wanted_width(bindings)),
Constraint::Length(bindings.len() as u16 + BORDERS),
)
}
pub(crate) fn bindings_block() -> Block<'static> {
Block::bordered().padding(Padding::horizontal(MARGIN))
}
fn wanted_width(bindings: &[(String, &str)]) -> u16 {
let keys = key_column(bindings);
let widest = bindings
.iter()
.map(|(_, does)| GAP + keys + GAP + does.chars().count())
.chain([
GAP + left_off(bindings.len()).chars().count(),
CLOSE_BINDINGS.chars().count(),
])
.max()
.unwrap_or(0);
u16::try_from(widest)
.unwrap_or(u16::MAX)
.saturating_add(BORDERS + MARGINS)
}
fn key_column(bindings: &[(String, &str)]) -> usize {
bindings
.iter()
.map(|(keys, _)| columns(&[Span::raw(keys.clone())]))
.max()
.unwrap_or(0)
}
pub fn key_bindings(frame: &mut Frame, area: Rect, bindings: &[(String, &str)]) {
let window = bindings_window(area, bindings);
if window.is_empty() {
return;
}
let block = bindings_block().title(Span::styled(CLOSE_BINDINGS, palette::TITLE));
let inner = block.inner(window);
cover(frame, window);
frame.render_widget(block, window);
let room = inner.height as usize;
let shown = if bindings.len() <= room {
bindings.len()
} else {
room.saturating_sub(1)
};
let width = key_column(bindings);
let row = |n: usize| Rect {
y: inner.y + n as u16,
height: 1,
..inner
};
for (n, (keys, does)) in bindings.iter().take(shown).enumerate() {
frame.render_widget(
Fitted::new(
vec![Span::raw(format!("{}{keys:<width$}", indent()))],
vec![Span::raw((*does).to_string())],
Vec::new(),
),
row(n),
);
}
if shown < bindings.len() && room > 0 {
frame.render_widget(
Fitted::new(
vec![Span::raw(format!(
"{}{}",
indent(),
left_off(bindings.len() - shown)
))],
Vec::new(),
Vec::new(),
),
row(shown),
);
}
}
fn left_off(count: usize) -> String {
let binding = if count == 1 { "binding" } else { "bindings" };
format!("{CUT} {count} more {binding} · no room on a screen this short")
}
const BORDERS: u16 = 2;
const MARGIN: u16 = 1;
const MARGINS: u16 = MARGIN * 2;
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use ratatui::backend::TestBackend;
use ratatui::Terminal;
use crate::view::painted::Painted;
fn a_few_bindings() -> Vec<(String, &'static str)> {
vec![
("Down, j".to_string(), "move down one row"),
(
"Enter".to_string(),
"focus the selected bead's pane in herdr",
),
("q, ^C".to_string(), "quit"),
]
}
fn bindings_frame(bindings: &[(String, &str)], width: u16, height: u16) -> Vec<String> {
let mut terminal = Terminal::new(TestBackend::new(width, height)).expect("a test backend");
terminal
.draw(|frame| key_bindings(frame, frame.area(), bindings))
.expect("a draw into memory");
let buffer = terminal.backend().buffer();
(0..height)
.map(|y| (0..width).map(|x| buffer[(x, y)].symbol()).collect())
.collect()
}
#[test]
fn the_key_bindings_view_names_the_keys_and_what_pressing_them_does() {
assert_eq!(
bindings_frame(&a_few_bindings(), 60, 5),
vec![
" ┌Key bindings · press any key to close─────────────────┐ ",
" │ Down, j move down one row │ ",
" │ Enter focus the selected bead's pane in herdr │ ",
" │ q, ^C quit │ ",
" └──────────────────────────────────────────────────────┘ ",
]
);
}
#[test]
fn the_way_out_is_drawn_before_any_binding_is() {
let drawn = bindings_frame(&a_few_bindings(), 60, 1);
assert!(drawn[0].contains("press any key to close"), "{drawn:?}");
}
#[test]
fn a_screen_too_short_for_every_binding_counts_the_ones_it_left_off() {
assert_eq!(
bindings_frame(&a_few_bindings(), 60, 4),
vec![
" ┌Key bindings · press any key to close─────────────────┐ ",
" │ Down, j move down one row │ ",
" │ … 2 more bindings · no room on a screen this short │ ",
" └──────────────────────────────────────────────────────┘ ",
]
);
}
#[test]
fn one_binding_left_off_is_not_counted_in_the_plural() {
assert!(left_off(1).contains("1 more binding ·"), "{}", left_off(1));
assert!(left_off(2).contains("2 more bindings ·"), "{}", left_off(2));
}
#[test]
fn the_count_is_never_spent_to_hide_fewer_bindings_than_it_displaces() {
let bindings = a_few_bindings();
for height in 2..=(bindings.len() as u16 + 2) {
let drawn = bindings_frame(&bindings, 60, height);
let counted = drawn.iter().filter(|row| row.contains("more binding"));
for row in counted {
assert!(!row.contains("1 more binding"), "at {height} rows: {row}");
}
}
}
#[test]
fn a_screen_with_one_row_spends_it_on_the_way_out() {
assert_eq!(
bindings_frame(&a_few_bindings(), 60, 1),
vec![" ┌Key bindings · press any key to close─────────────────┐ "]
);
}
#[test]
fn a_window_with_no_room_inside_it_draws_nothing_inside_it() {
assert_eq!(
bindings_frame(&a_few_bindings(), 60, 2),
vec![
" ┌Key bindings · press any key to close─────────────────┐ ",
" └──────────────────────────────────────────────────────┘ ",
]
);
}
#[test]
fn no_row_is_drawn_in_the_column_beside_a_border() {
let lines = a_table_of(8);
let bindings = bindings_over(&lines);
for width in [24, 40, 60, 80] {
for height in 3..=14 {
let area = Rect::new(0, 0, width, height);
let drawn = Painted::drawn_by(width, height, |frame| {
key_bindings(frame, frame.area(), &bindings);
});
let window = bindings_window(area, &bindings);
let margins = drawn.margins(window);
assert_eq!(
margins.trim(),
"",
"at {width} by {height}: {:#?}",
drawn.rows()
);
}
}
}
fn a_table_of(count: usize) -> Vec<String> {
(0..count).map(|n| format!("does the {n} thing")).collect()
}
fn bindings_over(lines: &[String]) -> Vec<(String, &str)> {
lines
.iter()
.enumerate()
.map(|(n, does)| (format!("k{n}"), does.as_str()))
.collect()
}
fn counted(drawn: &[String]) -> usize {
drawn
.iter()
.find_map(|row| {
row.split_once(CUT)?
.1
.split_whitespace()
.next()?
.parse()
.ok()
})
.unwrap_or(0)
}
#[test]
fn every_binding_is_drawn_or_counted_at_every_height() {
for count in [1usize, 2, 3, 12, 30] {
let lines = a_table_of(count);
let bindings = bindings_over(&lines);
for height in 3..=(count as u16 + 4) {
let drawn = bindings_frame(&bindings, 80, height);
let on_screen = lines
.iter()
.filter(|does| drawn.iter().any(|row| row.contains(*does)))
.count();
assert_eq!(
on_screen + counted(&drawn),
count,
"at {height} rows over {count} bindings: {drawn:#?}"
);
}
}
}
#[test]
fn the_screen_is_the_windows_only_ceiling() {
for count in [1usize, 3, 12, 30] {
let lines = a_table_of(count);
let bindings = bindings_over(&lines);
let wanted = count as u16 + BORDERS;
for height in 1..=40u16 {
let window = bindings_window(Rect::new(0, 0, 80, height), &bindings);
assert_eq!(
window.height,
wanted.min(height),
"a window over {count} bindings on a screen {height} rows tall"
);
}
}
}
#[test]
fn a_band_with_no_rows_in_it_draws_nothing() {
let mut terminal = Terminal::new(TestBackend::new(20, 1)).expect("a test backend");
terminal
.draw(|frame| {
key_bindings(frame, Rect::new(0, 0, 20, 0), &a_few_bindings());
})
.expect("a draw into memory");
assert_eq!(terminal.backend().buffer()[(0, 0)].symbol(), " ");
}
}