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_HEIGHTS: [f32; 7] = [0.6, 0.85, 0.45, 0.95, 0.70, 0.55, 0.80];
13
14#[must_use]
19#[derive(Debug, Clone)]
20pub struct SkeletonBarChart<'a> {
21 elapsed_ms: u64,
22 mode: AnimationMode,
23 braille: bool,
24 base: Color,
25 highlight: Color,
26 bars: u16,
27 bar_width: u16,
28 heights: &'a [f32],
29 block: Option<ratatui_widgets::block::Block<'a>>,
30}
31
32impl<'a> SkeletonBarChart<'a> {
33 pub fn new(elapsed_ms: u64) -> Self {
34 Self {
35 elapsed_ms,
36 mode: AnimationMode::default(),
37 braille: false,
38 base: defaults::BASE,
39 highlight: defaults::HIGHLIGHT,
40 bars: 6,
41 bar_width: 3,
42 heights: &DEFAULT_HEIGHTS,
43 block: None,
44 }
45 }
46
47 pub fn mode(mut self, mode: AnimationMode) -> Self {
48 self.mode = mode;
49 self
50 }
51
52 pub fn braille(mut self, braille: bool) -> Self {
53 self.braille = braille;
54 self
55 }
56
57 pub fn base(mut self, color: impl Into<Color>) -> Self {
58 self.base = color.into();
59 self
60 }
61
62 pub fn highlight(mut self, color: impl Into<Color>) -> Self {
63 self.highlight = color.into();
64 self
65 }
66
67 pub fn bars(mut self, bars: u16) -> Self {
69 self.bars = bars;
70 self
71 }
72
73 pub fn bar_width(mut self, width: u16) -> Self {
75 self.bar_width = width;
76 self
77 }
78
79 pub fn heights(mut self, heights: &'a [f32]) -> Self {
83 self.heights = heights;
84 self
85 }
86
87 pub fn block(mut self, block: ratatui_widgets::block::Block<'a>) -> Self {
88 self.block = Some(block);
89 self
90 }
91}
92
93impl Widget for SkeletonBarChart<'_> {
94 fn render(self, area: Rect, buf: &mut Buffer) {
95 let inner = if let Some(ref block) = self.block {
96 let inner_area = block.inner(area);
97 block.render(area, buf);
98 inner_area
99 } else {
100 area
101 };
102
103 if inner.is_empty() || self.heights.is_empty() || self.bar_width == 0 {
104 return;
105 }
106
107 let stride = self.bar_width + 1; let bar_count = self.bars.min((inner.width + 1) / stride);
109
110 let uniform_t = is_uniform(self.mode)
111 .then(|| cell_intensity(self.mode, self.elapsed_ms, 0, inner.width));
112
113 for i in 0..bar_count {
114 let frac = self.heights[i as usize % self.heights.len()].clamp(0.0, 1.0);
115 let bar_height = ((inner.height as f32) * frac).ceil() as u16;
116 let bar_x = inner.x + i * stride;
117 let bar_top = inner.y + inner.height - bar_height;
118
119 for dy in 0..bar_height {
120 let y = bar_top + dy;
121 let row = y - inner.y;
122
123 for dx in 0..self.bar_width {
124 let x = bar_x + dx;
125
126 if x >= inner.right() {
127 break;
128 }
129
130 let col = x - inner.x;
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 = "bar_chart.ingredient.rs"]
154pub mod ingredient;
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn bars_rise_from_bottom() {
162 let area = Rect::new(0, 0, 10, 10);
163 let mut buf = Buffer::empty(area);
164
165 SkeletonBarChart::new(1000)
166 .bars(1)
167 .bar_width(2)
168 .heights(&[0.5])
169 .render(area, &mut buf);
170
171 assert_eq!(buf[(0, 5)].symbol(), "█");
173 assert_eq!(buf[(1, 5)].symbol(), "█");
174
175 assert_eq!(buf[(0, 4)].symbol(), " ");
177
178 assert_eq!(buf[(0, 9)].symbol(), "█");
180 }
181
182 #[test]
183 fn bars_have_gaps() {
184 let area = Rect::new(0, 0, 10, 5);
185 let mut buf = Buffer::empty(area);
186
187 SkeletonBarChart::new(1000)
188 .bars(2)
189 .bar_width(2)
190 .heights(&[1.0, 1.0])
191 .render(area, &mut buf);
192
193 assert_eq!(buf[(0, 0)].symbol(), "█");
195 assert_eq!(buf[(1, 0)].symbol(), "█");
196 assert_eq!(buf[(2, 0)].symbol(), " ");
197 assert_eq!(buf[(3, 0)].symbol(), "█");
198 }
199
200 #[test]
201 fn overflow_bars_clipped() {
202 let area = Rect::new(0, 0, 5, 5);
203 let mut buf = Buffer::empty(area);
204
205 SkeletonBarChart::new(1000)
207 .bars(3)
208 .bar_width(3)
209 .heights(&[1.0])
210 .render(area, &mut buf);
211
212 assert_eq!(buf[(0, 0)].symbol(), "█");
213 assert_eq!(buf[(2, 0)].symbol(), "█");
214
215 }
218}