Add support for animationend event

This is triggered when an animation finishes. This is a high priority
because it allows us to start rooting nodes with animations in the
script thread.

This doesn't yet cause a lot of tests to pass because they rely on the
existence of `Document.getAnimations()` and the presence of
`animationstart` and animationiteration` events.
This commit is contained in:
Martin Robinson 2020-04-29 12:19:21 +02:00
parent 6fb75c2b9e
commit 3903c1fb98
27 changed files with 335 additions and 331 deletions

View file

@ -13,20 +13,20 @@ use ipc_channel::ipc::IpcSender;
use msg::constellation_msg::PipelineId; use msg::constellation_msg::PipelineId;
use script_traits::UntrustedNodeAddress; use script_traits::UntrustedNodeAddress;
use script_traits::{ use script_traits::{
AnimationState, ConstellationControlMsg, LayoutMsg as ConstellationMsg, TransitionEventType, AnimationState, ConstellationControlMsg, LayoutMsg as ConstellationMsg,
TransitionOrAnimationEventType,
}; };
use servo_arc::Arc; use servo_arc::Arc;
use style::animation::{ use style::animation::{update_style_for_animation, Animation, ElementAnimationState};
update_style_for_animation, Animation, ElementAnimationState, PropertyAnimation,
};
use style::dom::TElement; use style::dom::TElement;
use style::font_metrics::ServoMetricsProvider; use style::font_metrics::ServoMetricsProvider;
use style::selector_parser::RestyleDamage; use style::selector_parser::RestyleDamage;
use style::timer::Timer; use style::timer::Timer;
/// Collect newly transitioning nodes, which is used by the script process during /// Collect newly animating nodes, which is used by the script process during
/// forced, synchronous reflows to root DOM nodes for the duration of their transitions. /// forced, synchronous reflows to root DOM nodes for the duration of their
pub fn collect_newly_transitioning_nodes( /// animations or transitions.
pub fn collect_newly_animating_nodes(
animation_states: &FxHashMap<OpaqueNode, ElementAnimationState>, animation_states: &FxHashMap<OpaqueNode, ElementAnimationState>,
mut out: Option<&mut Vec<UntrustedNodeAddress>>, mut out: Option<&mut Vec<UntrustedNodeAddress>>,
) { ) {
@ -35,12 +35,7 @@ pub fn collect_newly_transitioning_nodes(
// currently stores a rooted node for every property that is transitioning. // currently stores a rooted node for every property that is transitioning.
if let Some(ref mut out) = out { if let Some(ref mut out) = out {
out.extend(animation_states.iter().flat_map(|(node, state)| { out.extend(animation_states.iter().flat_map(|(node, state)| {
let num_transitions = state std::iter::repeat(node.to_untrusted_node_address()).take(state.new_animations.len())
.new_animations
.iter()
.filter(|animation| animation.is_transition())
.count();
std::iter::repeat(node.to_untrusted_node_address()).take(num_transitions)
})); }));
} }
} }
@ -101,21 +96,28 @@ pub fn update_animation_state(
now: f64, now: f64,
node: OpaqueNode, node: OpaqueNode,
) { ) {
let send_transition_event = |property_animation: &PropertyAnimation, event_type| { let send_event = |animation: &Animation, event_type, elapsed_time| {
let property_or_animation_name = match *animation {
Animation::Transition(_, _, ref property_animation) => {
property_animation.property_name().into()
},
Animation::Keyframes(_, _, ref name, _) => name.to_string(),
};
script_channel script_channel
.send(ConstellationControlMsg::TransitionEvent { .send(ConstellationControlMsg::TransitionOrAnimationEvent {
pipeline_id, pipeline_id,
event_type, event_type,
node: node.to_untrusted_node_address(), node: node.to_untrusted_node_address(),
property_name: property_animation.property_name().into(), property_or_animation_name,
elapsed_time: property_animation.duration, elapsed_time,
}) })
.unwrap() .unwrap()
}; };
handle_cancelled_animations(animation_state, send_transition_event); handle_cancelled_animations(animation_state, now, send_event);
handle_running_animations(animation_state, now, send_transition_event); handle_running_animations(animation_state, now, send_event);
handle_new_animations(animation_state, send_transition_event); handle_new_animations(animation_state, send_event);
} }
/// 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
@ -123,7 +125,7 @@ pub fn update_animation_state(
pub fn handle_running_animations( pub fn handle_running_animations(
animation_state: &mut ElementAnimationState, animation_state: &mut ElementAnimationState,
now: f64, now: f64,
mut send_transition_event: impl FnMut(&PropertyAnimation, TransitionEventType), mut send_event: impl FnMut(&Animation, TransitionOrAnimationEventType, f64),
) { ) {
let mut running_animations = let mut running_animations =
std::mem::replace(&mut animation_state.running_animations, Vec::new()); std::mem::replace(&mut animation_state.running_animations, Vec::new());
@ -144,9 +146,18 @@ pub fn handle_running_animations(
animation_state.running_animations.push(running_animation); animation_state.running_animations.push(running_animation);
} else { } else {
debug!("Finishing transition: {:?}", running_animation); debug!("Finishing transition: {:?}", running_animation);
if let Animation::Transition(_, _, ref property_animation) = running_animation { let (event_type, elapsed_time) = match running_animation {
send_transition_event(property_animation, TransitionEventType::TransitionEnd); Animation::Transition(_, _, ref property_animation) => (
} TransitionOrAnimationEventType::TransitionEnd,
property_animation.duration,
),
Animation::Keyframes(_, _, _, ref mut state) => (
TransitionOrAnimationEventType::AnimationEnd,
state.active_duration(),
),
};
send_event(&running_animation, event_type, elapsed_time);
animation_state.finished_animations.push(running_animation); animation_state.finished_animations.push(running_animation);
} }
} }
@ -157,12 +168,19 @@ pub fn handle_running_animations(
/// well. /// well.
pub fn handle_cancelled_animations( pub fn handle_cancelled_animations(
animation_state: &mut ElementAnimationState, animation_state: &mut ElementAnimationState,
mut send_transition_event: impl FnMut(&PropertyAnimation, TransitionEventType), now: f64,
mut send_event: impl FnMut(&Animation, TransitionOrAnimationEventType, f64),
) { ) {
for animation in animation_state.cancelled_animations.drain(..) { for animation in animation_state.cancelled_animations.drain(..) {
match animation { match animation {
Animation::Transition(_, _, ref property_animation) => { Animation::Transition(_, start_time, _) => {
send_transition_event(property_animation, TransitionEventType::TransitionCancel) // TODO(mrobinson): We need to properly compute the elapsed_time here
// according to https://drafts.csswg.org/css-transitions/#event-transitionevent
send_event(
&animation,
TransitionOrAnimationEventType::TransitionCancel,
(now - start_time).max(0.),
);
}, },
// TODO(mrobinson): We should send animationcancel events. // TODO(mrobinson): We should send animationcancel events.
Animation::Keyframes(..) => {}, Animation::Keyframes(..) => {},
@ -172,12 +190,18 @@ pub fn handle_cancelled_animations(
pub fn handle_new_animations( pub fn handle_new_animations(
animation_state: &mut ElementAnimationState, animation_state: &mut ElementAnimationState,
mut send_transition_event: impl FnMut(&PropertyAnimation, TransitionEventType), mut send_event: impl FnMut(&Animation, TransitionOrAnimationEventType, f64),
) { ) {
for animation in animation_state.new_animations.drain(..) { for animation in animation_state.new_animations.drain(..) {
match animation { match animation {
Animation::Transition(_, _, ref property_animation) => { Animation::Transition(..) => {
send_transition_event(property_animation, TransitionEventType::TransitionRun) // TODO(mrobinson): We need to properly compute the elapsed_time here
// according to https://drafts.csswg.org/css-transitions/#event-transitionevent
send_event(
&animation,
TransitionOrAnimationEventType::TransitionRun,
0.,
)
}, },
Animation::Keyframes(..) => {}, Animation::Keyframes(..) => {},
} }

View file

@ -86,9 +86,9 @@ pub struct LayoutContext<'a> {
/// A None value means that this layout was not initiated by the script thread. /// A None value means that this layout was not initiated by the script thread.
pub pending_images: Option<Mutex<Vec<PendingImage>>>, pub pending_images: Option<Mutex<Vec<PendingImage>>>,
/// A list of nodes that have just initiated a CSS transition. /// A list of nodes that have just initiated a CSS transition or animation.
/// A None value means that this layout was not initiated by the script thread. /// A None value means that this layout was not initiated by the script thread.
pub newly_transitioning_nodes: Option<Mutex<Vec<UntrustedNodeAddress>>>, pub newly_animating_nodes: Option<Mutex<Vec<UntrustedNodeAddress>>>,
} }
impl<'a> Drop for LayoutContext<'a> { impl<'a> Drop for LayoutContext<'a> {

View file

@ -648,7 +648,7 @@ impl LayoutThread {
} else { } else {
None None
}, },
newly_transitioning_nodes: if script_initiated_layout { newly_animating_nodes: if script_initiated_layout {
Some(Mutex::new(vec![])) Some(Mutex::new(vec![]))
} else { } else {
None None
@ -1565,11 +1565,11 @@ impl LayoutThread {
}; };
reflow_result.pending_images = pending_images; reflow_result.pending_images = pending_images;
let newly_transitioning_nodes = match context.newly_transitioning_nodes { let newly_animating_nodes = match context.newly_animating_nodes {
Some(ref nodes) => std::mem::replace(&mut *nodes.lock().unwrap(), vec![]), Some(ref nodes) => std::mem::replace(&mut *nodes.lock().unwrap(), vec![]),
None => vec![], None => vec![],
}; };
reflow_result.newly_transitioning_nodes = newly_transitioning_nodes; reflow_result.newly_animating_nodes = newly_animating_nodes;
let mut root_flow = match self.root_flow.borrow().clone() { let mut root_flow = match self.root_flow.borrow().clone() {
Some(root_flow) => root_flow, Some(root_flow) => root_flow,
@ -1741,7 +1741,7 @@ impl LayoutThread {
invalid_nodes, invalid_nodes,
); );
assert!(layout_context.pending_images.is_none()); assert!(layout_context.pending_images.is_none());
assert!(layout_context.newly_transitioning_nodes.is_none()); assert!(layout_context.newly_animating_nodes.is_none());
} }
} }
@ -1756,19 +1756,13 @@ impl LayoutThread {
invalid_nodes: FxHashSet<OpaqueNode>, invalid_nodes: FxHashSet<OpaqueNode>,
) { ) {
{ {
let mut newly_transitioning_nodes = context let mut newly_animating_nodes = context
.newly_transitioning_nodes .newly_animating_nodes
.as_ref() .as_ref()
.map(|nodes| nodes.lock().unwrap()); .map(|nodes| nodes.lock().unwrap());
let newly_transitioning_nodes = let newly_animating_nodes = newly_animating_nodes.as_mut().map(|nodes| &mut **nodes);
newly_transitioning_nodes.as_mut().map(|nodes| &mut **nodes);
let mut animation_states = self.animation_states.write(); let mut animation_states = self.animation_states.write();
animation::collect_newly_animating_nodes(&animation_states, newly_animating_nodes);
animation::collect_newly_transitioning_nodes(
&animation_states,
newly_transitioning_nodes,
);
animation::update_animation_states( animation::update_animation_states(
&self.constellation_chan, &self.constellation_chan,
&self.script_chan, &self.script_chan,

View file

@ -0,0 +1,76 @@
/* 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/. */
use crate::dom::bindings::codegen::Bindings::AnimationEventBinding::{
AnimationEventInit, AnimationEventMethods,
};
use crate::dom::bindings::codegen::Bindings::EventBinding::EventMethods;
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::num::Finite;
use crate::dom::bindings::reflector::reflect_dom_object;
use crate::dom::bindings::root::DomRoot;
use crate::dom::bindings::str::DOMString;
use crate::dom::event::Event;
use crate::dom::window::Window;
use dom_struct::dom_struct;
use servo_atoms::Atom;
#[dom_struct]
pub struct AnimationEvent {
event: Event,
animation_name: Atom,
elapsed_time: Finite<f32>,
pseudo_element: DOMString,
}
impl AnimationEvent {
fn new_inherited(init: &AnimationEventInit) -> AnimationEvent {
AnimationEvent {
event: Event::new_inherited(),
animation_name: Atom::from(init.animationName.clone()),
elapsed_time: init.elapsedTime.clone(),
pseudo_element: init.pseudoElement.clone(),
}
}
pub fn new(window: &Window, type_: Atom, init: &AnimationEventInit) -> DomRoot<AnimationEvent> {
let ev = reflect_dom_object(Box::new(AnimationEvent::new_inherited(init)), window);
{
let event = ev.upcast::<Event>();
event.init_event(type_, init.parent.bubbles, init.parent.cancelable);
}
ev
}
#[allow(non_snake_case)]
pub fn Constructor(
window: &Window,
type_: DOMString,
init: &AnimationEventInit,
) -> DomRoot<AnimationEvent> {
AnimationEvent::new(window, Atom::from(type_), init)
}
}
impl AnimationEventMethods for AnimationEvent {
// https://drafts.csswg.org/css-animations/#interface-animationevent-attributes
fn AnimationName(&self) -> DOMString {
DOMString::from(&*self.animation_name)
}
// https://drafts.csswg.org/css-animations/#interface-animationevent-attributes
fn ElapsedTime(&self) -> Finite<f32> {
self.elapsed_time.clone()
}
// https://drafts.csswg.org/css-animations/#interface-animationevent-attributes
fn PseudoElement(&self) -> DOMString {
self.pseudo_element.clone()
}
// https://dom.spec.whatwg.org/#dom-event-istrusted
fn IsTrusted(&self) -> bool {
self.upcast::<Event>().IsTrusted()
}
}

View file

@ -442,6 +442,7 @@ macro_rules! global_event_handlers(
); );
(NoOnload) => ( (NoOnload) => (
event_handler!(abort, GetOnabort, SetOnabort); event_handler!(abort, GetOnabort, SetOnabort);
event_handler!(animationend, GetOnanimationend, SetOnanimationend);
event_handler!(cancel, GetOncancel, SetOncancel); event_handler!(cancel, GetOncancel, SetOncancel);
event_handler!(canplay, GetOncanplay, SetOncanplay); event_handler!(canplay, GetOncanplay, SetOncanplay);
event_handler!(canplaythrough, GetOncanplaythrough, SetOncanplaythrough); event_handler!(canplaythrough, GetOncanplaythrough, SetOncanplaythrough);

View file

@ -213,6 +213,7 @@ pub mod abstractworker;
pub mod abstractworkerglobalscope; pub mod abstractworkerglobalscope;
pub mod activation; pub mod activation;
pub mod analysernode; pub mod analysernode;
pub mod animationevent;
pub mod attr; pub mod attr;
pub mod audiobuffer; pub mod audiobuffer;
pub mod audiobuffersourcenode; pub mod audiobuffersourcenode;

View file

@ -0,0 +1,26 @@
/* 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 http://mozilla.org/MPL/2.0/.
*
* The origin of this IDL file is
* http://www.w3.org/TR/css3-animations/#animation-events-
* http://dev.w3.org/csswg/css3-animations/#animation-events-
*
* Copyright © 2012 W3C® (MIT, ERCIM, Keio), All Rights Reserved. W3C
* liability, trademark and document use rules apply.
*/
[Exposed=Window]
interface AnimationEvent : Event {
constructor(DOMString type, optional AnimationEventInit eventInitDict = {});
readonly attribute DOMString animationName;
readonly attribute float elapsedTime;
readonly attribute DOMString pseudoElement;
};
dictionary AnimationEventInit : EventInit {
DOMString animationName = "";
float elapsedTime = 0;
DOMString pseudoElement = "";
};

View file

@ -90,6 +90,11 @@ interface mixin GlobalEventHandlers {
attribute EventHandler onwaiting; attribute EventHandler onwaiting;
}; };
// https://drafts.csswg.org/css-animations/#interface-globaleventhandlers-idl
partial interface mixin GlobalEventHandlers {
attribute EventHandler onanimationend;
};
// https://drafts.csswg.org/css-transitions/#interface-globaleventhandlers-idl // https://drafts.csswg.org/css-transitions/#interface-globaleventhandlers-idl
partial interface mixin GlobalEventHandlers { partial interface mixin GlobalEventHandlers {
attribute EventHandler ontransitionrun; attribute EventHandler ontransitionrun;

View file

@ -1582,11 +1582,11 @@ impl Window {
} }
let for_display = reflow_goal == ReflowGoal::Full; let for_display = reflow_goal == ReflowGoal::Full;
let pipeline_id = self.upcast::<GlobalScope>().pipeline_id();
if for_display && self.suppress_reflow.get() { if for_display && self.suppress_reflow.get() {
debug!( debug!(
"Suppressing reflow pipeline {} for reason {:?} before FirstLoad or RefreshTick", "Suppressing reflow pipeline {} for reason {:?} before FirstLoad or RefreshTick",
self.upcast::<GlobalScope>().pipeline_id(), pipeline_id, reason
reason
); );
return false; return false;
} }
@ -1617,11 +1617,7 @@ impl Window {
// On debug mode, print the reflow event information. // On debug mode, print the reflow event information.
if self.relayout_event { if self.relayout_event {
debug_reflow_events( debug_reflow_events(pipeline_id, &reflow_goal, &reason);
self.upcast::<GlobalScope>().pipeline_id(),
&reflow_goal,
&reason,
);
} }
let document = self.Document(); let document = self.Document();
@ -1699,12 +1695,11 @@ impl Window {
{ {
let (responder, responder_listener) = let (responder, responder_listener) =
ProfiledIpc::channel(self.global().time_profiler_chan().clone()).unwrap(); ProfiledIpc::channel(self.global().time_profiler_chan().clone()).unwrap();
let pipeline = self.upcast::<GlobalScope>().pipeline_id();
let image_cache_chan = self.image_cache_chan.clone(); let image_cache_chan = self.image_cache_chan.clone();
ROUTER.add_route( ROUTER.add_route(
responder_listener.to_opaque(), responder_listener.to_opaque(),
Box::new(move |message| { Box::new(move |message| {
let _ = image_cache_chan.send((pipeline, message.to().unwrap())); let _ = image_cache_chan.send((pipeline_id, message.to().unwrap()));
}), }),
); );
self.image_cache self.image_cache
@ -1714,7 +1709,7 @@ impl Window {
} }
unsafe { unsafe {
ScriptThread::note_newly_transitioning_nodes(complete.newly_transitioning_nodes); ScriptThread::note_newly_animating_nodes(pipeline_id, complete.newly_animating_nodes);
} }
true true

View file

@ -19,7 +19,9 @@
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,
}; };
@ -138,9 +140,9 @@ use script_traits::{
EventResult, HistoryEntryReplacement, InitialScriptState, JsEvalResult, LayoutMsg, LoadData, EventResult, HistoryEntryReplacement, InitialScriptState, JsEvalResult, LayoutMsg, LoadData,
LoadOrigin, MediaSessionActionType, MouseButton, MouseEventType, NewLayoutInfo, Painter, LoadOrigin, MediaSessionActionType, MouseButton, MouseEventType, NewLayoutInfo, Painter,
ProgressiveWebMetricType, ScriptMsg, ScriptThreadFactory, ScriptToConstellationChan, ProgressiveWebMetricType, ScriptMsg, ScriptThreadFactory, ScriptToConstellationChan,
StructuredSerializedData, TimerSchedulerMsg, TouchEventType, TouchId, TransitionEventType, StructuredSerializedData, TimerSchedulerMsg, TouchEventType, TouchId,
UntrustedNodeAddress, UpdatePipelineIdReason, WebrenderIpcSender, WheelDelta, WindowSizeData, TransitionOrAnimationEventType, UntrustedNodeAddress, UpdatePipelineIdReason,
WindowSizeType, WebrenderIpcSender, WheelDelta, WindowSizeData, WindowSizeType,
}; };
use servo_atoms::Atom; use servo_atoms::Atom;
use servo_config::opts; use servo_config::opts;
@ -639,7 +641,7 @@ pub struct ScriptThread {
/// A list of nodes with in-progress CSS transitions, which roots them for the duration /// A list of nodes with in-progress CSS transitions, which roots them for the duration
/// of the transition. /// of the transition.
transitioning_nodes: DomRefCell<Vec<Dom<Node>>>, animating_nodes: DomRefCell<HashMap<PipelineId, Vec<Dom<Node>>>>,
/// <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,
@ -823,7 +825,10 @@ impl ScriptThread {
}) })
} }
pub unsafe fn note_newly_transitioning_nodes(nodes: Vec<UntrustedNodeAddress>) { pub unsafe fn note_newly_animating_nodes(
pipeline_id: PipelineId,
nodes: Vec<UntrustedNodeAddress>,
) {
SCRIPT_THREAD_ROOT.with(|root| { SCRIPT_THREAD_ROOT.with(|root| {
let script_thread = &*root.get().unwrap(); let script_thread = &*root.get().unwrap();
let js_runtime = script_thread.js_runtime.rt(); let js_runtime = script_thread.js_runtime.rt();
@ -831,8 +836,10 @@ impl ScriptThread {
.into_iter() .into_iter()
.map(|n| Dom::from_ref(&*from_untrusted_node_address(js_runtime, n))); .map(|n| Dom::from_ref(&*from_untrusted_node_address(js_runtime, n)));
script_thread script_thread
.transitioning_nodes .animating_nodes
.borrow_mut() .borrow_mut()
.entry(pipeline_id)
.or_insert_with(Vec::new)
.extend(new_nodes); .extend(new_nodes);
}) })
} }
@ -1345,7 +1352,7 @@ impl ScriptThread {
docs_with_no_blocking_loads: Default::default(), docs_with_no_blocking_loads: Default::default(),
transitioning_nodes: Default::default(), animating_nodes: Default::default(),
custom_element_reaction_stack: CustomElementReactionStack::new(), custom_element_reaction_stack: CustomElementReactionStack::new(),
@ -1697,7 +1704,7 @@ impl ScriptThread {
FocusIFrame(id, ..) => Some(id), FocusIFrame(id, ..) => Some(id),
WebDriverScriptCommand(id, ..) => Some(id), WebDriverScriptCommand(id, ..) => Some(id),
TickAllAnimations(id) => Some(id), TickAllAnimations(id) => Some(id),
TransitionEvent { .. } => None, TransitionOrAnimationEvent { .. } => None,
WebFontLoaded(id) => Some(id), WebFontLoaded(id) => Some(id),
DispatchIFrameLoadEvent { DispatchIFrameLoadEvent {
target: _, target: _,
@ -1898,18 +1905,18 @@ impl ScriptThread {
ConstellationControlMsg::TickAllAnimations(pipeline_id) => { ConstellationControlMsg::TickAllAnimations(pipeline_id) => {
self.handle_tick_all_animations(pipeline_id) self.handle_tick_all_animations(pipeline_id)
}, },
ConstellationControlMsg::TransitionEvent { ConstellationControlMsg::TransitionOrAnimationEvent {
pipeline_id, pipeline_id,
event_type, event_type,
node, node,
property_name, property_or_animation_name,
elapsed_time, elapsed_time,
} => { } => {
self.handle_transition_event( self.handle_transition_or_animation_event(
pipeline_id, pipeline_id,
event_type, event_type,
node, node,
property_name, property_or_animation_name,
elapsed_time, elapsed_time,
); );
}, },
@ -2846,6 +2853,9 @@ 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 {
// 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
@ -2916,20 +2926,32 @@ impl ScriptThread {
/// Handles firing of transition-related events. /// Handles firing of transition-related events.
/// ///
/// TODO(mrobinson): Add support for more events. /// TODO(mrobinson): Add support for more events.
fn handle_transition_event( fn handle_transition_or_animation_event(
&self, &self,
pipeline_id: PipelineId, pipeline_id: PipelineId,
event_type: TransitionEventType, event_type: TransitionOrAnimationEventType,
unsafe_node: UntrustedNodeAddress, unsafe_node: UntrustedNodeAddress,
property_name: String, property_or_animation_name: String,
elapsed_time: f64, elapsed_time: f64,
) { ) {
let js_runtime = self.js_runtime.rt(); let js_runtime = self.js_runtime.rt();
let node = unsafe { from_untrusted_node_address(js_runtime, unsafe_node) }; let node = unsafe { from_untrusted_node_address(js_runtime, unsafe_node) };
let node_index = self // We limit the scope of the borrow here, so that we don't maintain this borrow
.transitioning_nodes // and then incidentally trigger another layout. That might result in a double
.borrow() // mutable borrow of `animating_nodes`.
{
let mut animating_nodes = self.animating_nodes.borrow_mut();
let nodes = match animating_nodes.get_mut(&pipeline_id) {
Some(nodes) => nodes,
None => {
return warn!(
"Ignoring transition event for pipeline without animating nodes."
);
},
};
let node_index = nodes
.iter() .iter()
.position(|n| &**n as *const _ == &*node as *const _); .position(|n| &**n as *const _ == &*node as *const _);
let node_index = match node_index { let node_index = match node_index {
@ -2937,45 +2959,58 @@ impl ScriptThread {
None => { None => {
// If no index is found, we can't know whether this node is safe to use. // 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. // It's better not to fire a DOM event than crash.
warn!("Ignoring transition end notification for unknown node."); warn!("Ignoring transition event for unknown node.");
return; return;
}, },
}; };
if self.closed_pipelines.borrow().contains(&pipeline_id) { if event_type.finalizes_transition_or_animation() {
warn!("Ignoring transition event for closed pipeline."); nodes.remove(node_index);
return; }
}
// Not quite the right thing - see #13865.
if event_type.finalizes_transition_or_animation() {
node.dirty(NodeDamage::NodeStyleDamaged);
} }
let event_atom = match event_type { let event_atom = match event_type {
TransitionEventType::TransitionRun => atom!("transitionrun"), TransitionOrAnimationEventType::AnimationEnd => atom!("animationend"),
TransitionEventType::TransitionEnd => { TransitionOrAnimationEventType::TransitionCancel => atom!("transitioncancel"),
// Not quite the right thing - see #13865. TransitionOrAnimationEventType::TransitionEnd => atom!("transitionend"),
node.dirty(NodeDamage::NodeStyleDamaged); TransitionOrAnimationEventType::TransitionRun => atom!("transitionrun"),
self.transitioning_nodes.borrow_mut().remove(node_index);
atom!("transitionend")
},
TransitionEventType::TransitionCancel => {
self.transitioning_nodes.borrow_mut().remove(node_index);
atom!("transitioncancel")
},
}; };
let parent = EventInit {
let event_init = TransitionEventInit {
parent: EventInit {
bubbles: true, bubbles: true,
cancelable: false, cancelable: false,
},
propertyName: DOMString::from(property_name),
elapsedTime: Finite::new(elapsed_time as f32).unwrap(),
// TODO: Handle pseudo-elements properly
pseudoElement: DOMString::new(),
}; };
// TODO: Handle pseudo-elements properly
let property_or_animation_name = DOMString::from(property_or_animation_name);
let elapsed_time = Finite::new(elapsed_time as f32).unwrap();
let window = window_from_node(&*node); let window = window_from_node(&*node);
if 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) TransitionEvent::new(&window, event_atom, &event_init)
.upcast::<Event>() .upcast::<Event>()
.fire(node.upcast()); .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.

View file

@ -193,7 +193,7 @@ pub struct ReflowComplete {
/// The list of images that were encountered that are in progress. /// The list of images that were encountered that are in progress.
pub pending_images: Vec<PendingImage>, pub pending_images: Vec<PendingImage>,
/// The list of nodes that initiated a CSS transition. /// The list of nodes that initiated a CSS transition.
pub newly_transitioning_nodes: Vec<UntrustedNodeAddress>, pub newly_animating_nodes: Vec<UntrustedNodeAddress>,
} }
/// Information needed for a script-initiated reflow. /// Information needed for a script-initiated reflow.

View file

@ -282,16 +282,40 @@ pub enum UpdatePipelineIdReason {
Traversal, Traversal,
} }
/// The type of transition event to trigger. /// The type of transition event to trigger. These are defined by
/// CSS Transitions § 6.1 and CSS Animations § 4.2
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
pub enum TransitionEventType { pub enum TransitionOrAnimationEventType {
/// The transition has started running. /// "The transitionrun event occurs when a transition is created (i.e., when it
/// is added to the set of running transitions)."
TransitionRun, TransitionRun,
/// The transition has ended by reaching the end of its animation. /// "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, TransitionEnd,
/// The transition ended early for some reason, such as the property /// "The transitioncancel event occurs when a transition is canceled."
/// no longer being transitionable or being replaced by another transition.
TransitionCancel, TransitionCancel,
/// "The animationend event occurs when the animation finishes"
AnimationEnd,
}
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 => false,
}
}
/// 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 => true,
Self::AnimationEnd => false,
}
}
} }
/// Messages sent from the constellation or layout to the script thread. /// Messages sent from the constellation or layout to the script thread.
@ -380,16 +404,17 @@ pub enum ConstellationControlMsg {
WebDriverScriptCommand(PipelineId, WebDriverScriptCommand), WebDriverScriptCommand(PipelineId, WebDriverScriptCommand),
/// Notifies script thread that all animations are done /// Notifies script thread that all animations are done
TickAllAnimations(PipelineId), TickAllAnimations(PipelineId),
/// Notifies the script thread that a transition related event should be sent. /// Notifies the script thread that a transition or animation related event should be sent.
TransitionEvent { TransitionOrAnimationEvent {
/// The pipeline id of the layout task that sent this message. /// The pipeline id of the layout task that sent this message.
pipeline_id: PipelineId, pipeline_id: PipelineId,
/// The type of transition event this should trigger. /// The type of transition event this should trigger.
event_type: TransitionEventType, event_type: TransitionOrAnimationEventType,
/// The address of the node which owns this transition. /// The address of the node which owns this transition.
node: UntrustedNodeAddress, node: UntrustedNodeAddress,
/// The property name of the property that is transitioning. /// The name of the property that is transitioning (in the case of a transition)
property_name: String, /// or the name of the animation (in the case of an animation).
property_or_animation_name: String,
/// The elapsed time property to send with this transition event. /// The elapsed time property to send with this transition event.
elapsed_time: f64, elapsed_time: f64,
}, },
@ -452,7 +477,7 @@ impl fmt::Debug for ConstellationControlMsg {
FocusIFrame(..) => "FocusIFrame", FocusIFrame(..) => "FocusIFrame",
WebDriverScriptCommand(..) => "WebDriverScriptCommand", WebDriverScriptCommand(..) => "WebDriverScriptCommand",
TickAllAnimations(..) => "TickAllAnimations", TickAllAnimations(..) => "TickAllAnimations",
TransitionEvent { .. } => "TransitionEvent", TransitionOrAnimationEvent { .. } => "TransitionOrAnimationEvent",
WebFontLoaded(..) => "WebFontLoaded", WebFontLoaded(..) => "WebFontLoaded",
DispatchIFrameLoadEvent { .. } => "DispatchIFrameLoadEvent", DispatchIFrameLoadEvent { .. } => "DispatchIFrameLoadEvent",
DispatchStorageEvent(..) => "DispatchStorageEvent", DispatchStorageEvent(..) => "DispatchStorageEvent",

View file

@ -172,6 +172,17 @@ impl KeyframesAnimationState {
self.current_direction = old_direction; self.current_direction = old_direction;
self.started_at = new_started_at; self.started_at = new_started_at;
} }
/// Calculate the active-duration of this animation according to
/// https://drafts.csswg.org/css-animations/#active-duration. active-duration
/// is not really meaningful for infinite animations so we just return 0
/// here in that case.
pub fn active_duration(&self) -> f64 {
match self.iteration_state {
KeyframesIterationState::Finite(_, max) => self.duration * (max as f64),
KeyframesIterationState::Infinite => 0.,
}
}
} }
impl fmt::Debug for KeyframesAnimationState { impl fmt::Debug for KeyframesAnimationState {

View file

@ -1,6 +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/21564
expected: TIMEOUT
[getAnimations for CSS Animations with animation-name: none] [getAnimations for CSS Animations with animation-name: none]
expected: FAIL expected: FAIL
@ -35,7 +34,7 @@
expected: FAIL expected: FAIL
[getAnimations for CSS Animations that have finished but are forwards filling] [getAnimations for CSS Animations that have finished but are forwards filling]
expected: TIMEOUT expected: FAIL
[getAnimations returns objects with the same identity] [getAnimations returns objects with the same identity]
expected: FAIL expected: FAIL
@ -56,7 +55,7 @@
expected: FAIL expected: FAIL
[getAnimations for CSS Animations that have finished] [getAnimations for CSS Animations that have finished]
expected: TIMEOUT expected: FAIL
[{ subtree: false } on a leaf element returns the element's animations and ignore pseudo-elements] [{ subtree: false } on a leaf element returns the element's animations and ignore pseudo-elements]
expected: FAIL expected: FAIL

View file

@ -1,124 +0,0 @@
[animationevent-interface.html]
[the event is an instance of AnimationEvent]
expected: FAIL
[animationName is readonly]
expected: FAIL
[type argument is null]
expected: FAIL
[elapsedTime has default value of 0.0]
expected: FAIL
[elapsedTime is readonly]
expected: FAIL
[elapsedTime set to 0.5]
expected: FAIL
[animationEventInit argument is empty dictionary]
expected: FAIL
[event type set to undefined]
expected: FAIL
[Missing type argument]
expected: FAIL
[AnimationEvent.pseudoElement initialized from the dictionary]
expected: FAIL
[the event inherts from Event]
expected: FAIL
[type argument is string]
expected: FAIL
[animationName set to 'sample']
expected: FAIL
[animationEventInit argument is undefined]
expected: FAIL
[animationEventInit argument is null]
expected: FAIL
[AnimationEventInit properties set value]
expected: FAIL
[animationName has default value of empty string]
expected: FAIL
[elapsedTime set to -0.5]
expected: FAIL
[elapsedTime cannot be set to -Infinity]
expected: FAIL
[elapsedTime cannot be set to Infinity]
expected: FAIL
[animationName set to [\]]
expected: FAIL
[elapsedTime set to null]
expected: FAIL
[elapsedTime set to an object with a valueOf function]
expected: FAIL
[animationName set to an object with a valueOf function]
expected: FAIL
[elapsedTime cannot be set to NaN]
expected: FAIL
[elapsedTime set to true]
expected: FAIL
[elapsedTime set to '']
expected: FAIL
[elapsedTime set to [0.5\]]
expected: FAIL
[elapsedTime set to undefined]
expected: FAIL
[animationName set to true]
expected: FAIL
[elapsedTime set to false]
expected: FAIL
[elapsedTime set to [\]]
expected: FAIL
[elapsedTime cannot be set to [0.5, 1.0\]]
expected: FAIL
[animationName set to null]
expected: FAIL
[animationName set to an object]
expected: FAIL
[elapsedTime cannot be set to 'sample']
expected: FAIL
[animationName set to undefined]
expected: FAIL
[animationName set to [1, 2, 3\]]
expected: FAIL
[animationName set to false]
expected: FAIL
[animationName set to a number]
expected: FAIL
[elapsedTime cannot be set to an object]
expected: FAIL

View file

@ -4,9 +4,6 @@
[animationiteration event is instanceof AnimationEvent] [animationiteration event is instanceof AnimationEvent]
expected: TIMEOUT expected: TIMEOUT
[animationend event is instanceof AnimationEvent]
expected: TIMEOUT
[animationstart event is instanceof AnimationEvent] [animationstart event is instanceof AnimationEvent]
expected: TIMEOUT expected: TIMEOUT

View file

@ -1,61 +1,25 @@
[idlharness.html] [idlharness.html]
[AnimationEvent interface: attribute pseudoElement]
expected: FAIL
[AnimationEvent interface: existence and properties of interface prototype object]
expected: FAIL
[Window interface: attribute onanimationend]
expected: FAIL
[Document interface: attribute onanimationiteration] [Document interface: attribute onanimationiteration]
expected: FAIL expected: FAIL
[AnimationEvent interface object length]
expected: FAIL
[CSSKeyframeRule interface: attribute style] [CSSKeyframeRule interface: attribute style]
expected: FAIL expected: FAIL
[AnimationEvent interface object name]
expected: FAIL
[AnimationEvent interface: attribute elapsedTime]
expected: FAIL
[Document interface: attribute onanimationstart] [Document interface: attribute onanimationstart]
expected: FAIL expected: FAIL
[HTMLElement interface: attribute onanimationiteration] [HTMLElement interface: attribute onanimationiteration]
expected: FAIL expected: FAIL
[AnimationEvent must be primary interface of new AnimationEvent("animationstart")]
expected: FAIL
[AnimationEvent interface: existence and properties of interface prototype object's @@unscopables property]
expected: FAIL
[Window interface: attribute onanimationcancel] [Window interface: attribute onanimationcancel]
expected: FAIL expected: FAIL
[CSSKeyframeRule interface: attribute keyText] [CSSKeyframeRule interface: attribute keyText]
expected: FAIL expected: FAIL
[AnimationEvent interface: new AnimationEvent("animationstart") must inherit property "pseudoElement" with the proper type]
expected: FAIL
[Stringification of new AnimationEvent("animationstart")]
expected: FAIL
[Document interface: attribute onanimationend]
expected: FAIL
[HTMLElement interface: attribute onanimationstart] [HTMLElement interface: attribute onanimationstart]
expected: FAIL expected: FAIL
[AnimationEvent interface: new AnimationEvent("animationstart") must inherit property "animationName" with the proper type]
expected: FAIL
[Document interface: attribute onanimationcancel] [Document interface: attribute onanimationcancel]
expected: FAIL expected: FAIL
@ -65,24 +29,9 @@
[HTMLElement interface: attribute onanimationcancel] [HTMLElement interface: attribute onanimationcancel]
expected: FAIL expected: FAIL
[AnimationEvent interface: attribute animationName]
expected: FAIL
[Window interface: attribute onanimationiteration] [Window interface: attribute onanimationiteration]
expected: FAIL expected: FAIL
[AnimationEvent interface: new AnimationEvent("animationstart") must inherit property "elapsedTime" with the proper type]
expected: FAIL
[CSSKeyframeRule interface: keyframes.cssRules[0\] must inherit property "keyText" with the proper type] [CSSKeyframeRule interface: keyframes.cssRules[0\] must inherit property "keyText" with the proper type]
expected: FAIL expected: FAIL
[AnimationEvent interface: existence and properties of interface prototype object's "constructor" property]
expected: FAIL
[AnimationEvent interface: existence and properties of interface object]
expected: FAIL
[HTMLElement interface: attribute onanimationend]
expected: FAIL

View file

@ -1,9 +1,8 @@
[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/21564
expected: TIMEOUT
[Verify border-bottom-color before animation] [Verify border-bottom-color before animation]
expected: FAIL expected: FAIL
[Verify border-bottom-color after animation] [Verify border-bottom-color after animation]
expected: TIMEOUT expected: FAIL

View file

@ -1,9 +1,8 @@
[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/21564
expected: TIMEOUT
[Verify transform before animation] [Verify transform before animation]
expected: FAIL expected: FAIL
[Verify transform after animation] [Verify transform after animation]
expected: TIMEOUT expected: FAIL

View file

@ -1,9 +1,8 @@
[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/21564
expected: TIMEOUT
[Verify color before animation] [Verify color before animation]
expected: FAIL expected: FAIL
[Verify color after animation] [Verify color after animation]
expected: TIMEOUT expected: FAIL

View file

@ -1,9 +1,8 @@
[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/21564
expected: TIMEOUT
[Verify color before animation] [Verify color before animation]
expected: FAIL expected: FAIL
[Verify color after animation] [Verify color after animation]
expected: TIMEOUT expected: FAIL

View file

@ -1,9 +1,8 @@
[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/21564
expected: TIMEOUT
[Verify color before animation] [Verify color before animation]
expected: FAIL expected: FAIL
[Verify color after animation] [Verify color after animation]
expected: TIMEOUT expected: FAIL

View file

@ -1,9 +1,8 @@
[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/21564
expected: TIMEOUT
[Verify color before animation] [Verify color before animation]
expected: FAIL expected: FAIL
[Verify color after animation] [Verify color after animation]
expected: TIMEOUT expected: FAIL

View file

@ -1,4 +1,5 @@
[webkit-animation-end-event.html] [webkit-animation-end-event.html]
expected: TIMEOUT
[event types for prefixed and unprefixed animationend event handlers should be named appropriately] [event types for prefixed and unprefixed animationend event handlers should be named appropriately]
expected: FAIL expected: FAIL
@ -6,13 +7,13 @@
expected: FAIL expected: FAIL
[webkitAnimationEnd event listener should not trigger if an unprefixed event handler also exists] [webkitAnimationEnd event listener should not trigger if an unprefixed event handler also exists]
expected: FAIL expected: NOTRUN
[webkitAnimationEnd event listener is case sensitive] [webkitAnimationEnd event listener is case sensitive]
expected: FAIL expected: NOTRUN
[webkitAnimationEnd event listener should trigger for an animation] [webkitAnimationEnd event listener should trigger for an animation]
expected: FAIL expected: TIMEOUT
[onwebkitanimationend event handler should trigger for an animation] [onwebkitanimationend event handler should trigger for an animation]
expected: FAIL expected: FAIL
@ -20,20 +21,17 @@
[onanimationend and onwebkitanimationend are not aliases] [onanimationend and onwebkitanimationend are not aliases]
expected: FAIL expected: FAIL
[dispatchEvent of a webkitAnimationEnd event does not trigger an unprefixed event handler or listener]
expected: FAIL
[dispatchEvent of a webkitAnimationEnd event does trigger a prefixed event handler or listener] [dispatchEvent of a webkitAnimationEnd event does trigger a prefixed event handler or listener]
expected: FAIL expected: FAIL
[webkitAnimationEnd event listener should not trigger if an unprefixed listener also exists] [webkitAnimationEnd event listener should not trigger if an unprefixed listener also exists]
expected: FAIL expected: NOTRUN
[dispatchEvent of an animationend event does not trigger a prefixed event handler or listener] [dispatchEvent of an animationend event does not trigger a prefixed event handler or listener]
expected: FAIL expected: FAIL
[event types for prefixed and unprefixed animationend event listeners should be named appropriately] [event types for prefixed and unprefixed animationend event listeners should be named appropriately]
expected: FAIL expected: NOTRUN
[onwebkitanimationend event handler should not trigger if an unprefixed event handler also exists] [onwebkitanimationend event handler should not trigger if an unprefixed event handler also exists]
expected: FAIL expected: FAIL

View file

@ -1,7 +1,4 @@
[webkit-transition-end-event.html] [webkit-transition-end-event.html]
[dispatchEvent of a webkitTransitionEnd event does not trigger an unprefixed event handler or listener]
expected: FAIL
[dispatchEvent of an transitionend event does not trigger a prefixed event handler or listener] [dispatchEvent of an transitionend event does not trigger a prefixed event handler or listener]
expected: FAIL expected: FAIL

View file

@ -13870,7 +13870,7 @@
] ]
], ],
"interfaces.html": [ "interfaces.html": [
"1a579837cc22d31a7792566615d9e321b3d7fe39", "b6034be26af3c2edd1ef41703857fa99bd2cd639",
[ [
null, null,
{} {}

View file

@ -12,6 +12,7 @@
// IMPORTANT: Do not change the list below without review from a DOM peer! // IMPORTANT: Do not change the list below without review from a DOM peer!
test_interfaces([ test_interfaces([
"AnalyserNode", "AnalyserNode",
"AnimationEvent",
"Attr", "Attr",
"Audio", "Audio",
"AudioBuffer", "AudioBuffer",