//! TRACES: FR-DSP-4 //! When the canvas renders a draft and when it renders sharp. //! //! The decision `build_redraw` acts on, kept apart from the timers and the //! window that carry it out so it can be driven event by event in a test. //! `lib.rs` owns the plumbing: it posts a zero-delay timer when //! [`Refine::request`] says to, calls [`Refine::tick`] from it, and arms a //! [`SETTLE_DELAY`](crate::SETTLE_DELAY) timer carrying whatever token //! `tick` hands back, which calls [`Refine::settle`] when it fires. /// Which quality of frame to render. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum Frame { /// Half resolution, while a gesture is still moving. Draft, /// Full resolution. Sharp, } /// A settle timer's claim on the sharp frame, redeemed by [`Refine::settle`]. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct Settle(u64); /// The coalescing and settling state behind the canvas redraw. #[derive(Debug, Default)] pub(crate) struct Refine { /// A render is posted and has not run yet. pending: bool, /// Something changed since the last render. dirty: bool, /// A request arrived while a render was already posted. coalesced: bool, /// A settle timer is armed. settling: bool, /// The token the most recent settle timer carries. generation: u64, } impl Refine { /// Something changed; `true` means post a render. pub(crate) fn request(&mut self) -> bool { self.dirty = true; if self.pending { self.coalesced = true; return false; } self.pending = true; true } /// The posted render runs: which frame to draw, if any, and a settle /// timer to arm afterwards, if one is wanted. /// /// **Every draft frame arms a fresh settle timer**, and only the newest /// one is honoured (see [`Self::settle`]). That is a debounce: the sharp /// frame arrives [`SETTLE_DELAY`](crate::SETTLE_DELAY) after the *last* /// movement, never mid-gesture. It used to arm once per burst and not /// again while that timer was pending, so the timer counted from the /// *first* draft of a drag and fired underneath the finger — a full /// resolution render every 128 ms of any drag longer than the delay, /// each one the most expensive frame the canvas draws, landing exactly /// where the budget is tightest. /// /// **What counts as "still dragging".** No control reports a gesture /// boundary, and threading one out of every slider, curve point and crop /// handle would be a lot of surface for a rendering concern. Two signals /// stand in for it. A request that had to coalesce into a render already /// posted can only mean a control moved again. And a request arriving /// while a settle is still owed belongs to the gesture that owes it: a /// slow stretch of a drag, one event per frame, would otherwise render /// sharp between two drafts. One-off changes — a click, a reset, a resize /// — meet neither, so they render sharp the first time and never draw a /// draft frame at all. pub(crate) fn tick(&mut self) -> Option<(Frame, Option)> { self.pending = false; if !std::mem::take(&mut self.dirty) { return None; } let dragging = std::mem::take(&mut self.coalesced) || self.settling; if !dragging { return Some((Frame::Sharp, None)); } self.settling = true; self.generation += 1; Some((Frame::Draft, Some(Settle(self.generation)))) } /// A settle timer fired: `Some` means render sharp now. /// /// `None` for any timer a later draft has superseded, and also when a /// render is already posted: that one will draw a draft and arm its own /// timer, and a sharp frame drawn now would be replaced a moment later. pub(crate) fn settle(&mut self, token: Settle) -> Option { if !self.settling || token.0 != self.generation || self.pending { return None; } self.settling = false; Some(Frame::Sharp) } } #[cfg(test)] mod tests { use super::*; /// The settle delay, in the simulated milliseconds below. const DELAY: u64 = 120; /// Drive a `Refine` through a timeline of input events the way the event /// loop would, and record every frame rendered with the time it was. /// /// `inputs` are the milliseconds at which a control moved. Each posted /// render runs one millisecond after the request that posted it, and a /// burst of requests landing in the same millisecond coalesces into it. fn run(inputs: &[u64]) -> Vec<(u64, Frame)> { let mut r = Refine::default(); let mut frames = Vec::new(); let mut tick_at: Option = None; let mut settles: Vec<(u64, Settle)> = Vec::new(); let end = inputs.last().copied().unwrap_or(0) + 10 * DELAY; let mut inputs = inputs.iter().copied().peekable(); for now in 0..=end { // Timers whose time has come, in the order they were armed. if tick_at == Some(now) { tick_at = None; if let Some((frame, arm)) = r.tick() { frames.push((now, frame)); if let Some(token) = arm { settles.push((now + DELAY, token)); } } } let due: Vec = settles .iter() .filter(|(t, _)| *t == now) .map(|(_, s)| *s) .collect(); settles.retain(|(t, _)| *t != now); for token in due { if let Some(frame) = r.settle(token) { frames.push((now, frame)); } } while inputs.peek() == Some(&now) { inputs.next(); if r.request() { tick_at = Some(now + 1); } } } frames } /// A drag: two moves per 16 ms frame from `from` for `len` milliseconds. fn drag(from: u64, len: u64) -> Vec { (from..from + len) .step_by(16) .flat_map(|t| [t, t]) .collect() } /// The sharp frames rendered in `(from, to]`. fn sharp_between(frames: &[(u64, Frame)], from: u64, to: u64) -> Vec<(u64, Frame)> { frames .iter() .copied() .filter(|(t, f)| *f == Frame::Sharp && *t > from && *t <= to) .collect() } #[test] fn a_long_drag_renders_no_sharp_frame_until_it_stops() { // The regression: before the debounce this drew sharp frames at 121, // 249, 377 and 505 ms, the settle timer counting from the first draft. let inputs = drag(0, 600); let last = *inputs.last().unwrap(); let frames = run(&inputs); let mid = sharp_between(&frames, 1, last + 1); assert!(mid.is_empty(), "sharp frames mid-drag: {mid:?}"); } #[test] fn the_sharp_frame_lands_one_delay_after_the_last_movement() { let inputs = drag(0, 600); let last = *inputs.last().unwrap(); let frames = run(&inputs); let sharp = sharp_between(&frames, 1, u64::MAX); // The last draft renders a millisecond after the last input, and the // sharp frame the settle delay after that. assert_eq!(sharp, vec![(last + 1 + DELAY, Frame::Sharp)]); assert_eq!(frames.last(), Some(&(last + 1 + DELAY, Frame::Sharp))); } #[test] fn a_slow_stretch_inside_a_drag_stays_draft() { // Coalesced moves, then one move per 40 ms (none coalesce), then stop. let mut inputs = drag(0, 100); inputs.extend((112..400).step_by(40)); let last = *inputs.last().unwrap(); let frames = run(&inputs); let mid = sharp_between(&frames, 1, last + 1); assert!(mid.is_empty(), "sharp frames mid-drag: {mid:?}"); assert_eq!(frames.last(), Some(&(last + 1 + DELAY, Frame::Sharp))); } #[test] fn a_pause_longer_than_the_delay_settles_and_the_next_drag_drafts_again() { let mut inputs = drag(0, 100); inputs.extend(drag(400, 100)); let frames = run(&inputs); let sharp = sharp_between(&frames, 1, u64::MAX); assert_eq!(sharp.len(), 2, "one settle per drag: {frames:?}"); assert!(frames .iter() .any(|(t, f)| *f == Frame::Draft && (400..500).contains(t))); } #[test] fn a_single_change_renders_sharp_once_and_arms_nothing() { assert_eq!(run(&[5]), vec![(6, Frame::Sharp)]); } #[test] fn a_superseded_timer_does_nothing() { let mut r = Refine::default(); assert!(r.request()); assert!(!r.request()); let (f, first) = r.tick().unwrap(); assert_eq!(f, Frame::Draft); assert!(r.request()); let (f, second) = r.tick().unwrap(); assert_eq!(f, Frame::Draft); assert_eq!(r.settle(first.unwrap()), None); assert_eq!(r.settle(second.unwrap()), Some(Frame::Sharp)); // And once redeemed, not again. assert_eq!(r.settle(second.unwrap()), None); } #[test] fn a_timer_firing_with_a_render_posted_defers_to_it() { let mut r = Refine::default(); r.request(); r.request(); let (_, token) = r.tick().unwrap(); assert!(r.request()); assert_eq!(r.settle(token.unwrap()), None); let (f, next) = r.tick().unwrap(); assert_eq!(f, Frame::Draft); assert_eq!(r.settle(next.unwrap()), Some(Frame::Sharp)); } }