servo: Merge #13056 - Implement transition event and infrastructure (from KiChjang:transition-event); r=mbrubeck
authorKeith Yeung <kungfukeith11@gmail.com>
Wed, 12 Oct 2016 02:08:37 -0500
changeset 339894 cd4435e2d8a5ec8acd35431f4ba90f7c16a6799d
parent 339893 c9bc6b91fbec94a4b33d9c2498ee3b6e65f7c8ca
child 339895 0a3f272cd195d0b8b19a8df94d30a36a6b139e6a
push id31307
push usergszorc@mozilla.com
push dateSat, 04 Feb 2017 00:59:06 +0000
treeherdermozilla-central@94079d43835f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmbrubeck
servo: Merge #13056 - Implement transition event and infrastructure (from KiChjang:transition-event); r=mbrubeck Fixes #10245. Source-Repo: https://github.com/servo/servo Source-Revision: cd2f950de3bcbf88208dec16f7025ff516473e0d
servo/components/layout/animation.rs
servo/components/layout_thread/lib.rs
servo/components/script/dom/bindings/js.rs
servo/components/script/dom/macros.rs
servo/components/script/dom/mod.rs
servo/components/script/dom/transitionevent.rs
servo/components/script/dom/webidls/EventHandler.webidl
servo/components/script/dom/webidls/TransitionEvent.webidl
servo/components/script/layout_wrapper.rs
servo/components/script/script_thread.rs
servo/components/script_traits/lib.rs
servo/components/style/animation.rs
servo/components/style/dom.rs
servo/components/style/matching.rs
servo/components/style/properties/helpers/animated_properties.mako.rs
servo/components/style_traits/lib.rs
--- a/servo/components/layout/animation.rs
+++ b/servo/components/layout/animation.rs
@@ -5,27 +5,28 @@
 //! CSS transitions and animations.
 
 use context::SharedLayoutContext;
 use flow::{self, Flow};
 use gfx::display_list::OpaqueNode;
 use ipc_channel::ipc::IpcSender;
 use msg::constellation_msg::PipelineId;
 use script_layout_interface::restyle_damage::RestyleDamage;
