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    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    /// Per-line width fractions (`0.0..=1.0`), cycling for longer areas.
61    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        // Line 0: 100% → col 19 filled.
122        assert_eq!(buf[(19, 0)].symbol(), "█");
123
124        // Line 2: 80% → col 16 empty (80% of 20 = 16).
125        assert_eq!(buf[(15, 2)].symbol(), "█");
126        assert_eq!(buf[(16, 2)].symbol(), " ");
127
128        // Line 4: 60% → col 12 empty (60% of 20 = 12).
129        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        // Line 0: 50% → col 5 empty.
143        assert_eq!(buf[(4, 0)].symbol(), "█");
144        assert_eq!(buf[(5, 0)].symbol(), " ");
145
146        // Line 1: 100% → col 9 filled.
147        assert_eq!(buf[(9, 1)].symbol(), "█");
148    }
149}