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 #[inline]
157 #[must_use]
158 pub fn inner(&self) -> &A {
159 &self.inner
160 }
161
162 #[inline]
164 pub fn inner_mut(&mut self) -> &mut A {
165 &mut self.inner
166 }
167
168 pub fn drain_events(&mut self) -> Vec<AnimationEvent> {
170 std::mem::take(&mut self.events)
171 }
172
173 #[inline]
175 #[must_use]
176 pub fn pending_event_count(&self) -> usize {
177 self.events.len()
178 }
179
180 fn check_events(&mut self) {
182 let value = self.inner.value();
183
184 if self.config.on_start && !self.state.started_fired {
186 self.state.started_fired = true;
187 self.events.push(AnimationEvent::Started);
188 }
189
190 for (i, &threshold) in self.config.thresholds.iter().enumerate() {
192 if !self.state.thresholds_fired[i] && value >= threshold {
193 self.state.thresholds_fired[i] = true;
194 self.events.push(AnimationEvent::Progress(threshold));
195 }
196 }
197
198 if self.config.on_complete && !self.state.completed_fired && self.inner.is_complete() {
200 self.state.completed_fired = true;
201 self.events.push(AnimationEvent::Completed);
202 }
203 }
204}
205
206impl<A: Animation> Animation for Callbacks<A> {
211 fn tick(&mut self, dt: Duration) {
212 self.inner.tick(dt);
213 self.check_events();
214 }
215
216 fn is_complete(&self) -> bool {
217 self.inner.is_complete()
218 }
219
220 fn value(&self) -> f32 {
221 self.inner.value()
222 }
223
224 fn reset(&mut self) {
225 self.inner.reset();
226 self.state.started_fired = false;
227 self.state.completed_fired = false;
228 for fired in &mut self.state.thresholds_fired {
229 *fired = false;
230 }
231 self.events.clear();
232 }
233
234 fn overshoot(&self) -> Duration {
235 self.inner.overshoot()
236 }
237}
238
239#[cfg(test)]
244mod tests {
245 use super::*;
246 use crate::animation::Fade;
247
248 const MS_100: Duration = Duration::from_millis(100);
249 const MS_250: Duration = Duration::from_millis(250);
250 const MS_500: Duration = Duration::from_millis(500);
251 const SEC_1: Duration = Duration::from_secs(1);
252
253 #[test]
254 fn no_events_configured() {
255 let mut anim = Callbacks::new(Fade::new(SEC_1));
256 anim.tick(MS_500);
257 assert!(anim.drain_events().is_empty());
258 }
259
260 #[test]
261 fn started_fires_on_first_tick() {
262 let mut anim = Callbacks::new(Fade::new(SEC_1)).on_start();
263 anim.tick(MS_100);
264 let events = anim.drain_events();
265 assert_eq!(events, vec![AnimationEvent::Started]);
266
267 anim.tick(MS_100);
269 assert!(anim.drain_events().is_empty());
270 }
271
272 #[test]
273 fn completed_fires_when_done() {
274 let mut anim = Callbacks::new(Fade::new(MS_500)).on_complete();
275 anim.tick(MS_250);
276 assert!(anim.drain_events().is_empty()); anim.tick(MS_500); let events = anim.drain_events();
280 assert_eq!(events, vec![AnimationEvent::Completed]);
281
282 anim.tick(MS_100);
284 assert!(anim.drain_events().is_empty());
285 }
286
287 #[test]
288 fn progress_threshold_fires_once() {
289 let mut anim = Callbacks::new(Fade::new(SEC_1)).at_progress(0.5);
290 anim.tick(MS_250);
291 assert!(anim.drain_events().is_empty()); anim.tick(MS_500); let events = anim.drain_events();
295 assert_eq!(events, vec![AnimationEvent::Progress(0.5)]);
296
297 anim.tick(MS_250);
299 assert!(anim.drain_events().is_empty());
300 }
301
302 #[test]
303 fn multiple_thresholds() {
304 let mut anim = Callbacks::new(Fade::new(SEC_1))
305 .at_progress(0.25)
306 .at_progress(0.75);
307
308 anim.tick(MS_500); let events = anim.drain_events();
310 assert_eq!(events, vec![AnimationEvent::Progress(0.25)]);
311
312 anim.tick(MS_500); let events = anim.drain_events();
314 assert_eq!(events, vec![AnimationEvent::Progress(0.75)]);
315 }
316
317 #[test]
318 fn all_events_in_order() {
319 let mut anim = Callbacks::new(Fade::new(MS_500))
320 .on_start()
321 .at_progress(0.5)
322 .on_complete();
323
324 anim.tick(MS_500); let events = anim.drain_events();
326 assert_eq!(
327 events,
328 vec![
329 AnimationEvent::Started,
330 AnimationEvent::Progress(0.5),
331 AnimationEvent::Completed,
332 ]
333 );
334 }
335
336 #[test]
337 fn reset_allows_events_to_fire_again() {
338 let mut anim = Callbacks::new(Fade::new(MS_500)).on_start().on_complete();
339 anim.tick(SEC_1);
340 let _ = anim.drain_events();
341
342 anim.reset();
343 anim.tick(SEC_1);
344 let events = anim.drain_events();
345 assert_eq!(
346 events,
347 vec![AnimationEvent::Started, AnimationEvent::Completed]
348 );
349 }
350
351 #[test]
352 fn drain_clears_queue() {
353 let mut anim = Callbacks::new(Fade::new(SEC_1)).on_start();
354 anim.tick(MS_100);
355 assert_eq!(anim.pending_event_count(), 1);
356
357 let _ = anim.drain_events();
358 assert_eq!(anim.pending_event_count(), 0);
359 }
360
361 #[test]
362 fn inner_access() {
363 let anim = Callbacks::new(Fade::new(SEC_1));
364 assert!(!anim.inner().is_complete());
365 }
366
367 #[test]
368 fn inner_mut_access() {
369 let mut anim = Callbacks::new(Fade::new(SEC_1));
370 anim.inner_mut().tick(SEC_1);
371 assert!(anim.inner().is_complete());
372 }
373
374 #[test]
375 fn animation_trait_value_delegates() {
376 let mut anim = Callbacks::new(Fade::new(SEC_1));
377 anim.tick(MS_500);
378 assert!((anim.value() - 0.5).abs() < 0.02);
379 }
380
381 #[test]
382 fn animation_trait_is_complete_delegates() {
383 let mut anim = Callbacks::new(Fade::new(MS_100));
384 assert!(!anim.is_complete());
385 anim.tick(MS_100);
386 assert!(anim.is_complete());
387 }
388
389 #[test]
390 fn threshold_clamped() {
391 let mut anim = Callbacks::new(Fade::new(SEC_1))
392 .at_progress(-0.5) .at_progress(1.5); anim.tick(Duration::from_nanos(1)); let events = anim.drain_events();
397 assert!(events.contains(&AnimationEvent::Progress(0.0)));
399 }
400
401 #[test]
402 fn debug_format() {
403 let anim = Callbacks::new(Fade::new(MS_100)).on_start();
404 let dbg = format!("{:?}", anim);
405 assert!(dbg.contains("Callbacks"));
406 assert!(dbg.contains("pending_events"));
407 }
408
409 #[test]
410 fn overshoot_delegates() {
411 let mut anim = Callbacks::new(Fade::new(MS_100));
412 anim.tick(MS_500);
413 assert!(anim.overshoot() > Duration::ZERO);
414 }
415}