tui_skeleton/
bar_chart.rs1use 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_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 base: Color,
24 highlight: Color,
25 bars: u16,
26 bar_width: u16,
27 heights: &'a [f32],
28 block: Option<ratatui_widgets::block::Block<'a>>,
29}
30
31impl<'a> SkeletonBarChart<'a> {
32 pub fn new(elapsed_ms: u64) -> Self {
33 Self {
34 elapsed_ms,
35 mode: AnimationMode::default(),
36 base: defaults::BASE,
37 highlight: defaults::HIGHLIGHT,
38 bars: 6,
39 bar_width: 3,
40 heights: &DEFAULT_HEIGHTS,
41 block: None,
42 }
43 }
44
45 pub fn mode(mut self, mode: AnimationMode) -> Self {
46 self.mode = mode;
47 self
48 }
49
50 pub fn base(mut self, color: impl Into<Color>) -> Self {
51 self.base = color.into();
52 self
53 }
54
55 pub fn highlight(mut self, color: impl Into<Color>) -> Self {
56 self.highlight = color.into();
57 self
58 }
59
60 pub fn bars(mut self, bars: u16) -> Self {
62 self.bars = bars;
63 self
64 }
65
66 pub fn bar_width(mut self, width: u16) -> Self {
68 self.bar_width = width;
69 self
70 }
71
72 pub fn heights(mut self, heights: &'a [f32]) -> Self {
76 self.heights = heights;
77 self
78 }
79
80 pub fn block(mut self, block: ratatui_widgets::block::Block<'a>) -> Self {
81 self.block = Some(block);
82 self
83 }
84}
85
86impl Widget for SkeletonBarChart<'_> {
87 fn render(self, area: Rect, buf: &mut Buffer) {
88 let inner = if let Some(ref block) = self.block {
89 let inner_area = block.inner(area);
90 block.render(area, buf);
91 inner_area
92 } else {
93 area
94 };
95
96 if inner.is_empty() || self.heights.is_empty() || self.bar_width == 0 {
97 return;
98 }
99
100 let stride = self.bar_width + 1; let bar_count = self.bars.min((inner.width + 1) / stride);
102
103 let breathe_t = matches!(self.mode, AnimationMode::Breathe)
105 .then(|| cell_intensity(self.mode, self.elapsed_ms, 0, inner.width));
106
107 for i in 0..bar_count {
108 let frac = self.heights[i as usize % self.heights.len()].clamp(0.0, 1.0);
109 let bar_height = ((inner.height as f32) * frac).ceil() as u16;
110 let bar_x = inner.x + i * stride;
111 let bar_top = inner.y + inner.height - bar_height;
112
113 for dy in 0..bar_height {
114 let y = bar_top + dy;
115
116 for dx in 0..self.bar_width {
117 let x = bar_x + dx;
118
119 if x >= inner.right() {
120 break;
121 }
122
123 let col = x - inner.x;
124 let t = breathe_t.unwrap_or_else(|| {
125 cell_intensity(self.mode, self.elapsed_ms, col, inner.width)
126 });
127 let fg = interpolate_color(self.base, self.highlight, self.mode, t);
128
129 buf[(x, y)].set_char('█').set_style(Style::default().fg(fg));
130 }
131 }
132 }
133 }
134}
135
136#[cfg(feature = "pantry")]
137#[path = "bar_chart.ingredient.rs"]
138pub mod ingredient;
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn bars_rise_from_bottom() {
146 let area = Rect::new(0, 0, 10, 10);
147 let mut buf = Buffer::empty(area);
148
149 SkeletonBarChart::new(1000)
150 .bars(1)
151 .bar_width(2)
152 .heights(&[0.5])
153 .render(area, &mut buf);
154
155 assert_eq!(buf[(0, 5)].symbol(), "█");
157 assert_eq!(buf[(1, 5)].symbol(), "█");
158
159 assert_eq!(buf[(0, 4)].symbol(), " ");
161
162 assert_eq!(buf[(0, 9)].symbol(), "█");
164 }
165
166 #[test]
167 fn bars_have_gaps() {
168 let area = Rect::new(0, 0, 10, 5);
169 let mut buf = Buffer::empty(area);
170
171 SkeletonBarChart::new(1000)
172 .bars(2)
173 .bar_width(2)
174 .heights(&[1.0, 1.0])
175 .render(area, &mut buf);
176
177 assert_eq!(buf[(0, 0)].symbol(), "█");
179 assert_eq!(buf[(1, 0)].symbol(), "█");
180 assert_eq!(buf[(2, 0)].symbol(), " ");
181 assert_eq!(buf[(3, 0)].symbol(), "█");
182 }
183
184 #[test]
185 fn overflow_bars_clipped() {
186 let area = Rect::new(0, 0, 5, 5);
187 let mut buf = Buffer::empty(area);
188
189 SkeletonBarChart::new(1000)
191 .bars(3)
192 .bar_width(3)
193 .heights(&[1.0])
194 .render(area, &mut buf);
195
196 assert_eq!(buf[(0, 0)].symbol(), "█");
197 assert_eq!(buf[(2, 0)].symbol(), "█");
198
199 }
202}