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