cranpose_ui/
fling_animation.rs1use cranpose_animation::{FloatDecayAnimationSpec, SplineBasedDecaySpec};
6use cranpose_core::{FrameCallbackRegistration, FrameClock, RuntimeHandle};
7use std::cell::{Cell, RefCell};
8use std::rc::Rc;
9
10pub const MIN_FLING_VELOCITY: f32 = 1.0;
13
14const DEFAULT_FLING_FRICTION: f32 = 0.015;
16
17const BOUNDARY_EPSILON: f32 = 0.5;
19
20fn schedule_next_frame<F, G>(
23 state: Rc<RefCell<Option<FlingAnimationState>>>,
24 frame_clock: FrameClock,
25 on_scroll: F,
26 on_end: G,
27) where
28 F: Fn(f32) -> f32 + 'static,
29 G: FnOnce() + 'static,
30{
31 let state_for_closure = state.clone();
32 let frame_clock_for_closure = frame_clock.clone();
33 let on_end = RefCell::new(Some(on_end));
34
35 let registration = frame_clock.with_frame_nanos(move |frame_time_nanos| {
36 let should_continue = {
37 let state_guard = state_for_closure.borrow();
38 let Some(anim_state) = state_guard.as_ref() else {
39 return;
40 };
41
42 if !anim_state.is_running.get() {
43 return;
44 }
45
46 let start_time = match anim_state.start_frame_time_nanos.get() {
47 Some(value) => value,
48 None => {
49 anim_state
50 .start_frame_time_nanos
51 .set(Some(frame_time_nanos));
52 frame_time_nanos
53 }
54 };
55
56 let play_time_nanos = frame_time_nanos.saturating_sub(start_time) as i64;
57
58 let new_value = anim_state.decay_spec.get_value_from_nanos(
59 play_time_nanos,
60 anim_state.initial_value,
61 anim_state.initial_velocity,
62 );
63
64 let last = anim_state.last_value.get();
65 let delta = new_value - last;
66 anim_state.last_value.set(new_value);
67 anim_state
68 .total_delta
69 .set(anim_state.total_delta.get() + delta);
70
71 let duration_nanos = anim_state
72 .decay_spec
73 .get_duration_nanos(anim_state.initial_value, anim_state.initial_velocity);
74
75 let current_velocity = anim_state.decay_spec.get_velocity_from_nanos(
76 play_time_nanos,
77 anim_state.initial_value,
78 anim_state.initial_velocity,
79 );
80
81 let is_finished = play_time_nanos >= duration_nanos
82 || current_velocity.abs() < anim_state.decay_spec.abs_velocity_threshold();
83
84 if is_finished {
85 anim_state.is_running.set(false);
86 }
87
88 let consumed = if delta.abs() > 0.001 {
89 on_scroll(delta)
90 } else {
91 0.0
92 };
93
94 let hit_boundary = (delta - consumed).abs() > BOUNDARY_EPSILON;
95 if hit_boundary {
96 anim_state.is_running.set(false);
97 }
98
99 !is_finished && !hit_boundary
100 };
101
102 if should_continue {
103 if let Some(on_end_fn) = on_end.borrow_mut().take() {
104 schedule_next_frame(
105 state_for_closure.clone(),
106 frame_clock_for_closure.clone(),
107 on_scroll,
108 on_end_fn,
109 );
110 }
111 } else if let Some(end_fn) = on_end.borrow_mut().take() {
112 end_fn();
113 }
114 });
115
116 if let Some(anim_state) = state.borrow_mut().as_mut() {
118 anim_state.registration = Some(registration);
119 }
120}
121
122struct FlingAnimationState {
124 initial_value: f32,
126 last_value: Cell<f32>,
128 initial_velocity: f32,
130 start_frame_time_nanos: Cell<Option<u64>>,
132 decay_spec: SplineBasedDecaySpec,
134 registration: Option<FrameCallbackRegistration>,
136 is_running: Cell<bool>,
138 total_delta: Cell<f32>,
140}
141
142pub struct FlingAnimation {
147 state: Rc<RefCell<Option<FlingAnimationState>>>,
148 frame_clock: FrameClock,
149}
150
151impl FlingAnimation {
152 pub fn new(runtime: RuntimeHandle) -> Self {
154 Self {
155 state: Rc::new(RefCell::new(None)),
156 frame_clock: runtime.frame_clock(),
157 }
158 }
159
160 pub fn start_fling<F, G>(
169 &self,
170 initial_value: f32,
171 velocity: f32,
172 density: f32,
173 on_scroll: F,
174 on_end: G,
175 ) where
176 F: Fn(f32) -> f32 + 'static, G: FnOnce() + 'static,
178 {
179 self.cancel();
181
182 if velocity.abs() < MIN_FLING_VELOCITY {
184 on_end();
185 return;
186 }
187
188 let friction = DEFAULT_FLING_FRICTION;
190 let calc = cranpose_animation::FlingCalculator::new(friction, density);
191 let decay_spec = SplineBasedDecaySpec::with_calculator(calc);
192
193 let anim_state = FlingAnimationState {
194 initial_value,
195 last_value: Cell::new(initial_value),
196 initial_velocity: velocity,
197 start_frame_time_nanos: Cell::new(None),
198 decay_spec,
199 registration: None,
200 is_running: Cell::new(true),
201 total_delta: Cell::new(0.0),
202 };
203
204 *self.state.borrow_mut() = Some(anim_state);
205
206 schedule_next_frame(
208 self.state.clone(),
209 self.frame_clock.clone(),
210 on_scroll,
211 on_end,
212 );
213 }
214
215 pub fn cancel(&self) {
216 if let Some(state) = self.state.borrow_mut().take() {
217 state.is_running.set(false);
219 drop(state.registration);
221 }
222 }
223
224 pub fn is_running(&self) -> bool {
226 self.state
227 .borrow()
228 .as_ref()
229 .is_some_and(|s| s.is_running.get())
230 }
231}
232
233impl Clone for FlingAnimation {
234 fn clone(&self) -> Self {
235 Self {
236 state: self.state.clone(),
237 frame_clock: self.frame_clock.clone(),
238 }
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use cranpose_core::DefaultScheduler;
246 use cranpose_core::Runtime;
247 use std::cell::Cell;
248 use std::rc::Rc;
249 use std::sync::Arc;
250
251 #[test]
252 fn test_min_velocity_threshold() {
253 assert_eq!(MIN_FLING_VELOCITY, 1.0);
254 }
255
256 #[test]
257 fn test_on_end_called_when_boundary_hit() {
258 let runtime = Runtime::new(Arc::new(DefaultScheduler));
259 let handle = runtime.handle();
260 let fling = FlingAnimation::new(handle.clone());
261 let finished = Rc::new(Cell::new(false));
262 let finished_flag = Rc::clone(&finished);
263
264 fling.start_fling(0.0, 10_000.0, 1.0, |_| 0.0, move || finished_flag.set(true));
265
266 handle.drain_frame_callbacks(0);
267 handle.drain_frame_callbacks(16_000_000);
268
269 assert!(finished.get());
270 }
271}