From 8220fe529ede69f121caa03efb5ec6b8cb7a48b2 Mon Sep 17 00:00:00 2001 From: Alexandre Bury Date: Sat, 2 Jul 2016 19:37:38 -0700 Subject: [PATCH] Better menubar example Added scrolling support, and left/right navigation --- examples/menubar.rs | 29 +++++++++-- src/lib.rs | 10 ++-- src/menu.rs | 7 +++ src/menubar.rs | 44 +++++++++++++++-- src/view/menu_popup.rs | 107 +++++++++++++++++++++++++++++++++-------- src/view/position.rs | 5 +- src/view/scroll.rs | 15 ++++-- src/view/stack_view.rs | 3 +- 8 files changed, 183 insertions(+), 37 deletions(-) diff --git a/examples/menubar.rs b/examples/menubar.rs index 4d64dfc..8a75e24 100644 --- a/examples/menubar.rs +++ b/examples/menubar.rs @@ -14,18 +14,41 @@ fn main() { .add("File", MenuTree::new() .leaf("New", |s| s.add_layer(Dialog::info("New file!"))) + .subtree("Recent", + MenuTree::new() + .leaf("Item 1", |s| ()) + .leaf("Item 1", |s| ()) + .leaf("Item 1", |s| ()) + .leaf("Item 1", |s| ())) + .delimiter() + .leaf("Item 1", |s| ()) + .leaf("Item 1", |s| ()) + .leaf("Item 1", |s| ()) + .leaf("Item 1", |s| ()) + .leaf("Item 1", |s| ()) + .leaf("Item 1", |s| ()) + .delimiter() .leaf("Quit", |s| s.quit())) .add("Help", MenuTree::new() - .leaf("Help", |s| s.add_layer(Dialog::info("Help message!"))) + .subtree("Help", + MenuTree::new() + .leaf("General", |s| { + s.add_layer(Dialog::info("Help message!")) + }) + .leaf("Online", |s| { + s.add_layer(Dialog::info("Google it \ + yourself!\nKids, \ + these days...")) + })) .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_menubar()); + siv.add_global_callback(Key::Esc, |s| s.select_menubar()); - siv.add_layer(Dialog::new(TextView::new("Hit to show the menu!"))); + siv.add_layer(Dialog::new(TextView::new("Hit to show the menu!"))); siv.run(); } diff --git a/src/lib.rs b/src/lib.rs index a03ffbe..353aa3f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -256,15 +256,17 @@ impl Cursive { } else { 1 }; - 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.menubar.visible() { + let printer = printer.sub_printer(Vec2::zero(), printer.size, self.menubar.receive_events()); self.menubar.draw(&printer); } + let selected = self.menubar.receive_events(); + + self.screen_mut() + .draw(&printer.sub_printer(Vec2::new(0, offset), printer.size, !selected)); + B::refresh(); } diff --git a/src/menu.rs b/src/menu.rs index e32c718..38c5fed 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -28,6 +28,13 @@ impl MenuItem { _ => false, } } + + pub fn is_subtree(&self) -> bool { + match *self { + MenuItem::Subtree(_,_) => true, + _ => false, + } + } } impl MenuTree { diff --git a/src/menubar.rs b/src/menubar.rs index c76e5b1..4df8557 100644 --- a/src/menubar.rs +++ b/src/menubar.rs @@ -1,5 +1,6 @@ use menu::MenuTree; use view::MenuPopup; +use view::KeyEventView; use theme::ColorStyle; use printer::Printer; use view::Position; @@ -79,8 +80,21 @@ impl Menubar { 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::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) => { // First, we need a new Rc to send the callback, // since we don't know when it will be called. @@ -89,7 +103,7 @@ impl Menubar { let offset = (self.menus[..self.focus] .iter() .map(|&(ref title, _)| title.len() + 2) - .fold(1, |a, b| a + b), + .fold(0, |a, b| a + b), if self.autohide { 1 } else { @@ -100,9 +114,29 @@ impl Menubar { // we also need a new Rc on every call. s.screen_mut() .add_layer_at(Position::absolute(offset), - MenuPopup::new(menu.clone()) + KeyEventView::new(MenuPopup::new(menu.clone()) .on_dismiss(|s| s.select_menubar()) - .on_action(|s| s.menubar().state = State::Inactive)); + .on_action(|s| { + s.menubar().state = State::Inactive + })) + .register(Key::Right, |s| { + s.pop_layer(); + // Act as if we sent "Left" then "Enter" + s.select_menubar(); + s.menubar().on_event(Event::Key(Key::Right)); + if let Some(cb) = s.menubar().on_event(Event::Key(Key::Down)) { + cb(s); + } + }) + .register(Key::Left, |s| { + s.pop_layer(); + // Act as if we sent "Left" then "Enter" + s.select_menubar(); + s.menubar().on_event(Event::Key(Key::Left)); + if let Some(cb) = s.menubar().on_event(Event::Key(Key::Down)) { + cb(s); + } + })); })); } _ => (), diff --git a/src/view/menu_popup.rs b/src/view/menu_popup.rs index 98ebbd5..85ba52f 100644 --- a/src/view/menu_popup.rs +++ b/src/view/menu_popup.rs @@ -7,6 +7,7 @@ use menu::{MenuItem, MenuTree}; use printer::Printer; use view::View; use view::Position; +use view::KeyEventView; use view::scroll::ScrollBase; use align::Align; use vec::Vec2; @@ -27,13 +28,22 @@ impl MenuPopup { MenuPopup { menu: menu, focus: 0, - scrollbase: ScrollBase::new(), + scrollbase: ScrollBase::new().bar_padding(1), align: Align::top_left(), on_dismiss: None, on_action: None, } } + fn item_width(item: &MenuItem) -> usize { + match *item { + MenuItem::Delimiter => 1, + MenuItem::Leaf(ref title, _) => title.width(), + MenuItem::Subtree(ref title, _) => title.width() + 3, + } + } + + /// Sets the alignment for this view. pub fn align(mut self, align: Align) -> Self { self.align = align; @@ -50,6 +60,35 @@ impl MenuPopup { self.on_action = Some(Rc::new(f)); self } + + fn make_subtree_cb(&self, tree: &Rc) -> EventResult { + let tree = tree.clone(); + let max_width = 4 + + self.menu + .children + .iter() + .map(Self::item_width) + .max() + .unwrap_or(1); + let offset = Vec2::new(max_width, self.focus); + let action_cb = self.on_action.clone(); + EventResult::with_cb(move |s| { + let action_cb = action_cb.clone(); + s.screen_mut() + .add_layer_at(Position::parent(offset), + KeyEventView::new(MenuPopup::new(tree.clone()) + .on_action(move |s| { + // This will happen when the subtree popup + // activates something; + // First, remove ourselve. + s.pop_layer(); + if let Some(ref action_cb) = action_cb { + action_cb.clone()(s); + } + })) + .register(Key::Left, |s| s.pop_layer())); + }) + } } impl View for MenuPopup { @@ -75,7 +114,11 @@ impl View for MenuPopup { MenuItem::Delimiter => { printer.print_hdelim((0, 0), printer.size.x) } - MenuItem::Subtree(ref label, _) | + MenuItem::Subtree(ref label, _) => { + printer.print_hline((1, 0), printer.size.x - 2, " "); + printer.print((2, 0), label); + printer.print((printer.size.x - 4, 0), ">>"); + } MenuItem::Leaf(ref label, _) => { printer.print_hline((1, 0), printer.size.x - 2, " "); printer.print((2, 0), label); @@ -88,11 +131,11 @@ 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 = 2 + + let w = 4 + self.menu .children .iter() - .map(|item| 2 + item.label().width()) + .map(Self::item_width) .max() .unwrap_or(1); let h = 2 + self.menu.children.len(); @@ -101,7 +144,7 @@ impl View for MenuPopup { let scrolling = req.y < h; let w = if scrolling { - w + 2 + w + 1 } else { w }; @@ -120,34 +163,60 @@ impl View for MenuPopup { 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::Up) => { + loop { + if self.focus > 0 { + self.focus -= 1; + } else { + self.focus = self.menu.children.len() - 1; + } + if !self.menu.children[self.focus].is_delimiter() { + break; + } + } + } + Event::Key(Key::Down) => { + loop { + if self.focus + 1 < self.menu.children.len() { + self.focus += 1; + } else { + self.focus = 0; + } + if !self.menu.children[self.focus].is_delimiter() { + break; + } + } + } + Event::Key(Key::Right) if self.menu.children[self.focus] + .is_subtree() => { + return match self.menu.children[self.focus] { + MenuItem::Subtree(_, ref tree) => { + self.make_subtree_cb(tree) + } + _ => panic!("Not a subtree???"), + + }; } Event::Key(Key::Enter) if !self.menu.children[self.focus] - .is_delimiter() => { + .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| { + // Remove ourselves from the face of the earth + s.pop_layer(); + // If we had prior orders, do it now. if let Some(ref action_cb) = action_cb { action_cb.clone()(s); } - s.pop_layer(); + // And transmit his last words. 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())); - }) + self.make_subtree_cb(tree) } _ => panic!("No delimiter here"), }; @@ -162,6 +231,6 @@ impl View for MenuPopup { } fn layout(&mut self, size: Vec2) { - self.scrollbase.set_heights(size.y, self.menu.children.len()); + self.scrollbase.set_heights(size.y - 2, self.menu.children.len()); } } diff --git a/src/view/position.rs b/src/view/position.rs index 401781f..057c04c 100644 --- a/src/view/position.rs +++ b/src/view/position.rs @@ -1,3 +1,4 @@ +use std::cmp::min; use vec::{ToVec2, Vec2}; /// Location of the view on screen @@ -47,8 +48,8 @@ impl Offset { pub fn compute_offset(&self, size: usize, available: usize, parent: usize) -> usize { match *self { Offset::Center => (available - size) / 2, - Offset::Absolute(offset) => offset, - Offset::Parent(offset) => parent + offset, + Offset::Absolute(offset) => min(offset, available - size), + Offset::Parent(offset) => min(parent + offset, available - size), } } } diff --git a/src/view/scroll.rs b/src/view/scroll.rs index b6f8453..fe48091 100644 --- a/src/view/scroll.rs +++ b/src/view/scroll.rs @@ -10,6 +10,7 @@ pub struct ScrollBase { pub start_line: usize, pub content_height: usize, pub view_height: usize, + pub scrollbar_padding: usize, } impl ScrollBase { @@ -18,9 +19,15 @@ impl ScrollBase { start_line: 0, content_height: 0, view_height: 0, + scrollbar_padding: 0, } } + pub fn bar_padding(mut self, padding: usize) -> Self { + self.scrollbar_padding = padding; + self + } + /// Call this method whem the content or the view changes. pub fn set_heights(&mut self, view_height: usize, content_height: usize) { self.view_height = view_height; @@ -104,7 +111,7 @@ impl ScrollBase { // Print the content in a sub_printer let max_y = min(self.view_height, self.content_height - self.start_line); let w = if self.scrollable() { - printer.size.x - 2 + printer.size.x - 1 // TODO: 2 } else { printer.size.x }; @@ -134,9 +141,11 @@ impl ScrollBase { ColorStyle::HighlightInactive }; - printer.print_vline((printer.size.x - 1, 0), printer.size.y, "|"); + // TODO: use 1 instead of 2 + let scrollbar_x = printer.size.x - 1 - self.scrollbar_padding; + printer.print_vline((scrollbar_x, 0), printer.size.y, "|"); printer.with_color(color, |printer| { - printer.print_vline((printer.size.x - 1, start), height, " "); + printer.print_vline((scrollbar_x, start), height, " "); }); } } diff --git a/src/view/stack_view.rs b/src/view/stack_view.rs index 575f0d7..d6c330e 100644 --- a/src/view/stack_view.rs +++ b/src/view/stack_view.rs @@ -62,8 +62,9 @@ impl View for StackView { for (i, v) in self.layers.iter_mut().enumerate() { // Place the view // Center the view - let offset = v.position + let mut offset = v.position .compute_offset(v.size, printer.size, previous); + previous = offset; v.view .draw(&printer.sub_printer(offset, v.size, i + 1 == last));