cursive-extras 0.13.4

Extra views for the Cursive TUI library as well some helper functions and macros
Documentation
use std::slice::{Iter, IterMut};
use cursive_core::{
    Cursive, Printer,
    Vec2, Rect,
    event::Callback,
    align::HAlign,
    theme::ColorStyle,
    utils::markup::StyledString,
    views::BoxedView
};
use unicode_width::UnicodeWidthStr;
use crate::SpannedStrExt;

mod container;
mod dialog;
mod layer;

pub use self::{
    container::TabContainer,
    dialog::TabDialog,
    layer::TabLayer
};

/// Iterator over the views and their IDs in a tabbed view
pub type TabIter<'a> = Iter<'a, (usize, BoxedView)>;

/// Mutable iterator over the views and their IDs in a tabbed view
pub type TabIterMut<'a> = IterMut<'a, (usize, BoxedView)>;

// what part of the tab view is being focused?
#[derive(Copy, Clone, PartialEq, Eq)]
enum TabFocus {
    TitleBar(usize),
    Content,
    Buttons(usize)
}

// what should the tab button do?
enum ButtonAction {
    Close, // close current tab
    Next, // next tab
    Prev, // previous tab
    CallBack(Callback) // custom action
}

impl<F: Fn(&mut Cursive) + Send + Sync + 'static> From<F> for ButtonAction {
    fn from(func: F) -> Self { ButtonAction::CallBack(Callback::from_fn(func)) }
}

// a button in the tab dialog title bar
#[derive(Clone)]
struct TitleButton {
    label: String,
    rect: Rect,
    b_type: TitleButtonType
}

impl TitleButton {
    fn new(label: &str, id: usize) -> TitleButton {
        TitleButton {
            label: format!(" {label} "),
            rect: Rect::from_size((1, 0), (label.width(), 1)),
            b_type: TitleButtonType::Tab(id)
        }
    }

    // creates an overflow button
    fn overflow() -> TitleButton {
        TitleButton {
            label: String::from("..."),
            rect: Rect::from_size((1, 0), (3, 1)),
            b_type: TitleButtonType::Overflow
        }
    }

    fn set_label(&mut self, text: &str) {
        self.label = format!(" {text} ");
        self.rect = Rect::from_size((1, 0), (text.width(), 1));
    }

    fn width(&self) -> usize { self.label.width() + 1 }
    fn has_mouse_pos(&self, pos: Vec2) -> bool { self.rect.contains(pos) }
    fn offset(&mut self, offset: usize) { self.rect.offset((offset, 0)); }

    fn draw(&self, printer: &Printer, selected: bool, focused: bool, first: bool, last: bool) {
        let loc = self.rect.top_left() - (1, 0);
        let fc = if first && focused { "┨" }
        else if first { "┤" }
        else if focused { "┃" }
        else { "│" };

        let lc = if last && focused { "â” " }
        else if last { "├" }
        else if focused { "┃" }
        else { "│" };

        printer.with_style(ColorStyle::primary(), |printer| {
            printer.print(loc, fc);
            printer.print(loc + (self.label.width() + 1, 0), lc);
        });

        let title_style =
            if selected{ ColorStyle::highlight() }
            else if focused { ColorStyle::title_primary() }
            else { ColorStyle::title_secondary() };

        printer.print_styled(loc + (1, 0), StyledString::styled(&self.label, title_style).as_spanned_str());
    }
}

// the type of title bar button
#[derive(Copy, Clone, PartialEq, Eq)]
enum TitleButtonType {
    // button with associated view
    Tab(usize),

    // overflow menu
    Overflow
}

// tab view button
struct ViewButton {
    text: String,
    action: ButtonAction,
    rect: Rect
}

impl ViewButton {
    fn new(text: &str, action: ButtonAction) -> ViewButton {
        ViewButton {
            text: text.to_string(),
            action,
            rect: Rect::from_size((0, 0), (text.width() + 2, 1))
        }
    }

    fn from_fn<F: Fn(&mut Cursive) + Send + Sync + 'static>(text: &str, func: F) -> ViewButton {
        Self::new(text, ButtonAction::from(func))
    }

    fn draw(&self, printer: &Printer, selected: bool) {
        let style = if selected { ColorStyle::highlight() }
        else { ColorStyle::primary() };
        let text = StyledString::styled(format!("<{}>", self.text), style);
        printer.print_styled(self.rect.top_left(), text.as_spanned_str());
    }

    fn width(&self) -> usize { self.text.chars().count() + 3 }
    fn has_mouse_pos(&self, pos: Vec2) -> bool  { self.rect.contains(pos) }
}

// align the title bar of a tabbed view
fn align_title_buttons(shown_buttons: &mut Vec<TitleButton>, buttons: &[TitleButton], align: HAlign, width: usize, dialog: bool) {
    if !buttons.is_empty() {
        shown_buttons.clear();
        let mut buttons_len = 0;
        for button in buttons.iter() {
            shown_buttons.push(button.clone());
            buttons_len += button.width();
        }

        if buttons_len > width {
            shown_buttons.clear();
            buttons_len = 0;
            for (i, button) in buttons.iter().enumerate() {
                if i >= 3 {
                    shown_buttons.push(TitleButton::overflow());
                    buttons_len += 4;
                    break;
                }
                else {
                    shown_buttons.push(button.clone());
                    buttons_len += button.width();
                }
            }
        }

        let offset = match align {
            HAlign::Left => if dialog { 1 } else { 0 },
            HAlign::Center => (width - buttons_len) / 2 ,
            HAlign::Right => width - buttons_len - if dialog { 0 } else { 1 }
        };

        let mut cur_len = 0;
        for button in shown_buttons.iter_mut() {
            button.offset(offset + cur_len);
            cur_len += button.width();
        }
    }
}

// align the buttons of a tabbed view
fn align_buttons(buttons: &mut [ViewButton], align: HAlign, size: Vec2, dialog: bool) {
    if !buttons.is_empty() {
        // the length of all the buttons
        let mut buttons_len = 0;
        for button in buttons.iter() {
            buttons_len += button.width();
        }
        let offset = match align {
            HAlign::Left => 1,
            HAlign::Center => (size.x - buttons_len) / 2 ,
            HAlign::Right => size.x + if dialog { 0 } else { 1 } - buttons_len
        };

        let mut cur_len = 0;
        for button in buttons.iter_mut() {
            let loc = (offset + cur_len, size.y - if dialog { 2 } else { 1 });
            let rsize = button.rect.size();
            button.rect = Rect::from_size(loc, rsize);
            cur_len += button.width();
        }
    }
}