Skip to repository content387 lines · 12.1 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T06:49:23.857Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
animation.rs
1use scheduler::Instant;
2use std::{rc::Rc, time::Duration};
3
4use crate::{
5 AnyElement, App, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement,
6 ParentElement, Window,
7};
8
9pub use easing::*;
10use smallvec::SmallVec;
11
12/// An animation that can be applied to an element.
13#[derive(Clone)]
14pub struct Animation {
15 /// The amount of time for which this animation should run
16 pub duration: Duration,
17 /// Whether to repeat this animation when it finishes
18 pub oneshot: bool,
19 /// A function that takes a delta between 0 and 1 and returns a new delta
20 /// between 0 and 1 based on the given easing function.
21 pub easing: Rc<dyn Fn(f32) -> f32>,
22}
23
24impl Animation {
25 /// Create a new animation with the given duration.
26 /// By default the animation will only run once and will use a linear easing function.
27 pub fn new(duration: Duration) -> Self {
28 Self {
29 duration,
30 oneshot: true,
31 easing: Rc::new(linear),
32 }
33 }
34
35 /// Set the animation to loop when it finishes.
36 pub fn repeat(mut self) -> Self {
37 self.oneshot = false;
38 self
39 }
40
41 /// Set the easing function to use for this animation.
42 /// The easing function will take a time delta between 0 and 1 and return a new delta
43 /// between 0 and 1
44 pub fn with_easing(mut self, easing: impl Fn(f32) -> f32 + 'static) -> Self {
45 self.easing = Rc::new(easing);
46 self
47 }
48}
49
50/// An extension trait for adding the animation wrapper to both Elements and Components
51///
52/// Animations rendered through this trait automatically respect
53/// [`App::reduce_motion`](crate::App::reduce_motion): when it is set,
54/// the element is rendered in a static state (the end state for oneshot
55/// animations, the start state for repeating ones) and no animation frames are
56/// scheduled.
57pub trait AnimationExt {
58 /// Render this component or element with an animation
59 fn with_animation(
60 self,
61 id: impl Into<ElementId>,
62 animation: Animation,
63 animator: impl Fn(Self, f32) -> Self + 'static,
64 ) -> AnimationElement<Self>
65 where
66 Self: Sized,
67 {
68 AnimationElement {
69 id: id.into(),
70 element: Some(self),
71 animator: Box::new(move |this, _, value| animator(this, value)),
72 animations: smallvec::smallvec![animation],
73 }
74 }
75
76 /// Render this component or element with a chain of animations
77 fn with_animations(
78 self,
79 id: impl Into<ElementId>,
80 animations: Vec<Animation>,
81 animator: impl Fn(Self, usize, f32) -> Self + 'static,
82 ) -> AnimationElement<Self>
83 where
84 Self: Sized,
85 {
86 AnimationElement {
87 id: id.into(),
88 element: Some(self),
89 animator: Box::new(animator),
90 animations: animations.into(),
91 }
92 }
93}
94
95impl<E: IntoElement + 'static> AnimationExt for E {}
96
97/// A GPUI element that applies an animation to another element
98pub struct AnimationElement<E> {
99 id: ElementId,
100 element: Option<E>,
101 animations: SmallVec<[Animation; 1]>,
102 animator: Box<dyn Fn(E, usize, f32) -> E + 'static>,
103}
104
105impl<E: ParentElement> ParentElement for AnimationElement<E> {
106 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
107 let Some(element) = &mut self.element else {
108 return;
109 };
110
111 element.extend(elements);
112 }
113}
114
115impl<E> AnimationElement<E> {
116 /// Returns a new [`AnimationElement<E>`] after applying the given function
117 /// to the element being animated.
118 pub fn map_element(mut self, f: impl FnOnce(E) -> E) -> AnimationElement<E> {
119 self.element = self.element.map(f);
120 self
121 }
122}
123
124impl<E: IntoElement + 'static> IntoElement for AnimationElement<E> {
125 type Element = AnimationElement<E>;
126
127 fn into_element(self) -> Self::Element {
128 self
129 }
130}
131
132struct AnimationState {
133 start: Instant,
134 animation_ix: usize,
135}
136
137impl<E: IntoElement + 'static> Element for AnimationElement<E> {
138 type RequestLayoutState = AnyElement;
139 type PrepaintState = ();
140
141 fn id(&self) -> Option<ElementId> {
142 Some(self.id.clone())
143 }
144
145 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
146 None
147 }
148
149 fn request_layout(
150 &mut self,
151 global_id: Option<&GlobalElementId>,
152 _inspector_id: Option<&InspectorElementId>,
153 window: &mut Window,
154 cx: &mut App,
155 ) -> (crate::LayoutId, Self::RequestLayoutState) {
156 window.with_element_state(global_id.unwrap(), |state, window| {
157 let mut state = state.unwrap_or_else(|| AnimationState {
158 start: Instant::now(),
159 animation_ix: 0,
160 });
161 let (animation_ix, delta, done) = if cx.reduce_motion() {
162 let animation_ix = self.animations.len() - 1;
163 let delta = if self.animations[animation_ix].oneshot {
164 1.0
165 } else {
166 0.0
167 };
168 (animation_ix, delta, true)
169 } else {
170 let animation_ix = state.animation_ix;
171
172 let mut delta = state.start.elapsed().as_secs_f32()
173 / self.animations[animation_ix].duration.as_secs_f32();
174
175 let mut done = false;
176 if delta > 1.0 {
177 if self.animations[animation_ix].oneshot {
178 if animation_ix >= self.animations.len() - 1 {
179 done = true;
180 } else {
181 state.start = Instant::now();
182 state.animation_ix += 1;
183 }
184 delta = 1.0;
185 } else {
186 delta %= 1.0;
187 }
188 }
189 (animation_ix, delta, done)
190 };
191 let delta = (self.animations[animation_ix].easing)(delta);
192
193 debug_assert!(
194 (0.0..=1.0).contains(&delta),
195 "delta should always be between 0 and 1"
196 );
197
198 let element = self.element.take().expect("should only be called once");
199 let mut element = (self.animator)(element, animation_ix, delta).into_any_element();
200
201 if !done {
202 window.request_animation_frame();
203 }
204
205 ((element.request_layout(window, cx), element), state)
206 })
207 }
208
209 fn prepaint(
210 &mut self,
211 _id: Option<&GlobalElementId>,
212 _inspector_id: Option<&InspectorElementId>,
213 _bounds: crate::Bounds<crate::Pixels>,
214 element: &mut Self::RequestLayoutState,
215 window: &mut Window,
216 cx: &mut App,
217 ) -> Self::PrepaintState {
218 element.prepaint(window, cx);
219 }
220
221 fn paint(
222 &mut self,
223 _id: Option<&GlobalElementId>,
224 _inspector_id: Option<&InspectorElementId>,
225 _bounds: crate::Bounds<crate::Pixels>,
226 element: &mut Self::RequestLayoutState,
227 _: &mut Self::PrepaintState,
228 window: &mut Window,
229 cx: &mut App,
230 ) {
231 element.paint(window, cx);
232 }
233}
234
235mod easing {
236 use std::f32::consts::PI;
237
238 /// The linear easing function, or delta itself
239 pub fn linear(delta: f32) -> f32 {
240 delta
241 }
242
243 /// The quadratic easing function, delta * delta
244 pub fn quadratic(delta: f32) -> f32 {
245 delta * delta
246 }
247
248 /// The quadratic ease-in-out function, which starts and ends slowly but speeds up in the middle
249 pub fn ease_in_out(delta: f32) -> f32 {
250 if delta < 0.5 {
251 2.0 * delta * delta
252 } else {
253 let x = -2.0 * delta + 2.0;
254 1.0 - x * x / 2.0
255 }
256 }
257
258 /// The Quint ease-out function, which starts quickly and decelerates to a stop
259 pub fn ease_out_quint() -> impl Fn(f32) -> f32 {
260 move |delta| 1.0 - (1.0 - delta).powi(5)
261 }
262
263 /// Apply the given easing function, first in the forward direction and then in the reverse direction
264 pub fn bounce(easing: impl Fn(f32) -> f32) -> impl Fn(f32) -> f32 {
265 move |delta| {
266 if delta < 0.5 {
267 easing(delta * 2.0)
268 } else {
269 easing((1.0 - delta) * 2.0)
270 }
271 }
272 }
273
274 /// A custom easing function for pulsating alpha that slows down as it approaches 0.1
275 pub fn pulsating_between(min: f32, max: f32) -> impl Fn(f32) -> f32 {
276 let range = max - min;
277
278 move |delta| {
279 // Use a combination of sine and cubic functions for a more natural breathing rhythm
280 let t = (delta * 2.0 * PI).sin();
281 let breath = (t * t * t + t) / 2.0;
282
283 // Map the breath to our desired alpha range
284 let normalized_alpha = (breath + 1.0) / 2.0;
285
286 min + (normalized_alpha * range)
287 }
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use std::{cell::RefCell, rc::Rc, time::Duration};
294
295 use crate::{
296 Animation, Context, InteractiveElement, Render, TestAppContext, WindowHandle, div,
297 prelude::*, px, size,
298 };
299
300 use super::*;
301
302 struct AnimationTestView {
303 rendered_deltas: Rc<RefCell<Vec<f32>>>,
304 }
305
306 impl Render for AnimationTestView {
307 fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
308 let rendered_deltas = self.rendered_deltas.clone();
309 div().size_full().child(div().with_animation(
310 "repeating-animation",
311 Animation::new(Duration::from_secs(1)).repeat(),
312 move |this, delta| {
313 rendered_deltas.borrow_mut().push(delta);
314 this
315 },
316 ))
317 }
318 }
319
320 fn open_test_window(
321 cx: &mut TestAppContext,
322 ) -> (Rc<RefCell<Vec<f32>>>, WindowHandle<AnimationTestView>) {
323 let rendered_deltas = Rc::new(RefCell::new(Vec::new()));
324 let window = cx.open_window(size(px(100.), px(100.)), {
325 let rendered_deltas = rendered_deltas.clone();
326 move |_, _| AnimationTestView { rendered_deltas }
327 });
328 cx.run_until_parked();
329 (rendered_deltas, window)
330 }
331
332 fn simulate_next_frame(
333 window: &WindowHandle<AnimationTestView>,
334 cx: &mut TestAppContext,
335 ) -> usize {
336 let callback_count = window
337 .update(cx, |_, window, cx| window.simulate_next_frame(cx))
338 .unwrap();
339 cx.run_until_parked();
340 callback_count
341 }
342 // Before parent-animation-element, using .with_animation
343 // would not allow chaining .parent after. This is just a
344 // build check that we can call div().id().with_animation().child()
345 #[test]
346 fn test_animation_parent() {
347 div()
348 .id("id")
349 //
350 .with_animation(
351 "animation",
352 Animation::new(Duration::from_secs(1)),
353 |el, _t| {
354 //
355 el
356 },
357 )
358 .child(
359 //
360 div(),
361 );
362 }
363
364 #[gpui::test]
365 fn test_repeating_animation_schedules_animation_frames(cx: &mut TestAppContext) {
366 let (rendered_deltas, window) = open_test_window(cx);
367
368 assert_eq!(rendered_deltas.borrow().len(), 1);
369
370 for expected_frames in 2..=3 {
371 assert_eq!(simulate_next_frame(&window, cx), 1);
372 assert_eq!(rendered_deltas.borrow().len(), expected_frames);
373 }
374 }
375
376 #[gpui::test]
377 fn test_reduce_motion_renders_single_static_frame(cx: &mut TestAppContext) {
378 cx.update(|cx| cx.set_reduce_motion(true));
379 let (rendered_deltas, window) = open_test_window(cx);
380
381 assert_eq!(*rendered_deltas.borrow(), vec![0.0]);
382
383 assert_eq!(simulate_next_frame(&window, cx), 0);
384 assert_eq!(*rendered_deltas.borrow(), vec![0.0]);
385 }
386}
387