Skip to main content

photon_ui/components/
cancellable_loader.rs

1use std::sync::Arc;
2
3use crate::{
4    Component,
5    Event,
6    InputResult,
7    RenderError,
8    Rendered,
9    components::Loader,
10};
11
12/// A [`Loader`] that can be cancelled via Ctrl-C.
13///
14/// The cancellation signal is an [`AtomicBool`](std::sync::atomic::AtomicBool)
15/// behind an [`Arc`], so it can be shared across threads or tasks. The loader
16/// itself does not stop rendering when cancelled; the application should check
17/// [`is_cancelled`](CancellableLoader::is_cancelled) and take action.
18pub struct CancellableLoader {
19    loader: Loader,
20    signal: Arc<std::sync::atomic::AtomicBool>,
21}
22
23impl CancellableLoader {
24    /// Create a new cancellable loader with the given message.
25    pub fn new(
26        message: impl Into<String>,
27        spinner_color: Option<String>,
28        message_color: Option<String>,
29    ) -> Self {
30        Self {
31            loader: Loader::new(message, spinner_color, message_color),
32            signal: Arc::new(std::sync::atomic::AtomicBool::new(false)),
33        }
34    }
35
36    /// Set the cancellation flag.
37    pub fn cancel(&self) {
38        self.signal
39            .store(true, std::sync::atomic::Ordering::Relaxed);
40    }
41
42    /// Returns `true` if cancellation has been requested.
43    pub fn is_cancelled(&self) -> bool {
44        self.signal.load(std::sync::atomic::Ordering::Relaxed)
45    }
46
47    /// Advance the underlying spinner frame.
48    pub fn tick(&mut self) {
49        self.loader.tick();
50    }
51}
52
53impl Component for CancellableLoader {
54    fn render(&self, width: u16) -> Result<Rendered, RenderError> {
55        self.loader.render(width)
56    }
57
58    fn handle_input(&mut self, event: &Event) -> InputResult {
59        use crate::events::matches_key;
60        if matches_key(event, &crate::events::Key::ctrl('c')) {
61            self.cancel();
62            InputResult::Handled
63        } else {
64            InputResult::Ignored
65        }
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use crossterm::event::{
72        KeyCode,
73        KeyEvent,
74        KeyModifiers,
75    };
76
77    use super::*;
78
79    #[test]
80    fn render_delegates() {
81        let cl = CancellableLoader::new("test", None, None);
82        let r = cl.render(80).unwrap();
83        assert!(r.lines[0].contains("test"));
84    }
85
86    #[test]
87    fn ignored_key() {
88        let mut cl = CancellableLoader::new("test", None, None);
89        let result = cl.handle_input(&Event::Key(KeyEvent::new(
90            KeyCode::Char('x'),
91            KeyModifiers::empty(),
92        )));
93        assert!(matches!(result, InputResult::Ignored));
94        assert!(!cl.is_cancelled());
95    }
96}