use std::time::{Duration, Instant};
pub const FRAME_CALLBACK_TIMEOUT: Duration = Duration::from_secs(1);
#[derive(Debug, Default)]
pub struct RedrawScheduler {
cause: Option<&'static str>,
awaiting_frame: Option<Instant>,
}
impl RedrawScheduler {
pub fn new() -> Self {
Self::default()
}
pub fn request(&mut self, cause: &'static str) {
self.cause.get_or_insert(cause);
}
pub fn is_dirty(&self) -> bool {
self.cause.is_some()
}
pub fn take_due(&mut self, now: Instant) -> Option<&'static str> {
if self
.awaiting_frame
.is_some_and(|since| now.saturating_duration_since(since) < FRAME_CALLBACK_TIMEOUT)
{
return None;
}
self.cause.take()
}
pub fn committed(&mut self, now: Instant) {
self.awaiting_frame = Some(now);
}
pub fn frame_done(&mut self) {
self.awaiting_frame = None;
}
pub fn wake_deadline(&self) -> Option<Instant> {
let since = self.awaiting_frame?;
self.cause
.is_some()
.then_some(since + FRAME_CALLBACK_TIMEOUT)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_clean_scheduler_has_nothing_to_paint() {
let mut scheduler = RedrawScheduler::new();
assert!(!scheduler.is_dirty());
assert_eq!(scheduler.take_due(Instant::now()), None);
}
#[test]
fn requests_before_a_flush_collapse_into_one_frame_with_the_first_cause() {
let mut scheduler = RedrawScheduler::new();
scheduler.request("workspaces");
scheduler.request("active-window");
scheduler.request("tick");
let now = Instant::now();
assert_eq!(scheduler.take_due(now), Some("workspaces"));
assert_eq!(scheduler.take_due(now), None);
assert!(!scheduler.is_dirty());
}
#[test]
fn a_request_waits_for_the_frame_callback_of_the_previous_commit() {
let mut scheduler = RedrawScheduler::new();
let committed = Instant::now();
scheduler.committed(committed);
scheduler.request("tick");
assert_eq!(scheduler.take_due(committed), None);
assert!(scheduler.is_dirty(), "the request is kept, not dropped");
scheduler.frame_done();
assert_eq!(scheduler.take_due(committed), Some("tick"));
}
#[test]
fn a_frame_callback_that_never_arrives_stops_blocking_after_the_timeout() {
let mut scheduler = RedrawScheduler::new();
let committed = Instant::now();
scheduler.committed(committed);
scheduler.request("tick");
let just_before = committed + FRAME_CALLBACK_TIMEOUT - Duration::from_millis(1);
assert_eq!(scheduler.take_due(just_before), None);
assert_eq!(
scheduler.take_due(committed + FRAME_CALLBACK_TIMEOUT),
Some("tick")
);
}
#[test]
fn a_frame_callback_with_nothing_requested_paints_nothing() {
let mut scheduler = RedrawScheduler::new();
scheduler.committed(Instant::now());
scheduler.frame_done();
assert_eq!(scheduler.take_due(Instant::now()), None);
}
#[test]
fn a_blocked_repaint_wakes_at_the_frame_callback_timeout() {
let mut scheduler = RedrawScheduler::new();
let committed = Instant::now();
scheduler.committed(committed);
assert_eq!(
scheduler.wake_deadline(),
None,
"nothing is waiting to paint"
);
scheduler.request("workspaces");
assert_eq!(
scheduler.wake_deadline(),
Some(committed + FRAME_CALLBACK_TIMEOUT)
);
scheduler.frame_done();
assert_eq!(
scheduler.wake_deadline(),
None,
"the callback arrived; the timeout must not also fire"
);
}
}