1use ratatui_core::{
2 buffer::Buffer,
3 layout::Rect,
4 style::{Color, Style},
5 widgets::Widget,
6};
7
8use crate::animation::{cell_intensity, interpolate_color, AnimationMode};
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 base: Color,
25 highlight: Color,
26 pairs: u16,
27 key_width: u16,
28 value_widths: &'a [f32],
29 block: Option<ratatui_widgets::block::Block<'a>>,
30}
31
32impl<'a> SkeletonKvTable<'a> {
33 pub fn new(elapsed_ms: u64) -> Self {
34 Self {
35 elapsed_ms,
36 mode: AnimationMode::default(),
37 base: defaults::BASE,
38 highlight: defaults::HIGHLIGHT,
39 pairs: 5,
40 key_width: 12,
41 value_widths: &DEFAULT_VALUE_WIDTHS,
42 block: None,
43 }
44 }
45
46 pub fn mode(mut self, mode: AnimationMode) -> Self {
47 self.mode = mode;
48 self
49 }
50
51 pub fn base(mut self, color: impl Into<Color>) -> Self {
52 self.base = color.into();
53 self
54 }
55
56 pub fn highlight(mut self, color: impl Into<Color>) -> Self {
57 self.highlight = color.into();
58 self
59 }
60
61 pub fn pairs(mut self, pairs: u16) -> Self {
63 self.pairs = pairs;
64 self
65 }
66
67 pub fn key_width(mut self, width: u16) -> Self {
69 self.key_width = width;
70 self
71 }
72
73 pub fn value_widths(mut self, widths: &'a [f32]) -> Self {
77 self.value_widths = widths;
78 self
79 }
80
81 pub fn block(mut self, block: ratatui_widgets::block::Block<'a>) -> Self {
82 self.block = Some(block);
83 self
84 }
85}
86
87impl Widget for SkeletonKvTable<'_> {
88 fn render(self, area: Rect, buf: &mut Buffer) {
89 let inner = if let Some(ref block) = self.block {
90 let inner_area = block.inner(area);
91 block.render(area, buf);
92 inner_area
93 } else {
94 area
95 };
96
97 if inner.is_empty() || inner.width < self.key_width + 3 || self.value_widths.is_empty() {
99 return;
100 }
101
102 let sep_col = self.key_width;
103 let value_start = sep_col + 2; let value_space = inner.width - value_start;
105
106 let stride = 2u16; let pair_count = self.pairs.min((inner.height + 1) / stride);
108
109 let breathe_t = matches!(self.mode, AnimationMode::Breathe)
111 .then(|| cell_intensity(self.mode, self.elapsed_ms, 0, inner.width));
112
113 for i in 0..pair_count {
114 let y = inner.y + i * stride;
115
116 if y >= inner.bottom() {
117 break;
118 }
119
120 for col in 0..self.key_width {
122 let x = inner.x + col;
123 let t = breathe_t.unwrap_or_else(|| {
124 cell_intensity(self.mode, self.elapsed_ms, col, inner.width)
125 });
126 let fg = interpolate_color(self.base, self.highlight, self.mode, t);
127
128 buf[(x, y)].set_char('█').set_style(Style::default().fg(fg));
129 }
130
131 buf[(inner.x + sep_col, y)]
133 .set_char('│')
134 .set_style(Style::default().fg(self.base));
135
136 let frac = self.value_widths[i as usize % self.value_widths.len()].clamp(0.0, 1.0);
138 let val_width = ((value_space as f32) * frac).ceil() as u16;
139
140 for col in 0..val_width.min(value_space) {
141 let abs_col = value_start + col;
142 let x = inner.x + abs_col;
143 let t = breathe_t.unwrap_or_else(|| {
144 cell_intensity(self.mode, self.elapsed_ms, abs_col, inner.width)
145 });
146 let fg = interpolate_color(self.base, self.highlight, self.mode, t);
147
148 buf[(x, y)].set_char('█').set_style(Style::default().fg(fg));
149 }
150 }
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn separator_between_key_and_value() {
160 let area = Rect::new(0, 0, 30, 3);
161 let mut buf = Buffer::empty(area);
162
163 SkeletonKvTable::new(1000)
164 .pairs(1)
165 .key_width(8)
166 .render(area, &mut buf);
167
168 assert_eq!(buf[(0, 0)].symbol(), "█");
170 assert_eq!(buf[(7, 0)].symbol(), "█");
171
172 assert_eq!(buf[(8, 0)].symbol(), "│");
174
175 assert_eq!(buf[(9, 0)].symbol(), " ");
177 assert_eq!(buf[(10, 0)].symbol(), "█");
178 }
179
180 #[test]
181 fn pairs_have_gaps() {
182 let area = Rect::new(0, 0, 30, 4);
183 let mut buf = Buffer::empty(area);
184
185 SkeletonKvTable::new(1000)
186 .pairs(2)
187 .key_width(5)
188 .render(area, &mut buf);
189
190 assert_eq!(buf[(0, 0)].symbol(), "█");
192
193 assert_eq!(buf[(0, 1)].symbol(), " ");
195
196 assert_eq!(buf[(0, 2)].symbol(), "█");
198 }
199
200 #[test]
201 fn too_narrow_is_noop() {
202 let area = Rect::new(0, 0, 5, 5);
203 let mut buf = Buffer::empty(area);
204 let expected = buf.clone();
205
206 SkeletonKvTable::new(1000)
207 .key_width(10)
208 .render(area, &mut buf);
209
210 assert_eq!(buf, expected);
211 }
212}