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    /// Whether this declaration requests the element to remain present.
246    pub fn is_present(self) -> bool {
247        self.present
248    }
249
250    /// Requests mount or removal. Continue sampling while false to finish exit.
251    pub fn present(mut self, value: bool) -> Self {
252        self.present = value;
253
254        self
255    }
256}
257
258/// A typed property for per-property transition configuration.
259#[derive(Clone, Copy, Debug, PartialEq, Eq)]
260#[non_exhaustive]
261pub enum Property {
262    /// Horizontal logical-pixel offset.
263    X,
264    /// Vertical logical-pixel offset.
265    Y,
266    /// Opacity.
267    Opacity,
268    /// Width in logical pixels.
269    Width,
270    /// Height in logical pixels.
271    Height,
272    /// Corner radius in logical pixels.
273    BorderRadius,
274}
275
276/// Lifecycle of a retained element. Exit completes only after all channels settle.
277#[derive(Clone, Copy, Debug, PartialEq, Eq)]
278#[non_exhaustive]
279pub enum Presence {
280    /// First mount or re-entry is moving toward the visible target.
281    Entering,
282    /// Mounted; may still be transitioning between visible targets.
283    Present,
284    /// Removal requested; the element must remain rendered until settled.
285    Exiting,
286    /// Exit completed; omit the element from layout, painting, and input.
287    Absent,
288}
289
290/// A read-only presentation snapshot. It owns no tasks or GPUI resources.
291#[derive(Clone, Copy, Debug)]
292pub struct Frame {
293    target: Target,
294    velocity: [f64; CHANNELS],
295    presence: Presence,
296    animating: bool,
297    completed: bool,
298}
299
300impl Frame {
301    /// Creates a static presentation, useful for scroll, drag, and custom value bindings.
302    pub fn at(target: Target) -> Self {
303        Self {
304            target,
305            velocity: [0.0; CHANNELS],
306            presence: Presence::Present,
307            animating: false,
308            completed: false,
309        }
310    }
311
312    pub(crate) fn animated(
313        target: Target,
314        velocity: [f64; CHANNELS],
315        active: bool,
316        completed: bool,
317    ) -> Self {
318        Self {
319            target,
320            velocity,
321            presence: Presence::Present,
322            animating: active,
323            completed,
324        }
325    }
326
327    /// Completion edge. Read once per view render to dispatch an owner-level event.
328    pub fn just_completed(self) -> bool {
329        self.completed
330    }
331
332    /// Current presentation values, rather than the declaration's destination.
333    pub fn target(self) -> Target {
334        self.target
335    }
336
337    /// Current x, y, and opacity velocities per second, before drawing clamps.
338    pub fn velocity(self) -> [f64; 3] {
339        [self.velocity[0], self.velocity[1], self.velocity[2]]
340    }
341
342    /// Width, height, and radius velocities in logical pixels per second.
343    /// Undeclared channels return `None`; velocities are before drawing clamps.
344    pub fn layout_velocity(self) -> [Option<f64>; 3] {
345        std::array::from_fn(|ix| {
346            (self.target.layout & (1 << (ix + 3)) != 0).then_some(self.velocity[ix + 3])
347        })
348    }
349
350    /// Current lifecycle phase.
351    pub fn presence(self) -> Presence {
352        self.presence
353    }
354
355    /// Whether another animation frame is needed.
356    pub fn is_animating(self) -> bool {
357        self.animating
358    }
359
360    /// Whether the element must remain in the rendered tree, including its exit.
361    pub fn is_mounted(self) -> bool {
362        self.presence != Presence::Absent
363    }
364
365    /// Whether the model requests presence. Disable exit-time input using this.
366    pub fn is_present(self) -> bool {
367        matches!(self.presence, Presence::Entering | Presence::Present)
368    }
369}
370
371/// Retained motion for one logical object, owned by a view or keyed collection.
372///
373/// Dropping this value cancels motion. Retain it through exit and reuse it for
374/// re-entry; replacing it with `Default` explicitly resets mount history. Sampling
375/// is O(1), uses no heap, and does not advance settled trajectories.
376#[derive(Debug)]
377pub struct MotionState {
378    layout: u8,
379    channels: [MotionValue; CHANNELS],
380    presence: Presence,
381    initialized: bool,
382    #[cfg(feature = "gpui")]
383    pub(crate) epoch: Instant,
384}
385
386impl Default for MotionState {
387    fn default() -> Self {
388        Self {
389            layout: 0,
390            channels: std::array::from_fn(|ix| MotionValue::new(if ix == 2 { 1.0 } else { 0.0 })),
391            presence: Presence::Absent,
392            initialized: false,
393            #[cfg(feature = "gpui")]
394            epoch: Instant::now(),
395        }
396    }
397}
398
399impl MotionState {
400    /// Samples absolute monotonic time. Identical targets do not restart. Each
401    /// property retargets independently, preserving spring velocity. Omitted layout
402    /// targets retain their previous destination. Reduced motion snaps and exits.
403    /// Do not mix this clock with the native `frame` clock on the same state.
404    pub fn sample(&mut self, motion: Motion, now: Duration, reduced_motion: bool) -> Frame {
405        if !self.initialized {
406            self.initialized = true;
407            let target = motion
408                .target
409                .with_layout_fallback(motion.initial.unwrap_or_default());
410
411            let initial = if motion.present {
412                motion
413                    .initial
414                    .unwrap_or(target)
415                    .with_layout_fallback(target)
416            } else {
417                motion.exit.with_layout_fallback(target)
418            };
419
420            self.layout = initial.layout;
421
422            for ix in 0..CHANNELS {
423                self.channels[ix].jump(initial.values[ix], now);
424            }
425
426            self.presence = if motion.present {
427                Presence::Entering
428            } else {
429                Presence::Absent
430            };
431        }
432
433        let previous_presence = self.presence;
434        let was_present = matches!(self.presence, Presence::Present | Presence::Entering);
435
436        let requested = if motion.present {
437            motion.target
438        } else {
439            motion.exit
440        }
441        .with_layout_fallback(Target {
442            values: std::array::from_fn(|ix| self.channels[ix].target()),
443            layout: self.layout,
444        });
445
446        let added = requested.layout & !self.layout;
447
448        for ix in 3..CHANNELS {
449            if added & (1 << ix) != 0 {
450                self.channels[ix].jump(requested.values[ix], now);
451            }
452        }
453
454        self.layout = requested.layout;
455
456        if was_present != motion.present {
457            self.presence = if motion.present {
458                Presence::Entering
459            } else {
460                Presence::Exiting
461            };
462        }
463
464        let mut active = false;
465        let mut completed = false;
466        let mut position = [0.0; CHANNELS];
467        let mut velocity = [0.0; CHANNELS];
468
469        for ix in 0..CHANNELS {
470            let (transition, delay) =
471                motion.overrides[ix].unwrap_or((motion.transition, motion.delay));
472
473            self.channels[ix].animate_delayed(requested.values[ix], transition, delay, now);
474            let value = self.channels[ix].sample(now, reduced_motion);
475
476            position[ix] = value.value();
477            velocity[ix] = value.velocity();
478            active |= value.is_animating();
479            completed |= value.just_completed();
480        }
481
482        if !active {
483            self.presence = if motion.present {
484                Presence::Present
485            } else {
486                Presence::Absent
487            };
488        }
489
490        completed &= !active;
491        completed |= self.presence == Presence::Absent && previous_presence != Presence::Absent;
492
493        Frame {
494            target: Target {
495                values: position,
496                layout: self.layout | 7,
497            },
498            velocity,
499            presence: self.presence,
500            animating: active,
501            completed,
502        }
503    }
504}