use std::cell::{Cell, RefCell};
use std::rc::Rc;
use std::time::{Duration, Instant};
use super::animate::{arm_timer_at, cancel_timer, timer_fire_now};
use super::scheduler::request_frame;
use super::scope::Scope;
struct IntervalState {
cancelled: Cell<bool>,
pending: Cell<Option<u64>>,
}
impl IntervalState {
fn cancel(&self) {
self.cancelled.set(true);
if let Some(id) = self.pending.take() {
let _ = cancel_timer(id);
}
}
}
#[derive(Clone)]
pub struct IntervalHandle {
state: Rc<IntervalState>,
}
impl IntervalHandle {
pub fn cancel(&self) {
self.state.cancel();
}
pub fn is_cancelled(&self) -> bool {
self.state.cancelled.get()
}
}
pub fn interval(cx: Scope, period: Duration, f: impl FnMut() + 'static) -> IntervalHandle {
assert!(
!period.is_zero(),
"abstracttui reactive: interval period must be nonzero — a zero period re-arms a \
due timer every turn and spins the loop. FIX: pick a real cadence (milliseconds \
up), or use the frame-task lane (reactive::animate) for per-frame work"
);
let state = Rc::new(IntervalState {
cancelled: Cell::new(false),
pending: Cell::new(None),
});
let callback: Rc<RefCell<dyn FnMut()>> = Rc::new(RefCell::new(f));
arm(state.clone(), callback, period, Instant::now() + period);
{
let state = state.clone();
cx.on_cleanup(move || state.cancel());
}
request_frame();
IntervalHandle { state }
}
fn arm(
state: Rc<IntervalState>,
callback: Rc<RefCell<dyn FnMut()>>,
period: Duration,
deadline: Instant,
) {
let st = state.clone();
let id = arm_timer_at(deadline, move || {
if st.cancelled.get() {
return;
}
(callback.borrow_mut())();
if st.cancelled.get() {
st.pending.set(None);
return;
}
let now = timer_fire_now().unwrap_or_else(Instant::now);
arm(st.clone(), callback.clone(), period, now + period);
});
state.pending.set(Some(id));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reactive::{create_root, next_timer_deadline, run_due_timers};
fn ms(n: u64) -> Duration {
Duration::from_millis(n)
}
#[test]
fn fires_once_per_elapsed_period_with_steady_clock() {
let (root, ()) = create_root(|cx| {
let fires = Rc::new(Cell::new(0u32));
let f2 = fires.clone();
let _handle = interval(cx, ms(100), move || f2.set(f2.get() + 1));
let t0 = Instant::now();
assert_eq!(run_due_timers(t0 + ms(50)), 0, "not due yet");
assert_eq!(run_due_timers(t0 + ms(110)), 1);
assert_eq!(run_due_timers(t0 + ms(215)), 1, "re-armed from fire time");
assert_eq!(run_due_timers(t0 + ms(320)), 1);
assert_eq!(fires.get(), 3);
});
root.dispose();
}
#[test]
fn missed_ticks_coalesce_into_one_fire() {
let (root, ()) = create_root(|cx| {
let fires = Rc::new(Cell::new(0u32));
let f2 = fires.clone();
let _handle = interval(cx, ms(100), move || f2.set(f2.get() + 1));
let t0 = Instant::now();
assert_eq!(run_due_timers(t0 + ms(1050)), 1, "coalesced fire");
assert_eq!(fires.get(), 1);
assert_eq!(
run_due_timers(t0 + ms(1051)),
0,
"no storm: next fire is a full period after the last"
);
assert_eq!(run_due_timers(t0 + ms(1160)), 1, "cadence resumed");
assert_eq!(fires.get(), 2);
});
root.dispose();
}
#[test]
fn cancel_between_fires_removes_the_pending_timer_entirely() {
let (root, ()) = create_root(|cx| {
let fires = Rc::new(Cell::new(0u32));
let f2 = fires.clone();
let handle = interval(cx, ms(100), move || f2.set(f2.get() + 1));
let t0 = Instant::now();
run_due_timers(t0 + ms(110));
assert_eq!(fires.get(), 1);
handle.cancel();
assert!(handle.is_cancelled());
assert_eq!(
next_timer_deadline(),
None,
"cancel must remove the armed entry — a dead interval may \
not bound the idle sleep"
);
assert_eq!(run_due_timers(t0 + ms(10_000)), 0);
assert_eq!(fires.get(), 1);
handle.cancel(); });
root.dispose();
}
#[test]
fn cancel_from_inside_the_callback_stops_the_rearm() {
let (root, ()) = create_root(|cx| {
let fires = Rc::new(Cell::new(0u32));
let handle_slot: Rc<RefCell<Option<IntervalHandle>>> = Rc::new(RefCell::new(None));
let f2 = fires.clone();
let hs = handle_slot.clone();
let handle = interval(cx, ms(100), move || {
f2.set(f2.get() + 1);
if f2.get() == 2 {
hs.borrow().as_ref().expect("handle stored").cancel();
}
});
*handle_slot.borrow_mut() = Some(handle);
let t0 = Instant::now();
run_due_timers(t0 + ms(110));
run_due_timers(t0 + ms(220)); assert_eq!(fires.get(), 2);
assert_eq!(next_timer_deadline(), None, "no re-arm after self-cancel");
assert_eq!(run_due_timers(t0 + ms(10_000)), 0);
assert_eq!(fires.get(), 2);
});
root.dispose();
}
#[test]
fn scope_disposal_cancels_the_interval() {
let (root, ()) = create_root(|cx| {
let child = cx.child();
let fires = Rc::new(Cell::new(0u32));
let f2 = fires.clone();
let handle = interval(child, ms(100), move || f2.set(f2.get() + 1));
let t0 = Instant::now();
run_due_timers(t0 + ms(110));
assert_eq!(fires.get(), 1);
child.dispose();
assert!(handle.is_cancelled(), "disposal flips the handle state");
assert_eq!(next_timer_deadline(), None, "disposal removed the entry");
assert_eq!(run_due_timers(t0 + ms(10_000)), 0);
assert_eq!(fires.get(), 1);
});
root.dispose();
}
#[test]
fn dropping_the_handle_does_not_cancel() {
let (root, ()) = create_root(|cx| {
let fires = Rc::new(Cell::new(0u32));
let f2 = fires.clone();
drop(interval(cx, ms(100), move || f2.set(f2.get() + 1)));
let t0 = Instant::now();
run_due_timers(t0 + ms(110));
assert_eq!(
fires.get(),
1,
"the scope owns the lifetime, not the handle"
);
});
root.dispose();
}
#[test]
#[should_panic(expected = "interval period must be nonzero")]
fn zero_period_panics_loudly() {
let (_root, ()) = create_root(|cx| {
let _ = interval(cx, Duration::ZERO, || {});
});
}
}