From a22c92a1a18aed6302720bf2ec32bdc81510fced Mon Sep 17 00:00:00 2001 From: Alexandre Bury Date: Sat, 16 Jul 2016 13:25:21 -0700 Subject: [PATCH] Add ListView Also added `examples/list_view.rs`. --- examples/list_view.rs | 27 ++++ examples/menubar.rs | 1 + src/event.rs | 8 ++ src/lib.rs | 2 + src/menu.rs | 9 +- src/view/edit_view.rs | 2 +- src/view/linear_layout.rs | 4 +- src/view/list_view.rs | 268 ++++++++++++++++++++++++++++++++++++++ src/view/mod.rs | 2 + src/view/scroll.rs | 2 +- src/with.rs | 11 ++ 11 files changed, 324 insertions(+), 12 deletions(-) create mode 100644 examples/list_view.rs create mode 100644 src/view/list_view.rs create mode 100644 src/with.rs diff --git a/examples/list_view.rs b/examples/list_view.rs new file mode 100644 index 0000000..a7796ca --- /dev/null +++ b/examples/list_view.rs @@ -0,0 +1,27 @@ +extern crate cursive; + +use cursive::Cursive; +use cursive::With; +use cursive::view::{ListView, Dialog, EditView, TextView, LinearLayout}; + +fn main() { + let mut siv = Cursive::new(); + + siv.add_layer(Dialog::new(ListView::new() + .child("Name", EditView::new().min_length(10)) + .child("Email", LinearLayout::horizontal() + .child(EditView::new().min_length(15)) + .child(TextView::new("@")) + .child(EditView::new().min_length(10))) + .delimiter() + .with(|list| { + for i in 0..50 { + list.add_child(&format!("Item {}", i), EditView::new()); + } + }) + ) + .title("Please fill out this form") + .button("Ok", |s| s.quit())); + + siv.run(); +} diff --git a/examples/menubar.rs b/examples/menubar.rs index 530d25e..001f240 100644 --- a/examples/menubar.rs +++ b/examples/menubar.rs @@ -1,6 +1,7 @@ extern crate cursive; use cursive::Cursive; +use cursive::With; use cursive::menu::MenuTree; use cursive::view::Dialog; use cursive::view::TextView; diff --git a/src/event.rs b/src/event.rs index 944cabf..ae60145 100644 --- a/src/event.rs +++ b/src/event.rs @@ -35,6 +35,14 @@ impl EventResult { pub fn with_cb(f: F) -> Self { EventResult::Consumed(Some(Rc::new(f))) } + + /// Returns `true` if `self` is `EventResult::Consumed`. + pub fn is_consumed(&self) -> bool { + match *self { + EventResult::Consumed(_) => true, + EventResult::Ignored => false, + } + } } /// A non-character key on the keyboard diff --git a/src/lib.rs b/src/lib.rs index f57bf8c..df98623 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -74,6 +74,7 @@ pub mod direction; mod printer; mod menubar; mod xy; +mod with; mod div; mod utf8; @@ -81,6 +82,7 @@ mod utf8; mod backend; pub use xy::XY; +pub use with::With; pub use printer::Printer; use backend::{Backend, NcursesBackend}; diff --git a/src/menu.rs b/src/menu.rs index dd4f2a5..dab54c4 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -1,5 +1,6 @@ //! Module to build menus. +use With; use Cursive; use std::rc::Rc; use event::Callback; @@ -105,12 +106,4 @@ impl MenuTree { pub fn subtree(self, title: &str, tree: MenuTree) -> Self { self.with(|menu| menu.add_subtree(title, tree)) } - - /// Calls the given closure on `self`. - /// - /// Useful in a function chain. - pub fn with(mut self, f: F) -> Self { - f(&mut self); - self - } } diff --git a/src/view/edit_view.rs b/src/view/edit_view.rs index 37169e4..d3d8ad5 100644 --- a/src/view/edit_view.rs +++ b/src/view/edit_view.rs @@ -80,7 +80,7 @@ impl EditView { impl View for EditView { fn draw(&self, printer: &Printer) { - assert!(printer.size.x == self.last_length); + assert!(printer.size.x == self.last_length, "Was promised {}, received {}", self.last_length, printer.size.x); let width = self.content.width(); printer.with_color(ColorStyle::Secondary, |printer| { diff --git a/src/view/linear_layout.rs b/src/view/linear_layout.rs index 21d0e26..ac58860 100644 --- a/src/view/linear_layout.rs +++ b/src/view/linear_layout.rs @@ -108,10 +108,10 @@ impl LinearLayout { /// Returns a cyclic mutable iterator starting with the child in focus fn iter_mut<'a>(&'a mut self, from_focus: bool, - direction: direction::Relative) + source: direction::Relative) -> Box + 'a> { - match direction { + match source { direction::Relative::Front => { let start = if from_focus { self.focus diff --git a/src/view/list_view.rs b/src/view/list_view.rs new file mode 100644 index 0000000..fb7bac2 --- /dev/null +++ b/src/view/list_view.rs @@ -0,0 +1,268 @@ +use Printer; +use With; +use vec::Vec2; +use view::View; +use direction; +use view::scroll::ScrollBase; +use event::{Event, EventResult, Key}; + +enum Child { + Row(String, Box), + Delimiter, +} + +impl Child { + fn label(&self) -> &str { + match *self { + Child::Row(ref label, _) => label, + _ => "", + } + } + + fn view(&mut self) -> Option<&mut Box> { + match *self { + Child::Row(_, ref mut view) => Some(view), + _ => None, + } + } + + // Currently unused + /* + fn is_delimiter(&self) -> bool { + match *self { + Child::Row(_, _) => false, + Child::Delimiter => true, + } + } + */ +} + +/// Displays a scrollable list of elements. +pub struct ListView { + children: Vec, + scrollbase: ScrollBase, + focus: usize, +} + +impl Default for ListView { + fn default() -> Self { + Self::new() + } +} + +impl ListView { + /// Creates a new, empty `ListView`. + pub fn new() -> Self { + ListView { + children: Vec::new(), + scrollbase: ScrollBase::new(), + focus: 0, + } + } + + /// Adds a view to the end of the list. + pub fn add_child(&mut self, label: &str, view: V) { + self.children.push(Child::Row(label.to_string(), Box::new(view))); + } + + /// Adds a view to the end of the list. + /// + /// Chainable variant. + pub fn child(self, label: &str, view: V) -> Self { + self.with(|s| s.add_child(label, view)) + } + + /// Adds a delimiter to the end of the list. + pub fn add_delimiter(&mut self) { + self.children.push(Child::Delimiter); + } + + /// Adds a delimiter to the end of the list. + /// + /// Chainable variant. + pub fn delimiter(self) -> Self { + self.with(Self::add_delimiter) + } + + fn iter_mut<'a>(&'a mut self, from_focus: bool, + source: direction::Relative) + -> Box + 'a> { + + match source { + direction::Relative::Front => { + let start = if from_focus { + self.focus + } else { + 0 + }; + + Box::new(self.children.iter_mut().enumerate().skip(start)) + } + direction::Relative::Back => { + let end = if from_focus { + self.focus + 1 + } else { + self.children.len() + }; + Box::new(self.children[..end].iter_mut().enumerate().rev()) + } + } + } + + fn move_focus(&mut self, n: usize, source: direction::Direction) + -> EventResult { + let i = if let Some(i) = + source.relative(direction::Orientation::Vertical) + .and_then(|rel| { + // The iterator starts at the focused element. + // We don't want that one. + self.iter_mut(true, rel) + .skip(1) + .filter_map(|p| try_focus(p, source)) + .take(n) + .last() + }) { + i + } else { + return EventResult::Ignored; + }; + self.focus = i; + self.scrollbase.scroll_to(self.focus); + + EventResult::Consumed(None) + } +} + +fn try_focus((i, child): (usize, &mut Child), source: direction::Direction) + -> Option { + match *child { + Child::Delimiter => None, + Child::Row(_, ref mut view) => { + if view.take_focus(source) { + Some(i) + } else { + None + } + } + + } +} + +impl View for ListView { + fn draw(&self, printer: &Printer) { + let offset = self.children + .iter() + .map(Child::label) + .map(str::len) + .max() + .unwrap_or(0) + 1; + + self.scrollbase.draw(printer, |printer, i| { + match self.children[i] { + Child::Row(ref label, ref view) => { + printer.print((0, 0), label); + view.draw(&printer.offset((offset, 0), i == self.focus)); + } + Child::Delimiter => (), + } + }); + } + + fn get_min_size(&mut self, req: Vec2) -> Vec2 { + let label_size = self.children + .iter() + .map(Child::label) + .map(str::len) + .max() + .unwrap_or(0); + let view_size = self.children + .iter_mut() + .filter_map(Child::view) + .map(|v| v.get_min_size(req).x) + .max() + .unwrap_or(0); + + if self.children.len() > req.y { + Vec2::new(label_size + 1 + view_size + 2, req.y) + } else { + Vec2::new(label_size + 1 + view_size, self.children.len()) + } + } + + fn layout(&mut self, size: Vec2) { + self.scrollbase.set_heights(size.y, self.children.len()); + + let label_size = self.children + .iter() + .map(Child::label) + .map(str::len) + .max() + .unwrap_or(0); + let mut available = size.x - label_size - 1; + + if self.children.len() > size.y { + available -= 2; + } + + for child in self.children.iter_mut().filter_map(Child::view) { + child.layout(Vec2::new(available, 1)); + } + } + + fn on_event(&mut self, event: Event) -> EventResult { + if let Child::Row(_, ref mut view) = self.children[self.focus] { + let result = view.on_event(event); + if result.is_consumed() { + return result; + } + } + + match event { + Event::Key(Key::Up) if self.focus > 0 => { + self.move_focus(1, direction::Direction::down()) + } + Event::Key(Key::Down) if self.focus + 1 < self.children.len() => { + self.move_focus(1, direction::Direction::up()) + } + Event::Key(Key::PageUp) => { + self.move_focus(10, direction::Direction::down()) + } + Event::Key(Key::PageDown) => { + self.move_focus(10, direction::Direction::up()) + } + Event::Key(Key::Home) | + Event::Ctrl(Key::Home) => { + self.move_focus(usize::max_value(), + direction::Direction::back()) + } + Event::Key(Key::End) | + Event::Ctrl(Key::End) => { + self.move_focus(usize::max_value(), + direction::Direction::front()) + } + Event::Key(Key::Tab) => { + self.move_focus(1, direction::Direction::front()) + } + Event::Shift(Key::Tab) => { + self.move_focus(1, direction::Direction::back()) + } + _ => EventResult::Ignored, + } + } + + fn take_focus(&mut self, source: direction::Direction) -> bool { + let rel = source.relative(direction::Orientation::Vertical); + let i = if let Some(i) = self.iter_mut(rel.is_none(), + rel.unwrap_or(direction::Relative::Front)) + .filter_map(|p| try_focus(p, source)) + .next() { + i + } else { + // No one wants to be in focus + return false; + }; + self.focus = i; + self.scrollbase.scroll_to(self.focus); + true + } +} diff --git a/src/view/mod.rs b/src/view/mod.rs index e4fdecb..8f0b016 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -54,6 +54,7 @@ mod full_view; mod id_view; mod key_event_view; mod linear_layout; +mod list_view; mod menu_popup; mod shadow_view; mod select_view; @@ -83,6 +84,7 @@ pub use self::edit_view::EditView; pub use self::full_view::FullView; pub use self::key_event_view::KeyEventView; pub use self::linear_layout::LinearLayout; +pub use self::list_view::ListView; pub use self::menu_popup::MenuPopup; pub use self::view_path::ViewPath; pub use self::select_view::SelectView; diff --git a/src/view/scroll.rs b/src/view/scroll.rs index ed1e1dc..768197b 100644 --- a/src/view/scroll.rs +++ b/src/view/scroll.rs @@ -134,7 +134,7 @@ impl ScrollBase { let max_y = min(self.view_height, self.content_height - self.start_line); let w = if self.scrollable() { - printer.size.x - 1 // TODO: 2 + printer.size.x - 2 + self.scrollbar_padding // TODO: 2 } else { printer.size.x }; diff --git a/src/with.rs b/src/with.rs new file mode 100644 index 0000000..9603191 --- /dev/null +++ b/src/with.rs @@ -0,0 +1,11 @@ + +/// Generic trait to enable chainable API +pub trait With: Sized { + /// Calls the given closure on `self`. + fn with(mut self, f: F) -> Self { + f(&mut self); + self + } +} + +impl With for T {}