Skip to main content

tui_skeleton/
kv_table.rs

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
11/// Deterministic value width fractions cycling across rows.
12const DEFAULT_VALUE_WIDTHS: [f32; 5] = [0.60, 0.40, 0.75, 0.35, 0.55];
13
14/// Skeleton key-value table (properties panel / detail view).
15///
16/// Renders pairs of short fixed-width keys on the left and
17/// variable-width values on the right, separated by a dim `│`.
18/// Each pair occupies one row with a gap row between pairs.
19#[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    /// Number of key-value pairs. Default: `5`.
62    pub fn pairs(mut self, pairs: u16) -> Self {
63        self.pairs = pairs;
64        self
65    }
66
67    /// Fixed width of the key column in cells. Default: `12`.
68    pub fn key_width(mut self, width: u16) -> Self {
69        self.key_width = width;
70        self
71    }
72
73    /// Per-pair value width fractions (`0.0..=1.0`) of the remaining space.
74    ///
75    /// The pattern cycles when there are more pairs than entries.
76    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        // key_width + separator(1) + gap(1) + at least 1 value cell
98        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; // separator + 1 gap
104        let value_space = inner.width - value_start;
105
106        let stride = 2u16; // content row + gap row
107        let pair_count = self.pairs.min((inner.height + 1) / stride);
108
109        // Breathe is uniform — hoist.
110        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            // Key cells.
121            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            // Separator.
132            buf[(inner.x + sep_col, y)]
133                .set_char('│')
134                .set_style(Style::default().fg(self.base));
135
136            // Value cells.
137            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        // Key fills cols 0..8.
169        assert_eq!(buf[(0, 0)].symbol(), "█");
170        assert_eq!(buf[(7, 0)].symbol(), "█");
171
172        // Separator at col 8.
173        assert_eq!(buf[(8, 0)].symbol(), "│");
174
175        // Gap at col 9, value starts at col 10.
176        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        // Row 0: content.
191        assert_eq!(buf[(0, 0)].symbol(), "█");
192
193        // Row 1: gap.
194        assert_eq!(buf[(0, 1)].symbol(), " ");
195
196        // Row 2: content.
197        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}