Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T06:49:23.857Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

animation.rs

387 lines · 12.1 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.