tui_realm_stdlib/components/
spinner.rs1use tuirealm::command::{Cmd, CmdResult};
6use tuirealm::props::{Alignment, AttrValue, Attribute, Color, Props, Style};
7use tuirealm::ratatui::text::Line as Spans;
8use tuirealm::ratatui::{
9 layout::Rect,
10 text::{Span as TuiSpan, Text},
11 widgets::Paragraph,
12};
13use tuirealm::{Frame, MockComponent, State};
14
15#[derive(Default)]
18pub struct SpinnerStates {
19 pub sequence: Vec<char>,
20 pub step: usize,
21}
22
23impl SpinnerStates {
24 pub fn reset(&mut self, sequence: &str) {
28 self.sequence = sequence.chars().collect();
29 self.step = 0;
30 }
31
32 pub fn step(&mut self) -> char {
36 let ch = self.sequence.get(self.step).copied().unwrap_or(' ');
37 if self.step + 1 >= self.sequence.len() {
39 self.step = 0;
40 } else {
41 self.step += 1;
42 }
43 ch
44 }
45
46 pub fn current_step(&self) -> char {
50 self.sequence.get(self.step).copied().unwrap_or(' ')
51 }
52}
53
54#[must_use]
60pub struct Spinner {
61 props: Props,
62 pub states: SpinnerStates,
63 pub view_auto_step: bool,
67}
68
69impl Default for Spinner {
70 fn default() -> Self {
71 Self {
72 props: Default::default(),
73 states: Default::default(),
74 view_auto_step: true,
75 }
76 }
77}
78
79impl Spinner {
80 pub fn foreground(mut self, fg: Color) -> Self {
81 self.attr(Attribute::Foreground, AttrValue::Color(fg));
82 self
83 }
84
85 pub fn background(mut self, bg: Color) -> Self {
86 self.attr(Attribute::Background, AttrValue::Color(bg));
87 self
88 }
89
90 pub fn sequence<S: Into<String>>(mut self, s: S) -> Self {
91 self.attr(Attribute::Text, AttrValue::String(s.into()));
92 self
93 }
94
95 pub fn manual_step(mut self) -> Self {
97 self.view_auto_step = false;
98 self
99 }
100}
101
102impl MockComponent for Spinner {
103 fn view(&mut self, render: &mut Frame, area: Rect) {
104 if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) == AttrValue::Flag(true) {
106 let foreground = self
108 .props
109 .get_or(Attribute::Foreground, AttrValue::Color(Color::Reset))
110 .unwrap_color();
111 let background = self
112 .props
113 .get_or(Attribute::Background, AttrValue::Color(Color::Reset))
114 .unwrap_color();
115 let seq_char = if self.view_auto_step {
117 self.states.step()
118 } else {
119 self.states.current_step()
120 };
121 let text: Text = Text::from(Spans::from(TuiSpan::from(seq_char.to_string())));
122 render.render_widget(
123 Paragraph::new(text)
124 .alignment(Alignment::Left)
125 .style(Style::default().bg(background).fg(foreground)),
126 area,
127 );
128 }
129 }
130
131 fn query(&self, attr: Attribute) -> Option<AttrValue> {
132 self.props.get(attr)
133 }
134
135 fn attr(&mut self, attr: Attribute, value: AttrValue) {
136 if matches!(attr, Attribute::Text) {
137 self.states.reset(value.unwrap_string().as_str());
139 } else {
140 self.props.set(attr, value);
141 }
142 }
143
144 fn state(&self) -> State {
145 State::None
146 }
147
148 fn perform(&mut self, _cmd: Cmd) -> CmdResult {
149 CmdResult::None
150 }
151}
152
153#[cfg(test)]
154mod tests {
155
156 use super::*;
157
158 use pretty_assertions::assert_eq;
159 use tuirealm::ratatui::{self};
160
161 #[test]
162 fn test_components_span() {
163 let component = Spinner::default()
164 .background(Color::Blue)
165 .foreground(Color::Red)
166 .sequence("⣾⣽⣻⢿⡿⣟⣯⣷");
167 assert_eq!(component.state(), State::None);
169 }
170
171 #[test]
172 fn should_step_in_view() {
173 let mut component = Spinner::default().sequence("123");
174
175 assert_eq!(component.states.step, 0);
176
177 let mut terminal =
178 ratatui::Terminal::new(ratatui::backend::TestBackend::new(16, 16)).unwrap();
179
180 terminal
181 .draw(|f| {
182 component.view(f, f.area());
183 assert_eq!(component.states.step, 1);
184 })
185 .unwrap();
186
187 terminal
188 .draw(|f| {
189 component.view(f, f.area());
190 assert_eq!(component.states.step, 2);
191 })
192 .unwrap();
193
194 terminal
195 .draw(|f| {
196 component.view(f, f.area());
197 assert_eq!(component.states.step, 0);
198 })
199 .unwrap();
200 }
201
202 #[test]
203 fn should_not_step_in_view() {
204 let mut component = Spinner::default().sequence("123").manual_step();
205
206 assert_eq!(component.states.step, 0);
207
208 let mut terminal =
209 ratatui::Terminal::new(ratatui::backend::TestBackend::new(16, 16)).unwrap();
210
211 terminal
212 .draw(|f| {
213 component.view(f, f.area());
214 assert_eq!(component.states.step, 0);
215 })
216 .unwrap();
217
218 component.states.step();
219
220 terminal
221 .draw(|f| {
222 component.view(f, f.area());
223 assert_eq!(component.states.step, 1);
224 })
225 .unwrap();
226
227 component.states.step();
228
229 terminal
230 .draw(|f| {
231 component.view(f, f.area());
232 assert_eq!(component.states.step, 2);
233 })
234 .unwrap();
235
236 component.states.step();
237
238 terminal
239 .draw(|f| {
240 component.view(f, f.area());
241 assert_eq!(component.states.step, 0);
242 })
243 .unwrap();
244 }
245}