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