Auto merge of #26659 - mrobinson:events, r=jdm

Add support for remaining animation and transition events

This PR adds support for remaining animation and transitions events.
There are two commits here. The first is a bit more complicated: it reworks
how rooting is done for animating nodes. Instead of having the `ScriptThread`
try to track which animations are active via events (which can be inaccurate),
it just maintains roots for nodes that are actually present in the animations-
-related data structures. The second commit adds support for the new events.

Unfortunately, the existing events tests either rely on the Web Animations API
or other behavior (for example, that changing animation delay restarts
an animation). Since those two things are out-of-scope for this change,
I've forked some of the WPT tests, removed the reliance on the Web
Animations API, and added them to Servo's internal tests.

---
<!-- Thank you for contributing to Servo! Please replace each `[ ]` by `[X]` when the step is complete, and replace `___` with appropriate data: -->
- [x] `./mach build -d` does not report any errors
- [x] `./mach test-tidy` does not report any errors
- [x] These changes fix #21564.
- [x] There are tests for these changes OR

<!-- Also, please make sure that "Allow edits from maintainers" checkbox is checked, so that we can help you if you get stuck somewhere along the way.-->

<!-- Pull requests that do not address these steps are welcome, but they will require additional verification as part of the review process. -->
This commit is contained in:
bors-servo 2020-05-27 01:50:39 -04:00 committed by GitHub
commit 93a6c37836
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 763 additions and 399 deletions

View file

@ -2,6 +2,7 @@ DOMContentLoaded
abort abort
activate activate
addtrack addtrack
animationcancel
animationend animationend
animationiteration animationiteration
animationstart animationstart
@ -132,6 +133,7 @@ track
transitioncancel transitioncancel
transitionend transitionend
transitionrun transitionrun
transitionstart
unhandledrejection unhandledrejection
unload unload
url url

View file

