Skip to main content

script/dom/document/
animations.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! The set of animations for a document.
6
7use std::cell::Cell;
8
9use cssparser::ToCss;
10use embedder_traits::{AnimationState as AnimationsPresentState, UntrustedNodeAddress};
11use js::context::NoGC;
12use libc::c_void;
13use rustc_hash::{FxHashMap, FxHashSet};
14use script_bindings::cell::DomRefCell;
15use serde::{Deserialize, Serialize};
16use servo_base::id::PipelineId;
17use servo_constellation_traits::ScriptToConstellationMessage;
18use style::animation::{
19    Animation, AnimationSetKey, AnimationState, DocumentAnimationSet, ElementAnimationSet,
20    KeyframesIterationState, Transition,
21};
22use style::dom::OpaqueNode;
23use style::selector_parser::PseudoElement;
24
25use crate::dom::animationevent::AnimationEvent;
26use crate::dom::bindings::codegen::Bindings::AnimationEventBinding::AnimationEventInit;
27use crate::dom::bindings::codegen::Bindings::EventBinding::EventInit;
28use crate::dom::bindings::codegen::Bindings::TransitionEventBinding::TransitionEventInit;
29use crate::dom::bindings::inheritance::Castable;
30use crate::dom::bindings::num::Finite;
31use crate::dom::bindings::root::{Dom, DomRoot};
32use crate::dom::bindings::str::DOMString;
33use crate::dom::bindings::trace::NoTrace;
34use crate::dom::event::Event;
35use crate::dom::node::{Node, NodeDamage, NodeTraits, from_untrusted_node_address};
36use crate::dom::transitionevent::TransitionEvent;
37use crate::dom::window::Window;
38
39/// The set of animations for a document.
40#[derive(Default, JSTraceable, MallocSizeOf)]
41#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
42pub(crate) struct Animations {
43    /// The map of nodes to their animation states.
44    #[no_trace]
45    pub(crate) sets: DocumentAnimationSet,
46
47    /// Whether or not we have animations that are running.
48    has_running_animations: Cell<bool>,
49
50    /// A list of nodes with in-progress CSS transitions or pending events.
51    rooted_nodes: DomRefCell<FxHashMap<NoTrace<OpaqueNode>, Dom<Node>>>,
52
53    /// A list of pending animation-related events.
54    pending_events: DomRefCell<Vec<TransitionOrAnimationEvent>>,
55
56    /// The timeline value at the last time all animations were marked dirty.
57    /// This is used to prevent marking animations dirty when the timeline
58    /// has not changed.
59    timeline_value_at_last_dirty: Cell<f64>,
60}
61
62impl Animations {
63    pub(crate) fn new() -> Self {
64        Animations {
65            sets: Default::default(),
66            has_running_animations: Cell::new(false),
67            rooted_nodes: Default::default(),
68            pending_events: Default::default(),
69            timeline_value_at_last_dirty: Cell::new(0.0),
70        }
71    }
72
73    pub(crate) fn clear(&self) {
74        self.sets.sets.write().clear();
75        self.rooted_nodes.borrow_mut().clear();
76        self.pending_events.borrow_mut().clear();
77    }
78
79    // Mark all animations dirty, if they haven't been marked dirty since the
80    // specified `current_timeline_value`. Returns true if animations were marked
81    // dirty or false otherwise.
82    pub(crate) fn mark_animating_nodes_as_dirty(
83        &self,
84        no_gc: &NoGC,
85        current_timeline_value: f64,
86    ) -> bool {
87        if current_timeline_value <= self.timeline_value_at_last_dirty.get() {
88            return false;
89        }
90        self.timeline_value_at_last_dirty
91            .set(current_timeline_value);
92
93        let sets = self.sets.sets.read();
94        let rooted_nodes = self.rooted_nodes.borrow();
95        for node in sets
96            .keys()
97            .filter_map(|key| rooted_nodes.get(&NoTrace(key.node)))
98        {
99            node.dirty(no_gc, NodeDamage::Style);
100        }
101
102        true
103    }
104
105    pub(crate) fn update_for_new_timeline_value(&self, window: &Window, now: f64) {
106        let pipeline_id = window.pipeline_id();
107        let mut sets = self.sets.sets.write();
108
109        for (key, set) in sets.iter_mut() {
110            self.start_pending_animations(key, set, now, pipeline_id);
111
112            // When necessary, iterate our running animations to the next iteration.
113            for animation in set.animations.iter_mut() {
114                if animation.iterate_if_necessary(now) {
115                    self.add_animation_event(
116                        key,
117                        animation,
118                        TransitionOrAnimationEventType::AnimationIteration,
119                        now,
120                        pipeline_id,
121                    );
122                }
123            }
124
125            self.finish_running_animations(key, set, now, pipeline_id);
126        }
127
128        self.unroot_unused_nodes(&sets);
129    }
130
131    /// Cancel animations for the given node, if any exist.
132    pub(crate) fn cancel_animations_for_node(&self, node: &Node) {
133        let mut animations = self.sets.sets.write();
134        let mut cancel_animations_for = |key| {
135            if let Some(set) = animations.get_mut(&key) {
136                set.cancel_all_animations();
137            }
138        };
139
140        let opaque_node = node.to_opaque();
141        cancel_animations_for(AnimationSetKey::new_for_non_pseudo(opaque_node));
142        cancel_animations_for(AnimationSetKey::new_for_pseudo(
143            opaque_node,
144            PseudoElement::Before,
145        ));
146        cancel_animations_for(AnimationSetKey::new_for_pseudo(
147            opaque_node,
148            PseudoElement::After,
149        ));
150    }
151
152    /// This does three things:
153    ///  - Cancel animations for any nodes that are no longer being rendered or delegating rendering.
154    ///  - Process any new animations that were discovered after reflow.
155    ///  - Collect pending events for any animations that changed state.
156    pub(crate) fn do_post_reflow_update(&self, window: &Window, now: f64) {
157        let mut sets = self.sets.sets.write();
158        {
159            let rooted_nodes = self.rooted_nodes.borrow();
160            for (key, set) in sets.iter_mut() {
161                if rooted_nodes.get(&NoTrace(key.node)).is_some_and(|node| {
162                    !node.is_being_rendered_or_delegates_rendering(key.pseudo_element)
163                }) {
164                    set.cancel_all_animations();
165                }
166            }
167        }
168
169        let pipeline_id = window.pipeline_id();
170        self.root_newly_animating_dom_nodes(&sets);
171
172        for (key, set) in sets.iter_mut() {
173            self.handle_canceled_animations(key, set, now, pipeline_id);
174            self.handle_new_animations(key, set, now, pipeline_id);
175        }
176
177        // Remove empty states from our collection of states in order to free
178        // up space as soon as we are no longer tracking any animations for
179        // a node.
180        sets.retain(|_, state| !state.is_empty());
181        let have_running_animations = sets.values().any(|state| state.needs_animation_ticks());
182
183        self.update_running_animations_presence(window, have_running_animations);
184    }
185
186    fn update_running_animations_presence(&self, window: &Window, new_value: bool) {
187        let had_running_animations = self.has_running_animations.get();
188        if new_value == had_running_animations {
189            return;
190        }
191
192        self.has_running_animations.set(new_value);
193        self.handle_animation_presence_or_pending_events_change(window);
194    }
195
196    fn handle_animation_presence_or_pending_events_change(&self, window: &Window) {
197        let has_running_animations = self.has_running_animations.get();
198        let has_pending_events = !self.pending_events.borrow().is_empty();
199
200        // Do not send the AnimationCallbacksAbsent state until all pending
201        // animation events are delivered.
202        let state = match has_running_animations || has_pending_events {
203            true => AnimationsPresentState::AnimationsPresent,
204            false => AnimationsPresentState::NoAnimationsPresent,
205        };
206        window.send_to_constellation(ScriptToConstellationMessage::ChangeRunningAnimationsState(
207            state,
208        ));
209    }
210
211    pub(crate) fn running_animation_count(&self) -> usize {
212        self.sets
213            .sets
214            .read()
215            .values()
216            .map(|state| state.running_animation_and_transition_count())
217            .sum()
218    }
219
220    /// Walk through the list of pending animations and start all of the ones that
221    /// have left the delay phase.
222    fn start_pending_animations(
223        &self,
224        key: &AnimationSetKey,
225        set: &mut ElementAnimationSet,
226        now: f64,
227        pipeline_id: PipelineId,
228    ) {
229        for animation in set.animations.iter_mut() {
230            if animation.state == AnimationState::Pending && animation.started_at <= now {
231                animation.state = AnimationState::Running;
232                self.add_animation_event(
233                    key,
234                    animation,
235                    TransitionOrAnimationEventType::AnimationStart,
236                    now,
237                    pipeline_id,
238                );
239            }
240        }
241
242        for transition in set.transitions.iter_mut() {
243            if transition.state == AnimationState::Pending && transition.start_time <= now {
244                transition.state = AnimationState::Running;
245                self.add_transition_event(
246                    key,
247                    transition,
248                    TransitionOrAnimationEventType::TransitionStart,
249                    now,
250                    pipeline_id,
251                );
252            }
253        }
254    }
255
256    /// Walk through the list of running animations and remove all of the ones that
257    /// have ended.
258    fn finish_running_animations(
259        &self,
260        key: &AnimationSetKey,
261        set: &mut ElementAnimationSet,
262        now: f64,
263        pipeline_id: PipelineId,
264    ) {
265        for animation in set.animations.iter_mut() {
266            if animation.state == AnimationState::Running && animation.has_ended(now) {
267                animation.state = AnimationState::Finished;
268                self.add_animation_event(
269                    key,
270                    animation,
271                    TransitionOrAnimationEventType::AnimationEnd,
272                    now,
273                    pipeline_id,
274                );
275            }
276        }
277
278        for transition in set.transitions.iter_mut() {
279            if transition.state == AnimationState::Running && transition.has_ended(now) {
280                transition.state = AnimationState::Finished;
281                self.add_transition_event(
282                    key,
283                    transition,
284                    TransitionOrAnimationEventType::TransitionEnd,
285                    now,
286                    pipeline_id,
287                );
288            }
289        }
290    }
291
292    /// Send events for canceled animations. Currently this only handles canceled
293    /// transitions, but eventually this should handle canceled CSS animations as
294    /// well.
295    fn handle_canceled_animations(
296        &self,
297        key: &AnimationSetKey,
298        set: &mut ElementAnimationSet,
299        now: f64,
300        pipeline_id: PipelineId,
301    ) {
302        for transition in &set.transitions {
303            if transition.state == AnimationState::Canceled {
304                self.add_transition_event(
305                    key,
306                    transition,
307                    TransitionOrAnimationEventType::TransitionCancel,
308                    now,
309                    pipeline_id,
310                );
311            }
312        }
313
314        for animation in &set.animations {
315            if animation.state == AnimationState::Canceled {
316                self.add_animation_event(
317                    key,
318                    animation,
319                    TransitionOrAnimationEventType::AnimationCancel,
320                    now,
321                    pipeline_id,
322                );
323            }
324        }
325
326        set.clear_canceled_animations();
327    }
328
329    fn handle_new_animations(
330        &self,
331        key: &AnimationSetKey,
332        set: &mut ElementAnimationSet,
333        now: f64,
334        pipeline_id: PipelineId,
335    ) {
336        for animation in set.animations.iter_mut() {
337            animation.is_new = false;
338        }
339
340        for transition in set.transitions.iter_mut() {
341            if transition.is_new {
342                self.add_transition_event(
343                    key,
344                    transition,
345                    TransitionOrAnimationEventType::TransitionRun,
346                    now,
347                    pipeline_id,
348                );
349                transition.is_new = false;
350            }
351        }
352    }
353
354    /// Ensure that all nodes with new animations are rooted. This should be called
355    /// immediately after a restyle, to ensure that these addresses are still valid.
356    #[expect(unsafe_code)]
357    fn root_newly_animating_dom_nodes(
358        &self,
359        sets: &FxHashMap<AnimationSetKey, ElementAnimationSet>,
360    ) {
361        let mut rooted_nodes = self.rooted_nodes.borrow_mut();
362        for (key, set) in sets.iter() {
363            let opaque_node = key.node;
364            if rooted_nodes.contains_key(&NoTrace(opaque_node)) {
365                continue;
366            }
367
368            if set.animations.iter().any(|animation| animation.is_new) ||
369                set.transitions.iter().any(|transition| transition.is_new)
370            {
371                let address = UntrustedNodeAddress(opaque_node.0 as *const c_void);
372                unsafe {
373                    rooted_nodes.insert(
374                        NoTrace(opaque_node),
375                        Dom::from_ref(&*from_untrusted_node_address(address)),
376                    )
377                };
378            }
379        }
380    }
381
382    // Unroot any nodes that we have rooted but are no longer tracking animations for.
383    fn unroot_unused_nodes(&self, sets: &FxHashMap<AnimationSetKey, ElementAnimationSet>) {
384        let pending_events = self.pending_events.borrow();
385        let nodes: FxHashSet<OpaqueNode> = sets.keys().map(|key| key.node).collect();
386        self.rooted_nodes.borrow_mut().retain(|node, _| {
387            nodes.contains(&node.0) || pending_events.iter().any(|event| event.node == node.0)
388        });
389    }
390
391    fn add_transition_event(
392        &self,
393        key: &AnimationSetKey,
394        transition: &Transition,
395        event_type: TransitionOrAnimationEventType,
396        now: f64,
397        pipeline_id: PipelineId,
398    ) {
399        // Calculate the `elapsed-time` property of the event and take the absolute
400        // value to prevent -0 values.
401        let elapsed_time = match event_type {
402            TransitionOrAnimationEventType::TransitionRun |
403            TransitionOrAnimationEventType::TransitionStart => transition
404                .property_animation
405                .duration
406                .min((-transition.delay).max(0.)),
407            TransitionOrAnimationEventType::TransitionEnd => transition.property_animation.duration,
408            TransitionOrAnimationEventType::TransitionCancel => {
409                (now - transition.start_time).max(0.)
410            },
411            _ => unreachable!(),
412        }
413        .abs();
414
415        self.pending_events
416            .borrow_mut()
417            .push(TransitionOrAnimationEvent {
418                pipeline_id,
419                event_type,
420                node: key.node,
421                pseudo_element: key.pseudo_element,
422                property_or_animation_name: transition
423                    .property_animation
424                    .property_id()
425                    .name()
426                    .into(),
427                elapsed_time,
428            });
429    }
430
431    fn add_animation_event(
432        &self,
433        key: &AnimationSetKey,
434        animation: &Animation,
435        event_type: TransitionOrAnimationEventType,
436        now: f64,
437        pipeline_id: PipelineId,
438    ) {
439        let iteration_index = match animation.iteration_state {
440            KeyframesIterationState::Finite(current, _) |
441            KeyframesIterationState::Infinite(current) => current,
442        };
443
444        let active_duration = match animation.iteration_state {
445            KeyframesIterationState::Finite(_, max) => max * animation.duration,
446            KeyframesIterationState::Infinite(_) => f64::MAX,
447        };
448
449        // Calculate the `elapsed-time` property of the event and take the absolute
450        // value to prevent -0 values.
451        let elapsed_time = match event_type {
452            TransitionOrAnimationEventType::AnimationStart => {
453                (-animation.delay).max(0.).min(active_duration)
454            },
455            TransitionOrAnimationEventType::AnimationIteration => {
456                iteration_index * animation.duration
457            },
458            TransitionOrAnimationEventType::AnimationEnd => {
459                (iteration_index * animation.duration) + animation.current_iteration_duration()
460            },
461            TransitionOrAnimationEventType::AnimationCancel => {
462                (iteration_index * animation.duration) + (now - animation.started_at).max(0.)
463            },
464            _ => unreachable!(),
465        }
466        .abs();
467
468        self.pending_events
469            .borrow_mut()
470            .push(TransitionOrAnimationEvent {
471                pipeline_id,
472                event_type,
473                node: key.node,
474                pseudo_element: key.pseudo_element,
475                property_or_animation_name: animation.name.to_string(),
476                elapsed_time,
477            });
478    }
479
480    /// An implementation of the final steps of
481    /// <https://drafts.csswg.org/web-animations-1/#update-animations-and-send-events>.
482    pub(crate) fn send_pending_events(&self, window: &Window, cx: &mut js::context::JSContext) {
483        // > 4. Let events to dispatch be a copy of doc’s pending animation event queue.
484        // > 5. Clear doc’s pending animation event queue.
485        //
486        // Take all of the events here, in case sending one of these events
487        // triggers adding new events by forcing a layout.
488        let events = std::mem::take(&mut *self.pending_events.safe_borrow_mut(cx.no_gc()));
489        if events.is_empty() {
490            return;
491        }
492
493        // > 6. Perform a stable sort of the animation events in events to dispatch as follows:
494        // >    1. Sort the events by their scheduled event time such that events that were
495        // >       scheduled to occur earlier sort before events scheduled to occur later, and
496        // >       events whose scheduled event time is unresolved sort before events with a
497        // >       resolved scheduled event time.
498        // >    2. Within events with equal scheduled event times, sort by their composite
499        // >       order.
500        //
501        // TODO: Sorting of animation events isn't done yet.
502
503        // 7. Dispatch each of the events in events to dispatch at their corresponding
504        // target using the order established in the previous step.
505        for event in events.into_iter() {
506            // We root the node here to ensure that sending this event doesn't
507            // unroot it as a side-effect.
508            let node = match self.rooted_nodes.borrow().get(&NoTrace(event.node)) {
509                Some(node) => DomRoot::from_ref(&**node),
510                None => {
511                    warn!("Tried to send an event for an unrooted node");
512                    continue;
513                },
514            };
515
516            let event_atom = match event.event_type {
517                TransitionOrAnimationEventType::AnimationEnd => atom!("animationend"),
518                TransitionOrAnimationEventType::AnimationStart => atom!("animationstart"),
519                TransitionOrAnimationEventType::AnimationCancel => atom!("animationcancel"),
520                TransitionOrAnimationEventType::AnimationIteration => atom!("animationiteration"),
521                TransitionOrAnimationEventType::TransitionCancel => atom!("transitioncancel"),
522                TransitionOrAnimationEventType::TransitionEnd => atom!("transitionend"),
523                TransitionOrAnimationEventType::TransitionRun => atom!("transitionrun"),
524                TransitionOrAnimationEventType::TransitionStart => atom!("transitionstart"),
525            };
526            let parent = EventInit {
527                bubbles: true,
528                cancelable: false,
529                composed: false,
530            };
531
532            let property_or_animation_name =
533                DOMString::from(event.property_or_animation_name.clone());
534            let pseudo_element = event
535                .pseudo_element
536                .map_or_else(DOMString::new, |pseudo_element| {
537                    DOMString::from(pseudo_element.to_css_string())
538                });
539            let elapsed_time = Finite::new(event.elapsed_time as f32).unwrap();
540            let window = node.owner_window();
541
542            if event.event_type.is_transition_event() {
543                let event_init = TransitionEventInit {
544                    parent,
545                    propertyName: property_or_animation_name,
546                    elapsedTime: elapsed_time,
547                    pseudoElement: pseudo_element,
548                };
549                TransitionEvent::new(cx, &window, event_atom, &event_init)
550                    .upcast::<Event>()
551                    .fire(cx, node.upcast());
552            } else {
553                let event_init = AnimationEventInit {
554                    parent,
555                    animationName: property_or_animation_name,
556                    elapsedTime: elapsed_time,
557                    pseudoElement: pseudo_element,
558                };
559                AnimationEvent::new(cx, &window, event_atom, &event_init)
560                    .upcast::<Event>()
561                    .fire(cx, node.upcast());
562            }
563        }
564
565        if self.pending_events.borrow().is_empty() {
566            self.handle_animation_presence_or_pending_events_change(window);
567        }
568    }
569}
570
571/// The type of transition event to trigger. These are defined by
572/// CSS Transitions § 6.1 and CSS Animations § 4.2
573#[derive(Clone, Debug, Deserialize, JSTraceable, MallocSizeOf, Serialize)]
574pub(crate) enum TransitionOrAnimationEventType {
575    /// "The transitionrun event occurs when a transition is created (i.e., when it
576    /// is added to the set of running transitions)."
577    TransitionRun,
578    /// "The transitionstart event occurs when a transition’s delay phase ends."
579    TransitionStart,
580    /// "The transitionend event occurs at the completion of the transition. In the
581    /// case where a transition is removed before completion, such as if the
582    /// transition-property is removed, then the event will not fire."
583    TransitionEnd,
584    /// "The transitioncancel event occurs when a transition is canceled."
585    TransitionCancel,
586    /// "The animationstart event occurs at the start of the animation. If there is
587    /// an animation-delay then this event will fire once the delay period has expired."
588    AnimationStart,
589    /// "The animationiteration event occurs at the end of each iteration of an
590    /// animation, except when an animationend event would fire at the same time."
591    AnimationIteration,
592    /// "The animationend event occurs when the animation finishes"
593    AnimationEnd,
594    /// "The animationcancel event occurs when the animation stops running in a way
595    /// that does not fire an animationend event..."
596    AnimationCancel,
597}
598
599impl TransitionOrAnimationEventType {
600    /// Whether or not this event is a transition-related event.
601    pub(crate) fn is_transition_event(&self) -> bool {
602        match *self {
603            Self::TransitionRun |
604            Self::TransitionEnd |
605            Self::TransitionCancel |
606            Self::TransitionStart => true,
607            Self::AnimationEnd |
608            Self::AnimationIteration |
609            Self::AnimationStart |
610            Self::AnimationCancel => false,
611        }
612    }
613}
614
615#[derive(Deserialize, JSTraceable, MallocSizeOf, Serialize)]
616/// A transition or animation event.
617pub(crate) struct TransitionOrAnimationEvent {
618    /// The pipeline id of the layout task that sent this message.
619    #[no_trace]
620    pub(crate) pipeline_id: PipelineId,
621    /// The type of transition event this should trigger.
622    pub(crate) event_type: TransitionOrAnimationEventType,
623    /// The address of the node which owns this transition.
624    #[no_trace]
625    pub(crate) node: OpaqueNode,
626    /// The pseudo element for this transition or animation, if applicable.
627    #[no_trace]
628    pub(crate) pseudo_element: Option<PseudoElement>,
629    /// The name of the property that is transitioning (in the case of a transition)
630    /// or the name of the animation (in the case of an animation).
631    pub(crate) property_or_animation_name: String,
632    /// The elapsed time property to send with this transition event.
633    pub(crate) elapsed_time: f64,
634}