Add stack, box and text views

Also add some documentation
This commit is contained in:
Alexandre Bury 2015-05-14 17:41:17 -07:00
parent 7dbeedcb49
commit a387bf5f06
11 changed files with 279 additions and 116 deletions

View File

@ -3,9 +3,14 @@ name = "cursive"
version = "0.1.0" version = "0.1.0"
authors = ["Alexandre Bury <alexandre.bury@gmail.com>"] authors = ["Alexandre Bury <alexandre.bury@gmail.com>"]
[[bin]]
name = "cursive-example"
[lib] [lib]
name = "cursive" name = "cursive"
[dependencies]
[dependencies.ncurses] [dependencies.ncurses]
git = "https://github.com/jeaye/ncurses-rs" git = "https://github.com/jeaye/ncurses-rs"

46
src/box_view.rs Normal file
View File

@ -0,0 +1,46 @@
use ncurses;
use event::EventResult;
use super::{Size,ToSize};
use view::{View,SizeRequest};
/// BoxView is a wrapper around an other view, with a given minimum size.
pub struct BoxView {
size: Size,
content: Box<View>,
}
impl BoxView {
/// Creates a new BoxView with the given minimum size and content
///
/// # Example
///
/// ```
/// // Creates a 20x4 BoxView with a TextView content.
/// let box = BoxView::new((20,4), TextView::new("Hello!"))
/// ```
pub fn new<S: ToSize, V: View + 'static>(size: S, view: V) -> Self {
BoxView {
size: size.to_size(),
content: Box::new(view),
}
}
}
impl View for BoxView {
fn on_key_event(&mut self, ch: i32) -> EventResult {
self.content.on_key_event(ch)
}
fn draw(&self, win: ncurses::WINDOW, size: Size) {
self.content.draw(win, size)
}
fn get_min_size(&self, _: SizeRequest) -> Size {
self.size
}
fn layout(&mut self, size: Size) {
self.content.layout(size);
}
}

9
src/div.rs Normal file
View File

@ -0,0 +1,9 @@
pub fn div_up_usize(p: usize, q: usize) -> usize {
if p % q == 0 { p/q }
else { 1 + p/q }
}
pub fn div_up(p: u32, q: u32) -> u32 {
if p % q == 0 { p/q }
else { 1 + p/q }
}

14
src/event.rs Normal file
View File

@ -0,0 +1,14 @@
use std::rc::Rc;
/// Callback is a function that can be triggered by an event.
/// It has a mutable access to the cursive root.
pub type Callback = Box<Fn(&mut super::Cursive)>;
/// Answer to an event notification.
/// The event can be consumed or ignored.
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<Rc<Callback>>),
}

View File

@ -1,4 +0,0 @@
pub enum FocusChange {
KeptFocus,
LostFocus,
}

View File

