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#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
18pub enum PresenceMode {
19 Sync,
21 #[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
40pub 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#[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 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}