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}