From 65432877043b166706b5dbe499afdb99f1241b56 Mon Sep 17 00:00:00 2001 From: Alexandre Bury Date: Fri, 5 Feb 2021 13:05:14 -0800 Subject: [PATCH] Add enabled state to menu items --- cursive-core/src/menu.rs | 204 +++++++++++++++++++------- cursive-core/src/views/menu_popup.rs | 71 +++++---- cursive-core/src/views/menubar.rs | 61 +++++--- cursive-core/src/views/select_view.rs | 4 +- examples/src/bin/menubar.rs | 17 +-- 5 files changed, 246 insertions(+), 111 deletions(-) diff --git a/cursive-core/src/menu.rs b/cursive-core/src/menu.rs index 0b8c092..66b8579 100644 --- a/cursive-core/src/menu.rs +++ b/cursive-core/src/menu.rs @@ -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, + pub children: Vec, } /// 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), + Subtree { + /// Text displayed for this entry. + label: String, + /// Subtree under this item. + tree: Rc, + /// 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(label: S, cb: F) -> Self + where + S: Into, + 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(label: S, tree: Tree) -> Self + where + S: Into, + { + 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(&mut self, title: S, cb: F) + pub fn add_leaf(&mut self, label: S, cb: F) where S: Into, 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(&mut self, i: usize, title: S, cb: F) + pub fn insert_leaf(&mut self, i: usize, label: S, cb: F) where S: Into, 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(self, title: S, cb: F) -> Self + pub fn leaf(self, label: S, cb: F) -> Self where S: Into, 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(&mut self, i: usize, title: S, tree: MenuTree) + pub fn insert_subtree(&mut self, i: usize, label: S, tree: Tree) where S: Into, { - 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(&mut self, title: S, tree: MenuTree) + pub fn add_subtree(&mut self, label: S, tree: Tree) where S: Into, { 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(self, title: S, tree: MenuTree) -> Self + pub fn subtree(self, label: S, tree: Tree) -> Self where S: Into, { - 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 { + pub fn find_position(&mut self, label: &str) -> Option { self.children .iter() - .position(|child| child.label() == title) + .position(|child| child.label() == label) } /// Removes the item at the given position. diff --git a/cursive-core/src/views/menu_popup.rs b/cursive-core/src/views/menu_popup.rs index 1047491..796a439 100644 --- a/cursive-core/src/views/menu_popup.rs +++ b/cursive-core/src/views/menu_popup.rs @@ -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, + menu: Rc, 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) -> Self { + pub fn new(menu: Rc) -> 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) -> EventResult { + fn make_subtree_cb(&self, tree: &Rc) -> 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; } diff --git a/cursive-core/src/views/menubar.rs b/cursive-core/src/views/menubar.rs index 50ebccb..3a4be69 100644 --- a/cursive-core/src/views/menubar.rs +++ b/cursive-core/src/views/menubar.rs @@ -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(&mut self, title: S, menu: MenuTree) -> &mut Self + pub fn add_subtree(&mut self, title: S, menu: menu::Tree) -> &mut Self where S: Into, { @@ -110,7 +122,7 @@ impl Menubar { &mut self, i: usize, title: S, - menu: MenuTree, + menu: menu::Tree, ) -> &mut Self where S: Into, @@ -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) { +fn show_child(s: &mut Cursive, offset: Vec2, menu: Rc) { // 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); diff --git a/cursive-core/src/views/select_view.rs b/cursive-core/src/views/select_view.rs index 935f8b7..d25a2ec 100644 --- a/cursive-core/src/views/select_view.rs +++ b/cursive-core/src/views/select_view.rs @@ -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 SelectView { 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(); diff --git a/examples/src/bin/menubar.rs b/examples/src/bin/menubar.rs index f382bdf..02f6fcc 100644 --- a/examples/src/bin/menubar.rs +++ b/examples/src/bin/menubar.rs @@ -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!")) })