1use std::panic::Location;
2use std::time::Duration;
3
4use fret_ui::{ElementContext, Invalidation, UiHost};
5use fret_ui_headless::motion::inertia::{InertiaBounds, InertiaSimulation};
6use fret_ui_headless::motion::simulation::Simulation1D;
7use fret_ui_headless::motion::spring::{SpringDescription, SpringSimulation};
8use fret_ui_headless::motion::tolerance::Tolerance;
9
10use crate::declarative::motion::{
11 DrivenMotionF32, effective_frame_delta_for_cx, tween_value_at, tween_velocity_at,
12};
13use crate::declarative::scheduling::set_continuous_frames;
14
15#[derive(Debug, Clone, Copy, PartialEq)]
16pub struct MotionKickF32 {
17 pub id: u64,
18 pub velocity: f32,
19}
20
21#[derive(Debug, Clone, Copy)]
22pub struct TweenSpecF32 {
23 pub duration: Duration,
24 pub ease: fn(f32) -> f32,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq)]
28pub struct SpringSpecF32 {
29 pub spring: SpringDescription,
30 pub tolerance: Tolerance,
31 pub snap_to_target: bool,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq)]
35pub struct InertiaSpecF32 {
36 pub drag: f64,
37 pub bounds: Option<(f32, f32)>,
38 pub bounce_spring: SpringDescription,
39 pub tolerance: Tolerance,
40}
41
42#[derive(Debug, Clone, Copy)]
43pub enum MotionToSpecF32 {
44 Tween(TweenSpecF32),
45 Spring(SpringSpecF32),
46}
47
48#[derive(Debug, Clone, Copy)]
49pub enum MotionValueF32Update {
50 Snap(f32),
51 To {
52 target: f32,
53 spec: MotionToSpecF32,
54 kick: Option<MotionKickF32>,
55 },
56 Inertia {
57 spec: InertiaSpecF32,
58 kick: MotionKickF32,
59 },
60}
61
62#[derive(Debug, Clone, Copy, PartialEq)]
63enum MotionValueF32Kind {
64 Snap,
65 Tween,
66 Spring,
67 Inertia,
68}
69
70#[derive(Debug, Clone, Copy)]
71struct MotionValueF32State {
72 initialized: bool,
73 last_frame_id: u64,
74 kind: MotionValueF32Kind,
75
76 value: f32,
77 velocity: f32,
78 animating: bool,
79 elapsed: Duration,
80
81 tween_start: f32,
82 tween_target: f32,
83 tween_duration: Duration,
84 tween_ease: fn(f32) -> f32,
85
86 spring_start: f32,
87 spring_target: f32,
88 spring: SpringDescription,
89 spring_tolerance: Tolerance,
90 spring_snap_to_target: bool,
91 spring_last_kick_id: u64,
92
93 inertia_start: f32,
94 inertia_start_velocity: f32,
95 inertia_drag: f64,
96 inertia_bounds: Option<(f32, f32)>,
97 inertia_bounce_spring: SpringDescription,
98 inertia_tolerance: Tolerance,
99 inertia_last_kick_id: u64,
100}
101
102impl Default for MotionValueF32State {
103 fn default() -> Self {
104 Self {
105 initialized: false,
106 last_frame_id: 0,
107 kind: MotionValueF32Kind::Snap,
108 value: 0.0,
109 velocity: 0.0,
110 animating: false,
111 elapsed: Duration::ZERO,
112
113 tween_start: 0.0,
114 tween_target: 0.0,
115 tween_duration: Duration::from_millis(200),
116 tween_ease: crate::headless::easing::smoothstep,
117
118 spring_start: 0.0,
119 spring_target: 0.0,
120 spring: SpringDescription::with_duration_and_bounce(Duration::from_millis(240), 0.0),
121 spring_tolerance: Tolerance::default(),
122 spring_snap_to_target: true,
123 spring_last_kick_id: 0,
124
125 inertia_start: 0.0,
126 inertia_start_velocity: 0.0,
127 inertia_drag: 0.135,
128 inertia_bounds: None,
129 inertia_bounce_spring: SpringDescription::with_duration_and_bounce(
130 Duration::from_millis(240),
131 0.25,
132 ),
133 inertia_tolerance: Tolerance::default(),
134 inertia_last_kick_id: 0,
135 }
136 }
137}
138
139#[track_caller]
140pub fn drive_motion_value_f32<H: UiHost>(
141 cx: &mut ElementContext<'_, H>,
142 initial: f32,
143 update: MotionValueF32Update,
144) -> DrivenMotionF32 {
145 let reduced_motion = super::prefers_reduced_motion(cx, Invalidation::Paint, false);
146 let loc = Location::caller();
147 cx.keyed(
148 (
149 loc.file(),
150 loc.line(),
151 loc.column(),
152 "drive_motion_value_f32",
153 ),
154 |cx| {
155 let frame_id = cx.frame_id.0;
156 let dt = effective_frame_delta_for_cx(cx);
157
158 let out = cx.slot_state(MotionValueF32State::default, |st| {
159 if !st.initialized {
160 st.initialized = true;
161 st.last_frame_id = frame_id;
162 st.kind = MotionValueF32Kind::Snap;
163 st.value = initial;
164 st.velocity = 0.0;
165 st.animating = false;
166 st.elapsed = Duration::ZERO;
167 }
168
169 if reduced_motion {
170 match update {
171 MotionValueF32Update::Snap(v) => {
172 st.kind = MotionValueF32Kind::Snap;
173 st.value = v;
174 st.velocity = 0.0;
175 st.animating = false;
176 st.elapsed = Duration::ZERO;
177 }
178 MotionValueF32Update::To { target, .. } => {
179 st.kind = MotionValueF32Kind::Snap;
180 st.value = target;
181 st.velocity = 0.0;
182 st.animating = false;
183 st.elapsed = Duration::ZERO;
184 }
185 MotionValueF32Update::Inertia { .. } => {
186 st.kind = MotionValueF32Kind::Snap;
187 st.velocity = 0.0;
188 st.animating = false;
189 st.elapsed = Duration::ZERO;
190 }
191 }
192
193 return DrivenMotionF32 {
194 value: st.value,
195 velocity: st.velocity,
196 animating: st.animating,
197 };
198 }
199
200 match update {
201 MotionValueF32Update::Snap(v) => {
202 st.kind = MotionValueF32Kind::Snap;
203 st.value = v;
204 st.velocity = 0.0;
205 st.animating = false;
206 st.elapsed = Duration::ZERO;
207 }
208 MotionValueF32Update::To { target, spec, kick } => match spec {
209 MotionToSpecF32::Tween(spec) => {
210 let needs_retarget = st.kind != MotionValueF32Kind::Tween
211 || st.tween_target != target
212 || st.tween_duration != spec.duration
213 || st.tween_ease as usize != spec.ease as usize;
214
215 if needs_retarget {
216 st.kind = MotionValueF32Kind::Tween;
217 st.tween_start = st.value;
218 st.tween_target = target;
219 st.tween_duration = spec.duration;
220 st.tween_ease = spec.ease;
221 st.elapsed = Duration::ZERO;
222 st.animating = true;
223 }
224
225 if st.animating && st.last_frame_id != frame_id {
226 st.last_frame_id = frame_id;
227 st.elapsed = st.elapsed.saturating_add(dt);
228
229 st.value = tween_value_at(
230 st.tween_start,
231 st.tween_target,
232 st.tween_duration,
233 st.tween_ease,
234 st.elapsed,
235 );
236 st.velocity = tween_velocity_at(
237 st.tween_start,
238 st.tween_target,
239 st.tween_duration,
240 st.tween_ease,
241 st.elapsed,
242 );
243
244 if st.elapsed >= st.tween_duration {
245 st.value = st.tween_target;
246 st.velocity = 0.0;
247 st.animating = false;
248 }
249 } else if st.last_frame_id == 0 {
250 st.last_frame_id = frame_id;
251 }
252 }
253 MotionToSpecF32::Spring(spec) => {
254 let kick_retarget = kick.is_some()
255 && kick.map(|k| k.id).unwrap_or(0) != st.spring_last_kick_id;
256 let needs_retarget = st.kind != MotionValueF32Kind::Spring
257 || st.spring_target != target
258 || st.spring != spec.spring
259 || st.spring_tolerance != spec.tolerance
260 || st.spring_snap_to_target != spec.snap_to_target
261 || kick_retarget;
262
263 if needs_retarget {
264 st.kind = MotionValueF32Kind::Spring;
265 st.spring_start = st.value;
266 st.spring_target = target;
267 st.spring = spec.spring;
268 st.spring_tolerance = spec.tolerance;
269 st.spring_snap_to_target = spec.snap_to_target;
270 st.elapsed = Duration::ZERO;
271 st.animating = true;
272
273 if let Some(kick) = kick
274 && kick.id != st.spring_last_kick_id
275 {
276 st.velocity = kick.velocity;
277 st.spring_last_kick_id = kick.id;
278 }
279 }
280
281 if st.animating && st.last_frame_id != frame_id {
282 st.last_frame_id = frame_id;
283 st.elapsed = st.elapsed.saturating_add(dt);
284
285 let sim = SpringSimulation::new(
286 st.spring,
287 st.spring_start as f64,
288 st.spring_target as f64,
289 st.velocity as f64,
290 st.spring_snap_to_target,
291 st.spring_tolerance,
292 );
293
294 st.value = sim.x(st.elapsed) as f32;
295 st.velocity = sim.dx(st.elapsed) as f32;
296
297 if sim.is_done(st.elapsed) {
298 st.value = st.spring_target;
299 st.velocity = 0.0;
300 st.animating = false;
301 }
302 } else if st.last_frame_id == 0 {
303 st.last_frame_id = frame_id;
304 }
305 }
306 },
307 MotionValueF32Update::Inertia { spec, kick } => {
308 let kick_retarget = kick.id != st.inertia_last_kick_id;
309 let needs_retarget = st.kind != MotionValueF32Kind::Inertia
310 || kick_retarget
311 || st.inertia_drag != spec.drag
312 || st.inertia_bounds != spec.bounds
313 || st.inertia_bounce_spring != spec.bounce_spring
314 || st.inertia_tolerance != spec.tolerance;
315
316 if needs_retarget {
317 st.kind = MotionValueF32Kind::Inertia;
318 st.inertia_drag = spec.drag;
319 st.inertia_bounds = spec.bounds;
320 st.inertia_bounce_spring = spec.bounce_spring;
321 st.inertia_tolerance = spec.tolerance;
322
323 if kick.id != st.inertia_last_kick_id {
324 st.inertia_last_kick_id = kick.id;
325 st.inertia_start = st.value;
326 st.inertia_start_velocity = kick.velocity;
327 st.velocity = kick.velocity;
328 st.elapsed = Duration::ZERO;
329 st.animating = true;
330 } else if st.animating {
331 st.inertia_start = st.value;
332 st.inertia_start_velocity = st.velocity;
333 st.elapsed = Duration::ZERO;
334 }
335 }
336
337 if st.animating && st.last_frame_id != frame_id {
338 st.last_frame_id = frame_id;
339 st.elapsed = st.elapsed.saturating_add(dt);
340
341 let inertia_bounds =
342 st.inertia_bounds.map(|(min, max)| InertiaBounds {
343 min: min as f64,
344 max: max as f64,
345 });
346 let sim = InertiaSimulation::new(
347 st.inertia_start as f64,
348 st.inertia_start_velocity as f64,
349 st.inertia_drag,
350 inertia_bounds,
351 st.inertia_bounce_spring,
352 st.inertia_tolerance,
353 );
354
355 st.value = sim.x(st.elapsed) as f32;
356 st.velocity = sim.dx(st.elapsed) as f32;
357
358 if sim.is_done(st.elapsed) {
359 st.value = sim.final_x() as f32;
360 st.velocity = 0.0;
361 st.animating = false;
362 }
363 } else if st.last_frame_id == 0 {
364 st.last_frame_id = frame_id;
365 }
366 }
367 }
368
369 DrivenMotionF32 {
370 value: st.value,
371 velocity: st.velocity,
372 animating: st.animating,
373 }
374 });
375
376 set_continuous_frames(cx, out.animating);
377 if out.animating {
378 cx.notify_for_animation_frame();
379 }
380 out
381 },
382 )
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 use fret_app::App;
390 use fret_core::{AppWindowId, Px, WindowFrameClockService};
391 use fret_runtime::{FrameId, TickId};
392 use fret_ui::elements::with_element_cx;
393
394 fn bounds() -> fret_core::Rect {
395 fret_core::Rect::new(
396 fret_core::Point::new(Px(0.0), Px(0.0)),
397 fret_core::Size::new(Px(800.0), Px(600.0)),
398 )
399 }
400
401 fn drive<H: UiHost>(
402 cx: &mut ElementContext<'_, H>,
403 update: MotionValueF32Update,
404 ) -> DrivenMotionF32 {
405 drive_motion_value_f32(cx, 0.0, update)
406 }
407
408 #[test]
409 fn motion_value_snap_then_spring_to_does_not_jump_on_first_frame() {
410 let window = AppWindowId::default();
411 let mut app = App::new();
412
413 app.with_global_mut(WindowFrameClockService::default, |svc, _app| {
414 svc.set_fixed_delta(window, Some(Duration::from_millis(16)));
415 });
416
417 for fid in [FrameId(1), FrameId(2)] {
418 app.set_frame_id(fid);
419 app.with_global_mut(WindowFrameClockService::default, |svc, app| {
420 svc.record_frame(window, app.frame_id());
421 });
422 }
423
424 app.set_tick_id(TickId(1));
425 let mut out = with_element_cx(&mut app, window, bounds(), "motion_value", |cx| {
426 drive(cx, MotionValueF32Update::Snap(10.0))
427 });
428 assert_eq!(out.value, 10.0);
429 assert!(!out.animating);
430
431 app.set_tick_id(TickId(2));
432 out = with_element_cx(&mut app, window, bounds(), "motion_value", |cx| {
433 drive(
434 cx,
435 MotionValueF32Update::To {
436 target: 20.0,
437 spec: MotionToSpecF32::Spring(SpringSpecF32 {
438 spring: SpringDescription::with_duration_and_bounce(
439 Duration::from_millis(240),
440 0.0,
441 ),
442 tolerance: Tolerance::default(),
443 snap_to_target: true,
444 }),
445 kick: Some(MotionKickF32 {
446 id: 1,
447 velocity: 0.0,
448 }),
449 },
450 )
451 });
452
453 assert_eq!(out.value, 10.0);
455 assert!(out.animating);
456 }
457}