Add enabled state to menu items

This commit is contained in:
Alexandre Bury 2021-02-05 13:05:14 -08:00
parent 06d64be0a0
commit 6543287704
5 changed files with 246 additions and 111 deletions

View File

@ -2,80 +2,159 @@
//!
//! Menus are a way to arrange many actions in groups of more manageable size.
//!
//! A menu can be seen as a [`MenuTree`]. It has a list of children:
//! A menu can be seen as a [`Tree`]. It has a list of children:
//!
//! * Leaf nodes are made of a label and a callback
//! * Sub-trees are made of a label, and another `MenuTree`.
//! * Sub-trees are made of a label, and another `Tree`.
//! * Delimiters are just there to separate groups of related children.
//!
//! The [menubar] is the main way to show menus.
//!
//! [`MenuTree`]: struct.MenuTree.html
//! [`Tree`]: struct.Tree.html
//! [menubar]: ../struct.Cursive.html#method.menubar
use crate::event::Callback;
use crate::Cursive;
use crate::With;
use crate::{event::Callback, Cursive, With};
use std::rc::Rc;
/// Root of a menu tree.
#[derive(Default, Clone)]
pub struct MenuTree {
pub struct Tree {
/// Menu items
pub children: Vec<MenuItem>,
pub children: Vec<Item>,
}
/// Node in the menu tree.
#[derive(Clone)]
pub enum MenuItem {
pub enum Item {
/// Actionnable button with a label.
Leaf(String, Callback),
Leaf {
/// Text displayed for this entry.
label: String,
/// Callback to run when the entry is selected.
cb: Callback,
/// Whether this item is enabled.
///
/// Disabled items cannot be selected and are displayed grayed out.
enabled: bool,
},
/// Sub-menu with a label.
Subtree(String, Rc<MenuTree>),
Subtree {
/// Text displayed for this entry.
label: String,
/// Subtree under this item.
tree: Rc<Tree>,
/// Whether this item is enabled.
///
/// Disabled items cannot be selected and are displayed grayed out.
enabled: bool,
},
/// Delimiter without a label.
Delimiter,
}
impl MenuItem {
impl Item {
/// Create a new leaf menu item.
pub fn leaf<S, F>(label: S, cb: F) -> Self
where
S: Into<String>,
F: 'static + Fn(&mut Cursive),
{
let label = label.into();
let cb = Callback::from_fn(cb);
let enabled = true;
Item::Leaf { label, cb, enabled }
}
/// Create a new subtree menu item.
pub fn subtree<S>(label: S, tree: Tree) -> Self
where
S: Into<String>,
{
let label = label.into();
let tree = Rc::new(tree);
let enabled = true;
Item::Subtree {
label,
tree,
enabled,
}
}
/// Returns the label for this item.
///
/// Returns an empty string if `self` is a delimiter.
/// Returns a vertical bar string if `self` is a delimiter.
pub fn label(&self) -> &str {
match *self {
MenuItem::Delimiter => "",
MenuItem::Leaf(ref label, _) | MenuItem::Subtree(ref label, _) => {
Item::Delimiter => "",
Item::Leaf { ref label, .. } | Item::Subtree { ref label, .. } => {
label
}
}
}
/// Returns true if this item is enabled.
///
/// Only labels and subtrees can be enabled. Delimiters
pub fn is_enabled(&self) -> bool {
match *self {
Item::Leaf { enabled, .. } | Item::Subtree { enabled, .. } => {
enabled
}
Item::Delimiter => false,
}
}
/// Return a disabled version of this item.
pub fn disabled(self) -> Self {
self.with(Self::disable)
}
/// Disable this item.
///
/// Disabled items cannot be selected and are shown grayed out.
///
/// Does not affect delimiters.
pub fn disable(&mut self) {
if let Item::Leaf {
ref mut enabled, ..
}
| Item::Subtree {
ref mut enabled, ..
} = self
{
*enabled = false;
}
}
/// Returns `true` if `self` is a delimiter.
pub fn is_delimiter(&self) -> bool {
matches!(*self, MenuItem::Delimiter)
matches!(*self, Item::Delimiter)
}
/// Returns `true` if `self` is a leaf node.
pub fn is_leaf(&self) -> bool {
matches!(*self, MenuItem::Leaf(_, _))
matches!(*self, Item::Leaf { .. })
}
/// Returns `true` if `self` is a subtree.
pub fn is_subtree(&self) -> bool {
matches!(*self, MenuItem::Subtree(_, _))
matches!(*self, Item::Subtree { .. })
}
/// Return a mutable reference to the subtree, if applicable.
///
/// Returns `None` if `self` is not a `MenuItem::Subtree`.
pub fn as_subtree(&mut self) -> Option<&mut MenuTree> {
/// Returns `None` if `self` is not a `Item::Subtree`.
pub fn as_subtree(&mut self) -> Option<&mut Tree> {
match *self {
MenuItem::Subtree(_, ref mut tree) => Some(Rc::make_mut(tree)),
Item::Subtree { ref mut tree, .. } => Some(Rc::make_mut(tree)),
_ => None,
}
}
}
impl MenuTree {
impl Tree {
/// Creates a new, empty tree.
pub fn new() -> Self {
Self::default()
@ -87,13 +166,13 @@ impl MenuTree {
}
/// Inserts an item at the given position.
pub fn insert(&mut self, i: usize, item: MenuItem) {
pub fn insert(&mut self, i: usize, item: Item) {
self.children.insert(i, item);
}
/// Inserts a delimiter at the given position.
pub fn insert_delimiter(&mut self, i: usize) {
self.insert(i, MenuItem::Delimiter);
self.insert(i, Item::Delimiter);
}
/// Adds a delimiter to the end of this tree.
@ -108,100 +187,123 @@ impl MenuTree {
}
/// Adds a actionnable leaf to the end of this tree.
pub fn add_leaf<S, F>(&mut self, title: S, cb: F)
pub fn add_leaf<S, F>(&mut self, label: S, cb: F)
where
S: Into<String>,
F: 'static + Fn(&mut Cursive),
{
let i = self.children.len();
self.insert_leaf(i, title, cb);
self.insert_leaf(i, label, cb);
}
/// Inserts a leaf at the given position.
pub fn insert_leaf<S, F>(&mut self, i: usize, title: S, cb: F)
pub fn insert_leaf<S, F>(&mut self, i: usize, label: S, cb: F)
where
S: Into<String>,
F: 'static + Fn(&mut Cursive),
{
let title = title.into();
self.insert(i, MenuItem::Leaf(title, Callback::from_fn(cb)));
let label = label.into();
self.insert(
i,
Item::Leaf {
label,
cb: Callback::from_fn(cb),
enabled: true,
},
);
}
/// Adds a actionnable leaf to the end of this tree - chainable variant.
pub fn leaf<S, F>(self, title: S, cb: F) -> Self
pub fn leaf<S, F>(self, label: S, cb: F) -> Self
where
S: Into<String>,
F: 'static + Fn(&mut Cursive),
{
self.with(|menu| menu.add_leaf(title, cb))
self.with(|menu| menu.add_leaf(label, cb))
}
/// Inserts a subtree at the given position.
pub fn insert_subtree<S>(&mut self, i: usize, title: S, tree: MenuTree)
pub fn insert_subtree<S>(&mut self, i: usize, label: S, tree: Tree)
where
S: Into<String>,
{
let title = title.into();
let tree = MenuItem::Subtree(title, Rc::new(tree));
let label = label.into();
let tree = Item::Subtree {
label,
tree: Rc::new(tree),
enabled: true,
};
self.insert(i, tree);
}
/// Adds an item to the end of this tree.
///
/// Chainable variant.
pub fn item(self, item: Item) -> Self {
self.with(|s| s.add_item(item))
}
/// Adds an item to the end of this tree.
pub fn add_item(&mut self, item: Item) {
let i = self.children.len();
self.insert(i, item);
}
/// Adds a submenu to the end of this tree.
pub fn add_subtree<S>(&mut self, title: S, tree: MenuTree)
pub fn add_subtree<S>(&mut self, label: S, tree: Tree)
where
S: Into<String>,
{
let i = self.children.len();
self.insert_subtree(i, title, tree);
self.insert_subtree(i, label, tree);
}
/// Adds a submenu to the end of this tree - chainable variant.
pub fn subtree<S>(self, title: S, tree: MenuTree) -> Self
pub fn subtree<S>(self, label: S, tree: Tree) -> Self
where
S: Into<String>,
{
self.with(|menu| menu.add_subtree(title, tree))
self.with(|menu| menu.add_subtree(label, tree))
}
/// Looks for the child at the given position.
///
/// Returns `None` if `i >= self.len()`.
pub fn get_mut(&mut self, i: usize) -> Option<&mut MenuItem> {
pub fn get_mut(&mut self, i: usize) -> Option<&mut Item> {
self.children.get_mut(i)
}
/// Returns the item at the given position.
///
/// Returns `None` if `i > self.len()` or if the item is not a subtree.
pub fn get_subtree(&mut self, i: usize) -> Option<&mut MenuTree> {
self.get_mut(i).and_then(MenuItem::as_subtree)
pub fn get_subtree(&mut self, i: usize) -> Option<&mut Tree> {
self.get_mut(i).and_then(Item::as_subtree)
}
/// Looks for a child with the given title.
/// Looks for a child with the given label.
///
/// Returns `None` if no such label was found.
pub fn find_item(&mut self, title: &str) -> Option<&mut MenuItem> {
pub fn find_item(&mut self, label: &str) -> Option<&mut Item> {
self.children
.iter_mut()
.find(|child| child.label() == title)
.find(|child| child.label() == label)
}
/// Looks for a subtree with the given title.
pub fn find_subtree(&mut self, title: &str) -> Option<&mut MenuTree> {
/// Looks for a subtree with the given label.
pub fn find_subtree(&mut self, label: &str) -> Option<&mut Tree> {
self.children
.iter_mut()
.filter(|child| child.label() == title)
.filter_map(MenuItem::as_subtree)
.next()
.filter(|child| child.label() == label)
.find_map(Item::as_subtree)
}
/// Returns the position of a child with the given label.
///
/// Returns `None` if no such label was found.
pub fn find_position(&mut self, title: &str) -> Option<usize> {
pub fn find_position(&mut self, label: &str) -> Option<usize> {
self.children
.iter()
.position(|child| child.label() == title)
.position(|child| child.label() == label)
}
/// Removes the item at the given position.

View File

@ -1,16 +1,14 @@
use crate::align::Align;
use crate::event::{
Callback, Event, EventResult, Key, MouseButton, MouseEvent,
use crate::{
align::Align,
event::{Callback, Event, EventResult, Key, MouseButton, MouseEvent},
menu,
rect::Rect,
theme::ColorStyle,
view::scroll,
view::{Position, View},
views::OnEventView,
Cursive, Printer, Vec2, With,
};
use crate::menu::{MenuItem, MenuTree};
use crate::rect::Rect;
use crate::view::scroll;
use crate::view::{Position, View};
use crate::views::OnEventView;
use crate::Cursive;
use crate::Printer;
use crate::Vec2;
use crate::With;
use std::cmp::min;
use std::rc::Rc;
use unicode_width::UnicodeWidthStr;
@ -23,7 +21,7 @@ use unicode_width::UnicodeWidthStr;
/// [1]: crate::views::SelectView::popup()
/// [2]: crate::Cursive::menubar()
pub struct MenuPopup {
menu: Rc<MenuTree>,
menu: Rc<menu::Tree>,
focus: usize,
scroll_core: scroll::Core,
align: Align,
@ -38,7 +36,7 @@ impl_scroller!(MenuPopup::scroll_core);
impl MenuPopup {
/// Creates a new `MenuPopup` using the given menu tree.
pub fn new(menu: Rc<MenuTree>) -> Self {
pub fn new(menu: Rc<menu::Tree>) -> Self {
MenuPopup {
menu,
focus: 0,
@ -66,11 +64,11 @@ impl MenuPopup {
self.focus
}
fn item_width(item: &MenuItem) -> usize {
fn item_width(item: &menu::Item) -> usize {
match *item {
MenuItem::Delimiter => 1,
MenuItem::Leaf(ref title, _) => title.width(),
MenuItem::Subtree(ref title, _) => title.width() + 3,
menu::Item::Delimiter => 1,
menu::Item::Leaf { ref label, .. } => label.width(),
menu::Item::Subtree { ref label, .. } => label.width() + 3,
}
}
@ -132,7 +130,7 @@ impl MenuPopup {
break;
}
if !self.menu.children[self.focus].is_delimiter() {
if self.menu.children[self.focus].is_enabled() {
n -= 1;
}
}
@ -149,7 +147,7 @@ impl MenuPopup {
break;
}
if !self.menu.children[self.focus].is_delimiter() {
if self.menu.children[self.focus].is_enabled() {
n -= 1;
}
}
@ -157,7 +155,7 @@ impl MenuPopup {
fn submit(&mut self) -> EventResult {
match self.menu.children[self.focus] {
MenuItem::Leaf(_, ref cb) => {
menu::Item::Leaf { ref cb, .. } => {
let cb = cb.clone();
let action_cb = self.on_action.clone();
EventResult::with_cb(move |s| {
@ -171,7 +169,7 @@ impl MenuPopup {
cb.clone()(s);
})
}
MenuItem::Subtree(_, ref tree) => self.make_subtree_cb(tree),
menu::Item::Subtree { ref tree, .. } => self.make_subtree_cb(tree),
_ => unreachable!("Delimiters cannot be submitted."),
}
}
@ -186,7 +184,7 @@ impl MenuPopup {
})
}
fn make_subtree_cb(&self, tree: &Rc<MenuTree>) -> EventResult {
fn make_subtree_cb(&self, tree: &Rc<menu::Tree>) -> EventResult {
let tree = Rc::clone(tree);
let max_width = 4 + self
.menu
@ -239,14 +237,14 @@ impl MenuPopup {
if self.menu.children[self.focus].is_subtree() =>
{
return match self.menu.children[self.focus] {
MenuItem::Subtree(_, ref tree) => {
menu::Item::Subtree { ref tree, .. } => {
self.make_subtree_cb(tree)
}
_ => unreachable!("Child is a subtree"),
};
}
Event::Key(Key::Enter)
if !self.menu.children[self.focus].is_delimiter() =>
if self.menu.children[self.focus].is_enabled() =>
{
return self.submit();
}
@ -260,7 +258,7 @@ impl MenuPopup {
// Now `position` is relative to the top-left of the content.
let focus = position.y;
if focus < self.menu.len()
&& !self.menu.children[focus].is_delimiter()
&& self.menu.children[focus].is_enabled()
{
self.focus = focus;
}
@ -270,7 +268,7 @@ impl MenuPopup {
event: MouseEvent::Release(MouseButton::Left),
position,
offset,
} if !self.menu.children[self.focus].is_delimiter()
} if self.menu.children[self.focus].is_enabled()
&& position
.checked_sub(offset)
.map(|position| position.y == self.focus)
@ -335,14 +333,23 @@ impl View for MenuPopup {
let printer = printer.shrinked_centered((2, 2));
scroll::draw_lines(self, &printer, |s, printer, i| {
printer.with_selection(i == s.focus, |printer| {
let item = &s.menu.children[i];
let item = &s.menu.children[i];
let enabled =
printer.enabled && (item.is_enabled() || item.is_delimiter());
let color = if !enabled {
ColorStyle::secondary()
} else if i == s.focus {
ColorStyle::highlight()
} else {
ColorStyle::primary()
};
printer.with_style(color, |printer| {
match *item {
MenuItem::Delimiter => {
menu::Item::Delimiter => {
// printer.print_hdelim((0, 0), printer.size.x)
printer.print_hline((0, 0), printer.size.x, "");
}
MenuItem::Subtree(ref label, _) => {
menu::Item::Subtree { ref label, .. } => {
if printer.size.x < 4 {
return;
}
@ -351,7 +358,7 @@ impl View for MenuPopup {
let x = printer.size.x.saturating_sub(3);
printer.print((x, 0), ">>");
}
MenuItem::Leaf(ref label, _) => {
menu::Item::Leaf { ref label, .. } => {
if printer.size.x < 2 {
return;
}

View File

@ -1,6 +1,6 @@
use crate::direction;
use crate::event::*;
use crate::menu::{MenuItem, MenuTree};
use crate::menu;
use crate::rect::Rect;
use crate::theme::ColorStyle;
use crate::view::{Position, View};
@ -34,7 +34,7 @@ enum State {
/// [`Cursive`]: crate::Cursive::menubar
pub struct Menubar {
/// Menu items in this menubar.
root: MenuTree,
root: menu::Tree,
/// TODO: move this out of this view.
pub autohide: bool,
@ -50,7 +50,7 @@ impl Menubar {
/// Creates a new, empty menubar.
pub fn new() -> Self {
Menubar {
root: MenuTree::new(),
root: menu::Tree::new(),
autohide: true,
state: State::Inactive,
focus: 0,
@ -77,11 +77,23 @@ impl Menubar {
!self.autohide || self.state != State::Inactive
}
/// Adds a new item to the menubar.
pub fn insert(&mut self, i: usize, item: menu::Item) -> &mut Self {
self.root.insert(i, item);
self
}
/// Adds a new item to the menubar.
pub fn item(&mut self, item: menu::Item) -> &mut Self {
let i = self.root.len();
self.insert(i, item)
}
/// 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
pub fn add_subtree<S>(&mut self, title: S, menu: menu::Tree) -> &mut Self
where
S: Into<String>,
{
@ -110,7 +122,7 @@ impl Menubar {
&mut self,
i: usize,
title: S,
menu: MenuTree,
menu: menu::Tree,
) -> &mut Self
where
S: Into<String>,
@ -158,12 +170,12 @@ impl Menubar {
/// Returns the item at the given position.
///
/// Returns `None` if `i > self.len()`
pub fn get_subtree(&mut self, i: usize) -> Option<&mut MenuTree> {
pub fn get_subtree(&mut self, i: usize) -> Option<&mut menu::Tree> {
self.root.get_subtree(i)
}
/// Looks for an item with the given label.
pub fn find_subtree(&mut self, label: &str) -> Option<&mut MenuTree> {
pub fn find_subtree(&mut self, label: &str) -> Option<&mut menu::Tree> {
self.root.find_subtree(label)
}
@ -197,12 +209,12 @@ impl Menubar {
fn select_child(&mut self, open_only: bool) -> EventResult {
match self.root.children[self.focus] {
MenuItem::Leaf(_, ref cb) if !open_only => {
menu::Item::Leaf { ref cb, .. } if !open_only => {
// Go inactive after an action.
self.state = State::Inactive;
EventResult::Consumed(Some(cb.clone()))
}
MenuItem::Subtree(_, ref tree) => {
menu::Item::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);
@ -226,7 +238,7 @@ impl Menubar {
}
}
fn show_child(s: &mut Cursive, offset: Vec2, menu: Rc<MenuTree>) {
fn show_child(s: &mut Cursive, offset: Vec2, menu: Rc<menu::Tree>) {
// 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.
@ -275,16 +287,30 @@ impl View for Menubar {
// TODO: draw the rest
let mut offset = 1;
for (i, item) in self.root.children.iter().enumerate() {
let title = item.label();
let label = item.label();
// We print disabled items differently, except delimiters,
// which are still white.
let enabled =
printer.enabled && (item.is_enabled() || item.is_delimiter());
// 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));
let color = if !enabled {
ColorStyle::secondary()
} else if selected {
ColorStyle::highlight()
} else {
ColorStyle::primary()
};
printer.with_style(color, |printer| {
printer.print((offset, 0), &format!(" {} ", label));
});
offset += title.width() + 2;
offset += label.width() + 2;
}
}
@ -295,12 +321,13 @@ impl View for Menubar {
return EventResult::with_cb(Cursive::clear);
}
Event::Key(Key::Left) => loop {
// TODO: fix endless loop if nothing is enabled?
if self.focus > 0 {
self.focus -= 1;
} else {
self.focus = self.root.len() - 1;
}
if !self.root.children[self.focus].is_delimiter() {
if self.root.children[self.focus].is_enabled() {
break;
}
},
@ -310,7 +337,7 @@ impl View for Menubar {
} else {
self.focus = 0;
}
if !self.root.children[self.focus].is_delimiter() {
if self.root.children[self.focus].is_enabled() {
break;
}
},
@ -329,7 +356,7 @@ impl View for Menubar {
.checked_sub(offset)
.and_then(|pos| self.child_at(pos.x))
{
if !self.root.children[child].is_delimiter() {
if self.root.children[child].is_enabled() {
self.focus = child;
if btn == MouseButton::Left {
return self.select_child(true);

View File

@ -3,7 +3,7 @@ use crate::direction::Direction;
use crate::event::{
Callback, Event, EventResult, Key, MouseButton, MouseEvent,
};
use crate::menu::MenuTree;
use crate::menu;
use crate::rect::Rect;
use crate::theme::ColorStyle;
use crate::utils::markup::StyledString;
@ -713,7 +713,7 @@ impl<T: 'static> SelectView<T> {
fn open_popup(&mut self) -> EventResult {
// Build a shallow menu tree to mimick the items array.
// TODO: cache it?
let mut tree = MenuTree::new();
let mut tree = menu::Tree::new();
for (i, item) in self.items.iter().enumerate() {
let focus = Rc::clone(&self.focus);
let on_submit = self.on_submit.as_ref().cloned();

View File

@ -1,7 +1,4 @@
use cursive::event::Key;
use cursive::menu::MenuTree;
use cursive::traits::*;
use cursive::views::Dialog;
use cursive::{event::Key, menu, traits::*, views::Dialog};
use std::sync::atomic::{AtomicUsize, Ordering};
// This examples shows how to configure and use a menubar at the top of the
@ -18,7 +15,7 @@ fn main() {
// We add a new "File" tree
.add_subtree(
"File",
MenuTree::new()
menu::Tree::new()
// Trees are made of leaves, with are directly actionable...
.leaf("New", move |s| {
// Here we use the counter to add an entry
@ -39,11 +36,13 @@ fn main() {
"Recent",
// The `.with()` method can help when running loops
// within builder patterns.
MenuTree::new().with(|tree| {
menu::Tree::new().with(|tree| {
for i in 1..100 {
// We don't actually do anything here,
// but you could!
tree.add_leaf(format!("Item {}", i), |_| ())
tree.add_item(menu::Item::leaf(format!("Item {}", i), |_| ()).with(|s| {
if i % 5 == 0 { s.disable(); }
}))
}
}),
)
@ -58,10 +57,10 @@ fn main() {
)
.add_subtree(
"Help",
MenuTree::new()
menu::Tree::new()
.subtree(
"Help",
MenuTree::new()
menu::Tree::new()
.leaf("General", |s| {
s.add_layer(Dialog::info("Help message!"))
})