1use ratatui_core::{
2 buffer::Buffer,
3 layout::Rect,
4 style::{Color, Style},
5 widgets::Widget,
6};
7
8use crate::animation::{AnimationMode, cell_glyph, cell_intensity, interpolate_color, is_uniform};
9use crate::defaults;
10
11#[must_use]
18#[derive(Debug, Clone)]
19pub struct SkeletonBlock<'a> {
20 elapsed_ms: u64,
21 mode: AnimationMode,
22 braille: bool,
23 base: Color,
24 highlight: Color,
25 block: Option<ratatui_widgets::block::Block<'a>>,
26}
27
28impl<'a> SkeletonBlock<'a> {
29 pub fn new(elapsed_ms: u64) -> Self {
30 Self {
31 elapsed_ms,
32 mode: AnimationMode::default(),
33 braille: false,
34 base: defaults::BASE,
35 highlight: defaults::HIGHLIGHT,
36 block: None,
37 }
38 }
39
40 pub fn mode(mut self, mode: AnimationMode) -> Self {
41 self.mode = mode;
42 self
43 }
44
45 pub fn braille(mut self, braille: bool) -> Self {
47 self.braille = braille;
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 block(mut self, block: ratatui_widgets::block::Block<'a>) -> Self {
62 self.block = Some(block);
63 self
64 }
65}
66
67impl Widget for SkeletonBlock<'_> {
68 fn render(self, area: Rect, buf: &mut Buffer) {
69 let inner = if let Some(ref block) = self.block {
70 let inner_area = block.inner(area);
71 block.render(area, buf);
72 inner_area
73 } else {
74 area
75 };
76
77 if inner.is_empty() {
78 return;
79 }
80
81 render_skeleton_cells(
82 inner,
83 buf,
84 self.mode,
85 self.braille,
86 self.elapsed_ms,
87 self.base,
88 self.highlight,
89 |_row, col, width| col < width,
90 );
91 }
92}
93
94#[expect(clippy::too_many_arguments)]
99pub(crate) fn render_skeleton_cells(
100 area: Rect,
101 buf: &mut Buffer,
102 mode: AnimationMode,
103 braille: bool,
104 elapsed_ms: u64,
105 base: Color,
106 highlight: Color,
107 visible: impl Fn(u16, u16, u16) -> bool,
108) {
109 let uniform_t = is_uniform(mode).then(|| cell_intensity(mode, elapsed_ms, 0, area.width));
110
111 for row in 0..area.height {
112 for col in 0..area.width {
113 if !visible(row, col, area.width) {
114 continue;
115 }
116
117 let t = uniform_t.unwrap_or_else(|| cell_intensity(mode, elapsed_ms, col, area.width));
118 let fg = interpolate_color(base, highlight, mode, t);
119 let ch = cell_glyph(braille, mode, elapsed_ms, row, col);
120
121 let cell = &mut buf[(area.x + col, area.y + row)];
122 cell.set_char(ch);
123 cell.set_style(Style::default().fg(fg));
124 }
125 }
126}
127
128#[cfg(feature = "pantry")]
129#[path = "block.ingredient.rs"]
130pub mod ingredient;
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 fn render_block(elapsed_ms: u64, width: u16, height: u16) -> Buffer {
137 let area = Rect::new(0, 0, width, height);
138 let mut buf = Buffer::empty(area);
139
140 SkeletonBlock::new(elapsed_ms).render(area, &mut buf);
141
142 buf
143 }
144
145 #[test]
146 fn fills_all_cells() {
147 let buf = render_block(1000, 10, 3);
148
149 for y in 0..3 {
150 for x in 0..10 {
151 assert_eq!(buf[(x, y)].symbol(), "█");
152 }
153 }
154 }
155
156 #[test]
157 fn noise_mode_fills_random_braille() {
158 let area = Rect::new(0, 0, 10, 3);
159 let mut buf = Buffer::empty(area);
160
161 SkeletonBlock::new(1000)
162 .mode(AnimationMode::Noise)
163 .render(area, &mut buf);
164
165 for y in 0..3u16 {
166 for x in 0..10u16 {
167 let ch = buf[(x, y)].symbol().chars().next().unwrap();
168 assert!((0x2800..=0x28FF).contains(&(ch as u32)));
169 }
170 }
171 }
172
173 #[test]
174 fn braille_flag_fills_solid_braille() {
175 let area = Rect::new(0, 0, 10, 3);
176 let mut buf = Buffer::empty(area);
177
178 SkeletonBlock::new(1000)
179 .braille(true)
180 .render(area, &mut buf);
181
182 for y in 0..3u16 {
183 for x in 0..10u16 {
184 assert_eq!(buf[(x, y)].symbol(), "⣿");
185 }
186 }
187 }
188
189 #[test]
190 fn empty_area_is_noop() {
191 let area = Rect::new(0, 0, 0, 0);
192 let mut buf = Buffer::empty(Rect::new(0, 0, 1, 1));
193 let expected = buf.clone();
194
195 SkeletonBlock::new(0).render(area, &mut buf);
196
197 assert_eq!(buf, expected);
198 }
199
200 #[test]
201 fn custom_colors_applied() {
202 let area = Rect::new(0, 0, 5, 1);
203 let mut buf = Buffer::empty(area);
204
205 SkeletonBlock::new(0)
206 .base(Color::Rgb(10, 20, 30))
207 .highlight(Color::Rgb(200, 200, 200))
208 .render(area, &mut buf);
209
210 for x in 0..5 {
212 let style = buf[(x, 0u16)].style();
213 assert_eq!(style.fg, Some(Color::Rgb(10, 20, 30)));
214 }
215 }
216}