gpui_animation/
animation.rs

1use std::{collections::HashMap, rc::Rc, sync::Arc, time::Duration};
2
3use gpui::{prelude::FluentBuilder, *};
4
5use crate::{
6    interpolate::State,
7    transition::{IntoArcTransition, Transition, TransitionRegistry, general::Linear},
8};
9
10#[derive(Debug, Clone, Hash, PartialEq, Eq)]
11pub enum Event {
12    NONE,
13    HOVER,
14    CLICK,
15}
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
18pub enum AnimationPriority {
19    Lowest = 0,
20    Low = 25,
21    Medium = 50,
22    High = 75,
23    Realtime = 100,
24}
25
26#[derive(IntoElement)]
27pub struct AnimatedWrapper<E>
28where
29    E: IntoElement + StatefulInteractiveElement + ParentElement + FluentBuilder + Styled + 'static,
30{
31    style: StyleRefinement,
32    children: Vec<AnyElement>,
33    id: ElementId,
34    child: E,
35    transitions: HashMap<Event, (Duration, Arc<dyn Transition>)>,
36    on_hover: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
37    hover_modifier: Option<Rc<dyn Fn(&bool, State<StyleRefinement>) -> State<StyleRefinement>>>,
38    on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
39    click_modifier:
40        Option<Rc<dyn Fn(&ClickEvent, State<StyleRefinement>) -> State<StyleRefinement>>>,
41}
42
43impl<E: IntoElement + StatefulInteractiveElement + ParentElement + FluentBuilder + Styled + 'static>
44    AnimatedWrapper<E>
45{
46    pub fn transition_on_hover<T, I>(
47        mut self,
48        duration: Duration,
49        transition: I,
50        modifier: impl Fn(&bool, State<StyleRefinement>) -> State<StyleRefinement> + 'static,
51    ) -> Self
52    where
53        T: Transition + 'static,
54        I: IntoArcTransition<T>,
55    {
56        self.transitions
57            .insert(Event::HOVER, (duration, transition.into_arc()));
58        self.hover_modifier = Some(Rc::new(modifier));
59
60        self
61    }
62
63    pub fn transition_on_click<T, I>(
64        mut self,
65        duration: Duration,
66        transition: I,
67        modifier: impl Fn(&ClickEvent, State<StyleRefinement>) -> State<StyleRefinement> + 'static,
68    ) -> Self
69    where
70        T: Transition + 'static,
71        I: IntoArcTransition<T>,
72    {
73        self.transitions
74            .insert(Event::CLICK, (duration, transition.into_arc()));
75        self.click_modifier = Some(Rc::new(modifier));
76
77        self
78    }
79
80    pub fn on_hover(mut self, listener: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
81        self.on_hover = Some(Rc::new(listener));
82
83        self
84    }
85
86    pub fn on_click(
87        mut self,
88        listener: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
89    ) -> Self {
90        self.on_click = Some(Rc::new(listener));
91
92        self
93    }
94
95    /**
96     * Changes made via .when(), .when_else(), etc., do not automatically trigger the animation cycle. Unlike event-based listeners that hold and manage the App context, these declarative methods do not pass the context to the animation controller. You must manually invoke a refresh or re-render to start the transition.
97     */
98    pub fn transition_when<T, I>(
99        self,
100        condition: bool,
101        duration: Duration,
102        transition: I,
103        then: impl FnOnce(State<StyleRefinement>) -> State<StyleRefinement> + 'static,
104    ) -> Self
105    where
106        T: Transition + 'static,
107        I: IntoArcTransition<T>,
108    {
109        self.transition_when_with_priority(
110            condition,
111            duration,
112            transition,
113            AnimationPriority::Lowest,
114            then,
115        )
116    }
117
118    /**
119     * Changes made via .when(), .when_else(), etc., do not automatically trigger the animation cycle. Unlike event-based listeners that hold and manage the App context, these declarative methods do not pass the context to the animation controller. You must manually invoke a refresh or re-render to start the transition.
120     */
121    pub fn transition_when_with_priority<T, I>(
122        self,
123        condition: bool,
124        duration: Duration,
125        transition: I,
126        priority: AnimationPriority,
127        then: impl FnOnce(State<StyleRefinement>) -> State<StyleRefinement> + 'static,
128    ) -> Self
129    where
130        T: Transition + 'static,
131        I: IntoArcTransition<T>,
132    {
133        if condition {
134            Self::animated_handle_without_event(
135                self.id.clone(),
136                then,
137                (duration, transition.into_arc()),
138                priority,
139            );
140        }
141
142        self
143    }
144
145    /**
146     * Changes made via .when(), .when_else(), etc., do not automatically trigger the animation cycle. Unlike event-based listeners that hold and manage the App context, these declarative methods do not pass the context to the animation controller. You must manually invoke a refresh or re-render to start the transition.
147     */
148    pub fn transition_when_else<T, I>(
149        self,
150        condition: bool,
151        duration: Duration,
152        transition: I,
153        then: impl Fn(State<StyleRefinement>) -> State<StyleRefinement> + 'static,
154        else_fn: impl Fn(State<StyleRefinement>) -> State<StyleRefinement> + 'static,
155    ) -> Self
156    where
157        T: Transition + 'static,
158        I: IntoArcTransition<T>,
159    {
160        self.transition_when_else_with_priority(
161            condition,
162            duration,
163            transition,
164            AnimationPriority::Lowest,
165            then,
166            else_fn,
167        )
168    }
169
170    /**
171     * Changes made via .when(), .when_else(), etc., do not automatically trigger the animation cycle. Unlike event-based listeners that hold and manage the App context, these declarative methods do not pass the context to the animation controller. You must manually invoke a refresh or re-render to start the transition.
172     */
173    pub fn transition_when_else_with_priority<T, I>(
174        self,
175        condition: bool,
176        duration: Duration,
177        transition: I,
178        priority: AnimationPriority,
179        then: impl Fn(State<StyleRefinement>) -> State<StyleRefinement> + 'static,
180        else_fn: impl Fn(State<StyleRefinement>) -> State<StyleRefinement> + 'static,
181    ) -> Self
182    where
183        T: Transition + 'static,
184        I: IntoArcTransition<T>,
185    {
186        if condition {
187            Self::animated_handle_without_event(
188                self.id.clone(),
189                then,
190                (duration, transition.into_arc()),
191                priority,
192            );
193        } else {
194            Self::animated_handle_without_event(
195                self.id.clone(),
196                else_fn,
197                (duration, transition.into_arc()),
198                priority,
199            );
200        }
201
202        self
203    }
204
205    /**
206     * Changes made via .when(), .when_else(), etc., do not automatically trigger the animation cycle. Unlike event-based listeners that hold and manage the App context, these declarative methods do not pass the context to the animation controller. You must manually invoke a refresh or re-render to start the transition.
207     */
208    pub fn transition_when_some<T, I, O>(
209        self,
210        option: Option<O>,
211        duration: Duration,
212        transition: I,
213        then: impl Fn(State<StyleRefinement>) -> State<StyleRefinement> + 'static,
214    ) -> Self
215    where
216        T: Transition + 'static,
217        I: IntoArcTransition<T>,
218    {
219        self.transition_when_some_with_priority(
220            option,
221            duration,
222            transition,
223            AnimationPriority::Lowest,
224            then,
225        )
226    }
227
228    /**
229     * Changes made via .when(), .when_else(), etc., do not automatically trigger the animation cycle. Unlike event-based listeners that hold and manage the App context, these declarative methods do not pass the context to the animation controller. You must manually invoke a refresh or re-render to start the transition.
230     */
231    pub fn transition_when_some_with_priority<T, I, O>(
232        self,
233        option: Option<O>,
234        duration: Duration,
235        transition: I,
236        priority: AnimationPriority,
237        then: impl Fn(State<StyleRefinement>) -> State<StyleRefinement> + 'static,
238    ) -> Self
239    where
240        T: Transition + 'static,
241        I: IntoArcTransition<T>,
242    {
243        if option.is_some() {
244            Self::animated_handle_without_event(
245                self.id.clone(),
246                then,
247                (duration, transition.into_arc()),
248                priority,
249            );
250        }
251
252        self
253    }
254
255    /**
256     * Changes made via .when(), .when_else(), etc., do not automatically trigger the animation cycle. Unlike event-based listeners that hold and manage the App context, these declarative methods do not pass the context to the animation controller. You must manually invoke a refresh or re-render to start the transition.
257     */
258    pub fn transition_when_none<T, I, O>(
259        self,
260        option: &Option<O>,
261        duration: Duration,
262        transition: I,
263        then: impl Fn(State<StyleRefinement>) -> State<StyleRefinement> + 'static,
264    ) -> Self
265    where
266        T: Transition + 'static,
267        I: IntoArcTransition<T>,
268    {
269        self.transition_when_none_with_priority(
270            option,
271            duration,
272            transition,
273            AnimationPriority::Lowest,
274            then,
275        )
276    }
277
278    /**
279     * Changes made via .when(), .when_else(), etc., do not automatically trigger the animation cycle. Unlike event-based listeners that hold and manage the App context, these declarative methods do not pass the context to the animation controller. You must manually invoke a refresh or re-render to start the transition.
280     */
281    pub fn transition_when_none_with_priority<T, I, O>(
282        self,
283        option: &Option<O>,
284        duration: Duration,
285        transition: I,
286        priority: AnimationPriority,
287        then: impl Fn(State<StyleRefinement>) -> State<StyleRefinement> + 'static,
288    ) -> Self
289    where
290        T: Transition + 'static,
291        I: IntoArcTransition<T>,
292    {
293        if option.is_none() {
294            Self::animated_handle_without_event(
295                self.id.clone(),
296                then,
297                (duration, transition.into_arc()),
298                priority,
299            );
300        }
301
302        self
303    }
304}
305
306impl<E: IntoElement + StatefulInteractiveElement + ParentElement + FluentBuilder + Styled + 'static>
307    AnimatedWrapper<E>
308{
309    fn with_transition(mut child: E, id: impl Into<ElementId>) -> Self {
310        Self {
311            style: child.style().clone(),
312            children: Vec::new(),
313            id: id.into(),
314            child,
315            transitions: HashMap::new(),
316            on_click: None,
317            on_hover: None,
318            hover_modifier: None,
319            click_modifier: None,
320        }
321    }
322}
323
324impl<E: IntoElement + StatefulInteractiveElement + ParentElement + FluentBuilder + Styled + 'static>
325    Styled for AnimatedWrapper<E>
326{
327    fn style(&mut self) -> &mut gpui::StyleRefinement {
328        &mut self.style
329    }
330}
331
332impl<E: IntoElement + StatefulInteractiveElement + ParentElement + FluentBuilder + Styled + 'static>
333    ParentElement for AnimatedWrapper<E>
334{
335    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
336        self.children.extend(elements);
337    }
338}
339
340impl<E: IntoElement + StatefulInteractiveElement + ParentElement + FluentBuilder + Styled + 'static>
341    RenderOnce for AnimatedWrapper<E>
342{
343    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
344        TransitionRegistry::init(cx);
345
346        let mut root = self.child;
347
348        TransitionRegistry::with_state_default(self.id.clone(), &self.style, |state| {
349            root.style().refine(&state.cur);
350        });
351
352        let id_for_hover = self.id.clone();
353        let on_hover_cb = self.on_hover;
354        let hover_mod = self.hover_modifier;
355        let hover_transition = self
356            .transitions
357            .get(&Event::HOVER)
358            .cloned()
359            .unwrap_or_else(|| (Duration::default(), Arc::new(Linear)));
360
361        let id_for_click = self.id.clone();
362        let on_click_cb = self.on_click;
363        let click_mod = self.click_modifier;
364        let click_transition = self
365            .transitions
366            .get(&Event::CLICK)
367            .cloned()
368            .unwrap_or_else(|| (Duration::default(), Arc::new(Linear)));
369
370        root.on_hover(move |hovered, window, app| {
371            if let Some(cb) = &on_hover_cb {
372                cb(hovered, window, app);
373            }
374
375            if *hovered {
376                Self::animated_handle_persistent(
377                    hovered,
378                    id_for_hover.clone(),
379                    Event::HOVER,
380                    hover_mod.clone(),
381                    hover_transition.clone(),
382                    AnimationPriority::Medium,
383                );
384            } else {
385                TransitionRegistry::remove_persistent_context(&id_for_hover, Event::HOVER);
386                Self::animated_handle(
387                    hovered,
388                    id_for_hover.clone(),
389                    Event::HOVER,
390                    hover_mod.clone(),
391                    hover_transition.clone(),
392                    AnimationPriority::High,
393                    false,
394                );
395            }
396        })
397        .on_click(move |event, window, app| {
398            if let Some(cb) = &on_click_cb {
399                cb(event, window, app);
400            }
401
402            Self::animated_handle(
403                event,
404                id_for_click.clone(),
405                Event::CLICK,
406                click_mod.clone(),
407                click_transition.clone(),
408                AnimationPriority::High,
409                true,
410            );
411        })
412        .children(self.children)
413    }
414}
415
416impl<E: IntoElement + StatefulInteractiveElement + ParentElement + FluentBuilder + Styled + 'static>
417    AnimatedWrapper<E>
418{
419    fn animated_handle<T>(
420        data: &T,
421        id: ElementId,
422        event: Event,
423        modifier: Option<Rc<dyn Fn(&T, State<StyleRefinement>) -> State<StyleRefinement>>>,
424        transition: (Duration, Arc<dyn Transition>),
425        priority: AnimationPriority,
426        save_persistent: bool,
427    ) {
428        let mut should_start_task = None;
429
430        {
431            if let Some(mut state) = TransitionRegistry::state_mut(id.clone()) {
432                let state_snapshot = state.clone();
433
434                if state.priority <= priority
435                    && let Some(modifier) = modifier
436                {
437                    if save_persistent {
438                        TransitionRegistry::save_persistent_context(
439                            &id,
440                            &state.to,
441                            transition.0,
442                            transition.1.clone(),
443                            state.priority,
444                        );
445                    }
446
447                    // instantaneous events like hoverless/click
448                    state.priority = priority;
449                    *state = modifier(data, state.clone());
450
451                    if state_snapshot.ne(&*state) {
452                        let (ver, dt) = state.pre_animated(transition.0);
453                        should_start_task = Some((ver, dt));
454                    } else {
455                        state.priority = AnimationPriority::Lowest;
456                    }
457                }
458            } else {
459                should_start_task = None;
460            }
461        }
462
463        if let Some((ver, dt)) = should_start_task {
464            TransitionRegistry::background_animated_task(
465                id,
466                event,
467                dt,
468                transition.0,
469                transition.1,
470                ver,
471                false,
472            );
473        }
474    }
475
476    fn animated_handle_persistent<T>(
477        data: &T,
478        id: ElementId,
479        event: Event,
480        modifier: Option<Rc<dyn Fn(&T, State<StyleRefinement>) -> State<StyleRefinement>>>,
481        transition: (Duration, Arc<dyn Transition>),
482        priority: AnimationPriority,
483    ) {
484        let mut should_start_task = None;
485
486        {
487            if let Some(mut state) = TransitionRegistry::state_mut(id.clone()) {
488                let state_snapshot = state.clone();
489
490                if state.priority <= priority
491                    && let Some(modifier) = modifier
492                {
493                    // allow overridden by medium/high/realtime
494                    state.priority = priority;
495                    *state = modifier(data, state.clone());
496
497                    if state_snapshot.ne(&*state) {
498                        let (ver, dt) = state.pre_animated(transition.0);
499                        should_start_task = Some((ver, dt));
500                    } else {
501                        state.priority = AnimationPriority::Lowest;
502                    }
503                }
504            } else {
505                should_start_task = None;
506            }
507        }
508
509        if let Some((ver, dt)) = should_start_task {
510            TransitionRegistry::background_animated_task(
511                id,
512                event,
513                dt,
514                transition.0,
515                transition.1,
516                ver,
517                true,
518            );
519        }
520    }
521    fn animated_handle_without_event(
522        id: ElementId,
523        modifier: impl FnOnce(State<StyleRefinement>) -> State<StyleRefinement> + 'static,
524        transition: (Duration, Arc<dyn Transition>),
525        priority: AnimationPriority,
526    ) {
527        let mut should_start_task = None;
528
529        {
530            if let Some(mut state) = TransitionRegistry::state_mut(id.clone()) {
531                let state_snapshot = state.clone();
532
533                if state.priority <= priority {
534                    state.priority = priority;
535                    *state = modifier(state.clone());
536
537                    if state_snapshot.ne(&*state) {
538                        let (ver, dt) = state.pre_animated(transition.0);
539                        should_start_task = Some((ver, dt));
540                    } else {
541                        state.priority = AnimationPriority::Lowest;
542                    }
543                }
544            } else {
545                should_start_task = None;
546            }
547        }
548
549        if let Some((ver, dt)) = should_start_task {
550            TransitionRegistry::background_animated_task(
551                id,
552                Event::NONE,
553                dt,
554                transition.0,
555                transition.1,
556                ver,
557                false,
558            );
559        }
560    }
561}
562
563pub trait TransitionExt:
564    IntoElement + StatefulInteractiveElement + ParentElement + FluentBuilder + Styled + 'static
565{
566    fn with_transition(self, id: impl Into<ElementId>) -> AnimatedWrapper<Self> {
567        AnimatedWrapper::with_transition(self, id)
568    }
569}
570
571impl<T: IntoElement + StatefulInteractiveElement + ParentElement + FluentBuilder + Styled + 'static>
572    TransitionExt for T
573{
574}