@ -1,53 +1,56 @@
extern crate ncurses; extern crate ncurses;
pub mod focus; /// Module for user-input events and their effects.
pub mod event;
/// Define various views to use when creating the layout.
pub mod view; pub mod view;
mod box_view;
mod stack_view;
mod text_view;
pub use self::view::{View,TextView,Button,Dialog,BackgroundView}; mod div;
use std::ops::DerefMut; use view::View;
use stack_view::StackView;
use event::EventResult;
/// Central part of the cursive library.
/// It initializes ncurses on creation and cleans up on drop.
/// To use it, you should populate it with views, layouts and callbacks,
/// then start the event loop with run().
pub struct Cursive { pub struct Cursive {
background: Box<View>, stacks: StackView,
layers: Vec<Box<View>>,
running: bool, running: bool,
} }
pub type Callback = Fn(&mut Cursive);
impl Cursive { impl Cursive {
/// Creates a new Cursive root, and initialize ncurses.
pub fn new() -> Self { pub fn new() -> Self {
ncurses::initscr(); ncurses::initscr();
ncurses::keypad(ncurses::stdscr, true); ncurses::keypad(ncurses::stdscr, true);
ncurses::noecho(); ncurses::noecho();
Cursive{ Cursive{
background: Box::new(BackgroundView), stacks: StackView::new(),
layers: Vec::new(),
running: true, running: true,
} }
} }
pub fn new_layer<V: 'static + View>(&mut self, view: V) { /// Runs the event loop.
self.layers.push(Box::new(view)); /// It will wait for user input (key presses) and trigger callbacks accordingly.
} /// Blocks until quit() is called.
pub fn run(&mut self) { pub fn run(&mut self) {
while self.running { while self.running {
ncurses::refresh(); ncurses::refresh();
// Handle event // Handle event
match ncurses::getch() { let ch = ncurses::getch();
10 => { match self.stacks.on_key_event(ch) {
let cb = self.layers.last_mut().unwrap_or(&mut self.background).click(); EventResult::Ignored => (),
cb.map(|cb| cb(self)); EventResult::Consumed(None) => (),
}, EventResult::Consumed(Some(cb)) => cb(self),
ncurses::KEY_LEFT => { self.layers.last_mut().unwrap_or(&mut self.background).focus_left(); },
ncurses::KEY_RIGHT => { self.layers.last_mut().unwrap_or(&mut self.background).focus_right(); },
ncurses::KEY_DOWN => { self.layers.last_mut().unwrap_or(&mut self.background).focus_bottom(); },
ncurses::KEY_UP => { self.layers.last_mut().unwrap_or(&mut self.background).focus_top(); },
a => println!("Key: {}", a),
} }
} }
} }
@ -63,3 +66,36 @@ impl Drop for Cursive {
ncurses::endwin(); ncurses::endwin();
} }
} }
/// Simple 2D size, in characters.
#[derive(Clone,Copy)]
pub struct Size {
pub w: u32,
pub h: u32,
}
impl Size {
pub fn new(w: u32, h: u32) -> Self {
Size {
w: w,
h: h,
}
}
}
/// A generic trait for converting a value into a 2D size
pub trait ToSize {
fn to_size(self) -> Size;
}
impl ToSize for Size {
fn to_size(self) -> Size {
self
}
}
impl ToSize for (u32,u32) {
fn to_size(self) -> Size {
Size::new(self.0, self.1)
}
}

View File

@ -1,13 +1,10 @@
extern crate cursive; extern crate cursive;
use cursive::{Cursive,Dialog}; use cursive::Cursive;
fn main() { fn main() {
let mut siv = Cursive::new(); let mut siv = Cursive::new();
siv.new_layer(
Dialog::new("Hello World !")
.button("ok", |s| s.quit() ));
siv.run(); siv.run();
} }

25
src/stack_view.rs Normal file
View File

@ -0,0 +1,25 @@
use view::View;
use super::Size;
use ncurses;
/// Simple stack of views.
/// Only the top-most view is active and can receive input.
pub struct StackView {
layers: Vec<Box<View>>,
}
impl StackView {
/// Creates a new empty StackView
pub fn new() -> Self {
StackView {
layers: Vec::new(),
}
}
}
impl View for StackView {
fn draw(&self, win: ncurses::WINDOW, size: Size) {
}
}

73
src/text_view.rs Normal file
View File

@ -0,0 +1,73 @@
use std::cmp::max;
use ncurses;
use super::Size;
use view::{View,DimensionRequest,SizeRequest};
use div::*;
/// A simple view showing a fixed text
pub struct TextView {
content: String,
}
/// Returns the number of lines required to display the given text with the
/// specified maximum line width.
fn get_line_span(line: &str, maxWidth: usize) -> usize {
let mut lines = 1;
let mut length = 0;
line.split(" ")
.map(|word| word.len())
.map(|l| {
length += l;
if length > maxWidth {
length = l;
lines += 1;
}
});
lines
}
impl TextView {
/// Creates a new TextView with the given content.
pub fn new(content: &str) -> Self {
TextView {
content: content.to_string(),
}
}
/// Returns the number of lines required to display the content
/// with the given width.
fn get_num_lines(&self, maxWidth: usize) -> usize {
self.content.split("\n")
.map(|line| get_line_span(line, maxWidth))
.fold(0, |sum, x| sum + x)
}
fn get_num_cols(&self, maxHeight: usize) -> usize {
(div_up_usize(self.content.len(), maxHeight)..self.content.len())
.find(|w| self.get_num_lines(*w) <= maxHeight)
.unwrap()
}
}
impl View for TextView {
fn draw(&self, win: ncurses::WINDOW, size: Size) {
}
fn get_min_size(&self, size: SizeRequest) -> Size {
match (size.w,size.h) {
(DimensionRequest::Unknown, DimensionRequest::Unknown) => Size::new(self.content.len() as u32, 1),
(DimensionRequest::Fixed(w),_) => {
let h = self.get_num_lines(w as usize) as u32;
Size::new(w, h)
},
(_,DimensionRequest::Fixed(h)) => {
let w = self.get_num_cols(h as usize) as u32;
Size::new(w, h)
},
(DimensionRequest::AtMost(w),DimensionRequest::AtMost(h)) => unreachable!(),
_ => unreachable!(),
}
}
}

