diff --git a/Cargo.toml b/Cargo.toml
index 7725dd8..e68d17c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,32 +10,35 @@ license = "MIT"
name = "cursive"
readme = "Readme.md"
repository = "https://github.com/gyscos/Cursive"
-version = "0.4.0"
+version = "0.4.1"
[badges.travis-ci]
repository = "gyscos/Cursive"
[build-dependencies.skeptic]
optional = true
-version = "0.6"
+version = "0.7"
[dependencies]
-chan = "0.1.18"
num = "0.1"
odds = "0.2"
owning_ref = "0.2.4"
-toml = "0.2"
+toml = "0.3"
unicode-segmentation = "1.0"
unicode-width = "0.1"
+[dependencies.chan]
+optional = true
+version = "0.1.18"
+
+[dependencies.chan-signal]
+optional = true
+version = "0.2"
+
[dependencies.bear-lib-terminal]
optional = true
version = "1.3.1"
-[dependencies.chan-signal]
-optional = true
-version = "0.1"
-
[dependencies.ncurses]
features = ["wide"]
optional = true
@@ -44,20 +47,23 @@ version = "5.85.0"
[dependencies.pancurses]
features = ["wide"]
optional = true
-version = "0.7"
+version = "0.8"
[dependencies.termion]
optional = true
-version = "1.1.1"
+version = "1.3.0"
[dev-dependencies]
rand = "0.3"
-skeptic = "0.6"
+skeptic = "0.7"
[features]
-default = ["ncurses"]
+default = ["ncurses-backend"]
+
+ncurses-backend = ["ncurses"]
+termion-backend = ["termion", "chan", "chan-signal"]
pancurses-backend = ["pancurses"]
-termion-backend = ["termion", "chan-signal"]
+blt-backend = ["bear-lib-terminal"]
[lib]
name = "cursive"
diff --git a/Readme.md b/Readme.md
index cfbda31..53e890d 100644
--- a/Readme.md
+++ b/Readme.md
@@ -3,6 +3,7 @@
[![crates.io](https://meritbadge.herokuapp.com/cursive)](https://crates.io/crates/cursive)
[![Build Status](https://travis-ci.org/gyscos/Cursive.svg?branch=master)](https://travis-ci.org/gyscos/Cursive)
[![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE)
+[![Gitter chat](https://badges.gitter.im/gyscos/cursive.png)](https://gitter.im/cursive-rs/cursive)
Cursive is a TUI (Text User Interface) library for rust. It is currently based on jeaye's [ncurses-rs](https://github.com/jeaye/ncurses-rs), but [other backends are available](https://github.com/gyscos/Cursive/wiki/Backends).
@@ -50,12 +51,14 @@ fn main() {
Check out the other [examples](https://github.com/gyscos/Cursive/tree/master/examples) to get these results, and more:
-
-
-
-
-
-
+
_(Colors may depend on your terminal configuration.)_
diff --git a/examples/logs.rs b/examples/logs.rs
index 53d7b17..749a493 100644
--- a/examples/logs.rs
+++ b/examples/logs.rs
@@ -22,8 +22,8 @@ fn main() {
let (tx, rx) = mpsc::channel();
// Generate data in a separate thread.
- thread::spawn(|| {
- generate_logs(tx);
+ thread::spawn(move || {
+ generate_logs(&tx);
});
// And sets the view to read from the other end of the channel.
@@ -34,7 +34,7 @@ fn main() {
// We will only simulate log generation here.
// In real life, this may come from a running task, a separate process, ...
-fn generate_logs(tx: mpsc::Sender) {
+fn generate_logs(tx: &mpsc::Sender) {
let mut i = 1;
loop {
let line = format!("Interesting log line {}", i);
diff --git a/examples/progress.rs b/examples/progress.rs
index d22c1a8..3a83235 100644
--- a/examples/progress.rs
+++ b/examples/progress.rs
@@ -28,7 +28,7 @@ fn main() {
}
// Function to simulate a long process.
-fn fake_load(n_max: usize, counter: Counter) {
+fn fake_load(n_max: usize, counter: &Counter) {
for _ in 0..n_max {
thread::sleep(Duration::from_millis(5));
// The `counter.tick()` method increases the progress value
@@ -50,7 +50,7 @@ fn phase_1(s: &mut Cursive) {
.range(0, n_max)
.with_task(move |counter| {
// This closure will be called in a separate thread.
- fake_load(n_max, counter);
+ fake_load(n_max, &counter);
// When we're done, send a callback through the channel
cb.send(Box::new(coffee_break)).unwrap();
diff --git a/src/backend/curses/n.rs b/src/backend/curses/n.rs
index 256fc3d..467a6c2 100644
--- a/src/backend/curses/n.rs
+++ b/src/backend/curses/n.rs
@@ -94,7 +94,7 @@ impl backend::Backend for Concrete {
if 32 <= ch && ch <= 255 && ch != 127 {
Event::Char(utf8::read_char(ch as u8,
|| Some(ncurses::getch() as u8))
- .unwrap())
+ .unwrap())
} else {
parse_ncurses_char(ch)
}
@@ -226,6 +226,11 @@ fn parse_ncurses_char(ch: i32) -> Event {
// Values 8-10 (H,I,J) are used by other commands,
// so we probably won't receive them. Meh~
c @ 1...25 => Event::CtrlChar((b'a' + (c - 1) as u8) as char),
- _ => Event::Unknown(ch),
+ other => {
+ // Split the i32 into 4 bytes
+ Event::Unknown((0..4)
+ .map(|i| ((other >> (8 * i)) & 0xFF) as u8)
+ .collect())
+ }
}
}
diff --git a/src/backend/curses/pan.rs b/src/backend/curses/pan.rs
index 65d0d55..062df26 100644
--- a/src/backend/curses/pan.rs
+++ b/src/backend/curses/pan.rs
@@ -5,13 +5,11 @@ extern crate pancurses;
use self::super::find_closest;
use backend;
use event::{Event, Key};
-use std::cell::Cell;
use theme::{Color, ColorStyle, Effect};
use utf8;
pub struct Concrete {
window: pancurses::Window,
- current_style: Cell,
}
impl backend::Backend for Concrete {
@@ -23,13 +21,9 @@ impl backend::Backend for Concrete {
pancurses::cbreak();
pancurses::start_color();
pancurses::curs_set(0);
- window.bkgd(pancurses::COLOR_PAIR(ColorStyle::Background.id() as
- pancurses::chtype));
+ window.bkgd(pancurses::ColorPair(ColorStyle::Background.id() as u8));
- Concrete {
- window: window,
- current_style: Cell::new(ColorStyle::Background),
- }
+ Concrete { window: window }
}
fn screen_size(&self) -> (usize, usize) {
@@ -53,29 +47,18 @@ impl backend::Backend for Concrete {
}
fn with_color(&self, color: ColorStyle, f: F) {
- // TODO: pancurses doesn't have an `attr_get` equivalent
- // let mut current_style: pancurses::attr_t = 0;
- // let mut current_color: i16 = 0;
- // pancurses::attr_get(&mut current_style, &mut current_color);
- let current_style = self.current_style.get();
+ let (_, current_color_pair) = self.window.attrget();
+ let color_attribute = pancurses::ColorPair(color.id() as u8);
- let style = pancurses::COLOR_PAIR(color.id() as pancurses::chtype);
- self.window.attron(style);
-
- self.current_style.set(color);
+ self.window.attron(color_attribute);
f();
- self.current_style.set(current_style);
-
- // self.window.attroff(style);
- self.window.attron(pancurses::COLOR_PAIR(current_style.id() as
- pancurses::chtype));
+ self.window.attron(pancurses::ColorPair(current_color_pair as u8));
}
fn with_effect(&self, effect: Effect, f: F) {
let style = match effect {
- // A_REVERSE, taken from ncurses
- Effect::Reverse => 1 << (10 + 8u32),
- Effect::Simple => pancurses::A_NORMAL,
+ Effect::Reverse => pancurses::Attribute::Reverse,
+ Effect::Simple => pancurses::Attribute::Normal,
};
self.window.attron(style);
f();
@@ -111,19 +94,31 @@ impl backend::Backend for Concrete {
pancurses::Input::Character(c) if 32 <= (c as u32) &&
(c as u32) <= 255 => {
Event::Char(utf8::read_char(c as u8, || {
- self.window.getch().and_then(|i| match i {
+ self.window.getch().and_then(|i| match i {
pancurses::Input::Character(c) => {
Some(c as u8)
}
_ => None,
})
- })
- .unwrap())
+ })
+ .unwrap())
+ }
+ pancurses::Input::Character(c) => {
+ let mut bytes = [0u8; 4];
+ Event::Unknown(c.encode_utf8(&mut bytes)
+ .as_bytes()
+ .to_vec())
}
- pancurses::Input::Character(c) => Event::Unknown(c as i32),
// TODO: Some key combos are not recognized by pancurses,
// but are sent as Unknown. We could still parse them here.
- pancurses::Input::Unknown(i) => Event::Unknown(i),
+ pancurses::Input::Unknown(other) => {
+ Event::Unknown((0..4)
+ .map(|i| {
+ ((other >> (8 * i)) & 0xFF) as
+ u8
+ })
+ .collect())
+ }
// TODO: I honestly have no fucking idea what KeyCodeYes is
pancurses::Input::KeyCodeYes => Event::Refresh,
pancurses::Input::KeyBreak => Event::Key(Key::PauseBreak),
diff --git a/src/backend/termion.rs b/src/backend/termion.rs
index bb15999..5ae7851 100644
--- a/src/backend/termion.rs
+++ b/src/backend/termion.rs
@@ -3,6 +3,7 @@ extern crate termion;
extern crate chan_signal;
use self::termion::color as tcolor;
+use self::termion::event::Event as TEvent;
use self::termion::event::Key as TKey;
use self::termion::input::TermRead;
use self::termion::raw::IntoRawMode;
@@ -15,7 +16,6 @@ use std::collections::BTreeMap;
use std::fmt;
use std::io::Write;
use std::thread;
-use std::time;
use theme;
@@ -84,11 +84,11 @@ impl backend::Backend for Concrete {
let terminal = ::std::io::stdout().into_raw_mode().unwrap();
let (sender, receiver) = chan::async();
- thread::spawn(move || for key in ::std::io::stdin().keys() {
- if let Ok(key) = key {
- sender.send(map_key(key))
- }
- });
+ thread::spawn(move || for key in ::std::io::stdin().events() {
+ if let Ok(key) = key {
+ sender.send(map_key(key))
+ }
+ });
let backend = Concrete {
terminal: terminal,
@@ -185,29 +185,30 @@ impl backend::Backend for Concrete {
}
}
-fn map_key(key: TKey) -> Event {
- match key {
- TKey::Esc => Event::Key(Key::Esc),
- TKey::Backspace => Event::Key(Key::Backspace),
- TKey::Left => Event::Key(Key::Left),
- TKey::Right => Event::Key(Key::Right),
- TKey::Up => Event::Key(Key::Up),
- TKey::Down => Event::Key(Key::Down),
- TKey::Home => Event::Key(Key::Home),
- TKey::End => Event::Key(Key::End),
- TKey::PageUp => Event::Key(Key::PageUp),
- TKey::PageDown => Event::Key(Key::PageDown),
- TKey::Delete => Event::Key(Key::Del),
- TKey::Insert => Event::Key(Key::Ins),
- TKey::F(i) if i < 12 => Event::Key(Key::from_f(i)),
- TKey::F(j) => Event::Unknown(-(j as i32)),
- TKey::Char('\n') => Event::Key(Key::Enter),
- TKey::Char('\t') => Event::Key(Key::Tab),
- TKey::Char(c) => Event::Char(c),
- TKey::Ctrl('c') => Event::Exit,
- TKey::Ctrl(c) => Event::CtrlChar(c),
- TKey::Alt(c) => Event::AltChar(c),
- _ => Event::Unknown(-1),
+fn map_key(event: TEvent) -> Event {
+ match event {
+ TEvent::Unsupported(bytes) => Event::Unknown(bytes),
+ TEvent::Key(TKey::Esc) => Event::Key(Key::Esc),
+ TEvent::Key(TKey::Backspace) => Event::Key(Key::Backspace),
+ TEvent::Key(TKey::Left) => Event::Key(Key::Left),
+ TEvent::Key(TKey::Right) => Event::Key(Key::Right),
+ TEvent::Key(TKey::Up) => Event::Key(Key::Up),
+ TEvent::Key(TKey::Down) => Event::Key(Key::Down),
+ TEvent::Key(TKey::Home) => Event::Key(Key::Home),
+ TEvent::Key(TKey::End) => Event::Key(Key::End),
+ TEvent::Key(TKey::PageUp) => Event::Key(Key::PageUp),
+ TEvent::Key(TKey::PageDown) => Event::Key(Key::PageDown),
+ TEvent::Key(TKey::Delete) => Event::Key(Key::Del),
+ TEvent::Key(TKey::Insert) => Event::Key(Key::Ins),
+ TEvent::Key(TKey::F(i)) if i < 12 => Event::Key(Key::from_f(i)),
+ TEvent::Key(TKey::F(j)) => Event::Unknown(vec![j]),
+ TEvent::Key(TKey::Char('\n')) => Event::Key(Key::Enter),
+ TEvent::Key(TKey::Char('\t')) => Event::Key(Key::Tab),
+ TEvent::Key(TKey::Char(c)) => Event::Char(c),
+ TEvent::Key(TKey::Ctrl('c')) => Event::Exit,
+ TEvent::Key(TKey::Ctrl(c)) => Event::CtrlChar(c),
+ TEvent::Key(TKey::Alt(c)) => Event::AltChar(c),
+ _ => Event::Unknown(vec![]),
}
}
diff --git a/src/event.rs b/src/event.rs
index cf3c384..9e77d68 100644
--- a/src/event.rs
+++ b/src/event.rs
@@ -186,7 +186,7 @@ impl Key {
}
/// Represents an event as seen by the application.
-#[derive(PartialEq,Eq,Clone,Copy,Hash,Debug)]
+#[derive(PartialEq,Eq,Clone,Hash,Debug)]
pub enum Event {
/// Event fired when the window is resized.
WindowResize,
@@ -217,7 +217,7 @@ pub enum Event {
CtrlAlt(Key),
/// An unknown event was received.
- Unknown(i32),
+ Unknown(Vec),
#[doc(hidden)]
/// The application is about to exit.
diff --git a/src/lib.rs b/src/lib.rs
index bd0d636..f9a9028 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -66,6 +66,7 @@ extern crate odds;
extern crate num;
extern crate owning_ref;
+#[cfg(feature = "termion")]
#[macro_use]
extern crate chan;
@@ -399,6 +400,8 @@ impl Cursive {
self.screen_mut().find(sel, callback)
}
+ /// Tries to find the view identified by the given id.
+ ///
/// Convenient method to use `find` with a `view::Selector::Id`.
///
/// # Examples
@@ -439,6 +442,18 @@ impl Cursive {
self.find_id(id, views::RefCellView::::get_mut)
}
+ /// Moves the focus to the view identified by `id`.
+ ///
+ /// Convenient method to call `focus` with a `view::Selector::Id`.
+ pub fn focus_id(&mut self, id: &str) -> Result<(), ()> {
+ self.focus(&view::Selector::Id(id))
+ }
+
+ /// Moves the focus to the view identified by `sel`.
+ pub fn focus(&mut self, sel: &view::Selector) -> Result<(), ()> {
+ self.screen_mut().focus_view(sel)
+ }
+
/// Adds a global callback.
///
/// Will be triggered on the given key press when no view catches it.
@@ -616,7 +631,7 @@ impl Cursive {
if self.menubar.receive_events() {
self.menubar.on_event(event).process(self);
} else {
- match self.screen_mut().on_event(event) {
+ match self.screen_mut().on_event(event.clone()) {
// If the event was ignored,
// it is our turn to play with it.
EventResult::Ignored => self.on_event(event),
diff --git a/src/theme.rs b/src/theme.rs
index 44a6943..5b4f0e4 100644
--- a/src/theme.rs
+++ b/src/theme.rs
@@ -210,7 +210,7 @@ impl Default for Theme {
}
impl Theme {
- fn load(&mut self, table: &toml::Table) {
+ fn load(&mut self, table: &toml::value::Table) {
if let Some(&toml::Value::Boolean(shadow)) = table.get("shadow") {
self.shadow = shadow;
}
@@ -314,7 +314,7 @@ pub struct Palette {
impl Palette {
/// Fills `self` with the colors from the given `table`.
- fn load(&mut self, table: &toml::Table) {
+ fn load(&mut self, table: &toml::value::Table) {
load_color(&mut self.background, table.get("background"));
load_color(&mut self.shadow, table.get("shadow"));
load_color(&mut self.view, table.get("view"));
@@ -413,7 +413,7 @@ pub enum Error {
/// An error occured when reading the file.
Io(io::Error),
/// An error occured when parsing the toml content.
- Parse,
+ Parse(toml::de::Error),
}
impl From for Error {
@@ -422,6 +422,12 @@ impl From for Error {
}
}
+impl From for Error {
+ fn from(err: toml::de::Error) -> Self {
+ Error::Parse(err)
+ }
+}
+
impl Color {
fn parse(value: &str) -> Option {
Some(match value {
@@ -490,8 +496,7 @@ pub fn load_theme_file>(filename: P) -> Result {
/// Loads a theme string and sets it as active.
pub fn load_theme(content: &str) -> Result {
- let mut parser = toml::Parser::new(content);
- let table = try!(parser.parse().ok_or(Error::Parse));
+ let table = toml::de::from_str(content)?;
let mut theme = Theme::default();
theme.load(&table);
diff --git a/src/utils/mod.rs b/src/utils/mod.rs
index 527b991..6b33050 100644
--- a/src/utils/mod.rs
+++ b/src/utils/mod.rs
@@ -49,25 +49,29 @@ pub fn prefix<'a, I>(iter: I, available_width: usize, delimiter: &str)
let delimiter_width = delimiter.width();
let delimiter_len = delimiter.len();
+ // `current_width` is the width of everything
+ // before the next token, including any space.
let mut current_width = 0;
- let sum = iter.take_while(|token| {
+ let sum: usize = iter.take_while(|token| {
let width = token.width();
if current_width + width > available_width {
false
} else {
- if current_width != 0 {
- current_width += delimiter_width;
- }
+ // Include the delimiter after this token.
current_width += width;
+ current_width += delimiter_width;
true
}
})
.map(|token| token.len() + delimiter_len)
- .fold(0, |a, b| a + b);
+ .sum();
// We counted delimiter once too many times,
// but only if the iterator was non empty.
- let length = if sum == 0 { sum } else { sum - delimiter_len };
+ let length = sum.saturating_sub(delimiter_len);
+
+ // `current_width` includes a delimiter after the last token
+ debug_assert!(current_width <= available_width + delimiter_width);
Prefix {
length: length,
@@ -95,3 +99,15 @@ pub fn suffix<'a, I>(iter: I, width: usize, delimiter: &str) -> Prefix
pub fn simple_suffix(text: &str, width: usize) -> Prefix {
suffix(text.graphemes(true), width, "")
}
+
+#[cfg(test)]
+mod tests {
+ use utils;
+
+ #[test]
+ fn test_prefix() {
+ assert_eq!(utils::prefix(" abra ".split(' '), 5, " ").length, 5);
+ assert_eq!(utils::prefix("abra a".split(' '), 5, " ").length, 4);
+ assert_eq!(utils::prefix("a a br".split(' '), 5, " ").length, 3);
+ }
+}
diff --git a/src/view/mod.rs b/src/view/mod.rs
index 56201ce..36e6b59 100644
--- a/src/view/mod.rs
+++ b/src/view/mod.rs
@@ -114,7 +114,7 @@ pub trait View {
/// Draws the view with the given printer (includes bounds) and focus.
fn draw(&self, printer: &Printer);
- /// Finds the view pointed to by the given path.
+ /// Finds the view identified by the given selector.
///
/// See [`Finder::find`] for a nicer interface, implemented for all views.
///
@@ -127,6 +127,13 @@ pub trait View {
// TODO: FnMut -> FnOnce once it works
}
+ /// Moves the focus to the view identified by the given selector.
+ ///
+ /// Returns `Ok(())` if the view was found and selected.
+ fn focus_view(&mut self, &Selector) -> Result<(), ()> {
+ Err(())
+ }
+
/// This view is offered focus. Will it take it?
///
/// `source` indicates where the focus comes from.
diff --git a/src/view/scroll.rs b/src/view/scroll.rs
index 7c40d46..2e417e9 100644
--- a/src/view/scroll.rs
+++ b/src/view/scroll.rs
@@ -119,22 +119,28 @@ impl ScrollBase {
/// Scroll to the bottom of the view.
pub fn scroll_bottom(&mut self) {
- self.start_line = self.content_height - self.view_height;
+ if self.scrollable() {
+ self.start_line = self.content_height - self.view_height;
+ }
}
/// Scroll down by the given number of line.
///
/// Never further than the bottom of the view.
pub fn scroll_down(&mut self, n: usize) {
- self.start_line = min(self.start_line + n,
- self.content_height - self.view_height);
+ if self.scrollable() {
+ self.start_line = min(self.start_line + n,
+ self.content_height - self.view_height);
+ }
}
/// Scroll up by the given number of lines.
///
/// Never above the top of the view.
pub fn scroll_up(&mut self, n: usize) {
- self.start_line -= min(self.start_line, n);
+ if self.scrollable() {
+ self.start_line -= min(self.start_line, n);
+ }
}
/// Draws the scroll bar and the content using the given drawer.
diff --git a/src/view/view_wrapper.rs b/src/view/view_wrapper.rs
index 4533115..07d19d6 100644
--- a/src/view/view_wrapper.rs
+++ b/src/view/view_wrapper.rs
@@ -57,6 +57,11 @@ pub trait ViewWrapper {
self.with_view_mut(|v| v.find_any(selector, callback));
}
+ /// Wraps the `focus_view` method.
+ fn wrap_focus_view(&mut self, selector: &Selector) -> Result<(), ()> {
+ self.with_view_mut(|v| v.focus_view(selector)).unwrap_or(Err(()))
+ }
+
/// Wraps the `needs_relayout` method.
fn wrap_needs_relayout(&self) -> bool {
self.with_view(|v| v.needs_relayout()).unwrap_or(true)
@@ -92,6 +97,10 @@ impl View for T {
fn needs_relayout(&self) -> bool {
self.wrap_needs_relayout()
}
+
+ fn focus_view(&mut self, selector: &Selector) -> Result<(), ()> {
+ self.wrap_focus_view(selector)
+ }
}
/// Convenient macro to implement the [`ViewWrapper`] trait.
diff --git a/src/views/dialog.rs b/src/views/dialog.rs
index 325657f..3d97afd 100644
--- a/src/views/dialog.rs
+++ b/src/views/dialog.rs
@@ -316,7 +316,7 @@ impl View for Dialog {
match self.focus {
// If we are on the content, we can only go down.
Focus::Content => {
- match self.content.on_event(event) {
+ match self.content.on_event(event.clone()) {
EventResult::Ignored if !self.buttons.is_empty() => {
match event {
Event::Key(Key::Down) |
@@ -334,7 +334,7 @@ impl View for Dialog {
}
// If we are on a button, we have more choice
Focus::Button(i) => {
- match self.buttons[i].on_event(event) {
+ match self.buttons[i].on_event(event.clone()) {
EventResult::Ignored => {
match event {
// Up goes back to the content
@@ -403,4 +403,8 @@ impl View for Dialog {
callback: Box) {
self.content.find_any(selector, callback);
}
+
+ fn focus_view(&mut self, selector: &Selector) -> Result<(), ()> {
+ self.content.focus_view(selector)
+ }
}
diff --git a/src/views/edit_view.rs b/src/views/edit_view.rs
index 80a9b73..7683a5f 100644
--- a/src/views/edit_view.rs
+++ b/src/views/edit_view.rs
@@ -3,6 +3,7 @@
use {Cursive, Printer, With};
use direction::Direction;
use event::{Callback, Event, EventResult, Key};
+use std::cell::RefCell;
use std::rc::Rc;
use theme::{ColorStyle, Effect};
@@ -73,7 +74,7 @@ pub struct EditView {
/// Callback when the content is modified.
///
- /// Will be called with the current content and the cursor position
+ /// Will be called with the current content and the cursor position.
on_edit: Option>,
/// Callback when is pressed.
@@ -135,25 +136,118 @@ impl EditView {
self.enabled = true;
}
+ /// Sets a mutable callback to be called whenever the content is modified.
+ ///
+ /// `callback` will be called with the view
+ /// content and the current cursor position.
+ ///
+ /// *Warning*: this callback cannot be called recursively. If you somehow
+ /// trigger this callback again in the given closure, it will be ignored.
+ ///
+ /// If you don't need a mutable closure but want the possibility of
+ /// recursive calls, see [`set_on_edit`](#method.set_on_edit).
+ pub fn set_on_edit_mut(&mut self, callback: F)
+ where F: FnMut(&mut Cursive, &str, usize) + 'static
+ {
+ let callback = RefCell::new(callback);
+ // Here's the weird trick: if we're already borrowed,
+ // just ignored the callback.
+ self.set_on_edit(move |s, text, cursor| {
+ if let Ok(mut f) = callback.try_borrow_mut() {
+ // Beeeaaah that's ugly.
+ // Why do we need to manually dereference here?
+ (&mut *f)(s, text, cursor);
+ }
+ });
+ }
+
/// Sets a callback to be called whenever the content is modified.
///
/// `callback` will be called with the view
/// content and the current cursor position.
- pub fn on_edit(mut self, callback: F) -> Self
+ ///
+ /// This callback can safely trigger itself recursively if needed
+ /// (for instance if you call `on_event` on this view from the callback).
+ ///
+ /// If you need a mutable closure and don't care about the recursive
+ /// aspect, see [`set_on_edit_mut`](#method.set_on_edit_mut).
+ pub fn set_on_edit(&mut self, callback: F)
where F: Fn(&mut Cursive, &str, usize) + 'static
{
self.on_edit = Some(Rc::new(callback));
- self
+ }
+
+ /// Sets a mutable callback to be called whenever the content is modified.
+ ///
+ /// Chainable variant. See [`set_on_edit_mut`](#method.set_on_edit_mut).
+ pub fn on_edit_mut(self, callback: F) -> Self
+ where F: FnMut(&mut Cursive, &str, usize) + 'static
+ {
+ self.with(|v| v.set_on_edit_mut(callback))
+ }
+
+ /// Sets a callback to be called whenever the content is modified.
+ ///
+ /// Chainable variant. See [`set_on_edit`](#method.set_on_edit).
+ pub fn on_edit(self, callback: F) -> Self
+ where F: Fn(&mut Cursive, &str, usize) + 'static
+ {
+ self.with(|v| v.set_on_edit(callback))
+ }
+
+ /// Sets a mutable callback to be called when `` is pressed.
+ ///
+ /// `callback` will be given the content of the view.
+ ///
+ /// *Warning*: this callback cannot be called recursively. If you somehow
+ /// trigger this callback again in the given closure, it will be ignored.
+ ///
+ /// If you don't need a mutable closure but want the possibility of
+ /// recursive calls, see [`set_on_submit`](#method.set_on_submit).
+ pub fn set_on_submit_mut(&mut self, callback: F)
+ where F: FnMut(&mut Cursive, &str) + 'static
+ {
+ // TODO: don't duplicate all those methods.
+ // Instead, have some generic function immutify()
+ // or something that wraps a FnMut closure.
+ let callback = RefCell::new(callback);
+ self.set_on_submit(move |s, text| if let Ok(mut f) =
+ callback.try_borrow_mut() {
+ (&mut *f)(s, text);
+ });
}
/// Sets a callback to be called when `` is pressed.
///
/// `callback` will be given the content of the view.
- pub fn on_submit(mut self, callback: F) -> Self
+ ///
+ /// This callback can safely trigger itself recursively if needed
+ /// (for instance if you call `on_event` on this view from the callback).
+ ///
+ /// If you need a mutable closure and don't care about the recursive
+ /// aspect, see [`set_on_submit_mut`](#method.set_on_submit_mut).
+ pub fn set_on_submit(&mut self, callback: F)
where F: Fn(&mut Cursive, &str) + 'static
{
self.on_submit = Some(Rc::new(callback));
- self
+ }
+
+ /// Sets a mutable callback to be called when `` is pressed.
+ ///
+ /// Chainable variant.
+ pub fn on_submit_mut(self, callback: F) -> Self
+ where F: FnMut(&mut Cursive, &str) + 'static
+ {
+ self.with(|v| v.set_on_submit_mut(callback))
+ }
+
+ /// Sets a callback to be called when `` is pressed.
+ ///
+ /// Chainable variant.
+ pub fn on_submit(self, callback: F) -> Self
+ where F: Fn(&mut Cursive, &str) + 'static
+ {
+ self.with(|v| v.set_on_submit(callback))
}
/// Enable or disable this view.
@@ -267,7 +361,7 @@ fn make_small_stars(length: usize) -> &'static str {
impl View for EditView {
fn draw(&self, printer: &Printer) {
- assert!(printer.size.x == self.last_length,
+ assert_eq!(printer.size.x, self.last_length,
"Was promised {}, received {}",
self.last_length,
printer.size.x);
@@ -343,6 +437,7 @@ impl View for EditView {
fn layout(&mut self, size: Vec2) {
self.last_length = size.x;
+ // println_stderr!("Promised: {}", size.x);
}
fn take_focus(&mut self, _: Direction) -> bool {
@@ -401,7 +496,7 @@ impl View for EditView {
let cb = self.on_edit.clone().map(|cb| {
- // Get a new Rc on it
+ // Get a new Rc on the content
let content = self.content.clone();
let cursor = self.cursor;
diff --git a/src/views/id_view.rs b/src/views/id_view.rs
index 7784335..21302eb 100644
--- a/src/views/id_view.rs
+++ b/src/views/id_view.rs
@@ -28,4 +28,11 @@ impl ViewWrapper for IdView {
s => self.view.find_any(s, callback),
}
}
+
+ fn wrap_focus_view(&mut self, selector: &Selector) -> Result<(), ()> {
+ match selector {
+ &Selector::Id(id) if id == self.id => Ok(()),
+ s => self.view.focus_view(s),
+ }
+ }
}
diff --git a/src/views/key_event_view.rs b/src/views/key_event_view.rs
index cbbc1b7..8206860 100644
--- a/src/views/key_event_view.rs
+++ b/src/views/key_event_view.rs
@@ -46,7 +46,7 @@ impl ViewWrapper for KeyEventView {
wrap_impl!(self.content: T);
fn wrap_on_event(&mut self, event: Event) -> EventResult {
- match self.content.on_event(event) {
+ match self.content.on_event(event.clone()) {
EventResult::Ignored => {
match self.callbacks.get(&event) {
None => EventResult::Ignored,
diff --git a/src/views/linear_layout.rs b/src/views/linear_layout.rs
index cc0ddad..0e44298 100644
--- a/src/views/linear_layout.rs
+++ b/src/views/linear_layout.rs
@@ -26,6 +26,7 @@ struct Child {
}
impl Child {
+ // Compute and caches the required size.
fn required_size(&mut self, req: Vec2) -> Vec2 {
self.size = self.view.required_size(req);
self.size
@@ -36,6 +37,17 @@ impl Child {
}
}
+fn cap<'a, I: Iterator- >(iter: I, max: usize) {
+ let mut available = max;
+ for item in iter {
+ if *item > available {
+ *item = available;
+ }
+
+ available -= *item;
+ }
+}
+
impl LinearLayout {
/// Creates a new layout with the given orientation.
pub fn new(orientation: direction::Orientation) -> Self {
@@ -197,6 +209,7 @@ impl View for LinearLayout {
let o = self.orientation;
for child in &mut self.children {
+ // Every item has the same size orthogonal to the layout
child.size.set_axis_from(o.swap(), &size);
child.view.layout(size.with_axis_from(o, &child.size));
}
@@ -225,8 +238,8 @@ impl View for LinearLayout {
return ideal;
}
- // Ok, so maybe it didn't.
- // Budget cuts, everyone.
+ // Ok, so maybe it didn't. Budget cuts, everyone.
+ // Let's pretend we have almost no space in this direction.
let budget_req = req.with_axis(self.orientation, 1);
// println_stderr!("Budget req: {:?}", budget_req);
@@ -240,8 +253,15 @@ impl View for LinearLayout {
// println_stderr!("Desperate: {:?}", desperate);
// This is the lowest we'll ever go. It better fit at least.
+ let orientation = self.orientation;
if !desperate.fits_in(req) {
// Just give up...
+ // TODO: hard-cut
+ cap(self.children
+ .iter_mut()
+ .map(|c| c.size.get_mut(orientation)),
+ *req.get(self.orientation));
+
// TODO: print some error message or something
// println_stderr!("Seriously? {:?} > {:?}???", desperate, req);
// self.cache = Some(SizeCache::build(desperate, req));
@@ -333,7 +353,7 @@ impl View for LinearLayout {
}
fn on_event(&mut self, event: Event) -> EventResult {
- match self.children[self.focus].view.on_event(event) {
+ match self.children[self.focus].view.on_event(event.clone()) {
EventResult::Ignored => {
match event {
Event::Shift(Key::Tab) if self.focus > 0 => {
@@ -381,4 +401,15 @@ impl View for LinearLayout {
child.view.find_any(selector, Box::new(|any| callback(any)));
}
}
+
+ fn focus_view(&mut self, selector: &Selector) -> Result<(), ()> {
+ for (i, child) in self.children.iter_mut().enumerate() {
+ if child.view.focus_view(selector).is_ok() {
+ self.focus = i;
+ return Ok(());
+ }
+ }
+
+ Err(())
+ }
}
diff --git a/src/views/list_view.rs b/src/views/list_view.rs
index 7f3490c..48a3fa3 100644
--- a/src/views/list_view.rs
+++ b/src/views/list_view.rs
@@ -6,6 +6,8 @@ use event::{Callback, Event, EventResult, Key};
use std::any::Any;
use std::rc::Rc;
+
+use unicode_width::UnicodeWidthStr;
use vec::Vec2;
use view::ScrollBase;
use view::Selector;
@@ -116,7 +118,10 @@ impl ListView {
direction::Relative::Front => {
let start = if from_focus { self.focus } else { 0 };
- Box::new(self.children.iter_mut().enumerate().skip(start))
+ Box::new(self.children
+ .iter_mut()
+ .enumerate()
+ .skip(start))
}
direction::Relative::Back => {
let end = if from_focus {
@@ -181,10 +186,11 @@ impl View for ListView {
let offset = self.children
.iter()
.map(Child::label)
- .map(str::len)
+ .map(UnicodeWidthStr::width)
.max()
.unwrap_or(0) + 1;
+ // println_stderr!("Offset: {}", offset);
self.scrollbase.draw(printer, |printer, i| match self.children[i] {
Child::Row(ref label, ref view) => {
printer.print((0, 0), label);
@@ -195,12 +201,14 @@ impl View for ListView {
}
fn required_size(&mut self, req: Vec2) -> Vec2 {
- let label_size = self.children
+ // We'll show 2 columns: the labels, and the views.
+ let label_width = self.children
.iter()
.map(Child::label)
- .map(str::len)
+ .map(UnicodeWidthStr::width)
.max()
.unwrap_or(0);
+
let view_size = self.children
.iter_mut()
.filter_map(Child::view)
@@ -209,26 +217,34 @@ impl View for ListView {
.unwrap_or(0);
if self.children.len() > req.y {
- Vec2::new(label_size + 1 + view_size + 2, req.y)
+ Vec2::new(label_width + 1 + view_size + 2, req.y)
} else {
- Vec2::new(label_size + 1 + view_size, self.children.len())
+ Vec2::new(label_width + 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
+ // We'll show 2 columns: the labels, and the views.
+ let label_width = self.children
.iter()
.map(Child::label)
- .map(str::len)
+ .map(UnicodeWidthStr::width)
.max()
.unwrap_or(0);
- let mut available = size.x - label_size - 1;
- if self.children.len() > size.y {
- available -= 2;
- }
+ let spacing = 1;
+ let scrollbar_width = if self.children.len() > size.y { 2 } else { 0 };
+
+ let available = if label_width + spacing + scrollbar_width > size.x {
+ // We have no space for the kids! :(
+ 0
+ } else {
+ size.x - label_width - spacing - scrollbar_width
+ };
+
+ // println_stderr!("Available: {}", available);
for child in self.children.iter_mut().filter_map(Child::view) {
child.layout(Vec2::new(available, 1));
@@ -241,7 +257,7 @@ impl View for ListView {
}
if let Child::Row(_, ref mut view) = self.children[self.focus] {
- let result = view.on_event(event);
+ let result = view.on_event(event.clone());
if result.is_consumed() {
return result;
}
@@ -282,10 +298,11 @@ impl View for ListView {
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() {
+ 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
@@ -304,4 +321,18 @@ impl View for ListView {
view.find_any(selector, Box::new(|any| callback(any)));
}
}
+
+ fn focus_view(&mut self, selector: &Selector) -> Result<(), ()> {
+ if let Some(i) = self.children
+ .iter_mut()
+ .enumerate()
+ .filter_map(|(i, v)| v.view().map(|v| (i, v)))
+ .filter_map(|(i, v)| v.focus_view(selector).ok().map(|_| i))
+ .next() {
+ self.focus = i;
+ Ok(())
+ } else {
+ Err(())
+ }
+ }
}
diff --git a/src/views/panel.rs b/src/views/panel.rs
index 8b1b1f1..e3720d6 100644
--- a/src/views/panel.rs
+++ b/src/views/panel.rs
@@ -20,12 +20,27 @@ impl ViewWrapper for Panel {
fn wrap_required_size(&mut self, req: Vec2) -> Vec2 {
// TODO: make borders conditional?
- self.view.required_size(req - (2, 2)) + (2, 2)
+ let req = if Vec2::new(2, 2).fits_in(req) {
+ req - (2, 2)
+ } else {
+ Vec2::zero()
+ };
+
+ self.view.required_size(req) + (2, 2)
}
fn wrap_draw(&self, printer: &Printer) {
printer.print_box((0, 0), printer.size, true);
- self.view
- .draw(&printer.sub_printer((1, 1), printer.size - (2, 2), true));
+ self.view.draw(&printer.sub_printer((1, 1),
+ printer.size - (2, 2),
+ true));
+ }
+
+ fn wrap_layout(&mut self, size: Vec2) {
+ self.view.layout(if Vec2::new(2, 2).fits_in(size) {
+ size - (2, 2)
+ } else {
+ size
+ });
}
}
diff --git a/src/views/select_view.rs b/src/views/select_view.rs
index d7181ee..b0301cd 100644
--- a/src/views/select_view.rs
+++ b/src/views/select_view.rs
@@ -340,6 +340,9 @@ impl View for SelectView {
ColorStyle::Highlight
};
let x = printer.size.x;
+ if x == 0 {
+ return;
+ }
printer.with_color(style, |printer| {
diff --git a/src/views/stack_view.rs b/src/views/stack_view.rs
index 7c0feb6..2806e15 100644
--- a/src/views/stack_view.rs
+++ b/src/views/stack_view.rs
@@ -186,4 +186,14 @@ impl View for StackView {
layer.view.find_any(selector, Box::new(|any| callback(any)));
}
}
+
+ fn focus_view(&mut self, selector: &Selector) -> Result<(), ()> {
+ for layer in &mut self.layers {
+ if layer.view.focus_view(selector).is_ok() {
+ return Ok(());
+ }
+ }
+
+ Err(())
+ }
}
diff --git a/src/xy.rs b/src/xy.rs
index 4416dbf..3e22b46 100644
--- a/src/xy.rs
+++ b/src/xy.rs
@@ -45,6 +45,14 @@ impl XY {
}
}
+ /// Returns a mutable reference to the value on the given axis.
+ pub fn get_mut(&mut self, o: Orientation) -> &mut T {
+ match o {
+ Orientation::Horizontal => &mut self.x,
+ Orientation::Vertical => &mut self.y,
+ }
+ }
+
/// Returns a new `XY` of tuples made by zipping `self` and `other`.
pub fn zip(self, other: XY) -> XY<(T, U)> {
XY::new((self.x, other.x), (self.y, other.y))