Skip to main content

guise/anim/
presence.rs

1//! `Presence` — mount/unmount with both enter *and* exit animations.
2//!
3//! Stateless conditionals (`if open { modal }`) can't animate out: the
4//! element is gone the frame `open` flips. `Presence` latches it — closing
5//! plays the exit animation, then emits [`PresenceEvent::Hidden`] and stops
6//! rendering. Wrap any overlay or conditional chunk whose exit should be
7//! seen.
8
9use std::rc::Rc;
10use std::time::Duration;
11
12use gpui::prelude::*;
13use gpui::{div, px, AnimationExt, AnyElement, App, Context, EventEmitter, IntoElement, Window};
14
15use super::Easing;
16use crate::devtools::ProbedAny;
17use crate::transition::TransitionKind;
18
19/// Emitted at the ends of the enter/exit animations.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum PresenceEvent {
22  Shown,
23  Hidden,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27enum Phase {
28  Closed,
29  Open,
30  Closing,
31}
32
33type ContentBuilder = Rc<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>;
34
35/// An animated mount/unmount gate. Create with `cx.new(...)`, give it a
36/// content builder, drive it with [`Presence::set_open`].
37pub struct Presence {
38  phase: Phase,
39  kind: TransitionKind,
40  easing: Easing,
41  duration_ms: u64,
42  content: Option<ContentBuilder>,
43  /// Bumped on every open/close so each run gets a fresh animation id and
44  /// stale close timers know to give up.
45  epoch: usize,
46}
47
48impl EventEmitter<PresenceEvent> for Presence {}
49
50impl Presence {
51  pub fn new(_cx: &mut Context<Self>) -> Self {
52    Presence {
53      phase: Phase::Closed,
54      kind: TransitionKind::Fade,
55      easing: Easing::default(),
56      duration_ms: 180,
57      content: None,
58      epoch: 0,
59    }
60  }
61
62  /// The element to gate. Re-invoked every frame while visible, so content
63  /// shows live data.
64  pub fn content(
65    mut self,
66    builder: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
67  ) -> Self {
68    self.content = Some(Rc::new(builder));
69    self
70  }
71
72  pub fn kind(mut self, kind: TransitionKind) -> Self {
73    self.kind = kind;
74    self
75  }
76
77  pub fn easing(mut self, easing: Easing) -> Self {
78    self.easing = easing;
79    self
80  }
81
82  pub fn duration_ms(mut self, duration: u64) -> Self {
83    self.duration_ms = duration.max(1);
84    self
85  }
86
87  /// Whether the gate is logically open (still true while closing).
88  pub fn is_open(&self) -> bool {
89    self.phase != Phase::Closed
90  }
91
92  pub fn set_open(&mut self, open: bool, cx: &mut Context<Self>) {
93    match (open, self.phase) {
94      (true, Phase::Closed) | (true, Phase::Closing) => {
95        self.phase = Phase::Open;
96        self.epoch += 1;
97        cx.emit(PresenceEvent::Shown);
98        cx.notify();
99      }
100      (false, Phase::Open) => {
101        self.phase = Phase::Closing;
102        self.epoch += 1;
103        let epoch = self.epoch;
104        let wait = Duration::from_millis(self.duration_ms);
105        cx.spawn(async move |this, cx| {
106          cx.background_executor().timer(wait).await;
107          this
108            .update(cx, |presence, cx| {
109              if presence.epoch == epoch && presence.phase == Phase::Closing {
110                presence.phase = Phase::Closed;
111                cx.emit(PresenceEvent::Hidden);
112                cx.notify();
113              }
114            })
115            .ok();
116        })
117        .detach();
118        cx.notify();
119      }
120      _ => {}
121    }
122  }
123
124  pub fn toggle(&mut self, cx: &mut Context<Self>) {
125    self.set_open(self.phase == Phase::Closed, cx);
126  }
127}
128
129impl Render for Presence {
130  fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
131    if self.phase == Phase::Closed {
132      return div().into_any_element();
133    }
134    let Some(builder) = self.content.clone() else {
135      return div().into_any_element();
136    };
137    let child = builder(window, cx);
138    let kind = self.kind;
139    let entering = self.phase == Phase::Open;
140    // Linear clock + animator-side curve, so overshooting easings
141    // (springs) never pass through gpui's 0..=1-asserted easing slot;
142    // see `Transition::render`.
143    let easing = self.easing;
144    let animation = easing.clock(self.duration_ms);
145    let id = (
146      if entering {
147        "guise-presence-in"
148      } else {
149        "guise-presence-out"
150      },
151      self.epoch,
152    );
153
154    div()
155      .child(child)
156      .with_animation(id, animation, move |el, t| {
157        // Exit runs the same curve toward zero visibility.
158        let delta = easing.apply(t);
159        let d = if entering { delta } else { 1.0 - delta };
160        let opacity = d.clamp(0.0, 1.0);
161        let shift = (1.0 - d) * 8.0;
162        match kind {
163          TransitionKind::Fade => el.opacity(opacity),
164          TransitionKind::SlideUp => el.opacity(opacity).mt(px(shift)),
165          TransitionKind::SlideDown => el.opacity(opacity).mt(px(-shift)),
166          TransitionKind::SlideLeft => el.opacity(opacity).ml(px(shift)),
167          TransitionKind::SlideRight => el.opacity(opacity).ml(px(-shift)),
168        }
169      })
170      .into_any_element()
171      .probe_any("Presence")
172      .into_any_element()
173  }
174}