1use ratatui_core::{
2 buffer::Buffer,
3 layout::Rect,
4 style::{Color, Style},
5 widgets::Widget,
6};
7
8use crate::animation::{AnimationMode, cell_intensity, interpolate_color, is_uniform};
9use crate::defaults;
10
11const DEFAULT_VALUE_WIDTHS: [f32; 5] = [0.60, 0.40, 0.75, 0.35, 0.55];
13
14#[must_use]
20#[derive(Debug, Clone)]
21pub struct SkeletonKvTable<'a> {
22 elapsed_ms: u64,
23 mode: AnimationMode,
24 braille: bool,
25 base: Color,
26 highlight: Color,
27 pairs: u16,
28 key_width: u16,
29 value_widths: &'a [f32],
30 block: Option<ratatui_widgets::block::Block<'a>>,
31}
32
33impl<'a> SkeletonKvTable<'a> {
34 pub fn new(elapsed_ms: u64) -> Self {
35 Self {
36 elapsed_ms,
37 mode: AnimationMode::default(),
38 braille: false,
39 base: defaults::BASE,
40 highlight: defaults::HIGHLIGHT,
41 pairs: 5,
42 key_width: 12,
43 value_widths: &DEFAULT_VALUE_WIDTHS,
44 block: None,
45 }
46 }
47
48 pub fn mode(mut self, mode: AnimationMode) -> Self {
49 self.mode = mode;
50 self
51 }
52
53 pub fn braille(mut self, braille: bool) -> Self {
54 self.braille = braille;
55 self
56 }
57
58 pub fn base(mut self, color: impl Into<Color>) -> Self {
59 self.base = color.into();
60 self
61 }
62
63 pub fn highlight(mut self, color: impl Into<Color>) -> Self {
64 self.highlight = color.into();
65 self
66 }
67
68 pub fn pairs(mut self, pairs: u16) -> Self {
70 self.pairs = pairs;
71 self
72 }
73
74 pub fn key_width(mut self, width: u16) -> Self {
76 self.key_width = width;
77 self
78 }
79
80 pub fn value_widths(mut self, widths: &'a [f32]) -> Self {
84 self.value_widths = widths;
85 self
86 }
87
88 pub fn block(mut self, block: ratatui_widgets::block::Block<'a>) -> Self {
89 self.block = Some(block);
90 self
91 }
92}
93
94impl Widget for SkeletonKvTable<'_> {
95 fn render(self, area: Rect, buf: &mut Buffer) {
96 let inner = if let Some(ref block) = self.block {
97 let inner_area = block.inner(area);
98 block.render(area, buf);
99 inner_area
100 } else {
101 area
102 };
103
104 if inner.is_empty() || inner.width < self.key_width + 3 || self.value_widths.is_empty() {
106 return;
107 }
108
109 let sep_col = self.key_width;
110 let value_start = sep_col + 2; let value_space = inner.width - value_start;
112
113 let stride = 2u16; let pair_count = self.pairs.min((inner.height + 1) / stride);
115
116 let uniform_t = is_uniform(self.mode)
117 .then(|| cell_intensity(self.mode, self.elapsed_ms, 0, inner.width));
118
119 for i in 0..pair_count {
120 let y = inner.y + i * stride;
121 let row = y - inner.y;
122
123 if y >= inner.bottom() {
124 break;
125 }
126
127 for col in 0..self.key_width {
129 let x = inner.x + col;
130 let t = uniform_t.unwrap_or_else(|| {
131 cell_intensity(self.mode, self.elapsed_ms, col, inner.width)
132 });
133 let fg = interpolate_color(self.base, self.highlight, self.mode, t);
134 let glyph = crate::animation::cell_glyph(
135 self.braille,
136 self.mode,
137 self.elapsed_ms,
138 row,
139 col,
140 );
141
142 buf[(x, y)]
143 .set_char(glyph)
144 .set_style(Style::default().fg(fg));
145 }
146
147 buf[(inner.x + sep_col, y)]
149 .set_char('│')
150 .set_style(Style::default().fg(self.base));
151
152 let frac = self.value_widths[i as usize % self.value_widths.len()].clamp(0.0, 1.0);
154 let val_width = ((value_space as f32) * frac).ceil() as u16;
155
156 for col in 0..val_width.min(value_space) {
157 let abs_col = value_start + col;
158 let x = inner.x + abs_col;
159 let t = uniform_t.unwrap_or_else(|| {
160 cell_intensity(self.mode, self.elapsed_ms, abs_col, inner.width)
161 });
162 let fg = interpolate_color(self.base, self.highlight, self.mode, t);
163 let glyph = crate::animation::cell_glyph(
164 self.braille,
165 self.mode,
166 self.elapsed_ms,
167 row,
168 abs_col,
169 );
170
171 buf[(x, y)]
172 .set_char(glyph)
173 .set_style(Style::default().fg(fg));
174 }
175 }
176 }
177}
178
179#[cfg(feature = "pantry")]
180#[path = "kv_table.ingredient.rs"]
181pub mod ingredient;
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 #[test]
188 fn separator_between_key_and_value() {
189 let area = Rect::new(0, 0, 30, 3);
190 let mut buf = Buffer::empty(area);
191
192 SkeletonKvTable::new(1000)
193 .pairs(1)
194 .key_width(8)
195 .render(area, &mut buf);
196
197 assert_eq!(buf[(0, 0)].symbol(), "█");
199 assert_eq!(buf[(7, 0)].symbol(), "█");
200
201 assert_eq!(buf[(8, 0)].symbol(), "│");
203
204 assert_eq!(buf[(9, 0)].symbol(), " ");
206 assert_eq!(buf[(10, 0)].symbol(), "█");
207 }
208
209 #[test]
210 fn pairs_have_gaps() {
211 let area = Rect::new(0, 0, 30, 4);
212 let mut buf = Buffer::empty(area);
213
214 SkeletonKvTable::new(1000)
215 .pairs(2)
216 .key_width(5)
217 .render(area, &mut buf);
218
219 assert_eq!(buf[(0, 0)].symbol(), "█");
221
222 assert_eq!(buf[(0, 1)].symbol(), " ");
224
225 assert_eq!(buf[(0, 2)].symbol(), "█");
227 }
228
229 #[test]
230 fn too_narrow_is_noop() {
231 let area = Rect::new(0, 0, 5, 5);
232 let mut buf = Buffer::empty(area);
233 let expected = buf.clone();
234
235 SkeletonKvTable::new(1000)
236 .key_width(10)
237 .render(area, &mut buf);
238
239 assert_eq!(buf, expected);
240 }
241}