@ -6,85 +6,132 @@
//! The set of animations for a document. //! 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 crate::dom::window::Window;
use fxhash::FxHashMap; use fxhash::FxHashMap;
use libc::c_void; use libc::c_void;
use malloc_size_of::{MallocSizeOf, MallocSizeOfOps};
use msg::constellation_msg::PipelineId; use msg::constellation_msg::PipelineId;
use parking_lot::RwLock; use parking_lot::RwLock;
use script_traits::{AnimationState as AnimationsPresentState, ScriptMsg, UntrustedNodeAddress}; use script_traits::{AnimationState as AnimationsPresentState, ScriptMsg, UntrustedNodeAddress};
use servo_arc::Arc; use servo_arc::Arc;
use style::animation::{AnimationState, ElementAnimationSet}; use std::cell::Cell;
use style::animation::{
Animation, AnimationState, ElementAnimationSet, KeyframesIterationState, Transition,
};
use style::dom::OpaqueNode; use style::dom::OpaqueNode;
/// The set of animations for a document. /// The set of animations for a document.
/// #[derive(Default, JSTraceable, MallocSizeOf)]
/// Make sure to update the MallocSizeOf implementation when changing the #[unrooted_must_root_lint::must_root]
/// contents of this struct.
#[derive(Clone, Debug, Default, JSTraceable)]
pub(crate) struct Animations { pub(crate) struct Animations {
/// The map of nodes to their animation states.
#[ignore_malloc_size_of = "Arc is hard"]
pub sets: Arc<RwLock<FxHashMap<OpaqueNode, ElementAnimationSet>>>, pub sets: Arc<RwLock<FxHashMap<OpaqueNode, ElementAnimationSet>>>,
have_running_animations: bool,
/// 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 { impl Animations {
pub(crate) fn new() -> Self { pub(crate) fn new() -> Self {
Animations { Animations {
sets: Default::default(), sets: Default::default(),
have_running_animations: false, have_running_animations: Cell::new(false),
rooted_nodes: Default::default(),
pending_events: Default::default(),
} }
} }
pub(crate) fn update_for_new_timeline_value( pub(crate) fn clear(&self) {
&mut self, self.sets.write().clear();
window: &Window, self.rooted_nodes.borrow_mut().clear();
now: f64, self.pending_events.borrow_mut().clear();
) -> AnimationsUpdate { }
let mut update = AnimationsUpdate::new(window.pipeline_id());
pub(crate) fn mark_animating_nodes_as_dirty(&self) {
let sets = self.sets.read();
let rooted_nodes = self.rooted_nodes.borrow();
for node in sets.keys().filter_map(|node| rooted_nodes.get(&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.write(); let mut sets = self.sets.write();
for set in sets.values_mut() { for set in sets.values_mut() {
self.start_pending_animations(set, now, pipeline_id);
// When necessary, iterate our running animations to the next iteration. // When necessary, iterate our running animations to the next iteration.
for animation in set.animations.iter_mut() { for animation in set.animations.iter_mut() {
if animation.iterate_if_necessary(now) { if animation.iterate_if_necessary(now) {
update.add_event( self.add_animation_event(
animation.node, animation,
animation.name.to_string(),
TransitionOrAnimationEventType::AnimationIteration, TransitionOrAnimationEventType::AnimationIteration,
animation.active_duration(), now,
pipeline_id,
); );
} }
} }
Self::finish_running_animations(set, now, &mut update); self.finish_running_animations(set, now, pipeline_id);
} }
update
self.unroot_unused_nodes(&sets);
} }
/// Processes any new animations that were discovered after reflow. Collect messages /// Processes any new animations that were discovered after reflow. Collect messages
/// that trigger events for any animations that changed state. /// that trigger events for any animations that changed state.
/// TODO(mrobinson): The specification dictates that this should happen before reflow. pub(crate) fn do_post_reflow_update(&self, window: &Window, now: f64) {
pub(crate) fn do_post_reflow_update(&mut self, window: &Window, now: f64) -> AnimationsUpdate { let pipeline_id = window.pipeline_id();
let mut update = AnimationsUpdate::new(window.pipeline_id()); let mut sets = self.sets.write();
self.root_newly_animating_dom_nodes(&sets, window);
{ for set in sets.values_mut() {
let mut sets = self.sets.write(); self.handle_canceled_animations(set, now, pipeline_id);
update.collect_newly_animating_nodes(&sets); self.handle_new_animations(set, now, pipeline_id);
for set in sets.values_mut() {
Self::handle_canceled_animations(set, now, &mut update);
Self::handle_new_animations(set, &mut update);
}
// 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());
} }
self.update_running_animations_presence(window); // 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());
update 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 { pub(crate) fn running_animation_count(&self) -> usize {
@ -95,40 +142,55 @@ impl Animations {
.sum() .sum()
} }
fn update_running_animations_presence(&mut self, window: &Window) { /// Walk through the list of pending animations and start all of the ones that
let have_running_animations = self /// have left the delay phase.
.sets fn start_pending_animations(
.read() &self,
.values() set: &mut ElementAnimationSet,
.any(|state| state.needs_animation_ticks()); now: f64,
if have_running_animations == self.have_running_animations { pipeline_id: PipelineId,
return; ) {
for animation in set.animations.iter_mut() {
if animation.state == AnimationState::Pending && animation.started_at <= now {
animation.state = AnimationState::Running;
self.add_animation_event(
animation,
TransitionOrAnimationEventType::AnimationStart,
now,
pipeline_id,
);
}
} }
self.have_running_animations = have_running_animations; for transition in set.transitions.iter_mut() {
let state = match have_running_animations { if transition.state == AnimationState::Pending && transition.start_time <= now {
true => AnimationsPresentState::AnimationsPresent, transition.state = AnimationState::Running;
false => AnimationsPresentState::NoAnimationsPresent, self.add_transition_event(
}; transition,
TransitionOrAnimationEventType::TransitionStart,
window.send_to_constellation(ScriptMsg::ChangeRunningAnimationsState(state)); now,
pipeline_id,
);
}
}
} }
/// Walk through the list of running animations and remove all of the ones that /// Walk through the list of running animations and remove all of the ones that
/// have ended. /// have ended.
fn finish_running_animations( fn finish_running_animations(
&self,
set: &mut ElementAnimationSet, set: &mut ElementAnimationSet,
now: f64, now: f64,
update: &mut AnimationsUpdate, pipeline_id: PipelineId,
) { ) {
for animation in set.animations.iter_mut() { for animation in set.animations.iter_mut() {
if animation.state == AnimationState::Running && animation.has_ended(now) { if animation.state == AnimationState::Running && animation.has_ended(now) {
animation.state = AnimationState::Finished; animation.state = AnimationState::Finished;
update.add_event( self.add_animation_event(
animation.node, animation,
animation.name.to_string(),
TransitionOrAnimationEventType::AnimationEnd, TransitionOrAnimationEventType::AnimationEnd,
animation.active_duration(), now,
pipeline_id,
); );
} }
} }
@ -136,11 +198,11 @@ impl Animations {
for transition in set.transitions.iter_mut() { for transition in set.transitions.iter_mut() {
if transition.state == AnimationState::Running && transition.has_ended(now) { if transition.state == AnimationState::Running && transition.has_ended(now) {
transition.state = AnimationState::Finished; transition.state = AnimationState::Finished;
update.add_event( self.add_transition_event(
transition.node, transition,
transition.property_animation.property_id().name().into(),
TransitionOrAnimationEventType::TransitionEnd, TransitionOrAnimationEventType::TransitionEnd,
transition.property_animation.duration, now,
pipeline_id,
); );
} }
} }
@ -150,161 +212,284 @@ impl Animations {
/// transitions, but eventually this should handle canceled CSS animations as /// transitions, but eventually this should handle canceled CSS animations as
/// well. /// well.
fn handle_canceled_animations( fn handle_canceled_animations(
&self,
set: &mut ElementAnimationSet, set: &mut ElementAnimationSet,
now: f64, now: f64,
update: &mut AnimationsUpdate, pipeline_id: PipelineId,
) { ) {
for transition in &set.transitions { for transition in &set.transitions {
if transition.state == AnimationState::Canceled { if transition.state == AnimationState::Canceled {
// TODO(mrobinson): We need to properly compute the elapsed_time here self.add_transition_event(
// according to https://drafts.csswg.org/css-transitions/#event-transitionevent transition,
update.add_event(
transition.node,
transition.property_animation.property_id().name().into(),
TransitionOrAnimationEventType::TransitionCancel, TransitionOrAnimationEventType::TransitionCancel,
(now - transition.start_time).max(0.), now,
pipeline_id,
);
}
}
for animation in &set.animations {
if animation.state == AnimationState::Canceled {
self.add_animation_event(
animation,
TransitionOrAnimationEventType::AnimationCancel,
now,
pipeline_id,
); );
} }
} }
// TODO(mrobinson): We need to send animationcancel events.
set.clear_canceled_animations(); set.clear_canceled_animations();
} }
fn handle_new_animations(set: &mut ElementAnimationSet, update: &mut AnimationsUpdate) { fn handle_new_animations(
&self,
set: &mut ElementAnimationSet,
now: f64,
pipeline_id: PipelineId,
) {
for animation in set.animations.iter_mut() { for animation in set.animations.iter_mut() {
animation.is_new = false; animation.is_new = false;
} }
for transition in set.transitions.iter_mut() { for transition in set.transitions.iter_mut() {
if transition.is_new { if transition.is_new {
// TODO(mrobinson): We need to properly compute the elapsed_time here self.add_transition_event(
// according to https://drafts.csswg.org/css-transitions/#event-transitionevent transition,
update.add_event(
transition.node,
transition.property_animation.property_id().name().into(),
TransitionOrAnimationEventType::TransitionRun, TransitionOrAnimationEventType::TransitionRun,
0., now,
pipeline_id,
); );
transition.is_new = false; transition.is_new = false;
} }
} }
} }
}
impl MallocSizeOf for Animations { /// Ensure that all nodes with new animations are rooted. This should be called
fn size_of(&self, ops: &mut MallocSizeOfOps) -> usize { /// immediately after a restyle, to ensure that these addresses are still valid.
self.sets.read().size_of(ops) + self.have_running_animations.size_of(ops) #[allow(unsafe_code)]
} fn root_newly_animating_dom_nodes(
} &self,
sets: &FxHashMap<OpaqueNode, ElementAnimationSet>,
window: &Window,
) {
let js_runtime = window.get_js_runtime().as_ref().unwrap().rt();
let mut rooted_nodes = self.rooted_nodes.borrow_mut();
for (opaque_node, set) in sets.iter() {
if rooted_nodes.contains_key(opaque_node) {
continue;
}
pub(crate) struct AnimationsUpdate { if set.animations.iter().any(|animation| animation.is_new) ||
pub pipeline_id: PipelineId, set.transitions.iter().any(|transition| transition.is_new)
pub events: Vec<TransitionOrAnimationEvent>, {
pub newly_animating_nodes: Vec<UntrustedNodeAddress>, let address = UntrustedNodeAddress(opaque_node.0 as *const c_void);
} unsafe {
rooted_nodes.insert(
impl AnimationsUpdate { opaque_node.clone(),
fn new(pipeline_id: PipelineId) -> Self { Dom::from_ref(&*from_untrusted_node_address(js_runtime, address)),
AnimationsUpdate { )
pipeline_id, };
events: Default::default(), }
newly_animating_nodes: Default::default(),
} }
} }
fn add_event( // Unroot any nodes that we have rooted but are no longer tracking animations for.
&mut self, fn unroot_unused_nodes(&self, sets: &FxHashMap<OpaqueNode, ElementAnimationSet>) {
node: OpaqueNode, let pending_events = self.pending_events.borrow();
property_or_animation_name: String, self.rooted_nodes.borrow_mut().retain(|key, _| {
event_type: TransitionOrAnimationEventType, sets.contains_key(key) || pending_events.iter().any(|event| event.node == *key)
elapsed_time: f64,
) {
let node = UntrustedNodeAddress(node.0 as *const c_void);
self.events.push(TransitionOrAnimationEvent {
pipeline_id: self.pipeline_id,
event_type,
node,
property_or_animation_name,
elapsed_time,
}); });
} }
pub(crate) fn is_empty(&self) -> bool { fn add_transition_event(
self.events.is_empty() && self.newly_animating_nodes.is_empty() &self,
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: transition.node.clone(),
property_or_animation_name: transition
.property_animation
.property_id()
.name()
.into(),
elapsed_time,
});
} }
/// Collect newly animating nodes, which is used by the script process during fn add_animation_event(
/// forced, synchronous reflows to root DOM nodes for the duration of their &self,
/// animations or transitions. animation: &Animation,
/// TODO(mrobinson): Look into handling the rooting inside this class. event_type: TransitionOrAnimationEventType,
fn collect_newly_animating_nodes( now: f64,
&mut self, pipeline_id: PipelineId,
animation_states: &FxHashMap<OpaqueNode, ElementAnimationSet>,
) { ) {
// This extends the output vector with an iterator that contains a copy of the node let num_iterations = match animation.iteration_state {
// address for every new animation. The script thread currently stores a rooted node KeyframesIterationState::Finite(current, _) |
// for every property that is transitioning. The current strategy of repeating the KeyframesIterationState::Infinite(current) => current,
// node address is a holdover from when the code here looked different. };
self.newly_animating_nodes
.extend(animation_states.iter().flat_map(|(node, state)| {
let mut num_new_animations = state
.animations
.iter()
.filter(|animation| animation.is_new)
.count();
num_new_animations += state
.transitions
.iter()
.filter(|transition| transition.is_new)
.count();
let node = UntrustedNodeAddress(node.0 as *const c_void); let active_duration = match animation.iteration_state {
std::iter::repeat(node).take(num_new_animations) 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 |
TransitionOrAnimationEventType::AnimationEnd => num_iterations * animation.duration,
TransitionOrAnimationEventType::AnimationCancel => {
(num_iterations * animation.duration) + (now - animation.started_at).max(0.)
},
_ => unreachable!(),
}
.abs();
self.pending_events
.borrow_mut()
.push(TransitionOrAnimationEvent {
pipeline_id,
event_type,
node: animation.node.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,
};
// TODO: Handle pseudo-elements properly
let property_or_animation_name =
DOMString::from(event.property_or_animation_name.clone());
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: DOMString::new(),
};
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: DOMString::new(),
};
AnimationEvent::new(&window, event_atom, &event_init)
.upcast::<Event>()
.fire(node.upcast());
}
}
} }
} }
/// The type of transition event to trigger. These are defined by /// The type of transition event to trigger. These are defined by
/// CSS Transitions § 6.1 and CSS Animations § 4.2 /// CSS Transitions § 6.1 and CSS Animations § 4.2
#[derive(Clone, Debug, Deserialize, JSTraceable, Serialize)] #[derive(Clone, Debug, Deserialize, JSTraceable, MallocSizeOf, Serialize)]
pub enum TransitionOrAnimationEventType { pub enum TransitionOrAnimationEventType {
/// "The transitionrun event occurs when a transition is created (i.e., when it /// "The transitionrun event occurs when a transition is created (i.e., when it
/// is added to the set of running transitions)." /// is added to the set of running transitions)."
TransitionRun, TransitionRun,
/// "The transitionstart event occurs when a transitions delay phase ends."
TransitionStart,
/// "The transitionend event occurs at the completion of the transition. In the /// "The transitionend event occurs at the completion of the transition. In the
/// case where a transition is removed before completion, such as if the /// case where a transition is removed before completion, such as if the
/// transition-property is removed, then the event will not fire." /// transition-property is removed, then the event will not fire."
TransitionEnd, TransitionEnd,
/// "The transitioncancel event occurs when a transition is canceled." /// "The transitioncancel event occurs when a transition is canceled."
TransitionCancel, TransitionCancel,
/// "The animationend event occurs when the animation finishes" /// "The animationstart event occurs at the start of the animation. If there is
AnimationEnd, /// 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 /// "The animationiteration event occurs at the end of each iteration of an
/// animation, except when an animationend event would fire at the same time." /// animation, except when an animationend event would fire at the same time."
AnimationIteration, 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 { impl TransitionOrAnimationEventType {
/// Whether or not this event finalizes the animation or transition. During finalization
/// the DOM object associated with this transition or animation is unrooted.
pub fn finalizes_transition_or_animation(&self) -> bool {
match *self {
Self::TransitionEnd | Self::TransitionCancel | Self::AnimationEnd => true,
Self::TransitionRun | Self::AnimationIteration => false,
}
}
/// Whether or not this event is a transition-related event. /// Whether or not this event is a transition-related event.
pub fn is_transition_event(&self) -> bool { pub fn is_transition_event(&self) -> bool {
match *self { match *self {
Self::TransitionRun | Self::TransitionEnd | Self::TransitionCancel => true, Self::TransitionRun |
Self::AnimationEnd | Self::AnimationIteration => false, Self::TransitionEnd |
Self::TransitionCancel |
Self::TransitionStart => true,
Self::AnimationEnd |
Self::AnimationIteration |
Self::AnimationStart |
Self::AnimationCancel => false,
} }
} }
} }
#[derive(Deserialize, JSTraceable, Serialize)] #[derive(Deserialize, JSTraceable, MallocSizeOf, Serialize)]
/// A transition or animation event. /// A transition or animation event.
pub struct TransitionOrAnimationEvent { pub struct TransitionOrAnimationEvent {
/// The pipeline id of the layout task that sent this message. /// The pipeline id of the layout task that sent this message.
@ -312,7 +497,7 @@ pub struct TransitionOrAnimationEvent {
/// The type of transition event this should trigger. /// The type of transition event this should trigger.
pub event_type: TransitionOrAnimationEventType, pub event_type: TransitionOrAnimationEventType,
/// The address of the node which owns this transition. /// The address of the node which owns this transition.
pub node: UntrustedNodeAddress, pub node: OpaqueNode,
/// The name of the property that is transitioning (in the case of a transition) /// 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). /// or the name of the animation (in the case of an animation).
pub property_or_animation_name: String, pub property_or_animation_name: String,

View file

@ -3,7 +3,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use crate::animation_timeline::AnimationTimeline; use crate::animation_timeline::AnimationTimeline;
use crate::animations::{Animations, AnimationsUpdate}; use crate::animations::Animations;
use crate::document_loader::{DocumentLoader, LoadType}; use crate::document_loader::{DocumentLoader, LoadType};
use crate::dom::attr::Attr; use crate::dom::attr::Attr;
use crate::dom::beforeunloadevent::BeforeUnloadEvent; use crate::dom::beforeunloadevent::BeforeUnloadEvent;
@ -3750,15 +3750,15 @@ impl Document {
.collect() .collect()
} }
pub(crate) fn advance_animation_timeline_for_testing(&self, delta: f64) -> AnimationsUpdate { pub(crate) fn advance_animation_timeline_for_testing(&self, delta: f64) {
self.animation_timeline.borrow_mut().advance_specific(delta); self.animation_timeline.borrow_mut().advance_specific(delta);
let current_timeline_value = self.current_animation_timeline_value(); let current_timeline_value = self.current_animation_timeline_value();
self.animations self.animations
.borrow_mut() .borrow()
.update_for_new_timeline_value(&self.window, current_timeline_value) .update_for_new_timeline_value(&self.window, current_timeline_value);
} }
pub(crate) fn update_animation_timeline(&self) -> AnimationsUpdate { pub(crate) fn update_animation_timeline(&self) {
// Only update the time if it isn't being managed by a test. // Only update the time if it isn't being managed by a test.
if !pref!(layout.animations.test.enabled) { if !pref!(layout.animations.test.enabled) {
self.animation_timeline.borrow_mut().update(); self.animation_timeline.borrow_mut().update();
@ -3768,8 +3768,8 @@ impl Document {
// value might have been advanced previously via the TestBinding. // value might have been advanced previously via the TestBinding.
let current_timeline_value = self.current_animation_timeline_value(); let current_timeline_value = self.current_animation_timeline_value();
self.animations self.animations
.borrow_mut() .borrow()
.update_for_new_timeline_value(&self.window, current_timeline_value) .update_for_new_timeline_value(&self.window, current_timeline_value);
} }
pub(crate) fn current_animation_timeline_value(&self) -> f64 { pub(crate) fn current_animation_timeline_value(&self) -> f64 {
@ -3780,10 +3780,10 @@ impl Document {
self.animations.borrow() self.animations.borrow()
} }
pub(crate) fn update_animations_post_reflow(&self) -> AnimationsUpdate { pub(crate) fn update_animations_post_reflow(&self) {
self.animations self.animations
.borrow_mut() .borrow()
.do_post_reflow_update(&self.window, self.current_animation_timeline_value()) .do_post_reflow_update(&self.window, self.current_animation_timeline_value());
} }
} }

View file

@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * 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/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::cell::{DomRefCell, Ref};
use crate::dom::bindings::codegen::Bindings::DocumentBinding::{ use crate::dom::bindings::codegen::Bindings::DocumentBinding::{
DocumentMethods, DocumentReadyState, DocumentMethods, DocumentReadyState,
}; };
@ -410,6 +410,10 @@ impl Window {
unsafe { JSContext::from_ptr(self.js_runtime.borrow().as_ref().unwrap().cx()) } unsafe { JSContext::from_ptr(self.js_runtime.borrow().as_ref().unwrap().cx()) }
} }
pub fn get_js_runtime(&self) -> Ref<Option<Rc<Runtime>>> {
self.js_runtime.borrow()
}
pub fn main_thread_script_chan(&self) -> &Sender<MainThreadScriptMsg> { pub fn main_thread_script_chan(&self) -> &Sender<MainThreadScriptMsg> {
&self.script_chan.0 &self.script_chan.0
} }
@ -1581,12 +1585,8 @@ impl Window {
#[allow(unsafe_code)] #[allow(unsafe_code)]
pub fn advance_animation_clock(&self, delta_ms: i32) { pub fn advance_animation_clock(&self, delta_ms: i32) {
let pipeline_id = self.upcast::<GlobalScope>().pipeline_id(); let pipeline_id = self.upcast::<GlobalScope>().pipeline_id();
let update = self self.Document()
.Document()
.advance_animation_timeline_for_testing(delta_ms as f64 / 1000.); .advance_animation_timeline_for_testing(delta_ms as f64 / 1000.);
unsafe {
ScriptThread::process_animations_update(update);
}
ScriptThread::handle_tick_all_animations_for_testing(pipeline_id); ScriptThread::handle_tick_all_animations_for_testing(pipeline_id);
} }
@ -1752,10 +1752,7 @@ impl Window {
} }
} }
let update = document.update_animations_post_reflow(); document.update_animations_post_reflow();
unsafe {
ScriptThread::process_animations_update(update);
}
true true
} }

View file

@ -17,26 +17,18 @@
//! a page runs its course and the script thread returns to processing events in the main event //! a page runs its course and the script thread returns to processing events in the main event
//! loop. //! loop.
use crate::animations::{
AnimationsUpdate, TransitionOrAnimationEvent, TransitionOrAnimationEventType,
};
use crate::devtools; use crate::devtools;
use crate::document_loader::DocumentLoader; use crate::document_loader::DocumentLoader;
use crate::dom::animationevent::AnimationEvent;
use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::codegen::Bindings::AnimationEventBinding::AnimationEventInit;
use crate::dom::bindings::codegen::Bindings::DocumentBinding::{ use crate::dom::bindings::codegen::Bindings::DocumentBinding::{
DocumentMethods, DocumentReadyState, DocumentMethods, DocumentReadyState,
}; };
use crate::dom::bindings::codegen::Bindings::EventBinding::EventInit;
use crate::dom::bindings::codegen::Bindings::NavigatorBinding::NavigatorMethods; use crate::dom::bindings::codegen::Bindings::NavigatorBinding::NavigatorMethods;
use crate::dom::bindings::codegen::Bindings::TransitionEventBinding::TransitionEventInit;
use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods; use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
use crate::dom::bindings::conversions::{ use crate::dom::bindings::conversions::{
ConversionResult, FromJSValConvertible, StringificationBehavior, ConversionResult, FromJSValConvertible, StringificationBehavior,
}; };
use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::num::Finite;
use crate::dom::bindings::refcounted::Trusted; use crate::dom::bindings::refcounted::Trusted;
use crate::dom::bindings::reflector::DomObject; use crate::dom::bindings::reflector::DomObject;
use crate::dom::bindings::root::ThreadLocalStackRoots; use crate::dom::bindings::root::ThreadLocalStackRoots;
@ -57,14 +49,11 @@ use crate::dom::htmlanchorelement::HTMLAnchorElement;
use crate::dom::htmliframeelement::{HTMLIFrameElement, NavigationType}; use crate::dom::htmliframeelement::{HTMLIFrameElement, NavigationType};
use crate::dom::identityhub::Identities; use crate::dom::identityhub::Identities;
use crate::dom::mutationobserver::MutationObserver; use crate::dom::mutationobserver::MutationObserver;
use crate::dom::node::{ use crate::dom::node::{window_from_node, Node, ShadowIncluding};
from_untrusted_node_address, window_from_node, Node, NodeDamage, ShadowIncluding,
};
use crate::dom::performanceentry::PerformanceEntry; use crate::dom::performanceentry::PerformanceEntry;
use crate::dom::performancepainttiming::PerformancePaintTiming; use crate::dom::performancepainttiming::PerformancePaintTiming;
use crate::dom::serviceworker::TrustedServiceWorkerAddress; use crate::dom::serviceworker::TrustedServiceWorkerAddress;
use crate::dom::servoparser::{ParserContext, ServoParser}; use crate::dom::servoparser::{ParserContext, ServoParser};
use crate::dom::transitionevent::TransitionEvent;
use crate::dom::uievent::UIEvent; use crate::dom::uievent::UIEvent;
use crate::dom::window::{ReflowReason, Window}; use crate::dom::window::{ReflowReason, Window};
use crate::dom::windowproxy::{CreatorBrowsingContextInfo, WindowProxy}; use crate::dom::windowproxy::{CreatorBrowsingContextInfo, WindowProxy};
@ -636,13 +625,6 @@ pub struct ScriptThread {
/// resources during a turn of the event loop. /// resources during a turn of the event loop.
docs_with_no_blocking_loads: DomRefCell<HashSet<Dom<Document>>>, docs_with_no_blocking_loads: DomRefCell<HashSet<Dom<Document>>>,
/// A list of nodes with in-progress CSS transitions, which roots them for the duration
/// of the transition.
animating_nodes: DomRefCell<HashMap<PipelineId, Vec<Dom<Node>>>>,
/// Animations events that are pending to be sent.
animation_events: RefCell<Vec<TransitionOrAnimationEvent>>,
/// <https://html.spec.whatwg.org/multipage/#custom-element-reactions-stack> /// <https://html.spec.whatwg.org/multipage/#custom-element-reactions-stack>
custom_element_reaction_stack: CustomElementReactionStack, custom_element_reaction_stack: CustomElementReactionStack,
@ -831,40 +813,6 @@ impl ScriptThread {
}) })
} }
/// Consume the list of pointer addresses corresponding to DOM nodes that are animating
/// and root them in a per-pipeline list of nodes.
///
/// Unsafety: any pointer to invalid memory (ie. a GCed node) will trigger a crash.
/// TODO: ensure caller uses rooted nodes instead of unsafe node addresses.
pub(crate) unsafe fn process_animations_update(mut update: AnimationsUpdate) {
if update.is_empty() {
return;
}
SCRIPT_THREAD_ROOT.with(|root| {
let script_thread = &*root.get().unwrap();
if !update.events.is_empty() {
script_thread
.animation_events
.borrow_mut()
.append(&mut update.events);
}
let js_runtime = script_thread.js_runtime.rt();
let new_nodes = update
.newly_animating_nodes
.into_iter()
.map(|n| Dom::from_ref(&*from_untrusted_node_address(js_runtime, n)));
script_thread
.animating_nodes
.borrow_mut()
.entry(update.pipeline_id)
.or_insert_with(Vec::new)
.extend(new_nodes);
})
}
pub fn set_mutation_observer_microtask_queued(value: bool) { pub fn set_mutation_observer_microtask_queued(value: bool) {
SCRIPT_THREAD_ROOT.with(|root| { SCRIPT_THREAD_ROOT.with(|root| {
let script_thread = unsafe { &*root.get().unwrap() }; let script_thread = unsafe { &*root.get().unwrap() };
@ -1363,9 +1311,6 @@ impl ScriptThread {
docs_with_no_blocking_loads: Default::default(), docs_with_no_blocking_loads: Default::default(),
animating_nodes: Default::default(),
animation_events: Default::default(),
custom_element_reaction_stack: CustomElementReactionStack::new(), custom_element_reaction_stack: CustomElementReactionStack::new(),
webrender_document: state.webrender_document, webrender_document: state.webrender_document,
@ -1644,19 +1589,13 @@ impl ScriptThread {
} }
// Perform step 11.10 from https://html.spec.whatwg.org/multipage/#event-loops. // Perform step 11.10 from https://html.spec.whatwg.org/multipage/#event-loops.
// TODO(mrobinson): This should also update the current animations to conform to
// the HTML specification.
fn update_animations_and_send_events(&self) { fn update_animations_and_send_events(&self) {
// We remove the events because handling these events might trigger for (_, document) in self.documents.borrow().iter() {
// a reflow which might want to add more events to the queue. document.animations().send_pending_events();
let events = self.animation_events.replace(Vec::new());
for event in events.into_iter() {
self.handle_transition_or_animation_event(&event);
} }
for (_, document) in self.documents.borrow().iter() { for (_, document) in self.documents.borrow().iter() {
let update = document.update_animation_timeline(); document.update_animation_timeline();
unsafe { ScriptThread::process_animations_update(update) };
} }
} }
@ -2860,11 +2799,11 @@ impl ScriptThread {
.send((id, ScriptMsg::PipelineExited)) .send((id, ScriptMsg::PipelineExited))
.ok(); .ok();
// Remove any rooted nodes for active animations and transitions.
self.animating_nodes.borrow_mut().remove(&id);
// Now that layout is shut down, it's OK to remove the document. // Now that layout is shut down, it's OK to remove the document.
if let Some(document) = document { if let Some(document) = document {
// Clear any active animations and unroot all of the associated DOM objects.
document.animations().clear();
// We don't want to dispatch `mouseout` event pointing to non-existing element // We don't want to dispatch `mouseout` event pointing to non-existing element
if let Some(target) = self.topmost_mouse_over_target.get() { if let Some(target) = self.topmost_mouse_over_target.get() {
if target.upcast::<Node>().owner_doc() == document { if target.upcast::<Node>().owner_doc() == document {
@ -2940,99 +2879,11 @@ impl ScriptThread {
document.run_the_animation_frame_callbacks(); document.run_the_animation_frame_callbacks();
} }
if tick_type.contains(AnimationTickType::CSS_ANIMATIONS_AND_TRANSITIONS) { if tick_type.contains(AnimationTickType::CSS_ANIMATIONS_AND_TRANSITIONS) {
match self.animating_nodes.borrow().get(&id) { document.animations().mark_animating_nodes_as_dirty();
Some(nodes) => {
for node in nodes.iter() {
node.dirty(NodeDamage::NodeStyleDamaged);
}
},
None => return,
}
document.window().add_pending_reflow(); document.window().add_pending_reflow();
} }
} }
/// Handles firing of transition-related events.
///
/// TODO(mrobinson): Add support for more events.
fn handle_transition_or_animation_event(&self, event: &TransitionOrAnimationEvent) {
// We limit the scope of the borrow here so that we aren't holding it when
// sending events. Event handlers may trigger another layout, resulting in
// a double mutable borrow of `animating_nodes`.
let node = {
let mut animating_nodes = self.animating_nodes.borrow_mut();
let nodes = match animating_nodes.get_mut(&event.pipeline_id) {
Some(nodes) => nodes,
None => {
return warn!(
"Ignoring transition event for pipeline without animating nodes."
);
},
};
let node_index = nodes
.iter()
.position(|n| n.to_untrusted_node_address() == event.node);
let node_index = match node_index {
Some(node_index) => node_index,
None => {
// If no index is found, we can't know whether this node is safe to use.
// It's better not to fire a DOM event than crash.
warn!("Ignoring transition event for unknown node.");
return;
},
};
// We need to root the node now, because if we remove it from the map
// a garbage collection might clean it up while we are sending events.
let node = DomRoot::from_ref(&*nodes[node_index]);
if event.event_type.finalizes_transition_or_animation() {
nodes.remove(node_index);
}
node
};
let event_atom = match event.event_type {
TransitionOrAnimationEventType::AnimationEnd => atom!("animationend"),
TransitionOrAnimationEventType::AnimationIteration => atom!("animationiteration"),
TransitionOrAnimationEventType::TransitionCancel => atom!("transitioncancel"),
TransitionOrAnimationEventType::TransitionEnd => atom!("transitionend"),
TransitionOrAnimationEventType::TransitionRun => atom!("transitionrun"),
};
let parent = EventInit {
bubbles: true,
cancelable: false,
};
// TODO: Handle pseudo-elements properly
let property_or_animation_name = DOMString::from(event.property_or_animation_name.clone());
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: DOMString::new(),
};
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: DOMString::new(),
};
AnimationEvent::new(&window, event_atom, &event_init)
.upcast::<Event>()
.fire(node.upcast());
}
}
/// Handles a Web font being loaded. Does nothing if the page no longer exists. /// Handles a Web font being loaded. Does nothing if the page no longer exists.
fn handle_web_font_loaded(&self, pipeline_id: PipelineId) { fn handle_web_font_loaded(&self, pipeline_id: PipelineId) {
let document = self.documents.borrow().find_document(pipeline_id); let document = self.documents.borrow().find_document(pipeline_id);

View file

@ -139,17 +139,27 @@ impl PropertyAnimation {
/// This structure represents the state of an animation. /// This structure represents the state of an animation.
#[derive(Clone, Debug, MallocSizeOf, PartialEq)] #[derive(Clone, Debug, MallocSizeOf, PartialEq)]
pub enum AnimationState { pub enum AnimationState {
/// The animation has been created, but is not running yet. This state
/// is also used when an animation is still in the first delay phase.
Pending,
/// This animation is currently running.
Running,
/// This animation is paused. The inner field is the percentage of progress /// This animation is paused. The inner field is the percentage of progress
/// when it was paused, from 0 to 1. /// when it was paused, from 0 to 1.
Paused(f64), Paused(f64),
/// This animation is currently running.
Running,
/// This animation has finished. /// This animation has finished.
Finished, Finished,
/// This animation has been canceled. /// This animation has been canceled.
Canceled, Canceled,
} }
impl AnimationState {
/// Whether or not this state requires its owning animation to be ticked.
fn needs_to_be_ticked(&self) -> bool {
*self == AnimationState::Running || *self == AnimationState::Pending
}
}
/// This structure represents a keyframes animation current iteration state. /// This structure represents a keyframes animation current iteration state.
/// ///
/// If the iteration count is infinite, there's no other state, otherwise we /// If the iteration count is infinite, there's no other state, otherwise we
@ -174,7 +184,8 @@ pub struct Animation {
/// The internal animation from the style system. /// The internal animation from the style system.
pub keyframes_animation: KeyframesAnimation, pub keyframes_animation: KeyframesAnimation,
/// The time this animation started at. /// The time this animation started at, which is the current value of the animation
/// timeline when this animation was created plus any animation delay.
pub started_at: f64, pub started_at: f64,
/// The duration of this animation. /// The duration of this animation.
@ -273,9 +284,11 @@ impl Animation {
/// canceled due to changes in the style. /// canceled due to changes in the style.
pub fn has_ended(&self, time: f64) -> bool { pub fn has_ended(&self, time: f64) -> bool {
match self.state { match self.state {
AnimationState::Canceled | AnimationState::Paused(_) => return false,
AnimationState::Finished => return true,
AnimationState::Running => {}, AnimationState::Running => {},
AnimationState::Finished => return true,
AnimationState::Pending | AnimationState::Canceled | AnimationState::Paused(_) => {
return false
},
} }
if !self.iteration_over(time) { if !self.iteration_over(time) {
@ -312,24 +325,11 @@ impl Animation {
let old_direction = self.current_direction; let old_direction = self.current_direction;
let old_state = self.state.clone(); let old_state = self.state.clone();
let old_iteration_state = self.iteration_state.clone(); let old_iteration_state = self.iteration_state.clone();
*self = other.clone(); *self = other.clone();
let mut new_started_at = old_started_at; self.started_at = old_started_at;
self.current_direction = old_direction;
// If we're unpausing the animation, fake the start time so we seem to
// restore it.
//
// If the animation keeps paused, keep the old value.
//
// If we're pausing the animation, compute the progress value.
match (&mut self.state, old_state) {
(&mut Running, Paused(progress)) => new_started_at = now - (self.duration * progress),
(&mut Paused(ref mut new), Paused(old)) => *new = old,
(&mut Paused(ref mut progress), Running) => {
*progress = (now - old_started_at) / old_duration
},
_ => {},
}
// Don't update the iteration count, just the iteration limit. // Don't update the iteration count, just the iteration limit.
// TODO: see how changing the limit affects rendering in other browsers. // TODO: see how changing the limit affects rendering in other browsers.
@ -342,16 +342,36 @@ impl Animation {
_ => {}, _ => {},
} }
self.current_direction = old_direction; // Don't pause or restart animations that should remain finished.
self.started_at = new_started_at; // We call mem::replace because `has_ended(...)` looks at `Animation::state`.
} let new_state = std::mem::replace(&mut self.state, Running);
if old_state == Finished && self.has_ended(now) {
self.state = Finished;
} else {
self.state = new_state;
}
/// Calculate the active-duration of this animation according to // If we're unpausing the animation, fake the start time so we seem to
/// https://drafts.csswg.org/css-animations/#active-duration. // restore it.
pub fn active_duration(&self) -> f64 { //
match self.iteration_state { // If the animation keeps paused, keep the old value.
KeyframesIterationState::Finite(current, _) | //
KeyframesIterationState::Infinite(current) => self.duration * current, // If we're pausing the animation, compute the progress value.
match (&mut self.state, &old_state) {
(&mut Pending, &Paused(progress)) => {
self.started_at = now - (self.duration * progress);
},
(&mut Paused(ref mut new), &Paused(old)) => *new = old,
(&mut Paused(ref mut progress), &Running) => {
*progress = (now - old_started_at) / old_duration
},
_ => {},
}
// Try to detect when we should skip straight to the running phase to
// avoid sending multiple animationstart events.
if self.state == Pending && self.started_at <= now && old_state != Pending {
self.state = Running;
} }
} }
@ -369,7 +389,7 @@ impl Animation {
let started_at = self.started_at; let started_at = self.started_at;
let now = match self.state { let now = match self.state {
AnimationState::Running | AnimationState::Finished => { AnimationState::Running | AnimationState::Pending | AnimationState::Finished => {
context.current_time_for_animations context.current_time_for_animations
}, },
AnimationState::Paused(progress) => started_at + duration * progress, AnimationState::Paused(progress) => started_at + duration * progress,
@ -560,9 +580,12 @@ pub struct Transition {
pub node: OpaqueNode, pub node: OpaqueNode,
/// The start time of this transition, which is the current value of the animation /// The start time of this transition, which is the current value of the animation
/// timeline when this transition created. /// timeline when this transition was created plus any animation delay.
pub start_time: f64, pub start_time: f64,
/// The delay used for this transition.
pub delay: f64,
/// The internal style `PropertyAnimation` for this transition. /// The internal style `PropertyAnimation` for this transition.
pub property_animation: PropertyAnimation, pub property_animation: PropertyAnimation,
@ -733,26 +756,25 @@ impl ElementAnimationSet {
} }
/// Whether or not this state needs animation ticks for its transitions /// Whether or not this state needs animation ticks for its transitions
/// or animations. New animations don't need ticks until they are no /// or animations.
/// longer marked as new.
pub fn needs_animation_ticks(&self) -> bool { pub fn needs_animation_ticks(&self) -> bool {
self.animations self.animations
.iter() .iter()
.any(|animation| animation.state == AnimationState::Running && !animation.is_new) || .any(|animation| animation.state.needs_to_be_ticked()) ||
self.transitions.iter().any(|transition| { self.transitions
transition.state == AnimationState::Running && !transition.is_new .iter()
}) .any(|transition| transition.state.needs_to_be_ticked())
} }
/// The number of running animations and transitions for this `ElementAnimationSet`. /// The number of running animations and transitions for this `ElementAnimationSet`.
pub fn running_animation_and_transition_count(&self) -> usize { pub fn running_animation_and_transition_count(&self) -> usize {
self.animations self.animations
.iter() .iter()
.filter(|animation| animation.state == AnimationState::Running) .filter(|animation| animation.state.needs_to_be_ticked())
.count() + .count() +
self.transitions self.transitions
.iter() .iter()
.filter(|transition| transition.state == AnimationState::Running) .filter(|transition| transition.state.needs_to_be_ticked())
.count() .count()
} }
@ -879,8 +901,9 @@ impl ElementAnimationSet {
let mut new_transition = Transition { let mut new_transition = Transition {
node: opaque_node, node: opaque_node,
start_time: now + delay, start_time: now + delay,
delay,
property_animation, property_animation,
state: AnimationState::Running, state: AnimationState::Pending,
is_new: true, is_new: true,
reversing_adjusted_start_value, reversing_adjusted_start_value,
reversing_shortening_factor: 1.0, reversing_shortening_factor: 1.0,
@ -1049,7 +1072,7 @@ pub fn maybe_start_animations<E>(
let state = match box_style.animation_play_state_mod(i) { let state = match box_style.animation_play_state_mod(i) {
AnimationPlayState::Paused => AnimationState::Paused(0.), AnimationPlayState::Paused => AnimationState::Paused(0.),
AnimationPlayState::Running => AnimationState::Running, AnimationPlayState::Running => AnimationState::Pending,
}; };
let new_animation = Animation { let new_animation = Animation {

View file

@ -1,5 +0,0 @@
[animationevent-types.html]
expected: TIMEOUT
[animationstart event is instanceof AnimationEvent]
expected: TIMEOUT

View file

@ -1,5 +1,5 @@
[Element-getAnimations.tentative.html] [Element-getAnimations.tentative.html]
bug: https://github.com/servo/servo/issues/21564 bug: https://github.com/servo/servo/issues/26626
[getAnimations for CSS Animations with animation-name: none] [getAnimations for CSS Animations with animation-name: none]
expected: FAIL expected: FAIL

View file

@ -1,5 +1,5 @@
[animationevent-pseudoelement.html] [animationevent-pseudoelement.html]
bug: https://github.com/servo/servo/issues/21564 bug: https://github.com/servo/servo/issues/10316
expected: TIMEOUT expected: TIMEOUT
[AnimationEvent should have the correct pseudoElement memeber] [AnimationEvent should have the correct pseudoElement memeber]
expected: TIMEOUT expected: TIMEOUT

View file

@ -1,6 +0,0 @@
[animationevent-types.html]
bug: https://github.com/servo/servo/issues/21564
expected: TIMEOUT
[animationstart event is instanceof AnimationEvent]
expected: TIMEOUT

View file

@ -1,5 +1,5 @@
[events-006.html] [events-006.html]
bug: https://github.com/servo/servo/issues/21564 bug: https://github.com/servo/servo/issues/10316
expected: TIMEOUT expected: TIMEOUT
[transition padding-left on ::after] [transition padding-left on ::after]
expected: NOTRUN expected: NOTRUN

View file

@ -1,5 +1,5 @@
[variable-animation-from-to.html] [variable-animation-from-to.html]
bug: https://github.com/servo/servo/issues/21564 bug: https://github.com/servo/servo/issues/26625
expected: TIMEOUT expected: TIMEOUT
[Verify CSS variable value before animation] [Verify CSS variable value before animation]
expected: FAIL expected: FAIL

View file

@ -1,5 +1,5 @@
[variable-animation-over-transition.html] [variable-animation-over-transition.html]
bug: https://github.com/servo/servo/issues/21564 bug: https://github.com/servo/servo/issues/26625
expected: TIMEOUT expected: TIMEOUT
[Verify CSS variable value before animation] [Verify CSS variable value before animation]
expected: FAIL expected: FAIL

View file

@ -1,2 +1,2 @@
[variable-animation-substitute-into-keyframe-shorthand.html] [variable-animation-substitute-into-keyframe-shorthand.html]
bug: https://github.com/servo/servo/issues/21564 bug: https://github.com/servo/servo/issues/26625

View file

@ -1,5 +1,5 @@
[variable-animation-substitute-into-keyframe-transform.html] [variable-animation-substitute-into-keyframe-transform.html]
bug: https://github.com/servo/servo/issues/21564 bug: https://github.com/servo/servo/issues/26625
[Verify transform before animation] [Verify transform before animation]
expected: FAIL expected: FAIL

View file

@ -1,2 +1,2 @@
[variable-animation-substitute-into-keyframe.html] [variable-animation-substitute-into-keyframe.html]
bug: https://github.com/servo/servo/issues/21564 bug: https://github.com/servo/servo/issues/26625

View file

@ -1,5 +1,5 @@
[variable-animation-substitute-within-keyframe-fallback.html] [variable-animation-substitute-within-keyframe-fallback.html]
bug: https://github.com/servo/servo/issues/21564 bug: https://github.com/servo/servo/issues/26625
[Verify color after animation] [Verify color after animation]
expected: FAIL expected: FAIL

View file

@ -1,5 +1,5 @@
[variable-animation-substitute-within-keyframe-multiple.html] [variable-animation-substitute-within-keyframe-multiple.html]
bug: https://github.com/servo/servo/issues/21564 bug: https://github.com/servo/servo/issues/26625
[Verify color after animation] [Verify color after animation]
expected: FAIL expected: FAIL

View file

@ -1,5 +1,5 @@
[variable-animation-substitute-within-keyframe.html] [variable-animation-substitute-within-keyframe.html]
bug: https://github.com/servo/servo/issues/21564 bug: https://github.com/servo/servo/issues/26625
[Verify color after animation] [Verify color after animation]
expected: FAIL expected: FAIL

View file

@ -1,5 +1,5 @@
[variable-animation-to-only.html] [variable-animation-to-only.html]
bug: https://github.com/servo/servo/issues/21564 bug: https://github.com/servo/servo/issues/26625
expected: TIMEOUT expected: TIMEOUT
[Verify CSS variable value after animation] [Verify CSS variable value after animation]
expected: TIMEOUT expected: TIMEOUT

View file

@ -1,5 +1,5 @@
[variable-transitions-from-no-value.html] [variable-transitions-from-no-value.html]
bug: https://github.com/servo/servo/issues/21564 bug: https://github.com/servo/servo/issues/26625
expected: TIMEOUT expected: TIMEOUT
[Verify CSS variable value after transition] [Verify CSS variable value after transition]
expected: NOTRUN expected: NOTRUN

View file

@ -1,5 +1,5 @@
[variable-transitions-to-no-value.html] [variable-transitions-to-no-value.html]
bug: https://github.com/servo/servo/issues/21564 bug: https://github.com/servo/servo/issues/26625
expected: TIMEOUT expected: TIMEOUT
[Verify CSS variable value after transition] [Verify CSS variable value after transition]
expected: NOTRUN expected: NOTRUN

View file

@ -1,5 +1,5 @@
[variable-transitions-transition-property-variable-before-value.html] [variable-transitions-transition-property-variable-before-value.html]
bug: https://github.com/servo/servo/issues/21564 bug: https://github.com/servo/servo/issues/26625
expected: TIMEOUT expected: TIMEOUT
[Verify CSS variable value after transition] [Verify CSS variable value after transition]
expected: NOTRUN expected: NOTRUN

View file

@ -1,5 +1,5 @@
[variable-transitions-value-before-transition-property-variable.html] [variable-transitions-value-before-transition-property-variable.html]
bug: https://github.com/servo/servo/issues/21564 bug: https://github.com/servo/servo/issues/26625
expected: TIMEOUT expected: TIMEOUT
[Verify CSS variable value after transition] [Verify CSS variable value after transition]
expected: NOTRUN expected: NOTRUN

View file

@ -0,0 +1,2 @@
prefs: ["layout.animations.test.enabled:false",
"dom.testbinding.enabled:false"]

View file

@ -0,0 +1,3 @@
prefs: ["layout.animations.test.enabled:false",
"dom.testbinding.enabled:false"]

View file

@ -12849,6 +12849,15 @@
}, },
"css": { "css": {
"animations": { "animations": {
"animation-events.html": [
"0975aa64ec47ca4b4c8fc1e0a40414a51719ad67",
[
null,
{
"timeout": "long"
}
]
],
"animation-fill-mode.html": [ "animation-fill-mode.html": [
"4cfaab9fbce0adccd83f592935e63fa8ff58a1cf", "4cfaab9fbce0adccd83f592935e63fa8ff58a1cf",
[ [
@ -12884,6 +12893,13 @@
{} {}
] ]
], ],
"transition-events.html": [
"b561fc8353276e6bdd13a9d1b965f57733ecd19b",
[
null,
{}
]
],
"transition-raf.html": [ "transition-raf.html": [
"c38404503408e04b3c75b42df18ec3a7ec0819f5", "c38404503408e04b3c75b42df18ec3a7ec0819f5",
[ [

View file

@ -0,0 +1,2 @@
prefs: ["layout.animations.test.enabled:false",
"dom.testbinding.enabled:false"]

View file

@ -0,0 +1,3 @@
prefs: ["layout.animations.test.enabled:false",
"dom.testbinding.enabled:false"]

View file

@ -0,0 +1,160 @@
<!doctype html>
<meta charset=utf-8>
<title>CSS animation event dispatch</title>
<meta name="timeout" content="long">
<link rel="help" href="https://drafts.csswg.org/css-animations-2/#event-dispatch"/>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<style>
@keyframes anim {
from { margin-left: 0px; }
to { margin-left: 100px; }
}
</style>
<div id="log"></div>
<script>
'use strict';
// This series of tests is a forked version of the Web Platform Test
// /css/css-animations/event-dispatch.tentative that do not make use
// of the Web Animations API, since Servo doesn't yet support Web Animations.
function waitForFrame() {
return new Promise(resolve => {
window.requestAnimationFrame(resolve);
});
}
// All animation events should be received on the next animation frame.
const animationEventsTimeout = () => {
return new Promise(function(resolve) {
setTimeout(resolve, 5000);
});
}
const setupAnimation = (t, animationStyle) => {
var div = document.createElement('div');
div.setAttribute('style', 'animation: ' + animationStyle);
document.body.appendChild(div);
if (t && typeof t.add_cleanup === 'function') {
t.add_cleanup(function() {
if (div.parentNode) {
div.remove();
}
});
}
const watcher = new EventWatcher(t, div, [ 'animationstart',
'animationiteration',
'animationend',
'animationcancel' ],
animationEventsTimeout);
return { watcher, div };
};
promise_test(async t => {
const { watcher } = setupAnimation(t, 'anim 100s');
const events = await watcher.wait_for(
['animationstart' ],
{
record: 'all',
}
);
assert_equals(events[0].elapsedTime, 0.0);
}, 'animationstart');
promise_test(async t => {
const { watcher } = setupAnimation(t, 'anim 0.1s');
const events = await watcher.wait_for(
['animationstart', 'animationend'],
{
record: 'all',
}
);
assert_equals(events[0].elapsedTime, 0);
assert_equals(events[0].animationName, "anim");
assert_approx_equals(events[1].elapsedTime, 0.1, 0.001);
assert_equals(events[1].animationName, "anim");
}, 'animationstart and animationend');
promise_test(async t => {
const { watcher } = setupAnimation(t, 'anim 1s 0.5s');
const events = await watcher.wait_for(
['animationstart', 'animationend'], { record: 'all', }
);
assert_approx_equals(events[0].elapsedTime, 0, 0.01);
assert_equals(events[0].animationName, "anim");
assert_approx_equals(events[1].elapsedTime, 1, 0.01);
assert_equals(events[1].animationName, "anim");
}, 'animationstart and animationend with positive delay');
promise_test(async t => {
const { watcher } = setupAnimation(t, 'anim 100s -99.99s');
const events = await watcher.wait_for(
['animationstart', 'animationend'], { record: 'all', }
);
assert_approx_equals(events[0].elapsedTime, 99.99, 0.1);
assert_equals(events[0].animationName, "anim");
assert_approx_equals(events[1].elapsedTime, 99.99, 0.1);
assert_equals(events[1].animationName, "anim");
}, 'animationstart and animationend with negative delay');
promise_test(async t => {
const { watcher } = setupAnimation(t, 'anim 100s -200s');
const events = await watcher.wait_for(
['animationstart', 'animationend'], { record: 'all', }
);
assert_approx_equals(events[0].elapsedTime, 99.99, 0.1);
assert_equals(events[0].animationName, "anim");
assert_approx_equals(events[1].elapsedTime, 99.99, 0.1);
assert_equals(events[1].animationName, "anim");
}, 'animationstart and animationend with negative delay larger than active duration');
promise_test(async t => {
const { watcher, div } = setupAnimation(t, 'anim 100s');
await watcher.wait_for('animationstart');
div.style.animation = "";
await watcher.wait_for('animationcancel');
}, 'animationcancel');
promise_test(async t => {
const { watcher, div } = setupAnimation(t, 'anim 100s 50s');
// Wait for two animation frames. One is not enough in some browser engines.
await waitForFrame();
await waitForFrame();
div.style.animation = "";
const events = await watcher.wait_for(
['animationcancel'], { record: 'all', }
);
assert_equals(events[0].elapsedTime, 0);
}, 'animationcancel with positive delay');
promise_test(async t => {
const { watcher, div } = setupAnimation(t, 'anim 100s -50s');
await watcher.wait_for('animationstart');
div.style.animation = "";
const events = await watcher.wait_for(
['animationcancel'], { record: 'all', }
);
assert_approx_equals(events[0].elapsedTime, 50, 0.1);
}, 'animationcancel with negative delay');
</script>

View file

@ -0,0 +1,131 @@
<!doctype html>
<meta charset=utf-8>
<title>CSS transition event dispatch</title>
<link rel="help" href="https://drafts.csswg.org/css-transitions-2/#event-dispatch">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<div id="log"></div>
<script>
'use strict';
// This series of tests is a forked version of the Web Platform Test
// /css/css-transitions/event-dispatch.tentative that do not make use
// of the Web Animations API, since Servo doesn't yet support Web Animations.
// All transition events should be received on the next animation frame.
function transitionEventsTimeout() {
return new Promise(function(resolve) {
return new Promise(resolve => {
window.requestAnimationFrame(resolve);
});
});
};
const setupTransition = (t, transitionStyle) => {
var div = document.createElement('div');
div.setAttribute('style', 'transition: ' + transitionStyle);
document.body.appendChild(div);
if (t && typeof t.add_cleanup === 'function') {
t.add_cleanup(function() {
if (div.parentNode) {
div.remove();
}
});
}
const watcher = new EventWatcher(t, div, [ 'transitionrun',
'transitionstart',
'transitionend',
'transitioncancel' ],
transitionEventsTimeout);
getComputedStyle(div).marginLeft;
div.style.marginLeft = '100px';
return { watcher, div };
};
promise_test(async t => {
const { watcher } = setupTransition(t, 'margin-left 100s 100s');
const events = await watcher.wait_for(
['transitionrun' ],
{
record: 'all',
}
);
assert_equals(events[0].elapsedTime, 0.0);
}, 'transitionrun');
promise_test(async t => {
const { watcher } = setupTransition(t, 'margin-left 100s');
const events = await watcher.wait_for(
['transitionrun', 'transitionstart', ],
{
record: 'all',
}
);
assert_equals(events[0].elapsedTime, 0.0);
assert_equals(events[1].elapsedTime, 0.0);
}, 'transitionrun, transitionstart');
promise_test(async t => {
const { watcher, div } = setupTransition(t, 'margin-left 0.1s');
const events = await watcher.wait_for(
['transitionrun', 'transitionstart', 'transitionend'],
{
record: 'all',
}
);
assert_equals(events[0].elapsedTime, 0);
assert_equals(events[0].propertyName, "margin-left");
assert_equals(events[1].elapsedTime, 0);
assert_equals(events[1].propertyName, "margin-left");
assert_approx_equals(events[2].elapsedTime, 0.1, 0.01);
assert_equals(events[2].propertyName, "margin-left");
}, 'transitionrun, transitionstart, transitionend');
promise_test(async t => {
const { watcher, div } = setupTransition(t, 'margin-left 1s 0.5s');
const events = await watcher.wait_for(
['transitionrun', 'transitionstart', 'transitionend'],
{
record: 'all',
}
);
assert_equals(events[0].elapsedTime, 0);
assert_equals(events[0].propertyName, "margin-left");
assert_equals(events[1].elapsedTime, 0);
assert_equals(events[1].propertyName, "margin-left");
assert_equals(events[2].elapsedTime, 1);
assert_equals(events[2].propertyName, "margin-left");
}, 'transitionrun, transitionstart, transitionend with positive delay');
promise_test(async t => {
const { watcher, div } = setupTransition(t, 'margin-left 100s -99.99s');
const events = await watcher.wait_for(
['transitionrun', 'transitionstart', 'transitionend'],
{
record: 'all',
}
);
assert_approx_equals(events[0].elapsedTime, 99.99, 0.1);
assert_equals(events[0].propertyName, "margin-left");
assert_approx_equals(events[1].elapsedTime, 99.99, 0.1);
assert_equals(events[1].propertyName, "margin-left");
assert_approx_equals(events[2].elapsedTime, 99.99, 0.1);
assert_equals(events[2].propertyName, "margin-left");
}, 'transitionrun, transitionstart, transitionend with negative delay');
</script>