1use std::cell::RefCell;
2use std::rc::Rc;
3use std::time::Duration;
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::runtime::{ElementId, Runtime};
12use crate::view::{PaintCtx, View};
13use crate::views::Unit;
14
15pub const ENTER_SECS: f32 = 0.2;
16pub const EXIT_SECS: f32 = 0.2;
17
18pub fn enter_sample(from: f32, delay: f32, duration: f32, easing: Easing, elapsed: f32) -> f32 {
19 sample_to(from, 1.0, delay, duration, easing, elapsed)
20}
21
22pub fn enter_progress(delay: f32, duration: f32, easing: Easing, elapsed: f32) -> f32 {
23 crate::animated::slide_progress(delay, duration, easing, elapsed)
24}
25
26pub fn exit_sample(delay: f32, duration: f32, easing: Easing, elapsed: f32) -> f32 {
27 let p = exit_progress(delay, duration, easing, elapsed);
28 1.0 - p
29}
30
31pub fn exit_progress(delay: f32, duration: f32, easing: Easing, elapsed: f32) -> f32 {
32 let delay = delay.max(0.0);
33 if elapsed < delay {
34 return 0.0;
35 }
36 if duration <= 0.0 {
37 return 1.0;
38 }
39 let t = (elapsed - delay) / duration;
40 if t <= 0.0 {
41 return 0.0;
42 }
43 if t >= 1.0 {
44 return 1.0;
45 }
46 easing.apply(t)
47}
48
49pub fn exit_done(delay: f32, duration: f32, elapsed: f32) -> bool {
50 elapsed >= delay.max(0.0) + duration.max(0.0)
51}
52
53fn sample_to(from: f32, to: f32, delay: f32, duration: f32, easing: Easing, elapsed: f32) -> f32 {
54 let p = enter_progress(delay, duration, easing, elapsed);
55 from + (to - from) * p
56}
57
58fn lerp_vec(from: Vec2, to: Vec2, t: f32) -> Vec2 {
59 Vec2::new(from.x + (to.x - from.x) * t, from.y + (to.y - from.y) * t)
60}
61
62#[derive(Clone, Copy)]
63struct PresenceState {
64 was_present: bool,
65 enter_start: Option<f32>,
66 exit_start: Option<f32>,
67 exit_fired: bool,
68
69 current_opacity: f32,
70 current_offset: Vec2,
71 current_scale: f32,
72
73 enter_from: f32,
74 enter_offset: Vec2,
75 enter_scale: f32,
76
77 exit_from: f32,
78 exit_offset: Vec2,
79 exit_scale: f32,
80}
81
82impl PresenceState {
83 fn new() -> Self {
84 Self {
85 was_present: false,
86 enter_start: None,
87 exit_start: None,
88 exit_fired: false,
89 current_opacity: 0.0,
90 current_offset: Vec2::ZERO,
91 current_scale: 1.0,
92 enter_from: 0.0,
93 enter_offset: Vec2::ZERO,
94 enter_scale: 1.0,
95 exit_from: 1.0,
96 exit_offset: Vec2::ZERO,
97 exit_scale: 1.0,
98 }
99 }
100}
101
102#[derive(Clone)]
103pub struct Presence {
104 present: Signal<bool>,
105 child: Option<AnyView>,
106 enter_from: f32,
107 enter_to: f32,
108 enter_has_opacity: bool,
109 enter_secs: f32,
110 enter_delay: f32,
111 enter_easing: Easing,
112 exit_to: f32,
113 exit_has_opacity: bool,
114 exit_secs: f32,
115 exit_delay: f32,
116 exit_easing: Easing,
117 enter_offset: Vec2,
118 enter_offset_to: Vec2,
119 exit_offset_to: Vec2,
120 enter_scale: f32,
121 exit_scale: f32,
122 on_exit_complete: Option<Rc<dyn Fn()>>,
123 state: Rc<RefCell<PresenceState>>,
124}
125
126impl Presence {
127 pub fn new(present: Signal<bool>) -> Self {
128 Self {
129 present,
130 child: None,
131 enter_from: 0.0,
132 enter_to: 1.0,
133 enter_has_opacity: true,
134 enter_secs: ENTER_SECS,
135 enter_delay: 0.0,
136 enter_easing: Easing::EaseOutCubic,
137 exit_to: 0.0,
138 exit_has_opacity: true,
139 exit_secs: EXIT_SECS,
140 exit_delay: 0.0,
141 exit_easing: Easing::EaseOutCubic,
142 enter_offset: Vec2::ZERO,
143 enter_offset_to: Vec2::ZERO,
144 exit_offset_to: Vec2::ZERO,
145 enter_scale: 1.0,
146 exit_scale: 1.0,
147 on_exit_complete: None,
148 state: Rc::new(RefCell::new(PresenceState::new())),
149 }
150 }
151
152 pub fn child<V: View + 'static>(mut self, child: V) -> Self {
153 self.child = Some(AnyView::new(child));
154 self
155 }
156
157 pub fn from(mut self, opacity: f32) -> Self {
158 self.enter_from = opacity.clamp(0.0, 1.0);
159 self
160 }
161
162 pub fn enter_duration(mut self, secs: f32) -> Self {
163 self.enter_secs = secs.max(0.0);
164 self
165 }
166
167 pub fn enter_duration_d(mut self, d: Duration) -> Self {
168 self.enter_secs = d.as_secs_f32().max(0.0);
169 self
170 }
171
172 pub fn enter_delay(mut self, secs: f32) -> Self {
173 self.enter_delay = secs.max(0.0);
174 self
175 }
176
177 pub fn enter_easing(mut self, easing: Easing) -> Self {
178 self.enter_easing = easing;
179 self
180 }
181
182 pub fn exit_duration(mut self, secs: f32) -> Self {
183 self.exit_secs = secs.max(0.0);
184 self
185 }
186
187 pub fn exit_duration_d(mut self, d: Duration) -> Self {
188 self.exit_secs = d.as_secs_f32().max(0.0);
189 self
190 }
191
192 pub fn exit_delay(mut self, secs: f32) -> Self {
193 self.exit_delay = secs.max(0.0);
194 self
195 }
196
197 pub fn exit_easing(mut self, easing: Easing) -> Self {
198 self.exit_easing = easing;
199 self
200 }
201
202 pub fn durations(mut self, enter_secs: f32, exit_secs: f32) -> Self {
203 self.enter_secs = enter_secs.max(0.0);
204 self.exit_secs = exit_secs.max(0.0);
205 self
206 }
207
208 pub fn durations_d(mut self, enter: Duration, exit: Duration) -> Self {
209 self.enter_secs = enter.as_secs_f32().max(0.0);
210 self.exit_secs = exit.as_secs_f32().max(0.0);
211 self
212 }
213
214 pub fn easing(mut self, easing: Easing) -> Self {
215 self.enter_easing = easing;
216 self.exit_easing = easing;
217 self
218 }
219
220 pub fn slide(mut self, dx: f32, dy: f32) -> Self {
221 let v = Vec2::new(dx, dy);
222 self.enter_offset = v;
223 self.enter_offset_to = Vec2::ZERO;
224 self.exit_offset_to = v;
225 self
226 }
227
228 pub fn scale(mut self, scale: f32) -> Self {
229 let s = if scale.is_finite() {
230 scale.max(0.0)
231 } else {
232 1.0
233 };
234 self.enter_scale = s;
235 self.exit_scale = s;
236 self
237 }
238
239 pub fn enter(mut self, anim: impl Into<Animate>) -> Self {
240 let a = anim.into();
241 if let Some((from, to)) = a.opacity {
242 self.enter_from = from;
243 self.enter_to = to;
244 self.enter_has_opacity = true;
245 } else {
246 self.enter_has_opacity = false;
247 }
248 self.enter_secs = a.duration_secs;
249 self.enter_delay = a.delay_secs;
250 self.enter_easing = a.easing;
251 if let Some((from, to)) = a.offset {
252 self.enter_offset = from;
253 self.enter_offset_to = to;
254 }
255 self
256 }
257
258 pub fn exit(mut self, anim: impl Into<Animate>) -> Self {
259 let a = anim.into();
260 if let Some((_, to)) = a.opacity {
261 self.exit_to = to;
262 self.exit_has_opacity = true;
263 } else {
264 self.exit_has_opacity = false;
265 }
266 self.exit_secs = a.duration_secs;
267 self.exit_delay = a.delay_secs;
268 self.exit_easing = a.easing;
269 if let Some((_, to)) = a.offset {
270 self.exit_offset_to = to;
271 }
272 self
273 }
274
275 pub fn on_exit_complete(mut self, f: impl Fn() + 'static) -> Self {
276 self.on_exit_complete = Some(Rc::new(f));
277 self
278 }
279
280 pub fn sample_enter_at(&self, elapsed_secs: f32) -> f32 {
281 sample_to(
282 self.enter_from,
283 self.enter_to,
284 self.enter_delay,
285 self.enter_secs,
286 self.enter_easing,
287 elapsed_secs,
288 )
289 }
290
291 pub fn sample_exit_at(&self, elapsed_secs: f32) -> f32 {
292 sample_to(
293 1.0,
294 self.exit_to,
295 self.exit_delay,
296 self.exit_secs,
297 self.exit_easing,
298 elapsed_secs,
299 )
300 }
301
302 pub fn is_mounted(&self) -> bool {
303 if self.child.is_none() {
304 return false;
305 }
306 if self.present.get_untracked() {
307 return true;
308 }
309 let st = *self.state.borrow();
310 st.was_present && !st.exit_fired
311 }
312
313 fn begin_frame(&self, now: f32) {
314 let present = self.present.get_untracked();
315 let mut st = self.state.borrow_mut();
316 if present {
317 let reversing = st.exit_start.is_some();
318 if reversing {
319 st.enter_from = st.current_opacity;
320 st.enter_offset = st.current_offset;
321 st.enter_scale = st.current_scale;
322 } else if !st.was_present || st.enter_start.is_none() {
323 st.enter_from = self.enter_from;
324 st.enter_offset = self.enter_offset;
325 st.enter_scale = self.enter_scale;
326 }
327 st.was_present = true;
328 st.exit_start = None;
329 st.exit_fired = false;
330 if reversing || st.enter_start.is_none() {
331 st.enter_start = Some(now);
332 }
333 } else if st.was_present && st.exit_start.is_none() {
334 st.exit_from = st.current_opacity;
335 st.exit_offset = st.current_offset;
336 st.exit_scale = st.current_scale;
337 st.exit_start = Some(now);
338 st.enter_start = None;
339 st.exit_fired = false;
340 }
341 }
342
343 fn opacity_at(&self, now: f32, reduced: bool) -> Option<f32> {
344 self.begin_frame(now);
345 let present = self.present.get_untracked();
346 let st = *self.state.borrow();
347 if present {
348 if !self.enter_has_opacity {
349 return None;
350 }
351 let value = if reduced {
352 self.enter_to
353 } else {
354 let elapsed = st.enter_start.map(|s| now - s).unwrap_or(0.0);
355 sample_to(
356 st.enter_from,
357 self.enter_to,
358 self.enter_delay,
359 self.enter_secs,
360 self.enter_easing,
361 elapsed,
362 )
363 };
364 self.state.borrow_mut().current_opacity = value;
365 Some(value)
366 } else {
367 if !st.was_present {
368 return None;
369 }
370 if !self.exit_has_opacity {
371 return None;
372 }
373 let value = if reduced {
374 self.exit_to
375 } else {
376 let elapsed = st.exit_start.map(|s| now - s).unwrap_or(0.0);
377 sample_to(
378 st.exit_from,
379 self.exit_to,
380 self.exit_delay,
381 self.exit_secs,
382 self.exit_easing,
383 elapsed,
384 )
385 };
386 self.state.borrow_mut().current_opacity = value;
387 Some(value)
388 }
389 }
390
391 fn offset_at(&self, now: f32, reduced: bool) -> Vec2 {
392 self.begin_frame(now);
393 let present = self.present.get_untracked();
394 let st = *self.state.borrow();
395 if present {
396 let value = if reduced {
397 self.enter_offset_to
398 } else {
399 let elapsed = st.enter_start.map(|s| now - s).unwrap_or(0.0);
400 let p = enter_progress(
401 self.enter_delay,
402 self.enter_secs,
403 self.enter_easing,
404 elapsed,
405 );
406 lerp_vec(st.enter_offset, self.enter_offset_to, p)
407 };
408 self.state.borrow_mut().current_offset = value;
409 value
410 } else {
411 if !st.was_present {
412 return self.exit_offset_to;
413 }
414 let value = if reduced {
415 self.exit_offset_to
416 } else {
417 let elapsed = st.exit_start.map(|s| now - s).unwrap_or(0.0);
418 let p = exit_progress(self.exit_delay, self.exit_secs, self.exit_easing, elapsed);
419 lerp_vec(st.exit_offset, self.exit_offset_to, p)
420 };
421 self.state.borrow_mut().current_offset = value;
422 value
423 }
424 }
425
426 fn scale_at(&self, now: f32, reduced: bool) -> f32 {
427 self.begin_frame(now);
428 let present = self.present.get_untracked();
429 let st = *self.state.borrow();
430 if present {
431 let value = if reduced {
432 1.0
433 } else {
434 let elapsed = st.enter_start.map(|s| now - s).unwrap_or(0.0);
435 let p = enter_progress(
436 self.enter_delay,
437 self.enter_secs,
438 self.enter_easing,
439 elapsed,
440 );
441 st.enter_scale + (1.0 - st.enter_scale) * p
442 };
443 self.state.borrow_mut().current_scale = value;
444 value
445 } else {
446 if !st.was_present {
447 return self.exit_scale;
448 }
449 let value = if reduced {
450 self.exit_scale
451 } else {
452 let elapsed = st.exit_start.map(|s| now - s).unwrap_or(0.0);
453 let p = exit_progress(self.exit_delay, self.exit_secs, self.exit_easing, elapsed);
454 st.exit_scale + (self.exit_scale - st.exit_scale) * p
455 };
456 self.state.borrow_mut().current_scale = value;
457 value
458 }
459 }
460}
461
462impl View for Presence {
463 fn body(&self) -> impl View + 'static {
464 Unit
465 }
466
467 fn reuse_state(&self, old: &Self) {
468 if Rc::ptr_eq(&self.state, &old.state) {
469 return;
470 }
471 let prev = *old.state.borrow();
472 *self.state.borrow_mut() = prev;
473 }
474
475 fn keyed_children(&self) -> Option<Vec<(u64, AnyView)>> {
476 let present = self.present.get();
477 let Some(child) = self.child.clone() else {
478 return Some(Vec::new());
479 };
480 let mut st = self.state.borrow_mut();
481 if present {
482 if !st.was_present {
483 st.was_present = true;
484 st.enter_start = None;
485 st.exit_start = None;
486 st.exit_fired = false;
487 }
488 Some(vec![(0, child)])
489 } else if st.was_present && !st.exit_fired {
490 Some(vec![(0, child)])
491 } else {
492 st.was_present = false;
493 st.enter_start = None;
494 st.exit_start = None;
495 Some(Vec::new())
496 }
497 }
498
499 fn layout_style(&self) -> LayoutStyle {
500 LayoutStyle {
501 width: Length::Fit,
502 height: Length::Fit,
503 ..LayoutStyle::default()
504 }
505 }
506
507 fn paint_opacity_ctx(&self, rt: &Runtime, _el: ElementId) -> Option<f32> {
508 self.opacity_at(rt.elapsed_secs(), rt.reduced_motion())
509 }
510
511 fn paint_offset_ctx(&self, rt: &Runtime, _el: ElementId) -> Vec2 {
512 self.offset_at(rt.elapsed_secs(), rt.reduced_motion())
513 }
514
515 fn paint_scale_ctx(&self, rt: &Runtime, _el: ElementId) -> f32 {
516 self.scale_at(rt.elapsed_secs(), rt.reduced_motion())
517 }
518
519 fn paint_self(&self, ctx: &mut PaintCtx) {
520 let now = ctx.runtime().elapsed_secs();
521 let reduced = ctx.runtime().reduced_motion();
522 self.begin_frame(now);
523 let present = self.present.get_untracked();
524 if present {
525 let st = *self.state.borrow();
526 let elapsed = st.enter_start.map(|s| now - s).unwrap_or(0.0);
527 if !reduced && elapsed < self.enter_delay + self.enter_secs {
528 ctx.request_frame();
529 }
530 return;
531 }
532
533 let st = *self.state.borrow();
534 if !st.was_present {
535 return;
536 }
537 let elapsed = st.exit_start.map(|s| now - s).unwrap_or(0.0);
538 if reduced || exit_done(self.exit_delay, self.exit_secs, elapsed) {
539 let just_fired = {
540 let mut st = self.state.borrow_mut();
541 if st.exit_fired {
542 false
543 } else {
544 st.exit_fired = true;
545 st.was_present = false;
546 st.enter_start = None;
547 st.exit_start = None;
548 st.current_opacity = self.exit_to;
549 st.current_offset = self.exit_offset_to;
550 st.current_scale = self.exit_scale;
551 true
552 }
553 };
554 if just_fired {
555 ctx.runtime().request_rebuild(ctx.element);
556 if let Some(f) = &self.on_exit_complete {
557 f();
558 }
559 }
560 } else {
561 ctx.request_frame();
562 }
563 }
564}
565
566#[cfg(test)]
567mod tests {
568 use super::*;
569
570 #[test]
571 fn exit_walks_one_to_zero() {
572 let p = Presence::new(Signal::new(false));
573 assert_eq!(p.sample_exit_at(-1.0), 1.0, "before delay full");
574 let mid = p.sample_exit_at(0.1);
575 assert!(mid > 0.0 && mid < 1.0, "output middle: {mid}");
576 assert_eq!(p.sample_exit_at(10.0), 0.0);
577 }
578
579 #[test]
580 fn exit_zero_duration_cuts() {
581 let p = Presence::new(Signal::new(false)).exit_duration(0.0);
582 assert_eq!(p.sample_exit_at(0.0), 0.0, "zero time = cut");
583 assert!(exit_done(0.0, 0.0, 0.0));
584 assert!(!exit_done(0.0, 0.2, 0.19));
585 assert!(exit_done(0.0, 0.2, 0.2));
586 }
587
588 #[test]
589 fn exit_delay_holds_full_then_fades() {
590 let p = Presence::new(Signal::new(false))
591 .exit_delay(0.5)
592 .exit_duration(0.2);
593 assert_eq!(p.sample_exit_at(0.0), 1.0);
594 assert_eq!(p.sample_exit_at(0.49), 1.0);
595 assert!(p.sample_exit_at(0.6) < 1.0);
596 assert!(!exit_done(0.5, 0.2, 0.69));
597 assert!(exit_done(0.5, 0.2, 0.7));
598 }
599
600 #[test]
601 fn enter_walks_from_to_one() {
602 let p = Presence::new(Signal::new(true)).from(0.3);
603 assert_eq!(p.sample_enter_at(-1.0), 0.3);
604 assert_eq!(p.sample_enter_at(10.0), 1.0);
605 }
606
607 #[test]
608 fn exit_progress_drives_slide() {
609 assert_eq!(exit_progress(0.0, 0.2, Easing::Linear, 0.0), 0.0);
610 assert_eq!(exit_progress(0.0, 0.2, Easing::Linear, 10.0), 1.0);
611 let mid = exit_progress(0.0, 0.2, Easing::Linear, 0.1);
612 assert!((mid - 0.5).abs() < 1e-4, "linear center: {mid}");
613 }
614
615 #[test]
616 fn reuse_state_keeps_finished_enter() {
617 let old = Presence::new(Signal::new(true)).child(Unit);
618 old.begin_frame(0.0);
619 old.state.borrow_mut().enter_start = Some(0.0);
620 let fresh = Presence::new(Signal::new(true)).child(Unit);
621 fresh.reuse_state(&old);
622 let op = fresh.opacity_at(10.0, false).unwrap_or(0.0);
623 assert!(op > 0.99, "rebuilding does not restart input: {op}");
624 }
625
626 #[test]
627 fn reversing_mid_enter_starts_from_current_opacity() {
628 let p = Presence::new(Signal::new(true))
629 .child(Unit)
630 .durations(1.0, 1.0)
631 .easing(Easing::Linear);
632 p.begin_frame(0.0);
633 p.state.borrow_mut().enter_start = Some(0.0);
634 let mid = p.opacity_at(0.5, false).unwrap();
635 assert!((mid - 0.5).abs() < 0.1, "mid enter: {mid}");
636
637 p.present.set(false);
638 p.begin_frame(0.5);
639 let first = p.opacity_at(0.5, false).unwrap();
640 assert!(
641 (first - mid).abs() < 1e-4,
642 "exit starts from the current value, not 1.0: {first} vs {mid}"
643 );
644 }
645}