cursive_spinner_view/
view.rs

1use 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/// Spinner view
23#[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    // Used in tests to prove that the spinner
32    // thread terminates after dropping the view
33    join_handle: Option<JoinHandle<()>>,
34}
35
36//todo? create a builder struct for SpinnerView
37impl SpinnerView {
38    /// New spinner view
39    ///
40    /// A CbSink is needed for new spinner.
41    ///
42    /// # Examples
43    ///
44    /// ```
45    /// use cursive_spinner_view::SpinnerView;
46    ///
47    /// let siv = cursive::default();
48    /// #[allow(unused)]
49    /// let spinner = SpinnerView::new(siv.cb_sink().clone());
50    /// ```
51    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, //todo? create kinda GearBox instead speeds
70            text_view,
71            tx_spinner,
72
73            #[cfg(test)]
74            join_handle: Some(_join_handle),
75        }
76    }
77
78    /// Spin up the spinner
79    ///
80    /// You can do it as many times as you need.
81    pub fn spin_up(&mut self) {
82        self.spin_ups = self.spin_ups.saturating_add(1);
83
84        self.recalc_duration();
85    }
86
87    /// Spin down the spinner
88    ///
89    /// To stop the spinner the numbers of spin-downs
90    /// have to be equal the numbers of spin-ups.
91    pub fn spin_down(&mut self) {
92        self.spin_ups = self.spin_ups.saturating_sub(1);
93
94        self.recalc_duration();
95    }
96
97    /// Stop the spinner immediately
98    pub fn stop(&mut self) {
99        self.spin_ups = 0;
100        self.recalc_duration();
101    }
102
103    /// The number of spin-ups
104    pub fn spin_ups(&self) -> usize {
105        self.spin_ups
106    }
107
108    /// Is the spinner spinning
109    pub fn is_spinning(&self) -> bool {
110        self.spin_ups() != 0
111    }
112
113    /// Set spinner's frames
114    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    /// Set spinner's style
123    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}