1use crate::prelude::*;
2use gpui::{AnimationElement, AnimationExt, Styled};
3use std::time::Duration;
4
5use gpui::ease_out_quint;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub enum AnimationDuration {
9 Instant = 50,
10 Fast = 150,
11 Slow = 300,
12}
13
14impl AnimationDuration {
15 pub fn duration(&self) -> Duration {
16 Duration::from_millis(*self as u64)
17 }
18}
19
20impl Into<std::time::Duration> for AnimationDuration {
21 fn into(self) -> Duration {
22 self.duration()
23 }
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub enum AnimationDirection {
28 FromBottom,
29 FromLeft,
30 FromRight,
31 FromTop,
32}
33
34pub trait DefaultAnimations: Styled + Sized + Element {
35 fn animate_in(
36 self,
37 animation_type: AnimationDirection,
38 fade_in: bool,
39 ) -> AnimationElement<Self> {
40 let animation_name = match animation_type {
41 AnimationDirection::FromBottom => "animate_from_bottom",
42 AnimationDirection::FromLeft => "animate_from_left",
43 AnimationDirection::FromRight => "animate_from_right",
44 AnimationDirection::FromTop => "animate_from_top",
45 };
46
47 let animation_id = self.id().map_or_else(
48 || ElementId::from(animation_name),
49 |id| (id, animation_name).into(),
50 );
51
52 self.with_animation(
53 animation_id,
54 gpui::Animation::new(AnimationDuration::Fast.into()).with_easing(ease_out_quint()),
55 move |mut this, delta| {
56 let start_opacity = 0.4;
57 let start_pos = 0.0;
58 let end_pos = 40.0;
59
60 if fade_in {
61 this = this.opacity(start_opacity + delta * (1.0 - start_opacity));
62 }
63
64 match animation_type {
65 AnimationDirection::FromBottom => {
66 this.bottom(px(start_pos + delta * (end_pos - start_pos)))
67 }
68 AnimationDirection::FromLeft => {
69 this.left(px(start_pos + delta * (end_pos - start_pos)))
70 }
71 AnimationDirection::FromRight => {
72 this.right(px(start_pos + delta * (end_pos - start_pos)))
73 }
74 AnimationDirection::FromTop => {
75 this.top(px(start_pos + delta * (end_pos - start_pos)))
76 }
77 }
78 },
79 )
80 }
81
82 fn animate_in_from_bottom(self, fade: bool) -> AnimationElement<Self> {
83 self.animate_in(AnimationDirection::FromBottom, fade)
84 }
85
86 fn animate_in_from_left(self, fade: bool) -> AnimationElement<Self> {
87 self.animate_in(AnimationDirection::FromLeft, fade)
88 }
89
90 fn animate_in_from_right(self, fade: bool) -> AnimationElement<Self> {
91 self.animate_in(AnimationDirection::FromRight, fade)
92 }
93
94 fn animate_in_from_top(self, fade: bool) -> AnimationElement<Self> {
95 self.animate_in(AnimationDirection::FromTop, fade)
96 }
97}
98
99impl<E: Styled + Element> DefaultAnimations for E {}
100
101// Don't use this directly, it only exists to show animation previews
102#[derive(RegisterComponent)]
103struct Animation {}
104
105impl Component for Animation {
106 fn scope() -> ComponentScope {
107 ComponentScope::Utilities
108 }
109
110 fn description() -> Option<&'static str> {
111 Some("Demonstrates various animation patterns and transitions available in the UI system.")
112 }
113
114 fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
115 let container_size = 128.0;
116 let element_size = 32.0;
117 let offset = container_size / 2.0 - element_size / 2.0;
118
119 let container = || {
120 h_flex()
121 .relative()
122 .justify_center()
123 .bg(cx.theme().colors().text.opacity(0.05))
124 .border_1()
125 .border_color(cx.theme().colors().border)
126 .rounded_sm()
127 };
128
129 Some(
130 v_flex()
131 .gap_6()
132 .children(vec![
133 example_group_with_title(
134 "Animate In",
135 vec![
136 single_example(
137 "From Bottom",
138 container()
139 .size(px(container_size))
140 .child(
141 div()
142 .id("animate-in-from-bottom")
143 .absolute()
144 .size(px(element_size))
145 .left(px(offset))
146 .rounded_md()
147 .bg(gpui::red())
148 .animate_in_from_bottom(false),
149 )
150 .into_any_element(),
151 ),
152 single_example(
153 "From Top",
154 container()
155 .size(px(container_size))
156 .child(
157 div()
158 .id("animate-in-from-top")
159 .absolute()
160 .size(px(element_size))
161 .left(px(offset))
162 .rounded_md()
163 .bg(gpui::blue())
164 .animate_in_from_top(false),
165 )
166 .into_any_element(),
167 ),
168 single_example(
169 "From Left",
170 container()
171 .size(px(container_size))
172 .child(
173 div()
174 .id("animate-in-from-left")
175 .absolute()
176 .size(px(element_size))
177 .top(px(offset))
178 .rounded_md()
179 .bg(gpui::green())
180 .animate_in_from_left(false),
181 )
182 .into_any_element(),
183 ),
184 single_example(
185 "From Right",
186 container()
187 .size(px(container_size))
188 .child(
189 div()
190 .id("animate-in-from-right")
191 .absolute()
192 .size(px(element_size))
193 .top(px(offset))
194 .rounded_md()
195 .bg(gpui::yellow())
196 .animate_in_from_right(false),
197 )
198 .into_any_element(),
199 ),
200 ],
201 )
202 .grow(),
203 example_group_with_title(
204 "Fade and Animate In",
205 vec![
206 single_example(
207 "From Bottom",
208 container()
209 .size(px(container_size))
210 .child(
211 div()
212 .id("fade-animate-in-from-bottom")
213 .absolute()
214 .size(px(element_size))
215 .left(px(offset))
216 .rounded_md()
217 .bg(gpui::red())
218 .animate_in_from_bottom(true),
219 )
220 .into_any_element(),
221 ),
222 single_example(
223 "From Top",
224 container()
225 .size(px(container_size))
226 .child(
227 div()
228 .id("fade-animate-in-from-top")
229 .absolute()
230 .size(px(element_size))
231 .left(px(offset))
232 .rounded_md()
233 .bg(gpui::blue())
234 .animate_in_from_top(true),
235 )
236 .into_any_element(),
237 ),
238 single_example(
239 "From Left",
240 container()
241 .size(px(container_size))
242 .child(
243 div()
244 .id("fade-animate-in-from-left")
245 .absolute()
246 .size(px(element_size))
247 .top(px(offset))
248 .rounded_md()
249 .bg(gpui::green())
250 .animate_in_from_left(true),
251 )
252 .into_any_element(),
253 ),
254 single_example(
255 "From Right",
256 container()
257 .size(px(container_size))
258 .child(
259 div()
260 .id("fade-animate-in-from-right")
261 .absolute()
262 .size(px(element_size))
263 .top(px(offset))
264 .rounded_md()
265 .bg(gpui::yellow())
266 .animate_in_from_right(true),
267 )
268 .into_any_element(),
269 ),
270 ],
271 )
272 .grow(),
273 ])
274 .into_any_element(),
275 )
276 }
277}