1use std::{
6 cell::{Cell, RefCell},
7 rc::Rc,
8};
9
10use cranpose_animation::{
11 ExponentialDecaySpec, FloatDecayAnimationSpec, IOS_DECELERATION_RATE_NORMAL,
12};
13use cranpose_core::{
14 RuntimeHandle,
15 internal::{FrameCallbackRegistration, FrameClock},
16};
17
18pub const MIN_FLING_VELOCITY: f32 = 300.0;
25
26const BOUNDARY_EPSILON: f32 = 0.5;
27
28fn 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() {
123 anim_state.registration = Some(registration);
124 }
125}
126
127struct FlingAnimationState {
128 initial_value: f32,
129 last_value: Cell<f32>,
130 initial_velocity: f32,
131 start_frame_time_nanos: Cell<Option<u64>>,
132 decay_spec: ExponentialDecaySpec,
133 registration: Option<FrameCallbackRegistration>,
134 is_running: Cell<bool>,
135 total_delta: Cell<f32>,
136}
137
138pub struct FlingAnimation {
143 state: Rc<RefCell<Option<FlingAnimationState>>>,
144 frame_clock: FrameClock,
145}
146
147impl FlingAnimation {
148 pub fn new(runtime: RuntimeHandle) -> Self {
150 Self {
151 state: Rc::new(RefCell::new(None)),
152 frame_clock: runtime.frame_clock(),
153 }
154 }
155
156 pub fn start_fling<F, G>(&self, initial_value: f32, velocity: f32, on_scroll: F, on_end: G)
164 where
165 F: Fn(f32) -> f32 + 'static,
166 G: FnOnce() + 'static,
167 {
168 self.cancel();
169
170 if velocity.abs() < MIN_FLING_VELOCITY {
171 on_end();
172 return;
173 }
174
175 let decay_spec = ExponentialDecaySpec::new(IOS_DECELERATION_RATE_NORMAL);
176
177 let anim_state = FlingAnimationState {
178 initial_value,
179 last_value: Cell::new(initial_value),
180 initial_velocity: velocity,
181 start_frame_time_nanos: Cell::new(None),
182 decay_spec,
183 registration: None,
184 is_running: Cell::new(true),
185 total_delta: Cell::new(0.0),
186 };
187
188 *self.state.borrow_mut() = Some(anim_state);
189
190 schedule_next_frame(
191 self.state.clone(),
192 self.frame_clock.clone(),
193 on_scroll,
194 on_end,
195 );
196 }
197
198 pub fn cancel(&self) {
199 if let Some(state) = self.state.borrow_mut().take() {
200 state.is_running.set(false);
201 drop(state.registration);
202 }
203 }
204
205 pub fn is_running(&self) -> bool {
207 self.state
208 .borrow()
209 .as_ref()
210 .is_some_and(|s| s.is_running.get())
211 }
212}
213
214impl Clone for FlingAnimation {
215 fn clone(&self) -> Self {
216 Self {
217 state: self.state.clone(),
218 frame_clock: self.frame_clock.clone(),
219 }
220 }
221}
222
223pub fn fling_rest_position(initial_value: f32, velocity: f32) -> f32 {
229 if velocity.abs() < MIN_FLING_VELOCITY {
230 return initial_value;
231 }
232 let spec = ExponentialDecaySpec::new(IOS_DECELERATION_RATE_NORMAL);
233 spec.get_target_value(initial_value, velocity)
234}
235
236#[derive(Debug, Clone, Copy)]
240pub struct SpringParams {
241 pub stiffness: f32,
242 pub damping_ratio: f32,
243}
244
245impl SpringParams {
246 pub const SETTLE_POLICY: Self = Self {
250 stiffness: 300.0,
251 damping_ratio: 1.0,
252 };
253
254 pub const OVERSCROLL_BOUNCE: Self = Self {
265 stiffness: 1909.69,
266 damping_ratio: 2.71,
267 };
268}
269
270const SETTLE_REST_DISTANCE: f32 = 0.1;
271const SETTLE_REST_VELOCITY: f32 = 4.0;
272
273struct SettleAnimationState {
274 value: Cell<f32>,
275 velocity: Cell<f32>,
276 target: f32,
277 params: SpringParams,
278 last_frame_time_nanos: Cell<Option<u64>>,
279 registration: Option<FrameCallbackRegistration>,
280 is_running: Cell<bool>,
281}
282
283pub(crate) struct SettleEnd {
284 pub(crate) velocity: f32,
285 pub(crate) hit_boundary: bool,
286}
287
288pub struct SettleAnimation {
293 state: Rc<RefCell<Option<SettleAnimationState>>>,
294 frame_clock: FrameClock,
295 params: SpringParams,
296}
297
298impl SettleAnimation {
299 pub fn new(runtime: RuntimeHandle, params: SpringParams) -> Self {
300 Self {
301 state: Rc::new(RefCell::new(None)),
302 frame_clock: runtime.frame_clock(),
303 params,
304 }
305 }
306
307 pub(crate) fn start_settle<F, G>(
308 &self,
309 initial_value: f32,
310 initial_velocity: f32,
311 target: f32,
312 on_scroll: F,
313 on_end: G,
314 ) where
315 F: Fn(f32) -> f32 + 'static,
316 G: FnOnce(SettleEnd) + 'static,
317 {
318 self.cancel();
319 *self.state.borrow_mut() = Some(SettleAnimationState {
320 value: Cell::new(initial_value),
321 velocity: Cell::new(initial_velocity),
322 target,
323 params: self.params,
324 last_frame_time_nanos: Cell::new(None),
325 registration: None,
326 is_running: Cell::new(true),
327 });
328 schedule_next_settle_frame(
329 self.state.clone(),
330 self.frame_clock.clone(),
331 on_scroll,
332 on_end,
333 );
334 }
335
336 pub fn cancel(&self) {
337 if let Some(state) = self.state.borrow_mut().take() {
338 state.is_running.set(false);
339 drop(state.registration);
340 }
341 }
342
343 pub fn is_running(&self) -> bool {
344 self.state
345 .borrow()
346 .as_ref()
347 .is_some_and(|s| s.is_running.get())
348 }
349}
350
351impl Clone for SettleAnimation {
352 fn clone(&self) -> Self {
353 Self {
354 state: self.state.clone(),
355 frame_clock: self.frame_clock.clone(),
356 params: self.params,
357 }
358 }
359}
360
361fn schedule_next_settle_frame<F, G>(
362 state: Rc<RefCell<Option<SettleAnimationState>>>,
363 frame_clock: FrameClock,
364 on_scroll: F,
365 on_end: G,
366) where
367 F: Fn(f32) -> f32 + 'static,
368 G: FnOnce(SettleEnd) + 'static,
369{
370 let state_for_closure = state.clone();
371 let frame_clock_for_closure = frame_clock.clone();
372 let on_end = RefCell::new(Some(on_end));
373 let hit_boundary = Cell::new(false);
374
375 let registration = frame_clock.with_frame_nanos(move |frame_time_nanos| {
376 let should_continue = {
377 let state_guard = state_for_closure.borrow();
378 let Some(anim_state) = state_guard.as_ref() else {
379 return;
380 };
381 if !anim_state.is_running.get() {
382 return;
383 }
384
385 let dt = match anim_state.last_frame_time_nanos.get() {
386 Some(last) => (frame_time_nanos.saturating_sub(last) as f32) / 1_000_000_000.0,
387 None => 0.0,
388 };
389 anim_state.last_frame_time_nanos.set(Some(frame_time_nanos));
390
391 let (mut next_value, next_velocity) = cranpose_animation::advance_spring(
392 anim_state.value.get(),
393 anim_state.velocity.get(),
394 anim_state.target,
395 anim_state.params.damping_ratio,
396 anim_state.params.stiffness,
397 dt.max(0.0),
398 );
399
400 let is_finished = (next_value - anim_state.target).abs() < SETTLE_REST_DISTANCE
401 && next_velocity.abs() < SETTLE_REST_VELOCITY;
402 if is_finished {
403 next_value = anim_state.target;
404 anim_state.is_running.set(false);
405 }
406
407 let delta = next_value - anim_state.value.get();
408 anim_state.value.set(next_value);
409 anim_state.velocity.set(next_velocity);
410
411 let consumed = if delta.abs() > 0.0001 {
412 on_scroll(delta)
413 } else {
414 delta
415 };
416 let boundary_hit = (delta - consumed).abs() > BOUNDARY_EPSILON;
417 if boundary_hit {
418 anim_state.is_running.set(false);
419 hit_boundary.set(true);
420 }
421
422 !is_finished && !boundary_hit
423 };
424
425 if should_continue {
426 if let Some(on_end_fn) = on_end.borrow_mut().take() {
427 schedule_next_settle_frame(
428 state_for_closure.clone(),
429 frame_clock_for_closure.clone(),
430 on_scroll,
431 on_end_fn,
432 );
433 }
434 } else if let Some(end_fn) = on_end.borrow_mut().take() {
435 let state_guard = state_for_closure.borrow();
436 let velocity = state_guard
437 .as_ref()
438 .map_or(0.0, |anim_state| anim_state.velocity.get());
439 end_fn(SettleEnd {
440 velocity,
441 hit_boundary: hit_boundary.get(),
442 });
443 }
444 });
445
446 if let Some(anim_state) = state.borrow_mut().as_mut() {
447 anim_state.registration = Some(registration);
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use std::{cell::Cell, rc::Rc, sync::Arc};
454
455 use cranpose_core::{DefaultScheduler, Runtime};
456
457 use super::*;
458
459 #[test]
460 fn test_min_velocity_threshold() {
461 assert_eq!(MIN_FLING_VELOCITY, 300.0);
462 }
463
464 #[test]
465 fn settle_animation_springs_to_target_and_ends() {
466 let runtime = Runtime::new(Arc::new(DefaultScheduler));
467 let handle = runtime.handle();
468 let settle = SettleAnimation::new(handle.clone(), SpringParams::SETTLE_POLICY);
469 let position = Rc::new(Cell::new(30.0f32));
470 let ended = Rc::new(Cell::new(false));
471 let position_for_scroll = Rc::clone(&position);
472 let ended_for_end = Rc::clone(&ended);
473 settle.start_settle(
474 30.0,
475 0.0,
476 52.0,
477 move |delta| {
478 position_for_scroll.set(position_for_scroll.get() + delta);
479 delta
480 },
481 move |_| ended_for_end.set(true),
482 );
483 for frame in 0..240u64 {
484 handle.drain_frame_callbacks(frame * 16_000_000);
485 if ended.get() {
486 break;
487 }
488 }
489 assert!(ended.get(), "settle animation must finish");
490 assert!(
491 (position.get() - 52.0).abs() < 0.2,
492 "settle must land on the target, got {}",
493 position.get()
494 );
495 }
496
497 #[test]
498 fn settle_reports_reduced_velocity_when_crossing_boundary() {
499 let runtime = Runtime::new(Arc::new(DefaultScheduler));
500 let handle = runtime.handle();
501 let settle = SettleAnimation::new(handle.clone(), SpringParams::SETTLE_POLICY);
502 let position = Rc::new(Cell::new(30.0f32));
503 let ended = Rc::new(Cell::new(None::<(f32, bool)>));
504 let position_for_scroll = Rc::clone(&position);
505 let ended_for_end = Rc::clone(&ended);
506 settle.start_settle(
507 30.0,
508 -1_200.0,
509 0.0,
510 move |delta| {
511 let previous = position_for_scroll.get();
512 let next = (previous + delta).max(0.0);
513 position_for_scroll.set(next);
514 next - previous
515 },
516 move |end| ended_for_end.set(Some((end.velocity, end.hit_boundary))),
517 );
518 for frame in 0..240u64 {
519 handle.drain_frame_callbacks(frame * 16_000_000);
520 if ended.get().is_some() {
521 break;
522 }
523 }
524
525 let (velocity, hit_boundary) = ended.get().expect("settle must finish");
526 assert!(hit_boundary);
527 assert!(velocity < 0.0 && velocity.abs() < 1_200.0);
528 assert_eq!(position.get(), 0.0);
529 }
530
531 #[test]
532 fn fling_rest_position_is_beyond_start_in_fling_direction() {
533 let rest = fling_rest_position(100.0, 900.0);
534 assert!(rest > 100.0, "rest {rest} must be past the start");
535 assert_eq!(fling_rest_position(100.0, 0.0), 100.0);
536 }
537
538 #[test]
539 fn test_on_end_called_when_boundary_hit() {
540 let runtime = Runtime::new(Arc::new(DefaultScheduler));
541 let handle = runtime.handle();
542 let fling = FlingAnimation::new(handle.clone());
543 let finished = Rc::new(Cell::new(false));
544 let finished_flag = Rc::clone(&finished);
545
546 fling.start_fling(0.0, 10_000.0, |_| 0.0, move || finished_flag.set(true));
547
548 handle.drain_frame_callbacks(0);
549 handle.drain_frame_callbacks(16_000_000);
550
551 assert!(finished.get());
552 }
553}