cursive 0.8.2

A TUI (Text User Interface) library focused on ease-of-use.
Documentation
use Cursive;
use Printer;
use direction;
use event::*;
use menu::{MenuItem, MenuTree};
use std::rc::Rc;
use theme::ColorStyle;
use unicode_width::UnicodeWidthStr;
use vec::Vec2;
use view::{Position, View};
use views::{MenuPopup, OnEventView};

/// Current state of the menubar
#[derive(PartialEq, Debug)]
enum State {
    /// The menubar is inactive.
    Inactive,
    /// The menubar is actively selected.
    ///
    /// It will receive input.
    Selected,
    /// The menubar is still visible, but a submenu is open.
    ///
    /// It will not receive input.
    Submenu,
}

/// Shows a single-line list of items, with pop-up menus when one is selected.
///
/// The [`Cursive`] root already includes a menubar
/// that you just need to configure.
///
/// [`Cursive`]: ../struct.Cursive.html#method.menubar
pub struct Menubar {
    /// Menu items in this menubar.
    root: MenuTree,

    /// TODO: move this out of this view.
    pub autohide: bool,
    focus: usize,

    // TODO: make Menubar impl View and take out the State management
    state: State,
}

new_default!(Menubar);

impl Menubar {
    /// Creates a new, empty menubar.
    pub fn new() -> Self {
        Menubar {
            root: MenuTree::new(),
            autohide: true,
            state: State::Inactive,
            focus: 0,
        }
    }

    /// Hides the menubar.
    fn hide(&mut self) {
        self.state = State::Inactive;
    }

    /// True if we should be receiving events.
    pub fn receive_events(&self) -> bool {
        self.state == State::Selected
    }

    /// True if some submenus are visible.
    pub fn has_submenu(&self) -> bool {
        self.state == State::Submenu
    }

    /// Returns `true` if we should be drawn.
    pub fn visible(&self) -> bool {
        !self.autohide || self.state != State::Inactive
    }

    /// Adds a new item to the menubar.
    ///
    /// The item will use the given title, and on selection, will open a
    /// popup-menu with the given menu tree.
    pub fn add_subtree<S>(&mut self, title: S, menu: MenuTree) -> &mut Self
    where
        S: Into<String>,
    {
        let i = self.root.len();
        self.insert_subtree(i, title, menu)
    }

    /// Adds a delimiter to the menubar.
    pub fn add_delimiter(&mut self) -> &mut Self {
        let i = self.root.len();
        self.insert_delimiter(i)
    }

    /// Adds a leaf node to the menubar.
    pub fn add_leaf<S, F>(&mut self, title: S, cb: F) -> &mut Self
    where
        S: Into<String>,
        F: 'static + Fn(&mut Cursive),
    {
        let i = self.root.len();
        self.insert_leaf(i, title, cb)
    }

    /// Insert a new item at the given position.
    pub fn insert_subtree<S>(
        &mut self, i: usize, title: S, menu: MenuTree
    ) -> &mut Self
    where
        S: Into<String>,
    {
        self.root.insert_subtree(i, title, menu);
        self
    }

    /// Inserts a new delimiter at the given position.
    ///
    /// It will show up as `|`.
    pub fn insert_delimiter(&mut self, i: usize) -> &mut Self {
        self.root.insert_delimiter(i);
        self
    }

    /// Inserts a new leaf node at the given position.
    ///
    /// It will be directly actionable.
    pub fn insert_leaf<S, F>(&mut self, i: usize, title: S, cb: F) -> &mut Self
    where
        S: Into<String>,
        F: 'static + Fn(&mut Cursive),
    {
        self.root.insert_leaf(i, title, cb);
        self
    }

    /// Removes all menu items from this menubar.
    pub fn clear(&mut self) {
        self.root.clear();
        self.focus = 0;
    }

    /// Returns the number of items in this menubar.
    pub fn len(&self) -> usize {
        self.root.len()
    }

    /// Returns `true` if this menubar is empty.
    pub fn is_empty(&self) -> bool {
        self.root.is_empty()
    }

    /// Returns the item at the given position.
    ///
    /// Returns `None` if `i > self.len()`
    pub fn get_subtree(&mut self, i: usize) -> Option<&mut MenuTree> {
        self.root.get_subtree(i)
    }

    /// Looks for an item with the given label.
    pub fn find_subtree(&mut self, label: &str) -> Option<&mut MenuTree> {
        self.root.find_subtree(label)
    }

    /// Returns the position of the item with the given label.
    ///
    /// Returns `None` if no such label was found.
    pub fn find_position(&mut self, label: &str) -> Option<usize> {
        self.root.find_position(label)
    }

    /// Remove the item at the given position.
    pub fn remove(&mut self, i: usize) {
        self.root.remove(i);
    }

    fn child_at(&self, x: usize) -> Option<usize> {
        if x == 0 {
            return None;
        }
        let mut offset = 1;

        for (i, child) in self.root.children.iter().enumerate() {
            offset += child.label().width() + 2;
            if x < offset {
                return Some(i);
            }
        }

        None
    }

