1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
//! Mount transitions and `Collapse`, built on gpui's animation API.
//!
//! [`Transition`] plays a one-shot fade/slide as its child appears;
//! [`Collapse`] reveals gated content — give it the content height and it
//! animates that height open *and* closed (overflow clipped), falling back
//! to a fade when the height is unknown. Both take an [`Easing`], including
//! springs. For exit animations on arbitrary conditionals, see
//! [`Presence`](crate::anim::Presence).
use gpui::prelude::*;
use gpui::{div, px, AnimationExt, AnyElement, App, ElementId, IntoElement, Window};
use crate::anim::Easing;
/// The kind of entrance motion [`Transition`] plays.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TransitionKind {
Fade,
SlideUp,
SlideDown,
SlideLeft,
SlideRight,
}
/// Plays a one-shot entrance animation around its child.
#[derive(IntoElement)]
pub struct Transition {
id: ElementId,
kind: TransitionKind,
easing: Easing,
duration: u64,
child: Option<AnyElement>,
}
impl Transition {
pub fn new(id: impl Into<ElementId>) -> Self {
Transition {
id: id.into(),
kind: TransitionKind::Fade,
easing: Easing::default(),
duration: 200,
child: None,
}
}
pub fn kind(mut self, kind: TransitionKind) -> Self {
self.kind = kind;
self
}
/// Timing curve, including `Easing::Spring(..)`.
pub fn easing(mut self, easing: Easing) -> Self {
self.easing = easing;
self
}
pub fn duration_ms(mut self, duration: u64) -> Self {
self.duration = duration;
self
}
pub fn child(mut self, child: impl IntoElement) -> Self {
self.child = Some(child.into_any_element());
self
}
}
impl RenderOnce for Transition {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let child = self.child.unwrap_or_else(|| div().into_any_element());
let kind = self.kind;
// Linear clock + animator-side curve: overshooting easings (springs)
// return deltas past 1.0, which gpui's easing slot debug-asserts
// against but the animator accepts — margins overshoot and settle,
// opacity clamps to its legal range.
let easing = self.easing;
div()
.child(child)
.with_animation(self.id, easing.clock(self.duration), move |el, t| {
let delta = easing.apply(t);
let opacity = delta.clamp(0.0, 1.0);
let shift = (1.0 - delta) * 8.0;
match kind {
TransitionKind::Fade => el.opacity(opacity),
TransitionKind::SlideUp => el.opacity(opacity).mt(px(shift)),
TransitionKind::SlideDown => el.opacity(opacity).mt(px(-shift)),
TransitionKind::SlideLeft => el.opacity(opacity).ml(px(shift)),
TransitionKind::SlideRight => el.opacity(opacity).ml(px(-shift)),
}
})
}
}
/// Reveals gated content. With a known content `height`, the box height
/// animates open and closed (a real collapse, clipped while moving); without
/// one it fades in and unmounts instantly on close.
#[derive(IntoElement)]
pub struct Collapse {
id: ElementId,
open: bool,
height: Option<f32>,
easing: Easing,
duration: u64,
child: Option<AnyElement>,
}
impl Collapse {
pub fn new(id: impl Into<ElementId>) -> Self {
Collapse {
id: id.into(),
open: false,
height: None,
easing: Easing::default(),
duration: 180,
child: None,
}
}
pub fn open(mut self, open: bool) -> Self {
self.open = open;
self
}
/// The content's height in px. Unlocks the true height animation — the
/// closed state keeps the child mounted at height 0 so it can animate
/// back open.
pub fn height(mut self, height: f32) -> Self {
self.height = Some(height.max(0.0));
self
}
pub fn easing(mut self, easing: Easing) -> Self {
self.easing = easing;
self
}
pub fn duration_ms(mut self, duration: u64) -> Self {
self.duration = duration;
self
}
pub fn child(mut self, child: impl IntoElement) -> Self {
self.child = Some(child.into_any_element());
self
}
}
impl RenderOnce for Collapse {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
// Linear clock + animator-side curve; see `Transition::render`.
let easing = self.easing;
let animation = easing.clock(self.duration);
let Some(height) = self.height else {
// No measured height: fade in on open, vanish on close.
if !self.open {
return div().into_any_element();
}
let child = self.child.unwrap_or_else(|| div().into_any_element());
return div()
.child(child)
.with_animation(self.id, animation, move |el, t| {
el.opacity(easing.apply(t).clamp(0.0, 1.0))
})
.into_any_element();
};
let child = self.child.unwrap_or_else(|| div().into_any_element());
let open = self.open;
// Swapping the animation id replays the animation: one id per
// direction gives a real two-way collapse from stateless renders.
let direction = if open {
"guise-collapse-open"
} else {
"guise-collapse-close"
};
div()
.id(self.id)
.overflow_hidden()
.child(child)
.with_animation(direction, animation, move |el, t| {
let d = if open {
easing.apply(t)
} else {
1.0 - easing.apply(t)
};
// A springy open overshoots the height and settles back;
// opacity and the closing height stay in legal range.
el.h(px(height * d.max(0.0))).opacity(d.clamp(0.0, 1.0))
})
.into_any_element()
}
}