1use std::cell::RefCell;
2use std::rc::Rc;
3use std::time::{Duration, Instant};
4
5use sequoia_animation::{Animate, Easing};
6use sequoia_core::{Length, Vec2};
7use sequoia_layout::LayoutStyle;
8use sequoia_reactive::Signal;
9
10use crate::any::AnyView;
11use crate::view::{PaintCtx, View};
12use crate::views::Unit;
13
14pub const ENTER_SECS: f32 = 0.2;
15pub const EXIT_SECS: f32 = 0.2;
16
17pub fn enter_sample(from: f32, delay: f32, duration: f32, easing: Easing, elapsed: f32) -> f32 {
18 crate::animated::fade_sample(from, delay, duration, easing, elapsed)
19}
20
21pub fn enter_progress(delay: f32, duration: f32, easing: Easing, elapsed: f32) -> f32 {
22 crate::animated::slide_progress(delay, duration, easing, elapsed)
23}
24
25pub fn exit_sample(delay: f32, duration: f32, easing: Easing, elapsed: f32) -> f32 {
26 let delay = delay.max(0.0);
27 if elapsed < delay {
28 return 1.0;
29 }
30 if duration <= 0.0 {
31 return 0.0;
32 }
33 let t = (elapsed - delay) / duration;
34 if t <= 0.0 {
35 return 1.0;
36 }
37 if t >= 1.0 {
38 return 0.0;
39 }
40 1.0 - easing.apply(t)
41}
42
43pub fn exit_progress(delay: f32, duration: f32, easing: Easing, elapsed: f32) -> f32 {
44 let delay = delay.max(0.0);
45 if elapsed < delay {
46 return 0.0;
47 }
48 if duration <= 0.0 {
49 return 1.0;
50 }
51 let t = (elapsed - delay) / duration;
52 if t <= 0.0 {
53 return 0.0;
54 }
55 if t >= 1.0 {
56 return 1.0;
57 }
58 easing.apply(t)
59}
60
61pub fn exit_done(delay: f32, duration: f32, elapsed: f32) -> bool {
62 elapsed >= delay.max(0.0) + duration.max(0.0)
63}
64
65#[derive(Clone, Copy)]
66struct PresenceState {
67 was_present: bool,
68 enter_start: Option<Instant>,
69 exit_start: Option<Instant>,
70 exit_fired: bool,
71}
72
73#[derive(Clone)]
74pub struct Presence {
75 present: Signal<bool>,
76 child: Option<AnyView>,
77 enter_from: f32,
78 enter_secs: f32,
79 enter_delay: f32,
80 enter_easing: Easing,
81 exit_secs: f32,
82 exit_delay: f32,
83 exit_easing: Easing,
84 slide: Vec2,
85 on_exit_complete: Option<Rc<dyn Fn()>>,
86 state: Rc<RefCell<PresenceState>>,
87
88 enter_anim: Option<Animate>,
89 exit_anim: Option<Animate>,
90}
91
92impl Presence {
93 pub fn new(present: Signal<bool>) -> Self {
94 Self {
95 present,
96 child: None,
97 enter_from: 0.0,
98 enter_secs: ENTER_SECS,
99 enter_delay: 0.0,
100 enter_easing: Easing::EaseOutCubic,
101 exit_secs: EXIT_SECS,
102 exit_delay: 0.0,
103 exit_easing: Easing::EaseOutCubic,
104 slide: Vec2::ZERO,
105 on_exit_complete: None,
106 state: Rc::new(RefCell::new(PresenceState {
107 was_present: false,
108 enter_start: None,
109 exit_start: None,
110 exit_fired: false,
111 })),
112 enter_anim: None,
113 exit_anim: None,
114 }
115 }
116
117 pub fn child<V: View + 'static>(mut self, child: V) -> Self {
118 self.child = Some(AnyView::new(child));
119 self
120 }
121
122 pub fn from(mut self, opacity: f32) -> Self {
123 self.enter_from = opacity.clamp(0.0, 1.0);
124 self.enter_anim = None;
125 self
126 }
127
128 pub fn enter_duration(mut self, secs: f32) -> Self {
129 self.enter_secs = secs.max(0.0);
130 self.enter_anim = None;
131 self
132 }
133
134 pub fn enter_duration_d(mut self, d: Duration) -> Self {
135 self.enter_secs = d.as_secs_f32().max(0.0);
136 self.enter_anim = None;
137 self
138 }
139
140 pub fn enter_delay(mut self, secs: f32) -> Self {
141 self.enter_delay = secs.max(0.0);
142 self.enter_anim = None;
143 self
144 }
145
146 pub fn enter_easing(mut self, easing: Easing) -> Self {
147 self.enter_easing = easing;
148 self.enter_anim = None;
149 self
150 }
151
152 pub fn exit_duration(mut self, secs: f32) -> Self {
153 self.exit_secs = secs.max(0.0);
154 self.exit_anim = None;
155 self
156 }
157
158 pub fn exit_duration_d(mut self, d: Duration) -> Self {
159 self.exit_secs = d.as_secs_f32().max(0.0);
160 self.exit_anim = None;
161 self
162 }
163
164 pub fn exit_delay(mut self, secs: f32) -> Self {
165 self.exit_delay = secs.max(0.0);
166 self.exit_anim = None;
167 self
168 }
169
170 pub fn exit_easing(mut self, easing: Easing) -> Self {
171 self.exit_easing = easing;
172 self.exit_anim = None;
173 self
174 }
175
176 pub fn durations(mut self, enter_secs: f32, exit_secs: f32) -> Self {
177 self.enter_secs = enter_secs.max(0.0);
178 self.exit_secs = exit_secs.max(0.0);
179 self.enter_anim = None;
180 self.exit_anim = None;
181 self
182 }
183
184 pub fn durations_d(mut self, enter: Duration, exit: Duration) -> Self {
185 self.enter_secs = enter.as_secs_f32().max(0.0);
186 self.exit_secs = exit.as_secs_f32().max(0.0);
187 self.enter_anim = None;
188 self.exit_anim = None;
189 self
190 }
191
192 pub fn easing(mut self, easing: Easing) -> Self {
193 self.enter_easing = easing;
194 self.exit_easing = easing;
195 self.enter_anim = None;
196 self.exit_anim = None;
197 self
198 }
199
200 pub fn slide(mut self, dx: f32, dy: f32) -> Self {
201 self.slide = Vec2::new(dx, dy);
202 self.enter_anim = None;
203 self.exit_anim = None;
204 self
205 }
206
207 pub fn enter(mut self, anim: impl Into<Animate>) -> Self {
208 let a = anim.into();
209 if let Some((from, _)) = a.opacity {
210 self.enter_from = from;
211 }
212 self.enter_secs = a.duration_secs;
213 self.enter_delay = a.delay_secs;
214 self.enter_easing = a.easing;
215 if let Some((from, _)) = a.offset {
216 self.slide = from;
217 }
218 self.enter_anim = Some(a);
219 self
220 }
221
222 pub fn exit(mut self, anim: impl Into<Animate>) -> Self {
223 let a = anim.into();
224 self.exit_secs = a.duration_secs;
225 self.exit_delay = a.delay_secs;
226 self.exit_easing = a.easing;
227 if let Some((_, to)) = a.offset {
228 self.slide = to;
229 } else if let Some((from, _)) = a.offset {
230 self.slide = from;
231 }
232 self.exit_anim = Some(a);
233 self
234 }
235
236 pub fn on_exit_complete(mut self, f: impl Fn() + 'static) -> Self {
237 self.on_exit_complete = Some(Rc::new(f));
238 self
239 }
240
241 pub fn sample_enter_at(&self, elapsed_secs: f32) -> f32 {
242 if let Some(a) = &self.enter_anim
243 && let Some(v) = a.sample_at(elapsed_secs).opacity
244 {
245 return v;
246 }
247 enter_sample(
248 self.enter_from,
249 self.enter_delay,
250 self.enter_secs,
251 self.enter_easing,
252 elapsed_secs,
253 )
254 }
255
256 pub fn sample_exit_at(&self, elapsed_secs: f32) -> f32 {
257 if let Some(a) = &self.exit_anim
258 && let Some(v) = a.sample_at(elapsed_secs).opacity
259 {
260 return v;
261 }
262 exit_sample(
263 self.exit_delay,
264 self.exit_secs,
265 self.exit_easing,
266 elapsed_secs,
267 )
268 }
269
270 fn sample_enter_offset_at(&self, elapsed_secs: f32) -> Vec2 {
271 if let Some(a) = &self.enter_anim
272 && let Some(v) = a.sample_at(elapsed_secs).offset
273 {
274 return v;
275 }
276 let p = enter_progress(
277 self.enter_delay,
278 self.enter_secs,
279 self.enter_easing,
280 elapsed_secs,
281 );
282 Vec2::new(self.slide.x * (1.0 - p), self.slide.y * (1.0 - p))
283 }
284
285 fn sample_exit_offset_at(&self, elapsed_secs: f32) -> Vec2 {
286 if let Some(a) = &self.exit_anim
287 && let Some(v) = a.sample_at(elapsed_secs).offset
288 {
289 return v;
290 }
291 let p = exit_progress(
292 self.exit_delay,
293 self.exit_secs,
294 self.exit_easing,
295 elapsed_secs,
296 );
297 Vec2::new(self.slide.x * p, self.slide.y * p)
298 }
299
300 pub fn is_mounted(&self) -> bool {
301 if self.present.get_untracked() {
302 return self.child.is_some();
303 }
304 let st = *self.state.borrow();
305 if !st.was_present || self.child.is_none() {
306 return false;
307 }
308 match st.exit_start {
309 None => true,
310 Some(t0) => !exit_done(
311 self.exit_delay,
312 self.exit_secs,
313 Instant::now().duration_since(t0).as_secs_f32(),
314 ),
315 }
316 }
317
318 fn wall_since(slot: &Option<Instant>) -> f32 {
319 match slot {
320 Some(t0) => Instant::now().duration_since(*t0).as_secs_f32().min(600.0),
321 None => 0.0,
322 }
323 }
324}
325
326impl View for Presence {
327 fn body(&self) -> impl View + 'static {
328 Unit
329 }
330 fn reuse_state(&self, old: &Self) {
331 if Rc::ptr_eq(&self.state, &old.state) {
332 return;
333 }
334 let prev = *old.state.borrow();
335 *self.state.borrow_mut() = prev;
336 }
337 fn keyed_children(&self) -> Option<Vec<(u64, AnyView)>> {
338 let present = self.present.get();
339 let Some(child) = self.child.clone() else {
340 return Some(Vec::new());
341 };
342 let mut st = self.state.borrow_mut();
343 if present {
344 if !st.was_present {
345 st.was_present = true;
346 st.enter_start = Some(Instant::now());
347 st.exit_start = None;
348 st.exit_fired = false;
349 }
350 Some(vec![(0, child)])
351 } else {
352 if !st.was_present {
353 return Some(Vec::new());
354 }
355 if st.exit_start.is_none() {
356 st.exit_start = Some(Instant::now());
357 }
358 let elapsed = Self::wall_since(&st.exit_start);
359 if exit_done(self.exit_delay, self.exit_secs, elapsed) {
360 st.was_present = false;
361 st.enter_start = None;
362 st.exit_start = None;
363 Some(Vec::new())
364 } else {
365 Some(vec![(0, child)])
366 }
367 }
368 }
369 fn layout_style(&self) -> LayoutStyle {
370 LayoutStyle {
371 width: Length::Fit,
372 height: Length::Fit,
373 ..LayoutStyle::default()
374 }
375 }
376 fn paint_opacity(&self) -> Option<f32> {
377 let st = *self.state.borrow();
378 if self.present.get_untracked() {
379 if !st.was_present {
380 if let Some(a) = &self.enter_anim {
381 return a.sample_at(0.0).opacity.or(Some(self.enter_from));
382 }
383 return Some(self.enter_from);
384 }
385 let el = Self::wall_since(&st.enter_start);
386 if self
387 .enter_anim
388 .as_ref()
389 .is_some_and(|a| a.opacity.is_none())
390 {
391 return None;
392 }
393 return Some(self.sample_enter_at(el));
394 }
395 if !st.was_present {
396 return None;
397 }
398 let el = Self::wall_since(&st.exit_start);
399 if self.exit_anim.as_ref().is_some_and(|a| a.opacity.is_none()) {
400 return None;
401 }
402 Some(self.sample_exit_at(el))
403 }
404 fn paint_offset(&self) -> Vec2 {
405 let st = *self.state.borrow();
406 if self.present.get_untracked() {
407 if !st.was_present {
408 return self
409 .enter_anim
410 .as_ref()
411 .and_then(|a| a.sample_at(0.0).offset)
412 .unwrap_or(self.slide);
413 }
414 let el = Self::wall_since(&st.enter_start);
415 return self.sample_enter_offset_at(el);
416 }
417 if !st.was_present {
418 return Vec2::ZERO;
419 }
420 let el = Self::wall_since(&st.exit_start);
421 self.sample_exit_offset_at(el)
422 }
423 fn paint_self(&self, ctx: &mut PaintCtx) {
424 let present = self.present.get_untracked();
425 let st = *self.state.borrow();
426 if present {
427 if st.was_present {
428 let el = Self::wall_since(&st.enter_start);
429 if el < self.enter_delay + self.enter_secs {
430 ctx.request_frame();
431 }
432 }
433 return;
434 }
435 if !st.was_present {
436 if !st.exit_fired && self.child.is_some() {
437 self.state.borrow_mut().exit_fired = true;
438 if let Some(f) = &self.on_exit_complete {
439 f();
440 }
441 }
442 return;
443 }
444 let el = Self::wall_since(&st.exit_start);
445 if exit_done(self.exit_delay, self.exit_secs, el) {
446 self.state.borrow_mut().exit_fired = true;
447 if let Some(f) = &self.on_exit_complete {
448 f();
449 }
450 } else {
451 ctx.request_frame();
452 }
453 }
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459
460 #[test]
461 fn exit_walks_one_to_zero() {
462 let p = Presence::new(Signal::new(false));
463 assert_eq!(p.sample_exit_at(-1.0), 1.0, "before delay full");
464 let mid = p.sample_exit_at(0.1);
465 assert!(mid > 0.0 && mid < 1.0, "output middle: {mid}");
466 assert_eq!(p.sample_exit_at(10.0), 0.0);
467 }
468
469 #[test]
470 fn exit_zero_duration_cuts() {
471 let p = Presence::new(Signal::new(false)).exit_duration(0.0);
472 assert_eq!(p.sample_exit_at(0.0), 0.0, "zero time = cut");
473 assert!(exit_done(0.0, 0.0, 0.0));
474 assert!(!exit_done(0.0, 0.2, 0.19));
475 assert!(exit_done(0.0, 0.2, 0.2));
476 }
477
478 #[test]
479 fn exit_delay_holds_full_then_fades() {
480 let p = Presence::new(Signal::new(false))
481 .exit_delay(0.5)
482 .exit_duration(0.2);
483 assert_eq!(p.sample_exit_at(0.0), 1.0);
484 assert_eq!(p.sample_exit_at(0.49), 1.0);
485 assert!(p.sample_exit_at(0.6) < 1.0);
486 assert!(!exit_done(0.5, 0.2, 0.69));
487 assert!(exit_done(0.5, 0.2, 0.7));
488 }
489
490 #[test]
491 fn enter_walks_from_to_one() {
492 let p = Presence::new(Signal::new(true)).from(0.3);
493 assert_eq!(p.sample_enter_at(-1.0), 0.3);
494 assert_eq!(p.sample_enter_at(10.0), 1.0);
495 }
496
497 #[test]
498 fn exit_progress_drives_slide() {
499 assert_eq!(exit_progress(0.0, 0.2, Easing::Linear, 0.0), 0.0);
500 assert_eq!(exit_progress(0.0, 0.2, Easing::Linear, 10.0), 1.0);
501 let mid = exit_progress(0.0, 0.2, Easing::Linear, 0.1);
502 assert!((mid - 0.5).abs() < 1e-4, "linear center: {mid}");
503 }
504
505 #[test]
506 fn reuse_state_keeps_finished_enter() {
507 let old = Presence::new(Signal::new(true)).child(Unit);
508 let _ = old.keyed_children();
509 std::thread::sleep(Duration::from_millis(300));
510 let fresh = Presence::new(Signal::new(true)).child(Unit);
511 fresh.reuse_state(&old);
512 let op = fresh.paint_opacity().unwrap_or(0.0);
513 assert!(op > 0.99, "rebuilding does not restart input: {op}");
514 }
515}