cursive_spinner_view/
view.rs1use std::sync::mpsc;
2use std::{sync::mpsc::Sender, time::Duration};
3
4#[cfg(test)]
5use std::thread::JoinHandle;
6
7use cursive_core::theme::StyleType;
8use cursive_core::views::TextView;
9use cursive_core::CbSink;
10use cursive_core::{views::TextContent, Printer, Vec2, View};
11
12use crate::{
13 spinner::Spinner, Frames, ACCCEL_FACTOR, DEFAULT_FRAMES, DEFAULT_IDLING_FRAME, MAX_FPS, MIN_FPS,
14};
15
16pub(crate) enum SpinnerControl {
17 Frames(Frames),
18 Duration(Duration),
19 Drop,
20}
21
22#[allow(missing_debug_implementations)]
24pub struct SpinnerView {
25 spin_ups: usize,
26 speeds: bool,
27 text_view: TextView,
28 tx_spinner: Sender<SpinnerControl>,
29
30 #[cfg(test)]
31 join_handle: Option<JoinHandle<()>>,
34}
35
36impl SpinnerView {
38 pub fn new(cb_sink: CbSink) -> Self {
52 let content = TextContent::new("");
53 let text_view = TextView::new_with_content(content.clone()).no_wrap();
54
55 let (tx_spinner, rx_spinner) = mpsc::channel();
56
57 let spinner = Spinner::new(
58 DEFAULT_FRAMES,
59 DEFAULT_IDLING_FRAME,
60 cb_sink.clone(),
61 content,
62 rx_spinner,
63 );
64
65 let _join_handle = spinner.spin_loop();
66
67 SpinnerView {
68 spin_ups: 0,
69 speeds: true, text_view,
71 tx_spinner,
72
73 #[cfg(test)]
74 join_handle: Some(_join_handle),
75 }
76 }
77
78 pub fn spin_up(&mut self) {
82 self.spin_ups = self.spin_ups.saturating_add(1);
83
84 self.recalc_duration();
85 }
86
87 pub fn spin_down(&mut self) {
92 self.spin_ups = self.spin_ups.saturating_sub(1);
93
94 self.recalc_duration();
95 }
96
97 pub fn stop(&mut self) {
99 self.spin_ups = 0;
100 self.recalc_duration();
101 }
102
103 pub fn spin_ups(&self) -> usize {
105 self.spin_ups
106 }
107
108 pub fn is_spinning(&self) -> bool {
110 self.spin_ups() != 0
111 }
112
113 pub fn frames(&mut self, frames: Frames) -> &mut Self {
115 self.tx_spinner
116 .send(SpinnerControl::Frames(frames))
117 .unwrap();
118
119 self
120 }
121
122 pub fn style<S: Into<StyleType>>(&mut self, style: S) -> &mut Self {
124 self.text_view.set_style(style);
125 self
126 }
127
128 fn recalc_duration(&self) {
129 let dur = match self.spin_ups() {
130 0 => Duration::ZERO,
131 spin_ups => Duration::from_secs_f32(1.0 / Self::fps(spin_ups, self.speeds) as f32),
132 };
133 self.tx_spinner.send(SpinnerControl::Duration(dur)).unwrap();
134 }
135
136 fn fps(spin_ups: usize, speeds: bool) -> usize {
137 if !speeds || spin_ups == 0 {
138 return MIN_FPS;
139 }
140
141 let fps = MIN_FPS.saturating_add(ACCCEL_FACTOR.saturating_mul(spin_ups - 1));
142
143 match fps {
144 fps if fps < MIN_FPS as usize => MIN_FPS,
145 fps if fps > MAX_FPS as usize => MAX_FPS,
146 _ => fps,
147 }
148 }
149
150 #[cfg(test)]
151 #[must_use]
152 fn join_handle(&mut self) -> JoinHandle<()> {
153 self.join_handle.take().unwrap()
154 }
155}
156
157impl Drop for SpinnerView {
158 fn drop(&mut self) {
159 let _ = self.tx_spinner.send(SpinnerControl::Drop);
160 }
161}
162
163impl View for SpinnerView {
164 fn draw(&self, printer: &Printer) {
165 self.text_view.draw(printer)
166 }
167
168 fn needs_relayout(&self) -> bool {
169 self.text_view.needs_relayout()
170 }
171
172 fn required_size(&mut self, constraint: Vec2) -> Vec2 {
173 self.text_view.required_size(constraint)
174 }
175
176 fn layout(&mut self, size: Vec2) {
177 self.text_view.layout(size)
178 }
179}
180
181#[cursive_core::blueprint(SpinnerView::new(cb_sink.into_inner()))]
182struct Blueprint {
183 cb_sink: cursive_core::builder::NoConfig<CbSink>,
184
185 #[blueprint(setter=style)]
186 style: Option<StyleType>,
187}
188
189#[cfg(test)]
190mod tests {
191 use std::thread;
192 use std::time::Duration;
193
194 use cursive;
195 use ntest::timeout;
196
197 use super::*;
198
199 #[test]
200 #[timeout(1000)]
201 fn drop_running_thread() {
202 let siv = cursive::default();
203 let mut spinner = SpinnerView::new(siv.cb_sink().clone());
204
205 spinner.spin_up();
206
207 thread::sleep(Duration::from_millis(10));
208
209 let handle = spinner.join_handle();
210 drop(spinner);
211 drop(siv);
212
213 assert!(matches!(handle.join(), Ok(())));
214 }
215
216 #[test]
217 #[timeout(1000)]
218 fn drop_sleeping_thread() {
219 let siv = cursive::default();
220
221 let mut spinner = SpinnerView::new(siv.cb_sink().clone());
222
223 thread::sleep(Duration::from_millis(10));
224
225 let handle = spinner.join_handle();
226
227 drop(spinner);
228 drop(siv);
229
230 assert!(matches!(handle.join(), Ok(())));
231 }
232}