1use crate::spring::{MotionModel, MotionScalarSample};
4use crate::value::MotionValue;
5use crate::{
6 MotionPolicyInput, MotionPolicyReport, MotionRunState, MotionSpec, validate_motion_policy,
7};
8use std::time::{Duration, Instant};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum MotionFrameDemand {
13 Idle,
15 NeedsFrame(MotionFrameReason),
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum MotionFrameReason {
22 UpdateRender,
24}
25
26impl MotionFrameDemand {
27 pub const fn needs_frame(self) -> bool {
29 matches!(self, Self::NeedsFrame(_))
30 }
31
32 pub const fn reason(self) -> Option<MotionFrameReason> {
34 match self {
35 Self::Idle => None,
36 Self::NeedsFrame(reason) => Some(reason),
37 }
38 }
39
40 fn from_active(active: bool) -> Self {
41 if active {
42 Self::NeedsFrame(MotionFrameReason::UpdateRender)
43 } else {
44 Self::Idle
45 }
46 }
47
48 pub const fn combine(self, other: Self) -> Self {
50 match (self, other) {
51 (Self::NeedsFrame(reason), _) | (_, Self::NeedsFrame(reason)) => {
52 Self::NeedsFrame(reason)
53 }
54 (Self::Idle, Self::Idle) => Self::Idle,
55 }
56 }
57
58 pub fn combine_all(demands: impl IntoIterator<Item = Self>) -> Self {
60 demands.into_iter().fold(Self::Idle, Self::combine)
61 }
62}
63
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
66pub struct MotionClockSample {
67 elapsed: Duration,
68 delta: Duration,
69 clamped: bool,
70}
71
72impl MotionClockSample {
73 pub const ZERO: Self = Self {
75 elapsed: Duration::ZERO,
76 delta: Duration::ZERO,
77 clamped: false,
78 };
79
80 pub fn from_elapsed(previous_elapsed: Duration, requested_elapsed: Duration) -> Self {
86 if requested_elapsed < previous_elapsed {
87 Self {
88 elapsed: previous_elapsed,
89 delta: Duration::ZERO,
90 clamped: true,
91 }
92 } else {
93 Self {
94 elapsed: requested_elapsed,
95 delta: requested_elapsed - previous_elapsed,
96 clamped: false,
97 }
98 }
99 }
100
101 pub fn from_instant(started_at: Instant, now: Instant) -> Self {
103 Self::from_elapsed(Duration::ZERO, now.saturating_duration_since(started_at))
104 }
105
106 pub fn from_instants(started_at: Instant, previous_now: Instant, now: Instant) -> Self {
108 Self::from_elapsed(
109 previous_now.saturating_duration_since(started_at),
110 now.saturating_duration_since(started_at),
111 )
112 }
113
114 pub const fn elapsed(self) -> Duration {
116 self.elapsed
117 }
118
119 pub const fn delta(self) -> Duration {
121 self.delta
122 }
123
124 pub const fn clamped(self) -> bool {
126 self.clamped
127 }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum MotionExecutionState {
133 Immediate,
135 Scheduled,
137}
138
139impl MotionExecutionState {
140 pub const fn is_immediate(self) -> bool {
142 matches!(self, Self::Immediate)
143 }
144
145 pub const fn is_scheduled(self) -> bool {
147 matches!(self, Self::Scheduled)
148 }
149}
150
151#[derive(Debug, Clone, PartialEq)]
153pub struct MotionExecutionPlan {
154 model: MotionModel,
155 policy_report: MotionPolicyReport,
156 state: MotionExecutionState,
157}
158
159impl MotionExecutionPlan {
160 pub fn resolve(input: MotionPolicyInput) -> Self {
163 let requested_model = input.model();
164 let policy_report = validate_motion_policy(input);
165 let model = if policy_report.is_ok() {
166 requested_model
167 } else {
168 MotionModel::timeline(MotionSpec::immediate())
169 };
170 let state = if model.is_immediate() {
171 MotionExecutionState::Immediate
172 } else {
173 MotionExecutionState::Scheduled
174 };
175 Self {
176 model,
177 policy_report,
178 state,
179 }
180 }
181
182 pub const fn model(&self) -> MotionModel {
184 self.model
185 }
186
187 pub const fn policy_report(&self) -> &MotionPolicyReport {
189 &self.policy_report
190 }
191
192 pub const fn state(&self) -> MotionExecutionState {
194 self.state
195 }
196
197 pub const fn is_immediate(&self) -> bool {
199 self.state.is_immediate()
200 }
201
202 pub fn into_parts(self) -> (MotionModel, MotionPolicyReport, MotionExecutionState) {
204 (self.model, self.policy_report, self.state)
205 }
206}
207
208#[derive(Debug, Clone, PartialEq)]
210pub struct MotionScalarTrack {
211 model: MotionModel,
212 value: MotionValue,
213 target: f32,
214 initial_velocity: f32,
215 started_at: Duration,
216 cancelled_at: Option<Duration>,
217 finished_at: Option<Duration>,
218}
219
220impl MotionScalarTrack {
221 pub fn start(
223 model: MotionModel,
224 from: f32,
225 target: f32,
226 initial_velocity: f32,
227 started_at: Duration,
228 ) -> Self {
229 Self {
230 model,
231 value: MotionValue::new(from),
232 target,
233 initial_velocity,
234 started_at,
235 cancelled_at: None,
236 finished_at: None,
237 }
238 }
239
240 pub fn immediate(value: f32, started_at: Duration) -> Self {
242 Self::start(
243 MotionModel::timeline(MotionSpec::immediate()),
244 value,
245 value,
246 0.0,
247 started_at,
248 )
249 }
250
251 pub const fn model(&self) -> MotionModel {
253 self.model
254 }
255
256 pub const fn from(&self) -> f32 {
258 self.value.current()
259 }
260
261 pub const fn target(&self) -> f32 {
263 self.target
264 }
265
266 pub const fn initial_velocity(&self) -> f32 {
268 self.initial_velocity
269 }
270
271 pub const fn started_at(&self) -> Duration {
273 self.started_at
274 }
275
276 pub const fn cancelled_at(&self) -> Option<Duration> {
278 self.cancelled_at
279 }
280
281 pub const fn finished_at(&self) -> Option<Duration> {
283 self.finished_at
284 }
285
286 pub fn cancel_at(&mut self, cancelled_at: Duration) {
288 if self.finished_at.is_none() {
289 self.cancelled_at = Some(cancelled_at);
290 }
291 }
292
293 pub fn finish_at(&mut self, finished_at: Duration) {
295 self.finished_at = Some(finished_at);
296 self.cancelled_at = None;
297 }
298
299 pub fn retarget(&self, model: MotionModel, target: f32, now: Duration) -> Self {
301 let sample = self.sample_at(now);
302 Self::start(model, sample.value(), target, sample.velocity(), now)
303 }
304
305 pub fn sample_at(&self, now: Duration) -> MotionScalarSample {
307 if let Some(finished_at) = self.finished_at {
308 return MotionScalarSample::new(
309 MotionRunState::Completed,
310 finished_at.saturating_sub(self.started_at),
311 self.target,
312 0.0,
313 self.target,
314 );
315 }
316
317 let effective_now = self.cancelled_at.unwrap_or(now);
318 let elapsed = effective_now.saturating_sub(self.started_at);
319 let mut sample = self.model.sample_scalar_elapsed(
320 self.value.current(),
321 self.target,
322 self.initial_velocity,
323 elapsed,
324 );
325 if self.cancelled_at.is_some() && sample.state().is_active() {
326 sample = MotionScalarSample::new(
327 MotionRunState::Cancelled,
328 sample.elapsed(),
329 sample.value(),
330 sample.velocity(),
331 sample.target(),
332 );
333 }
334 sample
335 }
336
337 pub fn frame_demand_at(&self, now: Duration) -> MotionFrameDemand {
339 MotionFrameDemand::from_active(self.sample_at(now).is_active())
340 }
341}
342
343#[derive(Debug, Clone, Copy, PartialEq)]
345pub struct MotionScalarExecutionSample {
346 sample: MotionScalarSample,
347 complete: bool,
348 frame_demand: MotionFrameDemand,
349}
350
351impl MotionScalarExecutionSample {
352 const fn new(
353 sample: MotionScalarSample,
354 complete: bool,
355 frame_demand: MotionFrameDemand,
356 ) -> Self {
357 Self {
358 sample,
359 complete,
360 frame_demand,
361 }
362 }
363
364 pub const fn scalar_sample(self) -> MotionScalarSample {
366 self.sample
367 }
368
369 pub const fn value(self) -> f32 {
371 self.sample.value()
372 }
373
374 pub const fn complete(self) -> bool {
376 self.complete
377 }
378
379 pub const fn frame_demand(self) -> MotionFrameDemand {
381 self.frame_demand
382 }
383}
384
385#[derive(Debug, Clone, Copy, PartialEq)]
387pub struct MotionProgressSample {
388 sample: MotionScalarExecutionSample,
389}
390
391impl MotionProgressSample {
392 const fn new(sample: MotionScalarExecutionSample) -> Self {
393 Self { sample }
394 }
395
396 pub const fn scalar_sample(self) -> MotionScalarExecutionSample {
398 self.sample
399 }
400
401 pub fn progress(self) -> f32 {
403 self.sample.value().clamp(0.0, 1.0)
404 }
405
406 pub const fn complete(self) -> bool {
408 self.sample.complete()
409 }
410
411 pub const fn frame_demand(self) -> MotionFrameDemand {
413 self.sample.frame_demand()
414 }
415}
416
417#[derive(Debug, Clone, PartialEq)]
419pub struct MotionScalarExecution {
420 plan: MotionExecutionPlan,
421 track: MotionScalarTrack,
422}
423
424impl MotionScalarExecution {
425 pub fn start(
427 plan: MotionExecutionPlan,
428 from: f32,
429 target: f32,
430 initial_velocity: f32,
431 started_at: Duration,
432 ) -> Self {
433 let track =
434 MotionScalarTrack::start(plan.model(), from, target, initial_velocity, started_at);
435 Self { plan, track }
436 }
437
438 pub fn start_resolved(
440 input: MotionPolicyInput,
441 from: f32,
442 target: f32,
443 initial_velocity: f32,
444 started_at: Duration,
445 ) -> Self {
446 Self::start(
447 MotionExecutionPlan::resolve(input),
448 from,
449 target,
450 initial_velocity,
451 started_at,
452 )
453 }
454
455 pub const fn plan(&self) -> &MotionExecutionPlan {
457 &self.plan
458 }
459
460 pub const fn track(&self) -> &MotionScalarTrack {
462 &self.track
463 }
464
465 pub const fn model(&self) -> MotionModel {
467 self.plan.model()
468 }
469
470 pub const fn policy_report(&self) -> &MotionPolicyReport {
472 self.plan.policy_report()
473 }
474
475 pub const fn state(&self) -> MotionExecutionState {
477 self.plan.state()
478 }
479
480 pub fn sample_at(&self, now: Duration) -> MotionScalarExecutionSample {
482 let sample = self.track.sample_at(now);
483 let complete = self.plan.is_immediate() || sample.reached_final_state();
484 let frame_demand = if complete {
485 MotionFrameDemand::Idle
486 } else {
487 MotionFrameDemand::from_active(sample.is_active())
488 };
489 MotionScalarExecutionSample::new(sample, complete, frame_demand)
490 }
491
492 pub fn sample_clock(&self, clock: MotionClockSample) -> MotionScalarExecutionSample {
494 self.sample_at(clock.elapsed())
495 }
496
497 pub fn sample_since(&self, started_at: Instant, now: Instant) -> MotionScalarExecutionSample {
500 self.sample_at(now.saturating_duration_since(started_at))
501 }
502}
503
504#[derive(Debug, Clone, PartialEq)]
510pub struct MotionProgressExecution {
511 execution: MotionScalarExecution,
512 started_at: Instant,
513}
514
515impl MotionProgressExecution {
516 pub fn start(plan: MotionExecutionPlan, started_at: Instant) -> Self {
518 Self {
519 execution: MotionScalarExecution::start(plan, 0.0, 1.0, 0.0, Duration::ZERO),
520 started_at,
521 }
522 }
523
524 pub fn start_resolved(input: MotionPolicyInput, started_at: Instant) -> Self {
526 Self::start(MotionExecutionPlan::resolve(input), started_at)
527 }
528
529 pub const fn scalar_execution(&self) -> &MotionScalarExecution {
531 &self.execution
532 }
533
534 pub const fn plan(&self) -> &MotionExecutionPlan {
536 self.execution.plan()
537 }
538
539 pub const fn model(&self) -> MotionModel {
541 self.execution.model()
542 }
543
544 pub const fn policy_report(&self) -> &MotionPolicyReport {
546 self.execution.policy_report()
547 }
548
549 pub const fn state(&self) -> MotionExecutionState {
551 self.execution.state()
552 }
553
554 pub const fn started_at(&self) -> Instant {
556 self.started_at
557 }
558
559 pub fn sample_at(&self, now: Duration) -> MotionProgressSample {
561 MotionProgressSample::new(self.execution.sample_at(now))
562 }
563
564 pub fn sample_clock(&self, clock: MotionClockSample) -> MotionProgressSample {
566 MotionProgressSample::new(self.execution.sample_clock(clock))
567 }
568
569 pub fn sample_since(&self, now: Instant) -> MotionProgressSample {
572 MotionProgressSample::new(self.execution.sample_since(self.started_at, now))
573 }
574}
575
576#[derive(Debug, Clone, PartialEq)]
578pub struct MotionScalarTrackSample<K> {
579 key: K,
580 sample: MotionScalarSample,
581}
582
583impl<K> MotionScalarTrackSample<K> {
584 pub const fn new(key: K, sample: MotionScalarSample) -> Self {
586 Self { key, sample }
587 }
588
589 pub const fn key(&self) -> &K {
591 &self.key
592 }
593
594 pub const fn sample(&self) -> MotionScalarSample {
596 self.sample
597 }
598}
599
600#[derive(Debug, Clone, PartialEq)]
602pub struct MotionScalarControllerSample<K> {
603 tracks: Vec<MotionScalarTrackSample<K>>,
604 frame_demand: MotionFrameDemand,
605}
606
607impl<K> MotionScalarControllerSample<K> {
608 pub fn new(tracks: Vec<MotionScalarTrackSample<K>>, frame_demand: MotionFrameDemand) -> Self {
610 Self {
611 tracks,
612 frame_demand,
613 }
614 }
615
616 pub fn tracks(&self) -> &[MotionScalarTrackSample<K>] {
618 &self.tracks
619 }
620
621 pub const fn frame_demand(&self) -> MotionFrameDemand {
623 self.frame_demand
624 }
625
626 pub const fn complete(&self) -> bool {
628 !self.frame_demand.needs_frame()
629 }
630}
631
632impl<K: PartialEq> MotionScalarControllerSample<K> {
633 pub fn track(&self, key: &K) -> Option<&MotionScalarTrackSample<K>> {
635 self.tracks.iter().find(|track| track.key() == key)
636 }
637}
638
639#[derive(Debug, Clone, PartialEq)]
641pub struct MotionScalarController<K> {
642 tracks: Vec<(K, MotionScalarTrack)>,
643}
644
645impl<K> Default for MotionScalarController<K> {
646 fn default() -> Self {
647 Self::new()
648 }
649}
650
651impl<K> MotionScalarController<K> {
652 pub const fn new() -> Self {
654 Self { tracks: Vec::new() }
655 }
656
657 pub fn tracks(&self) -> &[(K, MotionScalarTrack)] {
659 &self.tracks
660 }
661}
662
663impl<K: PartialEq> MotionScalarController<K> {
664 pub fn start(
666 &mut self,
667 key: K,
668 model: MotionModel,
669 from: f32,
670 target: f32,
671 initial_velocity: f32,
672 started_at: Duration,
673 ) {
674 let track = MotionScalarTrack::start(model, from, target, initial_velocity, started_at);
675 if let Some((_, existing)) = self
676 .tracks
677 .iter_mut()
678 .find(|(track_key, _)| track_key == &key)
679 {
680 *existing = track;
681 } else {
682 self.tracks.push((key, track));
683 }
684 }
685
686 pub fn set_immediate(&mut self, key: K, value: f32, now: Duration) {
688 let track = MotionScalarTrack::immediate(value, now);
689 if let Some((_, existing)) = self
690 .tracks
691 .iter_mut()
692 .find(|(track_key, _)| track_key == &key)
693 {
694 *existing = track;
695 } else {
696 self.tracks.push((key, track));
697 }
698 }
699
700 pub fn retarget(&mut self, key: K, model: MotionModel, target: f32, now: Duration) {
702 if let Some((_, existing)) = self
703 .tracks
704 .iter_mut()
705 .find(|(track_key, _)| track_key == &key)
706 {
707 *existing = existing.retarget(model, target, now);
708 } else {
709 self.start(key, model, target, target, 0.0, now);
710 }
711 }
712
713 pub fn cancel(&mut self, key: &K, now: Duration) {
715 if let Some((_, existing)) = self
716 .tracks
717 .iter_mut()
718 .find(|(track_key, _)| track_key == key)
719 {
720 existing.cancel_at(now);
721 }
722 }
723
724 pub fn finish(&mut self, key: &K, now: Duration) {
726 if let Some((_, existing)) = self
727 .tracks
728 .iter_mut()
729 .find(|(track_key, _)| track_key == key)
730 {
731 existing.finish_at(now);
732 }
733 }
734
735 pub fn frame_demand_at(&self, now: Duration) -> MotionFrameDemand {
737 MotionFrameDemand::combine_all(
738 self.tracks
739 .iter()
740 .map(|(_, track)| track.frame_demand_at(now)),
741 )
742 }
743}
744
745impl<K> MotionScalarController<K> {
746 pub fn prune_terminal_at(&mut self, now: Duration) -> usize {
748 let before = self.tracks.len();
749 self.tracks
750 .retain(|(_, track)| track.frame_demand_at(now).needs_frame());
751 before - self.tracks.len()
752 }
753}
754
755impl<K: Clone> MotionScalarController<K> {
756 pub fn sample_at(&self, now: Duration) -> MotionScalarControllerSample<K> {
758 let tracks = self
759 .tracks
760 .iter()
761 .map(|(key, track)| MotionScalarTrackSample::new(key.clone(), track.sample_at(now)))
762 .collect::<Vec<_>>();
763 let frame_demand = MotionFrameDemand::combine_all(
764 tracks
765 .iter()
766 .map(|track| MotionFrameDemand::from_active(track.sample().is_active())),
767 );
768 MotionScalarControllerSample::new(tracks, frame_demand)
769 }
770
771 pub fn sample_clock(&self, clock: MotionClockSample) -> MotionScalarControllerSample<K> {
773 self.sample_at(clock.elapsed())
774 }
775
776 pub fn sample_since(
779 &self,
780 started_at: Instant,
781 now: Instant,
782 ) -> MotionScalarControllerSample<K> {
783 self.sample_at(now.saturating_duration_since(started_at))
784 }
785}
786
787#[cfg(test)]
788mod tests {
789 use super::*;
790 use crate::{
791 MotionDuration, MotionEasing, MotionModel, MotionPolicyContext, MotionPreference,
792 MotionRunState, MotionSpec, MotionSpringSpec,
793 };
794 use std::time::Duration;
795
796 #[test]
797 fn grouped_tracks_report_frame_demand_until_every_track_completes() {
798 let mut controller = MotionScalarController::new();
799 let model = MotionModel::timeline(MotionSpec::new(
800 MotionPreference::Animated,
801 MotionDuration::Custom(Duration::from_millis(100)),
802 MotionEasing::Linear,
803 ));
804
805 controller.start("left", model, 0.0, 1.0, 0.0, Duration::ZERO);
806 controller.start("right", model, 1.0, 0.0, 0.0, Duration::ZERO);
807
808 let active = controller.sample_at(Duration::from_millis(50));
809 assert!(active.frame_demand().needs_frame());
810 assert_eq!(
811 active.frame_demand().reason(),
812 Some(MotionFrameReason::UpdateRender)
813 );
814 assert_eq!(active.tracks().len(), 2);
815 assert!(
816 active
817 .tracks()
818 .iter()
819 .all(|track| track.sample().is_active())
820 );
821
822 let complete = controller.sample_at(Duration::from_millis(120));
823 assert!(!complete.frame_demand().needs_frame());
824 assert!(
825 complete
826 .tracks()
827 .iter()
828 .all(|track| track.sample().reached_final_state())
829 );
830 assert!(complete.complete());
831 }
832
833 #[test]
834 fn frame_demand_combines_idle_and_active_with_stable_reason() {
835 let active = MotionFrameDemand::NeedsFrame(MotionFrameReason::UpdateRender);
836
837 assert_eq!(MotionFrameDemand::Idle.combine(active), active);
838 assert_eq!(active.combine(MotionFrameDemand::Idle), active);
839 assert_eq!(
840 MotionFrameDemand::combine_all([
841 MotionFrameDemand::Idle,
842 active,
843 MotionFrameDemand::Idle,
844 ]),
845 active
846 );
847 }
848
849 #[test]
850 fn clock_sample_clamps_non_monotonic_elapsed_time() {
851 let sample =
852 MotionClockSample::from_elapsed(Duration::from_millis(40), Duration::from_millis(15));
853
854 assert_eq!(sample.elapsed(), Duration::from_millis(40));
855 assert_eq!(sample.delta(), Duration::ZERO);
856 assert!(sample.clamped());
857 }
858
859 #[test]
860 fn finishing_track_jumps_to_target_and_stops_frame_demand() {
861 let mut track = MotionScalarTrack::start(
862 MotionModel::timeline(MotionSpec::new(
863 MotionPreference::Animated,
864 MotionDuration::Custom(Duration::from_millis(200)),
865 MotionEasing::Linear,
866 )),
867 0.0,
868 1.0,
869 0.0,
870 Duration::ZERO,
871 );
872
873 track.finish_at(Duration::from_millis(40));
874 let sample = track.sample_at(Duration::from_millis(50));
875
876 assert_eq!(sample.state(), MotionRunState::Completed);
877 assert_eq!(sample.value(), 1.0);
878 assert!(sample.reached_final_state());
879 assert!(
880 !track
881 .frame_demand_at(Duration::from_millis(50))
882 .needs_frame()
883 );
884 }
885
886 #[test]
887 fn controller_prunes_terminal_tracks_after_cancel_and_finish() {
888 let mut controller = MotionScalarController::new();
889 let model = MotionModel::timeline(MotionSpec::new(
890 MotionPreference::Animated,
891 MotionDuration::Custom(Duration::from_millis(200)),
892 MotionEasing::Linear,
893 ));
894
895 controller.start("cancelled", model, 0.0, 1.0, 0.0, Duration::ZERO);
896 controller.start("finished", model, 0.0, 1.0, 0.0, Duration::ZERO);
897 controller.start("active", model, 0.0, 1.0, 0.0, Duration::ZERO);
898 controller.cancel(&"cancelled", Duration::from_millis(40));
899 controller.finish(&"finished", Duration::from_millis(40));
900
901 assert_eq!(controller.prune_terminal_at(Duration::from_millis(50)), 2);
902 assert_eq!(controller.tracks().len(), 1);
903 assert_eq!(controller.tracks()[0].0, "active");
904 assert!(
905 controller
906 .frame_demand_at(Duration::from_millis(50))
907 .needs_frame()
908 );
909 }
910
911 #[test]
912 fn retarget_preserves_sampled_value_and_velocity_for_one_track() {
913 let mut controller = MotionScalarController::new();
914 let model = MotionModel::spring(MotionSpringSpec::layout(MotionPreference::Animated));
915
916 controller.start("pane", model, 0.0, 1.0, 0.0, Duration::ZERO);
917 let before = controller.sample_at(Duration::from_millis(80));
918 let sampled = before.track(&"pane").expect("pane track").sample();
919
920 controller.retarget("pane", model, 2.0, Duration::from_millis(80));
921 let after = controller.sample_at(Duration::from_millis(80));
922 let retargeted = after.track(&"pane").expect("pane track").sample();
923
924 assert_eq!(retargeted.value(), sampled.value());
925 assert_eq!(retargeted.velocity(), sampled.velocity());
926 assert_eq!(retargeted.target(), 2.0);
927 assert!(after.frame_demand().needs_frame());
928 assert_eq!(
929 after.frame_demand().reason(),
930 Some(MotionFrameReason::UpdateRender)
931 );
932 }
933
934 #[test]
935 fn cancelling_track_is_terminal_without_reaching_final_state() {
936 let mut track = MotionScalarTrack::start(
937 MotionModel::timeline(MotionSpec::new(
938 MotionPreference::Animated,
939 MotionDuration::Custom(Duration::from_millis(200)),
940 MotionEasing::Linear,
941 )),
942 0.0,
943 1.0,
944 0.0,
945 Duration::ZERO,
946 );
947
948 track.cancel_at(Duration::from_millis(40));
949 let sample = track.sample_at(Duration::from_millis(100));
950
951 assert_eq!(sample.state(), MotionRunState::Cancelled);
952 assert!(!sample.reached_final_state());
953 assert!(
954 !track
955 .frame_demand_at(Duration::from_millis(100))
956 .needs_frame()
957 );
958 }
959
960 #[test]
961 fn immediate_track_never_requests_a_frame() {
962 let track = MotionScalarTrack::immediate(0.75, Duration::from_millis(10));
963
964 let sample = track.sample_at(Duration::from_millis(10));
965 assert_eq!(sample.state(), MotionRunState::Immediate);
966 assert_eq!(sample.value(), 0.75);
967 assert!(
968 !track
969 .frame_demand_at(Duration::from_millis(10))
970 .needs_frame()
971 );
972 }
973
974 #[test]
975 fn execution_plan_downgrades_policy_failures_to_immediate_motion() {
976 let requested_model = MotionModel::timeline(MotionSpec::new(
977 MotionPreference::Animated,
978 MotionDuration::Custom(Duration::from_millis(900)),
979 MotionEasing::Linear,
980 ));
981 let plan = MotionExecutionPlan::resolve(
982 MotionPolicyInput::new(MotionPolicyContext::CommittedLayout, requested_model)
983 .with_spatial_motion(true)
984 .with_reduced_motion_final_state(true),
985 );
986
987 assert!(!plan.policy_report().is_ok());
988 assert!(plan.is_immediate());
989 assert!(plan.model().is_immediate());
990 }
991
992 #[test]
993 fn scalar_execution_reports_completion_and_frame_demand_from_one_sample() {
994 let execution = MotionScalarExecution::start_resolved(
995 MotionPolicyInput::new(
996 MotionPolicyContext::CommittedLayout,
997 MotionModel::timeline(MotionSpec::new(
998 MotionPreference::Animated,
999 MotionDuration::Custom(Duration::from_millis(100)),
1000 MotionEasing::Linear,
1001 )),
1002 )
1003 .with_spatial_motion(true)
1004 .with_reduced_motion_final_state(true),
1005 0.0,
1006 1.0,
1007 0.0,
1008 Duration::ZERO,
1009 );
1010
1011 let midpoint = execution.sample_at(Duration::from_millis(50));
1012 assert_eq!(midpoint.value(), 0.5);
1013 assert!(!midpoint.complete());
1014 assert!(midpoint.frame_demand().needs_frame());
1015
1016 let complete = execution.sample_at(Duration::from_millis(120));
1017 assert_eq!(complete.value(), 1.0);
1018 assert!(complete.complete());
1019 assert!(!complete.frame_demand().needs_frame());
1020 }
1021
1022 #[test]
1023 fn reduced_motion_execution_publishes_final_value_without_frame_demand() {
1024 let execution = MotionScalarExecution::start_resolved(
1025 MotionPolicyInput::new(
1026 MotionPolicyContext::CommittedLayout,
1027 MotionModel::timeline(MotionSpec::committed_layout(MotionPreference::Reduced)),
1028 )
1029 .with_reduced_motion_final_state(true),
1030 0.0,
1031 1.0,
1032 0.0,
1033 Duration::ZERO,
1034 );
1035
1036 let sample = execution.sample_at(Duration::ZERO);
1037
1038 assert_eq!(sample.scalar_sample().state(), MotionRunState::Immediate);
1039 assert_eq!(sample.value(), 1.0);
1040 assert!(sample.complete());
1041 assert!(!sample.frame_demand().needs_frame());
1042 }
1043
1044 #[test]
1045 fn progress_execution_samples_normalized_lifecycle() {
1046 let started_at = Instant::now();
1047 let progress = MotionProgressExecution::start_resolved(
1048 MotionPolicyInput::new(
1049 MotionPolicyContext::CommittedLayout,
1050 MotionModel::timeline(MotionSpec::new(
1051 MotionPreference::Animated,
1052 MotionDuration::Custom(Duration::from_millis(100)),
1053 MotionEasing::Linear,
1054 )),
1055 )
1056 .with_spatial_motion(true)
1057 .with_reduced_motion_final_state(true),
1058 started_at,
1059 );
1060
1061 let midpoint = progress.sample_since(started_at + Duration::from_millis(50));
1062 assert_eq!(midpoint.progress(), 0.5);
1063 assert!(midpoint.frame_demand().needs_frame());
1064
1065 let complete = progress.sample_at(Duration::from_millis(120));
1066 assert_eq!(complete.progress(), 1.0);
1067 assert!(complete.complete());
1068 assert!(!complete.frame_demand().needs_frame());
1069 }
1070}