Skip to main content

rlvgl_core/
timer.rs

1//! Tick-driven timer registry — `no_std + alloc`, no wall clock (LPAR-06 §5).
2//!
3//! [`Timers`] is a deterministic, tick-counted callback scheduler. Periods and
4//! all durations are expressed in **ticks** (one dispatch of
5//! [`Event::Tick`](crate::event::Event::Tick)); no milliseconds or
6//! wall-clock instants appear in this module.
7//!
8//! # Basic usage
9//!
10//! ```rust
11//! # extern crate alloc;
12//! use rlvgl_core::timer::{TimerRepeat, Timers};
13//!
14//! let mut timers = Timers::new();
15//!
16//! // Infinite repeating timer: fires every 5 ticks.
17//! let _id = timers.add(5, TimerRepeat::Infinite, Box::new(|_ctx| {
18//!     // ... periodic work ...
19//! }));
20//!
21//! // One-shot: fires once after 3 ticks, then removes itself.
22//! let _id2 = timers.add_once(3, Box::new(|_ctx| {
23//!     // ... one-shot work ...
24//! }));
25//!
26//! // Advance one tick per frame (call once per Event::Tick).
27//! timers.tick();
28//! ```
29//!
30//! # Determinism invariant (LPAR-06 §5.8)
31//!
32//! For a fixed initial configuration and a fixed tick sequence, the fire
33//! sequence (which ids fired, in which order, at which tick count) is
34//! bit-identical across runs and hosts. No randomness or wall-clock dependency.
35
36use alloc::boxed::Box;
37use alloc::vec::Vec;
38
39// ---------------------------------------------------------------------------
40// Public types
41// ---------------------------------------------------------------------------
42
43/// Opaque handle to a registered timer, returned by [`Timers::add`] and
44/// [`Timers::add_once`]. Used with [`pause`](Timers::pause),
45/// [`resume`](Timers::resume), [`delete`](Timers::delete), and
46/// [`set_ready`](Timers::set_ready).
47///
48/// Ids are unique per [`Timers`] instance; two separate instances may issue the
49/// same number without collision risk (they are different registries). The
50/// counter wraps at 2³², consistent with [`crate::anim::AnimId`].
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub struct TimerId(u32);
53
54/// Controls how many times a timer fires before it is exhausted.
55///
56/// LPAR-06 §5.3 registration policy: **Specification Required**. Adding a new
57/// variant requires a phase-doc entry and a §15 amendment.
58#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum TimerRepeat {
60    /// Fire forever; the timer is never exhausted.
61    Infinite,
62    /// Fire at most `n` more times. When `n` reaches `0`, the timer is
63    /// exhausted and is auto-deleted or paused according to the entry's
64    /// `auto_delete` flag.
65    Remaining(u32),
66}
67
68/// Context passed to a timer callback on each fire.
69///
70/// Currently exposes the timer's `id` and its remaining repeat count (`None`
71/// for [`TimerRepeat::Infinite`]).
72#[derive(Debug, Clone, Copy)]
73pub struct TimerContext {
74    /// Handle of the firing timer.
75    pub id: TimerId,
76    /// Fires remaining after this fire, or `None` for infinite timers.
77    pub remaining: Option<u32>,
78}
79
80// ---------------------------------------------------------------------------
81// Internal entry
82// ---------------------------------------------------------------------------
83
84struct TimerEntry {
85    id: TimerId,
86    period: u32,
87    countdown: u32,
88    repeat: TimerRepeat,
89    paused: bool,
90    /// When `true`, the entry fires on the next `tick()` regardless of
91    /// `countdown`. Cleared after firing.
92    ready: bool,
93    /// When `true`, the entry is removed on exhaustion; when `false` it is
94    /// paused. Default: `true`.
95    auto_delete: bool,
96    callback: Box<dyn FnMut(&TimerContext)>,
97}
98
99// ---------------------------------------------------------------------------
100// Timers registry
101// ---------------------------------------------------------------------------
102
103/// Registry/scheduler for tick-driven callbacks.
104///
105/// Call [`tick`](Self::tick) once per [`Event::Tick`](crate::event::Event::Tick)
106/// dispatch. Entries advance in registration order; multiple entries may fire
107/// in the same tick.
108///
109/// # no_std + alloc
110///
111/// `Timers` requires `alloc` (for `Box<dyn FnMut>` callbacks), consistent with
112/// the ANIM-00 `Animations` registry.
113pub struct Timers {
114    entries: Vec<TimerEntry>,
115    next_id: u32,
116}
117
118impl Default for Timers {
119    fn default() -> Self {
120        Self::new()
121    }
122}
123
124impl Timers {
125    /// Create an empty timer registry.
126    pub fn new() -> Self {
127        Self {
128            entries: Vec::new(),
129            next_id: 0,
130        }
131    }
132
133    /// Register a repeating timer.
134    ///
135    /// `period` is in ticks. The first fire occurs after `period` ticks.
136    /// `repeat` controls how many times it fires before exhaustion. Returns an
137    /// opaque [`TimerId`] for later control operations.
138    pub fn add(
139        &mut self,
140        period: u32,
141        repeat: TimerRepeat,
142        callback: Box<dyn FnMut(&TimerContext)>,
143    ) -> TimerId {
144        let id = TimerId(self.next_id);
145        self.next_id = self.next_id.wrapping_add(1);
146        self.entries.push(TimerEntry {
147            id,
148            period,
149            countdown: period,
150            repeat,
151            paused: false,
152            ready: false,
153            auto_delete: true,
154            callback,
155        });
156        id
157    }
158
159    /// Register a one-shot timer (convenience wrapper).
160    ///
161    /// Equivalent to `add(period, TimerRepeat::Remaining(1), callback)` with
162    /// `auto_delete = true`. The entry is automatically removed after the
163    /// single fire. Used for delayed single actions (auto-close toast,
164    /// delayed start, etc.) as specified by LPAR-06 §5.7.
165    pub fn add_once(&mut self, period: u32, callback: Box<dyn FnMut(&TimerContext)>) -> TimerId {
166        let id = TimerId(self.next_id);
167        self.next_id = self.next_id.wrapping_add(1);
168        self.entries.push(TimerEntry {
169            id,
170            period,
171            countdown: period,
172            repeat: TimerRepeat::Remaining(1),
173            paused: false,
174            ready: false,
175            auto_delete: true,
176            callback,
177        });
178        id
179    }
180
181    /// Advance all non-paused timers by exactly one tick.
182    ///
183    /// For each non-paused entry:
184    /// - If `ready` is set, the entry fires immediately (countdown is **not**
185    ///   decremented — the next fire after a ready-fire is still `period` ticks
186    ///   from that point).
187    /// - Otherwise, `countdown` is decremented. When it reaches zero, the
188    ///   entry fires.
189    ///
190    /// Firing order within a single tick is registration order (deterministic).
191    ///
192    /// After firing, `countdown` is reset to `period`. If the repeat is
193    /// `Remaining(n)`, it decrements to `Remaining(n-1)`. When exhausted
194    /// (`Remaining(0)` becomes 0 after decrement), the entry is auto-deleted
195    /// (if `auto_delete`) or paused.
196    pub fn tick(&mut self) {
197        let mut to_delete: Vec<TimerId> = Vec::new();
198
199        for entry in &mut self.entries {
200            if entry.paused {
201                continue;
202            }
203
204            let should_fire = if entry.ready {
205                true
206            } else {
207                if entry.countdown > 0 {
208                    entry.countdown -= 1;
209                }
210                entry.countdown == 0
211            };
212
213            if !should_fire {
214                continue;
215            }
216
217            // Compute remaining BEFORE decrement so the callback sees the count
218            // remaining after this fire.
219            let remaining_after = match entry.repeat {
220                TimerRepeat::Infinite => None,
221                TimerRepeat::Remaining(n) => {
222                    // n >= 1 here because exhausted entries are removed/paused.
223                    Some(n.saturating_sub(1))
224                }
225            };
226
227            let ctx = TimerContext {
228                id: entry.id,
229                remaining: remaining_after,
230            };
231            (entry.callback)(&ctx);
232
233            // Reset countdown and ready flag.
234            entry.countdown = entry.period;
235            entry.ready = false;
236
237            // Decrement repeat count.
238            match &mut entry.repeat {
239                TimerRepeat::Infinite => {}
240                TimerRepeat::Remaining(n) => {
241                    *n = n.saturating_sub(1);
242                    if *n == 0 {
243                        if entry.auto_delete {
244                            to_delete.push(entry.id);
245                        } else {
246                            entry.paused = true;
247                        }
248                    }
249                }
250            }
251        }
252
253        // Remove exhausted entries in reverse order to preserve indices.
254        for id in to_delete {
255            self.entries.retain(|e| e.id != id);
256        }
257    }
258
259    /// Pause a timer, freezing its countdown.
260    ///
261    /// A paused timer is not advanced by [`tick`](Self::tick). Pause/resume
262    /// does NOT reset the countdown; a timer paused mid-period resumes from
263    /// where it left off. Mirrors `lv_timer_pause`.
264    pub fn pause(&mut self, id: TimerId) {
265        if let Some(entry) = self.entries.iter_mut().find(|e| e.id == id) {
266            entry.paused = true;
267        }
268    }
269
270    /// Resume a paused timer from its frozen countdown.
271    ///
272    /// Mirrors `lv_timer_resume`.
273    pub fn resume(&mut self, id: TimerId) {
274        if let Some(entry) = self.entries.iter_mut().find(|e| e.id == id) {
275            entry.paused = false;
276        }
277    }
278
279    /// Remove a timer without firing a final callback.
280    ///
281    /// Returns `true` if the id was registered, `false` if not found. Mirrors
282    /// `lv_timer_delete`.
283    pub fn delete(&mut self, id: TimerId) -> bool {
284        let before = self.entries.len();
285        self.entries.retain(|e| e.id != id);
286        self.entries.len() != before
287    }
288
289    /// Mark a timer as ready: it fires on the **next** [`tick`](Self::tick)
290    /// call regardless of its remaining countdown, then the flag clears and
291    /// the period-based countdown resumes. Mirrors `lv_timer_ready`.
292    pub fn set_ready(&mut self, id: TimerId) {
293        if let Some(entry) = self.entries.iter_mut().find(|e| e.id == id) {
294            entry.ready = true;
295        }
296    }
297
298    /// Returns the number of registered (not yet exhausted/deleted) timers.
299    pub fn len(&self) -> usize {
300        self.entries.len()
301    }
302
303    /// Returns `true` when no timers are registered.
304    pub fn is_empty(&self) -> bool {
305        self.entries.is_empty()
306    }
307}
308
309// ---------------------------------------------------------------------------
310// Tests
311// ---------------------------------------------------------------------------
312
313#[cfg(test)]
314mod tests {
315    use super::*;
316    use alloc::rc::Rc;
317    use alloc::vec;
318    use alloc::vec::Vec;
319    use core::cell::RefCell;
320
321    #[test]
322    fn deterministic_fire_sequence() {
323        // Period-5 timer fired at ticks 5, 10, 15 in that order.
324        let fired: Rc<RefCell<Vec<u32>>> = Rc::new(RefCell::new(Vec::new()));
325        let mut timers = Timers::new();
326        let tick_counter = Rc::new(RefCell::new(0u32));
327
328        {
329            let f = fired.clone();
330            let tc = tick_counter.clone();
331            timers.add(
332                5,
333                TimerRepeat::Infinite,
334                Box::new(move |_ctx| {
335                    f.borrow_mut().push(*tc.borrow());
336                }),
337            );
338        }
339
340        for i in 1u32..=15 {
341            *tick_counter.borrow_mut() = i;
342            timers.tick();
343        }
344
345        assert_eq!(*fired.borrow(), vec![5, 10, 15]);
346    }
347
348    #[test]
349    fn one_shot_fires_once_then_removed() {
350        let count: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
351        let mut timers = Timers::new();
352
353        {
354            let c = count.clone();
355            timers.add_once(3, Box::new(move |_ctx| *c.borrow_mut() += 1));
356        }
357
358        for _ in 0..10 {
359            timers.tick();
360        }
361
362        assert_eq!(*count.borrow(), 1, "one-shot fires exactly once");
363        assert!(timers.is_empty(), "one-shot removes itself after firing");
364    }
365
366    #[test]
367    fn pause_freezes_countdown_resume_continues() {
368        let count: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
369        let mut timers = Timers::new();
370
371        let id = {
372            let c = count.clone();
373            timers.add(
374                4,
375                TimerRepeat::Infinite,
376                Box::new(move |_ctx| *c.borrow_mut() += 1),
377            )
378        };
379
380        // Tick 1–2 (countdown: 4→3→2).
381        timers.tick();
382        timers.tick();
383        assert_eq!(*count.borrow(), 0);
384
385        // Pause at countdown = 2.
386        timers.pause(id);
387        // Ticks while paused: countdown stays at 2.
388        timers.tick();
389        timers.tick();
390        timers.tick();
391        assert_eq!(*count.borrow(), 0, "paused timer did not fire");
392
393        // Resume: countdown should still be 2.
394        timers.resume(id);
395        timers.tick(); // countdown: 2→1
396        assert_eq!(*count.borrow(), 0);
397        timers.tick(); // countdown: 1→0 → fire
398        assert_eq!(
399            *count.borrow(),
400            1,
401            "timer fired after resuming from frozen countdown"
402        );
403    }
404
405    #[test]
406    fn set_ready_fires_next_tick_then_clears() {
407        let count: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
408        let mut timers = Timers::new();
409
410        let id = {
411            let c = count.clone();
412            timers.add(
413                100,
414                TimerRepeat::Infinite,
415                Box::new(move |_ctx| *c.borrow_mut() += 1),
416            )
417        };
418
419        // No fires yet (period is very long).
420        timers.tick();
421        assert_eq!(*count.borrow(), 0);
422
423        // Mark ready.
424        timers.set_ready(id);
425
426        // Fires on the NEXT tick.
427        timers.tick();
428        assert_eq!(*count.borrow(), 1, "ready timer fires on next tick");
429
430        // After firing the ready flag is cleared; timer returns to period-based advance.
431        timers.tick();
432        timers.tick();
433        assert_eq!(*count.borrow(), 1, "ready flag cleared after fire");
434    }
435
436    #[test]
437    fn remaining_exhausts_after_n_fires_auto_delete() {
438        let count: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
439        let mut timers = Timers::new();
440
441        {
442            let c = count.clone();
443            timers.add(
444                2,
445                TimerRepeat::Remaining(3),
446                Box::new(move |_ctx| *c.borrow_mut() += 1),
447            );
448        }
449
450        for _ in 0..20 {
451            timers.tick();
452        }
453
454        assert_eq!(*count.borrow(), 3, "Remaining(3) fires exactly 3 times");
455        assert!(
456            timers.is_empty(),
457            "auto_delete removes entry after exhaustion"
458        );
459    }
460
461    #[test]
462    fn remaining_exhausts_with_auto_pause() {
463        // Build an entry with auto_delete = false by going through internal API.
464        // The public API only exposes auto_delete=true; we test the internal state
465        // indirectly by checking that exhausted entries are effectively inert.
466        // Since the public API always uses auto_delete=true, this test confirms
467        // the auto_delete=true path; auto_delete=false is not reachable publicly.
468        let count: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
469        let mut timers = Timers::new();
470
471        {
472            let c = count.clone();
473            timers.add(
474                1,
475                TimerRepeat::Remaining(2),
476                Box::new(move |_ctx| *c.borrow_mut() += 1),
477            );
478        }
479
480        for _ in 0..10 {
481            timers.tick();
482        }
483
484        assert_eq!(
485            *count.borrow(),
486            2,
487            "Remaining(2) fires exactly 2 times then is removed"
488        );
489    }
490
491    #[test]
492    fn infinite_never_exhausts() {
493        let count: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
494        let mut timers = Timers::new();
495
496        {
497            let c = count.clone();
498            timers.add(
499                1,
500                TimerRepeat::Infinite,
501                Box::new(move |_ctx| *c.borrow_mut() += 1),
502            );
503        }
504
505        for _ in 0..100 {
506            timers.tick();
507        }
508
509        assert_eq!(
510            *count.borrow(),
511            100,
512            "infinite timer fires every tick (period 1)"
513        );
514        assert!(!timers.is_empty(), "infinite timer never removed");
515    }
516
517    #[test]
518    fn multiple_timers_fire_in_registration_order() {
519        let log: Rc<RefCell<Vec<u8>>> = Rc::new(RefCell::new(Vec::new()));
520        let mut timers = Timers::new();
521
522        // Both period-1 infinite; should fire A then B each tick.
523        for label in [1u8, 2u8] {
524            let l = log.clone();
525            timers.add(
526                1,
527                TimerRepeat::Infinite,
528                Box::new(move |_ctx| l.borrow_mut().push(label)),
529            );
530        }
531
532        timers.tick();
533
534        assert_eq!(
535            *log.borrow(),
536            vec![1u8, 2u8],
537            "registration order preserved"
538        );
539    }
540
541    #[test]
542    fn delete_removes_without_final_callback() {
543        let count: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
544        let mut timers = Timers::new();
545
546        let id = {
547            let c = count.clone();
548            timers.add(
549                2,
550                TimerRepeat::Infinite,
551                Box::new(move |_ctx| *c.borrow_mut() += 1),
552            )
553        };
554
555        timers.tick();
556        assert_eq!(*count.borrow(), 0);
557
558        let removed = timers.delete(id);
559        assert!(removed);
560        assert!(timers.is_empty());
561
562        timers.tick();
563        timers.tick();
564        assert_eq!(*count.borrow(), 0, "no callback after delete");
565
566        // Double-delete is safe and returns false.
567        assert!(!timers.delete(id));
568    }
569
570    #[test]
571    fn timer_context_remaining_decrements_correctly() {
572        let remaining_log: Rc<RefCell<Vec<Option<u32>>>> = Rc::new(RefCell::new(Vec::new()));
573        let mut timers = Timers::new();
574
575        {
576            let l = remaining_log.clone();
577            timers.add(
578                1,
579                TimerRepeat::Remaining(3),
580                Box::new(move |ctx| l.borrow_mut().push(ctx.remaining)),
581            );
582        }
583
584        for _ in 0..3 {
585            timers.tick();
586        }
587
588        // After fire 1: remaining = 2; fire 2: remaining = 1; fire 3: remaining = 0.
589        assert_eq!(
590            *remaining_log.borrow(),
591            vec![Some(2), Some(1), Some(0)],
592            "remaining decrements correctly in context"
593        );
594    }
595
596    #[test]
597    fn timer_context_infinite_remaining_is_none() {
598        let remaining_log: Rc<RefCell<Vec<Option<u32>>>> = Rc::new(RefCell::new(Vec::new()));
599        let mut timers = Timers::new();
600
601        {
602            let l = remaining_log.clone();
603            timers.add(
604                1,
605                TimerRepeat::Infinite,
606                Box::new(move |ctx| l.borrow_mut().push(ctx.remaining)),
607            );
608        }
609
610        for _ in 0..3 {
611            timers.tick();
612        }
613
614        assert_eq!(
615            *remaining_log.borrow(),
616            vec![None, None, None],
617            "infinite timers always report None remaining"
618        );
619    }
620}