diff options
Diffstat (limited to 'components/script/animations.rs')
-rw-r--r-- | components/script/animations.rs | 554 |
1 files changed, 554 insertions, 0 deletions
diff --git a/components/script/animations.rs b/components/script/animations.rs new file mode 100644 index 00000000000..cd581ed3cb3 --- /dev/null +++ b/components/script/animations.rs @@ -0,0 +1,554 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ + +#![deny(missing_docs)] + +//! The set of animations for a document. + +use crate::dom::animationevent::AnimationEvent; +use crate::dom::bindings::cell::DomRefCell; +use crate::dom::bindings::codegen::Bindings::AnimationEventBinding::AnimationEventInit; +use crate::dom::bindings::codegen::Bindings::EventBinding::EventInit; +use crate::dom::bindings::codegen::Bindings::TransitionEventBinding::TransitionEventInit; +use crate::dom::bindings::inheritance::Castable; +use crate::dom::bindings::num::Finite; +use crate::dom::bindings::root::{Dom, DomRoot}; +use crate::dom::bindings::str::DOMString; +use crate::dom::event::Event; +use crate::dom::node::{from_untrusted_node_address, window_from_node, Node, NodeDamage}; +use crate::dom::transitionevent::TransitionEvent; +use crate::dom::window::Window; +use cssparser::ToCss; +use fxhash::{FxHashMap, FxHashSet}; +use libc::c_void; +use msg::constellation_msg::PipelineId; +use script_traits::{AnimationState as AnimationsPresentState, ScriptMsg, UntrustedNodeAddress}; +use std::cell::Cell; +use style::animation::{ + Animation, AnimationSetKey, AnimationState, DocumentAnimationSet, ElementAnimationSet, + KeyframesIterationState, Transition, +}; +use style::dom::OpaqueNode; +use style::selector_parser::PseudoElement; + +/// The set of animations for a document. +#[derive(Default, JSTraceable, MallocSizeOf)] +#[unrooted_must_root_lint::must_root] +pub(crate) struct Animations { + /// The map of nodes to their animation states. + pub sets: DocumentAnimationSet, + + /// Whether or not we have animations that are running. + have_running_animations: Cell<bool>, + + /// A list of nodes with in-progress CSS transitions or pending events. + rooted_nodes: DomRefCell<FxHashMap<OpaqueNode, Dom<Node>>>, + + /// A list of pending animation-related events. + pending_events: DomRefCell<Vec<TransitionOrAnimationEvent>>, +} + +impl Animations { + pub(crate) fn new() -> Self { + Animations { + sets: Default::default(), + have_running_animations: Cell::new(false), + rooted_nodes: Default::default(), + pending_events: Default::default(), + } + } + + pub(crate) fn clear(&self) { + self.sets.sets.write().clear(); + self.rooted_nodes.borrow_mut().clear(); + self.pending_events.borrow_mut().clear(); + } + + pub(crate) fn mark_animating_nodes_as_dirty(&self) { + let sets = self.sets.sets.read(); + let rooted_nodes = self.rooted_nodes.borrow(); + for node in sets.keys().filter_map(|key| rooted_nodes.get(&key.node)) { + node.dirty(NodeDamage::NodeStyleDamaged); + } + } + + pub(crate) fn update_for_new_timeline_value(&self, window: &Window, now: f64) { + let pipeline_id = window.pipeline_id(); + let mut sets = self.sets.sets.write(); + + for (key, set) in sets.iter_mut() { + self.start_pending_animations(key, set, now, pipeline_id); + + // When necessary, iterate our running animations to the next iteration. + for animation in set.animations.iter_mut() { + if animation.iterate_if_necessary(now) { + self.add_animation_event( + key, + animation, + TransitionOrAnimationEventType::AnimationIteration, + now, + pipeline_id, + ); + } + } + + self.finish_running_animations(key, set, now, pipeline_id); + } + + self.unroot_unused_nodes(&sets); + } + + /// Cancel animations for the given node, if any exist. + pub(crate) fn cancel_animations_for_node(&self, node: &Node) { + let mut animations = self.sets.sets.write(); + let mut cancel_animations_for = |key| { + animations.get_mut(&key).map(|set| { + set.cancel_all_animations(); + }); + }; + + let opaque_node = node.to_opaque(); + cancel_animations_for(AnimationSetKey::new_for_non_pseudo(opaque_node)); + cancel_animations_for(AnimationSetKey::new_for_pseudo( + opaque_node, + PseudoElement::Before, + )); + cancel_animations_for(AnimationSetKey::new_for_pseudo( + opaque_node, + PseudoElement::After, + )); + } + + /// Processes any new animations that were discovered after reflow. Collect messages + /// that trigger events for any animations that changed state. + pub(crate) fn do_post_reflow_update(&self, window: &Window, now: f64) { + let pipeline_id = window.pipeline_id(); + let mut sets = self.sets.sets.write(); + self.root_newly_animating_dom_nodes(&sets); + + for (key, set) in sets.iter_mut() { + self.handle_canceled_animations(key, set, now, pipeline_id); + self.handle_new_animations(key, set, now, pipeline_id); + } + + // Remove empty states from our collection of states in order to free + // up space as soon as we are no longer tracking any animations for + // a node. + sets.retain(|_, state| !state.is_empty()); + let have_running_animations = sets.values().any(|state| state.needs_animation_ticks()); + + self.update_running_animations_presence(window, have_running_animations); + } + + fn update_running_animations_presence(&self, window: &Window, new_value: bool) { + let have_running_animations = self.have_running_animations.get(); + if new_value == have_running_animations { + return; + } + + self.have_running_animations.set(new_value); + let state = match new_value { + true => AnimationsPresentState::AnimationsPresent, + false => AnimationsPresentState::NoAnimationsPresent, + }; + + window.send_to_constellation(ScriptMsg::ChangeRunningAnimationsState(state)); + } + + pub(crate) fn running_animation_count(&self) -> usize { + self.sets + .sets + .read() + .values() + .map(|state| state.running_animation_and_transition_count()) + .sum() + } + + /// Walk through the list of pending animations and start all of the ones that + /// have left the delay phase. + fn start_pending_animations( + &self, + key: &AnimationSetKey, + set: &mut ElementAnimationSet, + now: f64, + pipeline_id: PipelineId, + ) { + for animation in set.animations.iter_mut() { + if animation.state == AnimationState::Pending && animation.started_at <= now { + animation.state = AnimationState::Running; + self.add_animation_event( + key, + animation, + TransitionOrAnimationEventType::AnimationStart, + now, + pipeline_id, + ); + } + } + + for transition in set.transitions.iter_mut() { + if transition.state == AnimationState::Pending && transition.start_time <= now { + transition.state = AnimationState::Running; + self.add_transition_event( + key, + transition, + TransitionOrAnimationEventType::TransitionStart, + now, + pipeline_id, + ); + } + } + } + + /// Walk through the list of running animations and remove all of the ones that + /// have ended. + fn finish_running_animations( + &self, + key: &AnimationSetKey, + set: &mut ElementAnimationSet, + now: f64, + pipeline_id: PipelineId, + ) { + for animation in set.animations.iter_mut() { + if animation.state == AnimationState::Running && animation.has_ended(now) { + animation.state = AnimationState::Finished; + self.add_animation_event( + key, + animation, + TransitionOrAnimationEventType::AnimationEnd, + now, + pipeline_id, + ); + } + } + + for transition in set.transitions.iter_mut() { + if transition.state == AnimationState::Running && transition.has_ended(now) { + transition.state = AnimationState::Finished; + self.add_transition_event( + key, + transition, + TransitionOrAnimationEventType::TransitionEnd, + now, + pipeline_id, + ); + } + } + } + + /// Send events for canceled animations. Currently this only handles canceled + /// transitions, but eventually this should handle canceled CSS animations as + /// well. + fn handle_canceled_animations( + &self, + key: &AnimationSetKey, + set: &mut ElementAnimationSet, + now: f64, + pipeline_id: PipelineId, + ) { + for transition in &set.transitions { + if transition.state == AnimationState::Canceled { + self.add_transition_event( + key, + transition, + TransitionOrAnimationEventType::TransitionCancel, + now, + pipeline_id, + ); + } + } + + for animation in &set.animations { + if animation.state == AnimationState::Canceled { + self.add_animation_event( + key, + animation, + TransitionOrAnimationEventType::AnimationCancel, + now, + pipeline_id, + ); + } + } + + set.clear_canceled_animations(); + } + + fn handle_new_animations( + &self, + key: &AnimationSetKey, + set: &mut ElementAnimationSet, + now: f64, + pipeline_id: PipelineId, + ) { + for animation in set.animations.iter_mut() { + animation.is_new = false; + } + + for transition in set.transitions.iter_mut() { + if transition.is_new { + self.add_transition_event( + key, + transition, + TransitionOrAnimationEventType::TransitionRun, + now, + pipeline_id, + ); + transition.is_new = false; + } + } + } + + /// Ensure that all nodes with new animations are rooted. This should be called + /// immediately after a restyle, to ensure that these addresses are still valid. + #[allow(unsafe_code)] + fn root_newly_animating_dom_nodes( + &self, + sets: &FxHashMap<AnimationSetKey, ElementAnimationSet>, + ) { + let mut rooted_nodes = self.rooted_nodes.borrow_mut(); + for (key, set) in sets.iter() { + let opaque_node = key.node; + if rooted_nodes.contains_key(&opaque_node) { + continue; + } + + if set.animations.iter().any(|animation| animation.is_new) || + set.transitions.iter().any(|transition| transition.is_new) + { + let address = UntrustedNodeAddress(opaque_node.0 as *const c_void); + unsafe { + rooted_nodes.insert( + opaque_node, + Dom::from_ref(&*from_untrusted_node_address(address)), + ) + }; + } + } + } + + // Unroot any nodes that we have rooted but are no longer tracking animations for. + fn unroot_unused_nodes(&self, sets: &FxHashMap<AnimationSetKey, ElementAnimationSet>) { + let pending_events = self.pending_events.borrow(); + let nodes: FxHashSet<OpaqueNode> = sets.keys().map(|key| key.node).collect(); + self.rooted_nodes.borrow_mut().retain(|node, _| { + nodes.contains(&node) || pending_events.iter().any(|event| event.node == *node) + }); + } + + fn add_transition_event( + &self, + key: &AnimationSetKey, + transition: &Transition, + event_type: TransitionOrAnimationEventType, + now: f64, + pipeline_id: PipelineId, + ) { + // Calculate the `elapsed-time` property of the event and take the absolute + // value to prevent -0 values. + let elapsed_time = match event_type { + TransitionOrAnimationEventType::TransitionRun | + TransitionOrAnimationEventType::TransitionStart => transition + .property_animation + .duration + .min((-transition.delay).max(0.)), + TransitionOrAnimationEventType::TransitionEnd => transition.property_animation.duration, + TransitionOrAnimationEventType::TransitionCancel => { + (now - transition.start_time).max(0.) + }, + _ => unreachable!(), + } + .abs(); + + self.pending_events + .borrow_mut() + .push(TransitionOrAnimationEvent { + pipeline_id, + event_type, + node: key.node, + pseudo_element: key.pseudo_element.clone(), + property_or_animation_name: transition + .property_animation + .property_id() + .name() + .into(), + elapsed_time, + }); + } + + fn add_animation_event( + &self, + key: &AnimationSetKey, + animation: &Animation, + event_type: TransitionOrAnimationEventType, + now: f64, + pipeline_id: PipelineId, + ) { + let iteration_index = match animation.iteration_state { + KeyframesIterationState::Finite(current, _) | + KeyframesIterationState::Infinite(current) => current, + }; + + let active_duration = match animation.iteration_state { + KeyframesIterationState::Finite(_, max) => max * animation.duration, + KeyframesIterationState::Infinite(_) => std::f64::MAX, + }; + + // Calculate the `elapsed-time` property of the event and take the absolute + // value to prevent -0 values. + let elapsed_time = match event_type { + TransitionOrAnimationEventType::AnimationStart => { + (-animation.delay).max(0.).min(active_duration) + }, + TransitionOrAnimationEventType::AnimationIteration => { + iteration_index * animation.duration + }, + TransitionOrAnimationEventType::AnimationEnd => { + (iteration_index * animation.duration) + animation.current_iteration_duration() + }, + TransitionOrAnimationEventType::AnimationCancel => { + (iteration_index * animation.duration) + (now - animation.started_at).max(0.) + }, + _ => unreachable!(), + } + .abs(); + + self.pending_events + .borrow_mut() + .push(TransitionOrAnimationEvent { + pipeline_id, + event_type, + node: key.node, + pseudo_element: key.pseudo_element.clone(), + property_or_animation_name: animation.name.to_string(), + elapsed_time, + }); + } + + pub(crate) fn send_pending_events(&self) { + // Take all of the events here, in case sending one of these events + // triggers adding new events by forcing a layout. + let events = std::mem::replace(&mut *self.pending_events.borrow_mut(), Vec::new()); + + for event in events.into_iter() { + // We root the node here to ensure that sending this event doesn't + // unroot it as a side-effect. + let node = match self.rooted_nodes.borrow().get(&event.node) { + Some(node) => DomRoot::from_ref(&**node), + None => { + warn!("Tried to send an event for an unrooted node"); + continue; + }, + }; + + let event_atom = match event.event_type { + TransitionOrAnimationEventType::AnimationEnd => atom!("animationend"), + TransitionOrAnimationEventType::AnimationStart => atom!("animationstart"), + TransitionOrAnimationEventType::AnimationCancel => atom!("animationcancel"), + TransitionOrAnimationEventType::AnimationIteration => atom!("animationiteration"), + TransitionOrAnimationEventType::TransitionCancel => atom!("transitioncancel"), + TransitionOrAnimationEventType::TransitionEnd => atom!("transitionend"), + TransitionOrAnimationEventType::TransitionRun => atom!("transitionrun"), + TransitionOrAnimationEventType::TransitionStart => atom!("transitionstart"), + }; + let parent = EventInit { + bubbles: true, + cancelable: false, + }; + + let property_or_animation_name = + DOMString::from(event.property_or_animation_name.clone()); + let pseudo_element = event + .pseudo_element + .map_or_else(DOMString::new, |pseudo_element| { + DOMString::from(pseudo_element.to_css_string()) + }); + let elapsed_time = Finite::new(event.elapsed_time as f32).unwrap(); + let window = window_from_node(&*node); + + if event.event_type.is_transition_event() { + let event_init = TransitionEventInit { + parent, + propertyName: property_or_animation_name, + elapsedTime: elapsed_time, + pseudoElement: pseudo_element, + }; + TransitionEvent::new(&window, event_atom, &event_init) + .upcast::<Event>() + .fire(node.upcast()); + } else { + let event_init = AnimationEventInit { + parent, + animationName: property_or_animation_name, + elapsedTime: elapsed_time, + pseudoElement: pseudo_element, + }; + AnimationEvent::new(&window, event_atom, &event_init) + .upcast::<Event>() + .fire(node.upcast()); + } + } + } +} + +/// The type of transition event to trigger. These are defined by +/// CSS Transitions § 6.1 and CSS Animations § 4.2 +#[derive(Clone, Debug, Deserialize, JSTraceable, MallocSizeOf, Serialize)] +pub enum TransitionOrAnimationEventType { + /// "The transitionrun event occurs when a transition is created (i.e., when it + /// is added to the set of running transitions)." + TransitionRun, + /// "The transitionstart event occurs when a transition’s delay phase ends." + TransitionStart, + /// "The transitionend event occurs at the completion of the transition. In the + /// case where a transition is removed before completion, such as if the + /// transition-property is removed, then the event will not fire." + TransitionEnd, + /// "The transitioncancel event occurs when a transition is canceled." + TransitionCancel, + /// "The animationstart event occurs at the start of the animation. If there is + /// an animation-delay then this event will fire once the delay period has expired." + AnimationStart, + /// "The animationiteration event occurs at the end of each iteration of an + /// animation, except when an animationend event would fire at the same time." + AnimationIteration, + /// "The animationend event occurs when the animation finishes" + AnimationEnd, + /// "The animationcancel event occurs when the animation stops running in a way + /// that does not fire an animationend event..." + AnimationCancel, +} + +impl TransitionOrAnimationEventType { + /// Whether or not this event is a transition-related event. + pub fn is_transition_event(&self) -> bool { + match *self { + Self::TransitionRun | + Self::TransitionEnd | + Self::TransitionCancel | + Self::TransitionStart => true, + Self::AnimationEnd | + Self::AnimationIteration | + Self::AnimationStart | + Self::AnimationCancel => false, + } + } +} + +#[derive(Deserialize, JSTraceable, MallocSizeOf, Serialize)] +/// A transition or animation event. +pub struct TransitionOrAnimationEvent { + /// The pipeline id of the layout task that sent this message. + pub pipeline_id: PipelineId, + /// The type of transition event this should trigger. + pub event_type: TransitionOrAnimationEventType, + /// The address of the node which owns this transition. + pub node: OpaqueNode, + /// The pseudo element for this transition or animation, if applicable. + pub pseudo_element: Option<PseudoElement>, + /// The name of the property that is transitioning (in the case of a transition) + /// or the name of the animation (in the case of an animation). + pub property_or_animation_name: String, + /// The elapsed time property to send with this transition event. + pub elapsed_time: f64, +} |