Skip to main content

sequoia_view/
presence.rs

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}