Skip to main content

tui_skeleton/
text.rs

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
7/// Default paragraph simulation: two full lines, one shorter, repeat.
8const DEFAULT_LINE_WIDTHS: [f32; 5] = [1.0, 1.0, 0.80, 1.0, 0.60];
9
10/// Skeleton paragraph with lines of varying width.
11///
12/// Simulates a block of text by rendering `█` characters at different
13/// widths per line. The pattern cycles when the area has more rows
14/// than entries in `line_widths`.
15#[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    /// Per-line width fractions (`0.0..=1.0`), cycling for longer areas.
54    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        // Line 0: 100% → col 19 filled.
114        assert_eq!(buf[(19, 0)].symbol(), "█");
115
116        // Line 2: 80% → col 16 empty (80% of 20 = 16).
117        assert_eq!(buf[(15, 2)].symbol(), "█");
118        assert_eq!(buf[(16, 2)].symbol(), " ");
119
120        // Line 4: 60% → col 12 empty (60% of 20 = 12).
121        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        // Line 0: 50% → col 5 empty.
135        assert_eq!(buf[(4, 0)].symbol(), "█");
136        assert_eq!(buf[(5, 0)].symbol(), " ");
137
138        // Line 1: 100% → col 9 filled.
139        assert_eq!(buf[(9, 1)].symbol(), "█");
140    }
141}