use std::cell::Cell;
use web_time::{Duration, Instant};
pub const BLINK_INTERVAL_MS: u64 = 500;
thread_local! {
static CURSOR_STATE: CursorAnimationState = const { CursorAnimationState::new() };
}
pub struct CursorAnimationState {
cursor_alpha: Cell<f32>,
next_blink_time: Cell<Option<Instant>>,
}
impl CursorAnimationState {
pub const BLINK_INTERVAL: Duration = Duration::from_millis(BLINK_INTERVAL_MS);
pub const fn new() -> Self {
Self {
cursor_alpha: Cell::new(1.0),
next_blink_time: Cell::new(None),
}
}
pub fn start(&self) {
self.cursor_alpha.set(1.0);
self.next_blink_time
.set(Some(Instant::now() + Self::BLINK_INTERVAL));
}
pub fn stop(&self) {
self.cursor_alpha.set(1.0); self.next_blink_time.set(None);
}
#[cfg(test)]
pub fn is_active(&self) -> bool {
self.next_blink_time.get().is_some()
}
pub fn is_visible(&self) -> bool {
self.cursor_alpha.get() > 0.5
}
pub fn tick(&self, now: Instant) -> bool {
if let Some(next) = self.next_blink_time.get() {
if now >= next {
let new_alpha = if self.cursor_alpha.get() > 0.5 {
0.0
} else {
1.0
};
self.cursor_alpha.set(new_alpha);
self.next_blink_time.set(Some(now + Self::BLINK_INTERVAL));
return true;
}
}
false
}
pub fn next_blink_time(&self) -> Option<Instant> {
self.next_blink_time.get()
}
}
pub fn start_cursor_blink() {
CURSOR_STATE.with(|state| state.start());
}
pub fn stop_cursor_blink() {
CURSOR_STATE.with(|state| state.stop());
}
#[inline]
pub fn reset_cursor_blink() {
start_cursor_blink();
}
pub fn is_cursor_visible() -> bool {
CURSOR_STATE.with(|state| state.is_visible())
}
pub fn tick_cursor_blink() -> bool {
CURSOR_STATE.with(|state| state.tick(Instant::now()))
}
pub fn next_cursor_blink_time() -> Option<Instant> {
CURSOR_STATE.with(|state| state.next_blink_time())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cursor_starts_visible() {
let state = CursorAnimationState::new();
assert!(state.is_visible());
assert!(!state.is_active());
}
#[test]
fn start_schedules_blink() {
let state = CursorAnimationState::new();
state.start();
assert!(state.is_active());
assert!(state.next_blink_time().is_some());
}
#[test]
fn stop_clears_blink() {
let state = CursorAnimationState::new();
state.start();
state.stop();
assert!(!state.is_active());
assert!(state.next_blink_time().is_none());
assert!(state.is_visible()); }
#[test]
fn tick_toggles_visibility() {
let state = CursorAnimationState::new();
state.start();
assert!(state.is_visible());
let future_time =
Instant::now() + CursorAnimationState::BLINK_INTERVAL + Duration::from_millis(1);
let changed = state.tick(future_time);
assert!(changed);
assert!(!state.is_visible());
let future_time2 =
future_time + CursorAnimationState::BLINK_INTERVAL + Duration::from_millis(1);
let changed2 = state.tick(future_time2);
assert!(changed2);
assert!(state.is_visible()); }
}