throbber_widgets_tui/widgets/
throbber.rs1use rand::Rng as _;
2
3#[derive(Debug, Clone, Default)]
5pub struct ThrobberState {
6 index: i8,
10}
11
12impl ThrobberState {
13 pub fn index(&self) -> i8 {
15 self.index
16 }
17
18 pub fn calc_next(&mut self) {
28 self.calc_step(1);
29 }
30
31 pub fn calc_step(&mut self, step: i8) {
49 self.index = if step == 0 {
50 let mut rng = rand::thread_rng();
51 rng.gen()
52 } else {
53 self.index.checked_add(step).unwrap_or(0)
54 }
55 }
56
57 pub fn normalize(&mut self, throbber: &Throbber) {
82 let len = throbber.throbber_set.symbols.len() as i8;
83 if len <= 0 {
84 } else {
86 self.index %= len;
87 if self.index < 0 {
88 self.index += len;
90 }
91 }
92 }
93}
94
95#[derive(Debug, Clone)]
115pub struct Throbber<'a> {
116 label: Option<ratatui::text::Span<'a>>,
117 style: ratatui::style::Style,
118 throbber_style: ratatui::style::Style,
119 throbber_set: crate::symbols::throbber::Set,
120 use_type: crate::symbols::throbber::WhichUse,
121}
122
123impl<'a> Default for Throbber<'a> {
124 fn default() -> Self {
125 Self {
126 label: None,
127 style: ratatui::style::Style::default(),
128 throbber_style: ratatui::style::Style::default(),
129 throbber_set: crate::symbols::throbber::BRAILLE_SIX,
130 use_type: crate::symbols::throbber::WhichUse::Spin,
131 }
132 }
133}
134
135impl<'a> Throbber<'a> {
136 pub fn label<T>(mut self, label: T) -> Self
137 where
138 T: Into<ratatui::text::Span<'a>>,
139 {
140 self.label = Some(label.into());
141 self
142 }
143
144 pub fn style(mut self, style: ratatui::style::Style) -> Self {
145 self.style = style;
146 self
147 }
148
149 pub fn throbber_style(mut self, style: ratatui::style::Style) -> Self {
150 self.throbber_style = style;
151 self
152 }
153
154 pub fn throbber_set(mut self, set: crate::symbols::throbber::Set) -> Self {
155 self.throbber_set = set;
156 self
157 }
158
159 pub fn use_type(mut self, use_type: crate::symbols::throbber::WhichUse) -> Self {
160 self.use_type = use_type;
161 self
162 }
163
164 pub fn to_symbol_span(&self, state: &ThrobberState) -> ratatui::text::Span<'a> {
166 let symbol = match self.use_type {
167 crate::symbols::throbber::WhichUse::Full => self.throbber_set.full,
168 crate::symbols::throbber::WhichUse::Empty => self.throbber_set.empty,
169 crate::symbols::throbber::WhichUse::Spin => {
170 let mut state = state.clone();
171 state.normalize(self);
172 let len = self.throbber_set.symbols.len() as i8;
173 if 0 <= state.index && state.index < len {
174 self.throbber_set.symbols[state.index as usize]
175 } else {
176 self.throbber_set.empty
177 }
178 }
179 };
180 let symbol_span = ratatui::text::Span::styled(format!("{} ", symbol), self.style)
181 .patch_style(self.throbber_style);
182 symbol_span
183 }
184
185 pub fn to_line(&self, state: &ThrobberState) -> ratatui::text::Line<'a> {
187 let mut line = ratatui::text::Line::default().style(self.style);
188 line.spans.push(self.to_symbol_span(state));
189 if let Some(label) = &self.label.clone() {
190 line.spans.push(label.clone());
191 }
192 line
193 }
194}
195
196impl<'a> ratatui::widgets::Widget for Throbber<'a> {
197 fn render(self, area: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) {
199 let mut state = ThrobberState::default();
200 state.calc_step(0);
201 ratatui::widgets::StatefulWidget::render(self, area, buf, &mut state);
202 }
203}
204
205impl<'a> ratatui::widgets::StatefulWidget for Throbber<'a> {
206 type State = ThrobberState;
207
208 fn render(
210 self,
211 area: ratatui::layout::Rect,
212 buf: &mut ratatui::buffer::Buffer,
213 state: &mut Self::State,
214 ) {
215 buf.set_style(area, self.style);
216
217 let throbber_area = area;
218 if throbber_area.height < 1 {
219 return;
220 }
221
222 let symbol = match self.use_type {
224 crate::symbols::throbber::WhichUse::Full => self.throbber_set.full,
225 crate::symbols::throbber::WhichUse::Empty => self.throbber_set.empty,
226 crate::symbols::throbber::WhichUse::Spin => {
227 state.normalize(&self);
228 let len = self.throbber_set.symbols.len() as i8;
229 if 0 <= state.index && state.index < len {
230 self.throbber_set.symbols[state.index as usize]
231 } else {
232 self.throbber_set.empty
233 }
234 }
235 };
236 let symbol_span = ratatui::text::Span::styled(format!("{} ", symbol), self.throbber_style);
237 let (col, row) = buf.set_span(
238 throbber_area.left(),
239 throbber_area.top(),
240 &symbol_span,
241 symbol_span.width() as u16,
242 );
243
244 if let Some(label) = self.label {
246 if throbber_area.right() <= col {
247 return;
248 }
249 buf.set_span(col, row, &label, label.width() as u16);
250 }
251 }
252}
253
254impl<'a> From<Throbber<'a>> for ratatui::text::Span<'a> {
258 fn from(throbber: Throbber<'a>) -> ratatui::text::Span<'a> {
259 let mut state = ThrobberState::default();
260 state.calc_step(0);
261 throbber.to_symbol_span(&state)
262 }
263}
264
265impl<'a> From<Throbber<'a>> for ratatui::text::Line<'a> {
269 fn from(throbber: Throbber<'a>) -> ratatui::text::Line<'a> {
270 let mut state = ThrobberState::default();
271 state.calc_step(0);
272 throbber.to_line(&state)
273 }
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279 #[test]
280 fn throbber_state_calc_step() {
281 let mut throbber_state = ThrobberState::default();
282 assert_eq!(throbber_state.index(), 0);
283
284 let mut difference = false;
287 for _ in 0..100 {
288 throbber_state.calc_step(0);
289 assert!((std::i8::MIN..=std::i8::MAX).contains(&throbber_state.index()));
290
291 if 0 != throbber_state.index() {
292 difference = true;
293 }
294 }
295 assert!(difference);
296 }
297
298 #[test]
299 fn throbber_state_normalize() {
300 let mut throbber_state = ThrobberState::default();
301 let throbber = Throbber::default();
302 let len = throbber.throbber_set.symbols.len() as i8;
303 let max = len - 1;
304
305 throbber_state.calc_step(max);
307 throbber_state.normalize(&throbber);
308 assert_eq!(throbber_state.index(), max);
309
310 throbber_state.calc_next();
312 throbber_state.normalize(&throbber);
313 assert_eq!(throbber_state.index(), 0);
314
315 throbber_state.calc_step(-1);
317 throbber_state.normalize(&throbber);
318 assert_eq!(throbber_state.index(), max);
319
320 throbber_state.calc_step(len * -2);
322 throbber_state.normalize(&throbber);
323 assert_eq!(throbber_state.index(), max);
324 }
325
326 #[test]
327 fn throbber_converts_to_span() {
328 let throbber = Throbber::default().use_type(crate::symbols::throbber::WhichUse::Full);
329 let span: ratatui::text::Span = throbber.into();
330 assert_eq!(span.content, "⠿ ");
331 }
332
333 #[test]
334 fn throbber_converts_to_line() {
335 let throbber = Throbber::default().use_type(crate::symbols::throbber::WhichUse::Full);
336 let line: ratatui::text::Line = throbber.into();
337 assert_eq!(line.spans[0].content, "⠿ ");
338 }
339
340 #[test]
341 fn throbber_reaches_upper_limit_step_resets_to_zero() {
342 let mut throbber_state = ThrobberState::default();
343
344 for _ in 0..i8::MAX {
345 throbber_state.calc_next();
346 }
347 throbber_state.calc_next();
348 assert!(throbber_state.index() != i8::MAX);
349 }
350}