mirror of
https://github.com/FliegendeWurst/cursive.git
synced 2024-11-23 17:35:00 +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"
|
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
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;
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -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
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
116
src/view.rs
116
src/view.rs
@ -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 {
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user