Skip to main content

guise/anim/
frame.rs

1//! One sampled instant of an animation: the set of properties that have a
2//! value right now, plus how far through the clip that instant is.
3//!
4//! A frame is built fresh every render — sampling is pure, so nothing has to
5//! be kept between frames and a paused animation costs exactly nothing.
6
7use gpui::prelude::*;
8use gpui::{px, Hsla};
9
10use super::{AnimValue, Prop};
11
12/// How many properties a frame holds without touching the heap.
13///
14/// A motion is two or three tracks in almost every case — an entrance is
15/// opacity plus one offset. Sampling runs once per animated element per
16/// frame, so the common case should not allocate at all; a sequence layering
17/// more than this spills to a `Vec` and nothing else changes.
18pub(crate) const INLINE: usize = 4;
19
20/// The filler the unused inline slots hold. Never read: `set` and `iter`
21/// bound themselves by `inline_len`.
22const EMPTY: (Prop, AnimValue) = (Prop::Opacity, AnimValue::Number(0.0));
23
24/// The values an animation resolves to at one moment in time.
25#[derive(Debug, Clone)]
26pub struct Frame {
27  inline: [(Prop, AnimValue); INLINE],
28  inline_len: usize,
29  spill: Vec<(Prop, AnimValue)>,
30  /// 0..=1 through the whole clip, loops included.
31  pub progress: f32,
32  /// True once the clip has run out of time. Always false while looping
33  /// forever.
34  pub finished: bool,
35}
36
37impl Default for Frame {
38  fn default() -> Self {
39    Frame {
40      inline: [EMPTY; INLINE],
41      inline_len: 0,
42      spill: Vec::new(),
43      progress: 0.0,
44      finished: false,
45    }
46  }
47}
48
49impl PartialEq for Frame {
50  /// Compared by what it holds, not by where it holds it — two frames with
51  /// the same properties are equal whether or not one of them spilled.
52  fn eq(&self, other: &Self) -> bool {
53    self.progress == other.progress
54      && self.finished == other.finished
55      && self.len() == other.len()
56      && self.iter().eq(other.iter())
57  }
58}
59
60impl Frame {
61  pub fn new() -> Self {
62    Frame::default()
63  }
64
65  /// Set a property, replacing any value already there. Later writers win,
66  /// which is what layers a sequence's overlapping tracks.
67  pub fn set(&mut self, prop: Prop, value: impl Into<AnimValue>) {
68    let value = value.into();
69    if let Some(slot) = self.inline[..self.inline_len]
70      .iter_mut()
71      .find(|(p, _)| *p == prop)
72    {
73      slot.1 = value;
74      return;
75    }
76    if let Some(slot) = self.spill.iter_mut().find(|(p, _)| *p == prop) {
77      slot.1 = value;
78      return;
79    }
80    if self.inline_len < INLINE {
81      self.inline[self.inline_len] = (prop, value);
82      self.inline_len += 1;
83    } else {
84      self.spill.push((prop, value));
85    }
86  }
87
88  pub fn get(&self, prop: Prop) -> Option<AnimValue> {
89    self.iter().find(|(p, _)| *p == prop).map(|(_, v)| v)
90  }
91
92  pub fn number(&self, prop: Prop) -> Option<f32> {
93    self.get(prop).map(AnimValue::number)
94  }
95
96  /// The number, or `fallback` when the property isn't animating.
97  pub fn number_or(&self, prop: Prop, fallback: f32) -> f32 {
98    self.number(prop).unwrap_or(fallback)
99  }
100
101  pub fn color(&self, prop: Prop) -> Option<Hsla> {
102    self.get(prop).and_then(AnimValue::color)
103  }
104
105  pub fn is_empty(&self) -> bool {
106    self.inline_len == 0
107  }
108
109  pub fn len(&self) -> usize {
110    self.inline_len + self.spill.len()
111  }
112
113  pub fn iter(&self) -> impl Iterator<Item = (Prop, AnimValue)> + '_ {
114    self.inline[..self.inline_len]
115      .iter()
116      .chain(self.spill.iter())
117      .copied()
118  }
119
120  /// Empty it for reuse, keeping any spill capacity it earned.
121  pub fn clear(&mut self) {
122    self.inline_len = 0;
123    self.spill.clear();
124    self.progress = 0.0;
125    self.finished = false;
126  }
127
128  /// Write every styled property onto an element.
129  ///
130  /// [`Prop::X`]/[`Prop::Y`] become a relative inset: gpui elements are
131  /// `Position::Relative` by default and taffy treats an inset on those as
132  /// a paint-time correction, so the element slides without pushing its
133  /// siblings around. [`Prop::Rotate`], [`Prop::Scale`] and
134  /// [`Prop::Custom`] are skipped — read those back yourself.
135  pub fn apply<E: Styled>(&self, mut el: E) -> E {
136    for (prop, value) in self.iter() {
137      // A NaN reaching taffy corrupts a layout silently and forever;
138      // treating it as zero is a visible, recoverable wrong answer. It
139      // can only get here from a caller tweening to one — the timing
140      // setters sanitize their own input.
141      let n = match value.number() {
142        n if n.is_finite() => n,
143        _ => 0.0,
144      };
145      el = match prop {
146        Prop::Opacity => el.opacity(n.clamp(0.0, 1.0)),
147        Prop::X => el.left(px(n)),
148        Prop::Y => el.top(px(n)),
149        Prop::Width => el.w(px(n.max(0.0))),
150        Prop::Height => el.h(px(n.max(0.0))),
151        Prop::MarginTop => el.mt(px(n)),
152        Prop::MarginRight => el.mr(px(n)),
153        Prop::MarginBottom => el.mb(px(n)),
154        Prop::MarginLeft => el.ml(px(n)),
155        Prop::PadTop => el.pt(px(n.max(0.0))),
156        Prop::PadRight => el.pr(px(n.max(0.0))),
157        Prop::PadBottom => el.pb(px(n.max(0.0))),
158        Prop::PadLeft => el.pl(px(n.max(0.0))),
159        Prop::Radius => el.rounded(px(n.max(0.0))),
160        Prop::BorderWidth => el.border(px(n.max(0.0))),
161        Prop::Gap => el.gap(px(n.max(0.0))),
162        Prop::FontSize => el.text_size(px(n.max(0.0))),
163        Prop::Background => match value.color() {
164          Some(color) => el.bg(color),
165          None => el,
166        },
167        Prop::BorderColor => match value.color() {
168          Some(color) => el.border_color(color),
169          None => el,
170        },
171        Prop::TextColor => match value.color() {
172          Some(color) => el.text_color(color),
173          None => el,
174        },
175        Prop::Rotate | Prop::Scale | Prop::Custom(_) => el,
176      };
177    }
178    el
179  }
180}
181
182#[cfg(test)]
183mod tests {
184  use super::*;
185
186  #[test]
187  fn setting_the_same_prop_twice_replaces_it() {
188    let mut frame = Frame::new();
189    frame.set(Prop::Opacity, 0.2);
190    frame.set(Prop::Opacity, 0.9);
191    assert_eq!(frame.len(), 1);
192    assert_eq!(frame.number(Prop::Opacity), Some(0.9));
193  }
194
195  #[test]
196  fn missing_props_fall_back() {
197    let frame = Frame::new();
198    assert_eq!(frame.number(Prop::X), None);
199    assert_eq!(frame.number_or(Prop::X, 4.0), 4.0);
200  }
201
202  /// The claim `INLINE` is chosen for: nothing you would normally reach
203  /// for should reach the heap once per frame.
204  #[test]
205  fn the_stock_motions_fit_inline() {
206    use crate::anim::Motion;
207    use crate::TransitionKind;
208
209    for kind in [
210      TransitionKind::Fade,
211      TransitionKind::SlideUp,
212      TransitionKind::SlideDown,
213      TransitionKind::SlideLeft,
214      TransitionKind::SlideRight,
215    ] {
216      for motion in [Motion::enter(kind), Motion::exit(kind)] {
217        let frame = motion.sample(motion.iteration_ms() / 2.0);
218        assert!(frame.len() <= INLINE, "{kind:?} spilled: {frame:?}");
219        assert!(frame.spill.is_empty());
220      }
221    }
222    assert!(Motion::pulse().sample(100.0).spill.is_empty());
223  }
224
225  #[test]
226  fn a_frame_spills_past_the_inline_slots_and_still_reads_back() {
227    let mut frame = Frame::new();
228    let props = [
229      Prop::Opacity,
230      Prop::X,
231      Prop::Y,
232      Prop::Radius,
233      Prop::Gap,
234      Prop::FontSize,
235    ];
236    for (i, prop) in props.iter().enumerate() {
237      frame.set(*prop, i as f32);
238    }
239    assert_eq!(frame.len(), props.len());
240    for (i, prop) in props.iter().enumerate() {
241      assert_eq!(frame.number(*prop), Some(i as f32), "{prop:?}");
242    }
243    // Order survives the spill boundary.
244    let seen: Vec<Prop> = frame.iter().map(|(p, _)| p).collect();
245    assert_eq!(seen, props);
246
247    // Replacing works on either side of it.
248    frame.set(Prop::Opacity, 9.0);
249    frame.set(Prop::FontSize, 9.0);
250    assert_eq!(frame.len(), props.len());
251    assert_eq!(frame.number(Prop::Opacity), Some(9.0));
252    assert_eq!(frame.number(Prop::FontSize), Some(9.0));
253  }
254
255  #[test]
256  fn frames_compare_by_content_not_by_storage() {
257    let mut small = Frame::new();
258    let mut spilled = Frame::new();
259    for i in 0..5 {
260      let prop = [Prop::Opacity, Prop::X, Prop::Y, Prop::Radius, Prop::Gap][i];
261      small.set(prop, i as f32);
262      spilled.set(prop, i as f32);
263    }
264    assert_eq!(small, spilled);
265    spilled.set(Prop::Gap, 99.0);
266    assert_ne!(small, spilled);
267  }
268
269  #[test]
270  fn a_non_finite_value_never_reaches_the_layout() {
271    let mut frame = Frame::new();
272    frame.set(Prop::X, f32::NAN);
273    frame.set(Prop::Width, f32::INFINITY);
274    frame.set(Prop::Opacity, f32::NEG_INFINITY);
275    // `apply` is where it matters, but the values are readable as-is —
276    // a host reading `Prop::Custom` gets exactly what was tweened.
277    assert!(frame.number(Prop::X).unwrap().is_nan());
278
279    let mut styled = frame.apply(gpui::div());
280    let style = styled.style();
281    assert_eq!(style.inset.left, Some(gpui::px(0.0).into()));
282    assert_eq!(style.size.width, Some(gpui::px(0.0).into()));
283    assert_eq!(style.opacity, Some(0.0));
284  }
285
286  #[test]
287  fn clearing_lets_a_frame_be_reused() {
288    let mut frame = Frame::new();
289    frame.set(Prop::Opacity, 1.0);
290    frame.progress = 0.5;
291    frame.finished = true;
292    frame.clear();
293    assert!(frame.is_empty());
294    assert_eq!(frame.len(), 0);
295    assert_eq!(frame.progress, 0.0);
296    assert!(!frame.finished);
297    assert_eq!(frame.number(Prop::Opacity), None);
298  }
299
300  #[test]
301  fn insertion_order_is_preserved() {
302    let mut frame = Frame::new();
303    frame.set(Prop::Y, 3.0);
304    frame.set(Prop::X, 1.0);
305    frame.set(Prop::Y, 5.0);
306    let props: Vec<Prop> = frame.iter().map(|(p, _)| p).collect();
307    assert_eq!(props, vec![Prop::Y, Prop::X]);
308  }
309}