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_WIDTHS: [f32; 5] = [0.45, 0.30, 0.55, 0.35, 0.50];
9
10#[must_use]
15#[derive(Debug, Clone)]
16pub struct SkeletonList<'a> {
17 elapsed_ms: u64,
18 mode: AnimationMode,
19 braille: bool,
20 base: Color,
21 highlight: Color,
22 items: u16,
23 widths: &'a [f32],
24 block: Option<ratatui_widgets::block::Block<'a>>,
25}
26
27impl<'a> SkeletonList<'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 items: 5,
36 widths: &DEFAULT_WIDTHS,
37 block: None,
38 }
39 }
40
41 pub fn mode(mut self, mode: AnimationMode) -> Self {
42 self.mode = mode;
43 self
44 }
45
46 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 items(mut self, items: u16) -> Self {
63 self.items = items;
64 self
65 }
66
67 pub fn widths(mut self, widths: &'a [f32]) -> Self {
71 self.widths = widths;
72 self
73 }
74
75 pub fn block(mut self, block: ratatui_widgets::block::Block<'a>) -> Self {
76 self.block = Some(block);
77 self
78 }
79}
80
81impl Widget for SkeletonList<'_> {
82 fn render(self, area: Rect, buf: &mut Buffer) {
83 let inner = if let Some(ref block) = self.block {
84 let inner_area = block.inner(area);
85 block.render(area, buf);
86 inner_area
87 } else {
88 area
89 };
90
91 if inner.is_empty() || self.widths.is_empty() {
92 return;
93 }
94
95 let rows_needed = self.items * 2;
97 let render_height = rows_needed.min(inner.height);
98 let widths = self.widths;
99 let total_width = inner.width;
100
101 render_skeleton_cells(
102 Rect::new(inner.x, inner.y, inner.width, render_height),
103 buf,
104 self.mode,
105 self.braille,
106 self.elapsed_ms,
107 self.base,
108 self.highlight,
109 |row, col, _width| {
110 if row % 2 == 1 {
112 return false;
113 }
114
115 let item_index = (row / 2) as usize;
116 let frac = widths[item_index % widths.len()].clamp(0.0, 1.0);
117 let item_width = (total_width as f32 * frac) as u16;
118 col < item_width
119 },
120 );
121 }
122}
123
124#[cfg(feature = "pantry")]
125#[path = "list.ingredient.rs"]
126pub mod ingredient;
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn items_have_gaps() {
134 let area = Rect::new(0, 0, 20, 6);
135 let mut buf = Buffer::empty(area);
136
137 SkeletonList::new(1000)
138 .items(3)
139 .widths(&[0.5, 0.5, 0.5])
140 .render(area, &mut buf);
141
142 assert_eq!(buf[(0, 0)].symbol(), "█");
144
145 assert_eq!(buf[(0, 1)].symbol(), " ");
147
148 assert_eq!(buf[(0, 2)].symbol(), "█");
150
151 assert_eq!(buf[(0, 3)].symbol(), " ");
153 }
154
155 #[test]
156 fn ragged_edges() {
157 let area = Rect::new(0, 0, 20, 6);
158 let mut buf = Buffer::empty(area);
159
160 SkeletonList::new(1000)
161 .items(3)
162 .widths(&[0.5, 0.3, 0.8])
163 .render(area, &mut buf);
164
165 assert_eq!(buf[(9, 0)].symbol(), "█");
167 assert_eq!(buf[(10, 0)].symbol(), " ");
168
169 assert_eq!(buf[(5, 2)].symbol(), "█");
171 assert_eq!(buf[(6, 2)].symbol(), " ");
172 }
173
174 #[test]
175 fn respects_item_limit() {
176 let area = Rect::new(0, 0, 10, 10);
177 let mut buf = Buffer::empty(area);
178
179 SkeletonList::new(1000).items(2).render(area, &mut buf);
180
181 assert_ne!(buf[(0, 0)].symbol(), " ");
183 assert_ne!(buf[(0, 2)].symbol(), " ");
184 assert_eq!(buf[(0, 4)].symbol(), " ");
185 }
186}