Skip to main content

gpui_ce_motion/
value.rs

1//! Scalar presentation values, explicit updates, and bounded inertial release.
2use crate::{Spring, Transition};
3use std::time::Duration;
4
5/// Deceleration and optional hard bounds for inertial motion.
6#[derive(Clone, Copy, Debug, PartialEq)]
7pub struct Inertia {
8    tau: f64,
9    bounds: Option<(f64, f64)>,
10    spring: Spring,
11}
12
13impl Inertia {
14    /// Creates exponential deceleration. Panics for a zero time constant.
15    pub fn new(time_constant: Duration) -> Self {
16        assert!(
17            !time_constant.is_zero(),
18            "inertia time constant must be positive"
19        );
20
21        Self {
22            tau: time_constant.as_secs_f64(),
23            bounds: None,
24            spring: Spring::default(),
25        }
26    }
27
28    /// Sets finite ordered bounds. Crossing a bound switches analytically to a spring.
29    pub fn bounds(mut self, min: f64, max: f64) -> Self {
30        assert!(
31            min.is_finite() && max.is_finite() && min <= max,
32            "invalid inertia bounds"
33        );
34        self.bounds = Some((min, max));
35
36        self
37    }
38
39    /// Chooses the spring used at a boundary.
40    pub fn spring(mut self, spring: Spring) -> Self {
41        self.spring = spring;
42
43        self
44    }
45
46    /// Projects a resting position and clamps it to configured bounds.
47    pub fn project(self, position: f64, velocity: f64) -> f64 {
48        assert!(
49            position.is_finite() && velocity.is_finite(),
50            "non-finite projection"
51        );
52        let value = finite(position + velocity * self.tau);
53
54        self.bounds
55            .map_or(value, |(min, max)| value.clamp(min, max))
56    }
57}
58
59impl Default for Inertia {
60    fn default() -> Self {
61        Self::new(Duration::from_millis(350))
62    }
63}
64
65/// A scalar presentation and its velocity, in caller-chosen units per second.
66#[derive(Clone, Copy, Debug)]
67pub struct ValueFrame {
68    value: f64,
69    velocity: f64,
70    active: bool,
71    completed: bool,
72}
73
74impl ValueFrame {
75    /// Current presentation value.
76    pub fn value(self) -> f64 {
77        self.value
78    }
79
80    /// Current velocity in units per second.
81    pub fn velocity(self) -> f64 {
82        self.velocity
83    }
84
85    /// Whether another sample is required to complete motion.
86    pub fn is_animating(self) -> bool {
87        self.active
88    }
89
90    /// A completion edge, consumed by this sample.
91    pub fn just_completed(self) -> bool {
92        self.completed
93    }
94}
95
96#[derive(Clone, Copy, Debug, PartialEq)]
97enum Mode {
98    Transition,
99    Inertia(Inertia),
100}
101
102/// A retained scalar motion value, with no timers, observers, or heap allocations.
103/// Feed its samples into any native style, paint parameter, text, or derived value.
104/// Arithmetic at numeric extremes saturates to finite limits; indeterminate overflow
105/// resolves to zero. Use ordinary UI-sized units for meaningful physical trajectories.
106#[derive(Clone, Debug)]
107pub struct MotionValue {
108    value: f64,
109    velocity: f64,
110    from: f64,
111    initial_velocity: f64,
112    target: f64,
113    transition: Transition,
114    delay: Duration,
115    started: Duration,
116    last: Duration,
117    set_time: Duration,
118    active: bool,
119    completed: bool,
120    mode: Mode,
121}
122
123impl MotionValue {
124    /// Creates a resting value. Panics for non-finite input.
125    pub fn new(value: f64) -> Self {
126        assert!(value.is_finite(), "motion value must be finite");
127
128        Self {
129            value,
130            velocity: 0.0,
131            from: value,
132            initial_velocity: 0.0,
133            target: value,
134            transition: Transition::Instant,
135            delay: Duration::ZERO,
136            started: Duration::ZERO,
137            last: Duration::ZERO,
138            set_time: Duration::ZERO,
139            active: false,
140            completed: false,
141            mode: Mode::Transition,
142        }
143    }
144
145    /// Last sampled value; this does not advance time.
146    pub fn get(&self) -> f64 {
147        self.value
148    }
149
150    pub(crate) fn active(&self) -> bool {
151        self.active
152    }
153
154    pub(crate) fn target(&self) -> f64 {
155        self.target
156    }
157
158    pub(crate) fn matches(&self, target: f64, transition: Transition) -> bool {
159        self.mode == Mode::Transition
160            && self.target == target
161            && self.transition == transition
162            && self.delay.is_zero()
163    }
164
165    #[cfg(feature = "text")]
166    pub(crate) fn translate(&mut self, offset: f64) {
167        debug_assert_eq!(self.mode, Mode::Transition);
168        self.value = finite(self.value + offset);
169        self.from = finite(self.from + offset);
170        self.target = finite(self.target + offset);
171    }
172
173    /// Converts positive linear units while retaining trajectory, momentum and
174    /// inertia bounds. Panics for a nonpositive or non-finite factor. Numeric
175    /// overflow saturates using the same policy as ordinary motion arithmetic.
176    /// Spring rest thresholds remain expressed in the new units.
177    pub fn rescale(&mut self, factor: f64) {
178        assert!(factor.is_finite() && factor > 0.0, "invalid unit scale");
179        self.value = finite(self.value * factor);
180        self.velocity = finite(self.velocity * factor);
181        self.from = finite(self.from * factor);
182        self.initial_velocity = finite(self.initial_velocity * factor);
183        self.target = finite(self.target * factor);
184
185        if let Mode::Inertia(ref mut inertia) = self.mode {
186            inertia.bounds = inertia
187                .bounds
188                .map(|(min, max)| (finite(min * factor), finite(max * factor)));
189        }
190    }
191
192    /// Last sampled velocity.
193    pub fn velocity(&self) -> f64 {
194        self.velocity
195    }
196
197    /// Directly sets a value and estimates velocity from elapsed time. Cancels animation.
198    pub fn set(&mut self, value: f64, now: Duration) {
199        assert!(value.is_finite(), "motion value must be finite");
200        let now = now.max(self.last);
201        let dt = now.saturating_sub(self.set_time).as_secs_f64();
202
203        self.velocity = if dt > 0.0 {
204            finite((value - self.value) / dt)
205        } else {
206            self.velocity
207        };
208        self.value = value;
209        self.target = value;
210        self.from = value;
211        self.active = false;
212        self.completed = false;
213        self.last = now;
214        self.set_time = now;
215        self.mode = Mode::Transition;
216    }
217
218    /// Assigns a value, clears momentum, and cancels animation without a completion event.
219    pub fn jump(&mut self, value: f64, now: Duration) {
220        self.set(value, now);
221        self.velocity = 0.0;
222    }
223
224    /// Retargets from the presentation at `now`. Identical declarations do not restart.
225    pub fn animate(&mut self, target: f64, transition: Transition, now: Duration) {
226        self.animate_delayed(target, transition, Duration::ZERO, now);
227    }
228
229    /// Retargets with a delay, preserving incoming spring velocity across the hold.
230    pub fn animate_delayed(
231        &mut self,
232        target: f64,
233        transition: Transition,
234        delay: Duration,
235        now: Duration,
236    ) {
237        assert!(target.is_finite(), "motion target must be finite");
238        let now = now.max(self.last);
239
240        if self.mode == Mode::Transition
241            && self.target == target
242            && self.transition == transition
243            && self.delay == delay
244        {
245            return;
246        }
247
248        self.advance(now);
249        self.from = self.value;
250        self.initial_velocity = self.velocity;
251        self.target = target;
252        self.transition = transition;
253        self.delay = delay;
254        self.started = now;
255        self.active = self.from != target || self.velocity != 0.0;
256        self.mode = Mode::Transition;
257        self.completed = false;
258    }
259
260    /// Starts exponential inertia from the last presentation and velocity.
261    /// Bounds switch to a spring at the exact crossing time, independent of frame rate.
262    pub fn coast(&mut self, inertia: Inertia, now: Duration) {
263        let now = now.max(self.last);
264
265        self.advance(now);
266        self.from = self.value;
267        self.initial_velocity = self.velocity;
268        self.target = inertia.project(self.value, self.velocity);
269        self.started = now;
270        self.delay = Duration::ZERO;
271        self.mode = Mode::Inertia(inertia);
272        self.active = self.value != self.target || self.velocity != 0.0;
273        self.completed = false;
274    }
275
276    /// Samples a monotonic clock. Reduced motion finishes immediately, including inertia.
277    pub fn sample(&mut self, now: Duration, reduced_motion: bool) -> ValueFrame {
278        if reduced_motion && self.active {
279            self.finish();
280        } else {
281            self.advance(now.max(self.last));
282        }
283
284        self.last = self.last.max(now);
285        let completed = std::mem::take(&mut self.completed);
286
287        ValueFrame {
288            value: self.value,
289            velocity: self.velocity,
290            active: self.active,
291            completed,
292        }
293    }
294
295    fn finish(&mut self) {
296        self.value = self.target;
297        self.velocity = 0.0;
298        self.completed |= self.active;
299        self.active = false;
300    }
301
302    fn advance(&mut self, now: Duration) {
303        self.last = now;
304
305        if !self.active {
306            return;
307        }
308
309        let elapsed = now.saturating_sub(self.started);
310
311        if elapsed < self.delay {
312            self.velocity = 0.0;
313
314            return;
315        }
316
317        let t = elapsed.saturating_sub(self.delay).as_secs_f64();
318
319        let (value, velocity, done) = match self.mode {
320            Mode::Transition => sample_transition(
321                self.from,
322                self.initial_velocity,
323                self.target,
324                self.transition,
325                t,
326            ),
327            Mode::Inertia(config) => {
328                let amplitude = finite(self.initial_velocity * config.tau);
329                let end = finite(self.from + amplitude);
330
331                let boundary = config.bounds.and_then(|(min, max)| {
332                    if self.from < min {
333                        Some((min, 0.0))
334                    } else if self.from > max {
335                        Some((max, 0.0))
336                    } else if end < min || end > max {
337                        let bound = if end < min { min } else { max };
338                        let crossing = -config.tau * (1.0 - (bound - self.from) / amplitude).ln();
339
340                        Some((bound, crossing.max(0.0)))
341                    } else {
342                        None
343                    }
344                });
345
346                if let Some((bound, crossing)) = boundary
347                    && t >= crossing
348                {
349                    let from = if crossing == 0.0 { self.from } else { bound };
350                    let velocity = self.initial_velocity * (-crossing / config.tau).exp();
351
352                    sample_transition(
353                        from,
354                        velocity,
355                        bound,
356                        Transition::Spring(config.spring),
357                        t - crossing,
358                    )
359                } else {
360                    let decay = (-t / config.tau).exp();
361                    let value = finite(self.from + amplitude * (1.0 - decay));
362                    let velocity = finite(self.initial_velocity * decay);
363
364                    (
365                        value,
366                        velocity,
367                        (end - value).abs() <= 0.001 && velocity.abs() <= 0.001,
368                    )
369                }
370            }
371        };
372
373        self.value = value;
374        self.velocity = velocity;
375        self.set_time = now;
376
377        if done {
378            self.finish();
379        }
380    }
381}
382
383pub(crate) fn sample_transition(
384    from: f64,
385    velocity: f64,
386    target: f64,
387    transition: Transition,
388    t: f64,
389) -> (f64, f64, bool) {
390    match transition {
391        Transition::Instant => (target, 0.0, true),
392        Transition::Tween(duration, easing) => {
393            let seconds = duration.as_secs_f64();
394
395            if t >= seconds {
396                return (target, 0.0, true);
397            }
398
399            let (progress, derivative) = easing.sample(t / seconds);
400
401            (
402                if t == 0.0 {
403                    from
404                } else {
405                    finite(from * (1.0 - progress) + target * progress)
406                },
407                finite(finite(target - from) * derivative / seconds),
408                false,
409            )
410        }
411        Transition::Spring(spring) => {
412            if t == 0.0 {
413                return (from, velocity, false);
414            }
415
416            let m = spring.matrix(t);
417            let displacement = finite(from - target);
418            let value = finite(target + m[0] * displacement + m[1] * velocity);
419            let velocity = finite(m[2] * displacement + m[3] * velocity);
420
421            (
422                value,
423                velocity,
424                spring.is_settled(value, velocity, target, 0.001),
425            )
426        }
427    }
428}
429
430pub(crate) fn finite(value: f64) -> f64 {
431    if value.is_nan() {
432        0.0
433    } else {
434        value.clamp(-f64::MAX, f64::MAX)
435    }
436}