Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add new example: stopwatch #503

Merged
merged 35 commits into from
Oct 9, 2020
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
4121212
new example: autocomplete search box
TianyiShi2001 Sep 20, 2020
6578e31
add new example: stopwatch
TianyiShi2001 Sep 21, 2020
586e614
better match function
TianyiShi2001 Sep 22, 2020
5bf442d
fixed size and scrollable
TianyiShi2001 Sep 22, 2020
1a85b73
submit without moving into the `matches` view
TianyiShi2001 Sep 22, 2020
1bd2a64
remove unsed imports
TianyiShi2001 Sep 22, 2020
f1f6bab
isolate the search function
TianyiShi2001 Sep 22, 2020
71d0669
no need to own
TianyiShi2001 Sep 22, 2020
258daca
remove the fixed width
TianyiShi2001 Sep 22, 2020
96a0fbb
working timer
TianyiShi2001 Sep 22, 2020
2fd22a6
this won't work
TianyiShi2001 Sep 22, 2020
bad48c4
laps
TianyiShi2001 Sep 22, 2020
8fd04b6
improve precision
TianyiShi2001 Sep 22, 2020
8262a7f
on_stop is working
TianyiShi2001 Sep 22, 2020
9cff334
fix https://github.com/gyscos/cursive/pull/503#discussion_r492864443
TianyiShi2001 Sep 22, 2020
1521803
show laps optionally
TianyiShi2001 Sep 22, 2020
00c2166
Merge remote-tracking branch 'upstream/main' into stopwatch
TianyiShi2001 Sep 22, 2020
5888427
refactor
TianyiShi2001 Sep 22, 2020
f8f3a8f
more docs
TianyiShi2001 Sep 22, 2020
cc13465
return full data when stop
TianyiShi2001 Sep 23, 2020
03b4f23
reduce funtionality
TianyiShi2001 Sep 23, 2020
3c54ac5
more comments
TianyiShi2001 Sep 23, 2020
2f90337
remove cloning
TianyiShi2001 Sep 23, 2020
8fc4faf
however, this won't work
TianyiShi2001 Sep 23, 2020
ad14207
fixed
TianyiShi2001 Sep 23, 2020
761d738
.
TianyiShi2001 Sep 23, 2020
f954e87
add info of my crate
TianyiShi2001 Sep 23, 2020
da2731e
minimize
TianyiShi2001 Sep 29, 2020
f7ce122
simplify
TianyiShi2001 Oct 9, 2020
8875418
add new example: stopwatch
TianyiShi2001 Sep 21, 2020
4b86afc
Improved the `select` example. (#501)
LunarEclipse363 Sep 21, 2020
7a3d906
Merge branch 'stopwatch' of https://github.com/TianyiShi2001/cursive …
TianyiShi2001 Oct 9, 2020
4d90e36
remove dependency on chrono
TianyiShi2001 Oct 9, 2020
7712beb
Merge branch 'main' of https://github.com/gyscos/cursive into main
TianyiShi2001 Oct 9, 2020
2ff936a
Merge branch 'main' into stopwatch
TianyiShi2001 Oct 9, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pretty-bytes = "0.2.2"
rand = "0.7.3"
cursive = { path = "../cursive", default-features=false }
crossbeam-channel = "0.4.2"
chrono = "0.4.15"
lazy_static = "1.4"

[features]
Expand Down
198 changes: 198 additions & 0 deletions examples/src/bin/stopwatch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
use chrono::Duration;
use cursive::{traits::*, views::Dialog, Cursive};
use stopwatch::{StopWatch, StopWatchView};

// A simple stopwatch without 'lap time' function is implemented in this example.
// Press "Space" to start/pause/resume the stopwatch. Press "Enter" to stop and
// get all data: moments at which the stopwatch is started/resumed, moments at which
// the stopwatch is paused/stopped; elapsed time.

fn main() {
let mut siv = cursive::default();
let stopwatch = StopWatchView::new();
siv.add_layer(
stopwatch
// On stop, get all data and summarize them in an info box.
.on_stop(|s: &mut Cursive, stopwatch| {
s.add_layer(Dialog::info(summarize(stopwatch)))
}),
);
siv.add_layer(Dialog::info(
"Press 'Space' to start/pause/resume the stopwatch\nPress 'Enter' to stop",
));
// the stopwatch is redrawn 15 times per second
siv.set_fps(15);
siv.run();
}

fn summarize(stopwatch: &StopWatch) -> String {
let elapsed = stopwatch.elapsed;
let n = stopwatch.pause_moments.len();
let total_elapsed =
stopwatch.pause_moments[n - 1] - stopwatch.start_moments[0];
format!(
"Elapsed time: {}\nTotal elapsed: {}\nPaused {} times",
elapsed.pretty(),
total_elapsed.pretty(),
n - 1,
)
}

pub trait PrettyDuration {
fn pretty(&self) -> String;
}
impl PrettyDuration for Duration {
/// Pretty-prints a chrono::Duration in the form `HH:MM:SS.xxx`
/// A custom trait is used because `std::fmt::Diaplay` cannot be implemented
/// for a struct coming from another external crate, due to the orphan rule
fn pretty(&self) -> String {
let s = self.num_seconds();
let ms = self.num_milliseconds() - 1000 * s;
let (h, s) = (s / 3600, s % 3600);
let (m, s) = (s / 60, s % 60);
format!("{:02}:{:02}:{:02}.{:03}", h, m, s, ms)
}
}

mod stopwatch {
use super::PrettyDuration;
use chrono::{DateTime, Duration, Local};
use cursive::{
event::{Event, EventResult, Key},
view::View,
Cursive, Printer, Vec2, With,
};
use std::rc::Rc;

#[derive(Clone, Debug)]
pub struct StopWatch {
// These data might be useful to the user
pub elapsed: Duration, // total elapsed time
pub pause_moments: Vec<DateTime<Local>>, // moments at which the stopwatch is paused
pub start_moments: Vec<DateTime<Local>>, // moments at which the stopwatch resumes
paused: bool,
}

impl StopWatch {
/// Returns a stopwatch that is reset to zero
pub fn new() -> Self {
Self {
elapsed: Duration::zero(),
start_moments: Vec::new(),
pause_moments: Vec::new(),
paused: true, // stopped by default; start by explicitly calling `.resume()`
}
}

fn last_start(&self) -> DateTime<Local> {
self.start_moments[self.start_moments.len() - 1]
}
fn pause(&mut self) {
assert!(self.paused == false, "Already paused!");
let moment = Local::now();
self.pause_moments.push(moment);
self.elapsed = self.elapsed + (moment - self.last_start());
self.paused = true;
}
fn resume(&mut self) {
assert!(self.paused == true, "Already running!");
self.start_moments.push(Local::now());
self.paused = false;
}
fn pause_or_resume(&mut self) {
if self.paused {
self.resume();
} else {
self.pause();
}
}
/// Read the total time elapsed
fn read(&self) -> Duration {
if self.paused {
self.elapsed
} else {
self.elapsed + (Local::now() - self.last_start())
}
}
}

/// Separating the `StopWatch` 'core' and the `StopWatchView` improves reusability
/// and flexibility. The user may implement their own `View`s, i.e. layouts, based
/// on the same `StopWatch` logic.
pub struct StopWatchView {
stopwatch: StopWatch,
on_stop: Option<Rc<dyn Fn(&mut Cursive, &StopWatch)>>,
}

impl StopWatchView {
pub fn new() -> Self {
Self {
stopwatch: StopWatch::new(),
on_stop: None,
}
}

/// Sets a callback to be used when `<Enter>` is pressed.
///
/// The elapsed time will be given to the callback.
///
/// See also cursive::views::select_view::SelectView::set_on_submit
pub fn set_on_stop<F, R>(&mut self, cb: F)
where
F: 'static + Fn(&mut Cursive, &StopWatch) -> R,
{
self.on_stop = Some(Rc::new(move |s, t| {
cb(s, t);
}));
}

pub fn on_stop<F, R>(self, cb: F) -> Self
where
F: 'static + Fn(&mut Cursive, &StopWatch) -> R,
{
self.with(|s| s.set_on_stop(cb))
}

fn stop(&mut self) -> EventResult {
let stopwatch = &mut self.stopwatch;
if !stopwatch.paused {
stopwatch.pause();
}
let result = if self.on_stop.is_some() {
let cb = self.on_stop.clone().unwrap();
let stopwatch_data = self.stopwatch.clone(); // TODO: remove clone
Copy link
Contributor Author

@TianyiShi2001 TianyiShi2001 Sep 23, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost done... Except in line 163 where I haven't figured out how not to clone

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeaah... the issue is that the callback will be called later, possibly after removing this view. So we can't depend on any data in the view itself; the callback must be self-supported.
The solution is to either clone the data as you are doing (making it standalone), or wrap it in a Rc shared by both the view and the callback.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just came up with another approach: use mem::replace to replace self.stopwatch with a new, zeroed StopWatch, while getting the ownership of the 'freshly' produced data, which can then passed to the closure.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use std::mem::take which does this automatically for "default" types.

EventResult::with_cb(move |s| cb(s, &stopwatch_data))
} else {
EventResult::Consumed(None)
};
// reset the stopwatch data, but not other configurations related to the `View`
self.stopwatch = StopWatch::new();
// return result
result
}
}
impl View for StopWatchView {
fn draw(&self, printer: &Printer) {
printer.print((0, 0), &self.stopwatch.read().pretty());
}

fn required_size(&mut self, _constraint: Vec2) -> Vec2 {
Vec2::new(12, 1) // columns, rows (width, height)
}

fn on_event(&mut self, event: Event) -> EventResult {
match event {
// pause/resume the stopwatch when pressing "Space"
Event::Char(' ') => {
self.stopwatch.pause_or_resume();
}
// stop (reset) the stopwatch when pressing "Enter"
Event::Key(Key::Enter) => {
return self.stop();
}
_ => return EventResult::Ignored,
}
EventResult::Consumed(None)
}
}
}