    fn select_child(&mut self, open_only: bool) -> EventResult {
        match self.root.children[self.focus] {
            MenuItem::Leaf(_, ref cb) if !open_only => {
                EventResult::Consumed(Some(cb.clone()))
            }
            MenuItem::Subtree(_, ref tree) => {
                // First, we need a new Rc to send the callback,
                // since we don't know when it will be called.
                let menu = Rc::clone(tree);

                self.state = State::Submenu;
                let offset = Vec2::new(
                    self.root.children[..self.focus]
                        .iter()
                        .map(|child| child.label().width() + 2)
                        .sum(),
                    if self.autohide { 1 } else { 0 },
                );
                // Since the closure will be called multiple times,
                // we also need a new Rc on every call.
                EventResult::with_cb(move |s| {
                    show_child(s, offset, Rc::clone(&menu))
                })
            }
            _ => EventResult::Ignored,
        }
    }
}

fn show_child(s: &mut Cursive, offset: Vec2, menu: Rc<MenuTree>) {
    // Adds a new layer located near the item title with the menu popup.
    // Also adds two key callbacks on this new view, to handle `left` and
    // `right` key presses.
    // (If the view itself listens for a `left` or `right` press, it will
    // consume it before our OnEventView. This means sub-menus can properly
    // be entered.)
    s.screen_mut().add_layer_at(
        Position::absolute(offset),
        OnEventView::new(
            MenuPopup::new(menu)
                .on_dismiss(|s| s.select_menubar())
                .on_action(|s| s.menubar().state = State::Inactive),
        ).on_event(Key::Right, |s| {
            s.pop_layer();
            s.select_menubar();
            // Act as if we sent "Right" then "Down"
            s.menubar().on_event(Event::Key(Key::Right)).process(s);
            if let EventResult::Consumed(Some(cb)) =
                s.menubar().on_event(Event::Key(Key::Down))
            {
                cb(s);
            }
        })
            .on_event(Key::Left, |s| {
                s.pop_layer();
                s.select_menubar();
                // Act as if we sent "Left" then "Down"
                s.menubar().on_event(Event::Key(Key::Left)).process(s);
                if let EventResult::Consumed(Some(cb)) =
                    s.menubar().on_event(Event::Key(Key::Down))
                {
                    cb(s);
                }
            }),
    );
}

impl View for Menubar {
    fn draw(&self, printer: &Printer) {
        // Draw the bar at the top
        printer.with_color(ColorStyle::primary(), |printer| {
            printer.print_hline((0, 0), printer.size.x, " ");
        });

        // TODO: draw the rest
        let mut offset = 1;
        for (i, item) in self.root.children.iter().enumerate() {
            let title = item.label();

            // We don't want to show HighlightInactive when we're not selected,
            // because it's ugly on the menubar.
            let selected =
                (self.state != State::Inactive) && (i == self.focus);
            printer.with_selection(selected, |printer| {
                printer.print((offset, 0), &format!(" {} ", title));
            });
            offset += title.width() + 2;
        }
    }

    fn on_event(&mut self, event: Event) -> EventResult {
        match event {
            Event::Key(Key::Esc) => {
                self.hide();
                return EventResult::with_cb(|s| s.clear());
            }
            Event::Key(Key::Left) => loop {
                if self.focus > 0 {
                    self.focus -= 1;
                } else {
                    self.focus = self.root.len() - 1;
                }
                if !self.root.children[self.focus].is_delimiter() {
                    break;
                }
            },
            Event::Key(Key::Right) => loop {
                if self.focus + 1 < self.root.len() {
                    self.focus += 1;
                } else {
                    self.focus = 0;
                }
                if !self.root.children[self.focus].is_delimiter() {
                    break;
                }
            },
            Event::Key(Key::Down) => {
                return self.select_child(true);
            }
            Event::Key(Key::Enter) => {
                return self.select_child(false);
            }
            Event::Mouse {
                event: MouseEvent::Press(btn),
                position,
                offset,
            } if position.fits(offset) && position.y == offset.y =>
            {
                if let Some(child) = position
                    .checked_sub(offset)
                    .and_then(|pos| self.child_at(pos.x))
                {
                    if !self.root.children[child].is_delimiter() {
                        self.focus = child;
                        if btn == MouseButton::Left {
                            return self.select_child(true);
                        }
                    }
                }
            }
            Event::Mouse {
                event: MouseEvent::Release(btn),
                position,
                offset,
            } if position.fits(offset) && position.y == offset.y =>
            {
                if let Some(child) = position
                    .checked_sub(offset)
                    .and_then(|pos| self.child_at(pos.x))
                {
                    if self.focus == child && btn == MouseButton::Left
                        && self.root.children[child].is_leaf()
                    {
                        return self.select_child(false);
                    }
                }
            }
            Event::Mouse {
                event: MouseEvent::Press(_),
                ..
            } => {
                self.hide();
                return EventResult::with_cb(|s| s.clear());
            }
            _ => return EventResult::Ignored,
        }
        EventResult::Consumed(None)
    }

    fn take_focus(&mut self, _: direction::Direction) -> bool {
        self.state = State::Selected;
        true
    }

    fn required_size(&mut self, _: Vec2) -> Vec2 {
        // TODO: scroll the options if the screen is too small?

        // We add 2 to the length of every label for marin.
        // Also, we add 1 at the beginning.
        // (See the `draw()` method)
        let width = self.root
            .children
            .iter()
            .map(|item| item.label().len() + 2)
            .sum();

        Vec2::new(width, 1)
    }
}