tui/components/
spinner.rs1use crate::components::{Component, Event, ViewContext};
2use crate::line::Line;
3use crate::rendering::frame::Frame;
4
5pub const BRAILLE_FRAMES: &[char] = &['⠒', '⠮', '⠷', '⢷', '⡾', '⣯', '⣽', '⣿', '⣭', '⢯'];
6
7pub struct Spinner {
8 tick: u16,
9 pub visible: bool,
10 frames: &'static [char],
11}
12
13impl Spinner {
14 pub fn new(frames: &'static [char]) -> Self {
15 Self { tick: 0, visible: false, frames }
16 }
17
18 pub fn braille() -> Self {
19 Self::new(BRAILLE_FRAMES)
20 }
21
22 pub fn current_frame(&self) -> char {
23 self.frames[self.frame_index()]
24 }
25
26 pub fn frame_index(&self) -> usize {
27 self.tick as usize % self.frames.len()
28 }
29
30 pub fn reset(&mut self) {
31 self.tick = 0;
32 self.visible = true;
33 }
34
35 #[allow(dead_code)]
36 pub fn set_tick(&mut self, tick: u16) {
37 self.tick = tick;
38 }
39
40 pub fn on_tick(&mut self) {
42 if self.visible {
43 self.tick = self.tick.wrapping_add(1);
44 }
45 }
46}
47
48impl Component for Spinner {
49 type Message = ();
50
51 async fn on_event(&mut self, event: &Event) -> Option<Vec<Self::Message>> {
52 match event {
53 Event::Tick => {
54 self.on_tick();
55 Some(vec![])
56 }
57 _ => None,
58 }
59 }
60
61 fn render(&mut self, context: &ViewContext) -> Frame {
62 if !self.visible {
63 return Frame::new(vec![]);
64 }
65
66 let ch = self.current_frame();
67 let mut line = Line::default();
68 line.push_styled(ch.to_string(), context.theme.info());
69 Frame::new(vec![line])
70 }
71}
72
73impl Default for Spinner {
74 fn default() -> Self {
75 Self::braille()
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82
83 #[test]
84 fn custom_frames() {
85 static CUSTOM: &[char] = &['|', '/', '-', '\\'];
86 let mut spinner = Spinner::new(CUSTOM);
87 spinner.set_tick(1);
88 spinner.visible = true;
89 assert_eq!(spinner.current_frame(), '/');
90 }
91
92 #[test]
93 fn on_tick_advances_when_visible() {
94 let mut spinner = Spinner { visible: true, ..Spinner::default() };
95 spinner.on_tick();
96 assert_eq!(spinner.current_frame(), BRAILLE_FRAMES[1]);
97 }
98
99 #[test]
100 fn on_tick_noop_when_invisible() {
101 let mut spinner = Spinner::default();
102 spinner.on_tick();
103 assert_eq!(spinner.current_frame(), BRAILLE_FRAMES[0]);
104 }
105
106 #[test]
107 fn reset_sets_tick_zero_and_visible() {
108 let mut spinner = Spinner::default();
109 spinner.set_tick(5);
110 spinner.visible = false;
111 spinner.reset();
112 assert!(spinner.visible);
113 assert_eq!(spinner.current_frame(), BRAILLE_FRAMES[0]);
114 }
115}