use alloc::boxed::Box;
use alloc::vec::Vec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TimerId(u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TimerRepeat {
Infinite,
Remaining(u32),
}
#[derive(Debug, Clone, Copy)]
pub struct TimerContext {
pub id: TimerId,
pub remaining: Option<u32>,
}
struct TimerEntry {
id: TimerId,
period: u32,
countdown: u32,
repeat: TimerRepeat,
paused: bool,
ready: bool,
auto_delete: bool,
callback: Box<dyn FnMut(&TimerContext)>,
}
pub struct Timers {
entries: Vec<TimerEntry>,
next_id: u32,
}
impl Default for Timers {
fn default() -> Self {
Self::new()
}
}
impl Timers {
pub fn new() -> Self {
Self {
entries: Vec::new(),
next_id: 0,
}
}
pub fn add(
&mut self,
period: u32,
repeat: TimerRepeat,
callback: Box<dyn FnMut(&TimerContext)>,
) -> TimerId {
let id = TimerId(self.next_id);
self.next_id = self.next_id.wrapping_add(1);
self.entries.push(TimerEntry {
id,
period,
countdown: period,
repeat,
paused: false,
ready: false,
auto_delete: true,
callback,
});
id
}
pub fn add_once(&mut self, period: u32, callback: Box<dyn FnMut(&TimerContext)>) -> TimerId {
let id = TimerId(self.next_id);
self.next_id = self.next_id.wrapping_add(1);
self.entries.push(TimerEntry {
id,
period,
countdown: period,
repeat: TimerRepeat::Remaining(1),
paused: false,
ready: false,
auto_delete: true,
callback,
});
id
}
pub fn tick(&mut self) {
let mut to_delete: Vec<TimerId> = Vec::new();
for entry in &mut self.entries {
if entry.paused {
continue;
}
let should_fire = if entry.ready {
true
} else {
if entry.countdown > 0 {
entry.countdown -= 1;
}
entry.countdown == 0
};
if !should_fire {
continue;
}
let remaining_after = match entry.repeat {
TimerRepeat::Infinite => None,
TimerRepeat::Remaining(n) => {
Some(n.saturating_sub(1))
}
};
let ctx = TimerContext {
id: entry.id,
remaining: remaining_after,
};
(entry.callback)(&ctx);
entry.countdown = entry.period;
entry.ready = false;
match &mut entry.repeat {
TimerRepeat::Infinite => {}
TimerRepeat::Remaining(n) => {
*n = n.saturating_sub(1);
if *n == 0 {
if entry.auto_delete {
to_delete.push(entry.id);
} else {
entry.paused = true;
}
}
}
}
}
for id in to_delete {
self.entries.retain(|e| e.id != id);
}
}
pub fn pause(&mut self, id: TimerId) {
if let Some(entry) = self.entries.iter_mut().find(|e| e.id == id) {
entry.paused = true;
}
}
pub fn resume(&mut self, id: TimerId) {
if let Some(entry) = self.entries.iter_mut().find(|e| e.id == id) {
entry.paused = false;
}
}
pub fn delete(&mut self, id: TimerId) -> bool {
let before = self.entries.len();
self.entries.retain(|e| e.id != id);
self.entries.len() != before
}
pub fn set_ready(&mut self, id: TimerId) {
if let Some(entry) = self.entries.iter_mut().find(|e| e.id == id) {
entry.ready = true;
}
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::rc::Rc;
use alloc::vec;
use alloc::vec::Vec;
use core::cell::RefCell;
#[test]
fn deterministic_fire_sequence() {
let fired: Rc<RefCell<Vec<u32>>> = Rc::new(RefCell::new(Vec::new()));
let mut timers = Timers::new();
let tick_counter = Rc::new(RefCell::new(0u32));
{
let f = fired.clone();
let tc = tick_counter.clone();
timers.add(
5,
TimerRepeat::Infinite,
Box::new(move |_ctx| {
f.borrow_mut().push(*tc.borrow());
}),
);
}
for i in 1u32..=15 {
*tick_counter.borrow_mut() = i;
timers.tick();
}
assert_eq!(*fired.borrow(), vec![5, 10, 15]);
}
#[test]
fn one_shot_fires_once_then_removed() {
let count: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
let mut timers = Timers::new();
{
let c = count.clone();
timers.add_once(3, Box::new(move |_ctx| *c.borrow_mut() += 1));
}
for _ in 0..10 {
timers.tick();
}
assert_eq!(*count.borrow(), 1, "one-shot fires exactly once");
assert!(timers.is_empty(), "one-shot removes itself after firing");
}
#[test]
fn pause_freezes_countdown_resume_continues() {
let count: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
let mut timers = Timers::new();
let id = {
let c = count.clone();
timers.add(
4,
TimerRepeat::Infinite,
Box::new(move |_ctx| *c.borrow_mut() += 1),
)
};
timers.tick();
timers.tick();
assert_eq!(*count.borrow(), 0);
timers.pause(id);
timers.tick();
timers.tick();
timers.tick();
assert_eq!(*count.borrow(), 0, "paused timer did not fire");
timers.resume(id);
timers.tick(); assert_eq!(*count.borrow(), 0);
timers.tick(); assert_eq!(
*count.borrow(),
1,
"timer fired after resuming from frozen countdown"
);
}
#[test]
fn set_ready_fires_next_tick_then_clears() {
let count: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
let mut timers = Timers::new();
let id = {
let c = count.clone();
timers.add(
100,
TimerRepeat::Infinite,
Box::new(move |_ctx| *c.borrow_mut() += 1),
)
};
timers.tick();
assert_eq!(*count.borrow(), 0);
timers.set_ready(id);
timers.tick();
assert_eq!(*count.borrow(), 1, "ready timer fires on next tick");
timers.tick();
timers.tick();
assert_eq!(*count.borrow(), 1, "ready flag cleared after fire");
}
#[test]
fn remaining_exhausts_after_n_fires_auto_delete() {
let count: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
let mut timers = Timers::new();
{
let c = count.clone();
timers.add(
2,
TimerRepeat::Remaining(3),
Box::new(move |_ctx| *c.borrow_mut() += 1),
);
}
for _ in 0..20 {
timers.tick();
}
assert_eq!(*count.borrow(), 3, "Remaining(3) fires exactly 3 times");
assert!(
timers.is_empty(),
"auto_delete removes entry after exhaustion"
);
}
#[test]
fn remaining_exhausts_with_auto_pause() {
let count: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
let mut timers = Timers::new();
{
let c = count.clone();
timers.add(
1,
TimerRepeat::Remaining(2),
Box::new(move |_ctx| *c.borrow_mut() += 1),
);
}
for _ in 0..10 {
timers.tick();
}
assert_eq!(
*count.borrow(),
2,
"Remaining(2) fires exactly 2 times then is removed"
);
}
#[test]
fn infinite_never_exhausts() {
let count: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
let mut timers = Timers::new();
{
let c = count.clone();
timers.add(
1,
TimerRepeat::Infinite,
Box::new(move |_ctx| *c.borrow_mut() += 1),
);
}
for _ in 0..100 {
timers.tick();
}
assert_eq!(
*count.borrow(),
100,
"infinite timer fires every tick (period 1)"
);
assert!(!timers.is_empty(), "infinite timer never removed");
}
#[test]
fn multiple_timers_fire_in_registration_order() {
let log: Rc<RefCell<Vec<u8>>> = Rc::new(RefCell::new(Vec::new()));
let mut timers = Timers::new();
for label in [1u8, 2u8] {
let l = log.clone();
timers.add(
1,
TimerRepeat::Infinite,
Box::new(move |_ctx| l.borrow_mut().push(label)),
);
}
timers.tick();
assert_eq!(
*log.borrow(),
vec![1u8, 2u8],
"registration order preserved"
);
}
#[test]
fn delete_removes_without_final_callback() {
let count: Rc<RefCell<u32>> = Rc::new(RefCell::new(0));
let mut timers = Timers::new();
let id = {
let c = count.clone();
timers.add(
2,
TimerRepeat::Infinite,
Box::new(move |_ctx| *c.borrow_mut() += 1),
)
};
timers.tick();
assert_eq!(*count.borrow(), 0);
let removed = timers.delete(id);
assert!(removed);
assert!(timers.is_empty());
timers.tick();
timers.tick();
assert_eq!(*count.borrow(), 0, "no callback after delete");
assert!(!timers.delete(id));
}
#[test]
fn timer_context_remaining_decrements_correctly() {
let remaining_log: Rc<RefCell<Vec<Option<u32>>>> = Rc::new(RefCell::new(Vec::new()));
let mut timers = Timers::new();
{
let l = remaining_log.clone();
timers.add(
1,
TimerRepeat::Remaining(3),
Box::new(move |ctx| l.borrow_mut().push(ctx.remaining)),
);
}
for _ in 0..3 {
timers.tick();
}
assert_eq!(
*remaining_log.borrow(),
vec![Some(2), Some(1), Some(0)],
"remaining decrements correctly in context"
);
}
#[test]
fn timer_context_infinite_remaining_is_none() {
let remaining_log: Rc<RefCell<Vec<Option<u32>>>> = Rc::new(RefCell::new(Vec::new()));
let mut timers = Timers::new();
{
let l = remaining_log.clone();
timers.add(
1,
TimerRepeat::Infinite,
Box::new(move |ctx| l.borrow_mut().push(ctx.remaining)),
);
}
for _ in 0..3 {
timers.tick();
}
assert_eq!(
*remaining_log.borrow(),
vec![None, None, None],
"infinite timers always report None remaining"
);
}
}