cursive/src/views/menubar.rs

311 lines
9.5 KiB
Rust
Raw Normal View History

2016-07-10 02:05:51 +00:00
use Cursive;
2016-07-14 06:25:54 +00:00
use Printer;
2016-10-02 22:22:29 +00:00
use direction;
use event::*;
2016-10-02 22:22:29 +00:00
use menu::MenuTree;
use std::rc::Rc;
use theme::ColorStyle;
2016-07-04 23:04:32 +00:00
use unicode_width::UnicodeWidthStr;
2016-10-02 22:22:29 +00:00
use vec::Vec2;
use view::{Position, View};
2017-10-12 23:38:55 +00:00
use views::{MenuPopup, OnEventView};
2016-07-04 23:04:32 +00:00
/// 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,
}
2016-07-20 03:28:34 +00:00
/// 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.
2016-07-20 03:28:34 +00:00
///
2016-07-20 03:50:21 +00:00
/// [`Cursive`]: ../struct.Cursive.html#method.menubar
pub struct Menubar {
2016-07-20 03:28:34 +00:00
/// Menu items in this menubar.
menus: Vec<(String, Rc<MenuTree>)>,
2016-07-20 03:28:34 +00:00
/// TODO: move this out of this view.
pub autohide: bool,
2016-07-20 03:28:34 +00:00
focus: usize,
// TODO: make Menubar impl View and take out the State management
state: State,
}
2016-07-17 00:28:42 +00:00
new_default!(Menubar);
impl Menubar {
2016-07-20 03:28:34 +00:00
/// Creates a new, empty menubar.
pub fn new() -> Self {
Menubar {
menus: Vec::new(),
autohide: true,
state: State::Inactive,
focus: 0,
}
}
2016-07-20 03:28:34 +00:00
/// Hides the menubar.
fn hide(&mut self) {
self.state = State::Inactive;
}
2016-07-20 03:28:34 +00:00
/// True if we should be receiving events.
pub fn receive_events(&self) -> bool {
self.state == State::Selected
}
2016-07-20 03:28:34 +00:00
/// Returns `true` if we should be drawn.
pub fn visible(&self) -> bool {
!self.autohide || self.state != State::Inactive
}
2016-07-20 03:28:34 +00:00
/// 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(&mut self, title: &str, menu: MenuTree) -> &mut Self {
let i = self.menus.len();
self.insert_subtree(i, title, menu)
}
/// Insert a new item at the given position.
2017-10-12 23:38:55 +00:00
pub fn insert_subtree(
&mut self, i: usize, title: &str, menu: MenuTree
) -> &mut Self {
self.menus.insert(i, (title.to_string(), Rc::new(menu)));
self
}
/// Removes all menu items from this menubar.
pub fn clear(&mut self) {
self.menus.clear();
self.focus = 0;
}
/// Returns the number of items in this menubar.
pub fn len(&self) -> usize {
self.menus.len()
}
/// Returns `true` if this menubar is empty.
pub fn is_empty(&self) -> bool {
self.menus.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> {
2017-10-12 23:38:55 +00:00
self.menus
.get_mut(i)
.map(|&mut (_, ref mut tree)| Rc::make_mut(tree))
}
/// Looks for an item with the given label.
pub fn find_subtree(&mut self, label: &str) -> Option<&mut MenuTree> {
// Look for the menu with the correct label,
// then call Rc::make_mut on the tree.
// If another Rc on this tree existed, this will clone
// the tree and keep the forked version.
self.menus
.iter_mut()
.find(|&&mut (ref l, _)| l == label)
.map(|&mut (_, ref mut tree)| Rc::make_mut(tree))
}
/// 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> {
2017-06-12 23:39:12 +00:00
self.menus.iter().position(|&(ref l, _)| l == label)
}
/// Remove the item at the given position.
pub fn remove(&mut self, i: usize) {
self.menus.remove(i);
}
fn child_at(&self, x: usize) -> Option<usize> {
if x == 0 {
return None;
}
let mut offset = 1;
for (i, &(ref title, _)) in self.menus.iter().enumerate() {
offset += title.width() + 2;
if x < offset {
return Some(i);
}
}
None
}
fn select_child(&mut self) -> EventResult {
// First, we need a new Rc to send the callback,
// since we don't know when it will be called.
2017-10-13 18:22:02 +00:00
let menu = Rc::clone(&self.menus[self.focus].1);
self.state = State::Submenu;
let offset = (
self.menus[..self.focus]
.iter()
.map(|&(ref title, _)| title.width() + 2)
.fold(0, |a, b| a + b),
if self.autohide { 1 } else { 0 },
);
// Since the closure will be called multiple times,
// we also need a new Rc on every call.
2017-10-13 18:22:02 +00:00
EventResult::with_cb(move |s| show_child(s, offset, Rc::clone(&menu)))
}
2016-07-20 03:44:20 +00:00
}
2016-07-20 03:44:20 +00:00
fn show_child(s: &mut Cursive, offset: (usize, usize), 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
2017-06-12 23:39:12 +00:00
// consume it before our OnEventView. This means sub-menus can properly
2016-07-20 03:44:20 +00:00
// be entered.)
2017-10-12 23:38:55 +00:00
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| {
2017-06-12 23:39:12 +00:00
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)) =
2017-10-12 23:38:55 +00:00
s.menubar().on_event(Event::Key(Key::Down))
{
2017-06-12 23:39:12 +00:00
cb(s);
}
})
2017-10-12 23:38:55 +00:00
.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);
}
}),
);
2016-07-20 03:44:20 +00:00
}
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, &(ref title, _)) in self.menus.iter().enumerate() {
// We don't want to show HighlightInactive when we're not selected,
// because it's ugly on the menubar.
2017-10-12 23:38:55 +00:00
let selected =
(self.state != State::Inactive) && (i == self.focus);
printer.with_selection(selected, |printer| {
printer.print((offset, 0), &format!(" {} ", title));
});
offset += title.width() + 2;
}
}
2016-07-20 03:44:20 +00:00
fn on_event(&mut self, event: Event) -> EventResult {
match event {
Event::Key(Key::Esc) if self.autohide => {
2016-10-09 22:47:06 +00:00
self.hide();
return EventResult::with_cb(|s| s.clear());
}
2017-10-12 23:38:55 +00:00
Event::Key(Key::Left) => if self.focus > 0 {
self.focus -= 1
} else {
self.focus = self.menus.len() - 1
},
Event::Key(Key::Right) => if self.focus + 1 < self.menus.len() {
self.focus += 1
} else {
self.focus = 0
},
Event::Key(Key::Down) | Event::Key(Key::Enter) => {
return self.select_child();
}
Event::Mouse {
event: MouseEvent::Press(_),
position,
offset,
} if position.fits(offset) && position.y == offset.y =>
{
position
.checked_sub(offset)
.and_then(|pos| self.child_at(pos.x))
.map(|child| {
self.focus = child;
});
}
Event::Mouse {
event: MouseEvent::Press(_),
2017-10-13 18:22:02 +00:00
..
} => {
self.hide();
return EventResult::with_cb(|s| s.clear());
}
Event::Mouse {
event: MouseEvent::Release(MouseButton::Left),
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 {
return self.select_child();
}
}
}
2016-07-20 03:44:20 +00:00
_ => return EventResult::Ignored,
}
2016-07-20 03:44:20 +00:00
EventResult::Consumed(None)
}
2016-07-10 02:05:51 +00:00
2016-07-20 03:44:20 +00:00
fn take_focus(&mut self, _: direction::Direction) -> bool {
self.state = State::Selected;
true
}
fn required_size(&mut self, _: Vec2) -> Vec2 {
2016-07-20 03:44:20 +00:00
// TODO: scroll the options if the screen is too small?
2016-07-10 02:05:51 +00:00
2016-07-20 03:44:20 +00:00
// 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.menus
.iter()
.map(|&(ref title, _)| title.len() + 2)
.fold(1, |a, b| a + b);
Vec2::new(width, 1)
}
2016-07-10 02:05:51 +00:00
}