#![allow(dead_code)]
use std::cell::Cell;
use std::collections::VecDeque;
use std::time::{Duration, Instant};
use crate::TuiDom;
thread_local! {
static CURRENT_SCHEDULER: Cell<*mut Scheduler> = const { Cell::new(std::ptr::null_mut()) };
}
pub(crate) struct SchedulerGuard {
previous: *mut Scheduler,
}
impl SchedulerGuard {
pub(crate) fn install(scheduler: &mut Scheduler) -> Self {
let ptr = scheduler as *mut Scheduler;
let previous = CURRENT_SCHEDULER.with(|s| {
let prev = s.get();
s.set(ptr);
prev
});
SchedulerGuard { previous }
}
}
impl Drop for SchedulerGuard {
fn drop(&mut self) {
CURRENT_SCHEDULER.with(|s| s.set(self.previous));
}
}
fn with_current<F, R>(f: F) -> Option<R>
where
F: FnOnce(&mut Scheduler) -> R,
{
CURRENT_SCHEDULER.with(|s| {
let ptr = s.get();
if ptr.is_null() {
None
} else {
Some(f(unsafe { &mut *ptr }))
}
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TimerId(pub(crate) u32);
impl TimerId {
pub const NONE: TimerId = TimerId(0);
pub fn raw(self) -> u32 {
self.0
}
}
pub struct TimerCtx<'a> {
pub dom: &'a mut TuiDom,
scheduler: &'a mut Scheduler,
}
impl<'a> TimerCtx<'a> {
pub(crate) fn new(dom: &'a mut TuiDom, scheduler: &'a mut Scheduler) -> Self {
Self { dom, scheduler }
}
pub fn set_timeout(
&mut self,
callback: impl FnOnce(&mut TimerCtx<'_>) + 'static,
delay_ms: u32,
) -> TimerId {
self.scheduler.set_timeout(callback, delay_ms)
}
pub fn clear_timeout(&mut self, id: TimerId) {
self.scheduler.clear_timeout(id);
}
pub fn set_interval(
&mut self,
callback: impl FnMut(&mut TimerCtx<'_>) -> bool + 'static,
period_ms: u32,
) -> TimerId {
self.scheduler.set_interval(callback, period_ms)
}
pub fn clear_interval(&mut self, id: TimerId) {
self.scheduler.clear_interval(id);
}
pub fn request_animation_frame(
&mut self,
callback: impl FnOnce(&mut TimerCtx<'_>, f64) + 'static,
) -> TimerId {
self.scheduler.request_animation_frame(callback)
}
pub fn cancel_animation_frame(&mut self, id: TimerId) {
self.scheduler.cancel_animation_frame(id);
}
pub fn queue_microtask(&mut self, callback: impl FnOnce(&mut TimerCtx<'_>) + 'static) {
self.scheduler.queue_microtask(callback);
}
}
type OneShotCb = Box<dyn FnOnce(&mut TimerCtx<'_>) + 'static>;
type IntervalCb = Box<dyn FnMut(&mut TimerCtx<'_>) -> bool + 'static>;
type RafCb = Box<dyn FnOnce(&mut TimerCtx<'_>, f64) + 'static>;
struct TimeoutEntry {
id: TimerId,
fires_at: Instant,
callback: OneShotCb,
}
struct IntervalEntry {
id: TimerId,
period: Duration,
next_fire: Instant,
callback: IntervalCb,
}
struct RafEntry {
id: TimerId,
callback: RafCb,
}
struct MicrotaskEntry {
callback: OneShotCb,
}
pub(crate) struct Scheduler {
next_id: u32,
now: Instant,
app_start: Instant,
timeouts: Vec<TimeoutEntry>,
intervals: Vec<IntervalEntry>,
raf: Vec<RafEntry>,
microtasks: VecDeque<MicrotaskEntry>,
}
impl Scheduler {
pub(crate) fn new(start: Instant) -> Self {
Self {
next_id: 1, now: start,
app_start: start,
timeouts: Vec::new(),
intervals: Vec::new(),
raf: Vec::new(),
microtasks: VecDeque::new(),
}
}
pub(crate) fn frame_timestamp_ms(&self) -> f64 {
self.now
.saturating_duration_since(self.app_start)
.as_secs_f64()
* 1000.0
}
fn alloc_id(&mut self) -> TimerId {
let id = TimerId(self.next_id);
self.next_id = self.next_id.checked_add(1).unwrap_or(1);
id
}
pub(crate) fn now(&self) -> Instant {
self.now
}
pub(crate) fn set_now(&mut self, now: Instant) {
if now > self.now {
self.now = now;
}
}
pub fn set_timeout(
&mut self,
callback: impl FnOnce(&mut TimerCtx<'_>) + 'static,
delay_ms: u32,
) -> TimerId {
let id = self.alloc_id();
let fires_at = self.now + Duration::from_millis(delay_ms as u64);
self.timeouts.push(TimeoutEntry {
id,
fires_at,
callback: Box::new(callback),
});
id
}
pub fn clear_timeout(&mut self, id: TimerId) {
self.timeouts.retain(|e| e.id != id);
}
pub fn set_interval(
&mut self,
callback: impl FnMut(&mut TimerCtx<'_>) -> bool + 'static,
period_ms: u32,
) -> TimerId {
let id = self.alloc_id();
let period = Duration::from_millis(period_ms as u64);
self.intervals.push(IntervalEntry {
id,
period,
next_fire: self.now + period,
callback: Box::new(callback),
});
id
}
pub fn clear_interval(&mut self, id: TimerId) {
self.intervals.retain(|e| e.id != id);
}
pub fn request_animation_frame(
&mut self,
callback: impl FnOnce(&mut TimerCtx<'_>, f64) + 'static,
) -> TimerId {
let id = self.alloc_id();
self.raf.push(RafEntry {
id,
callback: Box::new(callback),
});
id
}
pub fn cancel_animation_frame(&mut self, id: TimerId) {
self.raf.retain(|e| e.id != id);
}
pub fn queue_microtask(&mut self, callback: impl FnOnce(&mut TimerCtx<'_>) + 'static) {
self.microtasks.push_back(MicrotaskEntry {
callback: Box::new(callback),
});
}
pub(crate) fn next_deadline(&self) -> Option<Instant> {
let timeout = self.timeouts.iter().map(|e| e.fires_at).min();
let interval = self.intervals.iter().map(|e| e.next_fire).min();
match (timeout, interval) {
(Some(a), Some(b)) => Some(a.min(b)),
(a, b) => a.or(b),
}
}
pub(crate) fn has_active_raf(&self) -> bool {
!self.raf.is_empty()
}
pub(crate) fn drain_expired_timeouts(&mut self) -> Vec<OneShotCb> {
let mut expired_idx: Vec<usize> = self
.timeouts
.iter()
.enumerate()
.filter_map(|(i, e)| {
if e.fires_at <= self.now {
Some(i)
} else {
None
}
})
.collect();
expired_idx.sort_by_key(|&i| self.timeouts[i].fires_at);
let mut out = Vec::with_capacity(expired_idx.len());
for &i in expired_idx.iter().rev() {
out.push(self.timeouts.remove(i).callback);
}
out.reverse(); out
}
pub(crate) fn drain_expired_interval_ids(&self) -> Vec<TimerId> {
let mut due: Vec<(TimerId, Instant)> = self
.intervals
.iter()
.filter(|e| e.next_fire <= self.now)
.map(|e| (e.id, e.next_fire))
.collect();
due.sort_by_key(|(_, t)| *t);
due.into_iter().map(|(id, _)| id).collect()
}
pub(crate) fn drain_raf(&mut self) -> Vec<RafCb> {
std::mem::take(&mut self.raf)
.into_iter()
.map(|e| e.callback)
.collect()
}
pub(crate) fn pop_microtask(&mut self) -> Option<OneShotCb> {
self.microtasks.pop_front().map(|e| e.callback)
}
}
pub(crate) fn pump_intervals(scheduler: &mut Scheduler, dom: &mut TuiDom, expired: &[TimerId]) {
for &id in expired {
let pos = scheduler.intervals.iter().position(|e| e.id == id);
let Some(pos) = pos else { continue };
let mut entry = scheduler.intervals.swap_remove(pos);
let keep = {
let mut ctx = TimerCtx::new(dom, scheduler);
(entry.callback)(&mut ctx)
};
if keep {
entry.next_fire += entry.period;
scheduler.intervals.push(entry);
}
}
}
pub(crate) fn pump_timeouts(scheduler: &mut Scheduler, dom: &mut TuiDom) {
let cbs = scheduler.drain_expired_timeouts();
for cb in cbs {
let mut ctx = TimerCtx::new(dom, scheduler);
cb(&mut ctx);
}
}
pub(crate) fn pump_raf(scheduler: &mut Scheduler, dom: &mut TuiDom) {
let timestamp = scheduler.frame_timestamp_ms();
let cbs = scheduler.drain_raf();
for cb in cbs {
let mut ctx = TimerCtx::new(dom, scheduler);
cb(&mut ctx, timestamp);
}
}
pub trait TuiTimers {
fn set_timeout(
&mut self,
callback: impl FnOnce(&mut TimerCtx<'_>) + 'static,
delay_ms: u32,
) -> TimerId;
fn clear_timeout(&mut self, id: TimerId);
fn set_interval(
&mut self,
callback: impl FnMut(&mut TimerCtx<'_>) -> bool + 'static,
period_ms: u32,
) -> TimerId;
fn clear_interval(&mut self, id: TimerId);
fn request_animation_frame(
&mut self,
callback: impl FnOnce(&mut TimerCtx<'_>, f64) + 'static,
) -> TimerId;
fn cancel_animation_frame(&mut self, id: TimerId);
fn queue_microtask(&mut self, callback: impl FnOnce(&mut TimerCtx<'_>) + 'static);
}
impl<'a> TuiTimers for crate::TuiEventCtx<'a> {
fn set_timeout(
&mut self,
callback: impl FnOnce(&mut TimerCtx<'_>) + 'static,
delay_ms: u32,
) -> TimerId {
with_current(|s| s.set_timeout(callback, delay_ms))
.expect("set_timeout called outside event dispatch")
}
fn clear_timeout(&mut self, id: TimerId) {
with_current(|s| s.clear_timeout(id));
}
fn set_interval(
&mut self,
callback: impl FnMut(&mut TimerCtx<'_>) -> bool + 'static,
period_ms: u32,
) -> TimerId {
with_current(|s| s.set_interval(callback, period_ms))
.expect("set_interval called outside event dispatch")
}
fn clear_interval(&mut self, id: TimerId) {
with_current(|s| s.clear_interval(id));
}
fn request_animation_frame(
&mut self,
callback: impl FnOnce(&mut TimerCtx<'_>, f64) + 'static,
) -> TimerId {
with_current(|s| s.request_animation_frame(callback))
.expect("request_animation_frame called outside event dispatch")
}
fn cancel_animation_frame(&mut self, id: TimerId) {
with_current(|s| s.cancel_animation_frame(id));
}
fn queue_microtask(&mut self, callback: impl FnOnce(&mut TimerCtx<'_>) + 'static) {
with_current(|s| s.queue_microtask(callback));
}
}
pub(crate) fn drain_microtasks(scheduler: &mut Scheduler, dom: &mut TuiDom) {
while let Some(cb) = scheduler.pop_microtask() {
let mut ctx = TimerCtx::new(dom, scheduler);
cb(&mut ctx);
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::Cell;
use std::rc::Rc;
fn epoch() -> Instant {
Instant::now()
}
fn dom_for_tests() -> TuiDom {
TuiDom::new()
}
#[test]
fn timeout_fires_after_delay() {
let start = epoch();
let mut sched = Scheduler::new(start);
let mut dom = dom_for_tests();
let fired = Rc::new(Cell::new(0u32));
let f = fired.clone();
sched.set_timeout(move |_ctx| f.set(f.get() + 1), 100);
sched.set_now(start + Duration::from_millis(50));
pump_timeouts(&mut sched, &mut dom);
assert_eq!(fired.get(), 0);
sched.set_now(start + Duration::from_millis(150));
pump_timeouts(&mut sched, &mut dom);
assert_eq!(fired.get(), 1);
sched.set_now(start + Duration::from_millis(300));
pump_timeouts(&mut sched, &mut dom);
assert_eq!(fired.get(), 1);
}
#[test]
fn clear_timeout_cancels_before_deadline() {
let start = epoch();
let mut sched = Scheduler::new(start);
let mut dom = dom_for_tests();
let fired = Rc::new(Cell::new(0u32));
let f = fired.clone();
let id = sched.set_timeout(move |_| f.set(f.get() + 1), 100);
sched.clear_timeout(id);
sched.set_now(start + Duration::from_millis(200));
pump_timeouts(&mut sched, &mut dom);
assert_eq!(fired.get(), 0);
}
#[test]
fn interval_repeats_at_period() {
let start = epoch();
let mut sched = Scheduler::new(start);
let mut dom = dom_for_tests();
let count = Rc::new(Cell::new(0u32));
let c = count.clone();
sched.set_interval(
move |_| {
c.set(c.get() + 1);
true
},
50,
);
sched.set_now(start + Duration::from_millis(175));
let due = sched.drain_expired_interval_ids();
pump_intervals(&mut sched, &mut dom, &due);
while !sched.drain_expired_interval_ids().is_empty() {
let due = sched.drain_expired_interval_ids();
pump_intervals(&mut sched, &mut dom, &due);
}
assert_eq!(count.get(), 3);
}
#[test]
fn interval_self_cancels_on_false_return() {
let start = epoch();
let mut sched = Scheduler::new(start);
let mut dom = dom_for_tests();
let count = Rc::new(Cell::new(0u32));
let c = count.clone();
sched.set_interval(
move |_| {
c.set(c.get() + 1);
c.get() < 2
},
50,
);
sched.set_now(start + Duration::from_millis(500));
loop {
let due = sched.drain_expired_interval_ids();
if due.is_empty() {
break;
}
pump_intervals(&mut sched, &mut dom, &due);
}
assert_eq!(count.get(), 2);
assert!(sched.intervals.is_empty());
}
#[test]
fn clear_timeout_on_stale_handle_is_noop() {
let start = epoch();
let mut sched = Scheduler::new(start);
let mut dom = dom_for_tests();
let fired = Rc::new(Cell::new(false));
let f = fired.clone();
let id = sched.set_timeout(move |_| f.set(true), 50);
sched.set_now(start + Duration::from_millis(100));
pump_timeouts(&mut sched, &mut dom);
assert!(fired.get());
sched.clear_timeout(id);
sched.clear_timeout(TimerId(99999));
}
#[test]
fn raf_fires_once_per_drain() {
let start = epoch();
let mut sched = Scheduler::new(start);
let mut dom = dom_for_tests();
let fired = Rc::new(Cell::new(0u32));
let f = fired.clone();
sched.request_animation_frame(move |_, _ts| f.set(f.get() + 1));
pump_raf(&mut sched, &mut dom);
assert_eq!(fired.get(), 1);
pump_raf(&mut sched, &mut dom);
assert_eq!(fired.get(), 1);
}
#[test]
fn microtasks_drain_in_fifo_order_including_late_queues() {
let start = epoch();
let mut sched = Scheduler::new(start);
let mut dom = dom_for_tests();
let order = Rc::new(std::cell::RefCell::new(Vec::<u32>::new()));
let o = order.clone();
sched.queue_microtask(move |ctx| {
o.borrow_mut().push(1);
let o2 = o.clone();
ctx.queue_microtask(move |_| o2.borrow_mut().push(3));
});
let o = order.clone();
sched.queue_microtask(move |_| o.borrow_mut().push(2));
drain_microtasks(&mut sched, &mut dom);
assert_eq!(*order.borrow(), vec![1, 2, 3]);
}
#[test]
fn handles_are_unique_and_monotonic() {
let start = epoch();
let mut sched = Scheduler::new(start);
let a = sched.set_timeout(|_| {}, 100);
let b = sched.set_timeout(|_| {}, 100);
let c = sched.request_animation_frame(|_, _ts| {});
let d = sched.set_interval(|_| true, 50);
assert_ne!(a, b);
assert_ne!(b, c);
assert_ne!(c, d);
assert_eq!(a.raw(), 1);
assert!(b.raw() > a.raw());
assert!(c.raw() > b.raw());
assert!(d.raw() > c.raw());
}
#[test]
fn listener_can_call_set_timeout_via_extension_trait() {
use rdom_core::ListenerOptions;
let mut dom = TuiDom::new();
let root = dom.root();
let div = dom.create_element("div");
dom.append_child(root, div).unwrap();
let fired = Rc::new(Cell::new(0u32));
let f = fired.clone();
dom.add_event_listener(div, "click", ListenerOptions::default(), move |ctx| {
let f2 = f.clone();
ctx.set_timeout(move |_| f2.set(f2.get() + 1), 100);
})
.unwrap();
let start = epoch();
let mut sched = Scheduler::new(start);
let _g = SchedulerGuard::install(&mut sched);
let mut ev = rdom_core::Event::new("click");
let _ = dom.dispatch_event(div, &mut ev);
drop(_g);
sched.set_now(start + Duration::from_millis(50));
pump_timeouts(&mut sched, &mut dom);
assert_eq!(fired.get(), 0);
sched.set_now(start + Duration::from_millis(200));
pump_timeouts(&mut sched, &mut dom);
assert_eq!(fired.get(), 1);
}
#[test]
fn scheduler_guard_restores_previous_on_drop() {
let start = epoch();
let mut a = Scheduler::new(start);
let mut b = Scheduler::new(start);
let _outer = SchedulerGuard::install(&mut a);
{
let _inner = SchedulerGuard::install(&mut b);
let count_b = with_current(|s| s.next_id);
assert_eq!(count_b, Some(1));
}
let count_a = with_current(|s| s.next_id);
assert_eq!(count_a, Some(1));
}
#[test]
fn raf_callback_receives_timestamp_zero_at_app_start() {
let start = epoch();
let mut sched = Scheduler::new(start);
let mut dom = dom_for_tests();
let observed = Rc::new(Cell::new(-1.0_f64));
let o = observed.clone();
sched.request_animation_frame(move |_ctx, ts| o.set(ts));
pump_raf(&mut sched, &mut dom);
assert_eq!(observed.get(), 0.0);
}
#[test]
fn raf_callback_timestamp_reflects_scheduler_clock() {
let start = epoch();
let mut sched = Scheduler::new(start);
let mut dom = dom_for_tests();
let observed = Rc::new(Cell::new(-1.0_f64));
let o = observed.clone();
sched.request_animation_frame(move |_ctx, ts| o.set(ts));
sched.set_now(start + Duration::from_millis(16));
pump_raf(&mut sched, &mut dom);
assert_eq!(observed.get(), 16.0);
}
#[test]
fn raf_timestamps_monotonic_across_ticks() {
let start = epoch();
let mut sched = Scheduler::new(start);
let mut dom = dom_for_tests();
let stamps = Rc::new(std::cell::RefCell::new(Vec::<f64>::new()));
let s = stamps.clone();
sched.request_animation_frame(move |_ctx, ts| s.borrow_mut().push(ts));
sched.set_now(start + Duration::from_millis(16));
pump_raf(&mut sched, &mut dom);
let s = stamps.clone();
sched.request_animation_frame(move |_ctx, ts| s.borrow_mut().push(ts));
sched.set_now(start + Duration::from_millis(33));
pump_raf(&mut sched, &mut dom);
let s = stamps.clone();
sched.request_animation_frame(move |_ctx, ts| s.borrow_mut().push(ts));
sched.set_now(start + Duration::from_millis(50));
pump_raf(&mut sched, &mut dom);
let captured = stamps.borrow().clone();
assert_eq!(captured.len(), 3);
assert!(captured[0] <= captured[1]);
assert!(captured[1] <= captured[2]);
assert_eq!(captured, vec![16.0, 33.0, 50.0]);
}
#[test]
fn raf_timestamps_coherent_within_one_tick() {
let start = epoch();
let mut sched = Scheduler::new(start);
let mut dom = dom_for_tests();
let stamps = Rc::new(std::cell::RefCell::new(Vec::<f64>::new()));
let s1 = stamps.clone();
sched.request_animation_frame(move |_ctx, ts| s1.borrow_mut().push(ts));
let s2 = stamps.clone();
sched.request_animation_frame(move |_ctx, ts| s2.borrow_mut().push(ts));
sched.set_now(start + Duration::from_millis(16));
pump_raf(&mut sched, &mut dom);
let captured = stamps.borrow().clone();
assert_eq!(captured.len(), 2);
assert_eq!(captured[0], 16.0);
assert_eq!(captured[1], 16.0);
}
#[test]
fn next_deadline_returns_shortest_pending() {
let start = epoch();
let mut sched = Scheduler::new(start);
assert_eq!(sched.next_deadline(), None);
sched.set_timeout(|_| {}, 200);
sched.set_timeout(|_| {}, 50); sched.set_timeout(|_| {}, 500);
assert_eq!(
sched.next_deadline(),
Some(start + Duration::from_millis(50))
);
sched.set_interval(|_| true, 10);
assert_eq!(
sched.next_deadline(),
Some(start + Duration::from_millis(10))
);
}
}