From 1d54764cdbc6d6aa1af0aab8d17151c637b62052 Mon Sep 17 00:00:00 2001 From: Alexandre Bury Date: Sat, 2 Jul 2016 15:02:42 -0700 Subject: [PATCH] Working menubar example Also update callback: use Rc instead of Box --- examples/menubar.rs | 25 +++++++---- src/event.rs | 10 ++++- src/lib.rs | 30 ++++++------- src/menu.rs | 38 ++++++---------- src/menubar.rs | 90 +++++++++++++++++++++++++++++++++++--- src/printer.rs | 1 + src/view/button.rs | 4 +- src/view/key_event_view.rs | 4 +- src/view/menu_popup.rs | 83 +++++++++++++++++++++++++++++++++-- src/view/select_view.rs | 10 ++--- src/view/shadow_view.rs | 51 ++++++++++++++++----- src/view/stack_view.rs | 13 +++--- 12 files changed, 274 insertions(+), 85 deletions(-) diff --git a/examples/menubar.rs b/examples/menubar.rs index 880b5c1..4d64dfc 100644 --- a/examples/menubar.rs +++ b/examples/menubar.rs @@ -1,24 +1,31 @@ extern crate cursive; use cursive::Cursive; +use cursive::menu::MenuTree; use cursive::view::Dialog; +use cursive::view::TextView; use cursive::event::Key; fn main() { let mut siv = Cursive::new(); - siv.menu() - .new_subtree("File") - .leaf("New", |s| s.add_layer(Dialog::info("New file!"))) - .leaf("Quit", |s| s.quit()); + siv.menubar() + .add("File", + MenuTree::new() + .leaf("New", |s| s.add_layer(Dialog::info("New file!"))) + .leaf("Quit", |s| s.quit())) + .add("Help", + MenuTree::new() + .leaf("Help", |s| s.add_layer(Dialog::info("Help message!"))) + .leaf("About", + |s| s.add_layer(Dialog::info("Cursive v0.0.0")))); - siv.menu() - .new_subtree("Help") - .leaf("Help", |s| s.add_layer(Dialog::info("Help message!"))) - .leaf("About", |s| s.add_layer(Dialog::info("Cursive v0.0.0"))); + // siv.set_autohide_menu(false); - siv.add_global_callback(Key::F(10), |s| s.select_menu()); + siv.add_global_callback(Key::F(10), |s| s.select_menubar()); + + siv.add_layer(Dialog::new(TextView::new("Hit to show the menu!"))); siv.run(); } diff --git a/src/event.rs b/src/event.rs index 4b40e54..2e285de 100644 --- a/src/event.rs +++ b/src/event.rs @@ -7,7 +7,7 @@ use Cursive; /// Callback is a function that can be triggered by an event. /// It has a mutable access to the cursive root. -pub type Callback = Box; +pub type Callback = Rc; /// Answer to an event notification. /// The event can be consumed or ignored. @@ -15,7 +15,13 @@ pub enum EventResult { /// The event was ignored. The parent can keep handling it. Ignored, /// The event was consumed. An optionnal callback to run is attached. - Consumed(Option>), + Consumed(Option), +} + +impl EventResult { + pub fn with_cb(f: F) -> Self { + EventResult::Consumed(Some(Rc::new(f))) + } } /// Represents a key, or a combination of keys. diff --git a/src/lib.rs b/src/lib.rs index 8cb64f7..a03ffbe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,8 +69,8 @@ pub type ScreenId = usize; pub struct Cursive { theme: theme::Theme, screens: Vec, - global_callbacks: HashMap>, - menu: menubar::Menubar, + global_callbacks: HashMap, + menubar: menubar::Menubar, active_screen: ScreenId, @@ -100,7 +100,7 @@ impl Cursive { theme: theme, screens: Vec::new(), global_callbacks: HashMap::new(), - menu: menubar::Menubar::new(), + menubar: menubar::Menubar::new(), active_screen: 0, running: true, }; @@ -111,8 +111,8 @@ impl Cursive { } /// Selects the menubar - pub fn select_menu(&mut self) { - self.menu.selected = true; + pub fn select_menubar(&mut self) { + self.menubar.take_focus(); } /// Sets the menubar autohide_menubar feature. @@ -120,12 +120,12 @@ impl Cursive { /// * When enabled, the menu is only visible when selected. /// * When disabled, the menu is always visible and reserves the top row. pub fn set_autohide_menu(&mut self, autohide: bool) { - self.menu.autohide = autohide; + self.menubar.autohide = autohide; } /// Retrieve the menu tree used by the menubar. - pub fn menu(&mut self) -> &mut menu::MenuTree { - &mut self.menu.menu + pub fn menubar(&mut self) -> &mut menubar::Menubar { + &mut self.menubar } /// Returns the currently used theme @@ -206,7 +206,7 @@ impl Cursive { pub fn add_global_callback(&mut self, event: E, cb: F) where F: Fn(&mut Cursive) + 'static { - self.global_callbacks.insert(event.to_event(), Rc::new(Box::new(cb))); + self.global_callbacks.insert(event.to_event(), Rc::new(cb)); } /// Convenient method to add a layer to the current screen. @@ -251,18 +251,18 @@ impl Cursive { // Draw the currently active screen // If the menubar is active, nothing else can be. - let offset = if self.menu.autohide { + let offset = if self.menubar.autohide { 0 } else { 1 }; - let selected = self.menu.selected; + let selected = self.menubar.receive_events(); self.screen_mut() .draw(&printer.sub_printer(Vec2::new(0, offset), printer.size, !selected)); // Draw the menubar? - if self.menu.selected || !self.menu.autohide { - self.menu.draw(&printer); + if self.menubar.visible() { + self.menubar.draw(&printer); } B::refresh(); @@ -295,8 +295,8 @@ impl Cursive { // * Menubar (if active) // * Current screen (top layer) // * Global callbacks - if self.menu.selected { - if let Some(cb) = self.menu.on_event(event) { + if self.menubar.receive_events() { + if let Some(cb) = self.menubar.on_event(event) { cb(self); } } else { diff --git a/src/menu.rs b/src/menu.rs index 0ef1d8a..e32c718 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -8,8 +8,8 @@ pub struct MenuTree { } pub enum MenuItem { - Leaf(String, Rc), - Subtree(String, Box), + Leaf(String, Callback), + Subtree(String, Rc), Delimiter, } @@ -21,6 +21,13 @@ impl MenuItem { MenuItem::Subtree(ref label, _) => label, } } + + pub fn is_delimiter(&self) -> bool { + match *self { + MenuItem::Delimiter => true, + _ => false, + } + } } impl MenuTree { @@ -40,37 +47,20 @@ impl MenuTree { self.children.is_empty() } - pub fn add_delimiter(&mut self) { - self.children.push(MenuItem::Delimiter); - } - pub fn delimiter(mut self) -> Self { - self.add_delimiter(); + self.children.push(MenuItem::Delimiter); self } - pub fn leaf(&mut self, title: &str, cb: F) -> &mut Self { + pub fn leaf(mut self, title: &str, cb: F) -> Self { self.children - .push(MenuItem::Leaf(title.to_string(), Rc::new(Box::new(cb)))); + .push(MenuItem::Leaf(title.to_string(), Rc::new(cb))); self } - pub fn add_subtree(&mut self, title: &str, tree: MenuTree) -> &mut Self { - self.children - .push(MenuItem::Subtree(title.to_string(), Box::new(tree))); - self - } - - pub fn new_subtree(&mut self, title: &str) -> &mut MenuTree { - self.add_subtree(title, MenuTree::new()); - match *self.children.last_mut().unwrap() { - MenuItem::Subtree(_, ref mut tree) => &mut *tree, - _ => panic!("??"), - } - } - pub fn subtree(mut self, title: &str, tree: MenuTree) -> Self { - self.add_subtree(title, tree); + self.children + .push(MenuItem::Subtree(title.to_string(), Rc::new(tree))); self } } diff --git a/src/menubar.rs b/src/menubar.rs index 8532b59..c76e5b1 100644 --- a/src/menubar.rs +++ b/src/menubar.rs @@ -1,25 +1,61 @@ -use menu::*; +use menu::MenuTree; +use view::MenuPopup; use theme::ColorStyle; use printer::Printer; +use view::Position; use event::*; use std::rc::Rc; +/// 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, +} + pub struct Menubar { - pub menu: MenuTree, + pub menus: Vec<(String, Rc)>, pub autohide: bool, - pub selected: bool, + pub focus: usize, + state: State, } impl Menubar { pub fn new() -> Self { Menubar { - menu: MenuTree::new(), + menus: Vec::new(), autohide: true, - selected: false, + state: State::Inactive, + focus: 0, } } + pub fn take_focus(&mut self) { + self.state = State::Selected; + } + + pub fn receive_events(&self) -> bool { + self.state == State::Selected + } + + pub fn visible(&self) -> bool { + !self.autohide || self.state != State::Inactive + } + + pub fn add(&mut self, title: &str, menu: MenuTree) -> &mut Self { + self.menus.push((title.to_string(), Rc::new(menu))); + self + } + pub fn draw(&mut self, printer: &Printer) { // Draw the bar at the top printer.with_color(ColorStyle::Primary, |printer| { @@ -27,10 +63,50 @@ impl Menubar { }); // 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. + let selected = (self.state != State::Inactive) && + (i == self.focus); + printer.with_selection(selected, |printer| { + printer.print((offset, 0), &format!(" {} ", title)); + offset += title.len() + 2; + }); + } } - pub fn on_event(&mut self, event: Event) -> Option> { - let _ = &event; + pub fn on_event(&mut self, event: Event) -> Option { + match event { + Event::Key(Key::Esc) => self.state = State::Inactive, + Event::Key(Key::Left) if self.focus > 0 => self.focus -= 1, + Event::Key(Key::Right) if self.focus + 1 < self.menus.len() => self.focus += 1, + Event::Key(Key::Enter) => { + // First, we need a new Rc to send the callback, + // since we don't know when it will be called. + let menu = self.menus[self.focus].1.clone(); + self.state = State::Submenu; + let offset = (self.menus[..self.focus] + .iter() + .map(|&(ref title, _)| title.len() + 2) + .fold(1, |a, b| a + b), + if self.autohide { + 1 + } else { + 0 + }); + return Some(Rc::new(move |s| { + // Since the closure will be called multiple times, + // we also need a new Rc on every call. + s.screen_mut() + .add_layer_at(Position::absolute(offset), + MenuPopup::new(menu.clone()) + .on_dismiss(|s| s.select_menubar()) + .on_action(|s| s.menubar().state = State::Inactive)); + })); + } + _ => (), + } None } } diff --git a/src/printer.rs b/src/printer.rs index ee0cf4e..3a79c33 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -10,6 +10,7 @@ use theme::{ColorStyle, Theme, Effect}; use vec::{ToVec2, Vec2}; /// Convenient interface to draw on a subset of the screen. +#[derive(Clone)] pub struct Printer { /// Offset into the window this printer should start drawing at. pub offset: Vec2, diff --git a/src/view/button.rs b/src/view/button.rs index 07b93b4..36cd172 100644 --- a/src/view/button.rs +++ b/src/view/button.rs @@ -11,7 +11,7 @@ use printer::Printer; /// A button shows its content in a single line and has a fixed size. pub struct Button { label: String, - callback: Rc, + callback: Callback, } impl Button { @@ -21,7 +21,7 @@ impl Button { { Button { label: label.to_string(), - callback: Rc::new(Box::new(cb)), + callback: Rc::new(cb), } } } diff --git a/src/view/key_event_view.rs b/src/view/key_event_view.rs index 4f393c7..69e611c 100644 --- a/src/view/key_event_view.rs +++ b/src/view/key_event_view.rs @@ -10,7 +10,7 @@ use super::{View, ViewWrapper}; /// Events ignored by its child without a callback will stay ignored. pub struct KeyEventView { content: Box, - callbacks: HashMap>, + callbacks: HashMap, } impl KeyEventView { @@ -26,7 +26,7 @@ impl KeyEventView { pub fn register(mut self, event: E, cb: F) -> Self where F: Fn(&mut Cursive) + 'static { - self.callbacks.insert(event.to_event(), Rc::new(Box::new(cb))); + self.callbacks.insert(event.to_event(), Rc::new(cb)); self } diff --git a/src/view/menu_popup.rs b/src/view/menu_popup.rs index 3139914..98ebbd5 100644 --- a/src/view/menu_popup.rs +++ b/src/view/menu_popup.rs @@ -2,12 +2,15 @@ use std::rc::Rc; use unicode_width::UnicodeWidthStr; +use Cursive; use menu::{MenuItem, MenuTree}; use printer::Printer; use view::View; +use view::Position; use view::scroll::ScrollBase; use align::Align; use vec::Vec2; +use event::{Callback, Event, EventResult, Key}; /// fd pub struct MenuPopup { @@ -15,6 +18,8 @@ pub struct MenuPopup { focus: usize, scrollbase: ScrollBase, align: Align, + on_dismiss: Option, + on_action: Option, } impl MenuPopup { @@ -24,6 +29,8 @@ impl MenuPopup { focus: 0, scrollbase: ScrollBase::new(), align: Align::top_left(), + on_dismiss: None, + on_action: None, } } @@ -33,6 +40,16 @@ impl MenuPopup { self } + + pub fn on_dismiss(mut self, f: F) -> Self { + self.on_dismiss = Some(Rc::new(f)); + self + } + + pub fn on_action(mut self, f: F) -> Self { + self.on_action = Some(Rc::new(f)); + self + } } impl View for MenuPopup { @@ -60,7 +77,8 @@ impl View for MenuPopup { } MenuItem::Subtree(ref label, _) | MenuItem::Leaf(ref label, _) => { - printer.print((2, 0), label) + printer.print_hline((1, 0), printer.size.x - 2, " "); + printer.print((2, 0), label); } } @@ -70,13 +88,14 @@ impl View for MenuPopup { fn get_min_size(&self, req: Vec2) -> Vec2 { // We can't really shrink our items here, so it's not flexible. - let w = self.menu + let w = 2 + + self.menu .children .iter() - .map(|item| item.label().width()) + .map(|item| 2 + item.label().width()) .max() .unwrap_or(1); - let h = self.menu.children.len(); + let h = 2 + self.menu.children.len(); let scrolling = req.y < h; @@ -89,4 +108,60 @@ impl View for MenuPopup { Vec2::new(w, h) } + + fn on_event(&mut self, event: Event) -> EventResult { + match event { + Event::Key(Key::Esc) => { + let dismiss_cb = self.on_dismiss.clone(); + return EventResult::with_cb(move |s| { + if let Some(ref cb) = dismiss_cb { + cb.clone()(s); + } + s.pop_layer(); + }); + } + Event::Key(Key::Up) if self.focus > 0 => self.focus -= 1, + Event::Key(Key::Down) if self.focus + 1 < + self.menu.children.len() => { + self.focus += 1 + } + Event::Key(Key::Enter) if !self.menu.children[self.focus] + .is_delimiter() => { + return match self.menu.children[self.focus] { + MenuItem::Leaf(_, ref cb) => { + + let cb = cb.clone(); + let action_cb = self.on_action.clone(); + EventResult::with_cb(move |s| { + if let Some(ref action_cb) = action_cb { + action_cb.clone()(s); + } + s.pop_layer(); + cb.clone()(s); + }) + } + MenuItem::Subtree(_, ref tree) => { + let tree = tree.clone(); + let offset = Vec2::new(10, self.focus + 1); + EventResult::with_cb(move |s| { + s.screen_mut() + .add_layer_at(Position::parent(offset), + MenuPopup::new(tree.clone())); + }) + } + _ => panic!("No delimiter here"), + }; + } + + _ => return EventResult::Ignored, + } + + self.scrollbase.scroll_to(self.focus); + + EventResult::Consumed(None) + } + + fn layout(&mut self, size: Vec2) { + self.scrollbase.set_heights(size.y, self.menu.children.len()); + } } diff --git a/src/view/select_view.rs b/src/view/select_view.rs index e657525..980f838 100644 --- a/src/view/select_view.rs +++ b/src/view/select_view.rs @@ -30,7 +30,8 @@ pub struct SelectView { items: Vec>, focus: usize, scrollbase: ScrollBase, - select_cb: Option>>, + // This is a custom callback to include a &T + select_cb: Option>, align: Align, } @@ -49,7 +50,7 @@ impl SelectView { pub fn set_on_select(&mut self, cb: F) where F: Fn(&mut Cursive, &T) + 'static { - self.select_cb = Some(Rc::new(Box::new(cb))); + self.select_cb = Some(Rc::new(cb)); } /// Sets a function to be called when an item is selected. @@ -180,9 +181,8 @@ impl View for SelectView { Event::Key(Key::Enter) if self.select_cb.is_some() => { let cb = self.select_cb.as_ref().unwrap().clone(); let v = self.selection(); - // We return a Rc> - // With callback being |s| cb(s, &*v) - return EventResult::Consumed(Some(Rc::new(Box::new(move |s| cb(s, &*v))))); + // We return a Callback Rc<|s| cb(s, &*v)> + return EventResult::Consumed(Some(Rc::new(move |s| cb(s, &*v)))); } Event::Char(c) => { // Starting from the current focus, diff --git a/src/view/shadow_view.rs b/src/view/shadow_view.rs index e068a54..cb0b6aa 100644 --- a/src/view/shadow_view.rs +++ b/src/view/shadow_view.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use view::{View, ViewWrapper}; use printer::Printer; use vec::Vec2; @@ -8,41 +10,70 @@ use theme::ColorStyle; /// It reserves a 1 pixel border on each side. pub struct ShadowView { view: T, + topleft_padding: bool, } impl ShadowView { /// Wraps the given view. pub fn new(view: T) -> Self { - ShadowView { view: view } + ShadowView { + view: view, + topleft_padding: true, + } } + + fn padding(&self) -> (usize, usize) { + if self.topleft_padding { + (2, 2) + } else { + (1, 1) + } + } + + pub fn no_topleft_padding(mut self) -> Self { + self.topleft_padding = false; + self + } + } impl ViewWrapper for ShadowView { wrap_impl!(&self.view); fn wrap_get_min_size(&self, req: Vec2) -> Vec2 { - self.view.get_min_size(req - (2, 2)) + (2, 2) + let offset = self.padding(); + self.view.get_min_size(req - offset) + offset } fn wrap_layout(&mut self, size: Vec2) { - self.view.layout(size - (2, 2)); + let offset = self.padding(); + self.view.layout(size - offset); } fn wrap_draw(&mut self, printer: &Printer) { + // Skip the first row/column + let printer = if self.topleft_padding { + Cow::Owned(printer.sub_printer(Vec2::new(1, 1), printer.size, true)) + } else { + Cow::Borrowed(printer) + }; + // Draw the view background - for y in 1..printer.size.y - 1 { - printer.print_hline((1, y), printer.size.x - 2, " "); + for y in 0..printer.size.y - 1 { + printer.print_hline((0, y), printer.size.x - 1, " "); } - self.view.draw(&printer.sub_printer(Vec2::new(1, 1), printer.size - (2, 2), true)); + self.view.draw(&printer.sub_printer(Vec2::zero(), + printer.size - (1, 1), + true)); - let h = printer.size.y - 1; - let w = printer.size.x - 1; + let h = printer.size.y; + let w = printer.size.x; printer.with_color(ColorStyle::Shadow, |printer| { - printer.print_hline((2, h), w - 1, " "); - printer.print_vline((w, 2), h - 1, " "); + printer.print_hline((1, h-1), w - 1, " "); + printer.print_vline((w-1, 1), h - 1, " "); }); } } diff --git a/src/view/stack_view.rs b/src/view/stack_view.rs index 54c34b0..575f0d7 100644 --- a/src/view/stack_view.rs +++ b/src/view/stack_view.rs @@ -1,7 +1,7 @@ use std::any::Any; use vec::Vec2; -use view::{Selector, ShadowView, View, Position}; +use view::{Position, Selector, ShadowView, View}; use event::{Event, EventResult}; use printer::Printer; use theme::ColorStyle; @@ -38,9 +38,10 @@ impl StackView { } /// Adds a view on top of the stack. - pub fn add_layer_at(&mut self, position: Position, view: T) { + pub fn add_layer_at(&mut self, position: Position, + view: T) { self.layers.push(Layer { - view: Box::new(ShadowView::new(view)), + view: Box::new(ShadowView::new(view).no_topleft_padding()), size: Vec2::new(0, 0), position: position, virgin: true, @@ -61,9 +62,11 @@ impl View for StackView { for (i, v) in self.layers.iter_mut().enumerate() { // Place the view // Center the view - let offset = v.position.compute_offset(v.size, printer.size, previous); + let offset = v.position + .compute_offset(v.size, printer.size, previous); previous = offset; - v.view.draw(&printer.sub_printer(offset, v.size, i + 1 == last)); + v.view + .draw(&printer.sub_printer(offset, v.size, i + 1 == last)); } }); }