1use crate::frame_scheduler::mark_needs_commit;
2use std::cell::RefCell;
3use std::rc::Rc;
4
5const MAX_FRAME_DELTA_MS: f64 = 100.0;
6
7fn clamp_unit(value: f32) -> f32 {
8 value.clamp(0.0, 1.0)
9}
10
11fn clamp_frame_delta(delta_ms: f64) -> f64 {
12 delta_ms.clamp(0.0, MAX_FRAME_DELTA_MS)
13}
14
15fn mix_float(from: f32, to: f32, amount: f32) -> f32 {
16 from + ((to - from) * clamp_unit(amount))
17}
18
19fn mix_color_channel(from: u32, to: u32, amount: f32) -> u32 {
20 let mixed = (from as f32) + (((to as f32) - (from as f32)) * clamp_unit(amount));
21 mixed.round().clamp(0.0, 255.0) as u32
22}
23
24fn mix_color(from: u32, to: u32, amount: f32) -> u32 {
25 let fr = (from >> 24) & 0xFF;
26 let fg = (from >> 16) & 0xFF;
27 let fb = (from >> 8) & 0xFF;
28 let fa = from & 0xFF;
29 let tr = (to >> 24) & 0xFF;
30 let tg = (to >> 16) & 0xFF;
31 let tb = (to >> 8) & 0xFF;
32 let ta = to & 0xFF;
33 (mix_color_channel(fr, tr, amount) << 24)
34 | (mix_color_channel(fg, tg, amount) << 16)
35 | (mix_color_channel(fb, tb, amount) << 8)
36 | mix_color_channel(fa, ta, amount)
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum Easing {
41 Linear,
42 CubicIn,
43 CubicOut,
44 CubicInOut,
45 QuadOut,
46}
47
48impl Easing {
49 pub fn sample(self, progress: f32) -> f32 {
50 let t = clamp_unit(progress);
51 match self {
52 Self::Linear => t,
53 Self::CubicIn => t * t * t,
54 Self::CubicOut => {
55 let offset = t - 1.0;
56 (offset * offset * offset) + 1.0
57 }
58 Self::CubicInOut => {
59 if t < 0.5 {
60 4.0 * t * t * t
61 } else {
62 let offset = (-2.0 * t) + 2.0;
63 1.0 - ((offset * offset * offset) * 0.5)
64 }
65 }
66 Self::QuadOut => {
67 let offset = 1.0 - t;
68 1.0 - (offset * offset)
69 }
70 }
71 }
72}
73
74pub struct Easings;
75
76impl Easings {
77 pub fn linear() -> Easing {
78 Easing::Linear
79 }
80
81 pub fn cubic_in() -> Easing {
82 Easing::CubicIn
83 }
84
85 pub fn cubic_out() -> Easing {
86 Easing::CubicOut
87 }
88
89 pub fn cubic_in_out() -> Easing {
90 Easing::CubicInOut
91 }
92
93 pub fn quad_out() -> Easing {
94 Easing::QuadOut
95 }
96}
97
98#[derive(Clone, Copy, Debug, PartialEq)]
99pub struct AnimationTiming {
100 pub duration_ms: f64,
101 pub easing: Easing,
102}
103
104impl AnimationTiming {
105 pub fn new(duration_ms: f64) -> Self {
106 Self {
107 duration_ms: duration_ms.max(0.0),
108 easing: Easing::Linear,
109 }
110 }
111
112 pub fn with_easing(duration_ms: f64, easing: Easing) -> Self {
113 Self {
114 duration_ms: duration_ms.max(0.0),
115 easing,
116 }
117 }
118}
119
120trait AnimationDriver {
121 fn on_start(&mut self, _timestamp_ms: f64) {}
122 fn on_sample(&mut self, eased_progress: f32, linear_progress: f32);
123 fn on_stop(&mut self, _finished: bool) {}
124}
125
126struct AnimationState {
127 timing: AnimationTiming,
128 running: bool,
129 started: bool,
130 last_timestamp_ms: f64,
131 elapsed_ms: f64,
132 driver: Box<dyn AnimationDriver>,
133}
134
135#[derive(Clone)]
136pub struct Animation {
137 inner: Rc<RefCell<AnimationState>>,
138}
139
140impl Animation {
141 fn new(timing: AnimationTiming, driver: Box<dyn AnimationDriver>) -> Self {
142 Self {
143 inner: Rc::new(RefCell::new(AnimationState {
144 timing,
145 running: false,
146 started: false,
147 last_timestamp_ms: 0.0,
148 elapsed_ms: 0.0,
149 driver,
150 })),
151 }
152 }
153
154 pub fn is_running(&self) -> bool {
155 self.inner.borrow().running
156 }
157
158 pub fn cancel(&self) {
159 get_animation_manager().cancel(self);
160 }
161
162 pub fn finish(&self) {
163 get_animation_manager().finish(self);
164 }
165
166 pub fn dispose(&self) {
167 self.cancel();
168 }
169
170 fn attach(&self, known_timestamp_ms: f64, has_known_timestamp: bool) {
171 {
172 let mut state = self.inner.borrow_mut();
173 state.running = true;
174 state.started = false;
175 state.last_timestamp_ms = 0.0;
176 state.elapsed_ms = 0.0;
177 }
178 if has_known_timestamp {
179 self.start_internal(known_timestamp_ms);
180 }
181 }
182
183 fn tick(&self, timestamp_ms: f64) {
184 if !self.is_running() {
185 return;
186 }
187 if !self.inner.borrow().started {
188 self.start_internal(timestamp_ms);
189 return;
190 }
191 let (duration_ms, easing, last_timestamp_ms) = {
192 let state = self.inner.borrow();
193 (
194 state.timing.duration_ms,
195 state.timing.easing,
196 state.last_timestamp_ms,
197 )
198 };
199 let delta_ms = clamp_frame_delta(timestamp_ms - last_timestamp_ms);
200 let should_finish = {
201 let mut state = self.inner.borrow_mut();
202 state.last_timestamp_ms = timestamp_ms;
203 state.elapsed_ms += delta_ms;
204 let progress = if duration_ms <= 0.0 {
205 1.0
206 } else {
207 clamp_unit((state.elapsed_ms / duration_ms) as f32)
208 };
209 state.driver.on_sample(easing.sample(progress), progress);
210 progress >= 1.0
211 };
212 if should_finish {
213 self.stop(true);
214 }
215 }
216
217 fn cancel_internal(&self) {
218 if self.is_running() {
219 self.stop(false);
220 }
221 }
222
223 fn finish_internal(&self) {
224 if !self.is_running() {
225 return;
226 }
227 if !self.inner.borrow().started {
228 let mut state = self.inner.borrow_mut();
229 state.started = true;
230 state.last_timestamp_ms = 0.0;
231 state.elapsed_ms = state.timing.duration_ms;
232 state.driver.on_start(0.0);
233 }
234 self.inner.borrow_mut().driver.on_sample(1.0, 1.0);
235 self.stop(true);
236 }
237
238 fn start_internal(&self, timestamp_ms: f64) {
239 let duration_ms = {
240 let mut state = self.inner.borrow_mut();
241 state.started = true;
242 state.last_timestamp_ms = timestamp_ms;
243 state.elapsed_ms = 0.0;
244 state.driver.on_start(timestamp_ms);
245 state.timing.duration_ms
246 };
247 if duration_ms <= 0.0 {
248 self.inner.borrow_mut().driver.on_sample(1.0, 1.0);
249 self.stop(true);
250 return;
251 }
252 let easing = self.inner.borrow().timing.easing;
253 self.inner
254 .borrow_mut()
255 .driver
256 .on_sample(easing.sample(0.0), 0.0);
257 }
258
259 fn stop(&self, finished: bool) {
260 let mut state = self.inner.borrow_mut();
261 state.running = false;
262 state.driver.on_stop(finished);
263 }
264}
265
266#[derive(Default)]
267struct AnimationManagerState {
268 active_animations: Vec<Animation>,
269 last_timestamp_ms: f64,
270 has_last_timestamp: bool,
271}
272
273thread_local! {
274 static ANIMATION_MANAGER: RefCell<AnimationManagerState> = const { RefCell::new(AnimationManagerState {
275 active_animations: Vec::new(),
276 last_timestamp_ms: 0.0,
277 has_last_timestamp: false,
278 }) };
279}
280
281#[derive(Clone, Copy, Debug, Default)]
282pub struct AnimationManager;
283
284impl AnimationManager {
285 pub fn start(&self, animation: Animation) -> Animation {
286 self.cancel(&animation);
287 ANIMATION_MANAGER.with(|slot| {
288 let mut state = slot.borrow_mut();
289 animation.attach(state.last_timestamp_ms, state.has_last_timestamp);
290 if animation.is_running() {
291 state.active_animations.push(animation.clone());
292 }
293 });
294 mark_needs_commit();
295 animation
296 }
297
298 pub fn cancel(&self, animation: &Animation) {
299 ANIMATION_MANAGER.with(|slot| {
300 let mut state = slot.borrow_mut();
301 if let Some(index) = state
302 .active_animations
303 .iter()
304 .position(|candidate| Rc::ptr_eq(&candidate.inner, &animation.inner))
305 {
306 state.active_animations.swap_remove(index);
307 animation.cancel_internal();
308 }
309 });
310 }
311
312 pub fn finish(&self, animation: &Animation) {
313 ANIMATION_MANAGER.with(|slot| {
314 let mut state = slot.borrow_mut();
315 if let Some(index) = state
316 .active_animations
317 .iter()
318 .position(|candidate| Rc::ptr_eq(&candidate.inner, &animation.inner))
319 {
320 state.active_animations.swap_remove(index);
321 animation.finish_internal();
322 }
323 });
324 }
325
326 pub fn tick(&self, timestamp_ms: f64) {
327 let active_animations = ANIMATION_MANAGER.with(|slot| {
328 let mut state = slot.borrow_mut();
329 state.last_timestamp_ms = timestamp_ms;
330 state.has_last_timestamp = true;
331 state.active_animations.clone()
332 });
333 let mut completed = Vec::new();
334 let mut has_active = false;
335 for animation in active_animations {
336 animation.tick(timestamp_ms);
337 if animation.is_running() {
338 has_active = true;
339 } else {
340 completed.push(animation);
341 }
342 }
343 if !completed.is_empty() {
344 ANIMATION_MANAGER.with(|slot| {
345 let mut state = slot.borrow_mut();
346 state.active_animations.retain(|candidate| {
347 !completed.iter().any(|completed_animation| {
348 Rc::ptr_eq(&candidate.inner, &completed_animation.inner)
349 })
350 });
351 });
352 }
353 if has_active {
354 mark_needs_commit();
355 }
356 }
357
358 pub fn has_active_animations(&self) -> bool {
359 ANIMATION_MANAGER.with(|slot| !slot.borrow().active_animations.is_empty())
360 }
361
362 pub fn reset(&self) {
363 ANIMATION_MANAGER.with(|slot| {
364 let mut state = slot.borrow_mut();
365 for animation in state.active_animations.drain(..) {
366 animation.cancel_internal();
367 }
368 state.last_timestamp_ms = 0.0;
369 state.has_last_timestamp = false;
370 });
371 }
372}
373
374pub fn get_animation_manager() -> AnimationManager {
375 AnimationManager
376}
377
378pub fn tick_animations(timestamp_ms: f64) {
379 get_animation_manager().tick(timestamp_ms);
380}
381
382pub fn reset_animations() {
383 get_animation_manager().reset();
384}
385
386struct FloatAnimationDriver {
387 from: f32,
388 to: f32,
389 handler: Box<dyn Fn(f32)>,
390}
391
392impl AnimationDriver for FloatAnimationDriver {
393 fn on_sample(&mut self, eased_progress: f32, _linear_progress: f32) {
394 (self.handler)(mix_float(self.from, self.to, eased_progress));
395 }
396}
397
398struct ColorAnimationDriver {
399 from: u32,
400 to: u32,
401 handler: Box<dyn Fn(u32)>,
402}
403
404impl AnimationDriver for ColorAnimationDriver {
405 fn on_sample(&mut self, eased_progress: f32, _linear_progress: f32) {
406 (self.handler)(mix_color(self.from, self.to, eased_progress));
407 }
408}
409
410pub fn animate_float(
411 from_value: f32,
412 to_value: f32,
413 timing: AnimationTiming,
414 handler: impl Fn(f32) + 'static,
415) -> Animation {
416 get_animation_manager().start(Animation::new(
417 timing,
418 Box::new(FloatAnimationDriver {
419 from: from_value,
420 to: to_value,
421 handler: Box::new(handler),
422 }),
423 ))
424}
425
426pub fn animate_float_with<Owner: 'static>(
427 owner: Owner,
428 from_value: f32,
429 to_value: f32,
430 timing: AnimationTiming,
431 handler: impl Fn(&Owner, f32) + 'static,
432) -> Animation {
433 animate_float(from_value, to_value, timing, move |value| {
434 handler(&owner, value);
435 })
436}
437
438pub fn animate_color(
439 from_value: u32,
440 to_value: u32,
441 timing: AnimationTiming,
442 handler: impl Fn(u32) + 'static,
443) -> Animation {
444 get_animation_manager().start(Animation::new(
445 timing,
446 Box::new(ColorAnimationDriver {
447 from: from_value,
448 to: to_value,
449 handler: Box::new(handler),
450 }),
451 ))
452}
453
454pub fn animate_color_with<Owner: 'static>(
455 owner: Owner,
456 from_value: u32,
457 to_value: u32,
458 timing: AnimationTiming,
459 handler: impl Fn(&Owner, u32) + 'static,
460) -> Animation {
461 animate_color(from_value, to_value, timing, move |value| {
462 handler(&owner, value);
463 })
464}
465
466#[cfg(test)]
467mod tests {
468 use super::*;
469 use crate::bridge_callbacks::__fui_on_frame;
470 use crate::ffi::{self, Call};
471 use crate::frame_scheduler::reset_commit_state;
472 use std::cell::Cell;
473 use std::rc::Rc;
474
475 #[test]
476 fn advances_float_animations_from_frame_timestamps() {
477 reset_animations();
478 reset_commit_state();
479 let value = Rc::new(Cell::new(-1.0));
480 let value_for_handler = value.clone();
481 let animation = animate_float(10.0, 30.0, AnimationTiming::new(200.0), move |next| {
482 value_for_handler.set(next);
483 });
484 let manager = get_animation_manager();
485
486 manager.tick(1000.0);
487 assert_eq!(value.get(), 10.0);
488 assert!(animation.is_running());
489
490 manager.tick(1050.0);
491 assert_eq!(value.get(), 15.0);
492
493 manager.tick(1150.0);
494 assert_eq!(value.get(), 25.0);
495
496 manager.tick(1250.0);
497 assert_eq!(value.get(), 30.0);
498 assert!(!animation.is_running());
499 assert!(!manager.has_active_animations());
500 }
501
502 #[test]
503 fn clamps_stale_frame_gaps_instead_of_jumping_to_completion() {
504 reset_animations();
505 reset_commit_state();
506 let value = Rc::new(Cell::new(-1.0));
507 let value_for_handler = value.clone();
508 let manager = get_animation_manager();
509 animate_float(0.0, 100.0, AnimationTiming::new(200.0), move |next| {
510 value_for_handler.set(next);
511 });
512
513 manager.tick(1000.0);
514 manager.tick(1800.0);
515
516 assert_eq!(value.get(), 50.0);
517 assert!(manager.has_active_animations());
518 }
519
520 #[test]
521 fn supports_owner_bound_float_and_color_helpers() {
522 reset_animations();
523 reset_commit_state();
524 let float_owner = Rc::new(Cell::new(-1.0));
525 let color_owner = Rc::new(Cell::new(0u32));
526 let manager = get_animation_manager();
527
528 animate_float_with(
529 float_owner.clone(),
530 4.0,
531 12.0,
532 AnimationTiming::new(80.0),
533 |owner, value| owner.set(value),
534 );
535 animate_color_with(
536 color_owner.clone(),
537 0x000000FF,
538 0xFF0000FF,
539 AnimationTiming::new(80.0),
540 |owner, value| owner.set(value),
541 );
542
543 manager.tick(500.0);
544 assert_eq!(float_owner.get(), 4.0);
545 assert_eq!(color_owner.get(), 0x000000FF);
546
547 manager.tick(540.0);
548 assert_eq!(float_owner.get(), 8.0);
549 assert_eq!(color_owner.get(), 0x800000FF);
550
551 manager.tick(580.0);
552 assert_eq!(float_owner.get(), 12.0);
553 assert_eq!(color_owner.get(), 0xFF0000FF);
554 }
555
556 #[test]
557 fn cancels_and_finishes_animations_through_the_animation_object() {
558 reset_animations();
559 reset_commit_state();
560 let value = Rc::new(Cell::new(-1.0));
561 let first_value = value.clone();
562 let animation = animate_float(0.0, 100.0, AnimationTiming::new(100.0), move |next| {
563 first_value.set(next);
564 });
565 let manager = get_animation_manager();
566
567 manager.tick(100.0);
568 manager.tick(140.0);
569 assert_eq!(value.get(), 40.0);
570
571 animation.cancel();
572 manager.tick(200.0);
573 assert_eq!(value.get(), 40.0);
574 assert!(!animation.is_running());
575
576 let second_value = value.clone();
577 let second = animate_float(0.0, 100.0, AnimationTiming::new(100.0), move |next| {
578 second_value.set(next);
579 });
580 manager.tick(300.0);
581 second.finish();
582 assert_eq!(value.get(), 100.0);
583 assert!(!second.is_running());
584 assert!(!manager.has_active_animations());
585 }
586
587 #[test]
588 fn uses_the_shared_frame_hook_to_tick_active_animations() {
589 reset_animations();
590 reset_commit_state();
591 let owner = Rc::new(Cell::new(-1.0));
592 animate_float_with(
593 owner.clone(),
594 2.0,
595 6.0,
596 AnimationTiming::new(100.0),
597 |slot, value| {
598 slot.set(value);
599 },
600 );
601
602 __fui_on_frame(1000.0);
603 assert_eq!(crate::frame_signal::frame_time_signal().value(), 1000.0);
604 assert_eq!(owner.get(), 2.0);
605
606 __fui_on_frame(1050.0);
607 assert_eq!(owner.get(), 4.0);
608 }
609
610 #[test]
611 fn requests_render_when_animation_starts() {
612 reset_animations();
613 reset_commit_state();
614 ffi::test::reset();
615 animate_float(0.0, 1.0, AnimationTiming::new(50.0), |_| {});
616 let calls = ffi::test::take_calls();
617 assert_eq!(
618 calls
619 .iter()
620 .filter(|call| matches!(call, Call::RequestRender))
621 .count(),
622 1
623 );
624 }
625}