bubbletea_widgets/timer.rs
1//! Timer component for Bubble Tea applications.
2//!
3//! Package timer provides a simple timeout component for Bubble Tea applications.
4//! It closely matches the Go bubbles timer component API for 1-1 compatibility.
5//!
6//! # Basic Usage
7//!
8//! ```rust
9//! use bubbletea_widgets::timer::{new, new_with_interval};
10//! use std::time::Duration;
11//!
12//! // Create a timer with default 1 second interval
13//! let timer = new(Duration::from_secs(30));
14//!
15//! // Create a timer with custom interval
16//! let timer = new_with_interval(Duration::from_secs(60), Duration::from_millis(100));
17//! ```
18//!
19//! # bubbletea-rs Integration
20//!
21//! ```rust
22//! use bubbletea_rs::{Model as BubbleTeaModel, Msg, Cmd};
23//! use bubbletea_widgets::timer::{new, Model, TickMsg, StartStopMsg, TimeoutMsg};
24//! use std::time::Duration;
25//!
26//! struct MyApp {
27//! timer: Model,
28//! }
29//!
30//! impl BubbleTeaModel for MyApp {
31//! fn init() -> (Self, Option<Cmd>) {
32//! let timer = new(Duration::from_secs(10));
33//! let cmd = timer.init();
34//! (Self { timer }, Some(cmd))
35//! }
36//!
37//! fn update(&mut self, msg: Msg) -> Option<Cmd> {
38//! // Handle timeout
39//! if let Some(timeout) = msg.downcast_ref::<TimeoutMsg>() {
40//! if timeout.id == self.timer.id() {
41//! // Timer finished!
42//! }
43//! }
44//!
45//! // Forward timer messages
46//! self.timer.update(msg)
47//! }
48//!
49//! fn view(&self) -> String {
50//! format!("Time remaining: {}", self.timer.view())
51//! }
52//! }
53//! ```
54//!
55//! # Start/Stop Control
56//!
57//! ```rust
58//! use bubbletea_widgets::timer::new;
59//! use std::time::Duration;
60//!
61//! let timer = new(Duration::from_secs(30));
62//!
63//! // These return commands that send StartStopMsg
64//! let start_cmd = timer.start(); // Resume timer
65//! let stop_cmd = timer.stop(); // Pause timer
66//! let toggle_cmd = timer.toggle(); // Toggle running state
67//! ```
68
69use bubbletea_rs::{tick as bubbletea_tick, Cmd, Model as BubbleTeaModel, Msg};
70use std::sync::atomic::{AtomicI64, Ordering};
71use std::time::{Duration, Instant};
72
73// Internal ID management for timer instances
74static LAST_ID: AtomicI64 = AtomicI64::new(0);
75
76/// Generates unique identifiers for timer instances.
77///
78/// This function ensures that each timer created gets a unique ID, allowing
79/// multiple timers to coexist in the same application without message conflicts.
80/// The IDs are generated atomically and start from 1.
81///
82/// # Returns
83///
84/// A unique `i64` identifier for a timer instance
85///
86/// # Thread Safety
87///
88/// This function is thread-safe and can be called from multiple threads
89/// concurrently without risk of duplicate IDs.
90fn next_id() -> i64 {
91 LAST_ID.fetch_add(1, Ordering::SeqCst) + 1
92}
93
94/// Formats a duration using Go's Duration.String() format for compatibility.
95///
96/// This function converts a Rust `Duration` into a string representation that
97/// matches Go's duration formatting exactly, ensuring consistent display across
98/// different language implementations of the bubbles library.
99///
100/// # Arguments
101///
102/// * `d` - The duration to format
103///
104/// # Returns
105///
106/// A formatted string representation of the duration
107///
108/// # Format Examples
109///
110/// - `0s` for zero duration
111/// - `500ms` for milliseconds
112/// - `1.5s` for seconds with decimals
113/// - `2m30s` for minutes and seconds
114/// - `1m` for exact minutes
115///
116/// # Examples
117///
118/// ```rust,ignore
119/// use std::time::Duration;
120///
121/// assert_eq!(format_duration(Duration::from_secs(0)), "0s");
122/// assert_eq!(format_duration(Duration::from_millis(500)), "500ms");
123/// assert_eq!(format_duration(Duration::from_secs(90)), "1m30s");
124/// ```
125fn format_duration(d: Duration) -> String {
126 let total_nanos = d.as_nanos();
127
128 if total_nanos == 0 {
129 return "0s".to_string();
130 }
131
132 // Convert to go-like format
133 if total_nanos >= 1_000_000_000 {
134 // Seconds or more
135 let secs = d.as_secs_f64();
136 if secs >= 60.0 {
137 let minutes = (secs / 60.0) as u64;
138 let remaining_secs = secs % 60.0;
139 if remaining_secs == 0.0 {
140 format!("{}m", minutes)
141 } else {
142 format!("{}m{:.0}s", minutes, remaining_secs)
143 }
144 } else if secs >= 1.0 {
145 if secs == secs.floor() {
146 format!("{:.0}s", secs)
147 } else {
148 format!("{:.1}s", secs)
149 }
150 } else {
151 format!("{:.3}s", secs)
152 }
153 } else if total_nanos >= 1_000_000 {
154 // Milliseconds
155 format!("{}ms", d.as_millis())
156 } else if total_nanos >= 1_000 {
157 // Microseconds
158 format!("{}µs", d.as_micros())
159 } else {
160 // Nanoseconds
161 format!("{}ns", total_nanos)
162 }
163}
164
165/// Message used to start and stop timer instances.
166///
167/// This message is sent by the timer's control methods (`start()`, `stop()`, `toggle()`)
168/// to change the running state of a specific timer. The message includes the timer's
169/// unique ID to ensure it only affects the intended timer instance.
170///
171/// # Examples
172///
173/// ```rust
174/// use bubbletea_widgets::timer::new;
175/// use std::time::Duration;
176///
177/// let timer = new(Duration::from_secs(30));
178///
179/// // Use the public API to control the timer
180/// let start_cmd = timer.start(); // Creates StartStopMsg internally
181/// let stop_cmd = timer.stop(); // Creates StartStopMsg internally
182/// ```
183///
184/// # Note
185///
186/// The `running` field is intentionally private to ensure it can only be set
187/// through the timer's control methods, maintaining proper state management.
188#[derive(Debug, Clone)]
189pub struct StartStopMsg {
190 /// The unique identifier of the timer this message targets.
191 ///
192 /// Only timers with matching IDs will respond to this message,
193 /// allowing multiple timers to coexist safely.
194 pub id: i64,
195 /// Whether the timer should be running after processing this message.
196 ///
197 /// This field is private to ensure proper state management through
198 /// the timer's public control methods.
199 running: bool,
200}
201
202/// Message sent on every timer tick to update the countdown.
203///
204/// This message is generated automatically by the timer at regular intervals
205/// (determined by the timer's `interval` setting). Each tick reduces the remaining
206/// timeout duration and triggers the next tick command.
207///
208/// # Message Filtering
209///
210/// Timers automatically filter tick messages to ensure they only process their own:
211/// - Messages with mismatched IDs are ignored
212/// - Messages with incorrect tags are rejected (prevents double-ticking)
213/// - Messages sent to stopped timers are ignored
214///
215/// # Examples
216///
217/// ```rust
218/// use bubbletea_widgets::timer::{TickMsg, new};
219/// use std::time::Duration;
220///
221/// let timer = new(Duration::from_secs(30));
222/// let timer_id = timer.id();
223///
224/// // This message would be generated internally by the timer
225/// // Use the timer's tick() method instead of constructing manually:
226/// let tick_cmd = timer.init(); // Starts the timer and generates tick messages
227/// ```
228///
229/// # Timeout Detection
230///
231/// The `timeout` field indicates whether this tick represents the final
232/// expiration of the timer. You can either check this field or listen
233/// for separate `TimeoutMsg` messages.
234#[derive(Debug, Clone)]
235pub struct TickMsg {
236 /// The unique identifier of the timer that generated this tick.
237 ///
238 /// This allows multiple timers to run simultaneously without interfering
239 /// with each other. Each timer only processes ticks with its own ID.
240 pub id: i64,
241
242 /// Whether this tick represents a timeout (timer expiration).
243 ///
244 /// When `true`, this indicates the timer has reached zero and expired.
245 /// You can alternatively listen for `TimeoutMsg` for timeout notifications.
246 pub timeout: bool,
247
248 /// Internal synchronization tag to prevent message overflow.
249 ///
250 /// This field is used internally to ensure timers don't process too many
251 /// tick messages simultaneously, which could cause timing inaccuracies.
252 /// Application code should not modify this field.
253 tag: i64,
254}
255
256/// Message sent when a timer reaches zero and expires.
257///
258/// This is a convenience message that provides a clear notification when a timer
259/// completes its countdown. It's sent in addition to the final `TickMsg` (which
260/// will have `timeout: true`), giving applications two ways to detect timer expiration.
261///
262/// # Usage Pattern
263///
264/// Applications typically handle this message in their update loop to respond
265/// to timer completion events, such as showing notifications, triggering actions,
266/// or starting new timers.
267///
268/// # Examples
269///
270/// ```rust
271/// use bubbletea_widgets::timer::{TimeoutMsg, new};
272/// use bubbletea_rs::{Model as BubbleTeaModel, Msg};
273/// use std::time::Duration;
274///
275/// struct App {
276/// timer: bubbletea_widgets::timer::Model,
277/// message: String,
278/// }
279///
280/// impl BubbleTeaModel for App {
281/// fn update(&mut self, msg: Msg) -> Option<bubbletea_rs::Cmd> {
282/// // Handle timer timeout
283/// if let Some(timeout) = msg.downcast_ref::<TimeoutMsg>() {
284/// if timeout.id == self.timer.id() {
285/// self.message = "Timer expired!".to_string();
286/// }
287/// }
288///
289/// self.timer.update(msg)
290/// }
291///
292/// // ... other methods
293/// # fn init() -> (Self, Option<bubbletea_rs::Cmd>) { unimplemented!() }
294/// # fn view(&self) -> String { unimplemented!() }
295/// }
296/// ```
297///
298/// # Relationship to TickMsg
299///
300/// While `TickMsg` with `timeout: true` and `TimeoutMsg` both indicate timer
301/// expiration, `TimeoutMsg` provides a cleaner, more semantic way to handle
302/// completion events in your application logic.
303#[derive(Debug, Clone)]
304pub struct TimeoutMsg {
305 /// The unique identifier of the timer that expired.
306 ///
307 /// Use this to identify which timer expired when multiple timers
308 /// are running in the same application.
309 pub id: i64,
310}
311
312/// High-precision countdown timer component for Bubble Tea applications.
313///
314/// This struct represents a timer that counts down from an initial timeout value
315/// at regular intervals. It provides fine-grained control over timing behavior
316/// and integrates seamlessly with the Bubble Tea message-passing architecture.
317///
318/// # Core Features
319///
320/// - **Precise Timing**: Configurable tick intervals for smooth countdown display
321/// - **State Management**: Start, stop, and toggle operations with proper state tracking
322/// - **Message Filtering**: Automatic ID-based filtering prevents cross-timer interference
323/// - **Timeout Detection**: Multiple ways to detect and handle timer expiration
324/// - **Go Compatibility**: API matches Go's bubbles timer for easy migration
325///
326/// # Examples
327///
328/// Basic timer usage:
329/// ```rust
330/// use bubbletea_widgets::timer::{new, new_with_interval};
331/// use std::time::Duration;
332///
333/// // Create a 30-second timer with default 1-second ticks
334/// let timer = new(Duration::from_secs(30));
335/// assert_eq!(timer.timeout, Duration::from_secs(30));
336/// assert!(timer.running());
337///
338/// // Create a timer with custom tick rate
339/// let fast_timer = new_with_interval(
340/// Duration::from_secs(10),
341/// Duration::from_millis(100)
342/// );
343/// assert_eq!(fast_timer.interval, Duration::from_millis(100));
344/// ```
345///
346/// Integration with Bubble Tea:
347/// ```rust
348/// use bubbletea_widgets::timer::{new, Model as TimerModel, TimeoutMsg};
349/// use bubbletea_rs::{Model as BubbleTeaModel, Cmd, Msg};
350/// use std::time::Duration;
351///
352/// struct App {
353/// timer: TimerModel,
354/// status: String,
355/// }
356///
357/// impl BubbleTeaModel for App {
358/// fn init() -> (Self, Option<Cmd>) {
359/// let timer = new(Duration::from_secs(5));
360/// let cmd = timer.init();
361/// (App {
362/// timer,
363/// status: "Timer running...".to_string(),
364/// }, Some(cmd))
365/// }
366///
367/// fn update(&mut self, msg: Msg) -> Option<Cmd> {
368/// // Handle timeout events
369/// if let Some(timeout) = msg.downcast_ref::<TimeoutMsg>() {
370/// if timeout.id == self.timer.id() {
371/// self.status = "Time's up!".to_string();
372/// return None;
373/// }
374/// }
375///
376/// // Forward other messages to timer
377/// self.timer.update(msg)
378/// }
379///
380/// fn view(&self) -> String {
381/// format!("{} - Remaining: {}", self.status, self.timer.view())
382/// }
383/// }
384/// ```
385///
386/// # State Management
387///
388/// The timer maintains several important states:
389/// - **Running**: Whether the timer is actively counting down
390/// - **Timeout**: The remaining time until expiration
391/// - **ID**: Unique identifier for message filtering
392/// - **Tag**: Internal synchronization for accurate timing
393///
394/// # Thread Safety
395///
396/// The Model struct is `Clone` and can be safely passed between threads.
397/// Each timer instance maintains its own unique ID to prevent conflicts.
398///
399/// # Performance Considerations
400///
401/// - Faster intervals (< 100ms) provide smoother display but use more CPU
402/// - Multiple timers can run simultaneously without significant overhead
403/// - Message filtering ensures efficient processing in complex applications
404#[derive(Debug, Clone)]
405pub struct Model {
406 /// The remaining time until the timer expires.
407 ///
408 /// This value decreases by `interval` on each tick. When it reaches zero
409 /// or below, the timer is considered expired and will stop automatically.
410 pub timeout: Duration,
411
412 /// The time between each timer tick.
413 ///
414 /// This controls how frequently the timer updates its display and sends
415 /// tick messages. Smaller intervals provide smoother countdown display
416 /// but consume more resources. Default is 1 second.
417 pub interval: Duration,
418
419 /// Unique identifier for this timer instance.
420 ///
421 /// Used to filter messages and ensure timers only respond to their own
422 /// tick and control messages. Generated automatically on creation.
423 id: i64,
424 /// Internal synchronization tag for accurate timing.
425 ///
426 /// Used to prevent the timer from processing too many tick messages
427 /// simultaneously, which could cause timing drift or inaccuracies.
428 tag: i64,
429 /// Whether the timer is currently counting down.
430 ///
431 /// When `false`, the timer ignores tick messages and remains paused.
432 /// Can be controlled via `start()`, `stop()`, and `toggle()` methods.
433 running: bool,
434 /// The time when this timer was started.
435 ///
436 /// Used for accurate timing calculations. Set when the timer first
437 /// starts running and updated when resumed after pausing.
438 start_instant: Option<Instant>,
439 /// The time when the last tick was processed.
440 ///
441 /// Used to calculate actual elapsed time between ticks, providing
442 /// more accurate countdown timing than interval-based calculations.
443 last_tick: Option<Instant>,
444}
445
446/// Creates a new timer with custom timeout and tick interval.
447///
448/// This function provides full control over timer behavior by allowing you to specify
449/// both the initial countdown duration and how frequently the timer updates. Use this
450/// when you need precise control over timing granularity or want smoother display updates.
451///
452/// # Arguments
453///
454/// * `timeout` - The initial countdown duration (how long until the timer expires)
455/// * `interval` - How frequently the timer ticks and updates its display
456///
457/// # Returns
458///
459/// A new `Model` instance configured with the specified timing parameters
460///
461/// # Examples
462///
463/// ```rust
464/// use bubbletea_widgets::timer::new_with_interval;
465/// use std::time::Duration;
466///
467/// // Create a 30-second timer that updates every 100ms (smooth display)
468/// let smooth_timer = new_with_interval(
469/// Duration::from_secs(30),
470/// Duration::from_millis(100)
471/// );
472/// assert_eq!(smooth_timer.timeout, Duration::from_secs(30));
473/// assert_eq!(smooth_timer.interval, Duration::from_millis(100));
474/// assert!(smooth_timer.running());
475/// ```
476///
477/// Different use cases:
478/// ```rust
479/// use bubbletea_widgets::timer::new_with_interval;
480/// use std::time::Duration;
481///
482/// // High-precision timer for animations (60 FPS)
483/// let animation_timer = new_with_interval(
484/// Duration::from_secs(5),
485/// Duration::from_millis(16) // ~60 FPS
486/// );
487///
488/// // Battery-friendly timer for long countdowns
489/// let efficient_timer = new_with_interval(
490/// Duration::from_secs(3600), // 1 hour
491/// Duration::from_secs(5) // Update every 5 seconds
492/// );
493///
494/// // Precise scientific timer
495/// let precise_timer = new_with_interval(
496/// Duration::from_millis(500),
497/// Duration::from_millis(10)
498/// );
499/// ```
500///
501/// # Performance Considerations
502///
503/// - **Smaller intervals** provide smoother display but use more CPU and battery
504/// - **Larger intervals** are more efficient but may appear jerky
505/// - Consider your application's needs when choosing interval duration
506/// - For display-only timers, 100ms-1000ms intervals work well
507/// - For animations, 16ms (60 FPS) provides smooth motion
508///
509/// # Timing Accuracy
510///
511/// The timer's accuracy depends on the underlying system's timer resolution
512/// and the Bubble Tea framework's message processing speed. Very small intervals
513/// (< 10ms) may not be achievable on all systems.
514///
515/// # Note
516///
517/// This function matches Go's `NewWithInterval` function exactly for compatibility
518/// with existing bubbles applications.
519pub fn new_with_interval(timeout: Duration, interval: Duration) -> Model {
520 Model {
521 timeout,
522 interval,
523 running: true,
524 id: next_id(),
525 tag: 0,
526 start_instant: None,
527 last_tick: None,
528 }
529}
530
531/// Creates a new timer with the specified timeout and default 1-second interval.
532///
533/// This is the most common way to create a timer. It uses a sensible default of
534/// 1-second intervals, which provides a good balance between display smoothness
535/// and resource usage for most applications.
536///
537/// # Arguments
538///
539/// * `timeout` - The initial countdown duration (how long until the timer expires)
540///
541/// # Returns
542///
543/// A new `Model` instance with 1-second tick intervals
544///
545/// # Examples
546///
547/// ```rust
548/// use bubbletea_widgets::timer::new;
549/// use std::time::Duration;
550///
551/// // Create a 30-second countdown timer
552/// let timer = new(Duration::from_secs(30));
553/// assert_eq!(timer.timeout, Duration::from_secs(30));
554/// assert_eq!(timer.interval, Duration::from_secs(1)); // Default interval
555/// assert!(timer.running());
556/// assert!(!timer.timedout());
557/// ```
558///
559/// Common timer durations:
560/// ```rust
561/// use bubbletea_widgets::timer::new;
562/// use std::time::Duration;
563///
564/// // Short timer for notifications
565/// let notification = new(Duration::from_secs(5));
566///
567/// // Medium timer for breaks
568/// let break_timer = new(Duration::from_secs(300)); // 5 minutes
569///
570/// // Long timer for cooking
571/// let cooking_timer = new(Duration::from_secs(1800)); // 30 minutes
572///
573/// // Sub-second timer for quick actions
574/// let quick_timer = new(Duration::from_millis(750));
575/// ```
576///
577/// Integration with Bubble Tea:
578/// ```rust
579/// use bubbletea_widgets::timer::new;
580/// use bubbletea_rs::{Model as BubbleTeaModel, Cmd};
581/// use std::time::Duration;
582///
583/// struct App {
584/// timer: bubbletea_widgets::timer::Model,
585/// }
586///
587/// impl BubbleTeaModel for App {
588/// fn init() -> (Self, Option<Cmd>) {
589/// let timer = new(Duration::from_secs(60));
590/// let init_cmd = timer.init(); // Start the timer
591/// (App { timer }, Some(init_cmd))
592/// }
593///
594/// // ... other methods
595/// # fn update(&mut self, _: bubbletea_rs::Msg) -> Option<Cmd> { None }
596/// # fn view(&self) -> String { self.timer.view() }
597/// }
598/// ```
599///
600/// # Default Configuration
601///
602/// - **Interval**: 1 second (good balance of smoothness and efficiency)
603/// - **State**: Running (timer starts immediately)
604/// - **ID**: Unique identifier generated automatically
605/// - **Display**: Shows remaining time in Go's duration format
606///
607/// # When to Use
608///
609/// Use this function when:
610/// - You want standard 1-second timer updates
611/// - Resource efficiency is important
612/// - You don't need sub-second display precision
613/// - Building typical countdown or timeout functionality
614///
615/// Use `new_with_interval()` instead when you need:
616/// - Smoother display updates (< 1 second intervals)
617/// - High-precision timing
618/// - Custom update frequencies
619///
620/// # Note
621///
622/// This function matches Go's `New` function exactly for compatibility
623/// with existing bubbles applications.
624pub fn new(timeout: Duration) -> Model {
625 new_with_interval(timeout, Duration::from_secs(1))
626}
627
628impl Model {
629 /// Returns the unique identifier of this timer instance.
630 ///
631 /// Each timer gets a unique ID when created, allowing multiple timers to coexist
632 /// in the same application without interfering with each other. This ID is used
633 /// internally for message filtering and can be used by applications to identify
634 /// which timer generated specific messages.
635 ///
636 /// # Returns
637 ///
638 /// The unique `i64` identifier for this timer
639 ///
640 /// # Examples
641 ///
642 /// ```rust
643 /// use bubbletea_widgets::timer::new;
644 /// use std::time::Duration;
645 ///
646 /// let timer1 = new(Duration::from_secs(30));
647 /// let timer2 = new(Duration::from_secs(60));
648 ///
649 /// // Each timer has a unique ID
650 /// assert_ne!(timer1.id(), timer2.id());
651 /// assert!(timer1.id() > 0);
652 /// assert!(timer2.id() > 0);
653 /// ```
654 ///
655 /// Using ID to identify timer messages:
656 /// ```rust
657 /// use bubbletea_widgets::timer::{new, TimeoutMsg};
658 /// use bubbletea_rs::{Model as BubbleTeaModel, Msg};
659 /// use std::time::Duration;
660 ///
661 /// struct App {
662 /// work_timer: bubbletea_widgets::timer::Model,
663 /// break_timer: bubbletea_widgets::timer::Model,
664 /// }
665 ///
666 /// impl BubbleTeaModel for App {
667 /// fn update(&mut self, msg: Msg) -> Option<bubbletea_rs::Cmd> {
668 /// if let Some(timeout) = msg.downcast_ref::<TimeoutMsg>() {
669 /// if timeout.id == self.work_timer.id() {
670 /// // Work period finished
671 /// } else if timeout.id == self.break_timer.id() {
672 /// // Break period finished
673 /// }
674 /// }
675 /// None
676 /// }
677 ///
678 /// // ... other methods
679 /// # fn init() -> (Self, Option<bubbletea_rs::Cmd>) { unimplemented!() }
680 /// # fn view(&self) -> String { unimplemented!() }
681 /// }
682 /// ```
683 ///
684 /// # Thread Safety
685 ///
686 /// The ID is assigned atomically during timer creation and remains constant
687 /// throughout the timer's lifetime, making it safe to use across threads.
688 ///
689 /// # Note
690 ///
691 /// This method matches Go's `ID()` method exactly for compatibility.
692 pub fn id(&self) -> i64 {
693 self.id
694 }
695
696 /// Returns whether the timer is currently counting down.
697 ///
698 /// A timer is considered running when it's actively counting down and has not
699 /// expired. This method returns `false` if the timer has been manually stopped
700 /// or if it has reached zero and timed out.
701 ///
702 /// # Returns
703 ///
704 /// `true` if the timer is actively counting down, `false` otherwise
705 ///
706 /// # Examples
707 ///
708 /// ```rust
709 /// use bubbletea_widgets::timer::new;
710 /// use std::time::Duration;
711 ///
712 /// let mut timer = new(Duration::from_secs(30));
713 ///
714 /// // Timer starts in running state
715 /// assert!(timer.running());
716 ///
717 /// // Manually stopping the timer
718 /// let _stop_cmd = timer.stop();
719 /// // Note: timer.running() would still return true until the stop message is processed
720 /// ```
721 ///
722 /// Checking timer state in different scenarios:
723 /// ```rust
724 /// use bubbletea_widgets::timer::new;
725 /// use std::time::Duration;
726 ///
727 /// let mut timer = new(Duration::from_secs(5));
728 /// assert!(timer.running()); // Initially running
729 ///
730 /// // Simulate timer expiration
731 /// timer.timeout = Duration::ZERO;
732 /// assert!(!timer.running()); // Not running when expired
733 ///
734 /// // Reset timeout but manually stop the timer
735 /// timer.timeout = Duration::from_secs(10);
736 /// // Use stop() method instead of accessing private field
737 /// let _stop_cmd = timer.stop();
738 /// // Note: timer.running() may still return true until stop message is processed
739 /// ```
740 ///
741 /// Integration with control commands:
742 /// ```rust
743 /// use bubbletea_widgets::timer::new;
744 /// use std::time::Duration;
745 ///
746 /// let timer = new(Duration::from_secs(60));
747 ///
748 /// // These commands change the running state
749 /// let start_cmd = timer.start(); // Will make running() return true
750 /// let stop_cmd = timer.stop(); // Will make running() return false
751 /// let toggle_cmd = timer.toggle(); // Will flip the running state
752 /// ```
753 ///
754 /// # State Priority
755 ///
756 /// The running state is determined by multiple factors in this priority order:
757 /// 1. **Timeout**: If the timer has expired (`timedout() == true`), it's not running
758 /// 2. **Manual State**: If manually stopped via `stop()`, it's not running
759 /// 3. **Default**: Otherwise, it follows the internal running flag
760 ///
761 /// # Note
762 ///
763 /// This method matches Go's `Running()` method exactly for compatibility.
764 pub fn running(&self) -> bool {
765 if self.timedout() || !self.running {
766 return false;
767 }
768 true
769 }
770
771 /// Returns whether the timer has reached zero and expired.
772 ///
773 /// A timer is considered timed out when its remaining timeout duration has
774 /// reached zero or below. Once timed out, the timer automatically stops
775 /// running and will not process further tick messages.
776 ///
777 /// # Returns
778 ///
779 /// `true` if the timer has expired, `false` if time remains
780 ///
781 /// # Examples
782 ///
783 /// ```rust
784 /// use bubbletea_widgets::timer::new;
785 /// use std::time::Duration;
786 ///
787 /// let mut timer = new(Duration::from_secs(30));
788 ///
789 /// // Timer starts with time remaining
790 /// assert!(!timer.timedout());
791 ///
792 /// // Simulate timer expiration
793 /// timer.timeout = Duration::ZERO;
794 /// assert!(timer.timedout());
795 /// ```
796 ///
797 /// Checking expiration in different states:
798 /// ```rust
799 /// use bubbletea_widgets::timer::new;
800 /// use std::time::Duration;
801 ///
802 /// let mut timer = new(Duration::from_millis(100));
803 ///
804 /// // With remaining time
805 /// assert!(!timer.timedout());
806 ///
807 /// // Exactly at zero
808 /// timer.timeout = Duration::ZERO;
809 /// assert!(timer.timedout());
810 ///
811 /// // Very small remaining time
812 /// timer.timeout = Duration::from_nanos(1);
813 /// assert!(!timer.timedout());
814 /// ```
815 ///
816 /// Using in timeout detection:
817 /// ```rust
818 /// use bubbletea_widgets::timer::new;
819 /// use std::time::Duration;
820 ///
821 /// let mut timer = new(Duration::from_secs(5));
822 ///
823 /// // Application loop simulation
824 /// while !timer.timedout() {
825 /// // Process timer ticks
826 /// // timer.update(tick_msg) would reduce timeout
827 ///
828 /// // Break for this example to avoid infinite loop
829 /// timer.timeout = Duration::ZERO;
830 /// }
831 ///
832 /// assert!(timer.timedout());
833 /// assert!(!timer.running()); // Timed out timers are not running
834 /// ```
835 ///
836 /// # Relationship to Running State
837 ///
838 /// When a timer times out:
839 /// - `timedout()` returns `true`
840 /// - `running()` returns `false` (expired timers don't run)
841 /// - The timer stops processing tick messages automatically
842 /// - A `TimeoutMsg` is typically sent to notify the application
843 ///
844 /// # Precision Note
845 ///
846 /// The timeout check uses `Duration::ZERO` as the threshold. Due to the
847 /// discrete nature of tick intervals, the actual remaining time when
848 /// expiration is detected may be slightly negative (saturated to zero).
849 ///
850 /// # Note
851 ///
852 /// This method matches Go's `Timedout()` method exactly for compatibility.
853 pub fn timedout(&self) -> bool {
854 self.timeout <= Duration::ZERO
855 }
856
857 /// Generates a command to start or resume the timer.
858 ///
859 /// This method returns a command that, when executed by the Bubble Tea runtime,
860 /// will send a `StartStopMsg` to resume the timer's countdown. If the timer is
861 /// already running, this has no effect. If the timer has timed out, the command
862 /// has no effect.
863 ///
864 /// # Returns
865 ///
866 /// A `Cmd` that will start the timer when executed
867 ///
868 /// # Examples
869 ///
870 /// ```rust
871 /// use bubbletea_widgets::timer::new;
872 /// use bubbletea_rs::{Model as BubbleTeaModel, Cmd};
873 /// use std::time::Duration;
874 ///
875 /// struct App {
876 /// timer: bubbletea_widgets::timer::Model,
877 /// }
878 ///
879 /// impl BubbleTeaModel for App {
880 /// fn init() -> (Self, Option<Cmd>) {
881 /// let timer = new(Duration::from_secs(60));
882 /// // Start the timer immediately
883 /// let start_cmd = timer.start();
884 /// (App { timer }, Some(start_cmd))
885 /// }
886 ///
887 /// fn update(&mut self, msg: bubbletea_rs::Msg) -> Option<Cmd> {
888 /// // Handle user input to start timer
889 /// // if space_key_pressed {
890 /// // return Some(self.timer.start());
891 /// // }
892 ///
893 /// self.timer.update(msg)
894 /// }
895 ///
896 /// // ... other methods
897 /// # fn view(&self) -> String { self.timer.view() }
898 /// }
899 /// ```
900 ///
901 /// Manual timer control:
902 /// ```rust
903 /// use bubbletea_widgets::timer::new;
904 /// use std::time::Duration;
905 ///
906 /// let timer = new(Duration::from_secs(30));
907 ///
908 /// // Generate control commands
909 /// let start_cmd = timer.start(); // Resume countdown
910 /// let stop_cmd = timer.stop(); // Pause countdown
911 /// let toggle_cmd = timer.toggle(); // Toggle running state
912 ///
913 /// // Commands are executed by the Bubble Tea runtime
914 /// // The actual state change happens when the StartStopMsg is processed
915 /// ```
916 ///
917 /// # Command Execution
918 ///
919 /// The returned command is not executed immediately. Instead, it's returned
920 /// to the Bubble Tea runtime, which will execute it asynchronously. The actual
921 /// state change occurs when the resulting `StartStopMsg` is processed by the
922 /// timer's `update()` method.
923 ///
924 /// # State Change Sequence
925 ///
926 /// 1. `start()` is called → returns `Cmd`
927 /// 2. Bubble Tea executes the command → generates `StartStopMsg`
928 /// 3. Message is sent to `update()` → timer state changes to running
929 /// 4. Timer begins processing tick messages and counting down
930 ///
931 /// # No Effect Scenarios
932 ///
933 /// The start command has no effect when:
934 /// - The timer has already timed out (`timedout() == true`)
935 /// - The timer is already running (redundant operation)
936 ///
937 /// # Note
938 ///
939 /// This method matches Go's `Start()` method exactly for compatibility.
940 pub fn start(&self) -> Cmd {
941 self.start_stop(true)
942 }
943
944 /// Generates a command to stop or pause the timer.
945 ///
946 /// This method returns a command that, when executed by the Bubble Tea runtime,
947 /// will send a `StartStopMsg` to pause the timer's countdown. The timer retains
948 /// its current timeout value and can be resumed later with `start()`. If the
949 /// timer has already timed out, this command has no effect.
950 ///
951 /// # Returns
952 ///
953 /// A `Cmd` that will stop the timer when executed
954 ///
955 /// # Examples
956 ///
957 /// ```rust
958 /// use bubbletea_widgets::timer::new;
959 /// use bubbletea_rs::{Model as BubbleTeaModel, Cmd, KeyMsg};
960 /// use crossterm::event::{KeyCode, KeyModifiers};
961 /// use std::time::Duration;
962 ///
963 /// struct App {
964 /// timer: bubbletea_widgets::timer::Model,
965 /// paused: bool,
966 /// }
967 ///
968 /// impl BubbleTeaModel for App {
969 /// fn update(&mut self, msg: bubbletea_rs::Msg) -> Option<Cmd> {
970 /// // Handle spacebar to pause/resume
971 /// if let Some(key) = msg.downcast_ref::<KeyMsg>() {
972 /// if key.key == KeyCode::Char(' ') {
973 /// self.paused = !self.paused;
974 /// return if self.paused {
975 /// Some(self.timer.stop())
976 /// } else {
977 /// Some(self.timer.start())
978 /// };
979 /// }
980 /// }
981 ///
982 /// self.timer.update(msg)
983 /// }
984 ///
985 /// // ... other methods
986 /// # fn init() -> (Self, Option<Cmd>) { unimplemented!() }
987 /// # fn view(&self) -> String { format!("Timer: {} {}", self.timer.view(), if self.paused { "(PAUSED)" } else { "" }) }
988 /// }
989 /// ```
990 ///
991 /// Timer control pattern:
992 /// ```rust
993 /// use bubbletea_widgets::timer::new;
994 /// use std::time::Duration;
995 ///
996 /// let timer = new(Duration::from_secs(300)); // 5 minute timer
997 ///
998 /// // Control commands for different scenarios
999 /// let pause_cmd = timer.stop(); // Pause for a break
1000 /// let resume_cmd = timer.start(); // Resume after break
1001 /// let toggle_cmd = timer.toggle(); // Quick pause/resume
1002 /// ```
1003 ///
1004 /// # Pause vs. Reset
1005 ///
1006 /// Important distinction:
1007 /// - **Stop/Pause**: Halts countdown but preserves remaining time
1008 /// - **Reset**: Would require creating a new timer with original timeout
1009 ///
1010 /// ```rust
1011 /// use bubbletea_widgets::timer::new;
1012 /// use std::time::Duration;
1013 ///
1014 /// let mut timer = new(Duration::from_secs(60));
1015 ///
1016 /// // Simulate some time passing
1017 /// timer.timeout = Duration::from_secs(45); // 15 seconds elapsed
1018 ///
1019 /// // Stopping preserves the remaining 45 seconds
1020 /// let _stop_cmd = timer.stop();
1021 /// // timer.timeout is still 45 seconds after stop command is processed
1022 ///
1023 /// // To reset, create a new timer
1024 /// let reset_timer = new(Duration::from_secs(60));
1025 /// ```
1026 ///
1027 /// # Command Execution
1028 ///
1029 /// Like all timer control methods, the returned command is executed
1030 /// asynchronously by the Bubble Tea runtime. The actual pause occurs
1031 /// when the `StartStopMsg` is processed.
1032 ///
1033 /// # No Effect Scenarios
1034 ///
1035 /// The stop command has no effect when:
1036 /// - The timer has already timed out (`timedout() == true`)
1037 /// - The timer is already stopped (redundant operation)
1038 ///
1039 /// # Note
1040 ///
1041 /// This method matches Go's `Stop()` method exactly for compatibility.
1042 pub fn stop(&self) -> Cmd {
1043 self.start_stop(false)
1044 }
1045
1046 /// Generates a command to toggle the timer's running state.
1047 ///
1048 /// This method provides a convenient way to switch between running and stopped
1049 /// states. If the timer is currently running, it will be stopped. If it's stopped,
1050 /// it will be started. This is particularly useful for pause/resume functionality
1051 /// controlled by a single user action.
1052 ///
1053 /// # Returns
1054 ///
1055 /// A `Cmd` that will toggle the timer's state when executed
1056 ///
1057 /// # Examples
1058 ///
1059 /// ```rust
1060 /// use bubbletea_widgets::timer::new;
1061 /// use bubbletea_rs::{Model as BubbleTeaModel, Cmd, KeyMsg};
1062 /// use crossterm::event::{KeyCode, KeyModifiers};
1063 /// use std::time::Duration;
1064 ///
1065 /// struct PomodoroApp {
1066 /// work_timer: bubbletea_widgets::timer::Model,
1067 /// }
1068 ///
1069 /// impl BubbleTeaModel for PomodoroApp {
1070 /// fn init() -> (Self, Option<Cmd>) {
1071 /// let timer = new(Duration::from_secs(25 * 60)); // 25 minute work session
1072 /// let start_cmd = timer.init();
1073 /// (PomodoroApp { work_timer: timer }, Some(start_cmd))
1074 /// }
1075 ///
1076 /// fn update(&mut self, msg: bubbletea_rs::Msg) -> Option<Cmd> {
1077 /// // Spacebar toggles timer
1078 /// if let Some(key) = msg.downcast_ref::<KeyMsg>() {
1079 /// if key.key == KeyCode::Char(' ') {
1080 /// return Some(self.work_timer.toggle());
1081 /// }
1082 /// }
1083 ///
1084 /// self.work_timer.update(msg)
1085 /// }
1086 ///
1087 /// fn view(&self) -> String {
1088 /// format!(
1089 /// "Work Timer: {}\n\n[SPACE] to {}",
1090 /// self.work_timer.view(),
1091 /// if self.work_timer.running() { "pause" } else { "resume" }
1092 /// )
1093 /// }
1094 /// }
1095 /// ```
1096 ///
1097 /// Simple toggle pattern:
1098 /// ```rust
1099 /// use bubbletea_widgets::timer::new;
1100 /// use std::time::Duration;
1101 ///
1102 /// let timer = new(Duration::from_secs(120));
1103 ///
1104 /// // One command handles both pause and resume
1105 /// let toggle_cmd = timer.toggle();
1106 ///
1107 /// // Equivalent to:
1108 /// // if timer.running() {
1109 /// // timer.stop()
1110 /// // } else {
1111 /// // timer.start()
1112 /// // }
1113 /// ```
1114 ///
1115 /// # State Determination
1116 ///
1117 /// The toggle decision is based on the current result of `running()`:
1118 /// - If `running()` returns `true` → generates stop command
1119 /// - If `running()` returns `false` → generates start command
1120 ///
1121 /// This means the toggle respects both manual stops and timeout states:
1122 /// ```rust
1123 /// use bubbletea_widgets::timer::new;
1124 /// use std::time::Duration;
1125 ///
1126 /// let mut timer = new(Duration::from_secs(30));
1127 ///
1128 /// // Normal toggle (running → stopped)
1129 /// assert!(timer.running());
1130 /// let _toggle1 = timer.toggle(); // Will stop
1131 ///
1132 /// // Toggle on expired timer (not running → no effect)
1133 /// timer.timeout = Duration::ZERO;
1134 /// assert!(!timer.running()); // Not running due to timeout
1135 /// let _toggle2 = timer.toggle(); // Will attempt start, but no effect due to timeout
1136 /// ```
1137 ///
1138 /// # User Interface Benefits
1139 ///
1140 /// Toggle is ideal for:
1141 /// - Single-key pause/resume (spacebar pattern)
1142 /// - Play/pause buttons in UI
1143 /// - Touch/click interfaces
1144 /// - Reducing cognitive load (one action vs. two separate actions)
1145 ///
1146 /// # Command Execution
1147 ///
1148 /// The toggle command evaluates the current state when called, not when executed.
1149 /// The actual state change occurs asynchronously when the resulting `StartStopMsg`
1150 /// is processed by the timer's `update()` method.
1151 ///
1152 /// # Note
1153 ///
1154 /// This method matches Go's `Toggle()` method exactly for compatibility.
1155 pub fn toggle(&self) -> Cmd {
1156 self.start_stop(!self.running())
1157 }
1158
1159 /// Internal tick function - matches Go's tick method.
1160 fn tick(&self) -> Cmd {
1161 let id = self.id;
1162 let tag = self.tag;
1163 let timeout = self.timedout();
1164 let interval = self.interval;
1165
1166 bubbletea_tick(interval, move |_| {
1167 if timeout {
1168 Box::new(TimeoutMsg { id }) as Msg
1169 } else {
1170 Box::new(TickMsg { id, timeout, tag }) as Msg
1171 }
1172 })
1173 }
1174
1175 /// Internal timedout command - matches Go's timedout method.
1176 #[allow(dead_code)]
1177 fn timedout_cmd(&self) -> std::option::Option<Cmd> {
1178 if !self.timedout() {
1179 return std::option::Option::None;
1180 }
1181 let id = self.id;
1182 std::option::Option::Some(bubbletea_tick(Duration::from_nanos(1), move |_| {
1183 Box::new(TimeoutMsg { id }) as Msg
1184 }))
1185 }
1186
1187 /// Internal start/stop command - matches Go's startStop method.
1188 fn start_stop(&self, running: bool) -> Cmd {
1189 let id = self.id;
1190 bubbletea_tick(Duration::from_nanos(1), move |_| {
1191 Box::new(StartStopMsg { id, running }) as Msg
1192 })
1193 }
1194
1195 /// Initializes the timer and returns the command to start its first tick.
1196 ///
1197 /// This method should be called once when the timer is first created to begin
1198 /// the countdown process. It generates the initial tick command that starts
1199 /// the timer's regular interval-based updates.
1200 ///
1201 /// # Returns
1202 ///
1203 /// A `Cmd` that will start the timer's tick cycle when executed
1204 ///
1205 /// # Examples
1206 ///
1207 /// ```rust
1208 /// use bubbletea_widgets::timer::new;
1209 /// use bubbletea_rs::{Model as BubbleTeaModel, Cmd};
1210 /// use std::time::Duration;
1211 ///
1212 /// struct App {
1213 /// timer: bubbletea_widgets::timer::Model,
1214 /// }
1215 ///
1216 /// impl BubbleTeaModel for App {
1217 /// fn init() -> (Self, Option<Cmd>) {
1218 /// let timer = new(Duration::from_secs(60));
1219 /// // Initialize the timer to start ticking
1220 /// let timer_cmd = timer.init();
1221 /// (App { timer }, Some(timer_cmd))
1222 /// }
1223 ///
1224 /// // ... other methods
1225 /// # fn update(&mut self, _: bubbletea_rs::Msg) -> Option<Cmd> { None }
1226 /// # fn view(&self) -> String { self.timer.view() }
1227 /// }
1228 /// ```
1229 ///
1230 /// Multiple timer initialization:
1231 /// ```rust
1232 /// use bubbletea_widgets::timer::new;
1233 /// use bubbletea_rs::{Model as BubbleTeaModel, Cmd};
1234 /// use std::time::Duration;
1235 ///
1236 /// struct MultiTimerApp {
1237 /// work_timer: bubbletea_widgets::timer::Model,
1238 /// break_timer: bubbletea_widgets::timer::Model,
1239 /// }
1240 ///
1241 /// impl BubbleTeaModel for MultiTimerApp {
1242 /// fn init() -> (Self, Option<Cmd>) {
1243 /// let work_timer = new(Duration::from_secs(25 * 60));
1244 /// let break_timer = new(Duration::from_secs(5 * 60));
1245 ///
1246 /// // Start the work timer initially
1247 /// let init_cmd = work_timer.init();
1248 ///
1249 /// (MultiTimerApp { work_timer, break_timer }, Some(init_cmd))
1250 /// }
1251 ///
1252 /// // ... other methods
1253 /// # fn update(&mut self, _: bubbletea_rs::Msg) -> Option<Cmd> { None }
1254 /// # fn view(&self) -> String { self.work_timer.view() }
1255 /// }
1256 /// ```
1257 ///
1258 /// # When to Call
1259 ///
1260 /// - **Application startup**: In your app's `init()` method
1261 /// - **Timer creation**: Immediately after creating a new timer
1262 /// - **Timer reset**: When restarting a timer with new settings
1263 ///
1264 /// # What It Does
1265 ///
1266 /// The `init()` method:
1267 /// 1. Creates the first tick command
1268 /// 2. Sets up the timer's internal tick cycle
1269 /// 3. Returns immediately (non-blocking)
1270 /// 4. The actual ticking starts when Bubble Tea executes the command
1271 ///
1272 /// # Timing Behavior
1273 ///
1274 /// The first tick occurs after the timer's `interval` duration. For example,
1275 /// with a 1-second interval, the first countdown update happens 1 second after
1276 /// the init command is executed.
1277 ///
1278 /// # Alternative to Start
1279 ///
1280 /// While `start()` is used to resume a paused timer, `init()` is specifically
1281 /// for initial timer setup and should be called once per timer instance.
1282 ///
1283 /// # Note
1284 ///
1285 /// This method matches Go's `Init()` method exactly for compatibility.
1286 pub fn init(&self) -> Cmd {
1287 self.tick()
1288 }
1289
1290 /// Processes messages and updates the timer state.
1291 ///
1292 /// This method handles all messages related to timer operation, including tick
1293 /// messages that advance the countdown and control messages that change the
1294 /// running state. It should be called from your application's update loop
1295 /// for proper timer functionality.
1296 ///
1297 /// # Arguments
1298 ///
1299 /// * `msg` - The message to process (typically `TickMsg` or `StartStopMsg`)
1300 ///
1301 /// # Returns
1302 ///
1303 /// An optional `Cmd` for the next timer operation, or `None` if no action needed
1304 ///
1305 /// # Message Types Handled
1306 ///
1307 /// - **`StartStopMsg`**: Changes the timer's running state
1308 /// - **`TickMsg`**: Advances the countdown and schedules the next tick
1309 /// - **Other messages**: Ignored (returns `None`)
1310 ///
1311 /// # Examples
1312 ///
1313 /// ```rust
1314 /// use bubbletea_widgets::timer::{new, TickMsg, StartStopMsg, TimeoutMsg};
1315 /// use bubbletea_rs::{Model as BubbleTeaModel, Msg, Cmd};
1316 /// use std::time::Duration;
1317 ///
1318 /// struct App {
1319 /// timer: bubbletea_widgets::timer::Model,
1320 /// status: String,
1321 /// }
1322 ///
1323 /// impl BubbleTeaModel for App {
1324 /// fn update(&mut self, msg: Msg) -> Option<Cmd> {
1325 /// // Handle application-specific timer events
1326 /// if let Some(timeout) = msg.downcast_ref::<TimeoutMsg>() {
1327 /// if timeout.id == self.timer.id() {
1328 /// self.status = "Timer completed!".to_string();
1329 /// }
1330 /// }
1331 ///
1332 /// // Forward all messages to timer for processing
1333 /// self.timer.update(msg)
1334 /// }
1335 ///
1336 /// // ... other methods
1337 /// # fn init() -> (Self, Option<Cmd>) { unimplemented!() }
1338 /// # fn view(&self) -> String { format!("{}: {}", self.status, self.timer.view()) }
1339 /// }
1340 /// ```
1341 ///
1342 /// Manual message handling:
1343 /// ```rust,ignore
1344 /// use bubbletea_widgets::timer::new;
1345 /// use std::time::Duration;
1346 ///
1347 /// let mut timer = new(Duration::from_secs(10));
1348 ///
1349 /// // Start the timer using the public API
1350 /// let start_cmd = timer.start();
1351 /// // In a real app, you'd send this command through bubbletea
1352 ///
1353 /// // Check initial timeout
1354 /// assert_eq!(timer.timeout(), Duration::from_secs(10));
1355 /// ```
1356 ///
1357 /// # Message Filtering
1358 ///
1359 /// The timer automatically filters messages to ensure it only processes
1360 /// messages intended for it:
1361 ///
1362 /// - **ID Matching**: Only processes messages with matching timer IDs
1363 /// - **Tag Validation**: Rejects tick messages with incorrect tags
1364 /// - **State Checks**: Ignores ticks when not running
1365 ///
1366 /// ```rust
1367 /// use bubbletea_widgets::timer::new;
1368 /// use std::time::Duration;
1369 ///
1370 /// let mut timer1 = new(Duration::from_secs(10));
1371 /// let _timer2 = new(Duration::from_secs(20));
1372 ///
1373 /// // Start timer1
1374 /// let _cmd = timer1.start();
1375 ///
1376 /// // Messages with wrong IDs are ignored
1377 /// // In a real app, messages are routed by ID
1378 /// assert_eq!(timer1.timeout, Duration::from_secs(10)); // No change
1379 /// ```
1380 ///
1381 /// # State Changes
1382 ///
1383 /// Different message types cause different state changes:
1384 ///
1385 /// - **`StartStopMsg`**: Changes `running` state, returns tick command
1386 /// - **`TickMsg`**: Reduces `timeout` by `interval`, returns next tick command
1387 /// - **Invalid messages**: No state change, returns `None`
1388 ///
1389 /// # Timeout Detection
1390 ///
1391 /// When a tick reduces the timeout to zero or below, the timer:
1392 /// 1. Automatically stops running
1393 /// 2. May send a `TimeoutMsg` (implementation detail)
1394 /// 3. Returns a tick command that will be ignored (since not running)
1395 ///
1396 /// # Error Handling
1397 ///
1398 /// This method never panics and handles invalid messages gracefully by
1399 /// ignoring them and returning `None`.
1400 ///
1401 /// # Performance
1402 ///
1403 /// Message processing is very fast, involving only basic comparisons and
1404 /// arithmetic operations. The method is designed to be called frequently
1405 /// in the Bubble Tea update loop.
1406 ///
1407 /// # Note
1408 ///
1409 /// This method matches Go's `Update()` method exactly for compatibility.
1410 pub fn update(&mut self, msg: Msg) -> std::option::Option<Cmd> {
1411 if let Some(start_stop_msg) = msg.downcast_ref::<StartStopMsg>() {
1412 if start_stop_msg.id != 0 && start_stop_msg.id != self.id {
1413 return std::option::Option::None;
1414 }
1415
1416 let was_running = self.running;
1417 self.running = start_stop_msg.running;
1418
1419 // Reset timing when starting from stopped state
1420 if !was_running && self.running {
1421 self.start_instant = None;
1422 self.last_tick = None;
1423 }
1424
1425 return std::option::Option::Some(self.tick());
1426 }
1427
1428 if let Some(tick_msg) = msg.downcast_ref::<TickMsg>() {
1429 if !self.running() || (tick_msg.id != 0 && tick_msg.id != self.id) {
1430 return std::option::Option::None;
1431 }
1432
1433 // If a tag is set, and it's not the one we expect, reject the message.
1434 // This prevents the ticker from receiving too many messages and
1435 // thus ticking too fast.
1436 if tick_msg.tag > 0 && tick_msg.tag != self.tag {
1437 return std::option::Option::None;
1438 }
1439
1440 // Use high-precision elapsed time tracking for accurate countdown
1441 let now = Instant::now();
1442
1443 // Initialize timing on first tick
1444 if self.last_tick.is_none() {
1445 self.start_instant = Some(now);
1446 self.last_tick = Some(now);
1447 // On first tick, just use the interval as fallback
1448 self.timeout = self.timeout.saturating_sub(self.interval);
1449 } else {
1450 // Calculate actual elapsed time since last tick
1451 let actual_elapsed = now.duration_since(self.last_tick.unwrap());
1452 self.timeout = self.timeout.saturating_sub(actual_elapsed);
1453 self.last_tick = Some(now);
1454 }
1455
1456 // Check if timer has expired after this tick
1457 if self.timedout() {
1458 // Timer has expired, send TimeoutMsg
1459 let id = self.id;
1460 return std::option::Option::Some(bubbletea_tick(
1461 Duration::from_nanos(1),
1462 move |_| Box::new(TimeoutMsg { id }) as Msg,
1463 ));
1464 }
1465
1466 // Timer still running, continue with next tick
1467 return std::option::Option::Some(self.tick());
1468 }
1469
1470 std::option::Option::None
1471 }
1472
1473 /// Renders the timer as a formatted string showing remaining time.
1474 ///
1475 /// This method converts the timer's current timeout duration into a
1476 /// human-readable string using Go's duration formatting conventions.
1477 /// The output is suitable for direct display in terminal applications.
1478 ///
1479 /// # Returns
1480 ///
1481 /// A formatted string representation of the remaining time
1482 ///
1483 /// # Format Examples
1484 ///
1485 /// The format matches Go's `Duration.String()` output:
1486 /// - `"5m30s"` for 5 minutes 30 seconds
1487 /// - `"2m"` for exactly 2 minutes
1488 /// - `"45.5s"` for 45.5 seconds
1489 /// - `"750ms"` for milliseconds
1490 /// - `"0s"` when expired
1491 ///
1492 /// # Examples
1493 ///
1494 /// ```rust
1495 /// use bubbletea_widgets::timer::new;
1496 /// use std::time::Duration;
1497 ///
1498 /// // Various timer displays
1499 /// let timer1 = new(Duration::from_secs(90));
1500 /// assert!(timer1.view().contains("1m"));
1501 ///
1502 /// let timer2 = new(Duration::from_millis(500));
1503 /// assert!(timer2.view().contains("500ms"));
1504 ///
1505 /// let mut timer3 = new(Duration::from_secs(1));
1506 /// timer3.timeout = Duration::ZERO;
1507 /// assert_eq!(timer3.view(), "0s");
1508 /// ```
1509 ///
1510 /// Integration in UI:
1511 /// ```rust
1512 /// use bubbletea_widgets::timer::new;
1513 /// use bubbletea_rs::{Model as BubbleTeaModel};
1514 /// use std::time::Duration;
1515 ///
1516 /// struct App {
1517 /// cooking_timer: bubbletea_widgets::timer::Model,
1518 /// recipe: String,
1519 /// }
1520 ///
1521 /// impl BubbleTeaModel for App {
1522 /// fn view(&self) -> String {
1523 /// format!(
1524 /// "Cooking: {}\n\nTime remaining: {}\n\n[SPACE] to pause",
1525 /// self.recipe,
1526 /// self.cooking_timer.view()
1527 /// )
1528 /// }
1529 ///
1530 /// // ... other methods
1531 /// # fn init() -> (Self, Option<bubbletea_rs::Cmd>) { unimplemented!() }
1532 /// # fn update(&mut self, _: bubbletea_rs::Msg) -> Option<bubbletea_rs::Cmd> { None }
1533 /// }
1534 /// ```
1535 ///
1536 /// Dynamic display updates:
1537 /// ```rust
1538 /// use bubbletea_widgets::timer::new;
1539 /// use std::time::Duration;
1540 ///
1541 /// let mut timer = new(Duration::from_secs(125)); // 2m5s
1542 ///
1543 /// // Display updates as timer counts down
1544 /// println!("Start: {}", timer.view()); // "2m5s"
1545 ///
1546 /// // Simulate 30 seconds passing
1547 /// timer.timeout -= Duration::from_secs(30);
1548 /// println!("After 30s: {}", timer.view()); // "1m35s"
1549 ///
1550 /// // Simulate completion
1551 /// timer.timeout = Duration::ZERO;
1552 /// println!("Finished: {}", timer.view()); // "0s"
1553 /// ```
1554 ///
1555 /// # Precision Display
1556 ///
1557 /// The display precision depends on the remaining time:
1558 /// - **Minutes**: Shows minutes and seconds (e.g., "3m45s")
1559 /// - **Seconds**: Shows seconds with decimals if needed (e.g., "1.5s")
1560 /// - **Milliseconds**: Shows millisecond precision (e.g., "250ms")
1561 /// - **Microseconds/Nanoseconds**: For very short durations
1562 ///
1563 /// # Use Cases
1564 ///
1565 /// Perfect for:
1566 /// - Countdown displays in TUIs
1567 /// - Progress indicators with time remaining
1568 /// - Cooking/work timers
1569 /// - Game time limits
1570 /// - Session timeouts
1571 ///
1572 /// # Performance
1573 ///
1574 /// String formatting is performed on each call, so for high-frequency
1575 /// updates, consider caching the result if the timeout hasn't changed.
1576 ///
1577 /// # Localization
1578 ///
1579 /// The format uses English abbreviations ("m", "s", "ms") and follows
1580 /// Go's conventions. For different locales, you may need to parse the
1581 /// `timeout` duration and format it according to local preferences.
1582 ///
1583 /// # Note
1584 ///
1585 /// This method matches Go's `View()` method exactly for compatibility.
1586 pub fn view(&self) -> String {
1587 format_duration(self.timeout)
1588 }
1589}
1590
1591impl BubbleTeaModel for Model {
1592 /// Creates a new timer model with default settings for standalone use.
1593 ///
1594 /// This implementation provides a default timer configuration suitable for
1595 /// applications that want to use the timer as a standalone component without
1596 /// custom initialization. It creates a 60-second timer with 1-second intervals.
1597 ///
1598 /// # Returns
1599 ///
1600 /// A tuple containing the new timer model and its initialization command
1601 ///
1602 /// # Examples
1603 ///
1604 /// ```rust
1605 /// use bubbletea_widgets::timer::Model as TimerModel;
1606 /// use bubbletea_rs::{Model as BubbleTeaModel};
1607 ///
1608 /// // Use timer as a standalone Bubble Tea application
1609 /// let model = TimerModel::default();
1610 /// let cmd = model.init();
1611 /// // Would start a 60-second timer app
1612 /// ```
1613 ///
1614 /// # Default Configuration
1615 ///
1616 /// - **Timeout**: 60 seconds
1617 /// - **Interval**: 1 second
1618 /// - **State**: Running (starts immediately)
1619 /// - **Display**: Shows countdown in "1m0s" format
1620 ///
1621 /// # Note
1622 ///
1623 /// Most applications will want to use `new()` or `new_with_interval()` instead
1624 /// to create timers with specific durations rather than this default.
1625 fn init() -> (Self, std::option::Option<Cmd>) {
1626 let model = new(Duration::from_secs(60));
1627 let cmd = model.init();
1628 (model, std::option::Option::Some(cmd))
1629 }
1630
1631 /// Forwards messages to the timer's update method.
1632 ///
1633 /// This implementation delegates all message processing to the timer's
1634 /// own `update()` method, ensuring consistent behavior whether the timer
1635 /// is used standalone or as part of a larger application.
1636 ///
1637 /// # Arguments
1638 ///
1639 /// * `msg` - The message to process
1640 ///
1641 /// # Returns
1642 ///
1643 /// An optional command for continued timer operation
1644 ///
1645 /// # Examples
1646 ///
1647 /// ```rust
1648 /// use bubbletea_widgets::timer::Model as TimerModel;
1649 /// use bubbletea_rs::{Model as BubbleTeaModel};
1650 ///
1651 /// let mut timer = TimerModel::default();
1652 /// // Start the timer
1653 /// let _start_cmd = timer.start();
1654 ///
1655 /// // In a real app, tick messages are generated automatically
1656 /// // Timer processes updates and returns commands for next ticks
1657 /// ```
1658 fn update(&mut self, msg: Msg) -> std::option::Option<Cmd> {
1659 self.update(msg)
1660 }
1661
1662 /// Renders the timer display using the timer's view method.
1663 ///
1664 /// This implementation delegates to the timer's own `view()` method,
1665 /// providing a consistent display format regardless of how the timer
1666 /// is integrated into the application.
1667 ///
1668 /// # Returns
1669 ///
1670 /// A formatted string showing the remaining time
1671 ///
1672 /// # Examples
1673 ///
1674 /// ```rust
1675 /// use bubbletea_widgets::timer::Model as TimerModel;
1676 /// use bubbletea_rs::Model as BubbleTeaModel;
1677 ///
1678 /// let timer = TimerModel::default();
1679 /// let display = timer.view();
1680 /// assert!(display.contains("1m") || display.contains("60s"));
1681 /// ```
1682 fn view(&self) -> String {
1683 self.view()
1684 }
1685}
1686
1687impl Default for Model {
1688 /// Creates a timer with sensible default settings.
1689 ///
1690 /// This implementation provides a standard 60-second timer with 1-second
1691 /// intervals, suitable for most common timing needs. The timer starts in
1692 /// a running state and is ready for immediate use.
1693 ///
1694 /// # Returns
1695 ///
1696 /// A new timer configured with default settings
1697 ///
1698 /// # Examples
1699 ///
1700 /// ```rust
1701 /// use bubbletea_widgets::timer::Model;
1702 /// use std::time::Duration;
1703 ///
1704 /// // Create timer with defaults
1705 /// let timer = Model::default();
1706 /// assert_eq!(timer.timeout, Duration::from_secs(60));
1707 /// assert_eq!(timer.interval, Duration::from_secs(1));
1708 /// assert!(timer.running());
1709 /// assert!(!timer.timedout());
1710 /// ```
1711 ///
1712 /// Using with struct initialization:
1713 /// ```rust
1714 /// use bubbletea_widgets::timer::Model as TimerModel;
1715 ///
1716 /// struct App {
1717 /// timer: TimerModel,
1718 /// // other fields...
1719 /// }
1720 ///
1721 /// impl Default for App {
1722 /// fn default() -> Self {
1723 /// Self {
1724 /// timer: TimerModel::default(),
1725 /// // initialize other fields...
1726 /// }
1727 /// }
1728 /// }
1729 /// ```
1730 ///
1731 /// # Default Values
1732 ///
1733 /// - **Timeout**: 60 seconds (1 minute)
1734 /// - **Interval**: 1 second (standard clock tick)
1735 /// - **Running**: `true` (starts immediately)
1736 /// - **ID**: Unique identifier (generated automatically)
1737 ///
1738 /// # When to Use
1739 ///
1740 /// Use `Default` when:
1741 /// - You need a quick timer for testing or prototyping
1742 /// - 60 seconds is an appropriate duration for your use case
1743 /// - You want to rely on struct field defaults in larger structures
1744 /// - Building utilities or examples that need reasonable timer behavior
1745 ///
1746 /// Use `new()` or `new_with_interval()` when:
1747 /// - You need specific timeout durations
1748 /// - Custom tick intervals are required
1749 /// - Explicit configuration is preferred for clarity
1750 ///
1751 /// # Equivalent Creation
1752 ///
1753 /// This default implementation is equivalent to:
1754 /// ```rust
1755 /// use bubbletea_widgets::timer::new;
1756 /// use std::time::Duration;
1757 ///
1758 /// let timer = new(Duration::from_secs(60));
1759 /// ```
1760 fn default() -> Self {
1761 new(Duration::from_secs(60))
1762 }
1763}
1764
1765#[cfg(test)]
1766mod tests {
1767 use super::*;
1768 use std::time::Duration;
1769
1770 #[test]
1771 fn test_new_with_timeout() {
1772 // Test Go's: New(timeout)
1773 let timeout = Duration::from_secs(30);
1774 let timer = new(timeout);
1775
1776 assert_eq!(timer.timeout, timeout);
1777 assert_eq!(timer.interval, Duration::from_secs(1)); // Default interval
1778 assert!(timer.id() > 0); // Should have unique ID
1779 assert!(timer.running()); // Should start running
1780 assert!(!timer.timedout()); // Should not be timed out initially
1781 }
1782
1783 #[test]
1784 fn test_new_with_interval() {
1785 // Test Go's: NewWithInterval(timeout, interval)
1786 let timeout = Duration::from_secs(60);
1787 let interval = Duration::from_millis(500);
1788 let timer = new_with_interval(timeout, interval);
1789
1790 assert_eq!(timer.timeout, timeout);
1791 assert_eq!(timer.interval, interval);
1792 assert!(timer.id() > 0);
1793 assert!(timer.running());
1794 assert!(!timer.timedout());
1795 }
1796
1797 #[test]
1798 fn test_unique_ids() {
1799 // Test that multiple timers get unique IDs
1800 let timer1 = new(Duration::from_secs(10));
1801 let timer2 = new(Duration::from_secs(20));
1802
1803 assert_ne!(timer1.id(), timer2.id());
1804 }
1805
1806 #[test]
1807 fn test_running_logic() {
1808 // Test Go's: Running() bool
1809 let mut timer = new(Duration::from_secs(5));
1810
1811 // Initially running
1812 assert!(timer.running());
1813
1814 // After manual stop
1815 timer.running = false;
1816 assert!(!timer.running());
1817
1818 // After timeout
1819 timer.running = true;
1820 timer.timeout = Duration::ZERO;
1821 assert!(!timer.running()); // Should be false when timed out
1822 }
1823
1824 #[test]
1825 fn test_timedout_logic() {
1826 // Test Go's: Timedout() bool
1827 let mut timer = new(Duration::from_secs(5));
1828
1829 assert!(!timer.timedout());
1830
1831 // Set to zero
1832 timer.timeout = Duration::ZERO;
1833 assert!(timer.timedout());
1834
1835 // Set to negative (should be handled as zero)
1836 timer.timeout = Duration::from_nanos(0);
1837 assert!(timer.timedout());
1838 }
1839
1840 #[test]
1841 fn test_id_method() {
1842 // Test Go's: ID() int
1843 let timer = new(Duration::from_secs(10));
1844 let id = timer.id();
1845
1846 assert!(id > 0);
1847 assert_eq!(timer.id(), id); // Should return same ID consistently
1848 }
1849
1850 #[test]
1851 fn test_start_stop_toggle_commands() {
1852 // Test Go's: Start(), Stop(), Toggle() return commands
1853 let timer = new(Duration::from_secs(10));
1854
1855 // These should return commands (not panic)
1856 let _start_cmd = timer.start();
1857 let _stop_cmd = timer.stop();
1858 let _toggle_cmd = timer.toggle();
1859
1860 // Commands should be callable (we can't easily test their content without executing)
1861 // In a real app, these would send StartStopMsg messages
1862 }
1863
1864 #[test]
1865 fn test_init_command() {
1866 // Test Go's: Init() tea.Cmd
1867 let timer = new(Duration::from_secs(10));
1868 let _cmd = timer.init();
1869
1870 // Should return a command (tick command)
1871 }
1872
1873 #[test]
1874 fn test_update_with_start_stop_msg() {
1875 // Test Go's: Update with StartStopMsg
1876 let mut timer = new(Duration::from_secs(10));
1877 timer.running = false; // Stop it first
1878
1879 let start_msg = StartStopMsg {
1880 id: timer.id(),
1881 running: true,
1882 };
1883
1884 let result = timer.update(Box::new(start_msg));
1885 assert!(result.is_some()); // Should return tick command
1886 assert!(timer.running); // Should now be running
1887 }
1888
1889 #[test]
1890 fn test_update_with_wrong_id() {
1891 // Test that timer rejects StartStopMsg with wrong ID
1892 let mut timer = new(Duration::from_secs(10));
1893
1894 let wrong_msg = StartStopMsg {
1895 id: timer.id() + 999, // Wrong ID
1896 running: false,
1897 };
1898
1899 let original_running = timer.running;
1900 let result = timer.update(Box::new(wrong_msg));
1901
1902 assert!(result.is_none()); // Should reject
1903 assert_eq!(timer.running, original_running); // State unchanged
1904 }
1905
1906 #[test]
1907 fn test_update_with_tick_msg() {
1908 // Test Go's: Update with TickMsg
1909 let mut timer = new(Duration::from_secs(5));
1910 let original_timeout = timer.timeout;
1911
1912 let tick_msg = TickMsg {
1913 id: timer.id(),
1914 timeout: false,
1915 tag: 0,
1916 };
1917
1918 let result = timer.update(Box::new(tick_msg));
1919 assert!(result.is_some()); // Should return next tick command
1920 assert!(timer.timeout < original_timeout); // Should have decreased
1921 }
1922
1923 #[test]
1924 fn test_update_tick_reduces_timeout() {
1925 // Test that TickMsg reduces timeout by interval
1926 let mut timer = new_with_interval(Duration::from_secs(10), Duration::from_secs(2));
1927 let original_timeout = timer.timeout;
1928
1929 let tick_msg = TickMsg {
1930 id: timer.id(),
1931 timeout: false,
1932 tag: 0,
1933 };
1934
1935 timer.update(Box::new(tick_msg));
1936
1937 assert_eq!(
1938 timer.timeout,
1939 original_timeout.saturating_sub(Duration::from_secs(2))
1940 );
1941 }
1942
1943 #[test]
1944 fn test_view_format() {
1945 // Test Go's: View() string (using timeout.String() format)
1946 let timer = new(Duration::from_secs(65)); // 1m5s
1947 let view = timer.view();
1948
1949 // Should be formatted like Go's Duration.String()
1950 assert!(view.contains("m") || view.contains("s"));
1951
1952 // Test zero duration
1953 let mut timer_zero = new(Duration::from_secs(1));
1954 timer_zero.timeout = Duration::ZERO;
1955 assert_eq!(timer_zero.view(), "0s");
1956 }
1957
1958 #[test]
1959 fn test_view_various_durations() {
1960 // Test various duration formats
1961 let test_cases = vec![
1962 (Duration::from_millis(500), "500ms"),
1963 (Duration::from_secs(1), "1s"),
1964 (Duration::from_secs(61), "1m1s"),
1965 (Duration::from_secs(120), "2m"),
1966 ];
1967
1968 for (duration, expected_contains) in test_cases {
1969 let mut timer = new(duration);
1970 timer.timeout = duration;
1971 let view = timer.view();
1972
1973 // At least check that it contains expected parts
1974 // (exact format matching with Go is complex due to precision)
1975 if expected_contains.contains("m") {
1976 assert!(
1977 view.contains("m"),
1978 "Duration {:?} should contain 'm' in view: {}",
1979 duration,
1980 view
1981 );
1982 }
1983 if expected_contains.contains("s") && !expected_contains.contains("ms") {
1984 assert!(
1985 view.contains("s"),
1986 "Duration {:?} should contain 's' in view: {}",
1987 duration,
1988 view
1989 );
1990 }
1991 }
1992 }
1993
1994 #[test]
1995 fn test_tag_filtering() {
1996 // Test that timer rejects TickMsg with wrong tag
1997 let mut timer = new(Duration::from_secs(10));
1998 timer.tag = 5; // Set specific tag
1999
2000 let wrong_tick = TickMsg {
2001 id: timer.id(),
2002 timeout: false,
2003 tag: 999, // Wrong tag
2004 };
2005
2006 let result = timer.update(Box::new(wrong_tick));
2007 assert!(result.is_none()); // Should reject
2008 }
2009
2010 #[test]
2011 fn test_not_running_rejects_ticks() {
2012 // Test that stopped timer rejects TickMsg
2013 let mut timer = new(Duration::from_secs(10));
2014 timer.running = false;
2015
2016 let tick_msg = TickMsg {
2017 id: timer.id(),
2018 timeout: false,
2019 tag: 0,
2020 };
2021
2022 let result = timer.update(Box::new(tick_msg));
2023 assert!(result.is_none()); // Should reject when not running
2024 }
2025
2026 #[test]
2027 fn test_default_timer() {
2028 // Test Default implementation
2029 let timer = Model::default();
2030 assert_eq!(timer.timeout, Duration::from_secs(60));
2031 assert_eq!(timer.interval, Duration::from_secs(1));
2032 assert!(timer.running());
2033 }
2034
2035 #[test]
2036 fn test_timeout_msg_semantics() {
2037 // Test TimeoutMsg structure
2038 let timeout_msg = TimeoutMsg { id: 123 };
2039 assert_eq!(timeout_msg.id, 123);
2040 }
2041
2042 #[test]
2043 fn test_timing_fields_initialization() {
2044 // Test that timing fields are properly initialized
2045 let timer = new(Duration::from_secs(10));
2046 assert!(timer.start_instant.is_none());
2047 assert!(timer.last_tick.is_none());
2048
2049 let timer_with_interval =
2050 new_with_interval(Duration::from_secs(30), Duration::from_millis(100));
2051 assert!(timer_with_interval.start_instant.is_none());
2052 assert!(timer_with_interval.last_tick.is_none());
2053 }
2054
2055 #[test]
2056 fn test_timing_initialization_on_first_tick() {
2057 // Test that timing is initialized on first tick message
2058 let mut timer = new(Duration::from_secs(5));
2059 assert!(timer.start_instant.is_none());
2060 assert!(timer.last_tick.is_none());
2061
2062 let tick_msg = TickMsg {
2063 id: timer.id(),
2064 timeout: false,
2065 tag: 0,
2066 };
2067
2068 // First tick should initialize timing
2069 let start_time = std::time::Instant::now();
2070 let result = timer.update(Box::new(tick_msg));
2071 let end_time = std::time::Instant::now();
2072
2073 assert!(result.is_some());
2074 assert!(timer.start_instant.is_some());
2075 assert!(timer.last_tick.is_some());
2076
2077 // Check that the timing was set to approximately the current time
2078 let tick_time = timer.last_tick.unwrap();
2079 assert!(tick_time >= start_time);
2080 assert!(tick_time <= end_time);
2081 }
2082
2083 #[test]
2084 fn test_timing_reset_on_restart() {
2085 // Test that timing is reset when starting from stopped state
2086 let mut timer = new(Duration::from_secs(10));
2087
2088 // Stop the timer
2089 timer.running = false;
2090 timer.start_instant = Some(std::time::Instant::now());
2091 timer.last_tick = Some(std::time::Instant::now());
2092
2093 // Start the timer (should reset timing)
2094 let start_msg = StartStopMsg {
2095 id: timer.id(),
2096 running: true,
2097 };
2098
2099 let result = timer.update(Box::new(start_msg));
2100 assert!(result.is_some());
2101 assert!(timer.running);
2102 assert!(timer.start_instant.is_none()); // Should be reset
2103 assert!(timer.last_tick.is_none()); // Should be reset
2104 }
2105
2106 #[test]
2107 fn test_timing_preserved_on_stop() {
2108 // Test that timing is preserved when stopping (not reset)
2109 let mut timer = new(Duration::from_secs(10));
2110
2111 // Initialize timing with a tick
2112 let tick_msg = TickMsg {
2113 id: timer.id(),
2114 timeout: false,
2115 tag: 0,
2116 };
2117 timer.update(Box::new(tick_msg));
2118
2119 let preserved_start = timer.start_instant;
2120 let preserved_tick = timer.last_tick;
2121
2122 // Stop the timer (should preserve timing)
2123 let stop_msg = StartStopMsg {
2124 id: timer.id(),
2125 running: false,
2126 };
2127
2128 let result = timer.update(Box::new(stop_msg));
2129 assert!(result.is_some());
2130 assert!(!timer.running);
2131 assert_eq!(timer.start_instant, preserved_start); // Should be preserved
2132 assert_eq!(timer.last_tick, preserved_tick); // Should be preserved
2133 }
2134}