cursive-extras 0.13.4

Extra views for the Cursive TUI library as well some helper functions and macros
Documentation
use cursive_core::{
    theme::{Color, ColorStyle, BaseColor},
    views::{DummyView, ResizedView},
    View,
    Printer,
    Vec2,
    utils::markup::StyledString
};
use rust_utils::{encapsulated, new_default};
use std::{
    time::Instant,
    fmt::Display
};

mod loading;
pub use loading::LoadingAnimation;

mod lazy_view;
pub use lazy_view::LazyView;

#[cfg(feature = "image_view")]
mod image_view;

#[cfg(feature = "image_view")]
pub use image_view::ImageView;

#[cfg(feature = "tabs")]
mod tabs;

#[cfg(feature = "tabs")]
pub use tabs::*;

#[cfg(feature = "advanced")]
mod advanced;

#[cfg(feature = "advanced")]
pub use advanced::*;

#[cfg(feature = "log_view")]
mod log_view;

#[cfg(feature = "log_view")]
pub use log_view::*;

use crate::SpannedStrExt;

/// A spacer between 2 views
///
/// This is created by one of the spacer functions
pub type Spacer = ResizedView<DummyView>;

// size for a divider
// either a fixed size or it fills the available space
#[derive(Copy, Clone, Eq, PartialEq)]
enum DividerSize {
    Free,
    Fixed(usize)
}

/// Horizontal divider view
///
/// # Example
/// ```
/// let mut root = cursive::default();
/// root.add_fullscreen_layer(
///     Dialog::around(
///         hlayout!(
///             select_view! {
///                 "item1" => 1,
///                 "item2" => 2,
///                 "item3" => 3
///             },
///             HDivider::new(),
///             select_view! {
///                 "item4" => 4,
///                 "item5" => 5,
///                 "item6" => 6
///              }
///         )
///     )
///         .button("Quit", Cursive::quit)
///         .title("Horizontal Divider Example")
/// );
/// root.run();
/// ```
#[derive(Copy, Clone)]
pub struct HDivider(DividerSize, usize);

impl HDivider {
    /// Creates new horizontal divider that takes up the available height
    pub fn new() -> HDivider { HDivider(DividerSize::Free, 0) }

    /// Creates new horizontal divider with a fixed height
    pub fn fixed(height: usize) -> HDivider {
        HDivider(DividerSize::Fixed(height), height)
    }

    pub fn height(&self) -> usize {
        self.1
    }
}

impl View for HDivider {
    fn draw(&self, printer: &Printer) {
        let height = if let DividerSize::Fixed(height) = self.0 {
            height
        }
        else { printer.size.y };

        printer.with_high_border(false, |printer| printer.print_vline((0, 0), height, ""));
    }

    fn required_size(&mut self, _bound: Vec2) -> Vec2 {
        if let DividerSize::Fixed(height) = self.0 {
            Vec2::new(1, height)
        }
        else {
            Vec2::new(1, 1)
        }
    }

    fn layout(&mut self, size: Vec2) {
        if self.0 == DividerSize::Free {
            self.1 = size.y;
        }
    }
}

new_default!(HDivider);

/// Vertical divider view
/// 
/// # Example
/// ```
/// let mut root = cursive::default();
/// root.add_fullscreen_layer(
///     Dialog::around(
///         vlayout!(
///             select_view! {
///                 "item1" => 1,
///                 "item2" => 2,
///                 "item3" => 3
///             },
///             VDivider::new(),
///             select_view! {
///                 "item4" => 4,
///                 "item5" => 5,
///                 "item6" => 6
///             }
///         )
///     )
///         .button("Quit", Cursive::quit)
///         .title("Vertical Divider Example")
/// );
/// root.run();
/// ```
#[derive(Copy, Clone)]
pub struct VDivider(DividerSize, usize);

impl VDivider {
    /// Creates new vertical divider that takes up the available width
    pub fn new() -> VDivider { VDivider(DividerSize::Free, 0) }

    /// Creates new vertical divider with a fixed width
    pub fn fixed(width: usize) -> VDivider {
        VDivider(DividerSize::Fixed(width), width)
    }

    pub fn width(&self) -> usize {
        self.1
    }
}

