1use gpui::prelude::*;
8use gpui::{px, Hsla};
9
10use super::{AnimValue, Prop};
11
12pub(crate) const INLINE: usize = 4;
19
20const EMPTY: (Prop, AnimValue) = (Prop::Opacity, AnimValue::Number(0.0));
23
24#[derive(Debug, Clone)]
26pub struct Frame {
27 inline: [(Prop, AnimValue); INLINE],
28 inline_len: usize,
29 spill: Vec<(Prop, AnimValue)>,
30 pub progress: f32,
32 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 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 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 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 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 pub fn apply<E: Styled>(&self, mut el: E) -> E {
136 for (prop, value) in self.iter() {
137 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 #[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 let seen: Vec<Prop> = frame.iter().map(|(p, _)| p).collect();
245 assert_eq!(seen, props);
246
247 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 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}