Skip to main content

gpui_ce_motion/
svg.rs

1//! Retained scale/rotation values for the stock GPUI CE SVG drawing path.
2use crate::{MotionValue, Transition};
3use std::time::Duration;
4#[cfg(feature = "gpui")]
5use std::time::Instant;
6
7/// SVG-only scale, rotation and translation destination. Defaults to unit scale and zero rotation.
8/// Negative scale mirrors an axis; zero collapses it. Rotation is unwrapped degrees:
9/// `0 -> 720` makes two turns; use `350 -> 370` to cross zero by the short route.
10#[derive(Clone, Copy, Debug, PartialEq)]
11pub struct SvgTarget {
12    values: [f64; 5],
13}
14
15impl Default for SvgTarget {
16    fn default() -> Self {
17        Self {
18            values: [1.0, 1.0, 0.0, 0.0, 0.0],
19        }
20    }
21}
22
23impl SvgTarget {
24    /// Sets equal scale on both axes. Panics for non-finite input.
25    pub fn scale(self, factor: f32) -> Self {
26        self.scale_xy(factor, factor)
27    }
28
29    /// Sets independent signed scale factors. Panics for non-finite input.
30    pub fn scale_xy(mut self, x: f32, y: f32) -> Self {
31        assert!(x.is_finite() && y.is_finite(), "SVG scale must be finite");
32        self.values[0] = f64::from(x);
33        self.values[1] = f64::from(y);
34
35        self
36    }
37
38    /// Sets an absolute unwrapped angle in degrees. Panics for non-finite input.
39    pub fn rotate_degrees(mut self, degrees: f64) -> Self {
40        assert!(degrees.is_finite(), "SVG rotation must be finite");
41        self.values[2] = degrees;
42
43        self
44    }
45
46    /// Sets a drawing-only logical-pixel translation. Layout and hitboxes stay fixed.
47    pub fn translate(mut self, x: f32, y: f32) -> Self {
48        assert!(
49            x.is_finite() && y.is_finite(),
50            "SVG translation must be finite"
51        );
52        self.values[3] = x.into();
53        self.values[4] = y.into();
54
55        self
56    }
57
58    /// Presented drawing translation in logical pixels.
59    pub fn translation(self) -> [f32; 2] {
60        [self.values[3], self.values[4]]
61            .map(|v| v.clamp(f64::from(f32::MIN), f64::from(f32::MAX)) as f32)
62    }
63
64    /// Drawable signed scale factors, saturated to finite `f32` limits.
65    pub fn scale_factors(self) -> [f32; 2] {
66        [self.values[0], self.values[1]]
67            .map(|v| v.clamp(f64::from(f32::MIN), f64::from(f32::MAX)) as f32)
68    }
69
70    /// Unwrapped rotation in degrees, including whole turns.
71    pub fn rotation_degrees(self) -> f64 {
72        self.values[2]
73    }
74}
75
76/// Disposable SVG transform declaration; retain one [`SvgMotionState`] per logical SVG.
77/// This does not transform a GPUI subtree or add native children to an SVG.
78#[derive(Clone, Copy, Debug, PartialEq)]
79pub struct SvgMotion {
80    target: SvgTarget,
81    initial: Option<SvgTarget>,
82    transition: Transition,
83    delay: Duration,
84}
85
86impl SvgMotion {
87    /// Declares a destination. First mount is immediate unless `initial` is supplied.
88    pub fn new(target: SvgTarget) -> Self {
89        Self {
90            target,
91            initial: None,
92            transition: Transition::default(),
93            delay: Duration::ZERO,
94        }
95    }
96
97    /// Sets the first presentation, used only once by the retained state.
98    pub fn initial(mut self, initial: SvgTarget) -> Self {
99        self.initial = Some(initial);
100
101        self
102    }
103
104    /// Chooses spring, tween, or instant motion for the five transform channels.
105    pub fn transition(mut self, transition: Transition) -> Self {
106        self.transition = transition;
107
108        self
109    }
110
111    /// Holds the presentation before each new destination. Identical declarations do not restart.
112    pub fn delay(mut self, delay: Duration) -> Self {
113        self.delay = delay;
114
115        self
116    }
117}
118
119/// A sampled SVG drawing transform. No layout, clipping, or hitbox changes are implied.
120#[derive(Clone, Copy, Debug)]
121pub struct SvgFrame {
122    target: SvgTarget,
123    velocity: [f64; 3],
124    translation_velocity: [f64; 2],
125    active: bool,
126    completed: bool,
127}
128
129impl SvgFrame {
130    /// Presented signed scales, unwrapped rotation and drawing translation.
131    pub fn target(self) -> SvgTarget {
132        self.target
133    }
134
135    /// X scale/second, Y scale/second, and degrees/second.
136    pub fn velocity(self) -> [f64; 3] {
137        self.velocity
138    }
139
140    /// Drawing translation velocity, logical pixels/second.
141    pub fn translation_velocity(self) -> [f64; 2] {
142        self.translation_velocity
143    }
144
145    /// Whether another frame is needed. False after settling or a reduced-motion snap.
146    pub fn is_animating(self) -> bool {
147        self.active
148    }
149
150    /// Completion edge when the final active channel finishes; consumed by this sample.
151    pub fn just_completed(self) -> bool {
152        self.completed
153    }
154}
155
156/// Allocation-free SVG transform state using the same analytic scalar engine as native motion.
157/// Springs preserve position and velocity at interruption; tweens preserve position.
158#[derive(Debug)]
159pub struct SvgMotionState {
160    channels: [MotionValue; 5],
161    initialized: bool,
162    #[cfg(feature = "gpui")]
163    pub(crate) epoch: Instant,
164}
165
166impl Default for SvgMotionState {
167    fn default() -> Self {
168        Self {
169            channels: SvgTarget::default().values.map(MotionValue::new),
170            initialized: false,
171            #[cfg(feature = "gpui")]
172            epoch: Instant::now(),
173        }
174    }
175}
176
177impl SvgMotionState {
178    /// Samples a monotonic application timestamp. Backward time holds; identical targets
179    /// do not restart. Reduced motion snaps to the destination and completes once.
180    /// Do not mix this epoch with the optional native `frame` clock on the same state.
181    ///
182    /// ```
183    /// use gpui_ce_motion::{SvgMotion, SvgMotionState, SvgTarget};
184    /// use std::time::Duration;
185    /// let mut state = SvgMotionState::default();
186    /// let motion = SvgMotion::new(SvgTarget::default().scale(1.5).rotate_degrees(90.0))
187    ///     .initial(SvgTarget::default());
188    /// assert!(state.sample(motion, Duration::ZERO, false).is_animating());
189    /// let settled = state.sample(motion, Duration::from_secs(5), false);
190    /// assert_eq!(settled.target().scale_factors(), [1.5, 1.5]);
191    /// assert_eq!(settled.target().rotation_degrees(), 90.0);
192    /// assert!(!settled.is_animating());
193    /// ```
194    pub fn sample(&mut self, motion: SvgMotion, now: Duration, reduced_motion: bool) -> SvgFrame {
195        if !self.initialized {
196            let initial = motion.initial.unwrap_or(motion.target);
197
198            for (channel, value) in self.channels.iter_mut().zip(initial.values) {
199                channel.jump(value, now);
200            }
201
202            self.initialized = true;
203        }
204
205        let mut active = false;
206        let mut completed = false;
207        let mut velocity = [0.0; 5];
208
209        let values = std::array::from_fn(|ix| {
210            self.channels[ix].animate_delayed(
211                motion.target.values[ix],
212                motion.transition,
213                motion.delay,
214                now,
215            );
216            let frame = self.channels[ix].sample(now, reduced_motion);
217
218            active |= frame.is_animating();
219            completed |= frame.just_completed();
220            velocity[ix] = frame.velocity();
221
222            frame.value()
223        });
224
225        SvgFrame {
226            target: SvgTarget { values },
227            velocity: [velocity[0], velocity[1], velocity[2]],
228            translation_velocity: [velocity[3], velocity[4]],
229            active,
230            completed: completed && !active,
231        }
232    }
233}
234
235impl crate::MotionData<5> for SvgTarget {
236    fn channels(self) -> [f64; 5] {
237        self.values
238    }
239
240    fn with_channels(self, values: [f64; 5]) -> Self {
241        Self { values }
242    }
243
244    fn compatible(self, _: Self) -> bool {
245        true
246    }
247}