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