From e29511e7571d99006e557dff97aae67fa712347b Mon Sep 17 00:00:00 2001 From: Alexandre Bury Date: Sun, 24 Jul 2016 23:00:13 -0700 Subject: [PATCH] Add ProgressBar Also make Callback its own NewType to add comversion methods. --- examples/progress.rs | 51 ++++++++++++++++++ src/backend/curses.rs | 3 ++ src/event.rs | 66 ++++++++++++++++++----- src/lib.rs | 3 +- src/menu.rs | 2 +- src/prelude.rs | 4 +- src/view/button.rs | 20 ++++--- src/view/dialog.rs | 31 +++++++++++ src/view/edit_view.rs | 7 +-- src/view/id_view.rs | 11 ++++ src/view/key_event_view.rs | 3 +- src/view/menu_popup.rs | 4 +- src/view/mod.rs | 6 ++- src/view/progress_bar.rs | 107 +++++++++++++++++++++++++++++++++++++ src/view/select_view.rs | 4 +- 15 files changed, 278 insertions(+), 44 deletions(-) create mode 100644 examples/progress.rs create mode 100644 src/view/progress_bar.rs diff --git a/examples/progress.rs b/examples/progress.rs new file mode 100644 index 0000000..0b9ff23 --- /dev/null +++ b/examples/progress.rs @@ -0,0 +1,51 @@ +extern crate cursive; + +use cursive::prelude::*; + +use std::thread; +use std::time::Duration; +use std::sync::{Arc, Mutex}; +use std::sync::atomic::{AtomicUsize, Ordering}; + +fn main() { + let mut siv = Cursive::new(); + + siv.add_layer(Dialog::new(Button::new("Start", |s| { + // These two values will allow us to communicate. + let value = Arc::new(AtomicUsize::new(0)); + let cb = Arc::new(Mutex::new(None)); + + let n_max = 1000; + + s.find_id::("dialog") + .unwrap() + .set_content(ProgressBar::new() + .range(0, n_max) + .with_value(value.clone()) + .with_callback(cb.clone())); + + // Spawn a thread to process things in the background. + thread::spawn(move || { + for _ in 0..n_max { + thread::sleep(Duration::from_millis(3)); + value.fetch_add(1, Ordering::Relaxed); + } + *cb.lock().unwrap() = Some(Box::new(move |s| { + s.pop_layer(); + s.add_layer(Dialog::new(TextView::new("Phew, that was \ + a lot of work!")) + .title("Work done!") + .button("Sure!", |s| s.quit())); + })); + }); + + })) + .title("Progress bar example") + .padding_top(1) + .padding_bottom(1) + .with_id("dialog")); + + siv.set_fps(10); + + siv.run(); +} diff --git a/src/backend/curses.rs b/src/backend/curses.rs index 45babec..c328034 100644 --- a/src/backend/curses.rs +++ b/src/backend/curses.rs @@ -105,6 +105,9 @@ impl backend::Backend for NcursesBackend { fn parse_ncurses_char(ch: i32) -> Event { match ch { + // Value sent by ncurses when nothing happens + -1 => Event::Refresh, + // Values under 256 are chars and control values // // Tab is '\t' diff --git a/src/event.rs b/src/event.rs index ae60145..fbf6707 100644 --- a/src/event.rs +++ b/src/event.rs @@ -14,12 +14,47 @@ //! table is checked. use std::rc::Rc; +use std::ops::Deref; use Cursive; /// Callback is a function that can be triggered by an event. /// It has a mutable access to the cursive root. -pub type Callback = Rc; +#[derive(Clone)] +pub struct Callback(Rc>); +// TODO: remove the Box when Box -> Rc is possible + +impl Callback { + /// Wraps the given function into a `Callback` object. + pub fn from_fn(f: F) -> Self { + Callback(Rc::new(Box::new(f))) + } +} + +impl Deref for Callback { + type Target = Box; + fn deref<'a>(&'a self) -> &'a Box { + &self.0 + } +} + +impl From>> for Callback { + fn from(f: Rc>) -> Self { + Callback(f) + } +} + +impl From> for Callback { + fn from(f: Box) -> Self { + Callback(Rc::new(f)) + } +} + +impl From> for Callback { + fn from(f: Box) -> Self { + Callback(Rc::new(f)) + } +} /// Answer to an event notification. /// The event can be consumed or ignored. @@ -27,13 +62,13 @@ 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), + Consumed(Option), // TODO: make this a FnOnce? } impl EventResult { /// Convenient method to create `Consumed(Some(f))` pub fn with_cb(f: F) -> Self { - EventResult::Consumed(Some(Rc::new(f))) + EventResult::Consumed(Some(Callback::from_fn(f))) } /// Returns `true` if `self` is `EventResult::Consumed`. @@ -138,29 +173,32 @@ impl Key { /// Represents an event as seen by the application. #[derive(PartialEq,Eq,Clone,Copy,Hash,Debug)] pub enum Event { - /// Event fired when the window is resized + /// Event fired when the window is resized. WindowResize, - /// A character was entered (includes numbers, punctuation, ...) + /// Event fired regularly when a auto-refresh is set. + Refresh, + + /// A character was entered (includes numbers, punctuation, ...). Char(char), - /// A character was entered with the Ctrl key pressed + /// A character was entered with the Ctrl key pressed. CtrlChar(char), - /// A character was entered with the Alt key pressed + /// A character was entered with the Alt key pressed. AltChar(char), - /// A non-character key was pressed + /// A non-character key was pressed. Key(Key), - /// A non-character key was pressed with the Shift key pressed + /// A non-character key was pressed with the Shift key pressed. Shift(Key), - /// A non-character key was pressed with the Alt key pressed + /// A non-character key was pressed with the Alt key pressed. Alt(Key), - /// A non-character key was pressed with the Shift and Alt keys pressed + /// A non-character key was pressed with the Shift and Alt keys pressed. AltShift(Key), - /// A non-character key was pressed with the Ctrl key pressed + /// A non-character key was pressed with the Ctrl key pressed. Ctrl(Key), - /// A non-character key was pressed with the Ctrl and Shift keys pressed + /// A non-character key was pressed with the Ctrl and Shift keys pressed. CtrlShift(Key), - /// A non-character key was pressed with the Ctrl and Alt keys pressed + /// A non-character key was pressed with the Ctrl and Alt keys pressed. CtrlAlt(Key), /// An unknown event was received. diff --git a/src/lib.rs b/src/lib.rs index bcd3528..b30d6a6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -97,7 +97,6 @@ pub use printer::Printer; use backend::{Backend, NcursesBackend}; use std::any::Any; -use std::rc::Rc; use std::collections::HashMap; use std::path::Path; @@ -364,7 +363,7 @@ impl Cursive { pub fn add_global_callback>(&mut self, event: E, cb: F) where F: Fn(&mut Cursive) + 'static { - self.global_callbacks.insert(event.into(), Rc::new(cb)); + self.global_callbacks.insert(event.into(), Callback::from_fn(cb)); } /// Convenient method to add a layer to the current screen. diff --git a/src/menu.rs b/src/menu.rs index 2f2208e..a9b4b90 100644 --- a/src/menu.rs +++ b/src/menu.rs @@ -98,7 +98,7 @@ impl MenuTree { pub fn add_leaf(&mut self, title: &str, cb: F) { self.children - .push(MenuItem::Leaf(title.to_string(), Rc::new(cb))); + .push(MenuItem::Leaf(title.to_string(), Callback::from_fn(cb))); } /// Adds a actionnable leaf to the end of this tree - chainable variant. diff --git a/src/prelude.rs b/src/prelude.rs index bb2ce3c..8895b57 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -9,7 +9,7 @@ pub use {Cursive, Printer, With}; pub use event::{Event, Key}; pub use view::{BoxView, Button, Checkbox, Dialog, EditView, FullView, IdView, - KeyEventView, LinearLayout, ListView, SelectView, Selector, - TextView, View}; + Identifiable, KeyEventView, LinearLayout, ListView, + ProgressBar, SelectView, Selector, TextView, View}; pub use vec::Vec2; pub use menu::MenuTree; diff --git a/src/view/button.rs b/src/view/button.rs index 96f068a..3aa2b93 100644 --- a/src/view/button.rs +++ b/src/view/button.rs @@ -1,14 +1,12 @@ -use std::rc::Rc; +use unicode_width::UnicodeWidthStr; +use {Cursive, Printer, With}; +use align::HAlign; +use event::*; use direction::Direction; use theme::ColorStyle; -use Cursive; -use With; use vec::Vec2; use view::View; -use event::*; -use Printer; -use unicode_width::UnicodeWidthStr; /// Simple text label with a callback when is pressed. /// @@ -33,7 +31,7 @@ impl Button { { Button { label: label.to_string(), - callback: Rc::new(cb), + callback: Callback::from_fn(cb), enabled: true, } } @@ -82,12 +80,12 @@ impl View for Button { } else { ColorStyle::Highlight }; - let x = printer.size.x - 1; + + let offset = + HAlign::Center.get_offset(self.label.len() + 2, printer.size.x); printer.with_color(style, |printer| { - printer.print((1, 0), &self.label); - printer.print((0, 0), "<"); - printer.print((x, 0), ">"); + printer.print((offset, 0), &format!("<{}>", self.label)); }); } diff --git a/src/view/dialog.rs b/src/view/dialog.rs index 63b6bf7..ac072d8 100644 --- a/src/view/dialog.rs +++ b/src/view/dialog.rs @@ -56,6 +56,13 @@ impl Dialog { } } + /// Sets the content for this dialog. + /// + /// Previous content will be dropped. + pub fn set_content(&mut self, view: V) { + self.content = Box::new(view); + } + /// Convenient method to create an infobox. /// /// It will contain the given text and a `Ok` dismiss button. @@ -108,6 +115,30 @@ impl Dialog { self } + + /// Sets the top padding in the dialog (under the title). + pub fn padding_top(mut self, padding: usize) -> Self { + self.padding.top = padding; + self + } + + /// Sets the bottom padding in the dialog (under buttons). + pub fn padding_bottom(mut self, padding: usize) -> Self { + self.padding.bottom = padding; + self + } + + /// Sets the left padding in the dialog. + pub fn padding_left(mut self, padding: usize) -> Self { + self.padding.left = padding; + self + } + + /// Sets the right padding in the dialog. + pub fn padding_right(mut self, padding: usize) -> Self { + self.padding.right = padding; + self + } } impl View for Dialog { diff --git a/src/view/edit_view.rs b/src/view/edit_view.rs index e502394..1225a3a 100644 --- a/src/view/edit_view.rs +++ b/src/view/edit_view.rs @@ -5,7 +5,7 @@ use With; use direction::Direction; use theme::{ColorStyle, Effect}; use vec::Vec2; -use view::{IdView, View}; +use view::View; use event::*; use Printer; @@ -135,11 +135,6 @@ impl EditView { self } - - /// Wraps this view into an IdView with the given id. - pub fn with_id(self, label: &str) -> IdView { - IdView::new(label, self) - } } impl View for EditView { diff --git a/src/view/id_view.rs b/src/view/id_view.rs index 102107f..8b9f3c6 100644 --- a/src/view/id_view.rs +++ b/src/view/id_view.rs @@ -28,3 +28,14 @@ impl ViewWrapper for IdView { } } } + +/// Makes a view wrappable in an `IdView`. +pub trait Identifiable: View + Sized { + + /// Wraps this view into an IdView with the given id. + fn with_id(self, id: &str) -> IdView { + IdView::new(id, self) + } +} + +impl Identifiable for T {} diff --git a/src/view/key_event_view.rs b/src/view/key_event_view.rs index 93f62c2..650acc0 100644 --- a/src/view/key_event_view.rs +++ b/src/view/key_event_view.rs @@ -1,5 +1,4 @@ use std::collections::HashMap; -use std::rc::Rc; use Cursive; use event::{Callback, Event, EventResult}; @@ -35,7 +34,7 @@ impl KeyEventView { pub fn register>(mut self, event: E, cb: F) -> Self where F: Fn(&mut Cursive) + 'static { - self.callbacks.insert(event.into(), Rc::new(cb)); + self.callbacks.insert(event.into(), Callback::from_fn(cb)); self } diff --git a/src/view/menu_popup.rs b/src/view/menu_popup.rs index 67f2da0..5e8ab94 100644 --- a/src/view/menu_popup.rs +++ b/src/view/menu_popup.rs @@ -101,7 +101,7 @@ impl MenuPopup { /// /// (When the user hits ) pub fn on_dismiss(mut self, f: F) -> Self { - self.on_dismiss = Some(Rc::new(f)); + self.on_dismiss = Some(Callback::from_fn(f)); self } @@ -111,7 +111,7 @@ impl MenuPopup { /// /// Usually used to hide the parent view. pub fn on_action(mut self, f: F) -> Self { - self.on_action = Some(Rc::new(f)); + self.on_action = Some(Callback::from_fn(f)); self } diff --git a/src/view/mod.rs b/src/view/mod.rs index bfd9448..c4b0f79 100644 --- a/src/view/mod.rs +++ b/src/view/mod.rs @@ -58,6 +58,7 @@ mod linear_layout; mod list_view; mod menubar; mod menu_popup; +mod progress_bar; mod shadow_view; mod select_view; mod sized_view; @@ -78,7 +79,7 @@ pub use self::position::{Offset, Position}; pub use self::scroll::ScrollBase; -pub use self::id_view::IdView; +pub use self::id_view::{IdView, Identifiable}; pub use self::box_view::BoxView; pub use self::button::Button; pub use self::checkbox::Checkbox; @@ -91,12 +92,13 @@ pub use self::list_view::ListView; pub use self::menubar::Menubar; pub use self::menu_popup::MenuPopup; pub use self::view_path::ViewPath; +pub use self::progress_bar::ProgressBar; pub use self::select_view::SelectView; pub use self::shadow_view::ShadowView; +pub use self::sized_view::SizedView; pub use self::stack_view::StackView; pub use self::text_view::TextView; pub use self::tracked_view::TrackedView; -pub use self::sized_view::SizedView; pub use self::view_wrapper::ViewWrapper; diff --git a/src/view/progress_bar.rs b/src/view/progress_bar.rs new file mode 100644 index 0000000..ef49d28 --- /dev/null +++ b/src/view/progress_bar.rs @@ -0,0 +1,107 @@ +use std::sync::{Arc, Mutex}; + +use std::sync::atomic::{AtomicUsize, Ordering}; + +use {Cursive, Printer}; +use event::*; +use theme::ColorStyle; +use view::View; + +/// Display progress. +pub struct ProgressBar { + min: usize, + max: usize, + value: Arc, + // TODO: use a Promise instead? + callback: Option>>>>, +} + +impl ProgressBar { + /// Creates a new progress bar. + /// + /// Default values: + /// + /// * `min`: 0 + /// * `max`: 100 + /// * `value`: 0 + pub fn new() -> Self { + ProgressBar { + min: 0, + max: 100, + value: Arc::new(AtomicUsize::new(0)), + callback: None, + } + } + + /// Sets the value to follow. + pub fn with_value(mut self, value: Arc) -> Self { + self.value = value; + self + } + + /// Sets the callback to follow. + /// + /// Whenever `callback` is set, it will be called on the next event loop. + pub fn with_callback(mut self, + callback: Arc>>>) + -> Self { + self.callback = Some(callback); + self + } + + /// Sets the minimum value. + /// + /// When `value` equals `min`, the bar is at the minimum level. + pub fn min(mut self, min: usize) -> Self { + self.min = min; + self + } + + /// Sets the maximum value. + /// + /// When `value` equals `max`, the bar is at the maximum level. + pub fn max(mut self, max: usize) -> Self { + self.max = max; + self + } + + /// Sets the `min` and `max` range for the value. + pub fn range(self, min: usize, max: usize) -> Self { + self.min(min).max(max) + } + + /// Sets the current value. + /// + /// Value is clamped between `min` and `max`. + pub fn set_value(&mut self, value: usize) { + self.value.store(value, Ordering::Relaxed); + } +} + +impl View for ProgressBar { + fn draw(&self, printer: &Printer) { + // TODO: make the brackets an option + // (Or a theme property? Or both?) + printer.print((0, 0), "["); + printer.print((printer.size.x - 1, 0), "]"); + + // Now, the bar itself... + let available = printer.size.x - 2; + + let value = self.value.load(Ordering::Relaxed); + let length = (available * (value - self.min)) / (self.max - self.min); + printer.with_color(ColorStyle::Highlight, |printer| { + printer.print_hline((1, 0), length, " "); + }); + } + + fn on_event(&mut self, _: Event) -> EventResult { + if let Some(ref cb) = self.callback { + if let Some(cb) = cb.lock().unwrap().take() { + return EventResult::Consumed(Some(cb.into())); + } + } + + EventResult::Ignored + } +} diff --git a/src/view/select_view.rs b/src/view/select_view.rs index d6b53ba..1cbb8b0 100644 --- a/src/view/select_view.rs +++ b/src/view/select_view.rs @@ -11,7 +11,7 @@ use view::position::Position; use view::{IdView, View}; use align::{Align, HAlign, VAlign}; use view::scroll::ScrollBase; -use event::{Event, EventResult, Key}; +use event::{Callback, Event, EventResult, Key}; use theme::ColorStyle; use vec::Vec2; use Printer; @@ -387,7 +387,7 @@ impl View for SelectView { let cb = self.select_cb.as_ref().unwrap().clone(); let v = self.selection(); // We return a Callback Rc<|s| cb(s, &*v)> - return EventResult::Consumed(Some(Rc::new(move |s| { + return EventResult::Consumed(Some(Callback::from_fn(move |s| { cb(s, &*v) }))); }