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(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn bars_rise_from_bottom() {
142 let area = Rect::new(0, 0, 10, 10);
143 let mut buf = Buffer::empty(area);
144
145 SkeletonBarChart::new(1000)
146 .bars(1)
147 .bar_width(2)
148 .heights(&[0.5])
149 .render(area, &mut buf);
150
151 assert_eq!(buf[(0, 5)].symbol(), "█");
153 assert_eq!(buf[(1, 5)].symbol(), "█");
154
155 assert_eq!(buf[(0, 4)].symbol(), " ");
157
158 assert_eq!(buf[(0, 9)].symbol(), "█");
160 }
161
162 #[test]
163 fn bars_have_gaps() {
164 let area = Rect::new(0, 0, 10, 5);
165 let mut buf = Buffer::empty(area);
166
167 SkeletonBarChart::new(1000)
168 .bars(2)
169 .bar_width(2)
170 .heights(&[1.0, 1.0])
171 .render(area, &mut buf);
172
173 assert_eq!(buf[(0, 0)].symbol(), "█");
175 assert_eq!(buf[(1, 0)].symbol(), "█");
176 assert_eq!(buf[(2, 0)].symbol(), " ");
177 assert_eq!(buf[(3, 0)].symbol(), "█");
178 }
179
180 #[test]
181 fn overflow_bars_clipped() {
182 let area = Rect::new(0, 0, 5, 5);
183 let mut buf = Buffer::empty(area);
184
185 SkeletonBarChart::new(1000)
187 .bars(3)
188 .bar_width(3)
189 .heights(&[1.0])
190 .render(area, &mut buf);
191
192 assert_eq!(buf[(0, 0)].symbol(), "█");
193 assert_eq!(buf[(2, 0)].symbol(), "█");
194
195 }
198}