Skip to main content

sequoia_view/
presence.rs

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}