use ratatui_core::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
widgets::Widget,
};
use crate::animation::{AnimationMode, cell_intensity, interpolate_color, is_uniform};
use crate::defaults;
const DEFAULT_VALUE_WIDTHS: [f32; 5] = [0.60, 0.40, 0.75, 0.35, 0.55];
#[must_use]
#[derive(Debug, Clone)]
pub struct SkeletonKvTable<'a> {
elapsed_ms: u64,
mode: AnimationMode,
braille: bool,
base: Color,
highlight: Color,
pairs: u16,
key_width: u16,
value_widths: &'a [f32],
block: Option<ratatui_widgets::block::Block<'a>>,
}
impl<'a> SkeletonKvTable<'a> {
pub fn new(elapsed_ms: u64) -> Self {
Self {
elapsed_ms,
mode: AnimationMode::default(),
braille: false,
base: defaults::BASE,
highlight: defaults::HIGHLIGHT,
pairs: 5,
key_width: 12,
value_widths: &DEFAULT_VALUE_WIDTHS,
block: None,
}
}
pub fn mode(mut self, mode: AnimationMode) -> Self {
self.mode = mode;
self
}
pub fn braille(mut self, braille: bool) -> Self {
self.braille = braille;
self
}
pub fn base(mut self, color: impl Into<Color>) -> Self {
self.base = color.into();
self
}
pub fn highlight(mut self, color: impl Into<Color>) -> Self {
self.highlight = color.into();
self
}
pub fn pairs(mut self, pairs: u16) -> Self {
self.pairs = pairs;
self
}
pub fn key_width(mut self, width: u16) -> Self {
self.key_width = width;
self
}
pub fn value_widths(mut self, widths: &'a [f32]) -> Self {
self.value_widths = widths;
self
}
pub fn block(mut self, block: ratatui_widgets::block::Block<'a>) -> Self {
self.block = Some(block);
self
}
}
impl Widget for SkeletonKvTable<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let inner = if let Some(ref block) = self.block {
let inner_area = block.inner(area);
block.render(area, buf);
inner_area
} else {
area
};
if inner.is_empty() || inner.width < self.key_width + 3 || self.value_widths.is_empty() {
return;
}
let sep_col = self.key_width;
let value_start = sep_col + 2; let value_space = inner.width - value_start;
let stride = 2u16; let pair_count = self.pairs.min((inner.height + 1) / stride);
let uniform_t = is_uniform(self.mode)
.then(|| cell_intensity(self.mode, self.elapsed_ms, 0, inner.width));
for i in 0..pair_count {
let y = inner.y + i * stride;
let row = y - inner.y;
if y >= inner.bottom() {
break;
}
for col in 0..self.key_width {
let x = inner.x + col;
let t = uniform_t.unwrap_or_else(|| {
cell_intensity(self.mode, self.elapsed_ms, col, inner.width)
});
let fg = interpolate_color(self.base, self.highlight, self.mode, t);
let glyph = crate::animation::cell_glyph(
self.braille,
self.mode,
self.elapsed_ms,
row,
col,
);
buf[(x, y)]
.set_char(glyph)
.set_style(Style::default().fg(fg));
}
buf[(inner.x + sep_col, y)]
.set_char('│')
.set_style(Style::default().fg(self.base));
let frac = self.value_widths[i as usize % self.value_widths.len()].clamp(0.0, 1.0);
let val_width = ((value_space as f32) * frac).ceil() as u16;
for col in 0..val_width.min(value_space) {
let abs_col = value_start + col;
let x = inner.x + abs_col;
let t = uniform_t.unwrap_or_else(|| {
cell_intensity(self.mode, self.elapsed_ms, abs_col, inner.width)
});
let fg = interpolate_color(self.base, self.highlight, self.mode, t);
let glyph = crate::animation::cell_glyph(
self.braille,
self.mode,
self.elapsed_ms,
row,
abs_col,
);
buf[(x, y)]
.set_char(glyph)
.set_style(Style::default().fg(fg));
}
}
}
}
#[cfg(feature = "pantry")]
#[path = "kv_table.ingredient.rs"]
pub mod ingredient;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn separator_between_key_and_value() {
let area = Rect::new(0, 0, 30, 3);
let mut buf = Buffer::empty(area);
SkeletonKvTable::new(1000)
.pairs(1)
.key_width(8)
.render(area, &mut buf);
assert_eq!(buf[(0, 0)].symbol(), "█");
assert_eq!(buf[(7, 0)].symbol(), "█");
assert_eq!(buf[(8, 0)].symbol(), "│");
assert_eq!(buf[(9, 0)].symbol(), " ");
assert_eq!(buf[(10, 0)].symbol(), "█");
}
#[test]
fn pairs_have_gaps() {
let area = Rect::new(0, 0, 30, 4);
let mut buf = Buffer::empty(area);
SkeletonKvTable::new(1000)
.pairs(2)
.key_width(5)
.render(area, &mut buf);
assert_eq!(buf[(0, 0)].symbol(), "█");
assert_eq!(buf[(0, 1)].symbol(), " ");
assert_eq!(buf[(0, 2)].symbol(), "█");
}
#[test]
fn too_narrow_is_noop() {
let area = Rect::new(0, 0, 5, 5);
let mut buf = Buffer::empty(area);
let expected = buf.clone();
SkeletonKvTable::new(1000)
.key_width(10)
.render(area, &mut buf);
assert_eq!(buf, expected);
}
}