use std::time::{Duration, Instant};
use crate::common::Direction;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum EdgeScrollState {
Idle,
ArmedInitial,
ArmedRepeat,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct EdgeScrollTimings {
pub initial_delay: Duration,
pub repeat_interval: Duration,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum EdgeScrollAction {
Scroll(Direction),
Arm(Instant),
Cancel,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct EdgeScrollScheduler {
state: EdgeScrollState,
direction: Option<Direction>,
}
impl Default for EdgeScrollScheduler {
fn default() -> Self {
Self::new()
}
}
impl EdgeScrollScheduler {
pub(super) fn new() -> Self {
Self {
state: EdgeScrollState::Idle,
direction: None,
}
}
#[cfg(test)]
pub(super) fn state(&self) -> EdgeScrollState {
self.state
}
pub(super) fn on_enter(&mut self, direction: Direction) -> EdgeScrollAction {
self.state = EdgeScrollState::Idle;
self.direction = Some(direction);
EdgeScrollAction::Scroll(direction)
}
pub(super) fn on_scroll_outcome(
&mut self,
scrolled: bool,
now: Instant,
timings: &EdgeScrollTimings,
) -> EdgeScrollAction {
match self.state {
EdgeScrollState::Idle => {
if scrolled {
self.state = EdgeScrollState::ArmedInitial;
EdgeScrollAction::Arm(now + timings.initial_delay)
} else {
self.direction = None;
EdgeScrollAction::Cancel
}
}
EdgeScrollState::ArmedInitial | EdgeScrollState::ArmedRepeat => {
if scrolled {
self.state = EdgeScrollState::ArmedRepeat;
EdgeScrollAction::Arm(now + timings.repeat_interval)
} else {
self.state = EdgeScrollState::Idle;
self.direction = None;
EdgeScrollAction::Cancel
}
}
}
}
pub(super) fn on_timer_fired(&mut self) -> Option<EdgeScrollAction> {
match self.state {
EdgeScrollState::ArmedInitial | EdgeScrollState::ArmedRepeat => {
self.direction.map(EdgeScrollAction::Scroll)
}
EdgeScrollState::Idle => None,
}
}
pub(super) fn on_leave(&mut self) -> EdgeScrollAction {
self.reset();
EdgeScrollAction::Cancel
}
pub(super) fn on_drag_end(&mut self) -> EdgeScrollAction {
self.reset();
EdgeScrollAction::Cancel
}
fn reset(&mut self) {
self.state = EdgeScrollState::Idle;
self.direction = None;
}
}
#[cfg(test)]
mod tests {
use super::*;
const TIMINGS: EdgeScrollTimings = EdgeScrollTimings {
initial_delay: Duration::from_millis(400),
repeat_interval: Duration::from_millis(240),
};
fn now() -> Instant {
Instant::now()
}
#[test]
fn enter_band_yields_immediate_scroll() {
let mut sched = EdgeScrollScheduler::new();
assert_eq!(
sched.on_enter(Direction::Left),
EdgeScrollAction::Scroll(Direction::Left)
);
assert_eq!(sched.state(), EdgeScrollState::Idle); }
#[test]
fn successful_entry_scroll_arms_first_gap_timer() {
let mut sched = EdgeScrollScheduler::new();
let t = now();
sched.on_enter(Direction::Left);
assert_eq!(
sched.on_scroll_outcome(true, t, &TIMINGS),
EdgeScrollAction::Arm(t + TIMINGS.initial_delay)
);
assert_eq!(sched.state(), EdgeScrollState::ArmedInitial);
}
#[test]
fn entry_scroll_at_content_edge_does_not_arm() {
let mut sched = EdgeScrollScheduler::new();
let t = now();
sched.on_enter(Direction::Right);
assert_eq!(
sched.on_scroll_outcome(false, t, &TIMINGS),
EdgeScrollAction::Cancel
);
assert_eq!(sched.state(), EdgeScrollState::Idle);
assert_eq!(sched.direction, None);
}
#[test]
fn initial_delay_timer_fire_yields_scroll_and_arms_repeat() {
let mut sched = EdgeScrollScheduler::new();
let t = now();
sched.on_enter(Direction::Left);
sched.on_scroll_outcome(true, t, &TIMINGS);
let fire_at = t + TIMINGS.initial_delay;
assert_eq!(
sched.on_timer_fired(),
Some(EdgeScrollAction::Scroll(Direction::Left))
);
assert_eq!(
sched.on_scroll_outcome(true, fire_at, &TIMINGS),
EdgeScrollAction::Arm(fire_at + TIMINGS.repeat_interval)
);
assert_eq!(sched.state(), EdgeScrollState::ArmedRepeat);
}
#[test]
fn repeat_timer_fire_yields_scroll_and_re_arms() {
let mut sched = EdgeScrollScheduler::new();
let t = now();
sched.on_enter(Direction::Left);
sched.on_scroll_outcome(true, t, &TIMINGS); let first_fire = t + TIMINGS.initial_delay;
sched.on_timer_fired();
sched.on_scroll_outcome(true, first_fire, &TIMINGS);
let second_fire = first_fire + TIMINGS.repeat_interval;
assert_eq!(
sched.on_timer_fired(),
Some(EdgeScrollAction::Scroll(Direction::Left))
);
assert_eq!(
sched.on_scroll_outcome(true, second_fire, &TIMINGS),
EdgeScrollAction::Arm(second_fire + TIMINGS.repeat_interval)
);
assert_eq!(sched.state(), EdgeScrollState::ArmedRepeat);
}
#[test]
fn repeat_continues_indefinitely_at_steady_cadence() {
let mut sched = EdgeScrollScheduler::new();
let t = now();
sched.on_enter(Direction::Right);
sched.on_scroll_outcome(true, t, &TIMINGS);
let mut fire = t + TIMINGS.initial_delay;
for _ in 0..10 {
assert_eq!(
sched.on_timer_fired(),
Some(EdgeScrollAction::Scroll(Direction::Right))
);
assert_eq!(
sched.on_scroll_outcome(true, fire, &TIMINGS),
EdgeScrollAction::Arm(fire + TIMINGS.repeat_interval)
);
assert_eq!(sched.state(), EdgeScrollState::ArmedRepeat);
fire += TIMINGS.repeat_interval;
}
}
#[test]
fn repeat_scroll_at_content_edge_cancels_and_resets() {
let mut sched = EdgeScrollScheduler::new();
let t = now();
sched.on_enter(Direction::Left);
sched.on_scroll_outcome(true, t, &TIMINGS);
let fire = t + TIMINGS.initial_delay;
sched.on_timer_fired();
sched.on_scroll_outcome(true, fire, &TIMINGS);
assert_eq!(sched.state(), EdgeScrollState::ArmedRepeat);
let next_fire = fire + TIMINGS.repeat_interval;
assert_eq!(
sched.on_timer_fired(),
Some(EdgeScrollAction::Scroll(Direction::Left))
);
assert_eq!(
sched.on_scroll_outcome(false, next_fire, &TIMINGS),
EdgeScrollAction::Cancel
);
assert_eq!(sched.state(), EdgeScrollState::Idle);
}
#[test]
fn leave_band_cancels_and_resets_to_idle() {
let mut sched = EdgeScrollScheduler::new();
let t = now();
sched.on_enter(Direction::Left);
sched.on_scroll_outcome(true, t, &TIMINGS);
assert_eq!(sched.state(), EdgeScrollState::ArmedInitial);
assert_eq!(sched.on_leave(), EdgeScrollAction::Cancel);
assert_eq!(sched.state(), EdgeScrollState::Idle);
}
#[test]
fn drag_end_cancels_and_resets_to_idle() {
let mut sched = EdgeScrollScheduler::new();
let t = now();
sched.on_enter(Direction::Left);
sched.on_scroll_outcome(true, t, &TIMINGS);
sched.on_timer_fired();
sched.on_scroll_outcome(true, t, &TIMINGS);
assert_eq!(sched.state(), EdgeScrollState::ArmedRepeat);
assert_eq!(sched.on_drag_end(), EdgeScrollAction::Cancel);
assert_eq!(sched.state(), EdgeScrollState::Idle);
}
#[test]
fn re_entry_after_leave_starts_fresh_at_immediate_scroll() {
let mut sched = EdgeScrollScheduler::new();
let t = now();
sched.on_enter(Direction::Left);
sched.on_scroll_outcome(true, t, &TIMINGS);
sched.on_timer_fired();
sched.on_scroll_outcome(true, t, &TIMINGS); sched.on_leave();
assert_eq!(
sched.on_enter(Direction::Left),
EdgeScrollAction::Scroll(Direction::Left)
);
assert_eq!(
sched.on_scroll_outcome(true, t, &TIMINGS),
EdgeScrollAction::Arm(t + TIMINGS.initial_delay)
);
assert_eq!(sched.state(), EdgeScrollState::ArmedInitial);
}
#[test]
fn timer_fires_in_last_known_direction() {
let mut sched = EdgeScrollScheduler::new();
let t = now();
sched.on_enter(Direction::Right);
sched.on_scroll_outcome(true, t, &TIMINGS);
assert_eq!(
sched.on_timer_fired(),
Some(EdgeScrollAction::Scroll(Direction::Right))
);
}
#[test]
fn leaving_left_and_entering_right_flips_direction_naturally() {
let mut sched = EdgeScrollScheduler::new();
let t = now();
sched.on_enter(Direction::Left);
sched.on_scroll_outcome(true, t, &TIMINGS);
sched.on_leave();
assert_eq!(
sched.on_enter(Direction::Right),
EdgeScrollAction::Scroll(Direction::Right)
);
sched.on_scroll_outcome(true, t, &TIMINGS);
assert_eq!(
sched.on_timer_fired(),
Some(EdgeScrollAction::Scroll(Direction::Right))
);
}
#[test]
fn timer_fire_from_idle_is_no_op() {
let mut sched = EdgeScrollScheduler::new();
assert_eq!(sched.on_timer_fired(), None);
assert_eq!(sched.state(), EdgeScrollState::Idle);
}
#[test]
fn leave_from_idle_is_idempotent_cancel() {
let mut sched = EdgeScrollScheduler::new();
assert_eq!(sched.on_leave(), EdgeScrollAction::Cancel);
assert_eq!(sched.state(), EdgeScrollState::Idle);
}
#[test]
fn zero_initial_delay_clamps_to_repeat_cadence() {
let timings = EdgeScrollTimings {
initial_delay: Duration::from_millis(240), repeat_interval: Duration::from_millis(240),
};
let mut sched = EdgeScrollScheduler::new();
let t = now();
sched.on_enter(Direction::Left);
assert_eq!(
sched.on_scroll_outcome(true, t, &timings),
EdgeScrollAction::Arm(t + timings.initial_delay)
);
let fire = t + timings.initial_delay;
sched.on_timer_fired();
assert_eq!(
sched.on_scroll_outcome(true, fire, &timings),
EdgeScrollAction::Arm(fire + timings.repeat_interval)
);
}
}