Skip to main content

gpui_base/motion/
easing.rs

1use std::{fmt, num::NonZeroU32, rc::Rc, sync::Arc};
2
3use crate::animation::cubic_bezier;
4
5/// The point at which a stepped easing jumps.
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub enum StepPosition {
8    JumpStart,
9    JumpEnd,
10    JumpNone,
11    JumpBoth,
12}
13
14/// One output and its optional input position in a CSS-like `linear()` curve.
15#[derive(Clone, Copy, Debug, PartialEq)]
16pub struct LinearStop {
17    pub output: f32,
18    pub input: Option<f32>,
19}
20
21impl LinearStop {
22    pub const fn new(output: f32) -> Self {
23        Self {
24            output,
25            input: None,
26        }
27    }
28
29    pub const fn at(output: f32, input: f32) -> Self {
30        Self {
31            output,
32            input: Some(input),
33        }
34    }
35}
36
37/// Invalid easing configuration.
38#[derive(Clone, Copy, Debug, Eq, PartialEq)]
39pub enum EasingError {
40    /// Retained for source compatibility. New validation reports
41    /// [`Self::InvalidBezierControlPoint`].
42    #[deprecated(note = "use InvalidBezierControlPoint")]
43    InvalidBezierX,
44    InvalidBezierControlPoint,
45    InvalidStepCount,
46    InvalidLinearStops,
47}
48
49impl fmt::Display for EasingError {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        #[allow(deprecated)]
52        match self {
53            Self::InvalidBezierX | Self::InvalidBezierControlPoint => {
54                f.write_str("cubic Bézier control points must be finite and x must be within 0..=1")
55            }
56            Self::InvalidStepCount => f.write_str("step easing requires a valid step count"),
57            Self::InvalidLinearStops => f.write_str("linear easing stops are invalid"),
58        }
59    }
60}
61
62impl std::error::Error for EasingError {}
63
64/// A cheap, cloneable CSS-compatible easing policy.
65#[derive(Clone, Default)]
66pub enum Easing {
67    Linear,
68    Ease,
69    EaseIn,
70    #[default]
71    EaseOut,
72    EaseInOut,
73    CubicBezier {
74        x1: f32,
75        y1: f32,
76        x2: f32,
77        y2: f32,
78    },
79    Steps {
80        count: NonZeroU32,
81        position: StepPosition,
82    },
83    LinearStops(Arc<[(f32, f32)]>),
84    Custom(Rc<dyn Fn(f32) -> f32>),
85}
86
87impl fmt::Debug for Easing {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        match self {
90            Self::Linear => f.write_str("Linear"),
91            Self::Ease => f.write_str("Ease"),
92            Self::EaseIn => f.write_str("EaseIn"),
93            Self::EaseOut => f.write_str("EaseOut"),
94            Self::EaseInOut => f.write_str("EaseInOut"),
95            Self::CubicBezier { x1, y1, x2, y2 } => f
96                .debug_struct("CubicBezier")
97                .field("x1", x1)
98                .field("y1", y1)
99                .field("x2", x2)
100                .field("y2", y2)
101                .finish(),
102            Self::Steps { count, position } => f
103                .debug_struct("Steps")
104                .field("count", count)
105                .field("position", position)
106                .finish(),
107            Self::LinearStops(stops) => f.debug_tuple("LinearStops").field(stops).finish(),
108            Self::Custom(_) => f.write_str("Custom(..)"),
109        }
110    }
111}
112
113impl Easing {
114    pub fn cubic_bezier(x1: f32, y1: f32, x2: f32, y2: f32) -> Result<Self, EasingError> {
115        if !x1.is_finite()
116            || !x2.is_finite()
117            || !(0.0..=1.0).contains(&x1)
118            || !(0.0..=1.0).contains(&x2)
119            || !y1.is_finite()
120            || !y2.is_finite()
121        {
122            return Err(EasingError::InvalidBezierControlPoint);
123        }
124        Ok(Self::CubicBezier { x1, y1, x2, y2 })
125    }
126
127    pub fn steps(count: u32, position: StepPosition) -> Result<Self, EasingError> {
128        let count = NonZeroU32::new(count).ok_or(EasingError::InvalidStepCount)?;
129        if position == StepPosition::JumpNone && count.get() == 1 {
130            return Err(EasingError::InvalidStepCount);
131        }
132        Ok(Self::Steps { count, position })
133    }
134
135    pub fn linear_stops(stops: impl IntoIterator<Item = LinearStop>) -> Result<Self, EasingError> {
136        let mut stops: Vec<_> = stops.into_iter().collect();
137        if stops.len() < 2
138            || stops
139                .iter()
140                .any(|stop| !stop.output.is_finite() || stop.input.is_some_and(|v| !v.is_finite()))
141        {
142            return Err(EasingError::InvalidLinearStops);
143        }
144
145        if stops[0].input.is_none() {
146            stops[0].input = Some(0.0);
147        }
148        let last = stops.len() - 1;
149        if stops[last].input.is_none() {
150            stops[last].input = Some(1.0);
151        }
152
153        let mut anchor = 0;
154        while anchor < last {
155            let Some(next) = ((anchor + 1)..=last).find(|&ix| stops[ix].input.is_some()) else {
156                return Err(EasingError::InvalidLinearStops);
157            };
158            let from = stops[anchor].input.unwrap();
159            let to = stops[next].input.unwrap();
160            if !(0.0..=1.0).contains(&from) || !(0.0..=1.0).contains(&to) || to < from {
161                return Err(EasingError::InvalidLinearStops);
162            }
163            let span = (next - anchor) as f32;
164            for (offset, stop) in stops[(anchor + 1)..next].iter_mut().enumerate() {
165                stop.input = Some(from + (to - from) * (offset + 1) as f32 / span);
166            }
167            anchor = next;
168        }
169
170        Ok(Self::LinearStops(
171            stops
172                .into_iter()
173                .map(|stop| (stop.input.unwrap(), stop.output))
174                .collect(),
175        ))
176    }
177
178    #[inline]
179    pub fn sample(&self, progress: f32) -> f32 {
180        let progress = progress.clamp(0.0, 1.0);
181        match self {
182            Self::Linear => progress,
183            Self::Ease => cubic_bezier(0.25, 0.1, 0.25, 1.0)(progress),
184            Self::EaseIn => cubic_bezier(0.42, 0.0, 1.0, 1.0)(progress),
185            Self::EaseOut => cubic_bezier(0.0, 0.0, 0.58, 1.0)(progress),
186            Self::EaseInOut => cubic_bezier(0.42, 0.0, 0.58, 1.0)(progress),
187            Self::CubicBezier { x1, y1, x2, y2 } => cubic_bezier(*x1, *y1, *x2, *y2)(progress),
188            Self::Steps { count, position } => {
189                let count = count.get() as f32;
190                let (jumps, offset) = match position {
191                    StepPosition::JumpStart => (count, 1.0),
192                    StepPosition::JumpEnd => (count, 0.0),
193                    StepPosition::JumpNone => (count - 1.0, 0.0),
194                    StepPosition::JumpBoth => (count + 1.0, 1.0),
195                };
196                ((progress * count).floor() + offset).clamp(0.0, jumps) / jumps
197            }
198            Self::LinearStops(stops) => {
199                let upper = stops.partition_point(|(input, _)| *input <= progress);
200                if upper == 0 {
201                    return stops[0].1;
202                }
203                if upper == stops.len() {
204                    return stops[stops.len() - 1].1;
205                }
206                let (x0, y0) = stops[upper - 1];
207                let (x1, y1) = stops[upper];
208                if x0 == x1 {
209                    y1
210                } else {
211                    y0 + (y1 - y0) * ((progress - x0) / (x1 - x0))
212                }
213            }
214            Self::Custom(easing) => easing(progress),
215        }
216    }
217}
218
219#[cfg(test)]
220mod error_tests {
221    use super::*;
222
223    #[test]
224    fn bezier_errors_name_the_invalid_control_point() {
225        let error = Easing::cubic_bezier(0.2, f32::NAN, 0.8, 1.0).unwrap_err();
226
227        assert_eq!(error, EasingError::InvalidBezierControlPoint);
228        assert_eq!(
229            error.to_string(),
230            "cubic Bézier control points must be finite and x must be within 0..=1"
231        );
232        let _: &dyn std::error::Error = &error;
233    }
234}