ftui_core/animation/
callbacks.rs1#![forbid(unsafe_code)]
2
3use std::time::Duration;
51
52use super::Animation;
53
54#[derive(Debug, Clone, PartialEq)]
60pub enum AnimationEvent {
61 Started,
63 Progress(f32),
65 Completed,
67}
68
69#[derive(Debug, Clone, Default)]
71struct EventConfig {
72 on_start: bool,
73 on_complete: bool,
74 thresholds: Vec<f32>,
76}
77
78#[derive(Debug, Clone, Default)]
80struct EventState {
81 started_fired: bool,
82 completed_fired: bool,
83 thresholds_fired: Vec<bool>,
85}
86
87pub struct Callbacks<A> {
92 inner: A,
93 config: EventConfig,
94 state: EventState,
95 events: Vec<AnimationEvent>,
96}
97
98impl<A: std::fmt::Debug> std::fmt::Debug for Callbacks<A> {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 f.debug_struct("Callbacks")
101 .field("inner", &self.inner)
102 .field("pending_events", &self.events.len())
103 .finish()
104 }
105}
106
107impl<A: Animation> Callbacks<A> {
112 #[must_use]
114 pub fn new(inner: A) -> Self {
115 Self {
116 inner,
117 config: EventConfig::default(),
118 state: EventState::default(),
119 events: Vec::new(),
120 }
121 }
122
123 #[must_use]
125 pub fn on_start(mut self) -> Self {
126 self.config.on_start = true;
127 self
128 }
129
130 #[must_use]
132 pub fn on_complete(mut self) -> Self {
133 self.config.on_complete = true;
134 self
135 }
136
137 #[must_use]
141 pub fn at_progress(mut self, threshold: f32) -> Self {
142 if !threshold.is_finite() {
143 return self;
144 }
145 let clamped = threshold.clamp(0.0, 1.0);
146 let idx = self
147 .config
148 .thresholds
149 .partition_point(|&value| value <= clamped);
150 self.config.thresholds.insert(idx, clamped);
151 self.state.thresholds_fired.insert(idx, false);
152 self
153 }
154
155 #[must_use]
157 pub fn inner(&self) -> &A {
158 &self.inner
159 }
160
161 pub fn inner_mut(&mut self) -> &mut A {
163 &mut self.inner
164 }
165
166 pub fn drain_events(&mut self) -> Vec<AnimationEvent> {
168 std::mem::take(&mut self.events)
169 }
170
171 #[must_use]
173 pub fn pending_event_count(&self) -> usize {
174 self.events.len()
175 }
176
177 fn check_events(&mut self) {
179 let value = self.inner.value();
180
181 if self.config.on_start && !self.state.started_fired {
183 self.state.started_fired = true;
184 self.events.push(AnimationEvent::Started);
185 }
186
187 for (i, &threshold) in self.config.thresholds.iter().enumerate() {
189 if !self.state.thresholds_fired[i] && value >= threshold {
190 self.state.thresholds_fired[i] = true;
191 self.events.push(AnimationEvent::Progress(threshold));
192 }
193 }
194
195 if self.config.on_complete && !self.state.completed_fired && self.inner.is_complete() {
197 self.state.completed_fired = true;
198 self.events.push(AnimationEvent::Completed);
199 }
200 }
201}
202
203impl<A: Animation> Animation for Callbacks<A> {
208 fn tick(&mut self, dt: Duration) {
209 self.inner.tick(dt);
210 self.check_events();
211 }
212
213 fn is_complete(&self) -> bool {
214 self.inner.is_complete()
215 }
216
217 fn value(&self) -> f32 {
218 self.inner.value()
219 }
220
221 fn reset(&mut self) {
222 self.inner.reset();
223 self.state.started_fired = false;
224 self.state.completed_fired = false;
225 for fired in &mut self.state.thresholds_fired {
226 *fired = false;
227 }
228 self.events.clear();
229 }
230
231 fn overshoot(&self) -> Duration {
232 self.inner.overshoot()
233 }
234}
235
236#[cfg(test)]
241mod tests {
242 use super::*;
243 use crate::animation::Fade;
244
245 const MS_100: Duration = Duration::from_millis(100);
246 const MS_250: Duration = Duration::from_millis(250);
247 const MS_500: Duration = Duration::from_millis(500);
248 const SEC_1: Duration = Duration::from_secs(1);
249
250 #[test]
251 fn no_events_configured() {
252 let mut anim = Callbacks::new(Fade::new(SEC_1));
253 anim.tick(MS_500);
254 assert!(anim.drain_events().is_empty());
255 }
256
257 #[test]
258 fn started_fires_on_first_tick() {
259 let mut anim = Callbacks::new(Fade::new(SEC_1)).on_start();
260 anim.tick(MS_100);
261 let events = anim.drain_events();
262 assert_eq!(events, vec![AnimationEvent::Started]);
263
264 anim.tick(MS_100);
266 assert!(anim.drain_events().is_empty());
267 }
268
269 #[test]
270 fn completed_fires_when_done() {
271 let mut anim = Callbacks::new(Fade::new(MS_500)).on_complete();
272 anim.tick(MS_250);
273 assert!(anim.drain_events().is_empty()); anim.tick(MS_500); let events = anim.drain_events();
277 assert_eq!(events, vec![AnimationEvent::Completed]);
278
279 anim.tick(MS_100);
281 assert!(anim.drain_events().is_empty());
282 }
283
284 #[test]
285 fn progress_threshold_fires_once() {
286 let mut anim = Callbacks::new(Fade::new(SEC_1)).at_progress(0.5);
287 anim.tick(MS_250);
288 assert!(anim.drain_events().is_empty()); anim.tick(MS_500); let events = anim.drain_events();
292 assert_eq!(events, vec![AnimationEvent::Progress(0.5)]);
293
294 anim.tick(MS_250);
296 assert!(anim.drain_events().is_empty());
297 }
298
299 #[test]
300 fn multiple_thresholds() {
301 let mut anim = Callbacks::new(Fade::new(SEC_1))
302 .at_progress(0.25)
303 .at_progress(0.75);
304
305 anim.tick(MS_500); let events = anim.drain_events();
307 assert_eq!(events, vec![AnimationEvent::Progress(0.25)]);
308
309 anim.tick(MS_500); let events = anim.drain_events();
311 assert_eq!(events, vec![AnimationEvent::Progress(0.75)]);
312 }
313
314 #[test]
315 fn all_events_in_order() {
316 let mut anim = Callbacks::new(Fade::new(MS_500))
317 .on_start()
318 .at_progress(0.5)
319 .on_complete();
320
321 anim.tick(MS_500); let events = anim.drain_events();
323 assert_eq!(
324 events,
325 vec![
326 AnimationEvent::Started,
327 AnimationEvent::Progress(0.5),
328 AnimationEvent::Completed,
329 ]
330 );
331 }
332
333 #[test]
334 fn reset_allows_events_to_fire_again() {
335 let mut anim = Callbacks::new(Fade::new(MS_500)).on_start().on_complete();
336 anim.tick(SEC_1);
337 let _ = anim.drain_events();
338
339 anim.reset();
340 anim.tick(SEC_1);
341 let events = anim.drain_events();
342 assert_eq!(
343 events,
344 vec![AnimationEvent::Started, AnimationEvent::Completed]
345 );
346 }
347
348 #[test]
349 fn drain_clears_queue() {
350 let mut anim = Callbacks::new(Fade::new(SEC_1)).on_start();
351 anim.tick(MS_100);
352 assert_eq!(anim.pending_event_count(), 1);
353
354 let _ = anim.drain_events();
355 assert_eq!(anim.pending_event_count(), 0);
356 }
357
358 #[test]
359 fn inner_access() {
360 let anim = Callbacks::new(Fade::new(SEC_1));
361 assert!(!anim.inner().is_complete());
362 }
363
364 #[test]
365 fn inner_mut_access() {
366 let mut anim = Callbacks::new(Fade::new(SEC_1));
367 anim.inner_mut().tick(SEC_1);
368 assert!(anim.inner().is_complete());
369 }
370
371 #[test]
372 fn animation_trait_value_delegates() {
373 let mut anim = Callbacks::new(Fade::new(SEC_1));
374 anim.tick(MS_500);
375 assert!((anim.value() - 0.5).abs() < 0.02);
376 }
377
378 #[test]
379 fn animation_trait_is_complete_delegates() {
380 let mut anim = Callbacks::new(Fade::new(MS_100));
381 assert!(!anim.is_complete());
382 anim.tick(MS_100);
383 assert!(anim.is_complete());
384 }
385
386 #[test]
387 fn threshold_clamped() {
388 let mut anim = Callbacks::new(Fade::new(SEC_1))
389 .at_progress(-0.5) .at_progress(1.5); anim.tick(Duration::from_nanos(1)); let events = anim.drain_events();
394 assert!(events.contains(&AnimationEvent::Progress(0.0)));
396 }
397
398 #[test]
399 fn debug_format() {
400 let anim = Callbacks::new(Fade::new(MS_100)).on_start();
401 let dbg = format!("{:?}", anim);
402 assert!(dbg.contains("Callbacks"));
403 assert!(dbg.contains("pending_events"));
404 }
405
406 #[test]
407 fn overshoot_delegates() {
408 let mut anim = Callbacks::new(Fade::new(MS_100));
409 anim.tick(MS_500);
410 assert!(anim.overshoot() > Duration::ZERO);
411 }
412}