use std::cell::Cell;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use web_time::Instant;
#[cfg(debug_assertions)]
std::thread_local! {
static DRAW_TRACE: std::cell::RefCell<Vec<&'static str>> =
const { std::cell::RefCell::new(Vec::new()) };
}
#[cfg(debug_assertions)]
const DRAW_TRACE_CAP: usize = 512;
#[cfg(debug_assertions)]
fn record_draw_trace(reason: &'static str) {
DRAW_TRACE.with(|t| {
let mut t = t.borrow_mut();
if t.len() >= DRAW_TRACE_CAP {
t.remove(0);
}
t.push(reason);
});
}
#[cfg(not(debug_assertions))]
#[inline(always)]
fn record_draw_trace(_reason: &'static str) {}
#[doc(hidden)]
pub fn drain_draw_trace() -> Vec<&'static str> {
#[cfg(debug_assertions)]
{
DRAW_TRACE.with(|t| std::mem::take(&mut *t.borrow_mut()))
}
#[cfg(not(debug_assertions))]
{
Vec::new()
}
}
pub fn request_draw_tagged(reason: &'static str) {
record_draw_trace(reason);
request_draw();
}
pub fn request_draw_after_tagged(delay: Duration, reason: &'static str) {
record_draw_trace(reason);
request_draw_after(delay);
}
std::thread_local! {
static NEEDS_DRAW: Cell<bool> = Cell::new(false);
static NEXT_DRAW_AT: Cell<Option<Instant>> = Cell::new(None);
static INVALIDATION_EPOCH: Cell<u64> = Cell::new(0);
static ASYNC_STATE_EPOCH: Cell<u64> = Cell::new(0);
static LAST_SEEN_ASYNC_WAKEUP: Cell<u64> = Cell::new(0);
static POINTER_PRESS_EPOCH: Cell<u64> = Cell::new(0);
}
pub fn bump_pointer_press_epoch() {
POINTER_PRESS_EPOCH.with(|c| c.set(c.get().wrapping_add(1)));
}
pub fn pointer_press_epoch() -> u64 {
POINTER_PRESS_EPOCH.with(|c| c.get())
}
static ASYNC_WAKEUP_COUNTER: AtomicU64 = AtomicU64::new(0);
type HostWaker = Arc<dyn Fn() + Send + Sync>;
static HOST_WAKER: Mutex<Option<HostWaker>> = Mutex::new(None);
pub fn set_host_waker(waker: impl Fn() + Send + Sync + 'static) {
let waker: HostWaker = Arc::new(waker);
match HOST_WAKER.lock() {
Ok(mut slot) => *slot = Some(waker),
Err(poisoned) => *poisoned.into_inner() = Some(waker),
}
}
pub fn clear_host_waker() {
match HOST_WAKER.lock() {
Ok(mut slot) => *slot = None,
Err(poisoned) => *poisoned.into_inner() = None,
}
}
fn host_waker() -> Option<HostWaker> {
match HOST_WAKER.lock() {
Ok(slot) => slot.clone(),
Err(poisoned) => poisoned.into_inner().clone(),
}
}
fn pump_async_wakeup() {
let current = ASYNC_WAKEUP_COUNTER.load(Ordering::Acquire);
let changed = LAST_SEEN_ASYNC_WAKEUP.with(|c| {
let prev = c.get();
if prev == current {
false
} else {
c.set(current);
true
}
});
if changed {
NEEDS_DRAW.with(|c| c.set(true));
INVALIDATION_EPOCH.with(|c| c.set(c.get().wrapping_add(1)));
ASYNC_STATE_EPOCH.with(|c| c.set(c.get().wrapping_add(1)));
}
}
pub fn request_draw() {
NEEDS_DRAW.with(|c| c.set(true));
INVALIDATION_EPOCH.with(|c| c.set(c.get().wrapping_add(1)));
}
pub fn request_draw_without_invalidation() {
NEEDS_DRAW.with(|c| c.set(true));
}
pub fn wants_draw() -> bool {
pump_async_wakeup();
if NEEDS_DRAW.with(|c| c.get()) {
return true;
}
let due = NEXT_DRAW_AT.with(|c| match c.get() {
Some(when) if Instant::now() >= when => {
c.set(None);
true
}
_ => false,
});
if due {
NEEDS_DRAW.with(|c| c.set(true));
}
due
}
pub fn invalidation_epoch() -> u64 {
pump_async_wakeup();
INVALIDATION_EPOCH.with(|c| c.get())
}
pub fn signal_async_state_change() {
ASYNC_WAKEUP_COUNTER.fetch_add(1, Ordering::AcqRel);
NEEDS_DRAW.with(|c| c.set(true));
INVALIDATION_EPOCH.with(|c| c.set(c.get().wrapping_add(1)));
ASYNC_STATE_EPOCH.with(|c| c.set(c.get().wrapping_add(1)));
if let Some(waker) = host_waker() {
waker();
}
}
pub fn async_state_epoch() -> u64 {
pump_async_wakeup();
ASYNC_STATE_EPOCH.with(|c| c.get())
}
pub fn clear_draw_request() {
NEEDS_DRAW.with(|c| c.set(false));
NEXT_DRAW_AT.with(|c| c.set(None));
let current = ASYNC_WAKEUP_COUNTER.load(Ordering::Acquire);
LAST_SEEN_ASYNC_WAKEUP.with(|c| c.set(current));
}
pub fn request_draw_after(delay: Duration) {
let when = Instant::now() + delay;
NEXT_DRAW_AT.with(|c| match c.get() {
Some(existing) if existing <= when => {}
_ => c.set(Some(when)),
});
}
#[doc(hidden)]
pub fn peek_draw_signals() -> (bool, Option<Instant>) {
let flag = NEEDS_DRAW.with(|c| c.get());
let deadline = NEXT_DRAW_AT.with(|c| c.get());
(flag, deadline)
}
pub fn peek_next_draw_deadline() -> Option<Instant> {
NEXT_DRAW_AT.with(|c| c.get())
}
#[derive(Clone, Copy)]
pub struct Tween {
current: f64,
start_value: f64,
target: f64,
start_time: Option<Instant>,
duration: f64,
}
impl Tween {
pub const fn new(initial: f64, duration_secs: f64) -> Self {
Self {
current: initial,
start_value: initial,
target: initial,
start_time: None,
duration: duration_secs,
}
}
pub fn set_target(&mut self, new_target: f64) {
if (self.target - new_target).abs() > 1e-9 {
self.start_value = self.current;
self.target = new_target;
self.start_time = Some(Instant::now());
}
}
pub fn tick(&mut self) -> f64 {
if let Some(start) = self.start_time {
let elapsed = start.elapsed().as_secs_f64();
let p = (elapsed / self.duration).min(1.0);
let eased = 1.0 - (1.0 - p).powi(3);
self.current = self.start_value + (self.target - self.start_value) * eased;
if p >= 1.0 {
self.current = self.target;
self.start_time = None;
} else {
request_draw();
}
}
self.current
}
pub fn value(&self) -> f64 {
self.current
}
pub fn target(&self) -> f64 {
self.target
}
pub fn is_animating(&self) -> bool {
self.start_time.is_some()
}
}
impl Default for Tween {
fn default() -> Self {
Self::new(0.0, 0.12)
}
}
#[cfg(test)]
mod host_waker_tests {
use super::*;
use std::sync::atomic::AtomicU64;
static SERIAL: Mutex<()> = Mutex::new(());
fn serial() -> std::sync::MutexGuard<'static, ()> {
SERIAL.lock().unwrap_or_else(|p| p.into_inner())
}
#[test]
fn waker_fires_on_signal() {
let _guard = serial();
let fires = Arc::new(AtomicU64::new(0));
let seen = Arc::clone(&fires);
set_host_waker(move || {
seen.fetch_add(1, Ordering::AcqRel);
});
signal_async_state_change();
clear_host_waker();
assert!(
fires.load(Ordering::Acquire) >= 1,
"installed waker must run when any thread signals an async change"
);
}
#[test]
fn waker_sees_counter_already_bumped() {
let _guard = serial();
let before = ASYNC_WAKEUP_COUNTER.load(Ordering::Acquire);
let observed = Arc::new(AtomicU64::new(0));
let sink = Arc::clone(&observed);
set_host_waker(move || {
sink.store(
ASYNC_WAKEUP_COUNTER.load(Ordering::Acquire),
Ordering::Release,
);
});
signal_async_state_change();
clear_host_waker();
assert!(
observed.load(Ordering::Acquire) > before,
"the counter must be bumped BEFORE the waker runs"
);
}
#[test]
fn replacing_the_waker_retires_the_old_one() {
let _guard = serial();
let old_fires = Arc::new(AtomicU64::new(0));
let old_sink = Arc::clone(&old_fires);
set_host_waker(move || {
old_sink.fetch_add(1, Ordering::AcqRel);
});
let new_fires = Arc::new(AtomicU64::new(0));
let new_sink = Arc::clone(&new_fires);
set_host_waker(move || {
new_sink.fetch_add(1, Ordering::AcqRel);
});
let old_baseline = old_fires.load(Ordering::Acquire);
signal_async_state_change();
clear_host_waker();
assert!(
new_fires.load(Ordering::Acquire) >= 1,
"the replacement waker runs"
);
assert_eq!(
old_fires.load(Ordering::Acquire),
old_baseline,
"the replaced waker no longer runs"
);
}
#[test]
fn clear_stops_the_waker() {
let _guard = serial();
let fires = Arc::new(AtomicU64::new(0));
let sink = Arc::clone(&fires);
set_host_waker(move || {
sink.fetch_add(1, Ordering::AcqRel);
});
clear_host_waker();
let baseline = fires.load(Ordering::Acquire);
signal_async_state_change();
assert_eq!(
fires.load(Ordering::Acquire),
baseline,
"a cleared waker must not run again"
);
}
#[test]
fn signal_from_a_worker_thread_wakes_the_host() {
let _guard = serial();
let fires = Arc::new(AtomicU64::new(0));
let sink = Arc::clone(&fires);
set_host_waker(move || {
sink.fetch_add(1, Ordering::AcqRel);
});
std::thread::spawn(signal_async_state_change)
.join()
.expect("worker thread signalled without panicking");
clear_host_waker();
assert!(
fires.load(Ordering::Acquire) >= 1,
"a worker-thread signal must reach the host waker"
);
}
#[test]
fn signalling_without_a_waker_is_a_no_op() {
let _guard = serial();
clear_host_waker();
let before = ASYNC_WAKEUP_COUNTER.load(Ordering::Acquire);
signal_async_state_change();
assert!(
ASYNC_WAKEUP_COUNTER.load(Ordering::Acquire) > before,
"counter-only behaviour is unchanged when no waker is installed"
);
}
}
#[cfg(test)]
mod scheduled_draw_tests {
use super::*;
use std::thread::sleep;
#[test]
fn peek_is_non_destructive() {
clear_draw_request();
request_draw_after(Duration::from_millis(50));
let first = peek_next_draw_deadline();
assert!(first.is_some(), "first peek sees the pending deadline");
let second = peek_next_draw_deadline();
assert_eq!(
first, second,
"second peek still sees the SAME pending deadline (lost-wakeup fix)"
);
}
#[test]
fn due_deadline_surfaces_then_clears() {
clear_draw_request();
assert!(!wants_draw(), "baseline: nothing pending after clear");
request_draw_after(Duration::from_millis(20));
sleep(Duration::from_millis(40));
assert!(wants_draw(), "a due deadline makes wants_draw() true");
clear_draw_request();
assert!(
!wants_draw(),
"without a re-arm the loop goes idle again after the draw"
);
}
#[test]
fn future_deadline_peeks_but_does_not_want_draw() {
clear_draw_request();
request_draw_after(Duration::from_millis(500));
assert!(
peek_next_draw_deadline().is_some(),
"future deadline is visible to the host's WaitUntil"
);
assert!(
!wants_draw(),
"a future deadline must not force continuous polling"
);
assert!(
peek_next_draw_deadline().is_some(),
"a non-due wants_draw() must not consume the deadline"
);
}
#[test]
fn earliest_deadline_wins() {
clear_draw_request();
request_draw_after(Duration::from_millis(400));
let after_long = peek_next_draw_deadline().expect("long deadline armed");
request_draw_after(Duration::from_millis(20));
let after_short = peek_next_draw_deadline().expect("short deadline armed");
assert!(
after_short < after_long,
"a nearer deadline replaces a farther one"
);
request_draw_after(Duration::from_millis(400));
assert_eq!(
peek_next_draw_deadline(),
Some(after_short),
"arming a farther deadline keeps the earliest"
);
}
}