Skip to main content

gpui_base/motion/
presence.rs

1use std::time::Duration;
2
3use gpui::{App, ElementId, Window};
4
5use super::{MotionStatus, Transition, TransitionId};
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub enum PresencePhase {
9    Entering,
10    Present,
11    Exiting,
12    Absent,
13}
14
15#[derive(Clone, Copy, Debug, PartialEq)]
16pub struct PresenceSample {
17    pub phase: PresencePhase,
18    pub progress: f32,
19    pub status: MotionStatus,
20}
21
22impl PresenceSample {
23    pub const fn should_render(self) -> bool {
24        !matches!(self.phase, PresencePhase::Absent)
25    }
26}
27
28pub struct Presence {
29    id: TransitionId,
30    present: bool,
31    transition: Transition,
32}
33
34impl Presence {
35    pub fn new(id: impl Into<TransitionId>, present: bool) -> Self {
36        Self {
37            id: id.into(),
38            present,
39            transition: Transition::new(Duration::ZERO),
40        }
41    }
42
43    pub fn transition(mut self, transition: Transition) -> Self {
44        self.transition = transition;
45        self
46    }
47
48    pub fn sample(self, window: &mut Window, cx: &mut App) -> PresenceSample {
49        let id = ElementId::NamedChild(self.id.0.into(), "__presence".into());
50        let now = cx.background_executor().now();
51        let target = if self.present { 1.0 } else { 0.0 };
52        let state = window.use_keyed_state(id, cx, |_, _| PresenceState {
53            from: 0.0,
54            target,
55            started_at: now,
56            reversing_factor: 1.0,
57            duration: self.transition.duration,
58        });
59        let snapshot = *state.read(cx);
60
61        if cx.reduce_motion() || self.transition.duration.is_zero() {
62            if snapshot.from != target || snapshot.target != target {
63                state.update(cx, |state, _| {
64                    state.from = target;
65                    state.target = target;
66                    state.started_at = now;
67                    state.reversing_factor = 1.0;
68                    state.duration = self.transition.duration;
69                });
70            }
71            return stable_sample(self.present);
72        }
73
74        let elapsed = now.saturating_duration_since(snapshot.started_at);
75        let (progress, status) = self.transition.progress(elapsed, snapshot.duration);
76        let sampled = interpolate(
77            snapshot.from,
78            snapshot.target,
79            self.transition.sample(progress),
80        );
81
82        let (progress, status) = if snapshot.target != target {
83            let reversing = target == snapshot.from;
84            let reversing_factor = if reversing {
85                (self.transition.sample(progress) * snapshot.reversing_factor
86                    + (1.0 - snapshot.reversing_factor))
87                    .clamp(0.0, 1.0)
88            } else {
89                1.0
90            };
91            let duration = self
92                .transition
93                .duration
94                .mul_f64(f64::from(reversing_factor));
95            state.update(cx, |state, _| {
96                state.from = sampled;
97                state.target = target;
98                state.started_at = now;
99                state.reversing_factor = reversing_factor;
100                state.duration = duration;
101            });
102            let (initial, status) = self.transition.progress(Duration::ZERO, duration);
103            (
104                interpolate(sampled, target, self.transition.sample(initial)),
105                status,
106            )
107        } else {
108            (sampled, status)
109        };
110
111        if matches!(status, MotionStatus::Delayed | MotionStatus::Running) {
112            window.request_animation_frame();
113        }
114
115        if status == MotionStatus::Finished {
116            stable_sample(self.present)
117        } else {
118            PresenceSample {
119                phase: if self.present {
120                    PresencePhase::Entering
121                } else {
122                    PresencePhase::Exiting
123                },
124                progress,
125                status,
126            }
127        }
128    }
129}
130
131#[derive(Clone, Copy)]
132struct PresenceState {
133    from: f32,
134    target: f32,
135    started_at: super::Instant,
136    reversing_factor: f32,
137    duration: Duration,
138}
139
140fn stable_sample(present: bool) -> PresenceSample {
141    PresenceSample {
142        phase: if present {
143            PresencePhase::Present
144        } else {
145            PresencePhase::Absent
146        },
147        progress: if present { 1.0 } else { 0.0 },
148        status: MotionStatus::Finished,
149    }
150}
151
152fn interpolate(from: f32, to: f32, progress: f32) -> f32 {
153    from + (to - from) * progress
154}