12
src/to_view.rs Normal file
View File

@ -0,0 +1,12 @@
use super::View;
pub trait ToView {
fn to_view(self) -> Box<View>;
}
impl<'a> ToView for &'a str {
fn to_view(self) -> Box<View> {
Box::new(TextView::new(self))
}
}

View File

@ -1,95 +1,45 @@
use super::Cursive;
use super::Callback;
use std::rc::Rc;
use ncurses; use ncurses;
use focus::FocusChange; use event::EventResult;
pub trait ToView { pub use box_view::BoxView;
fn to_view(self) -> Box<View>; pub use stack_view::StackView;
pub use text_view::TextView;
use super::Size;
/// Describe constraints on a view layout in one dimension.
#[derive(PartialEq)]
pub enum DimensionRequest {
/// The view must use exactly the attached size.
Fixed(u32),
/// The view is free to choose its size if it stays under the limit.
AtMost(u32),
/// No clear restriction apply.
Unknown,
} }
impl<'a> ToView for &'a str { /// Describes constraints on a view layout.
fn to_view(self) -> Box<View> { #[derive(PartialEq)]
Box::new(TextView::new(self)) pub struct SizeRequest {
} /// Restriction on the view width
pub w: DimensionRequest,
/// Restriction on the view height
pub h: DimensionRequest,
} }
/// Main trait defining a view behaviour.
pub trait View { pub trait View {
fn focus_left(&mut self) -> FocusChange { FocusChange::LostFocus } /// Called when a key was pressed. Default implementation just ignores it.
fn focus_right(&mut self) -> FocusChange { FocusChange::LostFocus } fn on_key_event(&mut self, i32) -> EventResult { EventResult::Ignored }
fn focus_bottom(&mut self) -> FocusChange { FocusChange::LostFocus }
fn focus_top(&mut self) -> FocusChange { FocusChange::LostFocus }
fn click(&mut self) -> Option<Rc<Box<Callback>>> { None } /// Returns the minimum size the view requires under the given restrictions.
} fn get_min_size(&self, SizeRequest) -> Size { Size::new(1,1) }
pub struct TextView { /// Called once the size for this view has been decided, so it can
content: String, /// propagate the information to its children.
} fn layout(&mut self, Size) { }
impl TextView { /// Draws the view within the given bounds.
pub fn new(content: &str) -> Self { fn draw(&self, ncurses::WINDOW, Size);
TextView {
content: content.to_string(),
}
}
}
impl View for TextView {
}
pub struct Button {
label: String,
callback: Rc<Box<Fn(&mut Cursive)>>,
}
impl Button {
pub fn new<F>(label: &str, callback: F) -> Self
where F: 'static + Fn(&mut Cursive) {
Button {
label: label.to_string(),
callback: Rc::new(Box::new(callback)),
}
}
}
pub struct Dialog<'a> {
view: Box<View>,
buttons: Vec<Button>,
focus: Option<&'a Button>,
}
impl<'a> Dialog<'a> {
pub fn new<V: 'static + ToView>(view: V) -> Self {
Dialog{
view: view.to_view(),
buttons: Vec::new(),
focus: None,
}
}
pub fn button<F>(mut self, label: &str, callback: F) -> Self
where F: 'static + Fn(&mut Cursive) {
self.buttons.push(Button::new(label, callback));
self
}
}
impl<'a> View for Dialog<'a> {
fn click(&mut self) -> Option<Rc<Box<Callback>>> {
match self.focus {
None => return None,
Some(btn) => return Some(self.buttons[0].callback.clone()),
}
}
}
pub struct BackgroundView;
impl View for BackgroundView {
} }