cursive-extras 0.13.2

Extra views for the Cursive TUI library as well some helper functions and macros
Documentation
use crate::SpannedStrExt;
use std::{
    sync::{Arc, RwLock},
    thread::{self, JoinHandle}
};
use crossbeam_channel::{bounded, Receiver, TryRecvError};
use cursive_core::{
    Printer, Vec2,
    view::View,
    theme::{ColorStyle, PaletteColor},
    utils::markup::StyledString
};

type WorkerThread<T> = Arc<RwLock<Option<JoinHandle<T>>>>;

/// Loading animation view that runs a task in a background thread and shows a loading animation and message while it completes and allows for retrieval of a return value.
/// It is recommended to use `load_resource()` instead of this directly since it has a simpler interface
/// 
/// # Example
/// ```
/// let mut root = cursive::default();
/// let anim = LoadingAnimation::new("Loading Dummy Resource...", || {
///     thread::sleep(Duration::from_secs(10));
/// });
/// 
/// root.add_layer(Dialog::around(anim.with_name("loader")).title("Loading..."));
/// root.set_fps(30);
///
/// root.set_global_callback(Event::Refresh, |root| {
///     let mut loader = root.find_name::<LoadingAnimation<()>>("loader").unwrap();
///     if loader.is_done() {
///         loader.finish().unwrap_or(());
///         root.quit()
///     }
/// });
/// root.run();
/// ```
#[derive(Clone)]
pub struct LoadingAnimation<T: Send + Sync + 'static> {
    worker: WorkerThread<T>,
    recv: Receiver<()>,
    message: StyledString,
    width: usize,
    anim_x: usize,
    reversed: bool
}

impl<T: Send + Sync> LoadingAnimation<T> {
    /// Create a new `LoadingAnimation` with the specified closure or function pointer as the task
    pub fn new<U, M: Into<StyledString>>(message: M, task: U) -> LoadingAnimation<T>
        where U: FnOnce() -> T + Send + Sync + 'static
    {
        let (sender, recv) = bounded(0);
        let worker = Arc::new(RwLock::new(
            Some(
                thread::spawn(move || {
                    let out = task();
                    sender.send(()).unwrap();
                    out
                })
            )
        ));
        let message = message.into();

        LoadingAnimation {
            worker,
            recv,
            width: message.char_len(),
            message,
            anim_x: 0,
            reversed: false
        }
    }

    /// Has the background task finished executing?
    pub fn is_done(&self) -> bool {
        match self.recv.try_recv() {
            Ok(()) => true,
            Err(e) => e == TryRecvError::Disconnected
        }
    }

    /// Join with the task's thread, block until it is finished, and return the resulting value
    /// 
    /// It is best to run this when `LoadingAnimation::is_done()` is true
    /// 
    /// This will return `None` if called more than once
    /// 
    /// # Panics
    /// This method will panic if the underlying task panicked while executing
    #[track_caller]
    pub fn finish(&mut self) -> Option<T> {
        let worker = self.worker.write().unwrap().take()?;
        Some(worker.join().unwrap())
    }
}

impl<T: Send + Sync> View for LoadingAnimation<T> {
    fn draw(&self, printer: &Printer) {
        let style = ColorStyle::new(
            PaletteColor::Highlight,
            PaletteColor::Background,
        );
        printer.with_color(style, |printer| {
            printer.print((self.anim_x, 0), "███");
        });
        printer.print_styled((0, 1), self.message.as_spanned_str());
    }

    fn layout(&mut self, _: Vec2) {
        if self.width > 0 {
            if self.reversed {
                if self.anim_x == 0 {
                    self.reversed = false;
                }
                else {
                    self.anim_x -= 1;
                }
            }
            else if self.anim_x < self.width - 3 {
                self.anim_x += 1;
            }
            else {
                self.reversed = true;
            }
        }
    }

    fn required_size(&mut self, _: Vec2) -> Vec2 { Vec2::new(self.message.char_len(), 2) }
}