diff --git a/Cargo.toml b/Cargo.toml index e760307..ae96838 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ license = "MIT" name = "cursive" readme = "Readme.md" repository = "https://github.com/gyscos/Cursive" -version = "0.5.0" +version = "0.5.1" [badges.travis-ci] repository = "gyscos/Cursive" diff --git a/Readme.md b/Readme.md index 8e3a6c6..da0be57 100644 --- a/Readme.md +++ b/Readme.md @@ -47,7 +47,7 @@ fn main() { } ``` -![Cursive dialog example](https://raw.githubusercontent.com/gyscos/Cursive/master/doc/cursive_example.png) +[![Cursive dialog example](https://raw.githubusercontent.com/gyscos/Cursive/master/doc/cursive_example.png)](examples/dialog.rs) Check out the other [examples](https://github.com/gyscos/Cursive/tree/master/examples) to get these results, and more: @@ -76,6 +76,7 @@ Here are a few crates implementing new views for you to use: * [cursive_table_view](https://github.com/BonsaiDen/cursive_table_view) * [cursive_calendar_view](https://github.com/BonsaiDen/cursive_calendar_view) +* [cursive_tree_view](https://github.com/BonsaiDen/cursive_tree_view) ## Goals diff --git a/examples/mutation.rs b/examples/mutation.rs index a7c9ba7..3afaea9 100644 --- a/examples/mutation.rs +++ b/examples/mutation.rs @@ -1,7 +1,7 @@ extern crate cursive; use cursive::Cursive; -use cursive::views::{Dialog, KeyEventView, TextView}; +use cursive::views::{Dialog, OnEventView, TextView}; use cursive::view::{Offset, Position}; use cursive::traits::*; @@ -33,8 +33,8 @@ fn main() { // Let's wrap the view to give it a recognizable ID, so we can look for it. // We add the P callback on the textview only (and not globally), // so that we can't call it when the popup is already visible. - siv.add_layer(KeyEventView::new(TextView::new(content).with_id("text")) - .register('p', |s| show_popup(s))); + siv.add_layer(OnEventView::new(TextView::new(content).with_id("text")) + .on_event('p', |s| show_popup(s))); siv.run(); diff --git a/src/backend/curses/n.rs b/src/backend/curses/n.rs index e06677d..fcf3ace 100644 --- a/src/backend/curses/n.rs +++ b/src/backend/curses/n.rs @@ -66,8 +66,8 @@ impl backend::Backend for Concrete { // The delay is the time ncurses wait after pressing ESC // to see if it's an escape sequence. // Default delay is way too long. 25 is imperceptible yet works fine. - ::std::env::set_var("ESCDELAY", "25"); ncurses::setlocale(ncurses::LcCategory::all, ""); + ::std::env::set_var("ESCDELAY", "25"); ncurses::initscr(); ncurses::keypad(ncurses::stdscr(), true); ncurses::noecho(); diff --git a/src/backend/curses/pan.rs b/src/backend/curses/pan.rs index 24ebab0..148388a 100644 --- a/src/backend/curses/pan.rs +++ b/src/backend/curses/pan.rs @@ -66,8 +66,8 @@ impl Concrete { impl backend::Backend for Concrete { fn init() -> Self { - ::std::env::set_var("ESCDELAY", "25"); let window = pancurses::initscr(); + ::std::env::set_var("ESCDELAY", "25"); window.keypad(true); pancurses::noecho(); pancurses::cbreak(); diff --git a/src/lib.rs b/src/lib.rs index 1c11f38..2f68148 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,6 +71,7 @@ extern crate owning_ref; extern crate chan; +#[allow(unused)] macro_rules! println_stderr( ($($arg:tt)*) => { { use ::std::io::Write; @@ -578,9 +579,13 @@ impl Cursive { /// /// Calls [`step(&mut self)`] until [`quit(&mut self)`] is called. /// + /// After this function returns, you can call + /// it again and it will start a new loop. + /// /// [`step(&mut self)`]: #method.step /// [`quit(&mut self)`]: #method.quit pub fn run(&mut self) { + self.running = true; // And the big event loop begins! while self.running { diff --git a/src/views/button.rs b/src/views/button.rs index 4cd4ef7..591d300 100644 --- a/src/views/button.rs +++ b/src/views/button.rs @@ -37,6 +37,15 @@ impl Button { } } + /// Sets the function to be called when the button is pressed. + /// + /// Replaces the previous callback. + pub fn set_callback(&mut self, cb: F) + where F: Fn(&mut Cursive) + 'static + { + self.callback = Callback::from_fn(cb); + } + /// Disables this view. /// /// A disabled view cannot be selected. diff --git a/src/views/list_view.rs b/src/views/list_view.rs index a25007f..fbd397a 100644 --- a/src/views/list_view.rs +++ b/src/views/list_view.rs @@ -13,22 +13,27 @@ use view::ScrollBase; use view::Selector; use view::View; -enum Child { +/// Represents a child from a [`ListView`]. +/// +/// [`ListView`]: struct.ListView.html +pub enum ListChild { + /// A single row, with a label and a view. Row(String, Box), + /// A delimiter between groups. Delimiter, } -impl Child { +impl ListChild { fn label(&self) -> &str { match *self { - Child::Row(ref label, _) => label, + ListChild::Row(ref label, _) => label, _ => "", } } fn view(&mut self) -> Option<&mut Box> { match *self { - Child::Row(_, ref mut view) => Some(view), + ListChild::Row(_, ref mut view) => Some(view), _ => None, } } @@ -36,7 +41,7 @@ impl Child { /// Displays a scrollable list of elements. pub struct ListView { - children: Vec, + children: Vec, scrollbase: ScrollBase, focus: usize, // This callback is called when the selection is changed. @@ -56,10 +61,42 @@ impl ListView { } } + /// Returns the number of children, including delimiters. + pub fn len(&self) -> usize { + self.children.len() + } + + /// Returns `true` if this view contains no children. + /// + /// Returns `false` if at least a delimiter or a view is present. + pub fn is_empty(&self) -> bool { + self.children.is_empty() + } + + + /// Returns a reference to the children + pub fn children(&self) -> &[ListChild] { + &self.children[..] + } + + /// Returns a reference to the child at the given position. + pub fn get_row(&self, id: usize) -> &ListChild { + &self.children[id] + } + + /// Gives mutable access to the child at the given position. + /// + /// # Panics + /// + /// Panics if `id >= self.len()`. + pub fn row_mut(&mut self, id: usize) -> &mut ListChild { + &mut self.children[id] + } + /// Adds a view to the end of the list. pub fn add_child(&mut self, label: &str, mut view: V) { view.take_focus(direction::Direction::none()); - self.children.push(Child::Row(label.to_string(), Box::new(view))); + self.children.push(ListChild::Row(label.to_string(), Box::new(view))); } /// Removes all children from this view. @@ -77,7 +114,7 @@ impl ListView { /// Adds a delimiter to the end of the list. pub fn add_delimiter(&mut self) { - self.children.push(Child::Delimiter); + self.children.push(ListChild::Delimiter); } /// Adds a delimiter to the end of the list. @@ -112,7 +149,7 @@ impl ListView { fn iter_mut<'a>(&'a mut self, from_focus: bool, source: direction::Relative) - -> Box + 'a> { + -> Box + 'a> { match source { direction::Relative::Front => { @@ -162,11 +199,11 @@ impl ListView { } } -fn try_focus((i, child): (usize, &mut Child), source: direction::Direction) +fn try_focus((i, child): (usize, &mut ListChild), source: direction::Direction) -> Option { match *child { - Child::Delimiter => None, - Child::Row(_, ref mut view) => { + ListChild::Delimiter => None, + ListChild::Row(_, ref mut view) => { if view.take_focus(source) { Some(i) } else { @@ -185,18 +222,18 @@ impl View for ListView { let offset = self.children .iter() - .map(Child::label) + .map(ListChild::label) .map(UnicodeWidthStr::width) .max() .unwrap_or(0) + 1; // println_stderr!("Offset: {}", offset); self.scrollbase.draw(printer, |printer, i| match self.children[i] { - Child::Row(ref label, ref view) => { + ListChild::Row(ref label, ref view) => { printer.print((0, 0), label); view.draw(&printer.offset((offset, 0), i == self.focus)); } - Child::Delimiter => (), + ListChild::Delimiter => (), }); } @@ -204,14 +241,14 @@ impl View for ListView { // We'll show 2 columns: the labels, and the views. let label_width = self.children .iter() - .map(Child::label) + .map(ListChild::label) .map(UnicodeWidthStr::width) .max() .unwrap_or(0); let view_size = self.children .iter_mut() - .filter_map(Child::view) + .filter_map(ListChild::view) .map(|v| v.required_size(req).x) .max() .unwrap_or(0); @@ -229,7 +266,7 @@ impl View for ListView { // We'll show 2 columns: the labels, and the views. let label_width = self.children .iter() - .map(Child::label) + .map(ListChild::label) .map(UnicodeWidthStr::width) .max() .unwrap_or(0); @@ -246,7 +283,7 @@ impl View for ListView { // println_stderr!("Available: {}", available); - for child in self.children.iter_mut().filter_map(Child::view) { + for child in self.children.iter_mut().filter_map(ListChild::view) { child.layout(Vec2::new(available, 1)); } } @@ -256,7 +293,7 @@ impl View for ListView { return EventResult::Ignored; } - if let Child::Row(_, ref mut view) = self.children[self.focus] { + if let ListChild::Row(_, ref mut view) = self.children[self.focus] { let result = view.on_event(event.clone()); if result.is_consumed() { return result; @@ -317,7 +354,7 @@ impl View for ListView { mut callback: Box) { for view in self.children .iter_mut() - .filter_map(Child::view) { + .filter_map(ListChild::view) { view.call_on_any(selector, Box::new(|any| callback(any))); } } diff --git a/src/views/menu_popup.rs b/src/views/menu_popup.rs index 3412cf9..283b595 100644 --- a/src/views/menu_popup.rs +++ b/src/views/menu_popup.rs @@ -12,7 +12,7 @@ use std::rc::Rc; use unicode_width::UnicodeWidthStr; use vec::Vec2; use view::{Position, ScrollBase, View}; -use views::KeyEventView; +use views::OnEventView; /// Popup that shows a list of items. pub struct MenuPopup { @@ -90,18 +90,31 @@ impl MenuPopup { /// Sets the alignment for this view. - pub fn align(mut self, align: Align) -> Self { - self.align = align; + /// + /// Chainable variant. + pub fn align(self, align: Align) -> Self { + self.with(|s| s.set_align(align)) + } - self + /// Sets the alignment for this view. + pub fn set_align(&mut self, align: Align) { + self.align = align; } /// Sets a callback to be used when this view is actively dismissed. /// /// (When the user hits ) - pub fn on_dismiss(mut self, f: F) -> Self { + /// + /// Chainable variant. + pub fn on_dismiss(self, f: F) -> Self { + self.with(|s| s.set_on_dismiss(f)) + } + + /// Sets a callback to be used when this view is actively dismissed. + /// + /// (When the user hits ) + pub fn set_on_dismiss(&mut self, f: F) { self.on_dismiss = Some(Callback::from_fn(f)); - self } /// Sets a callback to be used when a leaf is activated. @@ -109,20 +122,30 @@ impl MenuPopup { /// Will also be called if a leaf from a subtree is activated. /// /// Usually used to hide the parent view. - pub fn on_action(mut self, f: F) -> Self { + /// + /// Chainable variant. + pub fn on_action(self, f: F) -> Self { + self.with(|s| s.set_on_action(f)) + } + + /// Sets a callback to be used when a leaf is activated. + /// + /// Will also be called if a leaf from a subtree is activated. + /// + /// Usually used to hide the parent view. + pub fn set_on_action(&mut self, f: F) { self.on_action = Some(Callback::from_fn(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); + .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(); @@ -130,7 +153,7 @@ impl MenuPopup { let action_cb = action_cb.clone(); s.screen_mut() .add_layer_at(Position::parent(offset), - KeyEventView::new(MenuPopup::new(tree.clone()) + OnEventView::new(MenuPopup::new(tree.clone()) .on_action(move |s| { // This will happen when the subtree popup // activates something; @@ -140,7 +163,7 @@ impl MenuPopup { action_cb.clone()(s); } })) - .register(Key::Left, |s| s.pop_layer())); + .on_event(Key::Left, |s| s.pop_layer())); }) } } @@ -196,11 +219,11 @@ impl View for MenuPopup { // We can't really shrink our items here, so it's not flexible. let w = 4 + self.menu - .children - .iter() - .map(Self::item_width) - .max() - .unwrap_or(1); + .children + .iter() + .map(Self::item_width) + .max() + .unwrap_or(1); let h = 2 + self.menu.children.len(); @@ -231,38 +254,38 @@ impl View for MenuPopup { Event::Key(Key::End) => self.focus = self.menu.children.len() - 1, Event::Key(Key::Right) if self.menu.children[self.focus] - .is_subtree() => { + .is_subtree() => { return match self.menu.children[self.focus] { - MenuItem::Subtree(_, ref tree) => { - self.make_subtree_cb(tree) - } - _ => panic!("Not a subtree???"), + 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) => { + 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); - } - // And transmit his last words. - cb.clone()(s); - }) - } - MenuItem::Subtree(_, ref tree) => { - self.make_subtree_cb(tree) - } - _ => panic!("No delimiter here"), - }; + 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); + } + // And transmit his last words. + cb.clone()(s); + }) + } + MenuItem::Subtree(_, ref tree) => { + self.make_subtree_cb(tree) + } + _ => panic!("No delimiter here"), + }; } _ => return EventResult::Ignored, @@ -274,7 +297,6 @@ impl View for MenuPopup { } fn layout(&mut self, size: Vec2) { - self.scrollbase - .set_heights(size.y - 2, self.menu.children.len()); + self.scrollbase.set_heights(size.y - 2, self.menu.children.len()); } } diff --git a/src/views/menubar.rs b/src/views/menubar.rs index c3fdb40..d76a6bb 100644 --- a/src/views/menubar.rs +++ b/src/views/menubar.rs @@ -10,7 +10,7 @@ use theme::ColorStyle; use unicode_width::UnicodeWidthStr; use vec::Vec2; use view::{Position, View}; -use views::{KeyEventView, MenuPopup}; +use views::{OnEventView, MenuPopup}; /// Current state of the menubar #[derive(PartialEq, Debug)] @@ -108,9 +108,7 @@ impl Menubar { /// /// Returns `None` if `i > self.len()` pub fn get_subtree(&mut self, i: usize) -> Option<&mut MenuTree> { - self.menus - .get_mut(i) - .map(|&mut (_, ref mut tree)| Rc::make_mut(tree)) + self.menus.get_mut(i).map(|&mut (_, ref mut tree)| Rc::make_mut(tree)) } /// Looks for an item with the given label. @@ -129,9 +127,7 @@ impl Menubar { /// /// Returns `None` if no such label was found. pub fn find_position(&mut self, label: &str) -> Option { - self.menus - .iter() - .position(|&(ref l, _)| l == label) + self.menus.iter().position(|&(ref l, _)| l == label) } /// Remove the item at the given position. @@ -145,37 +141,38 @@ fn show_child(s: &mut Cursive, offset: (usize, usize), menu: Rc) { // 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 - // consume it before our KeyEventView. This means sub-menus can properly + // consume it before our OnEventView. This means sub-menus can properly // be entered.) s.screen_mut() .add_layer_at(Position::absolute(offset), - KeyEventView::new(MenuPopup::new(menu) - .on_dismiss(|s| s.select_menubar()) - .on_action(|s| { - s.menubar().state = State::Inactive - })) - .register(Key::Right, |s| { - 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)) = - s.menubar() - .on_event(Event::Key(Key::Down)) { - cb(s); - } - }) - .register(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); - } - })); + OnEventView::new(MenuPopup::new(menu) + .on_dismiss(|s| { + s.select_menubar() + }) + .on_action(|s| { + s.menubar().state = + State::Inactive + })) + .on_event(Key::Right, |s| { + 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)) = + s.menubar().on_event(Event::Key(Key::Down)) { + cb(s); + } + }) + .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); + } + })); } @@ -226,16 +223,19 @@ impl View for Menubar { // 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.width() + 2) - .fold(0, |a, b| a + b), - if self.autohide { 1 } else { 0 }); + 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. return EventResult::with_cb(move |s| { - show_child(s, offset, menu.clone()) - }); + show_child(s, + offset, + menu.clone()) + }); } _ => return EventResult::Ignored, } diff --git a/src/views/mod.rs b/src/views/mod.rs index 2e1b918..d5953fb 100644 --- a/src/views/mod.rs +++ b/src/views/mod.rs @@ -43,7 +43,7 @@ mod dialog; mod dummy; mod edit_view; mod id_view; -mod key_event_view; +mod on_event_view; mod layer; mod linear_layout; mod list_view; @@ -69,10 +69,10 @@ pub use self::dialog::Dialog; pub use self::dummy::DummyView; pub use self::edit_view::EditView; pub use self::id_view::{IdView, ViewRef}; -pub use self::key_event_view::KeyEventView; +pub use self::on_event_view::OnEventView; pub use self::layer::Layer; pub use self::linear_layout::LinearLayout; -pub use self::list_view::ListView; +pub use self::list_view::{ListChild, ListView}; pub use self::menu_popup::MenuPopup; pub use self::menubar::Menubar; pub use self::panel::Panel; diff --git a/src/views/key_event_view.rs b/src/views/on_event_view.rs similarity index 55% rename from src/views/key_event_view.rs rename to src/views/on_event_view.rs index 8206860..402de06 100644 --- a/src/views/key_event_view.rs +++ b/src/views/on_event_view.rs @@ -4,6 +4,7 @@ use Cursive; use event::{Callback, Event, EventResult}; use std::collections::HashMap; use view::{View, ViewWrapper}; +use With; /// A simple wrapper view that catches some ignored event from its child. /// @@ -13,36 +14,43 @@ use view::{View, ViewWrapper}; /// /// ``` /// # use cursive::event;; -/// # use cursive::views::{KeyEventView, TextView}; -/// let view = KeyEventView::new(TextView::new("This view has an event!")) -/// .register('q', |s| s.quit()) -/// .register(event::Key::Esc, |s| s.quit()); +/// # use cursive::views::{OnEventView, TextView}; +/// let view = OnEventView::new(TextView::new("This view has an event!")) +/// .on_event('q', |s| s.quit()) +/// .on_event(event::Key::Esc, |s| s.quit()); /// ``` -pub struct KeyEventView { +pub struct OnEventView { content: T, callbacks: HashMap, } -impl KeyEventView { - /// Wraps the given view in a new KeyEventView. +impl OnEventView { + /// Wraps the given view in a new OnEventView. pub fn new(view: T) -> Self { - KeyEventView { + OnEventView { content: view, callbacks: HashMap::new(), } } - /// Registers a callback when the given key is ignored by the child. - pub fn register>(mut self, event: E, cb: F) -> Self + /// Registers a callback when the given event is ignored by the child. + /// + /// Chainable variant. + pub fn on_event>(self, event: E, cb: F) -> Self + where F: Fn(&mut Cursive) + 'static + { + self.with(|s| s.set_on_event(event, cb)) + } + + /// Registers a callback when the given event is ignored by the child. + pub fn set_on_event>(&mut self, event: E, cb: F) where F: Fn(&mut Cursive) + 'static { self.callbacks.insert(event.into(), Callback::from_fn(cb)); - - self } } -impl ViewWrapper for KeyEventView { +impl ViewWrapper for OnEventView { wrap_impl!(self.content: T); fn wrap_on_event(&mut self, event: Event) -> EventResult {