cursive-extras 0.13.4

Extra views for the Cursive TUI library as well some helper functions and macros
Documentation
use crate::{
    hlayout,
    views::{LoadingAnimation, Spacer}
};
use std::fmt::Display;
use cursive_core::{
    Cursive, With,
    view::Nameable,
    event::{Event, Key},
    views::{
        EditView,
        Dialog,
        OnEventView,
        NamedView,
        Checkbox,
        LinearLayout,
        TextView,
        DummyView,
        ResizedView
    },
    utils::markup::StyledString,
    theme::{
        PaletteColor,
        Color,
        BaseColor,
        ColorType,
        ColorStyle,
        BorderStyle,
        Theme
    }
};

/// Convenience function that generates a better looking Cursive theme
///
/// # Example
/// ```
/// let mut root = cursive::default();
/// root.set_theme(better_theme());
/// ```
pub fn better_theme() -> Theme {
    let mut theme_def = Theme {
        shadow: false,
        .. Theme::default()
    };
    theme_def.palette[PaletteColor::Background] = Color::TerminalDefault;
    theme_def.palette[PaletteColor::Primary] = Color::Light(BaseColor::White);
    theme_def.palette[PaletteColor::View] = Color::TerminalDefault;
    theme_def.palette[PaletteColor::Highlight] = Color::Light(BaseColor::Blue);
    theme_def.palette[PaletteColor::HighlightText] = Color::Dark(BaseColor::White);
    theme_def.palette[PaletteColor::Secondary] = Color::Dark(BaseColor::Blue);
    theme_def.palette[PaletteColor::TitlePrimary] = Color::Light(BaseColor::Blue);
    theme_def.palette[PaletteColor::TitleSecondary] = Color::Dark(BaseColor::Blue);
    theme_def.borders = BorderStyle::Outset;
    theme_def
}

/// Convenience function that creates a dialog that runs a callback if the
/// user selects "Yes"
///
/// # Example
/// ```
/// let mut root = cursive::default();
/// root.add_layer(confirm_dialog("Are you sure?", "I solemnly swear that I am up to no good. /s", |view| view.quit()));
/// root.run();
/// ```
pub fn confirm_dialog<T, U, C>(title: T, text: U, cb: C) -> OnEventView<Dialog>
where
    T: Display,
    U: Into<StyledString>,
    C: Fn(&mut Cursive) + Send + Sync + 'static
{
    Dialog::text(text)
        .dismiss_button("No")
        .button("Yes", cb)
        .title(title.to_string())
        .wrap_with(OnEventView::new)
        .on_event(Event::Key(Key::Esc), |r| {
            if r.screen().len() <= 1 { r.quit(); }
            else { r.pop_layer(); }
        })
}

/// Convenience function that shows a user a dialog box
/// with a message that includes a back button
///
/// # Example
/// ```
/// let mut root = cursive::default();
/// root.add_layer(info_dialog("Info", "This is important!"));
/// root.run();
/// ```
pub fn info_dialog<T: Display, U: Into<StyledString>>(title: T, text: U) -> OnEventView<Dialog> {
    Dialog::text(text)
        .dismiss_button("Back")
        .title(title.to_string())
        .wrap_with(OnEventView::new)
        .on_event(Event::Key(Key::Esc), |r| {
            if r.screen().len() <= 1 { r.quit(); }
            else { r.pop_layer(); }
        })
}

/// Convenience function that creates a named `EditView` that has a better
/// looking style and can optionally act as a password entry box
///
/// # Example
/// ```
/// let mut root = cursive::default();
/// root.add_fullscreen_layer(
///     Dialog::around(styled_editview("yes", "edit", false))
///         .button("Quit", Cursive::quit)
///         .title("Styled EditView Example")
/// );
/// root.run();
/// ```
pub fn styled_editview<C: Display>(content: C, view_name: &str, password: bool) -> NamedView<EditView> {
    styled_editview_color(content, view_name, password, PaletteColor::TitlePrimary)
}

/// Same as `styled_editview()` but allows for a color to be chosen instead of using the highlight color
pub fn styled_editview_color<T: Display, C: Into<ColorType>>(content: T, view_name: &str, password: bool, color: C) -> NamedView<EditView> {
    let input_style = ColorStyle::new(
        Color::Light(BaseColor::White),
        color
    );
    let view = EditView::new().content(content.to_string()).style(input_style).filler(" ");

    if password { view.secret() }
    else { view }
        .with_name(view_name)
}

/// Convenience function that return the state of a named check box
///
/// Returns false if the checkbox is not checked or the checkbox with
/// the specified name does not exist
pub fn get_checkbox_option(root: &mut Cursive, name: &str) -> bool {
    if let Some(cbox) = root.find_name::<Checkbox>(name) {
        cbox.is_checked()
    }
    else { false }
}

/// Convenience function that returns a horizontal `LinearLayout` that is a named check box with a label
pub fn labeled_checkbox(text: &str, name: &str, checked: bool) -> LinearLayout {
    labeled_checkbox_cb(text, name, checked, |_, _| { })
}

/// Same as `labeled_checkbox()` but also accepts a closure to execute when the check box's state changes
pub fn labeled_checkbox_cb<C: Fn(&mut Cursive, bool) + Send + Sync + 'static>(text: &str, name: &str, checked: bool, callback: C) -> LinearLayout {
    hlayout!(
        Checkbox::new()
            .with_checked(checked)
            .on_change(callback)
            .with_name(name),
        TextView::new(format!(" {text}"))
    )
}

/// Convenience function that shows a loading pop up
///
/// # Example
/// ```
/// let mut root = cursive::default();
/// root.set_theme(better_theme());
/// load_resource(&mut root,
///     "Loading...", "Loading Dummy Number...",
///     || {
///         thread::sleep(Duration::from_secs(5));
///         2 + 4
///     },
///     |root, val| {
///         assert_eq!(val, 6);
///         root.quit()
///     }
/// );
///
/// root.run();
/// ```
pub fn load_resource<T, D, M, R, F>(root: &mut Cursive, title: D, msg: M, task: R, finish_task: F)
where
    T: Send + Sync + 'static,
    D: Display,
    M: Into<StyledString>,
    R: FnOnce() -> T + Send + Sync + 'static,
    F: FnOnce(&mut Cursive, T) + Send + Sync + 'static,
{
    let loader = LoadingAnimation::new(msg, move || (task(), finish_task));
    if root.fps().is_none() { root.set_fps(30); }
    root.add_layer(
        Dialog::around(loader.with_name("load")).title(title.to_string())
            .wrap_with(OnEventView::new)
            .on_event(Event::Refresh, |root: &mut Cursive| {
                let mut loader = root.find_name::<LoadingAnimation<(T, F)>>("load").unwrap();
                if loader.is_done() {
                    root.pop_layer();
                    let (val, finish_func) = loader.finish().unwrap();
                    finish_func(root, val);
                }
            })
    );
}

/// Horizontal spacer with fixed width
pub fn fixed_hspacer(width: usize) -> Spacer { ResizedView::with_fixed_width(width, DummyView) }

/// Horizontal spacer fills all the available width
pub fn filled_hspacer() -> Spacer { ResizedView::with_full_width(DummyView) }

/// Vertical spacer with fixed height
pub fn fixed_vspacer(height: usize) -> Spacer { ResizedView::with_fixed_height(height, DummyView) }

/// Vertical spacer fills all the available height
pub fn filled_vspacer() -> Spacer { ResizedView::with_full_height(DummyView) }