blob: 73323f84f59d5b438c5957026aa4212f541e766b [file] [log] [blame] [edit]
/* 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/. */
//! The set of animations for a document.
use std::cell::Cell;
use base::id::PipelineId;
use constellation_traits::ScriptToConstellationMessage;
use cssparser::ToCss;
use embedder_traits::{AnimationState as AnimationsPresentState, UntrustedNodeAddress};
use fxhash::{FxHashMap, FxHashSet};
use libc::c_void;
use serde::{Deserialize, Serialize};
use style::animation::{
Animation, AnimationSetKey, AnimationState, DocumentAnimationSet, ElementAnimationSet,
KeyframesIterationState, Transition,
};
use style::dom::OpaqueNode;
use style::selector_parser::PseudoElement;
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::bindings::trace::NoTrace;
use crate::dom::event::Event;
use crate::dom::node::{Node, NodeDamage, NodeTraits, from_untrusted_node_address};
use crate::dom::transitionevent::TransitionEvent;
use crate::dom::window::Window;
use crate::script_runtime::CanGc;
/// The set of animations for a document.
#[derive(Default, JSTraceable, MallocSizeOf)]
#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
pub(crate) struct Animations {
/// The map of nodes to their animation states.
#[no_trace]
pub(crate) sets: DocumentAnimationSet,
/// Whether or not we have animations that are running.
has_running_animations: Cell<bool>,
/// A list of nodes with in-progress CSS transitions or pending events.
rooted_nodes: DomRefCell<FxHashMap<NoTrace<OpaqueNode>, Dom<Node>>>,
/// A list of pending animation-related events.
pending_events: DomRefCell<Vec<TransitionOrAnimationEvent>>,
/// The timeline value at the last time all animations were marked dirty.
/// This is used to prevent marking animations dirty when the timeline
/// has not changed.
timeline_value_at_last_dirty: Cell<f64>,
}
impl Animations {
pub(crate) fn new() -> Self {
Animations {
sets: Default::default(),
has_running_animations: Cell::new(false),
rooted_nodes: Default::default(),
pending_events: Default::default(),
timeline_value_at_last_dirty: Cell::new(0.0),
}
}
pub(crate) fn clear(&self) {
self.sets.sets.write().clear();
self.rooted_nodes.borrow_mut().clear();
self.pending_events.borrow_mut().clear();
}
// Mark all animations dirty, if they haven't been marked dirty since the
// specified `current_timeline_value`. Returns true if animations were marked
// dirty or false otherwise.
pub(crate) fn mark_animating_nodes_as_dirty(&self, current_timeline_value: f64) -> bool {
if current_timeline_value <= self.timeline_value_at_last_dirty.get() {
return false;
}
self.timeline_value_at_last_dirty
.set(current_timeline_value);
let sets = self.sets.sets.read();
let rooted_nodes = self.rooted_nodes.borrow();
for node in sets
.keys()
.filter_map(|key| rooted_nodes.get(&NoTrace(key.node)))
{
node.dirty(NodeDamage::Style);
}
true
}
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| {
if let Some(set) = animations.get_mut(&key) {
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 had_running_animations = self.has_running_animations.get();
if new_value == had_running_animations {
return;
}
self.has_running_animations.set(new_value);
self.handle_animation_presence_or_pending_events_change(window);
}
fn handle_animation_presence_or_pending_events_change(&self, window: &Window) {
let has_running_animations = self.has_running_animations.get();
let has_pending_events = !self.pending_events.borrow().is_empty();
// Do not send the NoAnimationCallbacksPresent state until all pending
// animation events are delivered.
let state = match has_running_animations || has_pending_events {
true => AnimationsPresentState::AnimationsPresent,
false => AnimationsPresentState::NoAnimationsPresent,
};
window.send_to_constellation(ScriptToConstellationMessage::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(&NoTrace(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(
NoTrace(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.0) || pending_events.iter().any(|event| event.node == node.0)
});
}
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,
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(_) => 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,
property_or_animation_name: animation.name.to_string(),
elapsed_time,
});
}
/// An implementation of the final steps of
/// <https://drafts.csswg.org/web-animations-1/#update-animations-and-send-events>.
pub(crate) fn send_pending_events(&self, window: &Window, can_gc: CanGc) {
// > 4. Let events to dispatch be a copy of doc’s pending animation event queue.
// > 5. Clear doc’s pending animation event queue.
//
// 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::take(&mut *self.pending_events.borrow_mut());
if events.is_empty() {
return;
}
// > 6. Perform a stable sort of the animation events in events to dispatch as follows:
// > 1. Sort the events by their scheduled event time such that events that were
// > scheduled to occur earlier sort before events scheduled to occur later, and
// > events whose scheduled event time is unresolved sort before events with a
// > resolved scheduled event time.
// > 2. Within events with equal scheduled event times, sort by their composite
// > order.
//
// TODO: Sorting of animation events isn't done yet.
// 7. Dispatch each of the events in events to dispatch at their corresponding
// target using the order established in the previous step.
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(&NoTrace(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,
composed: 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 = node.owner_window();
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, can_gc)
.upcast::<Event>()
.fire(node.upcast(), can_gc);
} else {
let event_init = AnimationEventInit {
parent,
animationName: property_or_animation_name,
elapsedTime: elapsed_time,
pseudoElement: pseudo_element,
};
AnimationEvent::new(&window, event_atom, &event_init, can_gc)
.upcast::<Event>()
.fire(node.upcast(), can_gc);
}
}
if self.pending_events.borrow().is_empty() {
self.handle_animation_presence_or_pending_events_change(window);
}
}
}
/// 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(crate) 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(crate) 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(crate) struct TransitionOrAnimationEvent {
/// The pipeline id of the layout task that sent this message.
#[no_trace]
pub(crate) pipeline_id: PipelineId,
/// The type of transition event this should trigger.
pub(crate) event_type: TransitionOrAnimationEventType,
/// The address of the node which owns this transition.
#[no_trace]
pub(crate) node: OpaqueNode,
/// The pseudo element for this transition or animation, if applicable.
#[no_trace]
pub(crate) 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(crate) property_or_animation_name: String,
/// The elapsed time property to send with this transition event.
pub(crate) elapsed_time: f64,
}