impl View for VDivider {
    fn draw(&self, printer: &Printer) {
        let width = if let DividerSize::Fixed(width) = self.0 {
            width
        }
        else { printer.size.x };
        printer.with_high_border(false, |printer| printer.print_hline((0, 0), width, ""));
    }

    fn required_size(&mut self, _bound: Vec2) -> Vec2 {
        if let DividerSize::Fixed(width) = self.0 {
            Vec2::new(width, 1)
        }
        else {
            Vec2::new(1, 1)
        }
    }

    fn layout(&mut self, size: Vec2) {
        if self.0 == DividerSize::Free {
            self.1 = size.x;
        }
    }
}

new_default!(VDivider);

/// View that can be used to report an application's status. 
/// It is meant to be placed at the bottom of the main Cursive layer
///
/// Auto-refresh in cursive must be set by calling `Cursive::set_fps()`
/// or `Cursive::set_autorefresh()` before using this view or it won't work
/// 
/// # Examples
/// 
/// ## Reporting Application Status
/// ```
/// let mut root = cursive::default();
/// root.add_fullscreen_layer(
///    vlayout!(
///         Dialog::text("Yes")
///             .button("Quit", Cursive::quit)
///             .title("StatusView Example"),
///         StatusView::new().with_name("status")
///    )
/// );
/// root.set_fps(30);
/// root.set_global_callback(Event::Refresh, |root| {
///     let mut status = root.find_name::<StatusView>("status").expect("StatusView does not exist!");
///     status.info("Application Status");
/// });
/// root.run();
/// ```
/// 
/// ## Reporting an Error
/// ```
/// let mut root = cursive::default();
/// root.add_fullscreen_layer(
///     vlayout!(
///         Dialog::text("Yes")
///             .button("Quit", Cursive::quit)
///             .title("StatusView Example"),
///         StatusView::new().with_name("status")
///     )
/// );
/// 
/// root.set_fps(30);
/// root.set_global_callback(Event::Refresh, |root| {
///     let error: Result<&str, &str> = Err("Error: Houston, we have a problem!");
///     let mut status = root.find_name::<StatusView>("status").unwrap();
///     report_error!(status, error);
/// });
/// root.run();
/// ```

#[derive(Clone)]
#[encapsulated]
pub struct StatusView {
    #[setter(use_into_impl, doc = "Set the label of the [`StatusView`]")]
    #[chainable(use_into_impl)]
    label: StyledString,

    cur_msg: StyledString,
    time: Instant,
    error: bool
}

impl StatusView {
    /// Create a new `StatusView`
    pub fn new() -> StatusView {
        StatusView {
            label: StyledString::new(),
            cur_msg: StyledString::new(),
            time: Instant::now(),
            error: false
        }
    }

    /// Set the message in the `StatusView` with error formatting (bright red text)
    pub fn report_error<T: Display>(&mut self, text: T) {
        let err_style = ColorStyle::from(Color::Light(BaseColor::Red));
        self.cur_msg = StyledString::styled(text.to_string(), err_style);
        self.time = Instant::now();
        self.error = true;
    }

    /// Set the message in the `StatusView`
    pub fn info<T: Into<StyledString>>(&mut self, text: T) {
        self.cur_msg = text.into();
        self.time = Instant::now();
    }
}

impl Default for StatusView {
    fn default() -> StatusView { StatusView::new() }
}

impl View for StatusView {
    fn draw(&self, printer: &Printer) {
        if printer.size.x == 0 && printer.size.y == 0 {
            return;
        }

        if self.cur_msg.is_empty() {
            if !self.label.is_empty() {
                printer.print_styled((0, 0), self.label.as_spanned_str());
            }
        }
        else {
            printer.print_styled((0, 0), self.cur_msg.as_spanned_str());
        }
    }

    fn required_size(&mut self, bound: Vec2) -> Vec2 {
        let y = (!self.cur_msg.is_empty() || !self.label.is_empty()) as usize;
        Vec2::new(bound.x, y)
    }

    fn layout(&mut self, _: Vec2) {
        if self.time.elapsed().as_secs() >= 5 {
            self.cur_msg = StyledString::new();
            self.time = Instant::now();
            self.error = false;
        }
    }
}