Skip to main content

gpui_ce_motion/
motion.rs

1use std::time::Duration;
2#[cfg(feature = "gpui")]
3use std::time::Instant;
4
5use crate::{MotionValue, Transition};
6
7const CHANNELS: usize = 6;
8
9/// Opacity, offsets, and optional width, height, and border radius in logical pixels.
10///
11/// Builders panic for non-finite inputs, negative layout values, or opacity
12/// outside `0..=1`. Validate
13/// external input before building a target. Presentation opacity is clamped;
14/// internal spring values and velocity remain unclamped for continuous retargeting.
15#[derive(Clone, Copy, Debug, PartialEq)]
16pub struct Target {
17    layout: u8,
18    values: [f64; CHANNELS],
19}
20
21impl Default for Target {
22    fn default() -> Self {
23        Self {
24            layout: 0,
25            values: [0.0, 0.0, 1.0, 0.0, 0.0, 0.0],
26        }
27    }
28}
29
30impl Target {
31    /// Sets the horizontal logical-pixel offset. Panics for non-finite values.
32    pub fn x(mut self, value: f32) -> Self {
33        assert!(value.is_finite(), "x must be finite");
34        self.values[0] = value.into();
35        self.layout |= 1;
36
37        self
38    }
39
40    /// Sets the vertical logical-pixel offset. Panics for non-finite values.
41    pub fn y(mut self, value: f32) -> Self {
42        assert!(value.is_finite(), "y must be finite");
43        self.values[1] = value.into();
44        self.layout |= 2;
45
46        self
47    }
48
49    /// Sets opacity. Panics unless finite and in `0..=1`.
50    pub fn opacity(mut self, value: f32) -> Self {
51        assert!((0.0..=1.0).contains(&value), "opacity must be in 0..=1");
52        self.values[2] = value.into();
53        self.layout |= 4;
54
55        self
56    }
57
58    /// Sets width in logical pixels. Panics unless finite and non-negative.
59    /// Omitted layout properties inherit their previous destination; on first
60    /// mount they leave the element's own style untouched.
61    pub fn width(self, value: f32) -> Self {
62        self.with_layout(3, value)
63    }
64
65    /// Sets height in logical pixels. Panics unless finite and non-negative.
66    pub fn height(self, value: f32) -> Self {
67        self.with_layout(4, value)
68    }
69
70    /// Sets all four corner radii in logical pixels.
71    /// Panics unless finite and non-negative. This does not enable clipping.
72    pub fn border_radius(self, value: f32) -> Self {
73        self.with_layout(5, value)
74    }
75
76    /// Width, if declared, clamped to non-negative finite drawable values.
77    pub fn width_px(self) -> Option<f32> {
78        self.layout_value(3)
79    }
80
81    /// Height, if declared, clamped to non-negative finite drawable values.
82    pub fn height_px(self) -> Option<f32> {
83        self.layout_value(4)
84    }
85
86    /// Corner radius, if declared, clamped to non-negative finite drawable values.
87    pub fn border_radius_px(self) -> Option<f32> {
88        self.layout_value(5)
89    }
90
91    fn with_layout(mut self, ix: usize, value: f32) -> Self {
92        assert!(
93            value.is_finite() && value >= 0.0,
94            "layout targets must be finite and non-negative"
95        );
96        self.values[ix] = value.into();
97        self.layout |= 1 << ix;
98
99        self
100    }
101
102    fn layout_value(self, ix: usize) -> Option<f32> {
103        (self.layout & (1 << ix) != 0)
104            .then(|| self.values[ix].clamp(0.0, f64::from(f32::MAX)) as f32)
105    }
106
107    fn with_layout_fallback(mut self, fallback: Self) -> Self {
108        for ix in 3..CHANNELS {
109            let bit = 1 << ix;
110
111            if self.layout & bit == 0 && fallback.layout & bit != 0 {
112                self.values[ix] = fallback.values[ix];
113                self.layout |= bit;
114            }
115        }
116
117        self
118    }
119
120    /// Overlays only explicitly set properties, for variants and interaction targets.
121    /// Default x/y/opacity values in an otherwise empty patch do not override the base.
122    pub fn overlay(mut self, patch: Self) -> Self {
123        for ix in 0..CHANNELS {
124            if patch.layout & (1 << ix) != 0 {
125                self.values[ix] = patch.values[ix];
126            }
127        }
128
129        self.layout |= patch.layout;
130
131        self
132    }
133
134    pub(crate) fn values(self) -> [f64; CHANNELS] {
135        self.values
136    }
137
138    pub(crate) fn from_values(values: [f64; CHANNELS], layout: u8) -> Self {
139        Self { values, layout }
140    }
141
142    pub(crate) fn mask(self) -> u8 {
143        self.layout
144    }
145
146    pub(crate) fn inherit(self, base: Self) -> Self {
147        self.with_layout_fallback(base)
148    }
149
150    /// Horizontal offset in logical pixels, saturated to finite `f32` limits.
151    pub fn x_px(self) -> f32 {
152        self.values[0].clamp(f64::from(f32::MIN), f64::from(f32::MAX)) as f32
153    }
154
155    /// Vertical offset in logical pixels, saturated to finite `f32` limits.
156    pub fn y_px(self) -> f32 {
157        self.values[1].clamp(f64::from(f32::MIN), f64::from(f32::MAX)) as f32
158    }
159
160    /// Opacity, clamped to the drawable range.
161    pub fn alpha(self) -> f32 {
162        self.values[2].clamp(0.0, 1.0) as f32
163    }
164}
165
166/// A disposable declaration of desired state. Keep its [`MotionState`] alive.
167#[derive(Clone, Copy, Debug, PartialEq)]
168pub struct Motion {
169    target: Target,
170    initial: Option<Target>,
171    exit: Target,
172    transition: Transition,
173    present: bool,
174    delay: Duration,
175    overrides: [Option<(Transition, Duration)>; CHANNELS],
176}
177
178impl Motion {
179    /// Creates a declaration. Without `initial`, the first mount is immediate.
180    /// Default exit fades out at the original position using the same transition.
181    pub fn new(target: Target) -> Self {
182        Self {
183            target,
184            initial: None,
185            exit: target.opacity(0.0),
186            transition: Transition::default(),
187            present: true,
188            delay: Duration::ZERO,
189            overrides: [None; CHANNELS],
190        }
191    }
192
193    /// Reads the declared destination, before interaction overrides.
194    pub fn target(self) -> Target {
195        self.target
196    }
197
198    /// Replaces the destination while retaining transition, entrance, and exit policy.
199    pub fn with_target(mut self, target: Target) -> Self {
200        self.target = target;
201
202        self
203    }
204
205    /// Sets the first mount's presentation, used only once per retained state.
206    pub fn initial(mut self, value: Target) -> Self {
207        self.initial = Some(value);
208
209        self
210    }
211
212    /// Sets the exit target; re-entry starts from the current exit presentation.
213    pub fn exit(mut self, value: Target) -> Self {
214        self.exit = value;
215
216        self
217    }
218
219    /// Sets the transition. A change interrupts the current transition.
220    pub fn transition(mut self, value: Transition) -> Self {
221        self.transition = value;
222
223        self
224    }
225
226    /// Delays each new target. An exit uses the same delay unless overridden.
227    pub fn delay(mut self, delay: Duration) -> Self {
228        self.delay = delay;
229
230        self
231    }
232
233    /// Sets a property's independent transition and delay.
234    pub fn transition_for(
235        mut self,
236        property: Property,
237        transition: Transition,
238        delay: Duration,
239    ) -> Self {
240        self.overrides[property as usize] = Some((transition, delay));
241
242        self
243    }
244
245    /// Requests mount or removal. Continue sampling while false to finish exit.
246    pub fn present(mut self, value: bool) -> Self {
247        self.present = value;
248
249        self
250    }
251}
252
253/// A typed property for per-property transition configuration.
254#[derive(Clone, Copy, Debug, PartialEq, Eq)]
255#[non_exhaustive]
256pub enum Property {
257    /// Horizontal logical-pixel offset.
258    X,
259    /// Vertical logical-pixel offset.
260    Y,
261    /// Opacity.
262    Opacity,
263    /// Width in logical pixels.
264    Width,
265    /// Height in logical pixels.
266    Height,
267    /// Corner radius in logical pixels.
268    BorderRadius,
269}
270
271/// Lifecycle of a retained element. Exit completes only after all channels settle.
272#[derive(Clone, Copy, Debug, PartialEq, Eq)]
273#[non_exhaustive]
274pub enum Presence {
275    /// First mount or re-entry is moving toward the visible target.
276    Entering,
277    /// Mounted; may still be transitioning between visible targets.
278    Present,
279    /// Removal requested; the element must remain rendered until settled.
280    Exiting,
281    /// Exit completed; omit the element from layout, painting, and input.
282    Absent,
283}
284
285/// A read-only presentation snapshot. It owns no tasks or GPUI resources.
286#[derive(Clone, Copy, Debug)]
287pub struct Frame {
288    target: Target,
289    velocity: [f64; CHANNELS],
290    presence: Presence,
291    animating: bool,
292    completed: bool,
293}
294
295impl Frame {
296    /// Creates a static presentation, useful for scroll, drag, and custom value bindings.
297    pub fn at(target: Target) -> Self {
298        Self {
299            target,
300            velocity: [0.0; CHANNELS],
301            presence: Presence::Present,
302            animating: false,
303            completed: false,
304        }
305    }
306
307    pub(crate) fn animated(
308        target: Target,
309        velocity: [f64; CHANNELS],
310        active: bool,
311        completed: bool,
312    ) -> Self {
313        Self {
314            target,
315            velocity,
316            presence: Presence::Present,
317            animating: active,
318            completed,
319        }
320    }
321
322    /// Completion edge. Read once per view render to dispatch an owner-level event.
323    pub fn just_completed(self) -> bool {
324        self.completed
325    }
326
327    /// Current presentation values, rather than the declaration's destination.
328    pub fn target(self) -> Target {
329        self.target
330    }
331
332    /// Current x, y, and opacity velocities per second, before drawing clamps.
333    pub fn velocity(self) -> [f64; 3] {
334        [self.velocity[0], self.velocity[1], self.velocity[2]]
335    }
336
337    /// Width, height, and radius velocities in logical pixels per second.
338    /// Undeclared channels return `None`; velocities are before drawing clamps.
339    pub fn layout_velocity(self) -> [Option<f64>; 3] {
340        std::array::from_fn(|ix| {
341            (self.target.layout & (1 << (ix + 3)) != 0).then_some(self.velocity[ix + 3])
342        })
343    }
344
345    /// Current lifecycle phase.
346    pub fn presence(self) -> Presence {
347        self.presence
348    }
349
350    /// Whether another animation frame is needed.
351    pub fn is_animating(self) -> bool {
352        self.animating
353    }
354
355    /// Whether the element must remain in the rendered tree, including its exit.
356    pub fn is_mounted(self) -> bool {
357        self.presence != Presence::Absent
358    }
359
360    /// Whether the model requests presence. Disable exit-time input using this.
361    pub fn is_present(self) -> bool {
362        matches!(self.presence, Presence::Entering | Presence::Present)
363    }
364}
365
366/// Retained motion for one logical object, owned by a view or keyed collection.
367///
368/// Dropping this value cancels motion. Retain it through exit and reuse it for
369/// re-entry; replacing it with `Default` explicitly resets mount history. Sampling
370/// is O(1), uses no heap, and does not advance settled trajectories.
371#[derive(Debug)]
372pub struct MotionState {
373    layout: u8,
374    channels: [MotionValue; CHANNELS],
375    presence: Presence,
376    initialized: bool,
377    #[cfg(feature = "gpui")]
378    pub(crate) epoch: Instant,
379}
380
381impl Default for MotionState {
382    fn default() -> Self {
383        Self {
384            layout: 0,
385            channels: std::array::from_fn(|ix| MotionValue::new(if ix == 2 { 1.0 } else { 0.0 })),
386            presence: Presence::Absent,
387            initialized: false,
388            #[cfg(feature = "gpui")]
389            epoch: Instant::now(),
390        }
391    }
392}
393
394impl MotionState {
395    /// Samples absolute monotonic time. Identical targets do not restart. Each
396    /// property retargets independently, preserving spring velocity. Omitted layout
397    /// targets retain their previous destination. Reduced motion snaps and exits.
398    /// Do not mix this clock with the native `frame` clock on the same state.
399    pub fn sample(&mut self, motion: Motion, now: Duration, reduced_motion: bool) -> Frame {
400        if !self.initialized {
401            self.initialized = true;
402            let target = motion
403                .target
404                .with_layout_fallback(motion.initial.unwrap_or_default());
405
406            let initial = if motion.present {
407                motion
408                    .initial
409                    .unwrap_or(target)
410                    .with_layout_fallback(target)
411            } else {
412                motion.exit.with_layout_fallback(target)
413            };
414
415            self.layout = initial.layout;
416
417            for ix in 0..CHANNELS {
418                self.channels[ix].jump(initial.values[ix], now);
419            }
420
421            self.presence = if motion.present {
422                Presence::Entering
423            } else {
424                Presence::Absent
425            };
426        }
427
428        let previous_presence = self.presence;
429        let was_present = matches!(self.presence, Presence::Present | Presence::Entering);
430
431        let requested = if motion.present {
432            motion.target
433        } else {
434            motion.exit
435        }
436        .with_layout_fallback(Target {
437            values: std::array::from_fn(|ix| self.channels[ix].target()),
438            layout: self.layout,
439        });
440
441        let added = requested.layout & !self.layout;
442
443        for ix in 3..CHANNELS {
444            if added & (1 << ix) != 0 {
445                self.channels[ix].jump(requested.values[ix], now);
446            }
447        }
448
449        self.layout = requested.layout;
450
451        if was_present != motion.present {
452            self.presence = if motion.present {
453                Presence::Entering
454            } else {
455                Presence::Exiting
456            };
457        }
458
459        let mut active = false;
460        let mut completed = false;
461        let mut position = [0.0; CHANNELS];
462        let mut velocity = [0.0; CHANNELS];
463
464        for ix in 0..CHANNELS {
465            let (transition, delay) =
466                motion.overrides[ix].unwrap_or((motion.transition, motion.delay));
467
468            self.channels[ix].animate_delayed(requested.values[ix], transition, delay, now);
469            let value = self.channels[ix].sample(now, reduced_motion);
470
471            position[ix] = value.value();
472            velocity[ix] = value.velocity();
473            active |= value.is_animating();
474            completed |= value.just_completed();
475        }
476
477        if !active {
478            self.presence = if motion.present {
479                Presence::Present
480            } else {
481                Presence::Absent
482            };
483        }
484
485        completed &= !active;
486        completed |= self.presence == Presence::Absent && previous_presence != Presence::Absent;
487
488        Frame {
489            target: Target {
490                values: position,
491                layout: self.layout | 7,
492            },
493            velocity,
494            presence: self.presence,
495            animating: active,
496            completed,
497        }
498    }
499}