diff --git a/Cargo.toml b/Cargo.toml index e89610f..1680185 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,9 +3,14 @@ name = "cursive" version = "0.1.0" authors = ["Alexandre Bury "] +[[bin]] +name = "cursive-example" + [lib] name = "cursive" +[dependencies] + [dependencies.ncurses] git = "https://github.com/jeaye/ncurses-rs" diff --git a/src/box_view.rs b/src/box_view.rs new file mode 100644 index 0000000..21d2570 --- /dev/null +++ b/src/box_view.rs @@ -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, +} + +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(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); + } +} diff --git a/src/div.rs b/src/div.rs new file mode 100644 index 0000000..4f69031 --- /dev/null +++ b/src/div.rs @@ -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 } +} diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 0000000..13b3a63 --- /dev/null +++ b/src/event.rs @@ -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; + +/// 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>), +} diff --git a/src/focus.rs b/src/focus.rs deleted file mode 100644 index 485b665..0000000 --- a/src/focus.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub enum FocusChange { - KeptFocus, - LostFocus, -} diff --git a/src/lib.rs b/src/lib.rs index 27ac02c..15f886c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,53 +1,56 @@ 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; +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 { - background: Box, - layers: Vec>, + stacks: StackView, running: bool, } -pub type Callback = Fn(&mut Cursive); - impl Cursive { + /// Creates a new Cursive root, and initialize ncurses. pub fn new() -> Self { ncurses::initscr(); ncurses::keypad(ncurses::stdscr, true); ncurses::noecho(); Cursive{ - background: Box::new(BackgroundView), - layers: Vec::new(), + stacks: StackView::new(), running: true, } } - pub fn new_layer(&mut self, view: V) { - self.layers.push(Box::new(view)); - } - + /// Runs the event loop. + /// It will wait for user input (key presses) and trigger callbacks accordingly. + /// Blocks until quit() is called. pub fn run(&mut self) { while self.running { ncurses::refresh(); // Handle event - match ncurses::getch() { - 10 => { - let cb = self.layers.last_mut().unwrap_or(&mut self.background).click(); - cb.map(|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), + let ch = ncurses::getch(); + match self.stacks.on_key_event(ch) { + EventResult::Ignored => (), + EventResult::Consumed(None) => (), + EventResult::Consumed(Some(cb)) => cb(self), } } } @@ -63,3 +66,36 @@ impl Drop for Cursive { 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) + } +} diff --git a/src/main.rs b/src/main.rs index 93eb4e4..00621e0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,13 +1,10 @@ extern crate cursive; -use cursive::{Cursive,Dialog}; +use cursive::Cursive; fn main() { let mut siv = Cursive::new(); - siv.new_layer( - Dialog::new("Hello World !") - .button("ok", |s| s.quit() )); siv.run(); } diff --git a/src/stack_view.rs b/src/stack_view.rs new file mode 100644 index 0000000..cd83576 --- /dev/null +++ b/src/stack_view.rs @@ -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>, +} + +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) { + } +} diff --git a/src/text_view.rs b/src/text_view.rs new file mode 100644 index 0000000..623e2ae --- /dev/null +++ b/src/text_view.rs @@ -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!(), + } + } +} diff --git a/src/to_view.rs b/src/to_view.rs new file mode 100644 index 0000000..e55d6a2 --- /dev/null +++ b/src/to_view.rs @@ -0,0 +1,12 @@ +use super::View; + +pub trait ToView { + fn to_view(self) -> Box; +} + +impl<'a> ToView for &'a str { + fn to_view(self) -> Box { + Box::new(TextView::new(self)) + } +} + diff --git a/src/view.rs b/src/view.rs index 45a00c4..4dfb9c3 100644 --- a/src/view.rs +++ b/src/view.rs @@ -1,95 +1,45 @@ -use super::Cursive; -use super::Callback; - -use std::rc::Rc; - use ncurses; -use focus::FocusChange; +use event::EventResult; -pub trait ToView { - fn to_view(self) -> Box; +pub use box_view::BoxView; +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 { - fn to_view(self) -> Box { - Box::new(TextView::new(self)) - } +/// Describes constraints on a view layout. +#[derive(PartialEq)] +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 { - fn focus_left(&mut self) -> FocusChange { FocusChange::LostFocus } - fn focus_right(&mut self) -> FocusChange { FocusChange::LostFocus } - fn focus_bottom(&mut self) -> FocusChange { FocusChange::LostFocus } - fn focus_top(&mut self) -> FocusChange { FocusChange::LostFocus } + /// Called when a key was pressed. Default implementation just ignores it. + fn on_key_event(&mut self, i32) -> EventResult { EventResult::Ignored } - fn click(&mut self) -> Option>> { None } -} - -pub struct TextView { - content: String, -} - -impl TextView { - pub fn new(content: &str) -> Self { - TextView { - content: content.to_string(), - } - } -} - -impl View for TextView { -} - -pub struct Button { - label: String, - callback: Rc>, -} - -impl Button { - pub fn new(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, - buttons: Vec