Skip to main content

tui_skeleton/
hbar_chart.rs

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
11/// Deterministic width fractions cycling across bars.
12const DEFAULT_WIDTHS: [f32; 7] = [0.85, 0.60, 0.95, 0.45, 0.75, 0.55, 0.70];
13
14/// Skeleton horizontal bar chart with bars of varying length.
15///
16/// Renders rows of `█` extending from the left edge, separated by
17/// single-row gaps. Widths cycle deterministically. Mimics a
18/// leaderboard or ranking display.
19#[must_use]
20#[derive(Debug, Clone)]
21pub struct SkeletonHBarChart<'a> {
22    elapsed_ms: u64,
23    mode: AnimationMode,
24    braille: bool,
25    base: Color,
26    highlight: Color,
27    bars: u16,
28    bar_height: u16,
29    widths: &'a [f32],
30    block: Option<ratatui_widgets::block::Block<'a>>,
31}
32
33impl<'a> SkeletonHBarChart<'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            bars: 5,
42            bar_height: 1,
43            widths: &DEFAULT_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    /// Number of horizontal bars. Default: `5`.
69    pub fn bars(mut self, bars: u16) -> Self {
70        self.bars = bars;
71        self
72    }
73
74    /// Height of each bar in rows. Default: `1`.
75    pub fn bar_height(mut self, height: u16) -> Self {
76        self.bar_height = height;
77        self
78    }
79
80    /// Override the per-bar width fractions (`0.0..=1.0`).
81    ///
82    /// The pattern cycles when there are more bars than entries.
83    pub fn widths(mut self, widths: &'a [f32]) -> Self {
84        self.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 SkeletonHBarChart<'_> {
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() || self.widths.is_empty() || self.bar_height == 0 {
105            return;
106        }
107
108        let stride = self.bar_height + 1; // bar + 1-row gap
109        let bar_count = self.bars.min((inner.height + 1) / stride);
110
111        let uniform_t = is_uniform(self.mode)
112            .then(|| cell_intensity(self.mode, self.elapsed_ms, 0, inner.width));
113
114        for i in 0..bar_count {
115            let frac = self.widths[i as usize % self.widths.len()].clamp(0.0, 1.0);
116            let bar_width = ((inner.width as f32) * frac).ceil() as u16;
117            let bar_y = inner.y + i * stride;
118
119            for dy in 0..self.bar_height {
120                let y = bar_y + dy;
121
122                if y >= inner.bottom() {
123                    break;
124                }
125
126                let row = y - inner.y;
127
128                for col in 0..bar_width.min(inner.width) {
129                    let x = inner.x + col;
130
131                    let t = uniform_t.unwrap_or_else(|| {
132                        cell_intensity(self.mode, self.elapsed_ms, col, inner.width)
133                    });
134                    let fg = interpolate_color(self.base, self.highlight, self.mode, t);
135                    let glyph = crate::animation::cell_glyph(
136                        self.braille,
137                        self.mode,
138                        self.elapsed_ms,
139                        row,
140                        col,
141                    );
142
143                    buf[(x, y)]
144                        .set_char(glyph)
145                        .set_style(Style::default().fg(fg));
146                }
147            }
148        }
149    }
150}
151
152#[cfg(feature = "pantry")]
153#[path = "hbar_chart.ingredient.rs"]
154pub mod ingredient;
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn bars_extend_from_left() {
162        let area = Rect::new(0, 0, 20, 5);
163        let mut buf = Buffer::empty(area);
164
165        SkeletonHBarChart::new(1000)
166            .bars(1)
167            .bar_height(1)
168            .widths(&[0.5])
169            .render(area, &mut buf);
170
171        // 50% of 20 = 10 cells.
172        assert_eq!(buf[(9, 0)].symbol(), "█");
173        assert_eq!(buf[(10, 0)].symbol(), " ");
174    }
175
176    #[test]
177    fn bars_have_gaps() {
178        let area = Rect::new(0, 0, 10, 5);
179        let mut buf = Buffer::empty(area);
180
181        SkeletonHBarChart::new(1000)
182            .bars(2)
183            .bar_height(1)
184            .widths(&[1.0, 1.0])
185            .render(area, &mut buf);
186
187        // Bar 0: row 0. Gap: row 1. Bar 1: row 2.
188        assert_eq!(buf[(0, 0)].symbol(), "█");
189        assert_eq!(buf[(0, 1)].symbol(), " ");
190        assert_eq!(buf[(0, 2)].symbol(), "█");
191    }
192
193    #[test]
194    fn multi_row_bars() {
195        let area = Rect::new(0, 0, 10, 7);
196        let mut buf = Buffer::empty(area);
197
198        SkeletonHBarChart::new(1000)
199            .bars(2)
200            .bar_height(2)
201            .widths(&[1.0, 1.0])
202            .render(area, &mut buf);
203
204        // Bar 0: rows 0-1. Gap: row 2. Bar 1: rows 3-4.
205        assert_eq!(buf[(0, 0)].symbol(), "█");
206        assert_eq!(buf[(0, 1)].symbol(), "█");
207        assert_eq!(buf[(0, 2)].symbol(), " ");
208        assert_eq!(buf[(0, 3)].symbol(), "█");
209        assert_eq!(buf[(0, 4)].symbol(), "█");
210    }
211}