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
11#[must_use]
17#[derive(Debug, Clone)]
18pub struct SkeletonBlock<'a> {
19 elapsed_ms: u64,
20 mode: AnimationMode,
21 base: Color,
22 highlight: Color,
23 block: Option<ratatui_widgets::block::Block<'a>>,
24}
25
26impl<'a> SkeletonBlock<'a> {
27 pub fn new(elapsed_ms: u64) -> Self {
28 Self {
29 elapsed_ms,
30 mode: AnimationMode::default(),
31 base: defaults::BASE,
32 highlight: defaults::HIGHLIGHT,
33 block: None,
34 }
35 }
36
37 pub fn mode(mut self, mode: AnimationMode) -> Self {
38 self.mode = mode;
39 self
40 }
41
42 pub fn base(mut self, color: impl Into<Color>) -> Self {
43 self.base = color.into();
44 self
45 }
46
47 pub fn highlight(mut self, color: impl Into<Color>) -> Self {
48 self.highlight = color.into();
49 self
50 }
51
52 pub fn block(mut self, block: ratatui_widgets::block::Block<'a>) -> Self {
53 self.block = Some(block);
54 self
55 }
56}
57
58impl Widget for SkeletonBlock<'_> {
59 fn render(self, area: Rect, buf: &mut Buffer) {
60 let inner = if let Some(ref block) = self.block {
61 let inner_area = block.inner(area);
62 block.render(area, buf);
63 inner_area
64 } else {
65 area
66 };
67
68 if inner.is_empty() {
69 return;
70 }
71
72 render_skeleton_cells(
73 inner,
74 buf,
75 self.mode,
76 self.elapsed_ms,
77 self.base,
78 self.highlight,
79 |_row, col, width| col < width,
80 );
81 }
82}
83
84pub(crate) fn render_skeleton_cells(
88 area: Rect,
89 buf: &mut Buffer,
90 mode: AnimationMode,
91 elapsed_ms: u64,
92 base: Color,
93 highlight: Color,
94 visible: impl Fn(u16, u16, u16) -> bool,
95) {
96 let breathe_t = matches!(mode, AnimationMode::Breathe)
98 .then(|| cell_intensity(mode, elapsed_ms, 0, area.width));
99
100 for row in 0..area.height {
101 for col in 0..area.width {
102 if !visible(row, col, area.width) {
103 continue;
104 }
105
106 let t = breathe_t.unwrap_or_else(|| cell_intensity(mode, elapsed_ms, col, area.width));
107
108 let fg = interpolate_color(base, highlight, mode, t);
109
110 let cell = &mut buf[(area.x + col, area.y + row)];
111 cell.set_char('█');
112 cell.set_style(Style::default().fg(fg));
113 }
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 fn render_block(elapsed_ms: u64, width: u16, height: u16) -> Buffer {
122 let area = Rect::new(0, 0, width, height);
123 let mut buf = Buffer::empty(area);
124
125 SkeletonBlock::new(elapsed_ms).render(area, &mut buf);
126
127 buf
128 }
129
130 #[test]
131 fn fills_all_cells() {
132 let buf = render_block(1000, 10, 3);
133
134 for y in 0..3 {
135 for x in 0..10 {
136 assert_eq!(buf[(x, y)].symbol(), "█");
137 }
138 }
139 }
140
141 #[test]
142 fn empty_area_is_noop() {
143 let area = Rect::new(0, 0, 0, 0);
144 let mut buf = Buffer::empty(Rect::new(0, 0, 1, 1));
145 let expected = buf.clone();
146
147 SkeletonBlock::new(0).render(area, &mut buf);
148
149 assert_eq!(buf, expected);
150 }
151
152 #[test]
153 fn custom_colors_applied() {
154 let area = Rect::new(0, 0, 5, 1);
155 let mut buf = Buffer::empty(area);
156
157 SkeletonBlock::new(0)
158 .base(Color::Rgb(10, 20, 30))
159 .highlight(Color::Rgb(200, 200, 200))
160 .render(area, &mut buf);
161
162 for x in 0..5 {
164 let style = buf[(x, 0u16)].style();
165 assert_eq!(style.fg, Some(Color::Rgb(10, 20, 30)));
166 }
167 }
168}