1use std::sync::Arc;
4
5mod error;
6
7pub use error::MotionBindingError;
8
9use crate::{
10 runtime::{Motion, MotionRuntime, PlaybackId},
11 spring::{Spring, SpringConfig},
12 timing::Timing,
13 traits::{Animatable, Animation, BoxAnimation},
14 tween::Tween,
15};
16
17#[derive(Debug, Clone)]
23pub struct TransitionContext<S, T> {
24 pub from_state: S,
26 pub to_state: S,
28 pub from: T,
30 pub to: T,
32}
33
34impl<S, T: Animatable> TransitionContext<S, T> {
35 #[must_use]
37 pub fn tween(self, timing: Timing) -> Tween<T> {
38 Tween::between(self.from, self.to, timing)
39 }
40
41 #[must_use]
43 pub fn spring(self, config: SpringConfig) -> Spring<T> {
44 Spring::new(self.from, self.to, config)
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
53pub struct MotionBindingState<S> {
54 previous: S,
55}
56
57impl<S> MotionBindingState<S> {
58 #[must_use]
60 pub const fn new(previous: S) -> Self {
61 Self { previous }
62 }
63
64 #[must_use]
66 pub const fn current(&self) -> &S {
67 &self.previous
68 }
69}
70
71type TransitionFactory<S, T> = Arc<dyn Fn(TransitionContext<S, T>) -> BoxAnimation<T> + 'static>;
72
73#[derive(Clone)]
74struct Transition<S, T: Animatable> {
75 from: S,
76 to: S,
77 factory: TransitionFactory<S, T>,
78}
79
80#[derive(Clone)]
125pub struct MotionBinding<S, T: Animatable> {
126 initial_state: S,
127 initial_target: T,
128 targets: Vec<(S, T)>,
129 transitions: Vec<Transition<S, T>>,
130 fallback: Option<TransitionFactory<S, T>>,
131}
132
133impl<S, T> MotionBinding<S, T>
134where
135 S: Clone + PartialEq + 'static,
136 T: Animatable,
137{
138 #[must_use]
140 pub fn new(initial_state: S, initial_target: T) -> Self {
141 Self {
142 initial_state: initial_state.clone(),
143 initial_target: initial_target.clone(),
144 targets: vec![(initial_state, initial_target)],
145 transitions: Vec::new(),
146 fallback: None,
147 }
148 }
149
150 #[must_use]
152 pub fn when(mut self, state: S, target: T) -> Self {
153 if state == self.initial_state {
154 self.initial_target = target.clone();
155 }
156 if let Some((_, existing)) = self
157 .targets
158 .iter_mut()
159 .find(|(existing, _)| existing == &state)
160 {
161 *existing = target;
162 } else {
163 self.targets.push((state, target));
164 }
165 self
166 }
167
168 #[must_use]
173 pub fn transition<A>(
174 mut self,
175 from: S,
176 to: S,
177 factory: impl Fn(TransitionContext<S, T>) -> A + 'static,
178 ) -> Self
179 where
180 A: Animation<T>,
181 {
182 let factory: TransitionFactory<S, T> = Arc::new(move |context| Box::new(factory(context)));
183 if let Some(existing) = self
184 .transitions
185 .iter_mut()
186 .find(|transition| transition.from == from && transition.to == to)
187 {
188 existing.factory = factory;
189 } else {
190 self.transitions.push(Transition { from, to, factory });
191 }
192 self
193 }
194
195 #[must_use]
200 pub fn fallback<A>(mut self, factory: impl Fn(TransitionContext<S, T>) -> A + 'static) -> Self
201 where
202 A: Animation<T>,
203 {
204 self.fallback = Some(Arc::new(move |context| Box::new(factory(context))));
205 self
206 }
207
208 #[must_use]
210 pub const fn initial_state(&self) -> &S {
211 &self.initial_state
212 }
213
214 pub fn target(&self, state: &S) -> Result<&T, MotionBindingError<S>> {
216 let target = self
217 .targets
218 .iter()
219 .find_map(|(candidate, target)| (candidate == state).then_some(target));
220
221 #[cfg(feature = "tracing")]
222 if target.is_none() {
223 tracing::debug!(
224 target: "aura_anim::binding",
225 state_type = std::any::type_name::<S>(),
226 value_type = std::any::type_name::<T>(),
227 "motion binding target lookup failed"
228 );
229 }
230 target.ok_or_else(|| MotionBindingError::MissingTarget(state.clone()))
231 }
232
233 #[must_use]
235 pub fn state(&self) -> MotionBindingState<S> {
236 MotionBindingState::new(self.initial_state.clone())
237 }
238
239 pub fn create_motion(&self, runtime: &mut MotionRuntime) -> (Motion<T>, MotionBindingState<S>) {
241 (runtime.motion(self.initial_target.clone()), self.state())
242 }
243
244 pub fn set_state(
250 &self,
251 binding_state: &mut MotionBindingState<S>,
252 next_state: S,
253 motion: Motion<T>,
254 runtime: &mut MotionRuntime,
255 ) -> Result<bool, MotionBindingError<S>> {
256 self.set_state_tracked(binding_state, next_state, motion, runtime)
257 .map(|playback| playback.is_some())
258 }
259
260 pub fn set_state_tracked(
266 &self,
267 binding_state: &mut MotionBindingState<S>,
268 next_state: S,
269 motion: Motion<T>,
270 runtime: &mut MotionRuntime,
271 ) -> Result<Option<PlaybackId>, MotionBindingError<S>> {
272 if binding_state.previous == next_state {
273 #[cfg(feature = "tracing")]
274 tracing::trace!(
275 target: "aura_anim::binding",
276 state_type = std::any::type_name::<S>(),
277 value_type = std::any::type_name::<T>(),
278 "motion binding state is unchanged"
279 );
280 return Ok(None);
281 }
282
283 let target = self.target(&next_state).cloned()?;
284 let previous = binding_state.previous.clone();
285 let exact_factory = self
286 .transitions
287 .iter()
288 .find(|transition| transition.from == previous && transition.to == next_state)
289 .map(|transition| &transition.factory);
290 let Some(factory) = exact_factory.or(self.fallback.as_ref()) else {
291 #[cfg(feature = "tracing")]
292 tracing::debug!(
293 target: "aura_anim::binding",
294 state_type = std::any::type_name::<S>(),
295 value_type = std::any::type_name::<T>(),
296 "motion binding transition lookup failed"
297 );
298 return Err(MotionBindingError::MissingTransition {
299 from: previous.clone(),
300 to: next_state.clone(),
301 });
302 };
303 let current = motion.value(runtime)?;
304 #[cfg(feature = "tracing")]
305 {
306 let uses_fallback = exact_factory.is_none();
307 tracing::debug!(
308 target: "aura_anim::binding",
309 state_type = std::any::type_name::<S>(),
310 value_type = std::any::type_name::<T>(),
311 uses_fallback,
312 "applying motion binding transition"
313 );
314 let _ = uses_fallback;
315 }
316 let animation = factory(TransitionContext {
317 from_state: previous,
318 to_state: next_state.clone(),
319 from: current,
320 to: target,
321 });
322
323 let playback = motion.play_tracked(animation, runtime)?;
324
325 binding_state.previous = next_state;
326 #[cfg(feature = "tracing")]
327 tracing::debug!(
328 target: "aura_anim::binding",
329 state_type = std::any::type_name::<S>(),
330 value_type = std::any::type_name::<T>(),
331 "committed motion binding state"
332 );
333 Ok(Some(playback))
334 }
335}
336
337#[cfg(test)]
338mod tests {
339 use super::{MotionBinding, MotionBindingError};
340 use crate::{
341 keyframes::Keyframes,
342 runtime::{MotionError, MotionRuntime},
343 spring::SpringConfig,
344 timeline::Sequence,
345 timing::{Duration, Timing},
346 traits::AnimationExt,
347 tween::Tween,
348 };
349 use float_cmp::assert_approx_eq;
350
351 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
352 enum State {
353 Idle,
354 Hovered,
355 Pressed,
356 Disabled,
357 }
358
359 fn binding() -> MotionBinding<State, f32> {
360 MotionBinding::new(State::Idle, 0.0)
361 .when(State::Hovered, 10.0)
362 .when(State::Pressed, 20.0)
363 .transition(State::Idle, State::Hovered, |context| {
364 context.tween(Timing::new(100.0))
365 })
366 .transition(State::Hovered, State::Pressed, |context| {
367 context.spring(SpringConfig::default())
368 })
369 .fallback(|context| context.tween(Timing::new(50.0)))
370 }
371
372 #[test]
373 fn exact_transition_uses_current_sampled_value() {
374 let binding = binding();
375 let mut runtime = MotionRuntime::new();
376 let (motion, mut state) = binding.create_motion(&mut runtime);
377
378 binding
379 .set_state(&mut state, State::Hovered, motion, &mut runtime)
380 .unwrap();
381 runtime.tick(Duration::from_millis(40.0));
382 assert_approx_eq!(f32, motion.value(&runtime).unwrap(), 4.0);
383
384 binding
385 .set_state(&mut state, State::Pressed, motion, &mut runtime)
386 .unwrap();
387
388 assert_approx_eq!(f32, motion.value(&runtime).unwrap(), 4.0);
389 assert_eq!(state.current(), &State::Pressed);
390 }
391
392 #[test]
393 fn fallback_handles_unlisted_state_pair() {
394 let binding = binding();
395 let mut runtime = MotionRuntime::new();
396 let (motion, mut state) = binding.create_motion(&mut runtime);
397
398 binding
399 .set_state(&mut state, State::Pressed, motion, &mut runtime)
400 .unwrap();
401 runtime.tick(Duration::from_millis(50.0));
402
403 assert_approx_eq!(f32, motion.value(&runtime).unwrap(), 20.0);
404 }
405
406 #[test]
407 fn tracked_state_change_returns_playback_for_completion_event() {
408 let binding = binding();
409 let mut runtime = MotionRuntime::new();
410 let (motion, mut state) = binding.create_motion(&mut runtime);
411
412 let playback = binding
413 .set_state_tracked(&mut state, State::Hovered, motion, &mut runtime)
414 .unwrap()
415 .unwrap();
416
417 assert_eq!(motion.playback(&runtime).unwrap(), playback);
418 assert_eq!(
419 binding
420 .set_state_tracked(&mut state, State::Hovered, motion, &mut runtime)
421 .unwrap(),
422 None
423 );
424
425 runtime.tick(Duration::from_millis(100.0));
426 let events = runtime.take_events();
427 assert_eq!(events.len(), 1);
428 assert!(events[0].is_completed_for(playback));
429 }
430
431 #[test]
432 fn failed_lookup_does_not_commit_state() {
433 let binding = binding();
434 let mut runtime = MotionRuntime::new();
435 let (motion, mut state) = binding.create_motion(&mut runtime);
436
437 let error = binding
438 .set_state(&mut state, State::Disabled, motion, &mut runtime)
439 .unwrap_err();
440
441 assert_eq!(error, MotionBindingError::MissingTarget(State::Disabled));
442 assert_eq!(state.current(), &State::Idle);
443 }
444
445 #[test]
446 fn shared_configuration_creates_independent_trackers() {
447 let binding = binding();
448 let mut runtime = MotionRuntime::new();
449 let (first, mut first_state) = binding.create_motion(&mut runtime);
450 let (second, second_state) = binding.create_motion(&mut runtime);
451
452 binding
453 .set_state(&mut first_state, State::Hovered, first, &mut runtime)
454 .unwrap();
455
456 assert_eq!(first_state.current(), &State::Hovered);
457 assert_eq!(second_state.current(), &State::Idle);
458 assert_approx_eq!(f32, second.value(&runtime).unwrap(), 0.0);
459 }
460
461 #[test]
462 fn missing_transition_is_reported_before_playback() {
463 let binding = MotionBinding::new(State::Idle, 0.0).when(State::Hovered, 1.0);
464 let mut runtime = MotionRuntime::new();
465 let (motion, mut state) = binding.create_motion(&mut runtime);
466
467 let error = binding
468 .set_state(&mut state, State::Hovered, motion, &mut runtime)
469 .unwrap_err();
470
471 assert_eq!(
472 error,
473 MotionBindingError::MissingTransition {
474 from: State::Idle,
475 to: State::Hovered,
476 }
477 );
478 assert!(!motion.is_active(&runtime).unwrap());
479 }
480
481 #[test]
482 fn runtime_errors_are_preserved_by_binding_errors() {
483 let binding = binding();
484 let mut runtime = MotionRuntime::new();
485 let (motion, mut state) = binding.create_motion(&mut runtime);
486 motion.remove(&mut runtime).unwrap();
487
488 let error = binding
489 .set_state(&mut state, State::Hovered, motion, &mut runtime)
490 .unwrap_err();
491
492 assert_eq!(
493 error,
494 MotionBindingError::Motion(MotionError::Removed { slot: 0 })
495 );
496 assert_eq!(state.current(), &State::Idle);
497 }
498
499 #[test]
500 fn factories_accept_keyframes_and_timeline_sources() {
501 let binding = MotionBinding::new(State::Idle, 0.0_f32)
502 .when(State::Hovered, 10.0)
503 .when(State::Pressed, 20.0)
504 .transition(State::Idle, State::Hovered, |context| {
505 Keyframes::new(context.from).push(100.0, context.to)
506 })
507 .transition(State::Hovered, State::Pressed, |context| {
508 Sequence::new(context.from).then(Tween::between(
509 context.from,
510 context.to,
511 Timing::new(100.0),
512 ))
513 });
514 let mut runtime = MotionRuntime::new();
515 let (motion, mut state) = binding.create_motion(&mut runtime);
516
517 binding
518 .set_state(&mut state, State::Hovered, motion, &mut runtime)
519 .unwrap();
520 runtime.tick(Duration::from_millis(100.0));
521 assert_approx_eq!(f32, motion.value(&runtime).unwrap(), 10.0);
522
523 binding
524 .set_state(&mut state, State::Pressed, motion, &mut runtime)
525 .unwrap();
526 runtime.tick(Duration::from_millis(100.0));
527 assert_approx_eq!(f32, motion.value(&runtime).unwrap(), 20.0);
528 }
529
530 #[test]
531 fn boxed_factories_remain_supported() {
532 let binding = MotionBinding::new(State::Idle, 0.0_f32)
533 .when(State::Hovered, 10.0)
534 .fallback(|context| context.tween(Timing::new(100.0)).boxed());
535 let mut runtime = MotionRuntime::new();
536 let (motion, mut state) = binding.create_motion(&mut runtime);
537
538 binding
539 .set_state(&mut state, State::Hovered, motion, &mut runtime)
540 .unwrap();
541 runtime.tick(Duration::from_millis(100.0));
542
543 assert_approx_eq!(f32, motion.value(&runtime).unwrap(), 10.0);
544 }
545}