1use std::{
6 cell::{Cell, RefCell},
7 rc::Rc,
8};
9
10use cranpose_animation::{FloatDecayAnimationSpec, SplineBasedDecaySpec};
11use cranpose_core::{
12 RuntimeHandle,
13 internal::{FrameCallbackRegistration, FrameClock},
14};
15
16pub const MIN_FLING_VELOCITY: f32 = 1.0;
19
20const DEFAULT_FLING_FRICTION: f32 = 0.015;
22
23const BOUNDARY_EPSILON: f32 = 0.5;
25
26fn schedule_next_frame<F, G>(
29 state: Rc<RefCell<Option<FlingAnimationState>>>,
30 frame_clock: FrameClock,
31 on_scroll: F,
32 on_end: G,
33) where
34 F: Fn(f32) -> f32 + 'static,
35 G: FnOnce() + 'static,
36{
37 let state_for_closure = state.clone();
38 let frame_clock_for_closure = frame_clock.clone();
39 let on_end = RefCell::new(Some(on_end));
40
41 let registration = frame_clock.with_frame_nanos(move |frame_time_nanos| {
42 let should_continue = {
43 let state_guard = state_for_closure.borrow();
44 let Some(anim_state) = state_guard.as_ref() else {
45 return;
46 };
47
48 if !anim_state.is_running.get() {
49 return;
50 }
51
52 let start_time = match anim_state.start_frame_time_nanos.get() {
53 Some(value) => value,
54 None => {
55 anim_state
56 .start_frame_time_nanos
57 .set(Some(frame_time_nanos));
58 frame_time_nanos
59 }
60 };
61
62 let play_time_nanos = frame_time_nanos.saturating_sub(start_time) as i64;
63
64 let new_value = anim_state.decay_spec.get_value_from_nanos(
65 play_time_nanos,
66 anim_state.initial_value,
67 anim_state.initial_velocity,
68 );
69
70 let last = anim_state.last_value.get();
71 let delta = new_value - last;
72 anim_state.last_value.set(new_value);
73 anim_state
74 .total_delta
75 .set(anim_state.total_delta.get() + delta);
76
77 let duration_nanos = anim_state
78 .decay_spec
79 .get_duration_nanos(anim_state.initial_value, anim_state.initial_velocity);
80
81 let current_velocity = anim_state.decay_spec.get_velocity_from_nanos(
82 play_time_nanos,
83 anim_state.initial_value,
84 anim_state.initial_velocity,
85 );
86
87 let is_finished = play_time_nanos >= duration_nanos
88 || current_velocity.abs() < anim_state.decay_spec.abs_velocity_threshold();
89
90 if is_finished {
91 anim_state.is_running.set(false);
92 }
93
94 let consumed = if delta.abs() > 0.001 {
95 on_scroll(delta)
96 } else {
97 0.0
98 };
99
100 let boundary_hit = (delta - consumed).abs() > BOUNDARY_EPSILON;
101 if boundary_hit {
102 anim_state.is_running.set(false);
103 }
104
105 !is_finished && !boundary_hit
106 };
107
108 if should_continue {
109 if let Some(on_end_fn) = on_end.borrow_mut().take() {
110 schedule_next_frame(
111 state_for_closure.clone(),
112 frame_clock_for_closure.clone(),
113 on_scroll,
114 on_end_fn,
115 );
116 }
117 } else if let Some(end_fn) = on_end.borrow_mut().take() {
118 end_fn();
119 }
120 });
121
122 if let Some(anim_state) = state.borrow_mut().as_mut() {
124 anim_state.registration = Some(registration);
125 }
126}
127
128struct FlingAnimationState {
130 initial_value: f32,
132 last_value: Cell<f32>,
134 initial_velocity: f32,
136 start_frame_time_nanos: Cell<Option<u64>>,
138 decay_spec: SplineBasedDecaySpec,
140 registration: Option<FrameCallbackRegistration>,
142 is_running: Cell<bool>,
144 total_delta: Cell<f32>,
146}
147
148pub struct FlingAnimation {
153 state: Rc<RefCell<Option<FlingAnimationState>>>,
154 frame_clock: FrameClock,
155}
156
157impl FlingAnimation {
158 pub fn new(runtime: RuntimeHandle) -> Self {
160 Self {
161 state: Rc::new(RefCell::new(None)),
162 frame_clock: runtime.frame_clock(),
163 }
164 }
165
166 pub fn start_fling<F, G>(
175 &self,
176 initial_value: f32,
177 velocity: f32,
178 density: f32,
179 on_scroll: F,
180 on_end: G,
181 ) where
182 F: Fn(f32) -> f32 + 'static, G: FnOnce() + 'static,
184 {
185 self.cancel();
187
188 if velocity.abs() < MIN_FLING_VELOCITY {
190 on_end();
191 return;
192 }
193
194 let friction = DEFAULT_FLING_FRICTION;
196 let calc = cranpose_animation::FlingCalculator::new(friction, density);
197 let decay_spec = SplineBasedDecaySpec::with_calculator(calc);
198
199 let anim_state = FlingAnimationState {
200 initial_value,
201 last_value: Cell::new(initial_value),
202 initial_velocity: velocity,
203 start_frame_time_nanos: Cell::new(None),
204 decay_spec,
205 registration: None,
206 is_running: Cell::new(true),
207 total_delta: Cell::new(0.0),
208 };
209
210 *self.state.borrow_mut() = Some(anim_state);
211
212 schedule_next_frame(
214 self.state.clone(),
215 self.frame_clock.clone(),
216 on_scroll,
217 on_end,
218 );
219 }
220
221 pub fn cancel(&self) {
222 if let Some(state) = self.state.borrow_mut().take() {
223 state.is_running.set(false);
225 drop(state.registration);
227 }
228 }
229
230 pub fn is_running(&self) -> bool {
232 self.state
233 .borrow()
234 .as_ref()
235 .is_some_and(|s| s.is_running.get())
236 }
237}
238
239impl Clone for FlingAnimation {
240 fn clone(&self) -> Self {
241 Self {
242 state: self.state.clone(),
243 frame_clock: self.frame_clock.clone(),
244 }
245 }
246}
247
248pub fn fling_rest_position(initial_value: f32, velocity: f32, density: f32) -> f32 {
254 if velocity.abs() < MIN_FLING_VELOCITY {
255 return initial_value;
256 }
257 let calc = cranpose_animation::FlingCalculator::new(DEFAULT_FLING_FRICTION, density);
258 let spec = SplineBasedDecaySpec::with_calculator(calc);
259 spec.get_target_value(initial_value, velocity)
260}
261
262const SETTLE_STIFFNESS: f32 = 300.0;
265
266const SETTLE_REST_DISTANCE: f32 = 0.1;
268const SETTLE_REST_VELOCITY: f32 = 4.0;
269
270struct SettleAnimationState {
271 value: Cell<f32>,
272 velocity: Cell<f32>,
273 target: f32,
274 last_frame_time_nanos: Cell<Option<u64>>,
275 registration: Option<FrameCallbackRegistration>,
276 is_running: Cell<bool>,
277}
278
279pub(crate) struct SettleEnd {
280 pub(crate) velocity: f32,
281 pub(crate) hit_boundary: bool,
282}
283
284pub struct SettleAnimation {
288 state: Rc<RefCell<Option<SettleAnimationState>>>,
289 frame_clock: FrameClock,
290}
291
292impl SettleAnimation {
293 pub fn new(runtime: RuntimeHandle) -> Self {
294 Self {
295 state: Rc::new(RefCell::new(None)),
296 frame_clock: runtime.frame_clock(),
297 }
298 }
299
300 pub(crate) fn start_settle<F, G>(
305 &self,
306 initial_value: f32,
307 initial_velocity: f32,
308 target: f32,
309 on_scroll: F,
310 on_end: G,
311 ) where
312 F: Fn(f32) -> f32 + 'static,
313 G: FnOnce(SettleEnd) + 'static,
314 {
315 self.cancel();
316 *self.state.borrow_mut() = Some(SettleAnimationState {
317 value: Cell::new(initial_value),
318 velocity: Cell::new(initial_velocity),
319 target,
320 last_frame_time_nanos: Cell::new(None),
321 registration: None,
322 is_running: Cell::new(true),
323 });
324 schedule_next_settle_frame(
325 self.state.clone(),
326 self.frame_clock.clone(),
327 on_scroll,
328 on_end,
329 );
330 }
331
332 pub fn cancel(&self) {
333 if let Some(state) = self.state.borrow_mut().take() {
334 state.is_running.set(false);
335 drop(state.registration);
336 }
337 }
338
339 pub fn is_running(&self) -> bool {
340 self.state
341 .borrow()
342 .as_ref()
343 .is_some_and(|s| s.is_running.get())
344 }
345}
346
347impl Clone for SettleAnimation {
348 fn clone(&self) -> Self {
349 Self {
350 state: self.state.clone(),
351 frame_clock: self.frame_clock.clone(),
352 }
353 }
354}
355
356fn schedule_next_settle_frame<F, G>(
357 state: Rc<RefCell<Option<SettleAnimationState>>>,
358 frame_clock: FrameClock,
359 on_scroll: F,
360 on_end: G,
361) where
362 F: Fn(f32) -> f32 + 'static,
363 G: FnOnce(SettleEnd) + 'static,
364{
365 let state_for_closure = state.clone();
366 let frame_clock_for_closure = frame_clock.clone();
367 let on_end = RefCell::new(Some(on_end));
368 let hit_boundary = Cell::new(false);
369
370 let registration = frame_clock.with_frame_nanos(move |frame_time_nanos| {
371 let should_continue = {
372 let state_guard = state_for_closure.borrow();
373 let Some(anim_state) = state_guard.as_ref() else {
374 return;
375 };
376 if !anim_state.is_running.get() {
377 return;
378 }
379
380 let dt = match anim_state.last_frame_time_nanos.get() {
381 Some(last) => (frame_time_nanos.saturating_sub(last) as f32) / 1_000_000_000.0,
382 None => 0.0,
383 };
384 anim_state.last_frame_time_nanos.set(Some(frame_time_nanos));
385
386 let (mut next_value, next_velocity) = cranpose_animation::advance_spring(
387 anim_state.value.get(),
388 anim_state.velocity.get(),
389 anim_state.target,
390 1.0,
391 SETTLE_STIFFNESS,
392 dt.max(0.0),
393 );
394
395 let is_finished = (next_value - anim_state.target).abs() < SETTLE_REST_DISTANCE
396 && next_velocity.abs() < SETTLE_REST_VELOCITY;
397 if is_finished {
398 next_value = anim_state.target;
399 anim_state.is_running.set(false);
400 }
401
402 let delta = next_value - anim_state.value.get();
403 anim_state.value.set(next_value);
404 anim_state.velocity.set(next_velocity);
405
406 let consumed = if delta.abs() > 0.0001 {
407 on_scroll(delta)
408 } else {
409 delta
410 };
411 let boundary_hit = (delta - consumed).abs() > BOUNDARY_EPSILON;
412 if boundary_hit {
413 anim_state.is_running.set(false);
414 hit_boundary.set(true);
415 }
416
417 !is_finished && !boundary_hit
418 };
419
420 if should_continue {
421 if let Some(on_end_fn) = on_end.borrow_mut().take() {
422 schedule_next_settle_frame(
423 state_for_closure.clone(),
424 frame_clock_for_closure.clone(),
425 on_scroll,
426 on_end_fn,
427 );
428 }
429 } else if let Some(end_fn) = on_end.borrow_mut().take() {
430 let state_guard = state_for_closure.borrow();
431 let velocity = state_guard
432 .as_ref()
433 .map_or(0.0, |anim_state| anim_state.velocity.get());
434 end_fn(SettleEnd {
435 velocity,
436 hit_boundary: hit_boundary.get(),
437 });
438 }
439 });
440
441 if let Some(anim_state) = state.borrow_mut().as_mut() {
442 anim_state.registration = Some(registration);
443 }
444}
445
446#[cfg(test)]
447mod tests {
448 use std::{cell::Cell, rc::Rc, sync::Arc};
449
450 use cranpose_core::{DefaultScheduler, Runtime};
451
452 use super::*;
453
454 #[test]
455 fn test_min_velocity_threshold() {
456 assert_eq!(MIN_FLING_VELOCITY, 1.0);
457 }
458
459 #[test]
460 fn settle_animation_springs_to_target_and_ends() {
461 let runtime = Runtime::new(Arc::new(DefaultScheduler));
462 let handle = runtime.handle();
463 let settle = SettleAnimation::new(handle.clone());
464 let position = Rc::new(Cell::new(30.0f32));
465 let ended = Rc::new(Cell::new(false));
466 let position_for_scroll = Rc::clone(&position);
467 let ended_for_end = Rc::clone(&ended);
468 settle.start_settle(
469 30.0,
470 0.0,
471 52.0,
472 move |delta| {
473 position_for_scroll.set(position_for_scroll.get() + delta);
474 delta
475 },
476 move |_| ended_for_end.set(true),
477 );
478 for frame in 0..240u64 {
479 handle.drain_frame_callbacks(frame * 16_000_000);
480 if ended.get() {
481 break;
482 }
483 }
484 assert!(ended.get(), "settle animation must finish");
485 assert!(
486 (position.get() - 52.0).abs() < 0.2,
487 "settle must land on the target, got {}",
488 position.get()
489 );
490 }
491
492 #[test]
493 fn settle_reports_reduced_velocity_when_crossing_boundary() {
494 let runtime = Runtime::new(Arc::new(DefaultScheduler));
495 let handle = runtime.handle();
496 let settle = SettleAnimation::new(handle.clone());
497 let position = Rc::new(Cell::new(30.0f32));
498 let ended = Rc::new(Cell::new(None::<(f32, bool)>));
499 let position_for_scroll = Rc::clone(&position);
500 let ended_for_end = Rc::clone(&ended);
501 settle.start_settle(
502 30.0,
503 -1_200.0,
504 0.0,
505 move |delta| {
506 let previous = position_for_scroll.get();
507 let next = (previous + delta).max(0.0);
508 position_for_scroll.set(next);
509 next - previous
510 },
511 move |end| ended_for_end.set(Some((end.velocity, end.hit_boundary))),
512 );
513 for frame in 0..240u64 {
514 handle.drain_frame_callbacks(frame * 16_000_000);
515 if ended.get().is_some() {
516 break;
517 }
518 }
519
520 let (velocity, hit_boundary) = ended.get().expect("settle must finish");
521 assert!(hit_boundary);
522 assert!(velocity < 0.0 && velocity.abs() < 1_200.0);
523 assert_eq!(position.get(), 0.0);
524 }
525
526 #[test]
527 fn fling_rest_position_is_beyond_start_in_fling_direction() {
528 let rest = fling_rest_position(100.0, 900.0, 1.0);
529 assert!(rest > 100.0, "rest {rest} must be past the start");
530 assert_eq!(fling_rest_position(100.0, 0.0, 1.0), 100.0);
531 }
532
533 #[test]
534 fn test_on_end_called_when_boundary_hit() {
535 let runtime = Runtime::new(Arc::new(DefaultScheduler));
536 let handle = runtime.handle();
537 let fling = FlingAnimation::new(handle.clone());
538 let finished = Rc::new(Cell::new(false));
539 let finished_flag = Rc::clone(&finished);
540
541 fling.start_fling(0.0, 10_000.0, 1.0, |_| 0.0, move || finished_flag.set(true));
542
543 handle.drain_frame_callbacks(0);
544 handle.drain_frame_callbacks(16_000_000);
545
546 assert!(finished.get());
547 }
548}