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 self.state.thresholds_fired.fill(false);
229 self.events.clear();
230 }
231
232 fn overshoot(&self) -> Duration {
233 self.inner.overshoot()
234 }
235}
236
237#[cfg(test)]
242mod tests {
243 use super::*;
244 use crate::animation::Fade;
245
246 const MS_100: Duration = Duration::from_millis(100);
247 const MS_250: Duration = Duration::from_millis(250);
248 const MS_500: Duration = Duration::from_millis(500);
249 const SEC_1: Duration = Duration::from_secs(1);
250
251 #[test]
252 fn no_events_configured() {
253 let mut anim = Callbacks::new(Fade::new(SEC_1));
254 anim.tick(MS_500);
255 assert!(anim.drain_events().is_empty());
256 }
257
258 #[test]
259 fn started_fires_on_first_tick() {
260 let mut anim = Callbacks::new(Fade::new(SEC_1)).on_start();
261 anim.tick(MS_100);
262 let events = anim.drain_events();
263 assert_eq!(events, vec![AnimationEvent::Started]);
264
265 anim.tick(MS_100);
267 assert!(anim.drain_events().is_empty());
268 }
269
270 #[test]
271 fn completed_fires_when_done() {
272 let mut anim = Callbacks::new(Fade::new(MS_500)).on_complete();
273 anim.tick(MS_250);
274 assert!(anim.drain_events().is_empty()); anim.tick(MS_500); let events = anim.drain_events();
278 assert_eq!(events, vec![AnimationEvent::Completed]);
279
280 anim.tick(MS_100);
282 assert!(anim.drain_events().is_empty());
283 }
284
285 #[test]
286 fn progress_threshold_fires_once() {
287 let mut anim = Callbacks::new(Fade::new(SEC_1)).at_progress(0.5);
288 anim.tick(MS_250);
289 assert!(anim.drain_events().is_empty()); anim.tick(MS_500); let events = anim.drain_events();
293 assert_eq!(events, vec![AnimationEvent::Progress(0.5)]);
294
295 anim.tick(MS_250);
297 assert!(anim.drain_events().is_empty());
298 }
299
300 #[test]
301 fn multiple_thresholds() {
302 let mut anim = Callbacks::new(Fade::new(SEC_1))
303 .at_progress(0.25)
304 .at_progress(0.75);
305
306 anim.tick(MS_500); let events = anim.drain_events();
308 assert_eq!(events, vec![AnimationEvent::Progress(0.25)]);
309
310 anim.tick(MS_500); let events = anim.drain_events();
312 assert_eq!(events, vec![AnimationEvent::Progress(0.75)]);
313 }
314
315 #[test]
316 fn all_events_in_order() {
317 let mut anim = Callbacks::new(Fade::new(MS_500))
318 .on_start()
319 .at_progress(0.5)
320 .on_complete();
321
322 anim.tick(MS_500); let events = anim.drain_events();
324 assert_eq!(
325 events,
326 vec![
327 AnimationEvent::Started,
328 AnimationEvent::Progress(0.5),
329 AnimationEvent::Completed,
330 ]
331 );
332 }
333
334 #[test]
335 fn reset_allows_events_to_fire_again() {
336 let mut anim = Callbacks::new(Fade::new(MS_500)).on_start().on_complete();
337 anim.tick(SEC_1);
338 let _ = anim.drain_events();
339
340 anim.reset();
341 anim.tick(SEC_1);
342 let events = anim.drain_events();
343 assert_eq!(
344 events,
345 vec![AnimationEvent::Started, AnimationEvent::Completed]
346 );
347 }
348
349 #[test]
350 fn drain_clears_queue() {
351 let mut anim = Callbacks::new(Fade::new(SEC_1)).on_start();
352 anim.tick(MS_100);
353 assert_eq!(anim.pending_event_count(), 1);
354
355 let _ = anim.drain_events();
356 assert_eq!(anim.pending_event_count(), 0);
357 }
358
359 #[test]
360 fn inner_access() {
361 let anim = Callbacks::new(Fade::new(SEC_1));
362 assert!(!anim.inner().is_complete());
363 }
364
365 #[test]
366 fn inner_mut_access() {
367 let mut anim = Callbacks::new(Fade::new(SEC_1));
368 anim.inner_mut().tick(SEC_1);
369 assert!(anim.inner().is_complete());
370 }
371
372 #[test]
373 fn animation_trait_value_delegates() {
374 let mut anim = Callbacks::new(Fade::new(SEC_1));
375 anim.tick(MS_500);
376 assert!((anim.value() - 0.5).abs() < 0.02);
377 }
378
379 #[test]
380 fn animation_trait_is_complete_delegates() {
381 let mut anim = Callbacks::new(Fade::new(MS_100));
382 assert!(!anim.is_complete());
383 anim.tick(MS_100);
384 assert!(anim.is_complete());
385 }
386
387 #[test]
388 fn threshold_clamped() {
389 let mut anim = Callbacks::new(Fade::new(SEC_1))
390 .at_progress(-0.5) .at_progress(1.5); anim.tick(Duration::from_nanos(1)); let events = anim.drain_events();
395 assert!(events.contains(&AnimationEvent::Progress(0.0)));
397 }
398
399 #[test]
400 fn debug_format() {
401 let anim = Callbacks::new(Fade::new(MS_100)).on_start();
402 let dbg = format!("{:?}", anim);
403 assert!(dbg.contains("Callbacks"));
404 assert!(dbg.contains("pending_events"));
405 }
406
407 #[test]
408 fn overshoot_delegates() {
409 let mut anim = Callbacks::new(Fade::new(MS_100));
410 anim.tick(MS_500);
411 assert!(anim.overshoot() > Duration::ZERO);
412 }
413}