mirror of
https://github.com/FliegendeWurst/cursive.git
synced 2024-11-27 11:16:03 +00:00
Add stack, box and text views
Also add some documentation
This commit is contained in:
parent
7dbeedcb49
commit
a387bf5f06
@ -3,9 +3,14 @@ name = "cursive"
|
||||
version = "0.1.0"
|
||||
authors = ["Alexandre Bury <alexandre.bury@gmail.com>"]
|
||||
|
||||
[[bin]]
|
||||
name = "cursive-example"
|
||||
|
||||
[lib]
|
||||
name = "cursive"
|
||||
|
||||
[dependencies]
|
||||
|
||||
[dependencies.ncurses]
|
||||
git = "https://github.com/jeaye/ncurses-rs"
|
||||
|
||||
|
46
src/box_view.rs
Normal file
46
src/box_view.rs
Normal 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
9
src/div.rs
Normal 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
14
src/event.rs
Normal 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>>),
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
pub enum FocusChange {
|
||||
KeptFocus,
|
||||
LostFocus,
|
||||
}
|
82
src/lib.rs
82
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<View>,
|
||||
layers: Vec<Box<View>>,
|
||||
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<V: 'static + View>(&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)
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
25
src/stack_view.rs
Normal file
25
src/stack_view.rs
Normal 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
73
src/text_view.rs
Normal 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
12
src/to_view.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
|
120
src/view.rs
120
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<View>;
|
||||
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<View> {
|
||||
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<Rc<Box<Callback>>> { 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<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 {
|
||||
/// Returns the minimum size the view requires under the given restrictions.
|
||||
fn get_min_size(&self, SizeRequest) -> Size { Size::new(1,1) }
|
||||
|
||||
/// Called once the size for this view has been decided, so it can
|
||||
/// propagate the information to its children.
|
||||
fn layout(&mut self, Size) { }
|
||||
|
||||
/// Draws the view within the given bounds.
|
||||
fn draw(&self, ncurses::WINDOW, Size);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user