throbber_widgets_tui/widgets/
throbber.rs1use alloc::string::String;
2
3#[cfg(feature = "rand")]
4use rand::Rng as _;
5
6#[derive(Debug, Clone, Default, PartialEq, Eq)]
8pub struct ThrobberState {
9 index: i8,
13}
14
15impl ThrobberState {
16 pub fn index(&self) -> i8 {
18 self.index
19 }
20
21 pub fn calc_next(&mut self) {
31 self.calc_step(1);
32 }
33
34 pub fn calc_step(&mut self, step: i8) {
52 self.index = if step == 0 {
53 #[cfg(feature = "rand")]
54 {
55 let mut rng = rand::rng();
56 rng.random()
57 }
58 #[cfg(all(not(feature = "rand"), feature = "std"))]
59 {
60 let duration = std::time::SystemTime::now()
61 .duration_since(std::time::UNIX_EPOCH)
62 .unwrap_or_default();
63 (duration.as_nanos() % 0x100
65 + duration.as_micros() % 0x100
66 + duration.as_millis() % 0x100) as i8
67 }
68 #[cfg(all(not(feature = "rand"), not(feature = "std")))]
69 {
70 self.index
73 }
74 } else {
75 self.index.checked_add(step).unwrap_or(0)
76 }
77 }
78
79 pub fn normalize(&mut self, throbber: &Throbber) {
104 let len = throbber.throbber_set.symbols.len() as i8;
105 if len <= 0 {
106 } else {
108 self.index %= len;
109 if self.index < 0 {
110 self.index += len;
112 }
113 }
114 }
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct Throbber<'a> {
138 label: Option<ratatui::text::Span<'a>>,
139 style: ratatui::style::Style,
140 throbber_style: ratatui::style::Style,
141 throbber_set: crate::symbols::throbber::Set,
142 use_type: crate::symbols::throbber::WhichUse,
143}
144
145impl Default for Throbber<'_> {
146 fn default() -> Self {
147 Self {
148 label: None,
149 style: ratatui::style::Style::default(),
150 throbber_style: ratatui::style::Style::default(),
151 throbber_set: crate::symbols::throbber::BRAILLE_SIX,
152 use_type: crate::symbols::throbber::WhichUse::Spin,
153 }
154 }
155}
156
157impl<'a> Throbber<'a> {
158 pub fn label<T>(mut self, label: T) -> Self
159 where
160 T: Into<ratatui::text::Span<'a>>,
161 {
162 self.label = Some(label.into());
163 self
164 }
165
166 pub fn style(mut self, style: ratatui::style::Style) -> Self {
167 self.style = style;
168 self
169 }
170
171 pub fn throbber_style(mut self, style: ratatui::style::Style) -> Self {
172 self.throbber_style = style;
173 self
174 }
175
176 pub fn throbber_set(mut self, set: crate::symbols::throbber::Set) -> Self {
177 self.throbber_set = set;
178 self
179 }
180
181 pub fn use_type(mut self, use_type: crate::symbols::throbber::WhichUse) -> Self {
182 self.use_type = use_type;
183 self
184 }
185
186 pub fn to_symbol_span(&self, state: &ThrobberState) -> ratatui::text::Span<'a> {
188 let symbol = match self.use_type {
189 crate::symbols::throbber::WhichUse::Full => self.throbber_set.full,
190 crate::symbols::throbber::WhichUse::Empty => self.throbber_set.empty,
191 crate::symbols::throbber::WhichUse::Spin => {
192 let mut state = state.clone();
193 state.normalize(self);
194 let len = self.throbber_set.symbols.len() as i8;
195 if 0 <= state.index && state.index < len {
196 self.throbber_set.symbols[state.index as usize]
197 } else {
198 self.throbber_set.empty
199 }
200 }
201 };
202 let mut symbol_text = String::from(symbol);
203 symbol_text.push(' ');
204 let symbol_span =
205 ratatui::text::Span::styled(symbol_text, self.style).patch_style(self.throbber_style);
206 symbol_span
207 }
208
209 pub fn to_line(&self, state: &ThrobberState) -> ratatui::text::Line<'a> {
211 let mut line = ratatui::text::Line::default().style(self.style);
212 line.spans.push(self.to_symbol_span(state));
213 if let Some(label) = &self.label.clone() {
214 line.spans.push(label.clone());
215 }
216 line
217 }
218}
219
220impl ratatui::widgets::Widget for Throbber<'_> {
221 fn render(self, area: ratatui::layout::Rect, buf: &mut ratatui::buffer::Buffer) {
223 let mut state = ThrobberState::default();
224 state.calc_step(0);
225 ratatui::widgets::StatefulWidget::render(self, area, buf, &mut state);
226 }
227}
228
229impl ratatui::widgets::StatefulWidget for Throbber<'_> {
230 type State = ThrobberState;
231
232 fn render(
234 self,
235 area: ratatui::layout::Rect,
236 buf: &mut ratatui::buffer::Buffer,
237 state: &mut Self::State,
238 ) {
239 buf.set_style(area, self.style);
240
241 let throbber_area = area;
242 if throbber_area.height < 1 {
243 return;
244 }
245
246 let symbol = match self.use_type {
248 crate::symbols::throbber::WhichUse::Full => self.throbber_set.full,
249 crate::symbols::throbber::WhichUse::Empty => self.throbber_set.empty,
250 crate::symbols::throbber::WhichUse::Spin => {
251 state.normalize(&self);
252 let len = self.throbber_set.symbols.len() as i8;
253 if 0 <= state.index && state.index < len {
254 self.throbber_set.symbols[state.index as usize]
255 } else {
256 self.throbber_set.empty
257 }
258 }
259 };
260 let mut symbol_text = String::from(symbol);
261 symbol_text.push(' ');
262 let symbol_span = ratatui::text::Span::styled(symbol_text, self.throbber_style);
263 let (col, row) = buf.set_span(
264 throbber_area.left(),
265 throbber_area.top(),
266 &symbol_span,
267 symbol_span.width() as u16,
268 );
269
270 if let Some(label) = self.label {
272 if throbber_area.right() <= col {
273 return;
274 }
275 buf.set_span(col, row, &label, label.width() as u16);
276 }
277 }
278}
279
280impl<'a> From<Throbber<'a>> for ratatui::text::Span<'a> {
284 fn from(throbber: Throbber<'a>) -> ratatui::text::Span<'a> {
285 let mut state = ThrobberState::default();
286 state.calc_step(0);
287 throbber.to_symbol_span(&state)
288 }
289}
290
291impl<'a> From<Throbber<'a>> for ratatui::text::Line<'a> {
295 fn from(throbber: Throbber<'a>) -> ratatui::text::Line<'a> {
296 let mut state = ThrobberState::default();
297 state.calc_step(0);
298 throbber.to_line(&state)
299 }
300}
301
302#[cfg(test)]
303mod tests {
304 use super::*;
305 #[test]
306 #[allow(unused_variables, unused_assignments)]
307 fn throbber_state_calc_step() {
308 let mut throbber_state = ThrobberState::default();
309 assert_eq!(throbber_state.index(), 0);
310
311 let mut difference = false;
314 for _ in 0..100 {
315 throbber_state.calc_step(0);
316 assert!((i8::MIN..=i8::MAX).contains(&throbber_state.index()));
317
318 if 0 != throbber_state.index() {
319 difference = true;
320 }
321 }
322 #[cfg(any(feature = "rand", feature = "std"))]
324 assert!(difference);
325 }
326
327 #[test]
328 fn throbber_state_normalize() {
329 let mut throbber_state = ThrobberState::default();
330 let throbber = Throbber::default();
331 let len = throbber.throbber_set.symbols.len() as i8;
332 let max = len - 1;
333
334 throbber_state.calc_step(max);
336 throbber_state.normalize(&throbber);
337 assert_eq!(throbber_state.index(), max);
338
339 throbber_state.calc_next();
341 throbber_state.normalize(&throbber);
342 assert_eq!(throbber_state.index(), 0);
343
344 throbber_state.calc_step(-1);
346 throbber_state.normalize(&throbber);
347 assert_eq!(throbber_state.index(), max);
348
349 throbber_state.calc_step(len * -2);
351 throbber_state.normalize(&throbber);
352 assert_eq!(throbber_state.index(), max);
353 }
354
355 #[test]
356 fn throbber_converts_to_span() {
357 let throbber = Throbber::default().use_type(crate::symbols::throbber::WhichUse::Full);
358 let span: ratatui::text::Span = throbber.into();
359 assert_eq!(span.content, "⠿ ");
360 }
361
362 #[test]
363 fn throbber_converts_to_line() {
364 let throbber = Throbber::default().use_type(crate::symbols::throbber::WhichUse::Full);
365 let line: ratatui::text::Line = throbber.into();
366 assert_eq!(line.spans[0].content, "⠿ ");
367 }
368
369 #[test]
370 fn throbber_reaches_upper_limit_step_resets_to_zero() {
371 let mut throbber_state = ThrobberState::default();
372
373 for _ in 0..i8::MAX {
374 throbber_state.calc_next();
375 }
376 throbber_state.calc_next();
377 assert!(throbber_state.index() != i8::MAX);
378 }
379}