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
11const DEFAULT_WIDTHS: [f32; 7] = [0.85, 0.60, 0.95, 0.45, 0.75, 0.55, 0.70];
13
14#[must_use]
20#[derive(Debug, Clone)]
21pub struct SkeletonHBarChart<'a> {
22 elapsed_ms: u64,
23 mode: AnimationMode,
24 base: Color,
25 highlight: Color,
26 bars: u16,
27 bar_height: u16,
28 widths: &'a [f32],
29 block: Option<ratatui_widgets::block::Block<'a>>,
30}
31
32impl<'a> SkeletonHBarChart<'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 bars: 5,
40 bar_height: 1,
41 widths: &DEFAULT_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 pub fn bars(mut self, bars: u16) -> Self {
63 self.bars = bars;
64 self
65 }
66
67 pub fn bar_height(mut self, height: u16) -> Self {
69 self.bar_height = height;
70 self
71 }
72
73 pub fn widths(mut self, widths: &'a [f32]) -> Self {
77 self.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 SkeletonHBarChart<'_> {
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 if inner.is_empty() || self.widths.is_empty() || self.bar_height == 0 {
98 return;
99 }
100
101 let stride = self.bar_height + 1; let bar_count = self.bars.min((inner.height + 1) / stride);
103
104 let breathe_t = matches!(self.mode, AnimationMode::Breathe)
106 .then(|| cell_intensity(self.mode, self.elapsed_ms, 0, inner.width));
107
108 for i in 0..bar_count {
109 let frac = self.widths[i as usize % self.widths.len()].clamp(0.0, 1.0);
110 let bar_width = ((inner.width as f32) * frac).ceil() as u16;
111 let bar_y = inner.y + i * stride;
112
113 for dy in 0..self.bar_height {
114 let y = bar_y + dy;
115
116 if y >= inner.bottom() {
117 break;
118 }
119
120 for col in 0..bar_width.min(inner.width) {
121 let x = inner.x + col;
122
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 }
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn bars_extend_from_left() {
141 let area = Rect::new(0, 0, 20, 5);
142 let mut buf = Buffer::empty(area);
143
144 SkeletonHBarChart::new(1000)
145 .bars(1)
146 .bar_height(1)
147 .widths(&[0.5])
148 .render(area, &mut buf);
149
150 assert_eq!(buf[(9, 0)].symbol(), "█");
152 assert_eq!(buf[(10, 0)].symbol(), " ");
153 }
154
155 #[test]
156 fn bars_have_gaps() {
157 let area = Rect::new(0, 0, 10, 5);
158 let mut buf = Buffer::empty(area);
159
160 SkeletonHBarChart::new(1000)
161 .bars(2)
162 .bar_height(1)
163 .widths(&[1.0, 1.0])
164 .render(area, &mut buf);
165
166 assert_eq!(buf[(0, 0)].symbol(), "█");
168 assert_eq!(buf[(0, 1)].symbol(), " ");
169 assert_eq!(buf[(0, 2)].symbol(), "█");
170 }
171
172 #[test]
173 fn multi_row_bars() {
174 let area = Rect::new(0, 0, 10, 7);
175 let mut buf = Buffer::empty(area);
176
177 SkeletonHBarChart::new(1000)
178 .bars(2)
179 .bar_height(2)
180 .widths(&[1.0, 1.0])
181 .render(area, &mut buf);
182
183 assert_eq!(buf[(0, 0)].symbol(), "█");
185 assert_eq!(buf[(0, 1)].symbol(), "█");
186 assert_eq!(buf[(0, 2)].symbol(), " ");
187 assert_eq!(buf[(0, 3)].symbol(), "█");
188 assert_eq!(buf[(0, 4)].symbol(), "█");
189 }
190}