Skip to main content

rusty_bubbles/
stopwatch.rs

1//! Cleanroom Rust port of upstream Go source file: `stopwatch/stopwatch.go`
2//! Upstream Target Tag / Version: `v2.1.0`
3//!
4//! <public-docs>
5//! # Stopwatch
6//!
7//! A simple stopwatch component.
8//! </public-docs>
9
10use crate::internal::duration::duration_string;
11use rusty_bubbletea::commands;
12use rusty_bubbletea::model::{Cmd, Msg};
13use std::fmt;
14use std::sync::atomic::{AtomicI64, Ordering};
15use std::time::Duration;
16
17static LAST_ID: AtomicI64 = AtomicI64::new(0);
18
19fn next_id() -> i32 {
20    (LAST_ID.fetch_add(1, Ordering::SeqCst)) as i32
21}
22
23/// Option is a configuration option in [`new`]. For example:
24///
25/// ```rust
26/// # use rusty_bubbles::stopwatch;
27/// # use std::time::Duration;
28/// let timer = stopwatch::new(vec![stopwatch::with_interval(Duration::from_secs(5))]);
29/// ```
30pub type Option = Box<dyn FnOnce(&mut Model)>;
31
32/// WithInterval is an option for setting the interval between ticks. Pass as
33/// an argument to [`new`].
34pub fn with_interval(interval: Duration) -> Option {
35    Box::new(move |m: &mut Model| {
36        m.interval = interval;
37    })
38}
39
40/// TickMsg is a message that is sent on every timer tick.
41#[derive(Debug, Clone)]
42pub struct TickMsg {
43    /// ID is the identifier of the stopwatch that sends the message. This
44    /// makes it possible to determine which stopwatch a tick belongs to when
45    /// there are multiple stopwatches running.
46    ///
47    /// Note, however, that a stopwatch will reject ticks from other
48    /// stopwatches, so it's safe to flow all TickMsgs through all stopwatches
49    /// and have them still behave appropriately.
50    pub id: i32,
51    tag: i32,
52}
53
54/// StartStopMsg is sent when the stopwatch should start or stop.
55#[derive(Debug, Clone)]
56pub struct StartStopMsg {
57    /// The ID of the stopwatch the message is intended for.
58    pub id: i32,
59    running: bool,
60}
61
62/// ResetMsg is sent when the stopwatch should reset.
63#[derive(Debug, Clone)]
64pub struct ResetMsg {
65    /// The ID of the stopwatch the message is intended for.
66    pub id: i32,
67}
68
69/// Model for the stopwatch component.
70#[derive(Clone)]
71pub struct Model {
72    d: Duration,
73    id: i32,
74    tag: i32,
75    running: bool,
76
77    /// How long to wait before every tick. Defaults to 1 second.
78    pub interval: Duration,
79}
80
81impl fmt::Debug for Model {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        f.debug_struct("stopwatch::Model")
84            .field("id", &self.id)
85            .field("running", &self.running)
86            .field("d", &self.d)
87            .finish()
88    }
89}
90
91/// New creates a new stopwatch with 1s interval.
92pub fn new(opts: Vec<Option>) -> Model {
93    let mut m = Model {
94        id: next_id(),
95        interval: Duration::from_secs(1),
96        d: Duration::ZERO,
97        tag: 0,
98        running: false,
99    };
100
101    for opt in opts {
102        opt(&mut m);
103    }
104    m
105}
106
107impl Model {
108    /// ID returns the unique ID of the model.
109    pub fn id(&self) -> i32 {
110        self.id
111    }
112
113    /// Init starts the stopwatch.
114    pub fn init(&mut self) -> Cmd {
115        self.start()
116    }
117
118    /// Start starts the stopwatch.
119    pub fn start(&mut self) -> Cmd {
120        let start_msg: Box<dyn Msg> = Box::new(StartStopMsg {
121            id: self.id,
122            running: true,
123        });
124        let tick_cmd = tick(self.id, self.tag, self.interval);
125        commands::sequence(vec![Some(Box::new(move || Some(start_msg))), tick_cmd])
126    }
127
128    /// Stop stops the stopwatch.
129    pub fn stop(&mut self) -> Cmd {
130        let id = self.id;
131        Some(Box::new(move || {
132            Some(Box::new(StartStopMsg { id, running: false }))
133        }))
134    }
135
136    /// Toggle stops the stopwatch if it is running and starts it if it is
137    /// stopped.
138    pub fn toggle(&mut self) -> Cmd {
139        if self.running {
140            return self.stop();
141        }
142        self.start()
143    }
144
145    /// Reset resets the stopwatch to 0.
146    pub fn reset(&mut self) -> Cmd {
147        let id = self.id;
148        Some(Box::new(move || Some(Box::new(ResetMsg { id }))))
149    }
150
151    /// Running returns true if the stopwatch is running or false if it is
152    /// stopped.
153    pub fn running(&self) -> bool {
154        self.running
155    }
156
157    /// Update handles the timer tick.
158    pub fn update(&mut self, msg: &dyn Msg) -> Cmd {
159        if let Some(m) = msg.as_any().downcast_ref::<StartStopMsg>() {
160            if m.id != self.id {
161                return None;
162            }
163            self.running = m.running;
164            return None;
165        }
166
167        if let Some(m) = msg.as_any().downcast_ref::<ResetMsg>() {
168            if m.id != self.id {
169                return None;
170            }
171            self.d = Duration::ZERO;
172            return None;
173        }
174
175        if let Some(m) = msg.as_any().downcast_ref::<TickMsg>() {
176            if !self.running || m.id != self.id {
177                return None;
178            }
179
180            // If a tag is set, and it's not the one we expect, reject the
181            // message. This prevents the stopwatch from receiving too many
182            // messages and thus ticking too fast.
183            if m.tag > 0 && m.tag != self.tag {
184                return None;
185            }
186
187            self.d += self.interval;
188            self.tag += 1;
189            return tick(self.id, self.tag, self.interval);
190        }
191
192        None
193    }
194
195    /// Elapsed returns the time elapsed.
196    pub fn elapsed(&self) -> Duration {
197        self.d
198    }
199
200    /// View of the timer component.
201    pub fn view(&self) -> String {
202        duration_string(self.d)
203    }
204}
205
206fn tick(id: i32, tag: i32, d: Duration) -> Cmd {
207    commands::tick(d, move |_| Some(Box::new(TickMsg { id, tag })))
208}