Skip to main content

dioxus_nox_timer/
countdown.rs

1//! Countdown timer hook.
2
3use dioxus::prelude::*;
4
5use crate::time;
6use crate::types::{CountdownControls, TimerState};
7
8/// Countdown timer hook.
9///
10/// Returns `(remaining_seconds, state, controls)`.
11///
12/// Uses wall-clock calculation internally for accuracy across tab backgrounding.
13/// The timer ticks approximately every 100ms for smooth UI updates.
14///
15/// # Parameters
16///
17/// - `on_complete`: Optional callback fired once when the countdown reaches zero.
18///
19/// # Example
20///
21/// ```rust,ignore
22/// let (remaining, state, controls) = use_countdown(None);
23/// controls.start.call(90); // Start a 90-second countdown
24/// ```
25pub fn use_countdown(
26    on_complete: Option<Callback<()>>,
27) -> (Signal<i64>, Signal<TimerState>, CountdownControls) {
28    // Wall-clock end time (epoch ms). None when idle or paused.
29    let mut end_time_ms = use_signal(|| None::<i64>);
30    // Remaining ms when paused (so we can resume accurately).
31    let mut paused_remaining_ms = use_signal(|| None::<i64>);
32    // Current state.
33    let mut state = use_signal(|| TimerState::Idle);
34    // Remaining seconds (derived, updated by tick loop).
35    let mut remaining = use_signal(|| 0i64);
36    // Generation counter to cancel stale tick loops.
37    let mut generation = use_signal(|| 0u32);
38
39    // Tick loop effect: runs whenever generation changes.
40    let gen_value = *generation.read();
41    use_effect(move || {
42        let current_gen = gen_value;
43        spawn(async move {
44            loop {
45                time::sleep_ms(100).await;
46                // Stop if generation changed (a new start/skip/dismiss happened).
47                if *generation.read() != current_gen {
48                    break;
49                }
50                let current_state = *state.read();
51                if current_state != TimerState::Running {
52                    if current_state == TimerState::Idle || current_state == TimerState::Complete {
53                        break;
54                    }
55                    // Paused — keep looping but don't update.
56                    continue;
57                }
58                let end_val = *end_time_ms.read();
59                if let Some(end) = end_val {
60                    let now = time::now_ms();
61                    let remain_ms = (end - now).max(0);
62                    let remain_secs = (remain_ms + 999) / 1000; // Ceiling division
63                    remaining.set(remain_secs);
64
65                    if remain_ms <= 0 {
66                        state.set(TimerState::Complete);
67                        remaining.set(0);
68                        end_time_ms.set(None);
69                        if let Some(ref cb) = on_complete {
70                            cb.call(());
71                        }
72                        break;
73                    }
74                }
75            }
76        });
77    });
78
79    let controls = CountdownControls {
80        start: Callback::new(move |duration_secs: i64| {
81            let now = time::now_ms();
82            end_time_ms.set(Some(now + duration_secs * 1000));
83            paused_remaining_ms.set(None);
84            remaining.set(duration_secs);
85            state.set(TimerState::Running);
86            let next_gen = generation.read().wrapping_add(1);
87            generation.set(next_gen);
88        }),
89        pause: Callback::new(move |()| {
90            if *state.read() == TimerState::Running {
91                let end = *end_time_ms.read();
92                if let Some(end) = end {
93                    let remain = (end - time::now_ms()).max(0);
94                    paused_remaining_ms.set(Some(remain));
95                    end_time_ms.set(None);
96                    state.set(TimerState::Paused);
97                }
98            }
99        }),
100        resume: Callback::new(move |()| {
101            if *state.read() == TimerState::Paused {
102                let remain = *paused_remaining_ms.read();
103                if let Some(remain) = remain {
104                    let now = time::now_ms();
105                    end_time_ms.set(Some(now + remain));
106                    paused_remaining_ms.set(None);
107                    state.set(TimerState::Running);
108                    let next_gen = generation.read().wrapping_add(1);
109                    generation.set(next_gen);
110                }
111            }
112        }),
113        skip: Callback::new(move |()| {
114            end_time_ms.set(None);
115            paused_remaining_ms.set(None);
116            remaining.set(0);
117            state.set(TimerState::Idle);
118            let next_gen = generation.read().wrapping_add(1);
119            generation.set(next_gen);
120        }),
121        adjust: Callback::new(move |delta_secs: i64| {
122            let current_state = *state.read();
123            match current_state {
124                TimerState::Running => {
125                    let end = *end_time_ms.read();
126                    if let Some(end) = end {
127                        let new_end = end + delta_secs * 1000;
128                        let now = time::now_ms();
129                        if new_end <= now {
130                            end_time_ms.set(Some(now));
131                            remaining.set(0);
132                        } else {
133                            end_time_ms.set(Some(new_end));
134                            let remain_ms = new_end - now;
135                            remaining.set((remain_ms + 999) / 1000);
136                        }
137                    }
138                }
139                TimerState::Paused => {
140                    let remain = *paused_remaining_ms.read();
141                    if let Some(remain) = remain {
142                        let new_remain = (remain + delta_secs * 1000).max(0);
143                        paused_remaining_ms.set(Some(new_remain));
144                        remaining.set((new_remain + 999) / 1000);
145                    }
146                }
147                _ => {}
148            }
149        }),
150        dismiss: Callback::new(move |()| {
151            if *state.read() == TimerState::Complete {
152                end_time_ms.set(None);
153                paused_remaining_ms.set(None);
154                remaining.set(0);
155                state.set(TimerState::Idle);
156                let next_gen = generation.read().wrapping_add(1);
157                generation.set(next_gen);
158            }
159        }),
160    };
161
162    (remaining, state, controls)
163}