gpui_kit/motion/
stagger.rs1use std::time::Duration;
4
5use super::MotionSpec;
6
7const ROW_STEP_MS: u64 = 16;
10const ROW_WINDOW: usize = 8;
11
12pub const ROW_STAGGER_CAP: Duration = Duration::from_millis(ROW_STEP_MS * (ROW_WINDOW as u64 - 1));
14
15#[derive(Debug, Clone, Copy, PartialEq)]
20pub struct Stagger {
21 step: Duration,
22 max_items: usize,
23 reversed: bool,
24}
25
26impl Stagger {
27 pub fn new(step: Duration, max_items: usize) -> Self {
28 Self {
29 step,
30 max_items: max_items.max(2),
31 reversed: false,
32 }
33 }
34
35 pub fn from_millis(step_ms: u64) -> Self {
36 Self::new(Duration::from_millis(step_ms), 12)
37 }
38
39 pub fn rows() -> Self {
45 Self::new(Duration::from_millis(ROW_STEP_MS), ROW_WINDOW)
46 }
47
48 pub fn max_items(mut self, max_items: usize) -> Self {
49 self.max_items = max_items.max(2);
50 self
51 }
52
53 pub fn reversed(mut self) -> Self {
61 self.reversed = true;
62 self
63 }
64
65 pub fn is_reversed(&self) -> bool {
66 self.reversed
67 }
68
69 fn step_for(&self, count: usize) -> Duration {
73 if count > self.max_items {
74 self.step
75 .mul_f32((self.max_items - 1) as f32 / (count - 1) as f32)
76 } else {
77 self.step
78 }
79 }
80
81 pub fn delay(&self, index: usize, count: usize) -> Duration {
82 if count <= 1 {
83 return Duration::ZERO;
84 }
85 let last = count - 1;
86 let place = index.min(last);
87 let place = if self.reversed { last - place } else { place };
88 self.step_for(count).mul_f32(place as f32)
89 }
90
91 pub fn total(&self, count: usize, spec: MotionSpec) -> Duration {
97 let furthest = self.step_for(count).mul_f32(count.saturating_sub(1) as f32);
98 furthest + spec.total()
99 }
100
101 pub fn spec(&self, index: usize, count: usize, spec: MotionSpec) -> MotionSpec {
103 spec.with_delay(spec.delay_ms + self.delay(index, count).as_millis() as u64)
104 }
105}
106
107pub fn staggered_phase(raw: f32, index: usize, stagger: f32) -> f32 {
110 (raw - index as f32 * stagger).rem_euclid(1.0)
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use crate::motion::CubicBezier;
117
118 fn spec() -> MotionSpec {
119 MotionSpec::new(100, CubicBezier::new(0.0, 0.0, 1.0, 1.0))
120 }
121
122 #[test]
123 fn the_first_item_never_waits() {
124 let stagger = Stagger::from_millis(30);
125 assert_eq!(stagger.delay(0, 5), Duration::ZERO);
126 assert_eq!(stagger.delay(0, 1), Duration::ZERO);
127 }
128
129 #[test]
130 fn delays_increase_with_position() {
131 let stagger = Stagger::from_millis(30);
132 assert_eq!(stagger.delay(1, 5), Duration::from_millis(30));
133 assert_eq!(stagger.delay(4, 5), Duration::from_millis(120));
134 }
135
136 #[test]
137 fn a_long_list_compresses_instead_of_growing_without_bound() {
138 let stagger = Stagger::from_millis(30).max_items(10);
139 let short = stagger.total(10, spec());
140 for count in [200, 2000] {
141 let long = stagger.total(count, spec());
142 assert!(
143 long.abs_diff(short) < Duration::from_millis(1),
144 "{count} items took {long:?} against {short:?} for ten"
145 );
146 }
147 }
148
149 #[test]
150 fn the_group_window_covers_the_last_items_span() {
151 let stagger = Stagger::from_millis(30);
152 assert_eq!(stagger.total(3, spec()), Duration::from_millis(160));
153 }
154
155 #[test]
156 fn a_row_wave_never_outlasts_its_cap() {
157 let stagger = Stagger::rows();
158 assert_eq!(stagger.delay(0, 50), Duration::ZERO);
159 for count in [2, 8, 50, 500] {
160 let waited = stagger.delay(count - 1, count);
164 assert!(
165 waited.as_millis() <= ROW_STAGGER_CAP.as_millis(),
166 "{count} rows waited {waited:?}"
167 );
168 }
169 assert_eq!(stagger.delay(7, 8), ROW_STAGGER_CAP);
170 }
171
172 #[test]
173 fn a_reversed_wave_starts_at_the_far_end() {
174 let stagger = Stagger::from_millis(30).reversed();
175 assert!(stagger.is_reversed());
176 assert_eq!(stagger.delay(4, 5), Duration::ZERO);
177 assert_eq!(stagger.delay(3, 5), Duration::from_millis(30));
178 assert_eq!(stagger.delay(0, 5), Duration::from_millis(120));
179 }
180
181 #[test]
182 fn reversing_does_not_change_how_long_the_group_takes() {
183 let forward = Stagger::from_millis(30);
184 for count in [1, 2, 5, 200] {
185 assert_eq!(
186 forward.total(count, spec()),
187 forward.reversed().total(count, spec()),
188 "{count} items"
189 );
190 }
191 }
192
193 #[test]
194 fn a_reversed_wave_compresses_the_same_way_a_forward_one_does() {
195 let stagger = Stagger::rows().reversed();
196 for count in [2, 8, 50, 500] {
197 let waited = stagger.delay(0, count);
198 assert!(
199 waited.as_millis() <= ROW_STAGGER_CAP.as_millis(),
200 "{count} rows waited {waited:?}"
201 );
202 }
203 assert_eq!(stagger.delay(7, 8), Duration::ZERO);
204 assert_eq!(stagger.delay(0, 8), ROW_STAGGER_CAP);
205 }
206
207 #[test]
208 fn a_staggered_phase_wraps_within_one_cycle() {
209 for index in 0..6 {
210 let phase = staggered_phase(0.2, index, 0.15);
211 assert!((0.0..1.0).contains(&phase));
212 }
213 }
214}