diff options
author | Martin Robinson <mrobinson@igalia.com> | 2020-04-29 12:19:21 +0200 |
---|---|---|
committer | Martin Robinson <mrobinson@igalia.com> | 2020-05-01 15:29:57 +0200 |
commit | 3903c1fb98373ae08323c36e147f2f041d6f0280 (patch) | |
tree | a6f8a7ac23540c944ceefbf526a1fcf8def91968 | |
parent | 6fb75c2b9eb3825932a22b1c7a6d7ce03809fbb2 (diff) | |
download | servo-3903c1fb98373ae08323c36e147f2f041d6f0280.tar.gz servo-3903c1fb98373ae08323c36e147f2f041d6f0280.zip |
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.
27 files changed, 334 insertions, 330 deletions
diff --git a/components/layout/animation.rs b/components/layout/animation.rs index 00fbf4bc1d6..64c1dfd2e0a 100644 --- a/components/layout/animation.rs +++ b/components/layout/animation.rs @@ -13,20 +13,20 @@ use ipc_channel::ipc::IpcSender; use msg::constellation_msg::PipelineId; use script_traits::UntrustedNodeAddress; use script_traits::{ - AnimationState, ConstellationControlMsg, LayoutMsg as ConstellationMsg, TransitionEventType, + AnimationState, ConstellationControlMsg, LayoutMsg as ConstellationMsg, + TransitionOrAnimationEventType, }; use servo_arc::Arc; -use style::animation::{ - update_style_for_animation, Animation, ElementAnimationState, PropertyAnimation, -}; +use style::animation::{update_style_for_animation, Animation, ElementAnimationState}; use style::dom::TElement; use style::font_metrics::ServoMetricsProvider; use style::selector_parser::RestyleDamage; use style::timer::Timer; -/// Collect newly transitioning nodes, which is used by the script process during -/// forced, synchronous reflows to root DOM nodes for the duration of their transitions. -pub fn collect_newly_transitioning_nodes( +/// Collect newly animating nodes, which is used by the script process during +/// forced, synchronous reflows to root DOM nodes for the duration of their +/// animations or transitions. +pub fn collect_newly_animating_nodes( animation_states: &FxHashMap<OpaqueNode, ElementAnimationState>, 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. if let Some(ref mut out) = out { out.extend(animation_states.iter().flat_map(|(node, state)| { - let num_transitions = state - .new_animations - .iter() - .filter(|animation| animation.is_transition()) - .count(); - std::iter::repeat(node.to_untrusted_node_address()).take(num_transitions) + std::iter::repeat(node.to_untrusted_node_address()).take(state.new_animations.len()) })); } } @@ -101,21 +96,28 @@ pub fn update_animation_state( now: f64, 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 - .send(ConstellationControlMsg::TransitionEvent { + .send(ConstellationControlMsg::TransitionOrAnimationEvent { pipeline_id, event_type, node: node.to_untrusted_node_address(), - property_name: property_animation.property_name().into(), - elapsed_time: property_animation.duration, + property_or_animation_name, + elapsed_time, }) .unwrap() }; - handle_cancelled_animations(animation_state, send_transition_event); - handle_running_animations(animation_state, now, send_transition_event); - handle_new_animations(animation_state, send_transition_event); + handle_cancelled_animations(animation_state, now, send_event); + handle_running_animations(animation_state, now, send_event); + handle_new_animations(animation_state, send_event); } /// 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( animation_state: &mut ElementAnimationState, now: f64, - mut send_transition_event: impl FnMut(&PropertyAnimation, TransitionEventType), + mut send_event: impl FnMut(&Animation, TransitionOrAnimationEventType, f64), ) { let mut running_animations = 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); } else { debug!("Finishing transition: {:?}", running_animation); - if let Animation::Transition(_, _, ref property_animation) = running_animation { - send_transition_event(property_animation, TransitionEventType::TransitionEnd); - } + let (event_type, elapsed_time) = match running_animation { + 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); } } @@ -157,12 +168,19 @@ pub fn handle_running_animations( /// well. pub fn handle_cancelled_animations( 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(..) { match animation { - Animation::Transition(_, _, ref property_animation) => { - send_transition_event(property_animation, TransitionEventType::TransitionCancel) + Animation::Transition(_, start_time, _) => { + // 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. Animation::Keyframes(..) => {}, @@ -172,12 +190,18 @@ pub fn handle_cancelled_animations( pub fn handle_new_animations( 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(..) { match animation { - Animation::Transition(_, _, ref property_animation) => { - send_transition_event(property_animation, TransitionEventType::TransitionRun) + Animation::Transition(..) => { + // 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(..) => {}, } diff --git a/components/layout/context.rs b/components/layout/context.rs index f98f8bc8395..fc243b8234f 100644 --- a/components/layout/context.rs +++ b/components/layout/context.rs @@ -86,9 +86,9 @@ pub struct LayoutContext<'a> { /// A None value means that this layout was not initiated by the script thread. 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. - pub newly_transitioning_nodes: Option<Mutex<Vec<UntrustedNodeAddress>>>, + pub newly_animating_nodes: Option<Mutex<Vec<UntrustedNodeAddress>>>, } impl<'a> Drop for LayoutContext<'a> { diff --git a/components/layout_thread/lib.rs b/components/layout_thread/lib.rs index 4a346315e8a..046a9a6f30e 100644 --- a/components/layout_thread/lib.rs +++ b/components/layout_thread/lib.rs @@ -648,7 +648,7 @@ impl LayoutThread { } else { None }, - newly_transitioning_nodes: if script_initiated_layout { + newly_animating_nodes: if script_initiated_layout { Some(Mutex::new(vec![])) } else { None @@ -1565,11 +1565,11 @@ impl LayoutThread { }; 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![]), 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() { Some(root_flow) => root_flow, @@ -1741,7 +1741,7 @@ impl LayoutThread { invalid_nodes, ); 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>, ) { { - let mut newly_transitioning_nodes = context - .newly_transitioning_nodes + let mut newly_animating_nodes = context + .newly_animating_nodes .as_ref() .map(|nodes| nodes.lock().unwrap()); - let newly_transitioning_nodes = - newly_transitioning_nodes.as_mut().map(|nodes| &mut **nodes); + let newly_animating_nodes = newly_animating_nodes.as_mut().map(|nodes| &mut **nodes); let mut animation_states = self.animation_states.write(); - - animation::collect_newly_transitioning_nodes( - &animation_states, - newly_transitioning_nodes, - ); - + animation::collect_newly_animating_nodes(&animation_states, newly_animating_nodes); animation::update_animation_states( &self.constellation_chan, &self.script_chan, diff --git a/components/script/dom/animationevent.rs b/components/script/dom/animationevent.rs new file mode 100644 index 00000000000..191b460228d --- /dev/null +++ b/components/script/dom/animationevent.rs @@ -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() + } +} diff --git a/components/script/dom/macros.rs b/components/script/dom/macros.rs index b1ea4f5cde2..871e831dc82 100644 --- a/components/script/dom/macros.rs +++ b/components/script/dom/macros.rs @@ -442,6 +442,7 @@ macro_rules! global_event_handlers( ); (NoOnload) => ( event_handler!(abort, GetOnabort, SetOnabort); + event_handler!(animationend, GetOnanimationend, SetOnanimationend); event_handler!(cancel, GetOncancel, SetOncancel); event_handler!(canplay, GetOncanplay, SetOncanplay); event_handler!(canplaythrough, GetOncanplaythrough, SetOncanplaythrough); diff --git a/components/script/dom/mod.rs b/components/script/dom/mod.rs index a0f6241d55e..9536f113417 100644 --- a/components/script/dom/mod.rs +++ b/components/script/dom/mod.rs @@ -213,6 +213,7 @@ pub mod abstractworker; pub mod abstractworkerglobalscope; pub mod activation; pub mod analysernode; +pub mod animationevent; pub mod attr; pub mod audiobuffer; pub mod audiobuffersourcenode; diff --git a/components/script/dom/webidls/AnimationEvent.webidl b/components/script/dom/webidls/AnimationEvent.webidl new file mode 100644 index 00000000000..fd9d6c47f7e --- /dev/null +++ b/components/script/dom/webidls/AnimationEvent.webidl @@ -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 = ""; +}; diff --git a/components/script/dom/webidls/EventHandler.webidl b/components/script/dom/webidls/EventHandler.webidl index a3514182806..b5617564fbc 100644 --- a/components/script/dom/webidls/EventHandler.webidl +++ b/components/script/dom/webidls/EventHandler.webidl @@ -90,6 +90,11 @@ interface mixin GlobalEventHandlers { 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 partial interface mixin GlobalEventHandlers { attribute EventHandler ontransitionrun; diff --git a/components/script/dom/window.rs b/components/script/dom/window.rs index e6d8c1bd13f..ccb4b8dfb48 100644 --- a/components/script/dom/window.rs +++ b/components/script/dom/window.rs @@ -1582,11 +1582,11 @@ impl Window { } let for_display = reflow_goal == ReflowGoal::Full; + let pipeline_id = self.upcast::<GlobalScope>().pipeline_id(); if for_display && self.suppress_reflow.get() { debug!( "Suppressing reflow pipeline {} for reason {:?} before FirstLoad or RefreshTick", - self.upcast::<GlobalScope>().pipeline_id(), - reason + pipeline_id, reason ); return false; } @@ -1617,11 +1617,7 @@ impl Window { // On debug mode, print the reflow event information. if self.relayout_event { - debug_reflow_events( - self.upcast::<GlobalScope>().pipeline_id(), - &reflow_goal, - &reason, - ); + debug_reflow_events(pipeline_id, &reflow_goal, &reason); } let document = self.Document(); @@ -1699,12 +1695,11 @@ impl Window { { let (responder, responder_listener) = 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(); ROUTER.add_route( responder_listener.to_opaque(), 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 @@ -1714,7 +1709,7 @@ impl Window { } unsafe { - ScriptThread::note_newly_transitioning_nodes(complete.newly_transitioning_nodes); + ScriptThread::note_newly_animating_nodes(pipeline_id, complete.newly_animating_nodes); } true diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs index 16fd866f1b9..165e526fa76 100644 --- a/components/script/script_thread.rs +++ b/components/script/script_thread.rs @@ -19,7 +19,9 @@ use crate::devtools; use crate::document_loader::DocumentLoader; +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::DocumentBinding::{ DocumentMethods, DocumentReadyState, }; @@ -138,9 +140,9 @@ use script_traits::{ EventResult, HistoryEntryReplacement, InitialScriptState, JsEvalResult, LayoutMsg, LoadData, LoadOrigin, MediaSessionActionType, MouseButton, MouseEventType, NewLayoutInfo, Painter, ProgressiveWebMetricType, ScriptMsg, ScriptThreadFactory, ScriptToConstellationChan, - StructuredSerializedData, TimerSchedulerMsg, TouchEventType, TouchId, TransitionEventType, - UntrustedNodeAddress, UpdatePipelineIdReason, WebrenderIpcSender, WheelDelta, WindowSizeData, - WindowSizeType, + StructuredSerializedData, TimerSchedulerMsg, TouchEventType, TouchId, + TransitionOrAnimationEventType, UntrustedNodeAddress, UpdatePipelineIdReason, + WebrenderIpcSender, WheelDelta, WindowSizeData, WindowSizeType, }; use servo_atoms::Atom; 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 /// 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> 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| { let script_thread = &*root.get().unwrap(); let js_runtime = script_thread.js_runtime.rt(); @@ -831,8 +836,10 @@ impl ScriptThread { .into_iter() .map(|n| Dom::from_ref(&*from_untrusted_node_address(js_runtime, n))); script_thread - .transitioning_nodes + .animating_nodes .borrow_mut() + .entry(pipeline_id) + .or_insert_with(Vec::new) .extend(new_nodes); }) } @@ -1345,7 +1352,7 @@ impl ScriptThread { docs_with_no_blocking_loads: Default::default(), - transitioning_nodes: Default::default(), + animating_nodes: Default::default(), custom_element_reaction_stack: CustomElementReactionStack::new(), @@ -1697,7 +1704,7 @@ impl ScriptThread { FocusIFrame(id, ..) => Some(id), WebDriverScriptCommand(id, ..) => Some(id), TickAllAnimations(id) => Some(id), - TransitionEvent { .. } => None, + TransitionOrAnimationEvent { .. } => None, WebFontLoaded(id) => Some(id), DispatchIFrameLoadEvent { target: _, @@ -1898,18 +1905,18 @@ impl ScriptThread { ConstellationControlMsg::TickAllAnimations(pipeline_id) => { self.handle_tick_all_animations(pipeline_id) }, - ConstellationControlMsg::TransitionEvent { + ConstellationControlMsg::TransitionOrAnimationEvent { pipeline_id, event_type, node, - property_name, + property_or_animation_name, elapsed_time, } => { - self.handle_transition_event( + self.handle_transition_or_animation_event( pipeline_id, event_type, node, - property_name, + property_or_animation_name, elapsed_time, ); }, @@ -2846,6 +2853,9 @@ impl ScriptThread { .send((id, ScriptMsg::PipelineExited)) .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. if let Some(document) = document { // We don't want to dispatch `mouseout` event pointing to non-existing element @@ -2916,66 +2926,91 @@ impl ScriptThread { /// Handles firing of transition-related events. /// /// TODO(mrobinson): Add support for more events. - fn handle_transition_event( + fn handle_transition_or_animation_event( &self, pipeline_id: PipelineId, - event_type: TransitionEventType, + event_type: TransitionOrAnimationEventType, unsafe_node: UntrustedNodeAddress, - property_name: String, + property_or_animation_name: String, elapsed_time: f64, ) { let js_runtime = self.js_runtime.rt(); let node = unsafe { from_untrusted_node_address(js_runtime, unsafe_node) }; - let node_index = self - .transitioning_nodes - .borrow() - .iter() - .position(|n| &**n as *const _ == &*node as *const _); - 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 end notification for unknown node."); - return; - }, - }; + // We limit the scope of the borrow here, so that we don't maintain this borrow + // and then incidentally trigger another layout. That might result in a double + // 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." + ); + }, + }; - if self.closed_pipelines.borrow().contains(&pipeline_id) { - warn!("Ignoring transition event for closed pipeline."); - return; + let node_index = nodes + .iter() + .position(|n| &**n as *const _ == &*node as *const _); + 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; + }, + }; + + if event_type.finalizes_transition_or_animation() { + nodes.remove(node_index); + } + } + + // Not quite the right thing - see #13865. + if event_type.finalizes_transition_or_animation() { + node.dirty(NodeDamage::NodeStyleDamaged); } let event_atom = match event_type { - TransitionEventType::TransitionRun => atom!("transitionrun"), - TransitionEventType::TransitionEnd => { - // Not quite the right thing - see #13865. - node.dirty(NodeDamage::NodeStyleDamaged); - self.transitioning_nodes.borrow_mut().remove(node_index); - atom!("transitionend") - }, - TransitionEventType::TransitionCancel => { - self.transitioning_nodes.borrow_mut().remove(node_index); - atom!("transitioncancel") - }, + TransitionOrAnimationEventType::AnimationEnd => atom!("animationend"), + TransitionOrAnimationEventType::TransitionCancel => atom!("transitioncancel"), + TransitionOrAnimationEventType::TransitionEnd => atom!("transitionend"), + TransitionOrAnimationEventType::TransitionRun => atom!("transitionrun"), }; - - let event_init = TransitionEventInit { - parent: EventInit { - bubbles: true, - cancelable: false, - }, - propertyName: DOMString::from(property_name), - elapsedTime: Finite::new(elapsed_time as f32).unwrap(), - // TODO: Handle pseudo-elements properly - pseudoElement: DOMString::new(), + let parent = EventInit { + bubbles: true, + cancelable: false, }; + // 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); - TransitionEvent::new(&window, event_atom, &event_init) - .upcast::<Event>() - .fire(node.upcast()); + + 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) + .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. diff --git a/components/script_layout_interface/message.rs b/components/script_layout_interface/message.rs index 63854134cac..fe117436b51 100644 --- a/components/script_layout_interface/message.rs +++ b/components/script_layout_interface/message.rs @@ -193,7 +193,7 @@ pub struct ReflowComplete { /// The list of images that were encountered that are in progress. pub pending_images: Vec<PendingImage>, /// 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. diff --git a/components/script_traits/lib.rs b/components/script_traits/lib.rs index 3db883b6536..c23221f929f 100644 --- a/components/script_traits/lib.rs +++ b/components/script_traits/lib.rs @@ -282,16 +282,40 @@ pub enum UpdatePipelineIdReason { 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)] -pub enum TransitionEventType { - /// The transition has started running. +pub enum TransitionOrAnimationEventType { + /// "The transitionrun event occurs when a transition is created (i.e., when it + /// is added to the set of running transitions)." TransitionRun, - /// The 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, - /// The transition ended early for some reason, such as the property - /// no longer being transitionable or being replaced by another transition. + /// "The transitioncancel event occurs when a transition is canceled." 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. @@ -380,16 +404,17 @@ pub enum ConstellationControlMsg { WebDriverScriptCommand(PipelineId, WebDriverScriptCommand), /// Notifies script thread that all animations are done TickAllAnimations(PipelineId), - /// Notifies the script thread that a transition related event should be sent. - TransitionEvent { + /// Notifies the script thread that a transition or animation related event should be sent. + TransitionOrAnimationEvent { /// The pipeline id of the layout task that sent this message. pipeline_id: PipelineId, /// The type of transition event this should trigger. - event_type: TransitionEventType, + event_type: TransitionOrAnimationEventType, /// The address of the node which owns this transition. node: UntrustedNodeAddress, - /// The property name of the property that is transitioning. - property_name: String, + /// 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). + property_or_animation_name: String, /// The elapsed time property to send with this transition event. elapsed_time: f64, }, @@ -452,7 +477,7 @@ impl fmt::Debug for ConstellationControlMsg { FocusIFrame(..) => "FocusIFrame", WebDriverScriptCommand(..) => "WebDriverScriptCommand", TickAllAnimations(..) => "TickAllAnimations", - TransitionEvent { .. } => "TransitionEvent", + TransitionOrAnimationEvent { .. } => "TransitionOrAnimationEvent", WebFontLoaded(..) => "WebFontLoaded", DispatchIFrameLoadEvent { .. } => "DispatchIFrameLoadEvent", DispatchStorageEvent(..) => "DispatchStorageEvent", diff --git a/components/style/animation.rs b/components/style/animation.rs index 59c9f847e96..eed3aaada1a 100644 --- a/components/style/animation.rs +++ b/components/style/animation.rs @@ -172,6 +172,17 @@ impl KeyframesAnimationState { self.current_direction = old_direction; 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 { diff --git a/tests/wpt/metadata/css/css-animations/Element-getAnimations.tentative.html.ini b/tests/wpt/metadata/css/css-animations/Element-getAnimations.tentative.html.ini index bf27b93175b..854c8c6e480 100644 --- a/tests/wpt/metadata/css/css-animations/Element-getAnimations.tentative.html.ini +++ b/tests/wpt/metadata/css/css-animations/Element-getAnimations.tentative.html.ini @@ -1,6 +1,5 @@ [Element-getAnimations.tentative.html] bug: https://github.com/servo/servo/issues/21564 - expected: TIMEOUT [getAnimations for CSS Animations with animation-name: none] expected: FAIL @@ -35,7 +34,7 @@ expected: FAIL [getAnimations for CSS Animations that have finished but are forwards filling] - expected: TIMEOUT + expected: FAIL [getAnimations returns objects with the same identity] expected: FAIL @@ -56,7 +55,7 @@ expected: FAIL [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] expected: FAIL diff --git a/tests/wpt/metadata/css/css-animations/animationevent-interface.html.ini b/tests/wpt/metadata/css/css-animations/animationevent-interface.html.ini deleted file mode 100644 index a4d160df59e..00000000000 --- a/tests/wpt/metadata/css/css-animations/animationevent-interface.html.ini +++ /dev/null @@ -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 - diff --git a/tests/wpt/metadata/css/css-animations/animationevent-types.html.ini b/tests/wpt/metadata/css/css-animations/animationevent-types.html.ini index 9a5d66fd1ef..70ed3998119 100644 --- a/tests/wpt/metadata/css/css-animations/animationevent-types.html.ini +++ b/tests/wpt/metadata/css/css-animations/animationevent-types.html.ini @@ -4,9 +4,6 @@ [animationiteration event is instanceof AnimationEvent] expected: TIMEOUT - [animationend event is instanceof AnimationEvent] - expected: TIMEOUT - [animationstart event is instanceof AnimationEvent] expected: TIMEOUT diff --git a/tests/wpt/metadata/css/css-animations/idlharness.html.ini b/tests/wpt/metadata/css/css-animations/idlharness.html.ini index 01e1d42bf7f..6b8f7e14289 100644 --- a/tests/wpt/metadata/css/css-animations/idlharness.html.ini +++ b/tests/wpt/metadata/css/css-animations/idlharness.html.ini @@ -1,61 +1,25 @@ [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] expected: FAIL - [AnimationEvent interface object length] - expected: FAIL - [CSSKeyframeRule interface: attribute style] expected: FAIL - [AnimationEvent interface object name] - expected: FAIL - - [AnimationEvent interface: attribute elapsedTime] - expected: FAIL - [Document interface: attribute onanimationstart] expected: FAIL [HTMLElement interface: attribute onanimationiteration] 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] expected: FAIL [CSSKeyframeRule interface: attribute keyText] 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] expected: FAIL - [AnimationEvent interface: new AnimationEvent("animationstart") must inherit property "animationName" with the proper type] - expected: FAIL - [Document interface: attribute onanimationcancel] expected: FAIL @@ -65,24 +29,9 @@ [HTMLElement interface: attribute onanimationcancel] expected: FAIL - [AnimationEvent interface: attribute animationName] - expected: FAIL - [Window interface: attribute onanimationiteration] 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] 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 - diff --git a/tests/wpt/metadata/css/css-variables/variable-animation-substitute-into-keyframe-shorthand.html.ini b/tests/wpt/metadata/css/css-variables/variable-animation-substitute-into-keyframe-shorthand.html.ini index baa566691e6..5d6b41c4332 100644 --- a/tests/wpt/metadata/css/css-variables/variable-animation-substitute-into-keyframe-shorthand.html.ini +++ b/tests/wpt/metadata/css/css-variables/variable-animation-substitute-into-keyframe-shorthand.html.ini @@ -1,9 +1,8 @@ [variable-animation-substitute-into-keyframe-shorthand.html] bug: https://github.com/servo/servo/issues/21564 - expected: TIMEOUT [Verify border-bottom-color before animation] expected: FAIL [Verify border-bottom-color after animation] - expected: TIMEOUT + expected: FAIL diff --git a/tests/wpt/metadata/css/css-variables/variable-animation-substitute-into-keyframe-transform.html.ini b/tests/wpt/metadata/css/css-variables/variable-animation-substitute-into-keyframe-transform.html.ini index 83d7114a33c..77ff1af8a7d 100644 --- a/tests/wpt/metadata/css/css-variables/variable-animation-substitute-into-keyframe-transform.html.ini +++ b/tests/wpt/metadata/css/css-variables/variable-animation-substitute-into-keyframe-transform.html.ini @@ -1,9 +1,8 @@ [variable-animation-substitute-into-keyframe-transform.html] bug: https://github.com/servo/servo/issues/21564 - expected: TIMEOUT [Verify transform before animation] expected: FAIL [Verify transform after animation] - expected: TIMEOUT + expected: FAIL diff --git a/tests/wpt/metadata/css/css-variables/variable-animation-substitute-into-keyframe.html.ini b/tests/wpt/metadata/css/css-variables/variable-animation-substitute-into-keyframe.html.ini index 1e19344cbec..225a08b98ac 100644 --- a/tests/wpt/metadata/css/css-variables/variable-animation-substitute-into-keyframe.html.ini +++ b/tests/wpt/metadata/css/css-variables/variable-animation-substitute-into-keyframe.html.ini @@ -1,9 +1,8 @@ [variable-animation-substitute-into-keyframe.html] bug: https://github.com/servo/servo/issues/21564 - expected: TIMEOUT [Verify color before animation] expected: FAIL [Verify color after animation] - expected: TIMEOUT + expected: FAIL diff --git a/tests/wpt/metadata/css/css-variables/variable-animation-substitute-within-keyframe-fallback.html.ini b/tests/wpt/metadata/css/css-variables/variable-animation-substitute-within-keyframe-fallback.html.ini index ec903a327a8..a0f8055196a 100644 --- a/tests/wpt/metadata/css/css-variables/variable-animation-substitute-within-keyframe-fallback.html.ini +++ b/tests/wpt/metadata/css/css-variables/variable-animation-substitute-within-keyframe-fallback.html.ini @@ -1,9 +1,8 @@ [variable-animation-substitute-within-keyframe-fallback.html] bug: https://github.com/servo/servo/issues/21564 - expected: TIMEOUT [Verify color before animation] expected: FAIL [Verify color after animation] - expected: TIMEOUT + expected: FAIL diff --git a/tests/wpt/metadata/css/css-variables/variable-animation-substitute-within-keyframe-multiple.html.ini b/tests/wpt/metadata/css/css-variables/variable-animation-substitute-within-keyframe-multiple.html.ini index d4f2fefef86..baa8d60195e 100644 --- a/tests/wpt/metadata/css/css-variables/variable-animation-substitute-within-keyframe-multiple.html.ini +++ b/tests/wpt/metadata/css/css-variables/variable-animation-substitute-within-keyframe-multiple.html.ini @@ -1,9 +1,8 @@ [variable-animation-substitute-within-keyframe-multiple.html] bug: https://github.com/servo/servo/issues/21564 - expected: TIMEOUT [Verify color before animation] expected: FAIL [Verify color after animation] - expected: TIMEOUT + expected: FAIL diff --git a/tests/wpt/metadata/css/css-variables/variable-animation-substitute-within-keyframe.html.ini b/tests/wpt/metadata/css/css-variables/variable-animation-substitute-within-keyframe.html.ini index 6d7eafff16b..e1e48b487e4 100644 --- a/tests/wpt/metadata/css/css-variables/variable-animation-substitute-within-keyframe.html.ini +++ b/tests/wpt/metadata/css/css-variables/variable-animation-substitute-within-keyframe.html.ini @@ -1,9 +1,8 @@ [variable-animation-substitute-within-keyframe.html] bug: https://github.com/servo/servo/issues/21564 - expected: TIMEOUT [Verify color before animation] expected: FAIL [Verify color after animation] - expected: TIMEOUT + expected: FAIL diff --git a/tests/wpt/metadata/dom/events/webkit-animation-end-event.html.ini b/tests/wpt/metadata/dom/events/webkit-animation-end-event.html.ini index c560f81d9b6..2b8b4ec6d09 100644 --- a/tests/wpt/metadata/dom/events/webkit-animation-end-event.html.ini +++ b/tests/wpt/metadata/dom/events/webkit-animation-end-event.html.ini @@ -1,4 +1,5 @@ [webkit-animation-end-event.html] + expected: TIMEOUT [event types for prefixed and unprefixed animationend event handlers should be named appropriately] expected: FAIL @@ -6,13 +7,13 @@ expected: FAIL [webkitAnimationEnd event listener should not trigger if an unprefixed event handler also exists] - expected: FAIL + expected: NOTRUN [webkitAnimationEnd event listener is case sensitive] - expected: FAIL + expected: NOTRUN [webkitAnimationEnd event listener should trigger for an animation] - expected: FAIL + expected: TIMEOUT [onwebkitanimationend event handler should trigger for an animation] expected: FAIL @@ -20,20 +21,17 @@ [onanimationend and onwebkitanimationend are not aliases] 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] expected: FAIL [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] expected: FAIL [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] expected: FAIL diff --git a/tests/wpt/metadata/dom/events/webkit-transition-end-event.html.ini b/tests/wpt/metadata/dom/events/webkit-transition-end-event.html.ini index 2e7d6443e31..4ee5fc21ce9 100644 --- a/tests/wpt/metadata/dom/events/webkit-transition-end-event.html.ini +++ b/tests/wpt/metadata/dom/events/webkit-transition-end-event.html.ini @@ -1,7 +1,4 @@ [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] expected: FAIL diff --git a/tests/wpt/mozilla/meta/MANIFEST.json b/tests/wpt/mozilla/meta/MANIFEST.json index c4e49f39a76..6ad65fa2d0f 100644 --- a/tests/wpt/mozilla/meta/MANIFEST.json +++ b/tests/wpt/mozilla/meta/MANIFEST.json @@ -13870,7 +13870,7 @@ ] ], "interfaces.html": [ - "1a579837cc22d31a7792566615d9e321b3d7fe39", + "b6034be26af3c2edd1ef41703857fa99bd2cd639", [ null, {} diff --git a/tests/wpt/mozilla/tests/mozilla/interfaces.html b/tests/wpt/mozilla/tests/mozilla/interfaces.html index 1a579837cc2..b6034be26af 100644 --- a/tests/wpt/mozilla/tests/mozilla/interfaces.html +++ b/tests/wpt/mozilla/tests/mozilla/interfaces.html @@ -12,6 +12,7 @@ // IMPORTANT: Do not change the list below without review from a DOM peer! test_interfaces([ "AnalyserNode", + "AnimationEvent", "Attr", "Audio", "AudioBuffer", |