-use script_traits::{AnimationState, LayoutMsg as ConstellationMsg};
+use script_traits::{AnimationState, ConstellationControlMsg, LayoutMsg as ConstellationMsg};
 use std::collections::HashMap;
 use std::sync::mpsc::Receiver;
 use style::animation::{Animation, update_style_for_animation};
 use style::dom::TRestyleDamage;
 use style::timer::Timer;
 
 /// Processes any new animations that were discovered after style recalculation.
 /// Also expire any old animations that have completed, inserting them into
 /// `expired_animations`.
 pub fn update_animation_state(constellation_chan: &IpcSender<ConstellationMsg>,
+                              script_chan: &IpcSender<ConstellationControlMsg>,
                               running_animations: &mut HashMap<OpaqueNode, Vec<Animation>>,
                               expired_animations: &mut HashMap<OpaqueNode, Vec<Animation>>,
                               new_animations_receiver: &Receiver<Animation>,
                               pipeline_id: PipelineId,
                               timer: &Timer) {
     let mut new_running_animations = vec![];
     while let Ok(animation) = new_animations_receiver.try_recv() {
         let mut should_push = true;
@@ -65,31 +66,38 @@ pub fn update_animation_state(constellat
     // TODO: Do not expunge Keyframes animations, since we need that state if
     // the animation gets re-triggered. Probably worth splitting in two
     // different maps, or at least using a linked list?
     let mut keys_to_remove = vec![];
     for (key, running_animations) in running_animations.iter_mut() {
         let mut animations_still_running = vec![];
         for mut running_animation in running_animations.drain(..) {
             let still_running = !running_animation.is_expired() && match running_animation {
-                Animation::Transition(_, started_at, ref frame, _expired) => {
+                Animation::Transition(_, _, started_at, ref frame, _expired) => {
                     now < started_at + frame.duration
                 }
                 Animation::Keyframes(_, _, ref mut state) => {
                     // This animation is still running, or we need to keep
                     // iterating.
                     now < state.started_at + state.duration || state.tick()
                 }
             };
 
             if still_running {
                 animations_still_running.push(running_animation);
                 continue
             }
 
+            if let Animation::Transition(_, unsafe_node, _, ref frame, _) = running_animation {
+                script_chan.send(ConstellationControlMsg::TransitionEnd(unsafe_node,
+                                                                        frame.property_animation.property_name(),
+                                                                        frame.duration))
+                           .unwrap();
+            }
+
             expired_animations.entry(*key)
                               .or_insert_with(Vec::new)
                               .push(running_animation);
         }
 
         if animations_still_running.is_empty() {
             keys_to_remove.push(*key);
         } else {
--- a/servo/components/layout_thread/lib.rs
+++ b/servo/components/layout_thread/lib.rs
@@ -1472,16 +1472,17 @@ impl LayoutThread {
                                                data: &Reflow,
                                                query_type: Option<&ReflowQueryType>,
                                                document: Option<&ServoLayoutDocument>,
                                                rw_data: &mut LayoutThreadData,
                                                layout_context: &mut SharedLayoutContext) {
         if let Some(mut root_flow) = self.root_flow.clone() {
             // Kick off animations if any were triggered, expire completed ones.
             animation::update_animation_state(&self.constellation_chan,
+                                              &self.script_chan,
                                               &mut *self.running_animations.write().unwrap(),
                                               &mut *self.expired_animations.write().unwrap(),
                                               &self.new_animations_receiver,
                                               self.id,
                                               &self.timer);
 
             profile(time::ProfilerCategory::LayoutRestyleDamagePropagation,
                     self.profiler_metadata(),
--- a/servo/components/script/dom/bindings/js.rs
+++ b/servo/components/script/dom/bindings/js.rs
@@ -446,16 +446,24 @@ impl<T: HeapGCValue> HeapSizeOf for MutN
 impl<T: Reflectable> LayoutJS<T> {
     /// Returns an unsafe pointer to the interior of this JS object. This is
     /// the only method that be safely accessed from layout. (The fact that
     /// this is unsafe is what necessitates the layout wrappers.)
     pub unsafe fn unsafe_get(&self) -> *const T {
         debug_assert!(thread_state::get().is_layout());
         *self.ptr
     }
+
+    /// Returns a reference to the interior of this JS object. This method is
+    /// safe to call because it originates from the layout thread, and it cannot
+    /// mutate DOM nodes.
+    pub fn get_for_script(&self) -> &T {
+        debug_assert!(thread_state::get().is_script());
+        unsafe { &**self.ptr }
+    }
 }
 
 /// Get a reference out of a rooted value.
 pub trait RootedReference<'root> {
     /// The type of the reference.
     type Ref: 'root;
     /// Obtain a reference out of the rooted value.
     fn r(&'root self) -> Self::Ref;
--- a/servo/components/script/dom/macros.rs
+++ b/servo/components/script/dom/macros.rs
@@ -467,16 +467,17 @@ macro_rules! global_event_handlers(
         event_handler!(seeking, GetOnseeking, SetOnseeking);
         event_handler!(select, GetOnselect, SetOnselect);
         event_handler!(show, GetOnshow, SetOnshow);
         event_handler!(stalled, GetOnstalled, SetOnstalled);
         event_handler!(submit, GetOnsubmit, SetOnsubmit);
         event_handler!(suspend, GetOnsuspend, SetOnsuspend);
         event_handler!(timeupdate, GetOntimeupdate, SetOntimeupdate);
         event_handler!(toggle, GetOntoggle, SetOntoggle);
+        event_handler!(transitionend, GetOntransitionend, SetOntransitionend);
         event_handler!(volumechange, GetOnvolumechange, SetOnvolumechange);
         event_handler!(waiting, GetOnwaiting, SetOnwaiting);
     )
 );
 
 // https://html.spec.whatwg.org/multipage/#windoweventhandlers
 // see webidls/EventHandler.webidl
 // As more methods get added, just update them here.
--- a/servo/components/script/dom/mod.rs
+++ b/servo/components/script/dom/mod.rs
@@ -399,16 +399,17 @@ pub mod testbindingiterable;
 pub mod testbindingpairiterable;
 pub mod testbindingproxy;
 pub mod text;
 pub mod textdecoder;
 pub mod textencoder;
 pub mod touch;
 pub mod touchevent;
 pub mod touchlist;
+pub mod transitionevent;
 pub mod treewalker;
 pub mod uievent;
 pub mod url;
 pub mod urlhelper;
 pub mod urlsearchparams;
 pub mod userscripts;
 pub mod validation;
 pub mod validitystate;
new file mode 100644
--- /dev/null
+++ b/servo/components/script/dom/transitionevent.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 http://mozilla.org/MPL/2.0/. */
+
+use dom::bindings::codegen::Bindings::EventBinding::EventMethods;
+use dom::bindings::codegen::Bindings::TransitionEventBinding;
+use dom::bindings::codegen::Bindings::TransitionEventBinding::{TransitionEventInit, TransitionEventMethods};
+use dom::bindings::error::Fallible;
+use dom::bindings::inheritance::Castable;
+use dom::bindings::js::Root;
+use dom::bindings::num::Finite;
+use dom::bindings::reflector::reflect_dom_object;
+use dom::bindings::str::DOMString;
+use dom::event::Event;
+use dom::globalscope::GlobalScope;
+use string_cache::Atom;
+
+#[dom_struct]
+pub struct TransitionEvent {
+    event: Event,
+    property_name: Atom,
+    elapsed_time: Finite<f32>,
+    pseudo_element: DOMString,
+}
+
+impl TransitionEvent {
+    pub fn new_inherited(init: &TransitionEventInit) -> TransitionEvent {
+        TransitionEvent {
+            event: Event::new_inherited(),
+            property_name: Atom::from(init.propertyName.clone()),
+            elapsed_time: init.elapsedTime.clone(),
+            pseudo_element: init.pseudoElement.clone()
+        }
+    }
+
+    pub fn new(global: &GlobalScope,
+               type_: Atom,
+               init: &TransitionEventInit) -> Root<TransitionEvent> {
+        let ev = reflect_dom_object(box TransitionEvent::new_inherited(init),
+                                    global,
+                                    TransitionEventBinding::Wrap);
+        {
+            let event = ev.upcast::<Event>();
+            event.init_event(type_, init.parent.bubbles, init.parent.cancelable);
+        }
+        ev
+    }
+
+    pub fn Constructor(global: &GlobalScope,
+                       type_: DOMString,
+                       init: &TransitionEventInit) -> Fallible<Root<TransitionEvent>> {
+        Ok(TransitionEvent::new(global, Atom::from(type_), init))
+    }
+}
+
+impl TransitionEventMethods for TransitionEvent {
+    // https://drafts.csswg.org/css-transitions/#Events-TransitionEvent-propertyName
+    fn PropertyName(&self) -> DOMString {
+        DOMString::from(&*self.property_name)
+    }
+
+    // https://drafts.csswg.org/css-transitions/#Events-TransitionEvent-elapsedTime
+    fn ElapsedTime(&self) -> Finite<f32> {
+        self.elapsed_time.clone()
+    }
+
+    // https://drafts.csswg.org/css-transitions/#Events-TransitionEvent-pseudoElement
+    fn PseudoElement(&self) -> DOMString {
+        self.pseudo_element.clone()
+    }
+
+    // https://dom.spec.whatwg.org/#dom-event-istrusted
+    fn IsTrusted(&self) -> bool {
+        self.upcast::<Event>().IsTrusted()
+    }
+}
--- a/servo/components/script/dom/webidls/EventHandler.webidl
+++ b/servo/components/script/dom/webidls/EventHandler.webidl
@@ -84,16 +84,21 @@ interface GlobalEventHandlers {
            attribute EventHandler onsubmit;
            attribute EventHandler onsuspend;
            attribute EventHandler ontimeupdate;
            attribute EventHandler ontoggle;
            attribute EventHandler onvolumechange;
            attribute EventHandler onwaiting;
 };
 
+// https://drafts.csswg.org/css-transitions/#interface-globaleventhandlers-idl
+partial interface GlobalEventHandlers {
+           attribute EventHandler ontransitionend;
+};
+
 // https://html.spec.whatwg.org/multipage/#windoweventhandlers
 [NoInterfaceObject, Exposed=(Window,Worker)]
 interface WindowEventHandlers {
            attribute EventHandler onafterprint;
            attribute EventHandler onbeforeprint;
            attribute OnBeforeUnloadEventHandler onbeforeunload;
            attribute EventHandler onhashchange;
            attribute EventHandler onlanguagechange;
new file mode 100644
--- /dev/null
+++ b/servo/components/script/dom/webidls/TransitionEvent.webidl
@@ -0,0 +1,21 @@
+/* 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/. */
+/*
+ * For more information on this interface please see
+ * https://dom.spec.whatwg.org/#event
+ */
+
+[Constructor(DOMString type, optional TransitionEventInit transitionEventInitDict),
+ Exposed=Window]
+interface TransitionEvent : Event {
+  readonly attribute DOMString          propertyName;
+  readonly attribute float              elapsedTime;
+  readonly attribute DOMString          pseudoElement;
+};
+
+dictionary TransitionEventInit : EventInit {
+  DOMString propertyName = "";
+  float elapsedTime = 0.0;
+  DOMString pseudoElement = "";
+};
--- a/servo/components/script/layout_wrapper.rs
+++ b/servo/components/script/layout_wrapper.rs
@@ -408,17 +408,17 @@ impl<'ln> ServoLayoutNode<'ln> {
             }
         } else {
             format!("{:?}: style_data=None", self.script_type_id())
         }
     }
 
     /// Returns the interior of this node as a `LayoutJS`. This is highly unsafe for layout to
     /// call and as such is marked `unsafe`.
-    unsafe fn get_jsmanaged(&self) -> &LayoutJS<Node> {
+    pub unsafe fn get_jsmanaged(&self) -> &LayoutJS<Node> {
         &self.node
     }
 }
 
 // A wrapper around documents that ensures ayout can only ever access safe properties.
 #[derive(Copy, Clone)]
 pub struct ServoLayoutDocument<'ld> {
     document: LayoutJS<Document>,
--- a/servo/components/script/script_thread.rs
+++ b/servo/components/script/script_thread.rs
@@ -18,23 +18,27 @@
 //! loop.
 
 use devtools;
 use devtools_traits::{DevtoolScriptControlMsg, DevtoolsPageInfo};
 use devtools_traits::{ScriptToDevtoolsControlMsg, WorkerId};
 use devtools_traits::CSSError;
 use document_loader::DocumentLoader;
 use dom::bindings::cell::DOMRefCell;
+use dom::bindings::codegen::Bindings::CSSStyleDeclarationBinding::CSSStyleDeclarationMethods;
 use dom::bindings::codegen::Bindings::DocumentBinding::{DocumentMethods, DocumentReadyState};
+use dom::bindings::codegen::Bindings::EventBinding::EventInit;
 use dom::bindings::codegen::Bindings::LocationBinding::LocationMethods;
+use dom::bindings::codegen::Bindings::TransitionEventBinding::TransitionEventInit;
 use dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
 use dom::bindings::conversions::{ConversionResult, FromJSValConvertible, StringificationBehavior};
 use dom::bindings::inheritance::Castable;
 use dom::bindings::js::{JS, MutNullableHeap, Root, RootCollection};
 use dom::bindings::js::{RootCollectionPtr, RootedReference};
+use dom::bindings::num::Finite;
 use dom::bindings::refcounted::Trusted;
 use dom::bindings::reflector::Reflectable;
 use dom::bindings::str::DOMString;
 use dom::bindings::trace::JSTraceable;
 use dom::bindings::utils::WRAP_CALLBACKS;
 use dom::browsingcontext::BrowsingContext;
 use dom::document::{Document, DocumentProgressHandler, DocumentSource, FocusType, IsHTMLDocument, TouchEventResult};
 use dom::element::Element;
@@ -42,16 +46,17 @@ use dom::event::{Event, EventBubbles, Ev
 use dom::globalscope::GlobalScope;
 use dom::htmlanchorelement::HTMLAnchorElement;
 use dom::node::{Node, NodeDamage, window_from_node};
 use dom::serviceworker::TrustedServiceWorkerAddress;
 use dom::serviceworkerregistration::ServiceWorkerRegistration;
 use dom::servoparser::{ParserContext, ServoParser};
 use dom::servoparser::html::{ParseContext, parse_html};
 use dom::servoparser::xml::{self, parse_xml};
+use dom::transitionevent::TransitionEvent;
 use dom::uievent::UIEvent;
 use dom::window::{ReflowReason, Window};
 use dom::worker::TrustedWorkerAddress;
 use euclid::Rect;
 use euclid::point::Point2D;
 use gfx_traits::LayerId;
 use hyper::header::{ContentType, Headers, HttpDate, LastModified};
 use hyper::header::ReferrerPolicy as ReferrerPolicyHeader;
@@ -60,16 +65,17 @@ use hyper::mime::{Mime, SubLevel, TopLev
 use hyper_serde::Serde;
 use ipc_channel::ipc::{self, IpcSender};
 use ipc_channel::router::ROUTER;
 use js::glue::GetWindowProxyClass;
 use js::jsapi::{JSAutoCompartment, JSContext, JS_SetWrapObjectCallbacks};
 use js::jsapi::{JSTracer, SetWindowProxyClass};
 use js::jsval::UndefinedValue;
 use js::rust::Runtime;
+use layout_wrapper::ServoLayoutNode;
 use mem::heap_size_of_self_and_children;
 use msg::constellation_msg::{FrameType, LoadData, PipelineId, PipelineNamespace};
 use msg::constellation_msg::{ReferrerPolicy, WindowSizeType};
 use net_traits::{AsyncResponseTarget, CoreResourceMsg, LoadConsumer, LoadContext, Metadata, ResourceThreads};
 use net_traits::{IpcSend, LoadData as NetLoadData};
 use net_traits::bluetooth_thread::BluetoothMethodMsg;
 use net_traits::image_cache_thread::{ImageCacheChan, ImageCacheResult, ImageCacheThread};
 use network_listener::NetworkListener;
@@ -92,16 +98,17 @@ use std::collections::{HashMap, HashSet}
 use std::option::Option;
 use std::ptr;
 use std::rc::Rc;
 use std::result::Result;
 use std::sync::{Arc, Mutex};
 use std::sync::atomic::{AtomicBool, Ordering};
 use std::sync::mpsc::{Receiver, Select, Sender, channel};
 use style::context::ReflowGoal;
+use style::dom::{TNode, UnsafeNode};
 use style::thread_state;
 use task_source::TaskSource;
 use task_source::dom_manipulation::{DOMManipulationTask, DOMManipulationTaskSource};
 use task_source::file_reading::FileReadingTaskSource;
 use task_source::history_traversal::HistoryTraversalTaskSource;
 use task_source::networking::NetworkingTaskSource;
 use task_source::user_interaction::{UserInteractionTask, UserInteractionTaskSource};
 use time::Tm;
@@ -912,16 +919,18 @@ impl ScriptThread {
                                                old_pipeline_id,
                                                new_pipeline_id),
             ConstellationControlMsg::FocusIFrame(parent_pipeline_id, pipeline_id) =>
                 self.handle_focus_iframe_msg(parent_pipeline_id, pipeline_id),
             ConstellationControlMsg::WebDriverScriptCommand(pipeline_id, msg) =>
                 self.handle_webdriver_msg(pipeline_id, msg),
             ConstellationControlMsg::TickAllAnimations(pipeline_id) =>
                 self.handle_tick_all_animations(pipeline_id),
+            ConstellationControlMsg::TransitionEnd(unsafe_node, name, duration) =>
+                self.handle_transition_event(unsafe_node, name, duration),
             ConstellationControlMsg::WebFontLoaded(pipeline_id) =>
                 self.handle_web_font_loaded(pipeline_id),
             ConstellationControlMsg::DispatchFrameLoadEvent {
                 target: pipeline_id, parent: parent_pipeline_id } =>
                 self.handle_frame_load_event(parent_pipeline_id, pipeline_id),
             ConstellationControlMsg::FramedContentChanged(parent_pipeline_id, pipeline_id) =>
                 self.handle_framed_content_changed(parent_pipeline_id, pipeline_id),
             ConstellationControlMsg::ReportCSSError(pipeline_id, filename, line, column, msg) =>
@@ -1517,16 +1526,45 @@ impl ScriptThread {
     fn handle_tick_all_animations(&self, id: PipelineId) {
         let document = match self.root_browsing_context().find(id) {
             Some(browsing_context) => browsing_context.active_document(),
             None => return warn!("Message sent to closed pipeline {}.", id),
         };
         document.run_the_animation_frame_callbacks();
     }
 
+    /// Handles firing of transition events.
+    #[allow(unsafe_code)]
+    fn handle_transition_event(&self, unsafe_node: UnsafeNode, name: String, duration: f64) {
+        let node = unsafe { ServoLayoutNode::from_unsafe(&unsafe_node) };
+        let node = unsafe { node.get_jsmanaged().get_for_script() };
+        let window = window_from_node(node);
+
+        if let Some(el) = node.downcast::<Element>() {
+            if &*window.GetComputedStyle(el, None).Display() == "none" {
+                return;
+            }
+        }
+
+        let init = TransitionEventInit {
+            parent: EventInit {
+                bubbles: true,
+                cancelable: false,
+            },
+            propertyName: DOMString::from(name),
+            elapsedTime: Finite::new(duration as f32).unwrap(),
+            // FIXME: Handle pseudo-elements properly
+            pseudoElement: DOMString::new()
+        };
+        let transition_event = TransitionEvent::new(window.upcast(),
+                                                    atom!("transitionend"),
+                                                    &init);
+        transition_event.upcast::<Event>().fire(node.upcast());
+    }
+
     /// Handles a Web font being loaded. Does nothing if the page no longer exists.
     fn handle_web_font_loaded(&self, pipeline_id: PipelineId) {
         if let Some(context) = self.find_child_context(pipeline_id)  {
             self.rebuild_and_force_reflow(&context, ReflowReason::WebFontLoaded);
         }
     }
 
     /// Notify the containing document of a child frame that has completed loading.
--- a/servo/components/script_traits/lib.rs
+++ b/servo/components/script_traits/lib.rs
@@ -61,17 +61,17 @@ use net_traits::bluetooth_thread::Blueto
 use net_traits::image_cache_thread::ImageCacheThread;
 use net_traits::response::HttpsState;
 use profile_traits::mem;
 use profile_traits::time as profile_time;
 use serde::{Deserialize, Deserializer, Serialize, Serializer};
 use std::collections::HashMap;
 use std::fmt;
 use std::sync::mpsc::{Receiver, Sender};
-use style_traits::{PagePx, ViewportPx};
+use style_traits::{PagePx, UnsafeNode, ViewportPx};
 use url::Url;
 use util::ipc::OptionalOpaqueIpcSender;
 use webdriver_msg::{LoadStatus, WebDriverScriptCommand};
 
 pub use script_msg::{LayoutMsg, ScriptMsg, EventResult, LogEntry};
 pub use script_msg::{ServiceWorkerMsg, ScopeThings, SWManagerMsg, SWManagerSenders, DOMMessage};
 
 /// The address of a node. Layout sends these back. They must be validated via
@@ -192,16 +192,18 @@ pub enum ConstellationControlMsg {
     UpdatePipelineId(PipelineId, PipelineId, PipelineId),
     /// Set an iframe to be focused. Used when an element in an iframe gains focus.
     /// First PipelineId is for the parent, second PipelineId is for the actual pipeline.
     FocusIFrame(PipelineId, PipelineId),
     /// Passes a webdriver command to the script thread for execution
     WebDriverScriptCommand(PipelineId, WebDriverScriptCommand),
     /// Notifies script thread that all animations are done
     TickAllAnimations(PipelineId),
+    /// Notifies the script thread of a transition end
+    TransitionEnd(UnsafeNode, String, f64),
     /// Notifies the script thread that a new Web font has been loaded, and thus the page should be
     /// reflowed.
     WebFontLoaded(PipelineId),
     /// Cause a `load` event to be dispatched at the appropriate frame element.
     DispatchFrameLoadEvent {
         /// The pipeline that has been marked as loaded.
         target: PipelineId,
         /// The pipeline that contains a frame loading the target pipeline.
@@ -233,16 +235,17 @@ impl fmt::Debug for ConstellationControl
             ChangeFrameVisibilityStatus(..) => "ChangeFrameVisibilityStatus",
             NotifyVisibilityChange(..) => "NotifyVisibilityChange",
             Navigate(..) => "Navigate",
             MozBrowserEvent(..) => "MozBrowserEvent",
             UpdatePipelineId(..) => "UpdatePipelineId",
             FocusIFrame(..) => "FocusIFrame",
             WebDriverScriptCommand(..) => "WebDriverScriptCommand",
             TickAllAnimations(..) => "TickAllAnimations",
+            TransitionEnd(..) => "TransitionEnd",
             WebFontLoaded(..) => "WebFontLoaded",
             DispatchFrameLoadEvent { .. } => "DispatchFrameLoadEvent",
             FramedContentChanged(..) => "FramedContentChanged",
             ReportCSSError(..) => "ReportCSSError",
             Reload(..) => "Reload"
         })
     }
 }
--- a/servo/components/style/animation.rs
+++ b/servo/components/style/animation.rs
@@ -1,17 +1,17 @@
 /* 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/. */
 
 //! CSS transitions and animations.
 
 use bezier::Bezier;
 use context::SharedStyleContext;
-use dom::OpaqueNode;
+use dom::{OpaqueNode, UnsafeNode};
 use euclid::point::Point2D;
 use keyframes::{KeyframesStep, KeyframesStepValue};
 use properties::{self, ComputedValues, Importance};
 use properties::animated_properties::{AnimatedProperty, TransitionProperty};
 use properties::longhands::animation_direction::computed_value::AnimationDirection;
 use properties::longhands::animation_iteration_count::computed_value::AnimationIterationCount;
 use properties::longhands::animation_play_state::computed_value::AnimationPlayState;
 use properties::longhands::transition_timing_function::computed_value::StartEnd;
@@ -181,44 +181,44 @@ impl KeyframesAnimationState {
 /// State relating to an animation.
 #[derive(Clone, Debug)]
 pub enum Animation {
     /// A transition is just a single frame triggered at a time, with a reflow.
     ///
     /// the f64 field is the start time as returned by `time::precise_time_s()`.
     ///
     /// The `bool` field is werther this animation should no longer run.
-    Transition(OpaqueNode, f64, AnimationFrame, bool),
+    Transition(OpaqueNode, UnsafeNode, f64, AnimationFrame, bool),
     /// A keyframes animation is identified by a name, and can have a
     /// node-dependent state (i.e. iteration count, etc.).
     Keyframes(OpaqueNode, Atom, KeyframesAnimationState),
 }
 
 impl Animation {
     #[inline]
     pub fn mark_as_expired(&mut self) {
         debug_assert!(!self.is_expired());
         match *self {
-            Animation::Transition(_, _, _, ref mut expired) => *expired = true,
+            Animation::Transition(_, _, _, _, ref mut expired) => *expired = true,
             Animation::Keyframes(_, _, ref mut state) => state.expired = true,
         }
     }
 
     #[inline]
     pub fn is_expired(&self) -> bool {
         match *self {
-            Animation::Transition(_, _, _, expired) => expired,
+            Animation::Transition(_, _, _, _, expired) => expired,
             Animation::Keyframes(_, _, ref state) => state.expired,
         }
     }
 
     #[inline]
     pub fn node(&self) -> &OpaqueNode {
         match *self {
-            Animation::Transition(ref node, _, _, _) => node,
+            Animation::Transition(ref node, _, _, _, _) => node,
             Animation::Keyframes(ref node, _, _) => node,
         }
     }
 
     #[inline]
     pub fn is_paused(&self) -> bool {
         match *self {
             Animation::Transition(..) => false,
@@ -241,16 +241,20 @@ pub struct AnimationFrame {
 #[derive(Debug, Clone)]
 pub struct PropertyAnimation {
     property: AnimatedProperty,
     timing_function: TransitionTimingFunction,
     duration: Time, // TODO: isn't this just repeated?
 }
 
 impl PropertyAnimation {
+    pub fn property_name(&self) -> String {
+        self.property.name()
+    }
+
     /// Creates a new property animation for the given transition index and old and new styles.
     /// Any number of animations may be returned, from zero (if the property did not animate) to
     /// one (for a single transition property) to arbitrarily many (for `all`).
     pub fn from_transition(transition_index: usize,
                            old_style: &ComputedValues,
                            new_style: &mut ComputedValues)
                            -> Vec<PropertyAnimation> {
         let mut result = vec![];
@@ -336,17 +340,18 @@ impl PropertyAnimation {
 
 /// Inserts transitions into the queue of running animations as applicable for
 /// the given style difference. This is called from the layout worker threads.
 /// Returns true if any animations were kicked off and false otherwise.
 //
 // TODO(emilio): Take rid of this mutex splitting SharedLayoutContex into a
 // cloneable part and a non-cloneable part..
 pub fn start_transitions_if_applicable(new_animations_sender: &Sender<Animation>,
-                                       node: OpaqueNode,
+                                       opaque_node: OpaqueNode,
+                                       unsafe_node: UnsafeNode,
                                        old_style: &ComputedValues,
                                        new_style: &mut Arc<ComputedValues>,
                                        timer: &Timer)
                                        -> bool {
     let mut had_animations = false;
     for i in 0..new_style.get_box().transition_property_count() {
         // Create any property animations, if applicable.
         let property_animations = PropertyAnimation::from_transition(i, old_style, Arc::make_mut(new_style));
@@ -357,17 +362,17 @@ pub fn start_transitions_if_applicable(n
             property_animation.update(Arc::get_mut(new_style).unwrap(), 0.0);
 
             // Kick off the animation.
             let box_style = new_style.get_box();
             let now = timer.seconds();
             let start_time =
                 now + (box_style.transition_delay_mod(i).seconds() as f64);
             new_animations_sender
-                .send(Animation::Transition(node, start_time, AnimationFrame {
+                .send(Animation::Transition(opaque_node, unsafe_node, start_time, AnimationFrame {
                     duration: box_style.transition_duration_mod(i).seconds() as f64,
                     property_animation: property_animation,
                 }, /* is_expired = */ false)).unwrap();
 
             had_animations = true;
         }
     }
 
@@ -494,17 +499,17 @@ pub fn update_style_for_animation_frame(
 /// If `damage` is provided, inserts the appropriate restyle damage.
 pub fn update_style_for_animation(context: &SharedStyleContext,
                                   animation: &Animation,
                                   style: &mut Arc<ComputedValues>) {
     debug!("update_style_for_animation: entering");
     debug_assert!(!animation.is_expired());
 
     match *animation {
-        Animation::Transition(_, start_time, ref frame, _) => {
+        Animation::Transition(_, _, start_time, ref frame, _) => {
             debug!("update_style_for_animation: transition found");
             let now = context.timer.seconds();
             let mut new_style = (*style).clone();
             let updated_style = update_style_for_animation_frame(&mut new_style,
                                                                  now, start_time,
                                                                  frame);
             if updated_style {
                 *style = new_style
@@ -668,17 +673,17 @@ pub fn complete_expired_transitions(node
     let had_animations_to_expire;
     {
         let all_expired_animations = context.expired_animations.read().unwrap();
         let animations_to_expire = all_expired_animations.get(&node);
         had_animations_to_expire = animations_to_expire.is_some();
         if let Some(ref animations) = animations_to_expire {
             for animation in *animations {
                 // TODO: support animation-fill-mode
-                if let Animation::Transition(_, _, ref frame, _) = *animation {
+                if let Animation::Transition(_, _, _, ref frame, _) = *animation {
                     frame.property_animation.update(Arc::make_mut(style), 1.0);
                 }
             }
         }
     }
 
     if had_animations_to_expire {
         context.expired_animations.write().unwrap().remove(&node);
--- a/servo/components/style/dom.rs
+++ b/servo/components/style/dom.rs
@@ -14,19 +14,17 @@ use restyle_hints::{RESTYLE_DESCENDANTS,
 use selector_impl::{ElementExt, PseudoElement};
 use selector_matching::ApplicableDeclarationBlock;
 use sink::Push;
 use std::fmt::Debug;
 use std::ops::BitOr;
 use std::sync::Arc;
 use string_cache::{Atom, Namespace};
 
-/// Opaque type stored in type-unsafe work queues for parallel layout.
-/// Must be transmutable to and from TNode.
-pub type UnsafeNode = (usize, usize);
+pub use style_traits::UnsafeNode;
 
 /// An opaque handle to a node, which, unlike UnsafeNode, cannot be transformed
 /// back into a non-opaque representation. The only safe operation that can be
 /// performed on this node is to compare it to another opaque handle or to another
 /// OpaqueNode.
 ///
 /// Layout and Graphics use this to safely represent nodes for comparison purposes.
 /// Because the script task's GC does not trace layout, node data cannot be safely stored in layout
--- a/servo/components/style/matching.rs
+++ b/servo/components/style/matching.rs
@@ -564,16 +564,17 @@ trait PrivateMatchMethods: TNode {
 
             // Trigger transitions if necessary. This will reset `this_style` back
             // to its old value if it did trigger a transition.
             if let Some(ref style) = old_style {
                 animations_started |=
                     animation::start_transitions_if_applicable(
                         new_animations_sender,
                         this_opaque,
+                        self.to_unsafe(),
                         &**style,
                         &mut this_style,
                         &shared_context.timer);
             }
 
             cacheable = cacheable && !animations_started
         }
 
--- a/servo/components/style/properties/helpers/animated_properties.mako.rs
+++ b/servo/components/style/properties/helpers/animated_properties.mako.rs
@@ -96,16 +96,26 @@ pub enum AnimatedProperty {
         % if prop.animatable:
             ${prop.camel_case}(longhands::${prop.ident}::computed_value::T,
                                longhands::${prop.ident}::computed_value::T),
         % endif
     % endfor
 }
 
 impl AnimatedProperty {
+    pub fn name(&self) -> String {
+        match *self {
+            % for prop in data.longhands:
+                % if prop.animatable:
+                    AnimatedProperty::${prop.camel_case}(..) => "${prop.name}".to_owned(),
+                % endif
+            % endfor
+        }
+    }
+
     pub fn does_animate(&self) -> bool {
         match *self {
             % for prop in data.longhands:
                 % if prop.animatable:
                     AnimatedProperty::${prop.camel_case}(ref from, ref to) => from != to,
                 % endif
             % endfor
         }
--- a/servo/components/style_traits/lib.rs
+++ b/servo/components/style_traits/lib.rs
@@ -22,16 +22,20 @@ extern crate app_units;
 #[macro_use]
 extern crate cssparser;
 extern crate euclid;
 #[cfg(feature = "servo")] extern crate heapsize;
 extern crate rustc_serialize;
 #[cfg(feature = "servo")] extern crate serde;
 #[cfg(feature = "servo")] #[macro_use] extern crate serde_derive;
 
+/// Opaque type stored in type-unsafe work queues for parallel layout.
+/// Must be transmutable to and from TNode.
+pub type UnsafeNode = (usize, usize);
+
 /// One CSS "px" in the coordinate system of the "initial viewport":
 /// http://www.w3.org/TR/css-device-adapt/#initial-viewport
 ///
 /// ViewportPx is equal to ScreenPx times a "page zoom" factor controlled by the user.  This is
 /// the desktop-style "full page" zoom that enlarges content but then reflows the layout viewport
 /// so it still exactly fits the visible area.
 ///
 /// At the default zoom level of 100%, one PagePx is equal to one ScreenPx.  However, if the