返回 CodeWhale
frame_requester.rs
根目录 / crates / tui / src / tui / motion / frame_requester.rs
1 //! Coalescing frame-request scheduler.
2 //!
3 //! Widgets ask for a future frame; this scheduler merges requests and emits
4 //! at most one wake deadline compatible with the existing frame-cap
5 //! philosophy in [`crate::tui::frame_rate_limiter`]. It does **not** run a
6 //! competing animation loop — the main `ui` poll loop remains the sole
7 //! emitter of `terminal.draw`.
8
9 use std::time::Duration;
10 use std::time::Instant;
11
12 use super::mode::MotionPolicy;
13
14 /// Coalesced request for a future redraw.
15 #[derive(Debug, Default)]
16 pub struct FrameRequester {
17 /// Earliest instant a requester wants a frame.
18 next_due: Option<Instant>,
19 #[cfg(test)]
20 request_count: u64,
21 #[cfg(test)]
22 emit_count: u64,
23 }
24
25 impl FrameRequester {
26 #[must_use]
27 pub fn new() -> Self {
28 Self::default()
29 }
30
31 /// Request a frame as soon as the motion policy and frame cap allow.
32 pub fn request_frame(&mut self, now: Instant, policy: MotionPolicy) {
33 self.request_at(now, now, policy);
34 }
35
36 /// Request a frame no earlier than `earliest`.
37 pub fn request_at(&mut self, now: Instant, earliest: Instant, policy: MotionPolicy) {
38 if !policy.should_request_animation_frames() {
39 return;
40 }
41 let capped = earliest.max(now);
42 #[cfg(test)]
43 {
44 self.request_count = self.request_count.saturating_add(1);
45 }
46 self.next_due = Some(match self.next_due {
47 Some(existing) => existing.min(capped),
48 None => capped,
49 });
50 }
51
52 /// Time until a coalesced frame should emit, if one is pending.
53 #[must_use]
54 pub fn due_in(&self, now: Instant) -> Option<Duration> {
55 let due = self.next_due?;
56 Some(due.saturating_duration_since(now))
57 }
58
59 /// Consume a due frame request. Returns true when the main loop should
60 /// set `needs_redraw` for animation (not for state changes).
61 pub fn take_due(&mut self, now: Instant, policy: MotionPolicy) -> bool {
62 if !policy.should_request_animation_frames() {
63 self.next_due = None;
64 return false;
65 }
66 let Some(due) = self.next_due else {
67 return false;
68 };
69 if now < due {
70 return false;
71 }
72 self.next_due = None;
73 #[cfg(test)]
74 {
75 self.emit_count = self.emit_count.saturating_add(1);
76 }
77 true
78 }
79
80 #[must_use]
81 #[cfg(test)]
82 pub fn request_count(&self) -> u64 {
83 self.request_count
84 }
85
86 #[must_use]
87 #[cfg(test)]
88 pub fn emit_count(&self) -> u64 {
89 self.emit_count
90 }
91 }
92
93 #[cfg(test)]
94 mod tests {
95 use super::*;
96 use crate::tui::motion::MotionPolicy;
97
98 #[test]
99 fn coalesces_multiple_requests_into_one_emit() {
100 let policy = MotionPolicy::from_settings(false, true, false);
101 let mut req = FrameRequester::new();
102 let t0 = Instant::now();
103
104 for _ in 0..20 {
105 req.request_frame(t0, policy);
106 }
107 assert!(req.due_in(t0).is_some());
108 assert_eq!(req.request_count(), 20);
109 assert!(req.take_due(t0, policy));
110 assert_eq!(req.emit_count(), 1);
111 assert!(!req.take_due(t0, policy));
112 }
113
114 #[test]
115 fn reduced_motion_drops_animation_frame_requests() {
116 let policy = MotionPolicy::from_settings(true, true, false);
117 let mut req = FrameRequester::new();
118 let t0 = Instant::now();
119 req.request_frame(t0, policy);
120 assert!(req.due_in(t0).is_none());
121 assert!(!req.take_due(t0, policy));
122 }
123
124 #[test]
125 fn turning_motion_off_discards_a_pending_frame() {
126 let full = MotionPolicy::from_settings(false, true, false);
127 let still = MotionPolicy::from_settings(false, false, false);
128 let mut req = FrameRequester::new();
129 let now = Instant::now();
130 req.request_frame(now, full);
131 assert!(!req.take_due(now, still));
132 assert!(req.due_in(now).is_none());
133 assert!(!req.take_due(now, full));
134 }
135 }
136
136 lines RUST