1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
use crate::clock::FrameInstant;
use crate::reactive::Signal;
use crate::tree::LayoutCtx;
use crate::animation::{
Animatable, ExitTo, Keyframes, SpringState, Transition, TransitionConfig, carry_velocity,
};
/// A sequence a property plays, and what decides when it runs.
///
/// Unlike everything else in an `AnimationState` this has no target: while it
/// runs it *replaces* the declared value, and when it ends the property is
/// handed back — the rule CSS gives an animation over a normal declaration.
struct Timeline<T> {
keyframes: Keyframes<T>,
/// When the current run started, if one is running.
playing: Option<FrameInstant>,
play: Play,
}
/// What decides when a sequence runs.
///
/// Two states rather than two `Option`s, so "waiting for a trigger it does not
/// have" cannot be written down.
enum Play {
/// Every change of the trigger plays it once, and this is the count last
/// acted on. A count rather than a flag, because two refusals in a row are
/// two events and a signal that stays equal notifies nobody. Asking and
/// committing live in one place, so the pass that subscribes and the pass
/// that plays cannot disagree about what has been seen.
OnTrigger { trigger: Signal<u32>, last: u32 },
/// Nothing asks: it plays because the widget exists. `played` is what makes
/// that happen exactly once.
OnMount { played: bool },
}
/// A transition of no duration: a timeline speaks for its property while it
/// plays, and outside it the declared value applies at once, exactly as it
/// would with no animation at all.
pub(super) fn instant_transition() -> Transition {
Transition::new(0.0, crate::animation::TimingFunction::Linear)
}
/// Result of advancing an animation, indicating whether the value changed
#[derive(Debug, Clone, PartialEq)]
pub enum AdvanceResult<T> {
/// Value did not change (animation not running or same value)
NoChange,
/// Value changed to a new value
Changed(T),
}
impl<T> AdvanceResult<T> {
/// Returns true if the value changed
pub fn is_changed(&self) -> bool {
matches!(self, AdvanceResult::Changed(_))
}
}
/// Animation state for animatable properties
pub struct AnimationState<T: Animatable> {
/// Current interpolated value
current: T,
/// Target value from Signal
target: T,
/// Value when animation started
start: T,
/// Progress from 0.0 to 1.0 (or beyond for overshoot)
progress: f32,
/// Time when animation started
/// When the running segment began, if one is running. `None` until the
/// first one does: a state that has never animated has no start, and a
/// placeholder instant would be a time nobody chose.
start_time: Option<FrameInstant>,
/// Forward transition (used when value increases or no reverse is set)
transition: Transition,
/// Optional reverse transition (used when value decreases)
reverse_transition: Option<Transition>,
/// Whether the current animation is using the reverse transition
using_reverse: bool,
/// Spring state (for spring timing functions)
spring_state: Option<SpringState>,
/// Whether a real layout has placed this. A measure pass writes the value
/// without setting it — see [`set_immediate`](Self::set_immediate).
initialized: bool,
/// Previous value for change detection
prev_value: Option<T>,
/// Where this property begins the one time the widget appears, until the
/// first layout takes it. `None` for almost every declaration — see
/// [`Animated::entering_from`](crate::animation::Animated::entering_from)
/// — and boxed for the reason `timeline` below is: this struct is held
/// twelve times over by every container that animates anything, and a
/// `Shadow` inline here would widen all twelve to carry a value almost
/// none of them has and none of them keeps past the first layout.
enter_from: Option<Box<T>>,
/// Where this property goes when the widget is removed, asked at removal —
/// see [`Animated::exiting_to`](crate::animation::Animated::exiting_to).
/// Boxed twice for the reason `enter_from` is boxed once: a closure is a
/// wide pointer, and almost no declaration has one.
exit_to: Option<Box<ExitTo<T>>>,
/// Whether an exit has begun: the property is heading for the value
/// `exit_to` gave at removal, and nothing retargets it until the exit is
/// cancelled or the widget is disposed.
exiting: bool,
/// A sequence to play on demand, and what plays it. Boxed and absent by
/// default: most declared properties ease to a target and never carry one,
/// so they pay a pointer rather than the struct.
timeline: Option<Box<Timeline<T>>>,
}
impl<T: Animatable> AnimationState<T> {
pub fn new(initial_value: T, config: impl Into<TransitionConfig>) -> Self {
let config = config.into();
let spring_state = if matches!(
config.forward.timing,
crate::animation::TimingFunction::Spring(_)
) {
Some(SpringState::new())
} else {
None
};
Self {
current: initial_value,
target: initial_value,
start: initial_value,
progress: 1.0, // Start completed
start_time: None,
transition: config.forward,
reverse_transition: config.reverse,
using_reverse: false,
spring_state,
initialized: false, // Not yet initialized with real content-based value
prev_value: None,
enter_from: None,
exit_to: None,
exiting: false,
timeline: None,
}
}
/// Declare where this property starts on the frame it first appears.
pub(crate) fn with_enter_from(mut self, from: Option<T>) -> Self {
self.enter_from = from.map(Box::new);
self
}
/// Declare where this property goes when the widget is removed.
pub(crate) fn with_exit_to(mut self, to: Option<ExitTo<T>>) -> Self {
self.exit_to = to.map(Box::new);
self
}
/// Begin the exit this property declared, if it declared one, and answer
/// whether it did.
///
/// The value is asked for now, not at build: the caller knows which way a
/// child leaves only when the change that removes it happens. Forward
/// whichever way it travels, for the reason [`begin_enter`](Self::begin_enter)
/// gives, and from wherever the property is — a spring keeps its momentum.
/// An exit to where the property already is has nothing to play, at rest
/// or in flight — a segment of no length gives momentum no direction to
/// carry — and leaves it at rest there.
/// A sequence playing stops speaking for the property: it is leaving.
pub(crate) fn begin_exit(&mut self, now: FrameInstant) -> bool {
let Some(exit_to) = self.exit_to.as_deref() else {
return false;
};
// Read for the moment of removal, like an event handler reads — not a
// place to subscribe from.
let to = crate::reactive::diagnostics::snapshot_zone(exit_to);
self.exiting = true;
if let Some(timeline) = self.timeline.as_deref_mut() {
timeline.playing = None;
}
self.using_reverse = false;
if to == self.current {
// Declared, but already there: nothing to wait for, whatever the
// timing. A spring in flight has no momentum to carry into a
// segment of no length — `carry_velocity` answers 0 for one — so
// playing it would only hold the property still until it settled
// (#691). Left with no spring, as a settled one is: one left in
// flight here would be stepped again by the next `advance` while
// another property keeps the row, and report itself moving until
// it settled.
self.target = to;
self.start = to;
self.progress = 1.0;
self.spring_state = None;
return true;
}
let carried = self.carried_velocity(&to);
self.target = to;
self.begin_segment(self.current, carried, now);
true
}
/// Whether this property declared somewhere to go when it is removed.
pub(crate) fn declares_exit(&self) -> bool {
self.exit_to.is_some()
}
/// Whether an exit has begun on this property — settled or not. Playing
/// is `is_animating`; this stays true once it has landed.
pub(crate) fn is_leaving(&self) -> bool {
self.exiting
}
/// Stop leaving. The property is retargeted to its declared value by the
/// next advance, from wherever the exit had taken it.
pub(crate) fn cancel_exit(&mut self) {
self.exiting = false;
}
/// Begin the enter this property declared, if it declared one, and answer
/// whether it did.
///
/// Every first initialisation goes through here, which is what makes an
/// enter reach every animatable property rather than the ones whose
/// initialiser remembered to ask. Taken and not read: an enter happens
/// once, so the first initialisation is the appearance and every one after
/// it is not.
///
/// The instant is a closure because the overwhelmingly common answer is
/// "no enter was declared", and asking a `Tree` for the time outside a
/// frame is a diagnostic rather than a free read.
pub(crate) fn begin_enter(
&mut self,
ctx: &mut LayoutCtx,
target: T,
now: impl FnOnce() -> FrameInstant,
) -> bool {
// A measure pass is not an appearance. It runs before the surface
// exists — `measure_natural_size` sizes a popup so the compositor can
// be told how big to make it — and it reads targets rather than
// in-flight values for exactly this reason. Consuming the enter there
// would play it against a surface that is not yet mapped, and there is
// no frame instant to play it against either.
if ctx.measuring() {
return false;
}
let Some(from) = self.enter_from.take() else {
return false;
};
let from = *from;
if from == target {
// Declared, but nowhere to travel from. Placing it is what every
// property without an enter does, and it costs no frame.
self.set_immediate(ctx, target);
return false;
}
self.set_immediate(ctx, from);
// Forward, whichever way it travels. `animate_to` would read the
// direction off `current`, which is now the enter value, so an enter
// that shrinks — a card settling down from a larger scale — would run
// the transition written for the value *later* decreasing, and fire
// its `on_complete`. In the pattern this feature exists for that
// callback destroys the popup, so appearing would close it.
self.using_reverse = false;
self.target = target;
self.begin_segment_from(from, now());
true
}
/// Get the currently active transition (forward or reverse).
fn active_transition(&self) -> &Transition {
if self.using_reverse {
self.reverse_transition.as_ref().unwrap_or(&self.transition)
} else {
&self.transition
}
}
/// Start animating to a new target value
pub fn animate_to(&mut self, new_target: T, now: FrameInstant) {
// Don't restart if we're already animating to this target
if new_target == self.target {
return;
}
// Detect direction and select transition
self.using_reverse =
self.reverse_transition.is_some() && T::is_reverse(&self.current, &new_target);
// Check if active transition is spring before mutating other fields
let is_spring = matches!(
self.active_transition().timing,
crate::animation::TimingFunction::Spring(_)
);
let carried = if is_spring {
self.carried_velocity(&new_target)
} else {
0.0
};
self.target = new_target;
self.begin_segment(self.current, carried, now);
}
/// Start a fresh run toward the current target, from rest.
fn begin_segment_from(&mut self, from: T, now: FrameInstant) {
self.begin_segment(from, 0.0, now);
}
/// The bookkeeping every new segment shares.
fn begin_segment(&mut self, from: T, carried: f32, now: FrameInstant) {
let is_spring = matches!(
self.active_transition().timing,
crate::animation::TimingFunction::Spring(_)
);
self.start = from;
self.current = from;
self.progress = 0.0;
self.start_time = Some(now);
self.spring_state = if is_spring {
Some(SpringState::moving_at(carried))
} else {
None
};
}
/// The velocity the running spring should carry into the new segment.
///
/// Zero unless a spring is actually in flight. Starting from rest is right
/// for a value that was standing still, and is what this did for every
/// target change before: a hover reversed mid-flight restarted from a stop
/// instead of turning around, and a spring that never keeps its momentum
/// is an easing curve wearing a spring's name.
///
/// The projection is in [`carry_velocity`]; what belongs here is the one
/// case where the answer is "none of it": a transition that has not
/// started moving yet. Its spring is created now and stepped only after
/// the delay, so whatever the value is doing at this instant is stale by
/// the time it would be released — at full strength, and against a value
/// that has not moved in the meantime.
fn carried_velocity(&self, new_target: &T) -> f32 {
let Some(spring) = &self.spring_state else {
return 0.0;
};
if self.active_transition().delay_ms > 0.0 {
return 0.0;
}
carry_velocity(
spring.velocity,
&self.start,
&self.target,
&self.current,
new_target,
)
}
/// Advance the animation and return whether the value changed
pub fn advance(&mut self, now: FrameInstant) -> AdvanceResult<T> {
// A sequence speaks for the property while it runs, and nothing else
// does — the same rule the cascade gives a CSS animation over a normal
// declaration.
if let Some(result) = self.advance_timeline(now) {
return result;
}
if self.progress >= 1.0 && self.spring_state.is_none() {
return AdvanceResult::NoChange;
}
let was_running = self.is_animating();
// Extract scalar transition values upfront to avoid borrow conflicts
// with self.spring_state. Copy SpringConfig (which is Copy) instead of
// cloning the entire TimingFunction (which may contain an Arc).
let active = self.active_transition();
let delay_ms = active.delay_ms;
let duration_ms = active.duration_ms;
let spring_config = match active.timing {
crate::animation::TimingFunction::Spring(config) => Some(config),
_ => None,
};
// No segment has begun, so no time has passed in one.
let Some(started) = self.start_time else {
return AdvanceResult::NoChange;
};
let elapsed = now.duration_since(started).as_secs_f32() * 1000.0;
let adjusted_elapsed = (elapsed - delay_ms).max(0.0);
if adjusted_elapsed <= 0.0 {
// Still in delay period
return AdvanceResult::NoChange;
}
// Calculate eased value based on timing function type
let eased_t = if let Some(ref mut spring_state) = self.spring_state {
// For spring animations: use real elapsed time in seconds (not normalized)
// This allows the spring to continue oscillating until it naturally settles
let elapsed_secs = adjusted_elapsed / 1000.0;
if let Some(ref config) = spring_config {
spring_state.step(elapsed_secs, config)
} else {
// Fallback: shouldn't happen, but use normalized time
adjusted_elapsed / duration_ms
}
} else {
// For non-spring animations: use normalized time 0..1
let t = (adjusted_elapsed / duration_ms).min(1.0);
// Safe to borrow self again — spring_state mutable borrow ended above
self.active_transition().timing.evaluate(t)
};
// Interpolate
let mut new_value = T::lerp(&self.start, &self.target, eased_t);
let mut settled = false;
// Update progress
if let Some(ref state) = self.spring_state {
// For spring animations, only mark complete when spring has settled
if state.is_settled(0.01) {
self.progress = 1.0;
// Settled means at rest. Keeping the state would let the next
// retarget inherit whatever velocity was still under the
// threshold, and rescale it by however short the new segment
// is — a spring that had visibly stopped starting the next one
// with a kick.
settled = true;
// Snap to exact target to avoid floating-point drift.
// The spring settles within 0.01 of the target, but downstream
// checks (e.g. Transform::is_translation_only) use much tighter
// tolerances (1e-6), so the lerped value must be exact.
new_value = self.target;
} else {
// Keep progress < 1.0 to continue animating
self.progress = 0.5;
}
} else {
// For non-spring animations, use time-based progress
let t = (adjusted_elapsed / duration_ms).min(1.0);
self.progress = t;
}
if settled {
self.spring_state = None;
}
// Check if value actually changed
let changed = self.prev_value.as_ref() != Some(&new_value);
self.current = new_value;
self.prev_value = Some(new_value);
// Completion edge: fire the transition's callback once per run.
// The callback may write signals or push surface commands; it must
// not touch the widget tree synchronously (it runs inside
// advance_animations).
if was_running
&& !self.is_animating()
&& let Some(cb) = self.active_transition().on_complete.clone()
{
// A completion callback reads for the current value, like an
// event handler — it is not a place to subscribe from.
crate::reactive::diagnostics::snapshot_zone(|| cb());
}
if changed {
AdvanceResult::Changed(new_value)
} else {
AdvanceResult::NoChange
}
}
/// How far a sequence declared on this property can reach, or zero where
/// there is no sequence.
///
/// A timeline's stops are values the property will hold and no signal
/// mentions, so anything sizing a reach from the declarations has to ask
/// this as well — see [`Keyframes::reach`].
pub(crate) fn sequence_reach(&self, measure: impl Fn(&T) -> f32) -> f32 {
self.timeline
.as_ref()
.map_or(0.0, |t| t.keyframes.reach(measure))
}
/// How far past its target this animation can travel, as a fraction of the
/// distance — the worse of the forward and reverse curves.
pub fn peak_overshoot(&self) -> f32 {
let forward = self.transition.timing.peak_overshoot();
match &self.reverse_transition {
Some(reverse) => forward.max(reverse.timing.peak_overshoot()),
None => forward,
}
}
/// Check if animation is still running
pub fn is_animating(&self) -> bool {
self.timeline.as_ref().is_some_and(|t| t.playing.is_some())
|| self.progress < 1.0
|| (self.spring_state.is_some() && self.progress < 0.99)
}
/// Give this property a sequence.
///
/// One declaration per property, so this replaces rather than merges:
/// a motion arrives with the value it moves, and a second `.rotate(..)`
/// restates the whole property.
///
/// A sequence with no trigger is pending from here: the first frame plays
/// it, because a wait starts when the widget appears rather than when
/// something asks. One that has a trigger waits for it, and records what
/// it holds now so a change is what plays it.
pub(crate) fn with_timeline(mut self, keyframes: Keyframes<T>) -> Self {
let play = match keyframes.trigger() {
Some(trigger) => Play::OnTrigger {
trigger,
last: trigger.get_untracked(),
},
None => Play::OnMount { played: false },
};
self.timeline = Some(Box::new(Timeline {
keyframes,
playing: None,
play,
}));
self
}
/// Whether this sequence is owed the one play it gets for existing.
///
/// Reads no signal, which is what lets the first layout ask it: a tracked
/// read there would register the trigger against whichever widget's scope
/// happens to be open, which is the parent's.
pub(crate) fn owes_its_first_play(&self) -> bool {
self.timeline
.as_ref()
.is_some_and(|t| matches!(t.play, Play::OnMount { played: false }))
}
/// Whether the trigger has moved since the sequence last played.
///
/// Reading the signal is the subscription, so the pass that asks this is
/// the pass that gets woken.
pub(crate) fn wants_play(&self) -> bool {
self.timeline.as_ref().is_some_and(|t| match t.play {
Play::OnTrigger { trigger, last } => trigger.get() != last,
Play::OnMount { played } => !played,
})
}
/// The same question, answered once: `true` hands over the play and marks
/// it taken, so nothing can ask twice for the same change.
pub(crate) fn take_play(&mut self) -> bool {
let Some(timeline) = &mut self.timeline else {
return false;
};
match timeline.play {
Play::OnTrigger { trigger, last } => {
let now = trigger.get();
if now == last {
return false;
}
timeline.play = Play::OnTrigger { trigger, last: now };
}
Play::OnMount { played: true } => return false,
Play::OnMount { played: false } => {
timeline.play = Play::OnMount { played: true };
}
}
true
}
/// Start the sequence, from the top. Playing it again while it runs
/// restarts it: the second refusal is not half a shake.
pub(crate) fn play(&mut self, now: FrameInstant) {
if let Some(timeline) = &mut self.timeline
&& !timeline.keyframes.is_empty()
{
timeline.playing = Some(now);
}
}
/// Advance the running timeline. `None` when there is none, or when the
/// one that was running has just handed the property back.
fn advance_timeline(&mut self, now: FrameInstant) -> Option<AdvanceResult<T>> {
// Both halves together, so a `playing` without a sequence to play
// cannot survive the question. On its own it would keep
// `is_animating` true for good: a surface asking for a frame every
// vsync with nothing to draw.
let Some(timeline) = &mut self.timeline else {
return None;
};
let started = timeline.playing?;
// Measured in `f64` and handed on as `f32`. An endless sequence never
// ends, so its distance from its start grows for the life of the
// widget, and an `f32` count of milliseconds loses a whole 60Hz frame
// of resolution after about thirty-seven hours and a whole run of a
// short sequence within a few weeks — at which point it shows one value
// for frame after frame. A bar is the thing that runs for weeks.
//
// Whole runs behind it say nothing about where it is now, so they are
// taken off before the number is narrowed. Taken off here rather than
// by moving `playing` forward: the reduction is computed from the start
// on every frame anyway, so a stored one would be a second place for
// the same arithmetic to be wrong in.
//
// The guard on the length is not paranoia: an endless sequence has no
// total to be past, so a run of no length would be a division by zero
// every frame for the life of the widget. What such a sequence *shows*
// is unchanged either way, which is why nothing here asserts it.
let since_start = now.duration_since(started).as_secs_f64() * 1000.0;
let duration = f64::from(timeline.keyframes.duration_ms());
let elapsed = if timeline.keyframes.total_ms().is_none() && duration > 0.0 {
(since_start % duration) as f32
} else {
since_start as f32
};
let Some(value) = timeline.keyframes.value_at(elapsed) else {
// Over. The property goes back to whatever declares it — by
// *animating* there from where the sequence left it, not by
// snapping to it. The declared transition was suspended for the
// duration, not cancelled, and ending it at the last frame is
// what made a card jump the moment a hover arrived mid-shake.
timeline.playing = None;
let landed = self.current;
self.begin_segment_from(landed, now);
// Returning `None` lets this frame run the ordinary path, so the
// hand-back is animated by the declared transition and reaches
// its completion edge — which is what fires `on_complete`.
return None;
};
let changed = self.prev_value.as_ref() != Some(&value);
self.current = value;
self.prev_value = Some(value);
Some(if changed {
AdvanceResult::Changed(value)
} else {
AdvanceResult::NoChange
})
}
/// Get current value
pub fn current(&self) -> &T {
&self.current
}
/// Get target value
pub fn target(&self) -> &T {
&self.target
}
/// Set value immediately without animation, and mark it placed — unless this
/// is a measure pass.
///
/// A measure pass places the value and does **not** mark it placed. It has
/// to place it: `measure_natural_size` sizes a popup before the surface
/// exists, and the number it reports is read off this animation — through
/// [`displayed`](Self::displayed), which answers with the target for exactly
/// that reason. What it must not do is spend the appearance.
/// `begin_enter` already declines there, saying in as many words that a
/// measure pass is not an appearance; marking the property placed would
/// contradict it one line later, because the layout that *is* the appearance
/// reads `is_initial()` to decide whether to look at the enter at all.
pub fn set_immediate(&mut self, ctx: &mut LayoutCtx, value: T) {
self.current = value;
self.target = value;
self.start = value;
self.progress = 1.0;
// A measure is not an appearance: it runs before the surface exists,
// and marking the property placed there would spend the enter that the
// layout after it is supposed to play. So the measure leaves it
// unplaced, and says that this subtree's answer belongs to the pass
// that asked — otherwise the layout after it would skip on the
// measure's cache and never appear at all.
if ctx.measuring() {
ctx.answer_depends_on_the_pass();
} else {
self.initialized = true;
}
}
/// Check if animation has never been initialized (first layout)
pub fn is_initial(&self) -> bool {
!self.initialized
}
}
impl<T: Animatable + Copy> AnimationState<T> {
/// The value this animation contributes right now.
///
/// Normally the in-flight value — that is the animation. While a natural
/// size is being measured it is the destination instead, so a
/// content-sized surface resizes once to where the animation is going and
/// the animation plays inside it, rather than asking the compositor for a
/// new size on every frame.
///
/// Every read of an animated value goes through here, so that rule is
/// stated once.
#[inline]
pub fn displayed(&self) -> T {
*self.current()
}
/// The same, for the pass laying out: where the animation is going while a
/// natural size is being measured, and where it is otherwise.
///
/// A value in flight is read differently by the two passes, so the subtree
/// around it answers only the one that asked — which is what the context is
/// told here, as it happens, by the read that decided it.
#[inline]
pub fn displayed_in(&self, ctx: &mut LayoutCtx) -> T {
if self.is_animating() {
ctx.answer_depends_on_the_pass();
}
if ctx.measuring() {
*self.target()
} else {
*self.current()
}
}
}
/// The animated value if an animation exists, the fallback otherwise.
#[inline]
pub fn get_animated_value<T: Animatable + Copy>(
anim: Option<&AnimationState<T>>,
fallback: impl FnOnce() -> T,
) -> T {
match anim {
Some(a) => a.displayed(),
None => fallback(),
}
}
#[cfg(test)]
mod tests {
use std::time::Instant;
use super::*;
use crate::tree::Tree;
/// The layout these tests read their values through. They are about what a
/// value answers, not about a widget, so the tree under it is empty.
fn laying_out(tree: &mut Tree) -> LayoutCtx<'_> {
LayoutCtx::detached(tree)
}
/// on_complete fires exactly once per completed run; a retarget that
/// completes again fires again.
#[test]
fn on_complete_fires_once_per_run() {
use crate::animation::TimingFunction;
use std::cell::Cell;
use std::rc::Rc;
let fired = Rc::new(Cell::new(0));
let counter = fired.clone();
let mut anim = AnimationState::new(
0.0_f32,
crate::animation::Transition::new(1, TimingFunction::Linear)
.on_complete(move || counter.set(counter.get() + 1)),
);
anim.set_immediate(&mut laying_out(&mut Tree::new()), 0.0);
anim.animate_to(1.0, FrameInstant::from(Instant::now()));
std::thread::sleep(std::time::Duration::from_millis(10));
anim.advance(FrameInstant::from(Instant::now()));
assert_eq!(fired.get(), 1, "settle must fire the callback once");
anim.advance(FrameInstant::from(Instant::now()));
assert_eq!(fired.get(), 1, "no refire after completion");
anim.animate_to(2.0, FrameInstant::from(Instant::now()));
std::thread::sleep(std::time::Duration::from_millis(10));
anim.advance(FrameInstant::from(Instant::now()));
assert_eq!(fired.get(), 2, "a new completed run fires again");
}
use crate::animation::TimingFunction;
/// A trigger nothing ever writes to: these tests call `play` directly.
fn never_played() -> crate::reactive::Signal<u32> {
crate::reactive::create_stored(0)
}
/// Step a running sequence to `ms` into its run.
fn play_at<T: Animatable>(anim: &mut AnimationState<T>, ms: u64) {
let started = anim
.timeline
.as_ref()
.and_then(|t| t.playing)
.expect("a sequence is playing");
anim.advance(started + std::time::Duration::from_millis(ms));
}
/// While a sequence runs it speaks for the property, and when it ends the
/// property goes back to whatever is declared — including a value that
/// changed while it was playing.
#[test]
fn a_timeline_plays_and_then_hands_the_property_back() {
use crate::animation::Keyframes;
let mut anim = AnimationState::new(0.0_f32, Transition::new(0.0, TimingFunction::Linear));
anim.set_immediate(&mut laying_out(&mut Tree::new()), 0.0);
anim = anim.with_timeline(
Keyframes::new(60.0)
.at(0.0, 0.0)
.at(0.5, 10.0)
.at(1.0, 0.0)
.played_by(never_played()),
);
anim.play(FrameInstant::from(Instant::now()));
assert!(anim.is_animating(), "a playing timeline is an animation");
play_at(&mut anim, 30);
assert!(
*anim.current() > 4.0,
"halfway through it should be near the peak, got {}",
anim.current()
);
// The declared value moves while the sequence is running.
anim.animate_to(3.0, FrameInstant::from(Instant::now()));
play_at(&mut anim, 70);
at(&mut anim, 10);
assert_eq!(*anim.current(), 3.0, "over, and back to what is declared");
assert!(!anim.is_animating());
}
/// Played again mid-run it restarts: the second refusal is not half a
/// shake.
#[test]
fn playing_again_starts_the_sequence_over() {
use crate::animation::Keyframes;
let mut anim = AnimationState::new(0.0_f32, Transition::new(0.0, TimingFunction::Linear));
anim.set_immediate(&mut laying_out(&mut Tree::new()), 0.0);
anim = anim.with_timeline(
Keyframes::new(80.0)
.at(0.0, 0.0)
.at(1.0, 8.0)
.played_by(never_played()),
);
anim.play(FrameInstant::from(Instant::now()));
play_at(&mut anim, 60);
let far = *anim.current();
anim.play(FrameInstant::from(Instant::now()));
anim.advance(FrameInstant::from(Instant::now()));
assert!(
*anim.current() < far,
"back near the top of the run, got {} after {far}",
anim.current()
);
}
/// The end of a sequence hands the property back to its declared
/// transition — it does not cancel it.
///
/// Forcing the value to the target on the last frame made the case the
/// whole feature is sold on jump: a card shaking, a pointer arriving
/// mid-shake, and the hover landing instantly instead of on its spring.
#[test]
fn the_end_of_a_sequence_animates_back_rather_than_snapping() {
use crate::animation::Keyframes;
let mut anim = AnimationState::new(0.0_f32, Transition::new(200.0, TimingFunction::Linear));
anim.set_immediate(&mut laying_out(&mut Tree::new()), 0.0);
anim = anim.with_timeline(
Keyframes::new(60.0)
.at(0.0, 0.0)
.at(1.0, 10.0)
.played_by(never_played()),
);
anim.play(FrameInstant::from(Instant::now()));
play_at(&mut anim, 30);
// Something declares a new value while the sequence is running.
anim.animate_to(100.0, FrameInstant::from(Instant::now()));
// The sequence runs out.
play_at(&mut anim, 70);
let handed_back = *anim.current();
assert!(
handed_back < 100.0,
"the declared transition has to run, not be skipped: got \
{handed_back}"
);
assert!(anim.is_animating(), "and it is still going");
at(&mut anim, 400);
assert_eq!(*anim.current(), 100.0, "arriving under its own transition");
}
/// And the transition it hands back to reaches its completion edge, so a
/// callback gated on the property arriving still fires.
#[test]
fn a_sequence_does_not_swallow_the_completion_callback() {
use crate::animation::Keyframes;
use std::cell::Cell;
use std::rc::Rc;
let fired = Rc::new(Cell::new(0));
let seen = fired.clone();
let transition = Transition::new(50.0, TimingFunction::Linear)
.on_complete(move || seen.set(seen.get() + 1));
let mut anim = AnimationState::new(0.0_f32, transition);
anim.set_immediate(&mut laying_out(&mut Tree::new()), 0.0);
anim = anim.with_timeline(
Keyframes::new(60.0)
.at(0.0, 0.0)
.at(1.0, 5.0)
.played_by(never_played()),
);
anim.animate_to(1.0, FrameInstant::from(Instant::now()));
anim.play(FrameInstant::from(Instant::now()));
play_at(&mut anim, 30);
assert_eq!(fired.get(), 0, "nothing has arrived yet");
play_at(&mut anim, 70);
at(&mut anim, 100);
assert_eq!(fired.get(), 1, "the hand-back completes, and says so");
}
/// A `playing` with nothing to play cannot outlive the sequence: on its
/// own it would keep `is_animating` true for good, and the surface asking
/// for a frame every vsync with nothing to draw.
#[test]
fn a_play_without_a_sequence_does_not_pin_the_frame_loop() {
let mut anim = AnimationState::new(0.0_f32, Transition::new(10.0, TimingFunction::Linear));
anim.set_immediate(&mut laying_out(&mut Tree::new()), 0.0);
anim.play(FrameInstant::from(Instant::now()));
anim.advance(FrameInstant::from(Instant::now()));
assert!(!anim.is_animating(), "nothing to play, nothing to animate");
}
/// The trigger is asked and committed in one place, so the pass that
/// subscribes and the pass that plays cannot disagree about what they
/// have seen.
#[test]
fn a_trigger_is_taken_once_per_change() {
use crate::animation::Keyframes;
use crate::reactive::create_signal;
let plays = create_signal(0_u32);
let mut anim = AnimationState::new(0.0_f32, Transition::new(0.0, TimingFunction::Linear));
anim = anim.with_timeline(
Keyframes::new(40.0)
.at(0.0, 0.0)
.at(1.0, 1.0)
.played_by(plays),
);
assert!(!anim.wants_play(), "nothing has happened yet");
plays.set(1);
assert!(anim.wants_play());
assert!(anim.take_play(), "the first ask takes it");
assert!(!anim.take_play(), "the second finds nothing left");
assert!(!anim.wants_play());
}
/// Step an animation to `ms` after its segment began.
///
/// The instant is an argument now, so the helper reads when the segment
/// began and asks about `ms` after it, where it used to rewrite that start
/// behind the animation's back.
///
/// Sleeping instead would make the interruption point depend on how loaded
/// the machine is — and the interesting half of a spring's phase space is
/// on the far side of its overshoot, which a stretched sleep wanders into
/// by accident.
fn at<T: Animatable>(anim: &mut AnimationState<T>, ms: u64) {
let started = anim.start_time.expect("a segment has begun");
anim.advance(started + std::time::Duration::from_millis(ms));
}
/// Step through `ms` in 8ms frames, returning the extremes reached.
fn run(anim: &mut AnimationState<f32>, ms: u64) -> (f32, f32) {
let (mut low, mut high) = (*anim.current(), *anim.current());
for frame in 1..=(ms / 8) {
at(anim, frame * 8);
low = low.min(*anim.current());
high = high.max(*anim.current());
}
(low, high)
}
/// The spring's own state, for the tests that assert on the momentum
/// rather than on what it produced.
fn velocity_of<T: Animatable>(anim: &AnimationState<T>) -> f32 {
anim.spring_state.as_ref().expect("a spring").velocity
}
fn spring(config: crate::animation::SpringConfig) -> Transition {
Transition::new(0.0, TimingFunction::Spring(config))
}
/// A spring interrupted mid-flight turns around; it does not stop and
/// start again. Every target change used to hand the integrator a fresh
/// `SpringState`, velocity zero, so an animation reversed halfway through
/// snapped to a halt first — a spring in name and an easing curve in
/// behaviour.
#[test]
fn a_spring_reversed_in_flight_carries_its_momentum() {
use crate::animation::SpringConfig;
let mut anim = AnimationState::new(0.0_f32, spring(SpringConfig::BOUNCY));
anim.set_immediate(&mut laying_out(&mut Tree::new()), 0.0);
anim.animate_to(1.0, FrameInstant::from(Instant::now()));
run(&mut anim, 40);
assert!(
*anim.current() > 0.0,
"the spring has to be moving before it can be interrupted"
);
anim.animate_to(0.0, FrameInstant::from(Instant::now()));
let velocity = velocity_of(&anim);
assert!(
velocity < 0.0,
"still travelling away from the new target, so the new segment \
starts with the spring having to turn around, got {velocity}"
);
}
/// A spring whose exit is where it is has nothing to play, at rest or in
/// flight: there is no momentum to carry into a segment of no length, so
/// a segment would only hold it still until it settled (#691).
#[test]
fn a_spring_exit_to_where_it_is_has_nothing_to_play() {
use crate::animation::SpringConfig;
let mut tree = Tree::new();
let at_rest = AnimationState::new(0.0_f32, spring(SpringConfig::BOUNCY));
let mut at_rest = at_rest.with_exit_to(Some(Box::new(|| 0.0)));
at_rest.set_immediate(&mut laying_out(&mut tree), 0.0);
assert!(at_rest.begin_exit(FrameInstant::from(Instant::now())));
assert!(!at_rest.is_animating(), "at rest");
let mut moving = AnimationState::new(0.0_f32, spring(SpringConfig::BOUNCY));
moving.set_immediate(&mut laying_out(&mut tree), 0.0);
moving.animate_to(1.0, FrameInstant::from(Instant::now()));
run(&mut moving, 40);
let here = *moving.current();
assert!(moving.is_animating(), "in flight");
let mut moving = moving.with_exit_to(Some(Box::new(move || here)));
// Removed on the next frame after the last one it was stepped to.
let started = moving.start_time.expect("in flight");
let removed = started + std::time::Duration::from_millis(48);
assert!(moving.begin_exit(removed));
assert!(!moving.is_animating(), "in flight, but already there");
assert_eq!(*moving.current(), here, "and left where it was");
// Advanced a frame on, as it is while another property keeps the row,
// it stays at rest: no spring left in flight to step. A spring left
// in flight would be stepped from its last frame here, and report
// itself moving again.
at(&mut moving, 56);
assert!(!moving.is_animating(), "and stays at rest");
assert_eq!(*moving.current(), here);
}
/// And that momentum is what a wobble is made of: sent back to where it
/// came from, the spring overshoots past it rather than easing into it.
#[test]
fn the_carried_momentum_overshoots_the_way_back() {
use crate::animation::SpringConfig;
let mut anim = AnimationState::new(0.0_f32, spring(SpringConfig::BOUNCY));
anim.set_immediate(&mut laying_out(&mut Tree::new()), 0.0);
anim.animate_to(1.0, FrameInstant::from(Instant::now()));
run(&mut anim, 40);
anim.animate_to(0.0, FrameInstant::from(Instant::now()));
let (low, _) = run(&mut anim, 400);
assert!(
low < -0.02,
"the return leg should cross zero and come back, got {low} at its \
furthest"
);
}
/// Retargeted the way it was already going, the spring keeps closing.
#[test]
fn a_spring_sent_further_the_same_way_keeps_closing() {
use crate::animation::SpringConfig;
let mut anim = AnimationState::new(0.0_f32, spring(SpringConfig::DEFAULT));
anim.set_immediate(&mut laying_out(&mut Tree::new()), 0.0);
anim.animate_to(1.0, FrameInstant::from(Instant::now()));
run(&mut anim, 40);
anim.animate_to(2.0, FrameInstant::from(Instant::now()));
assert!(
velocity_of(&anim) > 0.0,
"still heading there, got {}",
velocity_of(&anim)
);
}
/// A value that was standing still starts from rest.
///
/// The obvious version of this test — `set_immediate` then `animate_to` —
/// asserts nothing: `start == target` makes the segment degenerate, so it
/// returns before ever reading the velocity. This one lets a spring
/// genuinely settle first, which is where the residue actually lives.
#[test]
fn a_spring_that_has_settled_starts_the_next_one_from_rest() {
use crate::animation::SpringConfig;
let mut anim = AnimationState::new(0.0_f32, spring(SpringConfig::DEFAULT));
anim.set_immediate(&mut laying_out(&mut Tree::new()), 0.0);
anim.animate_to(1.0, FrameInstant::from(Instant::now()));
run(&mut anim, 1200);
assert!(!anim.is_animating(), "it has to have settled first");
anim.animate_to(1.0001, FrameInstant::from(Instant::now()));
assert_eq!(
velocity_of(&anim),
0.0,
"a settled spring is at rest, whatever was left under the threshold"
);
}
/// Interrupted *after* its overshoot — the half of the phase space every
/// preset spends time in — the spring is travelling backwards along its own
/// segment, and the momentum it carries has to reflect that.
///
/// Reading the direction off the segment instead of off the integrator
/// inverts it here: the value is already falling toward the new target and
/// would be kicked back up, away from it.
#[test]
fn a_spring_interrupted_past_its_overshoot_carries_the_way_it_is_moving() {
use crate::animation::SpringConfig;
let mut anim = AnimationState::new(0.0_f32, spring(SpringConfig::BOUNCY));
anim.set_immediate(&mut laying_out(&mut Tree::new()), 0.0);
anim.animate_to(1.0, FrameInstant::from(Instant::now()));
run(&mut anim, 184);
assert!(
*anim.current() > 1.0 && velocity_of(&anim) < 0.0,
"the setup needs a spring past its target and coming back, got \
value {} velocity {}",
anim.current(),
velocity_of(&anim)
);
// Falling toward 1.0 from above is falling toward 0.0 as well.
anim.animate_to(0.0, FrameInstant::from(Instant::now()));
assert!(
velocity_of(&anim) > 0.0,
"it was already heading that way, so the new segment closes on its \
target, got {}",
velocity_of(&anim)
);
}
/// Momentum does not leak between channels that have nothing to do with
/// each other: a slide to the right, interrupted by a slide straight down,
/// starts the new segment from rest because the two directions are
/// orthogonal.
///
/// Translation against scale no longer needs saying — they are separate
/// properties with separate states, so nothing could cross between them
/// even by mistake.
#[test]
fn momentum_does_not_cross_from_one_channel_to_another() {
use crate::animation::SpringConfig;
use crate::transform::Translate;
let mut anim = AnimationState::new(Translate::NONE, spring(SpringConfig::DEFAULT));
anim.set_immediate(&mut laying_out(&mut Tree::new()), Translate::NONE);
anim.animate_to(
Translate::new(200.0, 0.0),
FrameInstant::from(Instant::now()),
);
at(&mut anim, 40);
assert!(velocity_of(&anim) > 0.0, "it has to be moving first");
// Straight down from wherever the rightward slide got to: the y
// channel moves, the x channel does not.
let here = *anim.current();
anim.animate_to(
Translate::new(here.x, here.y + 200.0),
FrameInstant::from(Instant::now()),
);
assert!(
velocity_of(&anim).abs() < 0.5,
"a rightward slide has no business driving a downward one, got {}",
velocity_of(&anim)
);
}
/// #212: a half turn used to annihilate the widget. Interpolating the six
/// matrix coefficients took the diagonal from 1 to -1, so at the midpoint
/// the matrix was all zeros and the widget shrank to a point and came
/// back. An angle has no midpoint that is not an angle.
#[test]
fn a_half_turn_does_not_collapse_the_widget() {
use crate::transform::{Scale, Transform};
let mut anim = AnimationState::new(0.0_f32, Transition::new(100.0, TimingFunction::Linear));
anim.set_immediate(&mut laying_out(&mut Tree::new()), 0.0);
anim.animate_to(180.0, FrameInstant::from(Instant::now()));
let mut smallest = f32::INFINITY;
for frame in 0..=25 {
at(&mut anim, frame * 4);
let composed = Transform::compose((0.0, 0.0), *anim.current(), Scale::NONE);
smallest = smallest.min(composed.extract_scale());
}
assert!(
(smallest - 1.0).abs() < 1e-4,
"a rotation must not change the size it turns; smallest scale was {smallest}"
);
}
/// #212, the other half: 0 -> 360 is the same matrix at both ends, so a
/// matrix lerp was constant and the widget sat still. The angle is a
/// number, and 360 is not 0.
#[test]
fn a_full_turn_is_a_turn_and_not_a_no_op() {
let mut anim = AnimationState::new(0.0_f32, Transition::new(100.0, TimingFunction::Linear));
anim.set_immediate(&mut laying_out(&mut Tree::new()), 0.0);
anim.animate_to(360.0, FrameInstant::from(Instant::now()));
at(&mut anim, 50);
let halfway = *anim.current();
assert!(
(halfway - 180.0).abs() < 5.0,
"halfway through a full turn is half a turn, got {halfway}"
);
at(&mut anim, 100);
assert!(
(*anim.current() - 360.0).abs() < 1e-3,
"and it ends where it was sent, got {}",
*anim.current()
);
}
/// Nothing takes a shorter way round on the caller's behalf: 350 -> 370 is
/// twenty degrees forward, not three hundred and forty back. Only the
/// caller knows whether an angle that crossed the wrap meant to.
#[test]
fn an_angle_past_the_wrap_keeps_going_forward() {
let mut anim = AnimationState::new(0.0_f32, Transition::new(100.0, TimingFunction::Linear));
anim.set_immediate(&mut laying_out(&mut Tree::new()), 350.0);
anim.animate_to(370.0, FrameInstant::from(Instant::now()));
at(&mut anim, 50);
let halfway = *anim.current();
assert!(
(halfway - 360.0).abs() < 1.0,
"halfway from 350 to 370 is 360, got {halfway}"
);
}
/// And the angle moves at the rate the easing asks for. The matrix lerp
/// got the endpoints right and everything between them wrong: a quarter of
/// the way into a 0 -> 90 run it passed through 8.13 degrees, not 22.5.
#[test]
fn the_angle_moves_at_the_rate_the_easing_asks_for() {
let mut anim = AnimationState::new(0.0_f32, Transition::new(100.0, TimingFunction::Linear));
anim.set_immediate(&mut laying_out(&mut Tree::new()), 0.0);
anim.animate_to(90.0, FrameInstant::from(Instant::now()));
at(&mut anim, 25);
assert!(
(*anim.current() - 22.5).abs() < 1.0,
"a quarter of the way is a quarter of the angle, got {}",
*anim.current()
);
}
/// A target that moves every frame — a field growing as it is typed into —
/// converges instead of building speed without bound.
#[test]
fn a_spring_following_a_moving_target_stays_bounded() {
use crate::animation::SpringConfig;
let mut anim = AnimationState::new(0.0_f32, spring(SpringConfig::DEFAULT));
anim.set_immediate(&mut laying_out(&mut Tree::new()), 0.0);
// 40 frames of the target creeping upward, then it stops.
for frame in 1..=40u64 {
anim.animate_to(frame as f32, FrameInstant::from(Instant::now()));
at(&mut anim, 8);
}
let (_, high) = run(&mut anim, 800);
assert!(
high < 60.0,
"following a target that stopped at 40 must not fly past it, \
reached {high}"
);
assert!(
(*anim.current() - 40.0).abs() < 0.5,
"and it has to arrive, got {}",
anim.current()
);
}
#[test]
fn a_property_with_no_timeline_cannot_be_played() {
let mut anim = AnimationState::new(0.0_f32, Transition::new(10.0, TimingFunction::Linear));
anim.set_immediate(&mut laying_out(&mut Tree::new()), 0.0);
anim.play(FrameInstant::from(Instant::now()));
assert!(!anim.is_animating(), "nothing to play");
}
/// A transition that has not started moving yet has nothing to carry: its
/// spring is stepped only after the delay, so a velocity stored now would
/// be released at full strength against a value that had not moved since.
#[test]
fn a_delayed_transition_starts_from_rest() {
use crate::animation::SpringConfig;
let delayed = Transition::new(0.0, TimingFunction::Spring(SpringConfig::BOUNCY)).delay(200);
let mut anim = AnimationState::new(0.0_f32, delayed);
anim.set_immediate(&mut laying_out(&mut Tree::new()), 0.0);
anim.animate_to(1.0, FrameInstant::from(Instant::now()));
run(&mut anim, 400);
assert!(*anim.current() > 0.0, "past the delay and moving");
anim.animate_to(0.0, FrameInstant::from(Instant::now()));
assert_eq!(velocity_of(&anim), 0.0);
}
/// Nothing of this touches the timed transitions, which have no momentum
/// to keep.
#[test]
fn a_timed_transition_has_no_spring_to_carry() {
let mut anim = AnimationState::new(0.0_f32, Transition::new(100.0, TimingFunction::Linear));
anim.set_immediate(&mut laying_out(&mut Tree::new()), 0.0);
anim.animate_to(1.0, FrameInstant::from(Instant::now()));
run(&mut anim, 24);
anim.animate_to(0.0, FrameInstant::from(Instant::now()));
assert!(anim.spring_state.is_none());
}
#[test]
fn test_animation_state_new() {
let transition = Transition::new(300.0, TimingFunction::Linear);
let state = AnimationState::new(0.0f32, transition);
assert_eq!(*state.current(), 0.0);
assert_eq!(*state.target(), 0.0);
assert!(!state.is_animating()); // Starts completed
assert!(state.is_initial()); // Not yet initialized
}
#[test]
fn test_animation_state_animate_to() {
let transition = Transition::new(300.0, TimingFunction::Linear);
let mut state = AnimationState::new(0.0f32, transition);
state.animate_to(100.0, FrameInstant::from(Instant::now()));
assert_eq!(*state.target(), 100.0);
assert!(state.is_animating());
}
#[test]
fn test_animation_state_animate_to_same_target() {
let transition = Transition::new(300.0, TimingFunction::Linear);
let mut state = AnimationState::new(0.0f32, transition);
state.animate_to(100.0, FrameInstant::from(Instant::now()));
let first_start_time = state.start_time;
// Animate to same target should not restart
state.animate_to(100.0, FrameInstant::from(Instant::now()));
assert_eq!(state.start_time, first_start_time);
}
#[test]
fn test_animation_state_set_immediate() {
let transition = Transition::new(300.0, TimingFunction::Linear);
let mut state = AnimationState::new(0.0f32, transition);
state.set_immediate(&mut laying_out(&mut Tree::new()), 50.0);
assert_eq!(*state.current(), 50.0);
assert_eq!(*state.target(), 50.0);
assert!(!state.is_animating());
assert!(!state.is_initial()); // Now initialized
}
#[test]
fn test_animation_state_is_initial() {
let transition = Transition::new(300.0, TimingFunction::Linear);
let mut state = AnimationState::new(0.0f32, transition);
assert!(state.is_initial());
state.set_immediate(&mut laying_out(&mut Tree::new()), 10.0);
assert!(!state.is_initial());
}
#[test]
fn test_get_animated_value_with_some() {
let transition = Transition::new(300.0, TimingFunction::Linear);
let mut state = AnimationState::new(42.0f32, transition);
state.set_immediate(&mut laying_out(&mut Tree::new()), 42.0);
let value = get_animated_value(Some(&state), || 0.0);
assert_eq!(value, 42.0);
}
#[test]
fn test_get_animated_value_with_none() {
let value = get_animated_value::<f32>(None, || 99.0);
assert_eq!(value, 99.0);
}
}