use std::time::{Duration, Instant};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ControlFlowIntent {
Wait,
WaitUntil(Instant),
Unchanged,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PacedDecision {
pub paced_wake: Option<Instant>,
pub request_redraw: bool,
pub control_flow: ControlFlowIntent,
}
pub fn next_paced_wake(
needs_frame: bool,
needs_frame_paced_only: bool,
anim_pacing: bool,
now: Instant,
cosmetic_loop_hz: f32,
requested_interval: Option<Duration>,
) -> PacedDecision {
if !needs_frame {
return PacedDecision {
paced_wake: None,
request_redraw: false,
control_flow: ControlFlowIntent::Wait,
};
}
if anim_pacing && needs_frame_paced_only {
let cap = Duration::from_secs_f32(1.0 / cosmetic_loop_hz);
let interval = match requested_interval {
Some(requested) => cap.max(requested),
None => cap,
};
PacedDecision {
paced_wake: Some(now + interval),
request_redraw: false,
control_flow: ControlFlowIntent::Unchanged,
}
} else {
PacedDecision {
paced_wake: None,
request_redraw: true,
control_flow: ControlFlowIntent::Wait,
}
}
}
pub fn paced_wake_action(paced_wake: Option<Instant>, now: Instant) -> PacedDecision {
match paced_wake {
None => PacedDecision {
paced_wake: None,
request_redraw: false,
control_flow: ControlFlowIntent::Wait,
},
Some(deadline) if now >= deadline => PacedDecision {
paced_wake: None,
request_redraw: true,
control_flow: ControlFlowIntent::Wait,
},
Some(deadline) => PacedDecision {
paced_wake: Some(deadline),
request_redraw: false,
control_flow: ControlFlowIntent::WaitUntil(deadline),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
fn base() -> Instant {
Instant::now()
}
fn interval_30hz() -> Duration {
Duration::from_secs_f32(1.0 / 30.0)
}
#[test]
fn settling_clears_to_idle_and_returns_to_wait() {
let now = base();
for paced_only in [true, false] {
for anim_pacing in [true, false] {
for requested in [None, Some(Duration::from_millis(500))] {
assert_eq!(
next_paced_wake(false, paced_only, anim_pacing, now, 30.0, requested),
PacedDecision {
paced_wake: None,
request_redraw: false,
control_flow: ControlFlowIntent::Wait,
}
);
}
}
}
}
#[test]
fn paced_only_with_pacing_enabled_schedules_and_leaves_control_flow_unchanged() {
let now = base();
assert_eq!(
next_paced_wake(true, true, true, now, 30.0, None),
PacedDecision {
paced_wake: Some(now + interval_30hz()),
request_redraw: false,
control_flow: ControlFlowIntent::Unchanged,
}
);
}
#[test]
fn paced_only_with_pacing_disabled_fires_now() {
let now = base();
assert_eq!(
next_paced_wake(true, true, false, now, 30.0, None),
PacedDecision {
paced_wake: None,
request_redraw: true,
control_flow: ControlFlowIntent::Wait,
}
);
}
#[test]
fn a_real_transition_fires_now_even_with_pacing_enabled() {
let now = base();
assert_eq!(
next_paced_wake(true, false, true, now, 30.0, None),
PacedDecision {
paced_wake: None,
request_redraw: true,
control_flow: ControlFlowIntent::Wait,
}
);
}
#[test]
fn no_requested_interval_falls_back_to_the_cosmetic_loop_cap() {
let now = base();
assert_eq!(
next_paced_wake(true, true, true, now, 30.0, None),
next_paced_wake(true, true, true, now, 30.0, Some(Duration::ZERO)),
"None and Some(ZERO) both mean \"at the theme's own rate\""
);
assert_eq!(
next_paced_wake(true, true, true, now, 30.0, None),
PacedDecision {
paced_wake: Some(now + interval_30hz()),
request_redraw: false,
control_flow: ControlFlowIntent::Unchanged,
}
);
}
#[test]
fn a_500ms_caret_request_paces_slower_than_the_30hz_cap() {
let now = base();
let requested = Duration::from_millis(500);
assert_eq!(
next_paced_wake(true, true, true, now, 30.0, Some(requested)),
PacedDecision {
paced_wake: Some(now + requested),
request_redraw: false,
control_flow: ControlFlowIntent::Unchanged,
}
);
}
#[test]
fn a_tighter_requested_interval_is_clamped_up_to_the_theme_cap() {
let now = base();
let too_fast = Duration::from_millis(5);
assert_eq!(
next_paced_wake(true, true, true, now, 30.0, Some(too_fast)),
PacedDecision {
paced_wake: Some(now + interval_30hz()),
request_redraw: false,
control_flow: ControlFlowIntent::Unchanged,
}
);
}
#[test]
fn requested_interval_is_ignored_while_pacing_is_disabled() {
let now = base();
assert_eq!(
next_paced_wake(
true,
true,
false,
now,
30.0,
Some(Duration::from_millis(500))
),
PacedDecision {
paced_wake: None,
request_redraw: true,
control_flow: ControlFlowIntent::Wait,
}
);
}
#[test]
fn interval_change_between_paints_re_derives_the_next_deadline() {
let t0 = base();
let d0 = next_paced_wake(true, true, true, t0, 30.0, None);
assert_eq!(d0.paced_wake, Some(t0 + interval_30hz()));
let t1 = t0 + Duration::from_millis(1);
let d1 = next_paced_wake(true, true, true, t1, 30.0, Some(Duration::from_millis(500)));
assert_eq!(
d1.paced_wake,
Some(t1 + Duration::from_millis(500)),
"the deadline must be re-derived from this paint's own requested \
interval, not carried over from the prior 30Hz decision"
);
assert_ne!(
d1.paced_wake, d0.paced_wake,
"the new deadline must differ from the stale 30Hz-derived one"
);
}
#[test]
fn no_pending_wake_returns_to_wait() {
assert_eq!(
paced_wake_action(None, base()),
PacedDecision {
paced_wake: None,
request_redraw: false,
control_flow: ControlFlowIntent::Wait,
}
);
}
#[test]
fn future_deadline_parks() {
let now = base();
let deadline = now + Duration::from_millis(10);
assert_eq!(
paced_wake_action(Some(deadline), now),
PacedDecision {
paced_wake: Some(deadline),
request_redraw: false,
control_flow: ControlFlowIntent::WaitUntil(deadline),
}
);
}
#[test]
fn elapsed_deadline_fires() {
let now = base();
let deadline = now - Duration::from_millis(1);
assert_eq!(
paced_wake_action(Some(deadline), now),
PacedDecision {
paced_wake: None,
request_redraw: true,
control_flow: ControlFlowIntent::Wait,
}
);
}
#[test]
fn exactly_at_deadline_fires() {
let now = base();
assert_eq!(
paced_wake_action(Some(now), now),
PacedDecision {
paced_wake: None,
request_redraw: true,
control_flow: ControlFlowIntent::Wait,
}
);
}
}