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 braille: bool,
21 base: Color,
22 highlight: Color,
23 line_widths: &'a [f32],
24 block: Option<ratatui_widgets::block::Block<'a>>,
25}
26
27impl<'a> SkeletonText<'a> {
28 pub fn new(elapsed_ms: u64) -> Self {
29 Self {
30 elapsed_ms,
31 mode: AnimationMode::default(),
32 braille: false,
33 base: defaults::BASE,
34 highlight: defaults::HIGHLIGHT,
35 line_widths: &DEFAULT_LINE_WIDTHS,
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 {
46 self.braille = braille;
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 line_widths(mut self, widths: &'a [f32]) -> Self {
62 self.line_widths = widths;
63 self
64 }
65
66 pub fn block(mut self, block: ratatui_widgets::block::Block<'a>) -> Self {
67 self.block = Some(block);
68 self
69 }
70}
71
72impl Widget for SkeletonText<'_> {
73 fn render(self, area: Rect, buf: &mut Buffer) {
74 let inner = if let Some(ref block) = self.block {
75 let inner_area = block.inner(area);
76 block.render(area, buf);
77 inner_area
78 } else {
79 area
80 };
81
82 if inner.is_empty() || self.line_widths.is_empty() {
83 return;
84 }
85
86 let widths = self.line_widths;
87 let total_width = inner.width;
88
89 render_skeleton_cells(
90 inner,
91 buf,
92 self.mode,
93 self.braille,
94 self.elapsed_ms,
95 self.base,
96 self.highlight,
97 |row, col, _width| {
98 let frac = widths[row as usize % widths.len()].clamp(0.0, 1.0);
99 let line_width = (total_width as f32 * frac) as u16;
100 col < line_width
101 },
102 );
103 }
104}
105
106#[cfg(feature = "pantry")]
107#[path = "text.ingredient.rs"]
108pub mod ingredient;
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn default_pattern_varies_width() {
116 let area = Rect::new(0, 0, 20, 5);
117 let mut buf = Buffer::empty(area);
118
119 SkeletonText::new(1000).render(area, &mut buf);
120
121 assert_eq!(buf[(19, 0)].symbol(), "█");
123
124 assert_eq!(buf[(15, 2)].symbol(), "█");
126 assert_eq!(buf[(16, 2)].symbol(), " ");
127
128 assert_eq!(buf[(11, 4)].symbol(), "█");
130 assert_eq!(buf[(12, 4)].symbol(), " ");
131 }
132
133 #[test]
134 fn custom_line_widths() {
135 let area = Rect::new(0, 0, 10, 2);
136 let mut buf = Buffer::empty(area);
137
138 SkeletonText::new(1000)
139 .line_widths(&[0.5, 1.0])
140 .render(area, &mut buf);
141
142 assert_eq!(buf[(4, 0)].symbol(), "█");
144 assert_eq!(buf[(5, 0)].symbol(), " ");
145
146 assert_eq!(buf[(9, 1)].symbol(), "█");
148 }
149}