1use std::time::Duration;
8
9use gpui::{App, Window};
10use web_time::Instant;
11
12use super::MotionSpec;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Phase {
17 Entering,
18 Present,
19 Exiting,
20 Gone,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq)]
42pub struct Presence {
43 phase: Phase,
44 elapsed: Duration,
45 enter: MotionSpec,
46 exit: MotionSpec,
47 last_frame: Option<Instant>,
48}
49
50impl Presence {
51 pub fn hidden(enter: MotionSpec, exit: MotionSpec) -> Self {
53 Self {
54 phase: Phase::Gone,
55 elapsed: Duration::ZERO,
56 enter,
57 exit,
58 last_frame: None,
59 }
60 }
61
62 pub fn visible(enter: MotionSpec, exit: MotionSpec) -> Self {
64 Self {
65 phase: Phase::Present,
66 elapsed: Duration::ZERO,
67 enter,
68 exit,
69 last_frame: None,
70 }
71 }
72
73 pub fn phase(&self) -> Phase {
74 self.phase
75 }
76
77 pub fn is_rendered(&self) -> bool {
80 self.phase != Phase::Gone
81 }
82
83 pub fn is_animating(&self) -> bool {
84 matches!(self.phase, Phase::Entering | Phase::Exiting)
85 }
86
87 pub fn progress(&self) -> f32 {
89 match self.phase {
90 Phase::Gone => 0.0,
91 Phase::Present => 1.0,
92 Phase::Entering => self.span_progress(self.enter),
93 Phase::Exiting => 1.0 - self.span_progress(self.exit),
94 }
95 }
96
97 fn span_progress(&self, spec: MotionSpec) -> f32 {
98 let total = spec.total().as_secs_f32();
99 if total <= 0.0 {
100 return 1.0;
101 }
102 spec.progress((self.elapsed.as_secs_f32() / total).clamp(0.0, 1.0))
103 }
104
105 pub fn show(&mut self) {
107 match self.phase {
108 Phase::Present | Phase::Entering => {}
109 Phase::Gone => {
110 self.phase = Phase::Entering;
111 self.elapsed = Duration::ZERO;
112 }
113 Phase::Exiting => {
114 let visible = self.progress();
115 self.phase = Phase::Entering;
116 self.elapsed = self.enter.time_at(visible);
117 }
118 }
119 }
120
121 pub fn hide(&mut self) {
122 match self.phase {
123 Phase::Gone | Phase::Exiting => {}
124 Phase::Present => {
125 self.phase = Phase::Exiting;
126 self.elapsed = Duration::ZERO;
127 }
128 Phase::Entering => {
129 let visible = self.progress();
130 self.phase = Phase::Exiting;
131 self.elapsed = self.exit.time_at(1.0 - visible);
134 }
135 }
136 }
137
138 pub fn toggle(&mut self) {
139 if matches!(self.phase, Phase::Present | Phase::Entering) {
140 self.hide();
141 } else {
142 self.show();
143 }
144 }
145
146 pub fn settle(&mut self) {
148 self.phase = match self.phase {
149 Phase::Entering | Phase::Present => Phase::Present,
150 Phase::Exiting | Phase::Gone => Phase::Gone,
151 };
152 self.elapsed = Duration::ZERO;
153 }
154
155 pub fn advance(&mut self, delta: Duration) {
156 if !self.is_animating() {
157 return;
158 }
159 self.elapsed += delta;
160 let span = match self.phase {
161 Phase::Entering => self.enter.total(),
162 _ => self.exit.total(),
163 };
164 if self.elapsed >= span {
165 self.settle();
166 }
167 }
168
169 pub fn animate(&mut self, window: &mut Window, cx: &mut App) -> f32 {
172 if cx.reduce_motion() {
173 self.settle();
174 self.last_frame = None;
175 return self.progress();
176 }
177
178 let now = cx.background_executor().now();
179 if let Some(last) = self.last_frame {
180 self.advance(now.saturating_duration_since(last));
181 }
182 if self.is_animating() {
183 self.last_frame = Some(now);
184 window.request_animation_frame();
185 } else {
186 self.last_frame = None;
187 }
188 self.progress()
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195 use crate::motion::{CubicBezier, MotionSpec};
196
197 fn presence() -> Presence {
198 let linear = |ms| MotionSpec::new(ms, CubicBezier::new(0.0, 0.0, 1.0, 1.0));
199 Presence::hidden(linear(200), linear(100))
200 }
201
202 #[test]
203 fn a_hidden_presence_renders_nothing() {
204 let presence = presence();
205 assert!(!presence.is_rendered());
206 assert_eq!(presence.progress(), 0.0);
207 }
208
209 #[test]
210 fn entering_becomes_present_only_after_the_full_span() {
211 let mut presence = presence();
212 presence.show();
213 assert_eq!(presence.phase(), Phase::Entering);
214 presence.advance(Duration::from_millis(100));
215 assert_eq!(presence.phase(), Phase::Entering);
216 assert!((presence.progress() - 0.5).abs() < 0.05);
217 presence.advance(Duration::from_millis(100));
218 assert_eq!(presence.phase(), Phase::Present);
219 assert_eq!(presence.progress(), 1.0);
220 }
221
222 #[test]
223 fn an_exiting_element_stays_rendered_until_the_exit_completes() {
224 let mut presence = presence();
225 presence.show();
226 presence.advance(Duration::from_millis(200));
227 presence.hide();
228
229 assert!(
230 presence.is_rendered(),
231 "the exit needs the element on screen"
232 );
233 presence.advance(Duration::from_millis(50));
234 assert!(presence.is_rendered());
235 assert!(presence.progress() < 1.0);
236 presence.advance(Duration::from_millis(50));
237 assert_eq!(presence.phase(), Phase::Gone);
238 assert!(!presence.is_rendered());
239 }
240
241 #[test]
242 fn reversing_an_exit_resumes_from_what_is_on_screen() {
243 let mut presence = presence();
244 presence.show();
245 presence.advance(Duration::from_millis(200));
246 presence.hide();
247 presence.advance(Duration::from_millis(50));
248 let interrupted = presence.progress();
249
250 presence.show();
251 assert_eq!(presence.phase(), Phase::Entering);
252 assert!((presence.progress() - interrupted).abs() < 0.05);
253 }
254
255 #[test]
256 fn reversing_an_entrance_resumes_from_what_is_on_screen() {
257 let mut presence = presence();
258 presence.show();
259 presence.advance(Duration::from_millis(100));
260 let interrupted = presence.progress();
261
262 presence.hide();
263 assert_eq!(presence.phase(), Phase::Exiting);
264 assert!((presence.progress() - interrupted).abs() < 0.05);
265 }
266
267 fn curved() -> Presence {
270 let curve = CubicBezier::new(0.42, 0.0, 0.58, 1.0);
271 Presence::hidden(MotionSpec::new(200, curve), MotionSpec::new(100, curve))
272 }
273
274 fn run_out(presence: &mut Presence) -> Duration {
277 let mut elapsed = Duration::ZERO;
278 while presence.is_animating() {
279 presence.advance(Duration::from_millis(1));
280 elapsed += Duration::from_millis(1);
281 }
282 elapsed
283 }
284
285 #[test]
286 fn a_cancelled_entrance_leaves_from_the_opacity_it_reached() {
287 let mut presence = curved();
288 presence.show();
289 presence.advance(Duration::from_millis(60));
290 let interrupted = presence.progress();
291 assert!(interrupted < 0.25, "the curve starts slowly: {interrupted}");
292
293 presence.hide();
294 assert!(
295 (presence.progress() - interrupted).abs() < 0.01,
296 "the element jumped from {interrupted} to {} on being cancelled",
297 presence.progress()
298 );
299 let took = run_out(&mut presence);
300 assert_eq!(presence.phase(), Phase::Gone);
301 assert!(
302 took < Duration::from_millis(40),
303 "leaving from {interrupted} took {took:?} of a 100ms exit"
304 );
305 }
306
307 #[test]
308 fn a_cancelled_exit_comes_back_the_way_it_went() {
309 let mut presence = curved();
310 presence.show();
311 presence.advance(Duration::from_millis(200));
312 presence.hide();
313 presence.advance(Duration::from_millis(40));
314 let interrupted = presence.progress();
315
316 presence.show();
317 assert!(
318 (presence.progress() - interrupted).abs() < 0.01,
319 "the element jumped from {interrupted} to {}",
320 presence.progress()
321 );
322 let took = run_out(&mut presence);
323 assert_eq!(presence.phase(), Phase::Present);
324 assert!(
325 took < Duration::from_millis(200),
326 "returning from {interrupted} took the whole {took:?} entrance"
327 );
328 }
329
330 #[test]
331 fn the_earlier_a_phase_is_cancelled_the_less_of_the_other_it_costs() {
332 let took = |after_ms| {
333 let mut presence = curved();
334 presence.show();
335 presence.advance(Duration::from_millis(after_ms));
336 presence.hide();
337 run_out(&mut presence)
338 };
339 assert!(
340 took(40) < took(100) && took(100) < took(180),
341 "{:?}, {:?}, {:?}",
342 took(40),
343 took(100),
344 took(180)
345 );
346 }
347
348 #[test]
349 fn a_delayed_entrance_reverses_from_what_is_on_screen() {
350 let linear = |ms| MotionSpec::new(ms, CubicBezier::new(0.0, 0.0, 1.0, 1.0));
351 let mut presence = Presence::hidden(linear(100).with_delay(100), linear(100));
352 presence.show();
353 presence.advance(Duration::from_millis(150));
354 let interrupted = presence.progress();
355 assert!((interrupted - 0.5).abs() < 0.02);
356
357 presence.hide();
358 assert!((presence.progress() - interrupted).abs() < 0.02);
359 let took = run_out(&mut presence);
360 assert!(
361 took.abs_diff(Duration::from_millis(50)) <= Duration::from_millis(2),
362 "half an exit is 50ms, not {took:?}"
363 );
364 }
365
366 #[test]
367 fn a_sprung_entrance_reverses_from_what_is_on_screen() {
368 let spring = MotionSpec::sprung(crate::motion::Spring::perceptual(
369 Duration::from_millis(300),
370 0.4,
371 ));
372 let mut presence = Presence::hidden(spring, spring);
373 presence.show();
374 presence.advance(Duration::from_millis(80));
375 let interrupted = presence.progress();
376
377 presence.hide();
378 assert!(
379 (presence.progress() - interrupted).abs() < 0.02,
380 "a sprung entrance jumped from {interrupted} to {}",
381 presence.progress()
382 );
383 }
384
385 #[test]
386 fn toggling_alternates_between_the_two_ends() {
387 let mut presence = presence();
388 presence.toggle();
389 assert_eq!(presence.phase(), Phase::Entering);
390 presence.settle();
391 presence.toggle();
392 assert_eq!(presence.phase(), Phase::Exiting);
393 }
394}