1use ratatui_core::{buffer::Buffer, layout::Rect, style::Color, widgets::Widget};
2
3use crate::animation::AnimationMode;
4use crate::block::render_skeleton_cells;
5use crate::defaults;
6
7const DEFAULT_LINE_WIDTHS: [f32; 5] = [1.0, 1.0, 0.80, 1.0, 0.60];
9
10#[must_use]
16#[derive(Debug, Clone)]
17pub struct SkeletonText<'a> {
18 elapsed_ms: u64,
19 mode: AnimationMode,
20 base: Color,
21 highlight: Color,
22 line_widths: &'a [f32],
23 block: Option<ratatui_widgets::block::Block<'a>>,
24}
25
26impl<'a> SkeletonText<'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 line_widths: &DEFAULT_LINE_WIDTHS,
34 block: None,
35 }
36 }
37
38 pub fn mode(mut self, mode: AnimationMode) -> Self {
39 self.mode = mode;
40 self
41 }
42
43 pub fn base(mut self, color: impl Into<Color>) -> Self {
44 self.base = color.into();
45 self
46 }
47
48 pub fn highlight(mut self, color: impl Into<Color>) -> Self {
49 self.highlight = color.into();
50 self
51 }
52
53 pub fn line_widths(mut self, widths: &'a [f32]) -> Self {
55 self.line_widths = widths;
56 self
57 }
58
59 pub fn block(mut self, block: ratatui_widgets::block::Block<'a>) -> Self {
60 self.block = Some(block);
61 self
62 }
63}
64
65impl Widget for SkeletonText<'_> {
66 fn render(self, area: Rect, buf: &mut Buffer) {
67 let inner = if let Some(ref block) = self.block {
68 let inner_area = block.inner(area);
69 block.render(area, buf);
70 inner_area
71 } else {
72 area
73 };
74
75 if inner.is_empty() || self.line_widths.is_empty() {
76 return;
77 }
78
79 let widths = self.line_widths;
80 let total_width = inner.width;
81
82 render_skeleton_cells(
83 inner,
84 buf,
85 self.mode,
86 self.elapsed_ms,
87 self.base,
88 self.highlight,
89 |row, col, _width| {
90 let frac = widths[row as usize % widths.len()].clamp(0.0, 1.0);
91 let line_width = (total_width as f32 * frac) as u16;
92 col < line_width
93 },
94 );
95 }
96}
97
98#[cfg(feature = "pantry")]
99#[path = "text.ingredient.rs"]
100pub mod ingredient;
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn default_pattern_varies_width() {
108 let area = Rect::new(0, 0, 20, 5);
109 let mut buf = Buffer::empty(area);
110
111 SkeletonText::new(1000).render(area, &mut buf);
112
113 assert_eq!(buf[(19, 0)].symbol(), "█");
115
116 assert_eq!(buf[(15, 2)].symbol(), "█");
118 assert_eq!(buf[(16, 2)].symbol(), " ");
119
120 assert_eq!(buf[(11, 4)].symbol(), "█");
122 assert_eq!(buf[(12, 4)].symbol(), " ");
123 }
124
125 #[test]
126 fn custom_line_widths() {
127 let area = Rect::new(0, 0, 10, 2);
128 let mut buf = Buffer::empty(area);
129
130 SkeletonText::new(1000)
131 .line_widths(&[0.5, 1.0])
132 .render(area, &mut buf);
133
134 assert_eq!(buf[(4, 0)].symbol(), "█");
136 assert_eq!(buf[(5, 0)].symbol(), " ");
137
138 assert_eq!(buf[(9, 1)].symbol(), "█");
140 }
141}