use std::time::{Duration, Instant};
pub const DEFAULT_BLINK_PERIOD: Duration = Duration::from_millis(530);
pub const DEFAULT_TYPING_PAUSE: Duration = Duration::from_millis(500);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CaretBlinkState {
visible: bool,
paused_until: Instant,
period: Duration,
pause: Duration,
}
impl CaretBlinkState {
pub fn new(now: Instant) -> Self {
Self::with_config(now, DEFAULT_BLINK_PERIOD, DEFAULT_TYPING_PAUSE)
}
pub fn with_period(now: Instant, period: Duration) -> Self {
Self::with_config(now, period, DEFAULT_TYPING_PAUSE)
}
pub fn with_config(now: Instant, period: Duration, pause: Duration) -> Self {
Self {
visible: true,
paused_until: now,
period,
pause,
}
}
pub fn visible(&self) -> bool {
self.visible
}
pub fn period(&self) -> Duration {
self.period
}
pub fn on_activity(&mut self, now: Instant) {
self.visible = true;
self.paused_until = now + self.pause;
}
pub fn tick(&mut self, now: Instant) -> bool {
if now < self.paused_until {
return false;
}
self.visible = !self.visible;
true
}
pub fn next_wake_delay(&self, now: Instant) -> Duration {
if now < self.paused_until {
self.paused_until - now
} else {
self.period
}
}
}
impl Default for CaretBlinkState {
fn default() -> Self {
Self::new(Instant::now())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn instants(n: usize) -> Vec<Instant> {
let base = Instant::now();
(0..n)
.map(|i| base + Duration::from_secs(i as u64))
.collect()
}
#[test]
fn starts_visible_with_no_pause_active() {
let t = instants(1);
let state = CaretBlinkState::new(t[0]);
assert!(state.visible());
}
#[test]
fn tick_toggles_when_not_paused() {
let t = instants(1);
let mut state = CaretBlinkState::new(t[0]);
let period = state.period();
assert!(state.tick(t[0] + period));
assert!(!state.visible());
assert!(state.tick(t[0] + period * 2));
assert!(state.visible());
}
#[test]
fn tick_is_a_noop_inside_the_typing_pause() {
let t = instants(1);
let mut state = CaretBlinkState::with_config(
t[0],
Duration::from_millis(100),
Duration::from_millis(500),
);
state.on_activity(t[0]);
assert!(!state.tick(t[0] + Duration::from_millis(100)));
assert!(state.visible(), "caret stays solid while typing");
}
#[test]
fn on_activity_forces_visible_and_restarts_the_pause() {
let t = instants(1);
let mut state = CaretBlinkState::new(t[0]);
let period = state.period();
assert!(state.tick(t[0] + period));
assert!(!state.visible());
state.on_activity(t[0] + period);
assert!(state.visible());
assert!(!state.tick(t[0] + period + Duration::from_millis(1)));
assert!(state.visible());
}
#[test]
fn tick_resumes_once_the_pause_elapses() {
let t = instants(1);
let mut state = CaretBlinkState::new(t[0]);
let pause = Duration::from_millis(500);
state.on_activity(t[0]);
assert!(state.tick(t[0] + pause));
assert!(!state.visible());
}
#[test]
fn next_wake_delay_matches_the_pause_then_the_period() {
let t = instants(1);
let mut state = CaretBlinkState::with_config(
t[0],
Duration::from_millis(530),
Duration::from_millis(500),
);
state.on_activity(t[0]);
assert_eq!(
state.next_wake_delay(t[0] + Duration::from_millis(200)),
Duration::from_millis(300)
);
let after_pause = t[0] + Duration::from_millis(500);
assert_eq!(
state.next_wake_delay(after_pause),
Duration::from_millis(530)
);
}
#[test]
fn default_starts_visible() {
assert!(CaretBlinkState::default().visible());
}
}