Skip to main content

glassy_ui/motion/
presence.rs

1use std::collections::HashMap;
2use std::rc::Rc;
3use std::time::{Duration, Instant};
4
5use gpui::{
6    div, AnyElement, App, Empty, Global, IntoElement, ParentElement, RenderOnce, SharedString,
7    StyleRefinement, Styled, Window,
8};
9
10use super::slot::StyledSlot;
11use super::style::{apply_style, MotionStyle, ResolvedStyle};
12use super::transition::{ease_out_cubic, Transition};
13
14type PresenceFactory = Rc<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>;
15
16/// How enter/exit overlap (motion.dev `AnimatePresence` mode).
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
18pub enum PresenceMode {
19    /// Play exit and enter together.
20    Sync,
21    /// Wait for exit to finish before enter (default).
22    #[default]
23    Wait,
24}
25
26struct ExitingChild {
27    factory: PresenceFactory,
28    started: Instant,
29    duration: Duration,
30    from: MotionStyle,
31    exit: MotionStyle,
32}
33
34struct PresenceSlot {
35    current_key: SharedString,
36    current: PresenceFactory,
37    exiting: Option<ExitingChild>,
38}
39
40/// Global presence tracking keyed by presence host id.
41pub struct PresenceStore {
42    slots: HashMap<SharedString, PresenceSlot>,
43}
44
45impl Global for PresenceStore {}
46
47pub fn init(cx: &mut App) {
48    if !cx.has_global::<PresenceStore>() {
49        cx.set_global(PresenceStore {
50            slots: HashMap::new(),
51        });
52    }
53}
54
55/// Keeps the previous child mounted until its exit animation finishes.
56#[derive(IntoElement)]
57pub struct AnimatePresence {
58    id: SharedString,
59    mode: PresenceMode,
60    key: SharedString,
61    factory: Option<PresenceFactory>,
62    exit: MotionStyle,
63    exit_transition: Transition,
64    style: StyleRefinement,
65}
66
67impl AnimatePresence {
68    pub fn new(id: impl Into<SharedString>) -> Self {
69        Self {
70            id: id.into(),
71            mode: PresenceMode::Wait,
72            key: SharedString::from(""),
73            factory: None,
74            exit: MotionStyle::new().opacity(0.).y(gpui::px(-10.)),
75            exit_transition: Transition::tween(Duration::from_millis(220)),
76            style: StyleRefinement::default(),
77        }
78    }
79
80    pub fn mode(mut self, mode: PresenceMode) -> Self {
81        self.mode = mode;
82        self
83    }
84
85    pub fn exit(mut self, style: MotionStyle) -> Self {
86        self.exit = style;
87        self
88    }
89
90    pub fn exit_transition(mut self, transition: Transition) -> Self {
91        self.exit_transition = transition;
92        self
93    }
94
95    pub fn child<F, E>(mut self, key: impl Into<SharedString>, factory: F) -> Self
96    where
97        F: Fn(&mut Window, &mut App) -> E + 'static,
98        E: IntoElement,
99    {
100        self.key = key.into();
101        self.factory = Some(Rc::new(move |window, cx| {
102            factory(window, cx).into_any_element()
103        }));
104        self
105    }
106}
107
108impl Styled for AnimatePresence {
109    fn style(&mut self) -> &mut StyleRefinement {
110        &mut self.style
111    }
112}
113
114impl RenderOnce for AnimatePresence {
115    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
116        init(cx);
117
118        let Some(factory) = self.factory else {
119            return Empty.into_any_element();
120        };
121
122        let exit_duration = self.exit_transition.resolved_duration();
123        let now = Instant::now();
124        let mode = self.mode;
125        let exit_style = self.exit;
126        let host_style = self.style;
127        let id = self.id;
128
129        {
130            let store = cx.global_mut::<PresenceStore>();
131            match store.slots.get_mut(&id) {
132                Some(slot) => {
133                    if slot.current_key != self.key {
134                        let old_factory = std::mem::replace(&mut slot.current, factory);
135                        slot.current_key = self.key.clone();
136                        slot.exiting = Some(ExitingChild {
137                            factory: old_factory,
138                            started: now,
139                            duration: exit_duration,
140                            from: MotionStyle::new().opacity(1.).y(gpui::px(0.)),
141                            exit: exit_style,
142                        });
143                    } else {
144                        slot.current = factory;
145                    }
146                }
147                None => {
148                    store.slots.insert(
149                        id.clone(),
150                        PresenceSlot {
151                            current_key: self.key.clone(),
152                            current: factory,
153                            exiting: None,
154                        },
155                    );
156                }
157            }
158        }
159
160        // Snapshot what we need, then drop the store borrow before calling factories.
161        let (current_factory, exiting_snap, still_exiting) = {
162            let store = cx.global_mut::<PresenceStore>();
163            let slot = store.slots.get_mut(&id).unwrap();
164
165            if let Some(exiting) = &slot.exiting {
166                if exiting.started.elapsed() >= exiting.duration {
167                    slot.exiting = None;
168                }
169            }
170
171            let still_exiting = slot.exiting.is_some();
172            let exiting_snap = slot.exiting.as_ref().map(|exiting| {
173                let t = (exiting.started.elapsed().as_secs_f32()
174                    / exiting.duration.as_secs_f32().max(0.0001))
175                .clamp(0.0, 1.0);
176                (exiting.factory.clone(), exiting.from, exiting.exit, t)
177            });
178            (slot.current.clone(), exiting_snap, still_exiting)
179        };
180
181        let show_enter = match mode {
182            PresenceMode::Sync => true,
183            PresenceMode::Wait => !still_exiting,
184        };
185
186        let exiting_el = exiting_snap.map(|(factory, from_style, exit_style, t)| {
187            let from = from_style.resolve(ResolvedStyle::default());
188            let to = exit_style.resolve(from);
189            let frame = from.lerp(to, ease_out_cubic(t));
190            let child = factory(window, cx);
191            apply_style(div().absolute().inset_0().size_full(), frame).child(child)
192        });
193
194        let current_el = if show_enter {
195            Some(current_factory(window, cx))
196        } else {
197            None
198        };
199
200        if still_exiting {
201            window.request_animation_frame();
202        }
203
204        div()
205            .refine_style(&host_style)
206            .size_full()
207            .relative()
208            .children(exiting_el)
209            .children(current_el)
210            .into_any_element()
211    }
212}