Implement CSS transitions: handle starting and animation of the transitions. (Bug 435441) r=bzbarsky sr=roc
authorL. David Baron <dbaron@dbaron.org>
Wed, 07 Oct 2009 20:22:42 -0700
changeset 33649 ff00a422b6f10d1ba50984413141fb3037deb835
parent 33648 95fb146a73378b03682b68d4067aa930e5b6d9f8
child 33650 d3ad054f80e229d997b64afb01333f027856f5d8
push idunknown
push userunknown
push dateunknown
reviewersbzbarsky, roc
bugs435441
milestone1.9.3a1pre
Implement CSS transitions: handle starting and animation of the transitions. (Bug 435441) r=bzbarsky sr=roc
content/base/src/nsGenericElement.cpp
content/base/src/nsGkAtomList.h
content/smil/Makefile.in
content/smil/nsSMILKeySpline.cpp
content/smil/nsSMILKeySpline.h
layout/base/Makefile.in
layout/base/nsCSSFrameConstructor.h
layout/base/nsFrameManager.cpp
layout/base/nsPresContext.cpp
layout/base/nsPresContext.h
layout/base/nsRefreshDriver.cpp
layout/base/nsRefreshDriver.h
layout/reftests/css-transitions/reftest.list
layout/reftests/css-transitions/transitions-inline-already-wrapped-1.html
layout/reftests/css-transitions/transitions-inline-already-wrapped-2.html
layout/reftests/css-transitions/transitions-inline-ref.html
layout/reftests/css-transitions/transitions-inline-rewrap-1.html
layout/reftests/css-transitions/transitions-inline-rewrap-2.html
layout/reftests/reftest.list
layout/style/Makefile.in
layout/style/nsStyleSet.cpp
layout/style/nsStyleSet.h
layout/style/nsStyleStruct.cpp
layout/style/nsTransitionManager.cpp
layout/style/nsTransitionManager.h
layout/style/test/Makefile.in
layout/style/test/test_transitions.html
xpcom/ds/TimeStamp.h
--- a/content/base/src/nsGenericElement.cpp
+++ b/content/base/src/nsGenericElement.cpp
@@ -2686,16 +2686,26 @@ nsGenericElement::UnbindFromTree(PRBool 
 
     if (HasAttr(kNameSpaceID_XLink, nsGkAtoms::href)) {
       document->ForgetLink(this);
     }
 
     document->ClearBoxObjectFor(this);
   }
 
+  // Ensure that CSS transitions don't continue on an element at a
+  // different place in the tree (even if reinserted before next
+  // animation refresh).
+  // FIXME: Need a test for this.
+  if (HasFlag(NODE_HAS_PROPERTIES)) {
+    DeleteProperty(nsGkAtoms::transitionsOfBeforeProperty);
+    DeleteProperty(nsGkAtoms::transitionsOfAfterProperty);
+    DeleteProperty(nsGkAtoms::transitionsProperty);
+  }
+
   // Unset this since that's what the old code effectively did.
   UnsetFlags(NODE_FORCE_XBL_BINDINGS);
   
 #ifdef MOZ_XUL
   nsXULElement* xulElem = nsXULElement::FromContent(this);
   if (xulElem) {
     xulElem->SetXULBindingParent(nsnull);
   }
--- a/content/base/src/nsGkAtomList.h
+++ b/content/base/src/nsGkAtomList.h
@@ -1669,16 +1669,19 @@ GK_ATOM(overflowContainersProperty, "Ove
 GK_ATOM(excessOverflowContainersProperty, "ExcessOverflowContainersProperty") // nsFrameList*
 GK_ATOM(overflowLinesProperty, "OverflowLinesProperty")    // list of nsLineBox*
 GK_ATOM(overflowOutOfFlowsProperty, "OverflowOutOfFlowsProperty")      // nsFrameList*
 GK_ATOM(preEffectsBBoxProperty, "PreEffectsBBoxProperty") // nsRect*
 GK_ATOM(preTransformBBoxProperty, "PreTransformBBoxProperty") // nsRect*
 GK_ATOM(rowUnpaginatedHeightProperty, "RowUnpaginatedHeightProperty")  // nscoord*
 GK_ATOM(tabWidthProperty, "TabWidthProperty")              // nsTArray<TabSetting>* array of tab widths
 GK_ATOM(tableBCProperty, "TableBCProperty")                // table border collapsing info (e.g. damage area, table border widths)
+GK_ATOM(transitionsProperty, "TransitionsProperty")        // FrameTransitions*
+GK_ATOM(transitionsOfBeforeProperty, "TransitionsOfBeforeProperty") // FrameTransitions*
+GK_ATOM(transitionsOfAfterProperty, "TransitionsOfAfterProperty") // FrameTransitions*
 GK_ATOM(usedMarginProperty, "UsedMarginProperty") // nsMargin*
 GK_ATOM(usedPaddingProperty, "UsedPaddingProperty") // nsMargin*
 GK_ATOM(viewProperty, "ViewProperty")                      
 
 // Content property names
 GK_ATOM(genConInitializerProperty, "QuoteNodeProperty")
 GK_ATOM(labelMouseDownPtProperty, "LabelMouseDownPtProperty")
 
--- a/content/smil/Makefile.in
+++ b/content/smil/Makefile.in
@@ -76,16 +76,17 @@ ifdef ENABLE_TESTS
 TOOL_DIRS		+= test
 endif
 
 EXPORTS		= \
 	  nsISMILAnimationElement.h \
 	  nsISMILAttr.h \
 	  nsSMILAnimationController.h \
 	  nsSMILCompositorTable.h \
+	  nsSMILKeySpline.h \
 	  nsSMILTimeContainer.h \
 	  nsSMILTypes.h \
 	  $(NULL)
 
 include $(topsrcdir)/config/rules.mk
 
 INCLUDES += 	\
 		-I$(srcdir)/../base/src \
--- a/content/smil/nsSMILKeySpline.cpp
+++ b/content/smil/nsSMILKeySpline.cpp
@@ -42,25 +42,27 @@
 #define NEWTON_ITERATIONS          4
 #define NEWTON_MIN_SLOPE           0.02
 #define SUBDIVISION_PRECISION      0.0000001
 #define SUBDIVISION_MAX_ITERATIONS 10
 
 const double nsSMILKeySpline::kSampleStepSize =
                                         1.0 / double(kSplineTableSize - 1);
 
-nsSMILKeySpline::nsSMILKeySpline(double aX1,
-                                 double aY1,
-                                 double aX2,
-                                 double aY2)
-: mX1(aX1),
-  mY1(aY1),
-  mX2(aX2),
-  mY2(aY2)
+void
+nsSMILKeySpline::Init(double aX1,
+                      double aY1,
+                      double aX2,
+                      double aY2)
 {
+  mX1 = aX1;
+  mY1 = aY1;
+  mX2 = aX2;
+  mY2 = aY2;
+
   if (mX1 != mY1 || mX2 != mY2)
     CalcSampleValues();
 }
 
 double
 nsSMILKeySpline::GetSplineValue(double aX) const
 {
   if (mX1 == mY1 && mX2 == mY2)
--- a/content/smil/nsSMILKeySpline.h
+++ b/content/smil/nsSMILKeySpline.h
@@ -39,24 +39,32 @@
 #define NS_SMILKEYSPLINE_H_
 
 /**
  * Utility class to provide scaling defined in a keySplines element.
  */
 class nsSMILKeySpline
 {
 public:
+  nsSMILKeySpline() { /* caller must call Init later */ }
+
   /**
    * Creates a new key spline control point description.
    *
    * aX1, etc. are the x1, y1, x2, y2 cubic Bezier control points as defined by
    * SMILANIM 3.2.3. They must each be in the range 0.0 <= x <= 1.0
    */
   nsSMILKeySpline(double aX1, double aY1,
-                  double aX2, double aY2);
+                  double aX2, double aY2)
+  {
+    Init(aX1, aY1, aX2, aY2);
+  }
+
+  void Init(double aX1, double aY1,
+            double aX2, double aY2);
 
   /**
    * Gets the output (y) value for an input (x).
    *
    * @param aX  The input x value. A floating-point number between 0 and
    *            1 (inclusive).
    */
   double GetSplineValue(double aX) const;
@@ -99,20 +107,20 @@ private:
   }
 
   static double
   C(double aA1)
   {
     return 3.0 * aA1;
   }
 
-  const double         mX1;
-  const double         mY1;
-  const double         mX2;
-  const double         mY2;
+  double               mX1;
+  double               mY1;
+  double               mX2;
+  double               mY2;
 
   enum { kSplineTableSize = 11 };
   double               mSampleValues[kSplineTableSize];
 
   static const double  kSampleStepSize;
 };
 
 #endif // NS_SMILKEYSPLINE_H_
--- a/layout/base/Makefile.in
+++ b/layout/base/Makefile.in
@@ -76,16 +76,17 @@ EXPORTS		= \
 		nsILayoutHistoryState.h \
 		nsIPercentHeightObserver.h  \
 		nsIPresShell.h \
 		nsIReflowCallback.h \
 		nsLayoutErrors.h \
 		nsLayoutUtils.h \
 		nsPresContext.h \
 		nsPresState.h \
+		nsRefreshDriver.h \
 		nsStyleChangeList.h \
 		nsStyleConsts.h \
 		$(NULL)
 
 CPPSRCS		= \
 		nsCSSColorUtils.cpp \
 		nsCSSFrameConstructor.cpp \
 		nsCSSRendering.cpp \
@@ -102,16 +103,17 @@ CPPSRCS		= \
 		nsLayoutDebugger.cpp \
 		nsLayoutHistoryState.cpp \
 		nsLayoutUtils.cpp \
 		nsPresArena.cpp \
 		nsPresContext.cpp \
 		nsPresShell.cpp \
 		nsPresState.cpp \
 		nsQuoteList.cpp \
+		nsRefreshDriver.cpp \
 		nsStyleChangeList.cpp \
 		nsStyleSheetService.cpp \
 		$(NULL)
 
 ifndef MOZ_XUL
 XPIDLSRCS  += \
 		nsIBoxObject.idl \
 		$(NULL)
--- a/layout/base/nsCSSFrameConstructor.h
+++ b/layout/base/nsCSSFrameConstructor.h
@@ -63,16 +63,17 @@ class nsIPresShell;
 class nsFrameManager;
 class nsIDOMHTMLSelectElement;
 class nsPresContext;
 class nsStyleChangeList;
 class nsIFrame;
 struct nsGenConInitializer;
 class ChildIterator;
 class nsICSSAnonBoxPseudo;
+class nsPageContentFrame;
 
 struct nsFindFrameHint
 {
   nsIFrame *mPrimaryFrameForPrevSibling;  // weak ref to the primary frame for the content for which we need a frame
   nsFindFrameHint() : mPrimaryFrameForPrevSibling(nsnull) { }
 };
 
 typedef void (nsLazyFrameConstructionCallback)
--- a/layout/base/nsFrameManager.cpp
+++ b/layout/base/nsFrameManager.cpp
@@ -85,16 +85,17 @@
 #include "nsContentUtils.h"
 #include "nsReadableUtils.h"
 #include "nsUnicharUtils.h"
 #include "nsPrintfCString.h"
 #include "nsLayoutErrors.h"
 #include "nsLayoutUtils.h"
 #include "nsAutoPtr.h"
 #include "imgIRequest.h"
+#include "nsTransitionManager.h"
 
 #include "nsFrameManager.h"
 #ifdef ACCESSIBILITY
 #include "nsIAccessibilityService.h"
 #include "nsIAccessibleEvent.h"
 #endif
 
   #ifdef DEBUG
@@ -921,16 +922,43 @@ nsFrameManager::DebugVerifyStyleTree(nsI
     nsStyleContext* context = aFrame->GetStyleContext();
     nsStyleContext* parentContext = context->GetParent();
     VerifyStyleTree(GetPresContext(), aFrame, parentContext);
   }
 }
 
 #endif // DEBUG
 
+// aContent must be the content for the frame in question, which may be
+// :before/:after content
+static void
+TryStartingTransition(nsPresContext *aPresContext, nsIContent *aContent,
+                      nsStyleContext *aOldStyleContext,
+                      nsRefPtr<nsStyleContext> *aNewStyleContext /* inout */)
+{
+  // Notify the transition manager, and if it starts a transition,
+  // it will give us back a transition-covering style rule which
+  // we'll use to get *another* style context.  We want to ignore
+  // any already-running transitions, but cover up any that we're
+  // currently starting with their start value so we don't start
+  // them again for descendants that inherit that value.
+  nsCOMPtr<nsIStyleRule> coverRule = 
+    aPresContext->TransitionManager()->StyleContextChanged(
+      aContent, aOldStyleContext, *aNewStyleContext);
+  if (coverRule) {
+    nsCOMArray<nsIStyleRule> rules;
+    rules.AppendObject(coverRule);
+    *aNewStyleContext = aPresContext->StyleSet()->ResolveStyleForRules(
+                     (*aNewStyleContext)->GetParent(),
+                     (*aNewStyleContext)->GetPseudoType(),
+                     (*aNewStyleContext)->GetRuleNode(),
+                     rules);
+  }
+}
+
 nsresult
 nsFrameManager::ReParentStyleContext(nsIFrame* aFrame)
 {
   if (nsGkAtoms::placeholderFrame == aFrame->GetType()) {
     // Also reparent the out-of-flow
     nsIFrame* outOfFlow =
       nsPlaceholderFrame::GetRealFrameForPlaceholder(aFrame);
     NS_ASSERTION(outOfFlow, "no out-of-flow frame");
@@ -966,16 +994,26 @@ nsFrameManager::ReParentStyleContext(nsI
     // Currently the IB anonymous block's style context takes the first part's
     // style context as parent, which is wrong since first-line style should
     // not apply to the anonymous block.
 
     newContext = mStyleSet->ReParentStyleContext(presContext, oldContext,
                                                  newParentContext);
     if (newContext) {
       if (newContext != oldContext) {
+        // We probably don't want to initiate transitions from
+        // ReParentStyleContext, since we call it during frame
+        // construction rather than in response to dynamic changes.
+        // Also see the comment at the start of
+        // nsTransitionManager::ConsiderStartingTransition.
+#if 0
+        TryStartingTransition(presContext, aFrame->GetContent(),
+                              oldContext, &newContext);
+#endif
+
         // Make sure to call CalcStyleDifference so that the new context ends
         // up resolving all the structs the old context resolved.
         nsChangeHint styleChange = oldContext->CalcStyleDifference(newContext);
         // The style change is always 0 because we have the same rulenode and
         // CalcStyleDifference optimizes us away.  That's OK, though:
         // reparenting should never trigger a frame reconstruct, and whenever
         // it's happening we already plan to reflow and repaint the frames.
         NS_ASSERTION(!(styleChange & nsChangeHint_ReconstructFrame),
@@ -1255,16 +1293,19 @@ nsFrameManager::ReResolveStyleContext(ns
           // we can use FindChildWithRules to keep a lot of the old
           // style contexts around.  However, we need to start from the
           // same root.
           newContext = oldContext;
         }
       }
 
       if (newContext != oldContext) {
+        TryStartingTransition(aPresContext, aFrame->GetContent(),
+                              oldContext, &newContext);
+
         aMinChange = CaptureChange(oldContext, newContext, aFrame,
                                    content, aChangeList, aMinChange,
                                    assumeDifferenceHint);
         if (!(aMinChange & nsChangeHint_ReconstructFrame)) {
           // if frame gets regenerated, let it keep old context
           aFrame->SetStyleContext(newContext);
         }
       }
--- a/layout/base/nsPresContext.cpp
+++ b/layout/base/nsPresContext.cpp
@@ -89,16 +89,17 @@
 #include "nsFontFaceLoader.h"
 #include "nsIEventListenerManager.h"
 #include "nsStyleStructInlines.h"
 #include "nsIAppShell.h"
 #include "prenv.h"
 #include "nsIPrivateDOMEvent.h"
 #include "nsIDOMEventTarget.h"
 #include "nsObjectFrame.h"
+#include "nsTransitionManager.h"
 
 #ifdef MOZ_SMIL
 #include "nsSMILAnimationController.h"
 #endif // MOZ_SMIL
 
 #ifdef IBMBIDI
 #include "nsBidiPresUtils.h"
 #endif // IBMBIDI
@@ -253,16 +254,18 @@ nsPresContext::nsPresContext(nsIDocument
 nsPresContext::~nsPresContext()
 {
   for (PRUint32 i = 0; i < IMAGE_LOAD_TYPE_COUNT; ++i)
     mImageLoaders[i].Enumerate(destroy_loads, nsnull);
 
   NS_PRECONDITION(!mShell, "Presshell forgot to clear our mShell pointer");
   SetShell(nsnull);
 
+  delete mTransitionManager;
+
   if (mEventManager) {
     // unclear if these are needed, but can't hurt
     mEventManager->NotifyDestroyPresContext(this);
     mEventManager->SetPresContext(nsnull);
 
     NS_RELEASE(mEventManager);
   }
 
@@ -864,16 +867,18 @@ nsPresContext::Init(nsIDeviceContext* aD
   }
 
   mEventManager = new nsEventStateManager();
   if (!mEventManager)
     return NS_ERROR_OUT_OF_MEMORY;
 
   NS_ADDREF(mEventManager);
 
+  mTransitionManager = new nsTransitionManager(this);
+
   mLangService = do_GetService(NS_LANGUAGEATOMSERVICE_CONTRACTID);
 
   // Register callbacks so we're notified when the preferences change
   nsContentUtils::RegisterPrefCallback("font.",
                                        nsPresContext::PrefChangedCallback,
                                        this);
   nsContentUtils::RegisterPrefCallback("browser.display.",
                                        nsPresContext::PrefChangedCallback,
--- a/layout/base/nsPresContext.h
+++ b/layout/base/nsPresContext.h
@@ -67,16 +67,17 @@
 #include "gfxRect.h"
 #include "nsRegion.h"
 #include "nsTArray.h"
 #include "nsAutoPtr.h"
 #include "nsThreadUtils.h"
 #include "nsContentUtils.h"
 #include "nsIWidget.h"
 #include "mozilla/TimeStamp.h"
+#include "nsRefreshDriver.h"
 
 class nsImageLoader;
 #ifdef IBMBIDI
 class nsBidiPresUtils;
 #endif // IBMBIDI
 
 struct nsRect;
 
@@ -96,16 +97,17 @@ class nsICSSPseudoComparator;
 class nsIAtom;
 struct nsStyleBackground;
 struct nsStyleBorder;
 class nsIRunnable;
 class gfxUserFontSet;
 class nsUserFontSet;
 struct nsFontFaceRuleContainer;
 class nsObjectFrame;
+class nsTransitionManager;
 
 #ifdef MOZ_REFLOW_PERF
 class nsIRenderingContext;
 #endif
 
 enum nsWidgetType {
   eWidgetType_Button  	= 1,
   eWidgetType_Checkbox	= 2,
@@ -222,16 +224,26 @@ public:
       return mDocument;
   }
 
 #ifdef _IMPL_NS_LAYOUT
   nsStyleSet* StyleSet() { return GetPresShell()->StyleSet(); }
 
   nsFrameManager* FrameManager()
     { return GetPresShell()->FrameManager(); } 
+
+  nsTransitionManager* TransitionManager() { return mTransitionManager; }
+
+  nsRefreshDriver* RefreshDriver() { return &mRefreshDriver; }
+
+  static nsPresContext* FromRefreshDriver(nsRefreshDriver* aRefreshDriver) {
+    return reinterpret_cast<nsPresContext*>(
+             reinterpret_cast<char*>(aRefreshDriver) -
+             offsetof(nsPresContext, mRefreshDriver));
+  }
 #endif
 
   /**
    * Rebuilds all style data by throwing out the old rule tree and
    * building a new one, and additionally applying aExtraHint (which
    * must not contain nsChangeHint_ReconstructFrame) to the root frame.
    * Also rebuild the user font set.
    */
@@ -940,16 +952,18 @@ protected:
   nsCOMPtr<nsIDocument> mDocument;
   nsIDeviceContext*     mDeviceContext; // [STRONG] could be weak, but
                                         // better safe than sorry.
                                         // Cannot reintroduce cycles
                                         // since there is no dependency
                                         // from gfx back to layout.
   nsIEventStateManager* mEventManager;  // [STRONG]
   nsILookAndFeel*       mLookAndFeel;   // [STRONG]
+  nsRefreshDriver       mRefreshDriver;
+  nsTransitionManager*  mTransitionManager; // owns; it aggregates our refcount
   nsIAtom*              mMedium;        // initialized by subclass ctors;
                                         // weak pointer to static atom
 
   nsILinkHandler*       mLinkHandler;   // [WEAK]
   nsIAtom*              mLangGroup;     // [STRONG]
 
   nsRefPtrHashtable<nsVoidPtrHashKey, nsImageLoader>
                         mImageLoaders[IMAGE_LOAD_TYPE_COUNT];
new file mode 100644
--- /dev/null
+++ b/layout/base/nsRefreshDriver.cpp
@@ -0,0 +1,219 @@
+/* vim: set shiftwidth=2 tabstop=8 autoindent cindent expandtab: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is nsRefreshDriver.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   L. David Baron <dbaron@dbaron.org>, Mozilla Corporation (original author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * Code to notify things that animate before a refresh, at an appropriate
+ * refresh rate.  (Perhaps temporary, until replaced by compositor.)
+ */
+
+#include "nsRefreshDriver.h"
+#include "nsPresContext.h"
+#include "nsComponentManagerUtils.h"
+#include "prlog.h"
+
+/*
+ * TODO:
+ * Once this is hooked in to suppressing updates when the presentation
+ * is not visible, we need to hook it up to FlushPendingNotifications so
+ * that we flush when necessary.
+ */
+
+#define REFRESH_INTERVAL_MILLISECONDS 20
+
+using mozilla::TimeStamp;
+
+nsRefreshDriver::nsRefreshDriver()
+{
+}
+
+nsRefreshDriver::~nsRefreshDriver()
+{
+  NS_ABORT_IF_FALSE(ObserverCount() == 0,
+                    "observers should have unregistered");
+  NS_ABORT_IF_FALSE(!mTimer, "timer should be gone");
+}
+
+TimeStamp
+nsRefreshDriver::MostRecentRefresh() const
+{
+  const_cast<nsRefreshDriver*>(this)->EnsureTimerStarted();
+
+  return mMostRecentRefresh;
+}
+
+PRBool
+nsRefreshDriver::AddRefreshObserver(nsARefreshObserver *aObserver,
+                                    mozFlushType aFlushType)
+{
+  ObserverArray& array = ArrayFor(aFlushType);
+  PRBool success = array.AppendElement(aObserver) != nsnull;
+
+  EnsureTimerStarted();
+
+  return success;
+}
+
+PRBool
+nsRefreshDriver::RemoveRefreshObserver(nsARefreshObserver *aObserver,
+                                       mozFlushType aFlushType)
+{
+  ObserverArray& array = ArrayFor(aFlushType);
+  PRBool success = array.RemoveElement(aObserver);
+
+  if (ObserverCount() == 0) {
+    StopTimer();
+  }
+
+  return success;
+}
+
+void
+nsRefreshDriver::EnsureTimerStarted()
+{
+  if (mTimer) {
+    // It's already been started.
+    return;
+  }
+
+  UpdateMostRecentRefresh();
+
+  mTimer = do_CreateInstance(NS_TIMER_CONTRACTID);
+  if (!mTimer) {
+    return;
+  }
+
+  nsresult rv = mTimer->InitWithCallback(this, REFRESH_INTERVAL_MILLISECONDS,
+                                         nsITimer::TYPE_REPEATING_SLACK);
+  if (NS_FAILED(rv)) {
+    mTimer = nsnull;
+  }
+}
+
+void
+nsRefreshDriver::StopTimer()
+{
+  if (!mTimer) {
+    return;
+  }
+
+  mTimer->Cancel();
+  mTimer = nsnull;
+}
+
+PRUint32
+nsRefreshDriver::ObserverCount() const
+{
+  PRUint32 sum = 0;
+  for (PRUint32 i = 0; i < NS_ARRAY_LENGTH(mObservers); ++i) {
+    sum += mObservers[i].Length();
+  }
+  return sum;
+}
+
+void
+nsRefreshDriver::UpdateMostRecentRefresh()
+{
+  mMostRecentRefresh = TimeStamp::Now();
+}
+
+nsRefreshDriver::ObserverArray&
+nsRefreshDriver::ArrayFor(mozFlushType aFlushType)
+{
+  switch (aFlushType) {
+    case Flush_Style:
+      return mObservers[0];
+    case Flush_Layout:
+      return mObservers[1];
+    case Flush_Display:
+      return mObservers[2];
+    default:
+      NS_ABORT_IF_FALSE(PR_FALSE, "bad flush type");
+      return *static_cast<ObserverArray*>(nsnull);
+  }
+}
+
+/*
+ * nsISupports implementation
+ */
+
+NS_IMPL_ADDREF_USING_AGGREGATOR(nsRefreshDriver,
+                                nsPresContext::FromRefreshDriver(this))
+NS_IMPL_RELEASE_USING_AGGREGATOR(nsRefreshDriver,
+                                 nsPresContext::FromRefreshDriver(this))
+NS_IMPL_QUERY_INTERFACE1(nsRefreshDriver, nsITimerCallback)
+
+/*
+ * nsITimerCallback implementation
+ */
+
+NS_IMETHODIMP
+nsRefreshDriver::Notify(nsITimer *aTimer)
+{
+  UpdateMostRecentRefresh();
+
+  nsPresContext *presContext = nsPresContext::FromRefreshDriver(this);
+  nsCOMPtr<nsIPresShell> presShell = presContext->GetPresShell();
+  if (!presShell) {
+    // Things are being destroyed.
+    StopTimer();
+    return NS_OK;
+  }
+
+  for (PRUint32 i = 0; i < NS_ARRAY_LENGTH(mObservers); ++i) {
+    ObserverArray::EndLimitedIterator etor(mObservers[i]);
+    while (etor.HasMore()) {
+      etor.GetNext()->WillRefresh(mMostRecentRefresh);
+    }
+    if (i == 0) {
+      // This is the Flush_Style case.
+      // FIXME: Maybe we should only flush if the WillRefresh calls did
+      // something?  It's probably ok as-is, though, especially as we
+      // hook up more things here (or to the replacement of this class).
+      // FIXME: We should probably flush for other sets of observers
+      // too.  But we should only flush layout once nsRefreshDriver is
+      // the driver for the interruptible layout timer (and we should
+      // then Flush_InterruptibleLayout).
+      presShell->FlushPendingNotifications(Flush_Style);
+    }
+  }
+
+  if (ObserverCount() == 0) {
+    StopTimer();
+  }
+
+  return NS_OK;
+}
new file mode 100644
--- /dev/null
+++ b/layout/base/nsRefreshDriver.h
@@ -0,0 +1,120 @@
+/* vim: set shiftwidth=2 tabstop=8 autoindent cindent expandtab: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is nsRefreshDriver.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   L. David Baron <dbaron@dbaron.org>, Mozilla Corporation (original author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/*
+ * Code to notify things that animate before a refresh, at an appropriate
+ * refresh rate.  (Perhaps temporary, until replaced by compositor.)
+ */
+
+#ifndef nsRefreshDriver_h_
+#define nsRefreshDriver_h_
+
+#include "mozilla/TimeStamp.h"
+#include "mozFlushType.h"
+#include "nsITimer.h"
+#include "nsCOMPtr.h"
+#include "nsTObserverArray.h"
+
+/**
+ * An abstract base class to be implemented by callers wanting to be
+ * notified at refresh times.  When nothing needs to be painted, callers
+ * may not be notified.
+ */
+class nsARefreshObserver {
+public:
+  virtual void WillRefresh(mozilla::TimeStamp aTime) = 0;
+};
+
+/*
+ * nsRefreshDriver MUST ONLY be constructed as a sub-object of
+ * nsPresContext (since its reference counting methods forward to the
+ * pres context of which it is an mRefreshDriver)
+ */
+class nsRefreshDriver : private nsITimerCallback {
+public:
+  nsRefreshDriver();
+  ~nsRefreshDriver();
+
+  /**
+   * Return the time of the most recent refresh.  This is intended to be
+   * used by callers who want to start an animation now and want to know
+   * what time to consider the start of the animation.  (This helps
+   * ensure that multiple animations started during the same event off
+   * the main event loop have the same start time.)
+   */
+  mozilla::TimeStamp MostRecentRefresh() const;
+
+  /**
+   * Add / remove refresh observers.  Returns whether the operation
+   * succeeded.
+   *
+   * The flush type affects:
+   *   + the order in which the observers are notified (lowest flush
+   *     type to highest, in order registered)
+   *   + (in the future) which observers are suppressed when the display
+   *     doesn't require current position data or isn't currently
+   *     painting, and, correspondingly, which get notified when there
+   *     is a flush during such suppression
+   * and it must be either Flush_Style, Flush_Layout, or Flush_Display.
+   */
+  PRBool AddRefreshObserver(nsARefreshObserver *aObserver,
+                            mozFlushType aFlushType);
+  PRBool RemoveRefreshObserver(nsARefreshObserver *aObserver,
+                               mozFlushType aFlushType);
+private:
+  // nsISupports implementation
+  NS_DECL_ISUPPORTS_INHERITED
+
+  // nsITimerCallback implementation
+  NS_IMETHOD Notify(nsITimer *aTimer);
+
+  typedef nsTObserverArray<nsARefreshObserver*> ObserverArray;
+
+  void EnsureTimerStarted();
+  void StopTimer();
+  PRUint32 ObserverCount() const;
+  void UpdateMostRecentRefresh();
+  ObserverArray& ArrayFor(mozFlushType aFlushType);
+
+  nsCOMPtr<nsITimer> mTimer;
+  mozilla::TimeStamp mMostRecentRefresh; // only valid when mTimer non-null
+
+  // separate arrays for each flush type we support
+  ObserverArray mObservers[3];
+};
+
+#endif /* !defined(nsRefreshDriver_h_) */
new file mode 100644
--- /dev/null
+++ b/layout/reftests/css-transitions/reftest.list
@@ -0,0 +1,4 @@
+== transitions-inline-already-wrapped-1.html transitions-inline-ref.html
+== transitions-inline-already-wrapped-2.html transitions-inline-ref.html
+== transitions-inline-rewrap-1.html transitions-inline-ref.html
+== transitions-inline-rewrap-2.html transitions-inline-ref.html
new file mode 100644
--- /dev/null
+++ b/layout/reftests/css-transitions/transitions-inline-already-wrapped-1.html
@@ -0,0 +1,30 @@
+<html class="reftest-wait">
+<title>Test for CSS transitions and re-wrapping of inlines</title>
+<style type="text/css">
+#test { -moz-transition: 5s color linear 200s; }
+</style>
+<script type="text/javascript">
+
+window.onload = run;
+
+function run() {
+  var test = document.getElementById("test");
+  var unused = test.offsetWidth;
+  // FIXME: It's a bug that we need to do this at all: the way we change
+  // style data essentially violates style rule immutability because we
+  // assume that all of the difference calculation will use
+  // PeekStyleData, which is no longer true with transitions.
+  // See the FIXME in nsTransitionManager::ConsiderStartingTransition.
+  unused = getComputedStyle(test, "").color;
+  test.style.color = "red";
+  unused = test.offsetWidth;
+  unused = getComputedStyle(test, "").color;
+  document.documentElement.removeAttribute("class");
+}
+
+</script>
+<div style="width: 3em">
+<span id="test" style="color: green">
+This is some text with a transition.
+</span>
+</div>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/css-transitions/transitions-inline-already-wrapped-2.html
@@ -0,0 +1,28 @@
+<html class="reftest-wait">
+<title>Test for CSS transitions and re-wrapping of inlines</title>
+<style type="text/css">
+#test { -moz-transition: 20ms color linear 0; }
+</style>
+<script type="text/javascript">
+
+window.onload = run;
+
+function run() {
+  var test = document.getElementById("test");
+  var unused = test.offsetWidth;
+  unused = getComputedStyle(test, "").color;
+  test.style.color = "green";
+  unused = test.offsetWidth;
+  unused = getComputedStyle(test, "").color;
+  setTimeout(step2, 100); // give transition time to run
+}
+function step2() {
+  document.documentElement.removeAttribute("class");
+}
+
+</script>
+<div style="width: 3em">
+<span id="test" style="color: red">
+This is some text with a transition.
+</span>
+</div>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/css-transitions/transitions-inline-ref.html
@@ -0,0 +1,7 @@
+<html>
+<title>Test for CSS transitions and re-wrapping of inlines</title>
+<div style="width: 3em">
+<span id="test" style="color: green">
+This is some text with a transition.
+</span>
+</div>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/css-transitions/transitions-inline-rewrap-1.html
@@ -0,0 +1,33 @@
+<html class="reftest-wait">
+<title>Test for CSS transitions and re-wrapping of inlines</title>
+<style type="text/css">
+#test { -moz-transition: 5s color linear 200s; }
+</style>
+<script type="text/javascript">
+
+window.onload = run;
+
+function run() {
+  var test = document.getElementById("test");
+  var unused = test.offsetWidth;
+  // FIXME: It's a bug that we need to do this at all: the way we change
+  // style data essentially violates style rule immutability because we
+  // assume that all of the difference calculation will use
+  // PeekStyleData, which is no longer true with transitions.
+  // See the FIXME in nsTransitionManager::ConsiderStartingTransition.
+  unused = getComputedStyle(test, "").color;
+  test.style.color = "red";
+  unused = test.offsetWidth;
+  unused = getComputedStyle(test, "").color;
+  test.parentNode.style.width = "3em";
+  unused = test.offsetWidth;
+  unused = getComputedStyle(test, "").color;
+  document.documentElement.removeAttribute("class");
+}
+
+</script>
+<div style="width: 50em">
+<span id="test" style="color: green">
+This is some text with a transition.
+</span>
+</div>
new file mode 100644
--- /dev/null
+++ b/layout/reftests/css-transitions/transitions-inline-rewrap-2.html
@@ -0,0 +1,31 @@
+<html class="reftest-wait">
+<title>Test for CSS transitions and re-wrapping of inlines</title>
+<style type="text/css">
+#test { -moz-transition: 20ms color linear 0; }
+</style>
+<script type="text/javascript">
+
+window.onload = run;
+
+function run() {
+  var test = document.getElementById("test");
+  var unused = test.offsetWidth;
+  unused = getComputedStyle(test, "").color;
+  test.style.color = "green";
+  unused = test.offsetWidth;
+  unused = getComputedStyle(test, "").color;
+  test.parentNode.style.width = "3em";
+  unused = test.offsetWidth;
+  unused = getComputedStyle(test, "").color;
+  setTimeout(step2, 100); // give transition time to run
+}
+function step2() {
+  document.documentElement.removeAttribute("class");
+}
+
+</script>
+<div style="width: 50em">
+<span id="test" style="color: red">
+This is some text with a transition.
+</span>
+</div>
--- a/layout/reftests/reftest.list
+++ b/layout/reftests/reftest.list
@@ -48,16 +48,19 @@ include css-charset/reftest.list
 include css-gradients/reftest.list
 
 # css media queries (tests for print mode)
 include css-mediaqueries/reftest.list
 
 # css namespaces
 include css-namespace/reftest.list
 
+# css transitions
+include css-transitions/reftest.list
+
 # css values and units
 include css-valuesandunits/reftest.list
 
 # columns/
 include columns/reftest.list
 
 # content/
 include ../../content/test/reftest/reftest.list
--- a/layout/style/Makefile.in
+++ b/layout/style/Makefile.in
@@ -136,16 +136,17 @@ CPPSRCS		= \
 		nsRuleNode.cpp \
 		nsStyleAnimation.cpp \
 		nsStyleContext.cpp \
 		nsStyleCoord.cpp \
 		nsStyleSet.cpp \
 		nsStyleStruct.cpp \
 		nsStyleTransformMatrix.cpp \
 		nsStyleUtil.cpp \
+		nsTransitionManager.cpp \
 		$(NULL)
 
 FORCE_STATIC_LIB = 1
 
 include $(topsrcdir)/config/rules.mk
 
 LOCAL_INCLUDES	= \
 		-I$(srcdir)/../../content/base/src \
--- a/layout/style/nsStyleSet.cpp
+++ b/layout/style/nsStyleSet.cpp
@@ -53,16 +53,17 @@
 #include "nsICSSStyleRule.h"
 #include "nsCSSAnonBoxes.h"
 #include "nsCSSPseudoElements.h"
 #include "nsCSSRuleProcessor.h"
 #include "nsIContent.h"
 #include "nsIFrame.h"
 #include "nsContentUtils.h"
 #include "nsRuleProcessorData.h"
+#include "nsTransitionManager.h"
 
 NS_IMPL_ISUPPORTS1(nsEmptyStyleRule, nsIStyleRule)
 
 NS_IMETHODIMP
 nsEmptyStyleRule::MapRuleInfoInto(nsRuleData* aRuleData)
 {
   return NS_OK;
 }
@@ -108,16 +109,18 @@ nsStyleSet::Init(nsPresContext *aPresCon
   }
 
   mRuleTree = nsRuleNode::CreateRootNode(aPresContext);
   if (!mRuleTree) {
     mDefaultStyleData.Destroy(0, aPresContext);
     return NS_ERROR_OUT_OF_MEMORY;
   }
 
+  GatherRuleProcessors(eTransitionSheet);
+
   return NS_OK;
 }
 
 nsresult
 nsStyleSet::BeginReconstruct()
 {
   NS_ASSERTION(!mInReconstruct, "Unmatched begin/end?");
   NS_ASSERTION(mRuleTree, "Reconstructing before first construction?");
@@ -187,16 +190,23 @@ nsStyleSet::GatherRuleProcessors(sheetTy
   mRuleProcessors[aType] = nsnull;
   if (mAuthorStyleDisabled && (aType == eDocSheet || 
                                aType == ePresHintSheet ||
                                aType == eHTMLPresHintSheet ||
                                aType == eStyleAttrSheet)) {
     //don't regather if this level is disabled
     return NS_OK;
   }
+  if (aType == eTransitionSheet) {
+    // We have no sheet for the transitions level; just a rule
+    // processor.  (XXX: We should probably do this for the other
+    // non-CSS levels too!)
+    mRuleProcessors[aType] = PresContext()->TransitionManager();
+    return NS_OK;
+  }
   if (mSheets[aType].Count()) {
     switch (aType) {
       case eAgentSheet:
       case eUserSheet:
       case eDocSheet:
       case eOverrideSheet: {
         // levels containing CSS stylesheets
         nsCOMArray<nsIStyleSheet>& sheets = mSheets[aType];
@@ -581,16 +591,26 @@ nsStyleSet::FileRules(nsIStyleRuleProces
   AddImportantRules(lastUserRN, lastPresHintRN, aRuleWalker); //user
 #ifdef DEBUG
   AssertNoCSSRules(lastPresHintRN, lastAgentRN);
   AssertNoImportantRules(lastPresHintRN, lastAgentRN); // preshints
 #endif
   aRuleWalker->SetLevel(eAgentSheet, PR_TRUE);
   AddImportantRules(lastAgentRN, nsnull, aRuleWalker);     //agent
 
+#ifdef DEBUG
+  nsRuleNode *lastImportantRN = aRuleWalker->GetCurrentNode();
+#endif
+  aRuleWalker->SetLevel(eTransitionSheet, PR_FALSE);
+  (*aCollectorFunc)(mRuleProcessors[eTransitionSheet], aData);
+#ifdef DEBUG
+  AssertNoCSSRules(aRuleWalker->GetCurrentNode(), lastImportantRN);
+  AssertNoImportantRules(aRuleWalker->GetCurrentNode(), lastImportantRN);
+#endif
+
 }
 
 // Enumerate all the rules in a way that doesn't care about the order
 // of the rules and doesn't walk !important-rules.
 void
 nsStyleSet::WalkRuleProcessors(nsIStyleRuleProcessor::EnumFunc aFunc,
                                RuleProcessorData* aData)
 {
@@ -618,16 +638,17 @@ nsStyleSet::WalkRuleProcessors(nsIStyleR
   }
   if (!skipUserStyles && !cutOffInheritance &&
       mRuleProcessors[eDocSheet]) // NOTE: different
     (*aFunc)(mRuleProcessors[eDocSheet], aData);
   if (mRuleProcessors[eStyleAttrSheet])
     (*aFunc)(mRuleProcessors[eStyleAttrSheet], aData);
   if (mRuleProcessors[eOverrideSheet])
     (*aFunc)(mRuleProcessors[eOverrideSheet], aData);
+  (*aFunc)(mRuleProcessors[eTransitionSheet], aData);
 }
 
 PRBool nsStyleSet::BuildDefaultStyleData(nsPresContext* aPresContext)
 {
   NS_ASSERTION(!mDefaultStyleData.mResetData &&
                !mDefaultStyleData.mInheritedData,
                "leaking default style data");
   mDefaultStyleData.mResetData = new (aPresContext) nsResetStyleData;
--- a/layout/style/nsStyleSet.h
+++ b/layout/style/nsStyleSet.h
@@ -191,16 +191,17 @@ class nsStyleSet
   enum sheetType {
     eAgentSheet, // CSS
     ePresHintSheet,
     eUserSheet, // CSS
     eHTMLPresHintSheet,
     eDocSheet, // CSS
     eStyleAttrSheet,
     eOverrideSheet, // CSS
+    eTransitionSheet,
     eSheetTypeCount
     // be sure to keep the number of bits in |mDirty| below and in
     // NS_RULE_NODE_LEVEL_MASK updated when changing the number of sheet
     // types
   };
 
   // APIs to manipulate the style sheet lists.  The sheets in each
   // list are stored with the most significant sheet last.
@@ -346,17 +347,17 @@ class nsStyleSet
   // Old rule trees, which should only be non-empty between
   // BeginReconstruct and EndReconstruct, but in case of bugs that cause
   // style contexts to exist too long, may last longer.
   nsTArray<nsRuleNode*> mOldRuleTrees;
 
   unsigned mInShutdown : 1;
   unsigned mAuthorStyleDisabled: 1;
   unsigned mInReconstruct : 1;
-  unsigned mDirty : 7;  // one dirty bit is used per sheet type
+  unsigned mDirty : 8;  // one dirty bit is used per sheet type
 
 };
 
 inline
 NS_HIDDEN_(void) nsRuleNode::AddRef()
 {
   if (mRefCnt++ == 0 && !IsRoot()) {
     mPresContext->StyleSet()->RuleNodeInUse();
--- a/layout/style/nsStyleStruct.cpp
+++ b/layout/style/nsStyleStruct.cpp
@@ -1904,16 +1904,24 @@ nsChangeHint nsStyleDisplay::CalcDiffere
     for (PRUint8 index = 0; index < 2; ++index)
       if (mTransformOrigin[index] != aOther.mTransformOrigin[index]) {
         NS_UpdateHint(hint, NS_CombineHint(nsChangeHint_ReflowFrame,
                                            nsChangeHint_RepaintFrame));
         break;
       }
   }
   
+  // Note:  Our current behavior for handling changes to transition
+  // properties is to do nothing.  In other words, the transition
+  // property that matters is what it is when the transition begins, and
+  // we don't stop a transition later because the transition property
+  // changed.
+  // FIXME:  Need to test for this and write it in the spec, if it's
+  // compatible with other browsers.  Test for behavior at
+  // http://dbaron.org/css/test/2009/transitions/dynamic-transition-change
   
   return hint;
 }
 
 #ifdef DEBUG
 /* static */
 nsChangeHint nsStyleDisplay::MaxDifference()
 {
new file mode 100644
--- /dev/null
+++ b/layout/style/nsTransitionManager.cpp
@@ -0,0 +1,899 @@
+/* vim: set shiftwidth=2 tabstop=8 autoindent cindent expandtab: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is nsTransitionManager.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   L. David Baron <dbaron@dbaron.org>, Mozilla Corporation (original author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/* Code to start and animate CSS transitions. */
+
+#include "nsTransitionManager.h"
+#include "nsIContent.h"
+#include "nsStyleContext.h"
+#include "nsCSSProps.h"
+#include "mozilla/TimeStamp.h"
+#include "nsRefreshDriver.h"
+#include "nsRuleProcessorData.h"
+#include "nsIStyleRule.h"
+#include "nsRuleWalker.h"
+#include "nsRuleData.h"
+#include "nsSMILKeySpline.h"
+#include "gfxColor.h"
+#include "nsCSSPseudoElements.h"
+#include "nsCSSPropertySet.h"
+#include "nsStyleAnimation.h"
+
+using mozilla::TimeStamp;
+using mozilla::TimeDuration;
+
+/*****************************************************************************
+ * Per-Element data                                                          *
+ *****************************************************************************/
+
+struct ElementPropertyTransition
+{
+  nsCSSProperty mProperty;
+  nsStyleCoord mStartValue, mEndValue;
+  TimeStamp mStartTime; // actual start plus transition delay
+
+  // data from the relevant nsTransition
+  TimeDuration mDuration;
+  nsSMILKeySpline mTimingFunction;
+};
+
+/**
+ * An ElementTransitionsStyleRule overrides style data with the
+ * currently-transitioning value for an element that is executing a
+ * transition.  It only matches when styling with animation.  When we
+ * style without animation, we need to not use it so that we can detect
+ * any new changes; if necessary we restyle immediately afterwards with
+ * animation.
+ */
+class ElementTransitionsStyleRule : public nsIStyleRule
+{
+public:
+  // nsISupportsImplementation
+  NS_DECL_ISUPPORTS
+
+  // nsIStyleRule implementation
+  NS_IMETHOD MapRuleInfoInto(nsRuleData* aRuleData);
+#ifdef DEBUG
+  NS_IMETHOD List(FILE* out = stdout, PRInt32 aIndent = 0) const;
+#endif
+
+  ElementTransitionsStyleRule(ElementTransitions *aOwner,
+                           TimeStamp aRefreshTime)
+    : mElementTransitions(aOwner)
+    , mRefreshTime(aRefreshTime)
+  {}
+
+  void Disconnect() { mElementTransitions = nsnull; }
+
+  ElementTransitions *ElementData() { return mElementTransitions; }
+  TimeStamp RefreshTime() { return mRefreshTime; }
+
+private:
+  ElementTransitions *mElementTransitions;
+  // The time stamp for which this style rule is valid.
+  TimeStamp mRefreshTime;
+};
+
+/**
+ * A CoverTransitionStyleRule sets any value for which we're starting a
+ * transition back to the pre-transition value for the period when we're
+ * resolving style on its descendants, so that we have the required
+ * behavior for initiating transitions on such descendants.  For more
+ * detail, see comment below, above "new CoverTransitionStartStyleRule".
+ */
+class CoverTransitionStartStyleRule : public nsIStyleRule
+{
+public:
+  // nsISupportsImplementation
+  NS_DECL_ISUPPORTS
+
+  // nsIStyleRule implementation
+  NS_IMETHOD MapRuleInfoInto(nsRuleData* aRuleData);
+#ifdef DEBUG
+  NS_IMETHOD List(FILE* out = stdout, PRInt32 aIndent = 0) const;
+#endif
+
+  NS_HIDDEN_(void) CoverValue(nsCSSProperty aProperty,
+                              nsStyleCoord &aStartValue)
+  {
+    CoveredValue v = { aProperty, aStartValue };
+    mCoveredValues.AppendElement(v);
+  }
+
+  NS_HIDDEN_(void)
+    FillStyleStruct(void* aStyleStruct, nsRuleData* aRuleData) const;
+
+  struct CoveredValue {
+    nsCSSProperty mProperty;
+    nsStyleCoord mCoveredValue;
+  };
+
+private:
+  nsTArray<CoveredValue> mCoveredValues;
+};
+
+struct ElementTransitions : public PRCList
+{
+  ElementTransitions(nsIContent *aElement, nsIAtom *aElementProperty,
+                     nsTransitionManager *aTransitionManager)
+    : mElement(aElement)
+    , mElementProperty(aElementProperty)
+    , mTransitionManager(aTransitionManager)
+  {
+    PR_INIT_CLIST(this);
+  }
+  ~ElementTransitions()
+  {
+    DropStyleRule();
+    PR_REMOVE_LINK(this);
+    mTransitionManager->TransitionsRemoved();
+  }
+
+  void Destroy()
+  {
+    // This will call our destructor.
+    mElement->DeleteProperty(mElementProperty);
+  }
+
+  void DropStyleRule();
+  PRBool EnsureStyleRuleFor(TimeStamp aRefreshTime);
+
+
+  // Either zero or one for each CSS property:
+  nsTArray<ElementPropertyTransition> mPropertyTransitions;
+
+  // The style rule for the transitions (which contains the time stamp
+  // for which it is valid).
+  nsRefPtr<ElementTransitionsStyleRule> mStyleRule;
+
+  nsIContent *mElement;
+
+  // the atom we use in mElement's prop table (must be a static atom,
+  // i.e., in an atom list)
+  nsIAtom *mElementProperty;
+
+  nsTransitionManager *mTransitionManager;
+};
+
+static void
+ElementTransitionsPropertyDtor(void           *aObject,
+                               nsIAtom        *aPropertyName,
+                               void           *aPropertyValue,
+                               void           *aData)
+{
+  ElementTransitions *et = static_cast<ElementTransitions*>(aPropertyValue);
+  delete et;
+}
+
+NS_IMPL_ISUPPORTS1(ElementTransitionsStyleRule, nsIStyleRule)
+
+static void
+ElementTransitionsPostResolveCallback(void* aStyleStruct, nsRuleData* aRuleData,
+                                      nsIStyleRule* aRule)
+{
+  ElementTransitionsStyleRule *rule =
+    static_cast<ElementTransitionsStyleRule*>(aRule);
+  ElementTransitions *et = rule->ElementData();
+  for (PRUint32 i = 0, i_end = et->mPropertyTransitions.Length();
+       i < i_end; ++i)
+  {
+    const ElementPropertyTransition &pt = et->mPropertyTransitions[i];
+    if (aRuleData->mSIDs & nsCachedStyleData::GetBitForSID(
+                             nsCSSProps::kSIDTable[pt.mProperty]))
+    {
+      double timePortion =
+        (rule->RefreshTime() - pt.mStartTime).ToSeconds() /
+        pt.mDuration.ToSeconds();
+      if (timePortion < 0.0)
+        timePortion = 0.0; // use start value during transition-delay
+      if (timePortion > 1.0)
+        timePortion = 1.0; // we might be behind on flushing
+
+      double valuePortion =
+        pt.mTimingFunction.GetSplineValue(timePortion);
+      nsStyleCoord value;
+#ifdef DEBUG
+      PRBool ok =
+#endif
+        nsStyleAnimation::Interpolate(pt.mStartValue, pt.mEndValue,
+                                      valuePortion, value);
+      NS_ABORT_IF_FALSE(ok, "could not interpolate values");
+#ifdef DEBUG
+      ok =
+#endif
+        nsStyleAnimation::StoreComputedValue(pt.mProperty,
+                                             aRuleData->mPresContext,
+                                             aStyleStruct, value);
+      NS_ABORT_IF_FALSE(ok, "could not store computed value");
+    }
+  }
+}
+
+NS_IMETHODIMP
+ElementTransitionsStyleRule::MapRuleInfoInto(nsRuleData* aRuleData)
+{
+  nsStyleContext *contextParent = aRuleData->mStyleContext->GetParent();
+  if (contextParent && contextParent->HasPseudoElementData()) {
+    // Don't apply transitions to things inside of pseudo-elements.
+    // FIXME: Add tests for this.
+    return NS_OK;
+  }
+
+  ElementTransitions *et = ElementData();
+  NS_ENSURE_TRUE(et, NS_OK); // FIXME: Why can this be null?
+  for (PRUint32 i = 0, i_end = et->mPropertyTransitions.Length();
+       i < i_end; ++i)
+  {
+    ElementPropertyTransition &pt = et->mPropertyTransitions[i];
+    if (aRuleData->mSIDs & nsCachedStyleData::GetBitForSID(
+                             nsCSSProps::kSIDTable[pt.mProperty]))
+    {
+      nsPostResolveCallback prc =
+        { &ElementTransitionsPostResolveCallback, this };
+      aRuleData->mPostResolveCallbacks.AppendElement(prc);
+      // This really doesn't matter much, since this ought to be
+      // the only node with the rule, but it's good practice for
+      // post-resolve callbacks.
+      aRuleData->mCanStoreInRuleTree = PR_FALSE;
+
+      return NS_OK;
+    }
+  }
+
+  return NS_OK;
+}
+
+#ifdef DEBUG
+NS_IMETHODIMP
+ElementTransitionsStyleRule::List(FILE* out, PRInt32 aIndent) const
+{
+  // WRITE ME?
+  return NS_OK;
+}
+#endif
+
+void
+ElementTransitions::DropStyleRule()
+{
+  if (mStyleRule) {
+    mStyleRule->Disconnect();
+    mStyleRule = nsnull;
+  }
+}
+
+PRBool
+ElementTransitions::EnsureStyleRuleFor(TimeStamp aRefreshTime)
+{
+  if (!mStyleRule || mStyleRule->RefreshTime() != aRefreshTime) {
+    DropStyleRule();
+
+    ElementTransitionsStyleRule *newRule =
+      new ElementTransitionsStyleRule(this, aRefreshTime);
+    if (!newRule) {
+      NS_WARNING("out of memory");
+      return PR_FALSE;
+    }
+
+    mStyleRule = newRule;
+  }
+
+  return PR_TRUE;
+}
+
+NS_IMPL_ISUPPORTS1(CoverTransitionStartStyleRule, nsIStyleRule)
+
+static void
+CoverTransitionStartPostResolveCallback(void* aStyleStruct,
+                                        nsRuleData* aRuleData,
+                                        nsIStyleRule* aRule)
+{
+  CoverTransitionStartStyleRule* coverRule =
+    static_cast<CoverTransitionStartStyleRule*>(aRule);
+  coverRule->FillStyleStruct(aStyleStruct, aRuleData);
+}
+
+NS_IMETHODIMP
+CoverTransitionStartStyleRule::MapRuleInfoInto(nsRuleData* aRuleData)
+{
+  for (PRUint32 i = 0, i_end = mCoveredValues.Length(); i < i_end; ++i) {
+    CoveredValue &cv = mCoveredValues[i];
+    if (aRuleData->mSIDs & nsCachedStyleData::GetBitForSID(
+                             nsCSSProps::kSIDTable[cv.mProperty]))
+    {
+      nsPostResolveCallback prc =
+        { &CoverTransitionStartPostResolveCallback, this };
+      aRuleData->mPostResolveCallbacks.AppendElement(prc);
+      // This really doesn't matter much, since this ought to be
+      // the only node with the rule, but it's good practice for
+      // post-resolve callbacks.
+      aRuleData->mCanStoreInRuleTree = PR_FALSE;
+
+      return NS_OK;
+    }
+  }
+
+  return NS_OK;
+}
+
+#ifdef DEBUG
+NS_IMETHODIMP
+CoverTransitionStartStyleRule::List(FILE* out, PRInt32 aIndent) const
+{
+  // WRITE ME?
+  return NS_OK;
+}
+#endif
+
+void
+CoverTransitionStartStyleRule::FillStyleStruct(void* aStyleStruct,
+                                               nsRuleData* aRuleData) const
+{
+  for (PRUint32 i = 0, i_end = mCoveredValues.Length(); i < i_end; ++i) {
+    const CoveredValue &cv = mCoveredValues[i];
+    if (aRuleData->mSIDs & nsCachedStyleData::GetBitForSID(
+                             nsCSSProps::kSIDTable[cv.mProperty]))
+    {
+#ifdef DEBUG
+      PRBool ok =
+#endif
+        nsStyleAnimation::StoreComputedValue(cv.mProperty,
+                                             aRuleData->mPresContext,
+                                             aStyleStruct, cv.mCoveredValue);
+      NS_ABORT_IF_FALSE(ok, "could not store computed value");
+    }
+  }
+}
+
+/*****************************************************************************
+ * nsTransitionManager                                                       *
+ *****************************************************************************/
+
+nsTransitionManager::nsTransitionManager(nsPresContext *aPresContext)
+  : mPresContext(aPresContext)
+{
+  PR_INIT_CLIST(&mElementTransitions);
+}
+
+nsTransitionManager::~nsTransitionManager()
+{
+  // Content nodes might outlive the transition manager.
+  while (!PR_CLIST_IS_EMPTY(&mElementTransitions)) {
+    ElementTransitions *head = static_cast<ElementTransitions*>(
+                                 PR_LIST_HEAD(&mElementTransitions));
+    head->Destroy();
+  }
+}
+
+already_AddRefed<nsIStyleRule>
+nsTransitionManager::StyleContextChanged(nsIContent *aElement,
+                                         nsStyleContext *aOldStyleContext,
+                                         nsStyleContext *aNewStyleContext)
+{
+  NS_PRECONDITION(aOldStyleContext->GetPseudoType() ==
+                      aNewStyleContext->GetPseudoType(),
+                  "pseudo type mismatch");
+  // If we were called from ReParentStyleContext, this assertion would
+  // actually fire.  If we need to be called from there, we can probably
+  // just remove it; the condition probably isn't critical, although
+  // it's worth thinking about some more.
+  NS_PRECONDITION(aOldStyleContext->HasPseudoElementData() ==
+                      aNewStyleContext->HasPseudoElementData(),
+                  "pseudo type mismatch");
+
+  // Return sooner (before the startedAny check below) for the most
+  // common case: no transitions specified.
+  const nsStyleDisplay *disp = aNewStyleContext->GetStyleDisplay();
+  if (disp->mTransitionPropertyCount == 1 &&
+      disp->mTransitions[0].GetDelay() == 0.0f &&
+      disp->mTransitions[0].GetDuration() == 0.0f) {
+    return nsnull;
+  }      
+
+
+  if (aNewStyleContext->PresContext()->IsProcessingAnimationStyleChange()) {
+    return nsnull;
+  }
+  
+  nsIAtom *pseudo = aNewStyleContext->GetPseudoType();
+  if (pseudo && (pseudo != nsCSSPseudoElements::before &&
+                 pseudo != nsCSSPseudoElements::after)) {
+    return nsnull;
+  }
+  if (aNewStyleContext->GetParent() &&
+      aNewStyleContext->GetParent()->HasPseudoElementData()) {
+    // Ignore transitions on things that inherit properties from
+    // pseudo-elements.
+    // FIXME: Add tests for this.
+    return nsnull;
+  }
+
+  // FIXME: When we have multiple continuations, we actually repeat this
+  // for each one, and if we have transitions we create separate cover
+  // rules for each one.  However, since we're attaching the transition
+  // data to the element, during the animation we create the same style
+  // rule, so it's not too horrible.
+
+  // Per http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html
+  // I'll consider only the transitions from the number of items in
+  // 'transition-property' on down, and later ones will override earlier
+  // ones (tracked using |whichStarted|).
+  PRBool startedAny = PR_FALSE;
+  nsCSSPropertySet whichStarted;
+  ElementTransitions *et = nsnull;
+  for (PRUint32 i = disp->mTransitionPropertyCount; i-- != 0; ) {
+    const nsTransition& t = disp->mTransitions[i];
+    // Check delay and duration first, since they default to zero, and
+    // when they're both zero, we can ignore the transition.
+    if (t.GetDelay() != 0.0f || t.GetDuration() != 0.0f) {
+      et = GetElementTransitions(aElement,
+                                 aNewStyleContext->GetPseudoType(),
+                                 PR_FALSE);
+
+      // We might have something to transition.  See if any of the
+      // properties in question changed and are animatable.
+      nsCSSProperty property = t.GetProperty();
+      if (property == eCSSPropertyExtra_no_properties ||
+          property == eCSSProperty_UNKNOWN) {
+        // Nothing to do, but need to exclude this from cases below.
+      } else if (property == eCSSPropertyExtra_all_properties) {
+        for (nsCSSProperty p = nsCSSProperty(0); 
+             p < eCSSProperty_COUNT_no_shorthands;
+             p = nsCSSProperty(p + 1)) {
+          ConsiderStartingTransition(p, t, aElement, et,
+                                     aOldStyleContext, aNewStyleContext,
+                                     &startedAny, &whichStarted);
+        }
+      } else if (nsCSSProps::IsShorthand(property)) {
+        CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(subprop, property) {
+          ConsiderStartingTransition(*subprop, t, aElement, et,
+                                     aOldStyleContext, aNewStyleContext,
+                                     &startedAny, &whichStarted);
+        }
+      } else {
+        ConsiderStartingTransition(property, t, aElement, et,
+                                   aOldStyleContext, aNewStyleContext,
+                                   &startedAny, &whichStarted);
+      }
+    }
+  }
+
+  if (!startedAny) {
+    return nsnull;
+  }
+
+  NS_ABORT_IF_FALSE(et, "must have element transitions if we started "
+                        "any transitions");
+
+  // In the CSS working group discussion (2009 Jul 15 telecon,
+  // http://www.w3.org/mid/4A5E1470.4030904@inkedblade.net ) of
+  // http://lists.w3.org/Archives/Public/www-style/2009Jun/0121.html ,
+  // the working group decided that a transition property on an
+  // element should not cause any transitions if the property change
+  // is itself inheriting a value that is transitioning on an
+  // ancestor.  So, to get the correct behavior, we continue the
+  // restyle that caused this transition using a "covering" rule that
+  // covers up any changes on which we started transitions, so that
+  // descendants don't start their own transitions.  (In the case of
+  // negative transition delay, this covering rule produces different
+  // results than applying the transition rule immediately would).
+  // Our caller is responsible for restyling again using this covering
+  // rule.
+
+  nsRefPtr<CoverTransitionStartStyleRule> coverRule =
+    new CoverTransitionStartStyleRule;
+  if (!coverRule) {
+    NS_WARNING("out of memory");
+    return nsnull;
+  }
+  
+  nsTArray<ElementPropertyTransition> &pts = et->mPropertyTransitions;
+  for (PRUint32 i = 0, i_end = pts.Length(); i < i_end; ++i) {
+    ElementPropertyTransition &pt = pts[i];
+    if (whichStarted.HasProperty(pt.mProperty)) {
+      coverRule->CoverValue(pt.mProperty, pt.mStartValue);
+    }
+  }
+
+  return already_AddRefed<nsIStyleRule>(
+           static_cast<nsIStyleRule*>(coverRule.forget().get()));
+}
+
+void
+nsTransitionManager::ConsiderStartingTransition(nsCSSProperty aProperty,
+                       const nsTransition& aTransition,
+                       nsIContent *aElement,
+                       ElementTransitions *&aElementTransitions,
+                       nsStyleContext *aOldStyleContext,
+                       nsStyleContext *aNewStyleContext,
+                       PRBool *aStartedAny,
+                       nsCSSPropertySet *aWhichStarted)
+{
+  // IsShorthand itself will assert if aProperty is not a property.
+  NS_ABORT_IF_FALSE(!nsCSSProps::IsShorthand(aProperty),
+                    "property out of range");
+
+  if (aWhichStarted->HasProperty(aProperty)) {
+    // A later item in transition-property already started a
+    // transition for this property, so we ignore this one.
+    // See comment above and
+    // http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html .
+    return;
+  }
+
+  if (nsCSSProps::kAnimTypeTable[aProperty] == eStyleAnimType_None) {
+    return;
+  }
+
+  ElementPropertyTransition pt;
+  nsStyleCoord dummyValue;
+  // FIXME: This call on the old style context gets incorrect style data
+  // since we don't quite enforce style rule immutability:  we didn't
+  // need to worry about callers calling GetStyleData rather than
+  // PeekStyleData after a style rule becomes "old" before transitions
+  // existed.
+  PRBool shouldAnimate =
+    nsStyleAnimation::ExtractComputedValue(aProperty, aOldStyleContext,
+                                           pt.mStartValue) &&
+    nsStyleAnimation::ExtractComputedValue(aProperty, aNewStyleContext,
+                                           pt.mEndValue) &&
+    pt.mStartValue != pt.mEndValue &&
+    // Check that we can interpolate between these values
+    // (If this is ever a performance problem, we could add a
+    // CanInterpolate method, but it seems fine for now.)
+    nsStyleAnimation::Interpolate(pt.mStartValue, pt.mEndValue, 0.5,
+                                  dummyValue);
+
+  PRUint32 currentIndex = nsTArray<ElementPropertyTransition>::NoIndex;
+  if (aElementTransitions) {
+    nsTArray<ElementPropertyTransition> &pts =
+      aElementTransitions->mPropertyTransitions;
+    for (PRUint32 i = 0, i_end = pts.Length(); i < i_end; ++i) {
+      if (pts[i].mProperty == aProperty) {
+        currentIndex = i;
+        break;
+      }
+    }
+  }
+
+  nsPresContext *presContext = aNewStyleContext->PresContext();
+
+  if (!shouldAnimate) {
+    if (currentIndex != nsTArray<ElementPropertyTransition>::NoIndex) {
+      // We're in the middle of a transition, but just got a
+      // non-transition style change changing to exactly the
+      // current in-progress value.   (This is quite easy to cause
+      // using 'transition-delay'.)
+      nsTArray<ElementPropertyTransition> &pts =
+        aElementTransitions->mPropertyTransitions;
+      pts.RemoveElementAt(currentIndex);
+      if (pts.IsEmpty()) {
+        aElementTransitions->Destroy();
+        // |aElementTransitions| is now a dangling pointer!
+        aElementTransitions = nsnull;
+      }
+      presContext->PresShell()->RestyleForAnimation(aElement);
+    }
+    return;
+  }
+
+  // When we interrupt a running transition, we want to reduce the
+  // duration of the new transition *if* the new transition would have
+  // been longer had it started from the endpoint of the currently
+  // running transition.
+  double durationFraction = 1.0;
+
+  // We need to check two things if we have a currently running
+  // transition for this property:  see durationFraction comment above
+  // and the endpoint check below.
+  if (currentIndex != nsTArray<ElementPropertyTransition>::NoIndex) {
+    const nsStyleCoord &endVal =
+      aElementTransitions->mPropertyTransitions[currentIndex].mEndValue;
+
+    if (endVal == pt.mEndValue) {
+      // If we got a style change that changed the value to the endpoint
+      // of the currently running transition, we don't want to interrupt
+      // its timing function.
+      // But don't forget to restyle with animation so we show the
+      // current transition.
+      presContext->PresShell()->RestyleForAnimation(aElement);
+      return;
+    }
+
+    double fullDistance, remainingDistance;
+#ifdef DEBUG
+    PRBool ok =
+#endif
+      nsStyleAnimation::ComputeDistance(pt.mStartValue, pt.mEndValue,
+                                        fullDistance);
+    NS_ABORT_IF_FALSE(ok, "could not compute distance");
+    NS_ABORT_IF_FALSE(fullDistance >= 0.0, "distance must be positive");
+
+    if (nsStyleAnimation::ComputeDistance(endVal, pt.mEndValue,
+                                          remainingDistance)) {
+      NS_ABORT_IF_FALSE(remainingDistance >= 0.0, "distance must be positive");
+      durationFraction = fullDistance / remainingDistance;
+      if (durationFraction > 1.0) {
+        durationFraction = 1.0;
+      }
+    }
+  }
+
+
+  nsRefreshDriver *rd = presContext->RefreshDriver();
+
+  pt.mProperty = aProperty;
+  float delay = aTransition.GetDelay();
+  float duration = aTransition.GetDuration();
+  if (durationFraction != 1.0) {
+    // Negative delays are essentially part of the transition
+    // function, so reduce them along with the duration, but don't
+    // reduce positive delays.  (See comment above about
+    // durationFraction.)
+    if (delay < 0.0f)
+        delay *= durationFraction;
+    duration *= durationFraction;
+  }
+  pt.mStartTime = rd->MostRecentRefresh() +
+                  TimeDuration::FromMilliseconds(delay);
+  pt.mDuration = TimeDuration::FromMilliseconds(duration);
+  const nsTimingFunction &tf = aTransition.GetTimingFunction();
+  pt.mTimingFunction.Init(tf.mX1, tf.mY1, tf.mX2, tf.mY2);
+
+  if (!aElementTransitions) {
+    aElementTransitions =
+      GetElementTransitions(aElement, aNewStyleContext->GetPseudoType(),
+                            PR_TRUE);
+    if (!aElementTransitions) {
+      NS_WARNING("allocating ElementTransitions failed");
+      return;
+    }
+  }
+  
+  nsTArray<ElementPropertyTransition> &pts =
+    aElementTransitions->mPropertyTransitions;
+#ifdef DEBUG
+  for (PRUint32 i = 0, i_end = pts.Length(); i < i_end; ++i) {
+    NS_ABORT_IF_FALSE(i == currentIndex ||
+                      pts[i].mProperty != aProperty,
+                      "duplicate transitions for property");
+  }
+#endif
+  if (currentIndex != nsTArray<ElementPropertyTransition>::NoIndex) {
+    pts[currentIndex] = pt;
+  } else {
+    if (!pts.AppendElement(pt)) {
+      NS_WARNING("out of memory");
+      return;
+    }
+  }
+
+  presContext->PresShell()->RestyleForAnimation(aElement);
+
+  *aStartedAny = PR_TRUE;
+  aWhichStarted->AddProperty(aProperty);
+}
+
+ElementTransitions*
+nsTransitionManager::GetElementTransitions(nsIContent *aElement,
+                                           nsIAtom *aPseudo,
+                                           PRBool aCreateIfNeeded)
+{
+  nsIAtom *propName;
+  if (aPseudo == nsCSSPseudoElements::before) {
+    propName = nsGkAtoms::transitionsOfBeforeProperty;
+  } else if (aPseudo == nsCSSPseudoElements::after) {
+    propName = nsGkAtoms::transitionsOfAfterProperty;
+  } else {
+    NS_ASSERTION(!aPseudo || !aCreateIfNeeded,
+                 "should never try to create transitions for pseudo "
+                 "other than :before or :after");
+    propName = nsGkAtoms::transitionsProperty;
+  }
+  ElementTransitions *et = static_cast<ElementTransitions*>(
+                             aElement->GetProperty(propName));
+  if (!et && aCreateIfNeeded) {
+    // FIXME: Consider arena-allocating?
+    et = new ElementTransitions(aElement, propName, this);
+    if (!et) {
+      NS_WARNING("out of memory");
+      return nsnull;
+    }
+    nsresult rv = aElement->SetProperty(propName, et,
+                                        ElementTransitionsPropertyDtor, nsnull);
+    if (NS_FAILED(rv)) {
+      NS_WARNING("SetProperty failed");
+      delete et;
+      return nsnull;
+    }
+
+    AddElementTransitions(et);
+  }
+
+  return et;
+}
+
+void
+nsTransitionManager::AddElementTransitions(ElementTransitions* aElementTransitions)
+{
+  if (PR_CLIST_IS_EMPTY(&mElementTransitions)) {
+    // We need to observe the refresh driver.
+    nsRefreshDriver *rd = mPresContext->RefreshDriver();
+    rd->AddRefreshObserver(this, Flush_Style);
+  }
+
+  PR_INSERT_BEFORE(aElementTransitions, &mElementTransitions);
+}
+
+/*
+ * nsISupports implementation
+ */
+
+NS_IMPL_ADDREF_USING_AGGREGATOR(nsTransitionManager, mPresContext)
+NS_IMPL_RELEASE_USING_AGGREGATOR(nsTransitionManager, mPresContext)
+NS_IMPL_QUERY_INTERFACE1(nsTransitionManager, nsIStyleRuleProcessor)
+
+/*
+ * nsIStyleRuleProcessor implementation
+ */
+
+nsresult
+nsTransitionManager::WalkTransitionRule(RuleProcessorData* aData,
+                                        nsIAtom *aPseudo)
+{
+  if (!aData->mPresContext->IsProcessingAnimationStyleChange()) {
+    // If we're processing a normal style change rather than one from
+    // animation, don't add the transition rule.  This allows us to
+    // compute the new style value rather than having the transition
+    // override it, so that we can start transitioning differently.
+
+    // In most cases, we need to immediately restyle with animation
+    // after doing this.  However, ConsiderStartingTransition takes care
+    // of that for us.
+    return NS_OK;
+  }
+
+  ElementTransitions *et =
+    GetElementTransitions(aData->mContent, aPseudo, PR_FALSE);
+  if (!et) {
+    return NS_OK;
+  }
+
+  if (!et->EnsureStyleRuleFor(
+        aData->mPresContext->RefreshDriver()->MostRecentRefresh())) {
+    return NS_ERROR_OUT_OF_MEMORY;
+  }
+
+  aData->mRuleWalker->Forward(et->mStyleRule);
+
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTransitionManager::RulesMatching(ElementRuleProcessorData* aData)
+{
+  NS_ABORT_IF_FALSE(aData->mPresContext == mPresContext,
+                    "pres context mismatch");
+  return WalkTransitionRule(aData, nsnull);
+}
+
+NS_IMETHODIMP
+nsTransitionManager::RulesMatching(PseudoRuleProcessorData* aData)
+{
+  NS_ABORT_IF_FALSE(aData->mPresContext == mPresContext,
+                    "pres context mismatch");
+  // Note:  If we're the only thing keeping a pseudo-element frame alive
+  // (per ProbePseudoStyleContext), we still want to keep it alive, so
+  // this is ok.
+  return WalkTransitionRule(aData, aData->mPseudoTag);
+}
+
+NS_IMETHODIMP
+nsTransitionManager::HasStateDependentStyle(StateRuleProcessorData* aData,
+                                            nsReStyleHint* aResult)
+{
+  *aResult = nsReStyleHint(0);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTransitionManager::HasAttributeDependentStyle(AttributeRuleProcessorData* aData,
+                                                nsReStyleHint* aResult)
+{
+  *aResult = nsReStyleHint(0);
+  return NS_OK;
+}
+
+NS_IMETHODIMP
+nsTransitionManager::MediumFeaturesChanged(nsPresContext* aPresContext,
+                                           PRBool* aRulesChanged)
+{
+  *aRulesChanged = PR_FALSE;
+  return NS_OK;
+}
+
+/* virtual */ void
+nsTransitionManager::WillRefresh(mozilla::TimeStamp aTime)
+{
+  // Trim transitions that have completed, and post restyle events for
+  // frames that are still transitioning.
+  {
+    PRCList *next = PR_LIST_HEAD(&mElementTransitions);
+    while (next != &mElementTransitions) {
+      ElementTransitions *et = static_cast<ElementTransitions*>(next);
+      next = PR_NEXT_LINK(next);
+
+      NS_ABORT_IF_FALSE(et->mElement->GetCurrentDoc() ==
+                          mPresContext->Document(),
+                        "nsGenericElement::UnbindFromTree should have "
+                        "destroyed the element transitions object");
+
+      PRUint32 i = et->mPropertyTransitions.Length();
+      NS_ABORT_IF_FALSE(i != 0, "empty transitions list?");
+      do {
+        --i;
+        ElementPropertyTransition &pt = et->mPropertyTransitions[i];
+        if (pt.mStartTime + pt.mDuration <= aTime) {
+          // This transition has completed.
+          et->mPropertyTransitions.RemoveElementAt(i);
+        }
+      } while (i != 0);
+
+      // We need to restyle even if the transition rule no longer
+      // applies (in which case we just made it not apply).
+      mPresContext->PresShell()->RestyleForAnimation(et->mElement);
+
+      if (et->mPropertyTransitions.IsEmpty()) {
+        et->Destroy();
+        // |et| is now a dangling pointer!
+        et = nsnull;
+      }
+    }
+  }
+
+  // We might have removed transitions above.
+  TransitionsRemoved();
+}
+
+void
+nsTransitionManager::TransitionsRemoved()
+{
+  // If we have no transitions left, remove ourselves from the refresh
+  // driver.
+  if (PR_CLIST_IS_EMPTY(&mElementTransitions)) {
+    mPresContext->RefreshDriver()->RemoveRefreshObserver(this, Flush_Style);
+  }
+}
new file mode 100644
--- /dev/null
+++ b/layout/style/nsTransitionManager.h
@@ -0,0 +1,121 @@
+/* vim: set shiftwidth=2 tabstop=8 autoindent cindent expandtab: */
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is nsTransitionManager.
+ *
+ * The Initial Developer of the Original Code is the Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2009
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   L. David Baron <dbaron@dbaron.org>, Mozilla Corporation (original author)
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/* Code to start and animate CSS transitions. */
+
+#ifndef nsTransitionManager_h_
+#define nsTransitionManager_h_
+
+#include "prclist.h"
+#include "nsCSSProperty.h"
+#include "nsIStyleRuleProcessor.h"
+#include "nsRefreshDriver.h"
+
+class nsStyleContext;
+class nsPresContext;
+class nsCSSPropertySet;
+struct nsTransition;
+struct ElementTransitions;
+
+/**
+ * Must be created only as a sub-object of an nsPresContext (since its
+ * reference counting methods assume that).
+ */
+class nsTransitionManager : public nsIStyleRuleProcessor,
+                            public nsARefreshObserver {
+public:
+  nsTransitionManager(nsPresContext *aPresContext);
+  ~nsTransitionManager();
+
+  /**
+   * StyleContextChanged 
+   *
+   * To be called from nsFrameManager::ReResolveStyleContext when the
+   * style of an element has changed, to initiate transitions from that
+   * style change.
+   *
+   * It may return a "cover rule" (see CoverTransitionStartStyleRule) to
+   * cover up some of the changes for the duration of the restyling of
+   * descendants.  If it does, this function will take care of causing
+   * the necessary restyle afterwards, but the caller must restyle the
+   * element *again* with the original sequence of rules plus the
+   * returned cover rule as the most specific rule.
+   */
+  already_AddRefed<nsIStyleRule>
+    StyleContextChanged(nsIContent *aElement,
+                        nsStyleContext *aOldStyleContext,
+                        nsStyleContext *aNewStyleContext);
+
+  // nsISupports
+  NS_DECL_ISUPPORTS_INHERITED
+
+  // nsIStyleRuleProcessor
+  NS_IMETHOD RulesMatching(ElementRuleProcessorData* aData);
+  NS_IMETHOD RulesMatching(PseudoRuleProcessorData* aData);
+  NS_IMETHOD HasStateDependentStyle(StateRuleProcessorData* aData,
+                                    nsReStyleHint* aResult);
+  NS_IMETHOD HasAttributeDependentStyle(AttributeRuleProcessorData* aData,
+                                        nsReStyleHint* aResult);
+  NS_IMETHOD MediumFeaturesChanged(nsPresContext* aPresContext,
+                                   PRBool* aRulesChanged);
+
+  // nsARefreshObserver
+  virtual void WillRefresh(mozilla::TimeStamp aTime);
+
+private:
+  friend class ElementTransitions; // for TransitionsRemoved
+
+  void ConsiderStartingTransition(nsCSSProperty aProperty,
+                                  const nsTransition& aTransition,
+                                  nsIContent *aElement,
+                                  ElementTransitions *&aElementTransitions,
+                                  nsStyleContext *aOldStyleContext,
+                                  nsStyleContext *aNewStyleContext,
+                                  PRBool *aStartedAny,
+                                  nsCSSPropertySet *aWhichStarted);
+  ElementTransitions* GetElementTransitions(nsIContent *aElement,
+                                            nsIAtom *aPseudo,
+                                            PRBool aCreateIfNeeded);
+  void AddElementTransitions(ElementTransitions* aElementTransitions);
+  void TransitionsRemoved();
+  nsresult WalkTransitionRule(RuleProcessorData* aData, nsIAtom *aPseudo);
+
+  PRCList mElementTransitions;
+  nsPresContext *mPresContext;
+};
+
+#endif /* !defined(nsTransitionManager_h_) */
--- a/layout/style/test/Makefile.in
+++ b/layout/style/test/Makefile.in
@@ -131,16 +131,17 @@ GARBAGE += css_properties.js
 		test_rem_unit.html \
 		test_selectors.html \
 		test_selectors_on_anonymous_content.html \
 		test_shorthand_property_getters.html \
 		test_style_struct_copy_constructors.html \
 		test_system_font_serialization.html \
 		test_transitions_computed_values.html \
 		test_transitions_computed_value_combinations.html \
+		test_transitions.html \
 		test_units_angle.html \
 		test_units_frequency.html \
 		test_units_length.html \
 		test_units_time.html \
 		test_value_computation.html \
 		test_value_storage.html \
 		test_visited_pref.html \
 		css_properties.js \
new file mode 100644
--- /dev/null
+++ b/layout/style/test/test_transitions.html
@@ -0,0 +1,626 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=435441
+-->
+<head>
+  <title>Test for Bug 435441</title>
+  <script type="application/javascript" src="/MochiKit/packed.js"></script>
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+  <style type="text/css">
+
+  #display p { margin-top: 0; margin-bottom: 0; }
+
+  </style>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=435441">Mozilla Bug 435441</a>
+<div id="display">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 435441 **/
+
+function px_to_num(str)
+{
+    return Number(String(str).match(/^([\d.]+)px$/)[1]);
+}
+
+// Run tests simultaneously so we don't have to take up too much time.
+SimpleTest.waitForExplicitFinish();
+var gTestsRunning = 0;
+function TestStarted() { ++gTestsRunning; }
+function TestFinished() { if (--gTestsRunning == 0) SimpleTest.finish(); }
+
+// An array of arrays of functions to be called at the outer index number
+// of seconds after the present.
+var gFutureCalls = [];
+
+function add_future_call(index, func)
+{
+    if (!(index in gFutureCalls)) {
+        gFutureCalls[index] = [];
+    }
+    gFutureCalls[index].push(func);
+    TestStarted();
+}
+var gStartTime1, gStartTime2;
+var gCurrentTime;
+function process_future_calls(index)
+{
+    var calls = gFutureCalls[index];
+    if (!calls)
+        return;
+    gCurrentTime = Date.now();
+    for (var i = 0; i < calls.length; ++i) {
+        calls[i]();
+        TestFinished();
+    }
+}
+
+function bezier(x1, y1, x2, y2) {
+    // Cubic bezier with control points (0, 0), (x1, y1), (x2, y2), and (1, 1).
+    function x_for_t(t) {
+        var omt = 1-t;
+        return 3 * omt * omt * t * x1 + 3 * omt * t * t * x2 + t * t * t;
+    }
+    function y_for_t(t) {
+        var omt = 1-t;
+        return 3 * omt * omt * t * y1 + 3 * omt * t * t * y2 + t * t * t;
+    }
+    function t_for_x(x) {
+        // Binary subdivision.
+        var mint = 0, maxt = 1;
+        for (var i = 0; i < 30; ++i) {
+            var guesst = (mint + maxt) / 2;
+            var guessx = x_for_t(guesst);
+            if (x < guessx)
+                maxt = guesst;
+            else
+                mint = guesst;
+        }
+        return (mint + maxt) / 2;
+    }
+    return function bezier_closure(x) {
+        if (x == 0) return 0;
+        if (x == 1) return 1;
+        return y_for_t(t_for_x(x));
+    }
+}
+
+var timingFunctions = {
+  // a map from the value of 'transition-timing-function' to an array of
+  // the portions this function yields at 0 (always 0), 1/4, 1/2, and
+  // 3/4 and all (always 1) of the way through the time of the
+  // transition.  Each portion is represented as a value and an
+  // acceptable error tolerance (based on a time error of 1%) for that
+  // value.
+
+  // ease
+  "ease": bezier(0.25, 0.1, 0.25, 1),
+  "cubic-bezier(0.25, 0.1, 0.25, 1.0)": bezier(0.25, 0.1, 0.25, 1),
+
+  // linear and various synonyms for it
+  "linear": function(x) { return x; },
+  "cubic-bezier(0.0, 0.0, 1.0, 1.0)": function(x) { return x; },
+  "cubic-bezier(0, 0, 1, 1)": function(x) { return x; },
+  "cubic-bezier(0, 0, 0, 0.0)": function(x) { return x; },
+  "cubic-bezier(1.0, 1, 0, 0)": function(x) { return x; },
+
+  // ease-in
+  "ease-in": bezier(0.42, 0, 1, 1),
+  "cubic-bezier(0.42, 0, 1.0, 1.0)": bezier(0.42, 0, 1, 1),
+
+  // ease-out
+  "ease-out": bezier(0, 0, 0.58, 1),
+  "cubic-bezier(0, 0, 0.58, 1.0)": bezier(0, 0, 0.58, 1),
+
+  // ease-in-out
+  "ease-in-out": bezier(0.42, 0, 0.58, 1),
+  "cubic-bezier(0.42, 0, 0.58, 1.0)": bezier(0.42, 0, 0.58, 1),
+
+  // other cubic-bezier values
+  "cubic-bezier(0.4, 0.1, 0.7, 0.95)": bezier(0.4, 0.1, 0.7, 0.95),
+  "cubic-bezier(1, 0, 0, 1)": bezier(1, 0, 0, 1),
+  "cubic-bezier(0, 1, 1, 0)": bezier(0, 1, 1, 0),
+
+};
+
+var div = document.getElementById("display");
+
+// Set up all the elements on which we are going to start transitions.
+
+// Test all timing functions using a set of 8-second transitions, which
+// we check at times 0, 2s, 4s, 6s, and 8s.
+var tftests = [];
+for (var tf in timingFunctions) {
+    var p = document.createElement("p");
+    var t = document.createTextNode("transition-timing-function: " + tf);
+    p.appendChild(t);
+    p.style.textIndent = "0px";
+    p.style.MozTransition = "8s text-indent linear";
+    p.style.MozTransitionTimingFunction = tf;
+    div.appendChild(p);
+    is(getComputedStyle(p, "").textIndent, "0px",
+       "should be zero before changing value");
+    tftests.push([ p, tf ]);
+}
+
+// Check that the timing function continues even when we restyle in the
+// middle.
+var interrupt_tests = [];
+for (var itime = 2; itime < 8; itime += 2) {
+    var p = document.createElement("p");
+    var t = document.createTextNode("interrupt at " + itime + "s");
+    p.appendChild(t);
+    p.style.textIndent = "0px";
+    p.style.MozTransition = "8s text-indent cubic-bezier(0, 1, 1, 0)";
+    div.appendChild(p);
+    is(getComputedStyle(p, "").textIndent, "0px",
+       "should be zero before changing value");
+    interrupt_tests.push([ p, itime ]);
+    
+}
+setTimeout("interrupt_tests[0][0].style.color = 'blue';" +
+           "check_interrupt_tests()", 2000);
+setTimeout("interrupt_tests[1][0].style.color = 'blue';" +
+           "check_interrupt_tests()", 4000);
+setTimeout("interrupt_tests[2][0].style.color = 'blue';" +
+           "check_interrupt_tests()", 6000);
+
+// Test transition-delay values of -4s through 4s on a 4s transition
+// with 'ease-out' timing function.
+var delay_tests = {};
+for (var d = -4; d <= 4; ++d) {
+    var p = document.createElement("p");
+    var delay = d + "s";
+    var t = document.createTextNode("transition-delay: " + delay);
+    p.appendChild(t);
+    p.style.marginLeft = "0px";
+    p.style.MozTransition = "4s margin-left ease-out " + delay;
+    div.appendChild(p);
+    is(getComputedStyle(p, "").marginLeft, "0px",
+       "should be zero before changing value");
+    delay_tests[d] = p;
+}
+
+// Test that changing the value on an already-running transition to the
+// value it currently happens to have resets the transition.
+var p = document.createElement("p");
+var t = document.createTextNode("transition-delay reset to starting point");
+p.appendChild(t);
+p.style.marginLeft = "0px";
+p.style.MozTransition = "4s margin-left ease-out 4s";
+div.appendChild(p);
+is(getComputedStyle(p, "").marginLeft, "0px",
+   "should be zero before changing value");
+var reset_test = p;
+
+// Test that transitions on descendants do not trigger when the
+// inherited value is itself transitioning.  In other words, when
+// ancestor and descendant both have a transition for the same property,
+// and the descendant inherits the property from the ancestor, the
+// descendant's transition is ignored (as part of the idea of not
+// starting transitions on changes that result from animation).
+// See http://lists.w3.org/Archives/Public/www-style/2009Jun/0121.html
+// and http://lists.w3.org/Archives/Public/www-style/2009Jul/0050.html
+var descendant_tests = [
+    { parent_transition: "",
+      child_transition: "4s text-indent" },
+    { parent_transition: "4s text-indent",
+      child_transition: "" },
+    { parent_transition: "4s text-indent",
+      child_transition: "16s text-indent" },
+    { parent_transition: "4s text-indent",
+      child_transition: "1s text-indent" },
+    { parent_transition: "8s letter-spacing",
+      child_transition: "4s text-indent" },
+    { parent_transition: "4s text-indent",
+      child_transition: "8s letter-spacing" },
+    { parent_transition: "4s text-indent",
+      child_transition: "8s all" },
+    { parent_transition: "8s text-indent",
+      child_transition: "4s all" },
+    // examples with positive and negative delay
+    { parent_transition: "4s text-indent 1s",
+      child_transition: "8s text-indent" },
+    { parent_transition: "4s text-indent -1s",
+      child_transition: "8s text-indent" }
+];
+
+for (var i in descendant_tests) {
+    var test = descendant_tests[i];
+    test.parentNode = document.createElement("div");
+    test.childNode = document.createElement("p");
+    test.parentNode.appendChild(test.childNode);
+    test.childNode.appendChild(document.createTextNode(
+        "parent with \"" + test.parent_transition + "\" and " +
+        "child with \"" + test.child_transition + "\""));
+    test.parentNode.style.MozTransition = test.parent_transition;
+    test.childNode.style.MozTransition = test.child_transition;
+    test.parentNode.style.textIndent = "50px"; // transition from 50 to 150
+    test.parentNode.style.letterSpacing = "10px"; // transition from 10 to 5
+    div.appendChild(test.parentNode);
+    var parentCS = getComputedStyle(test.parentNode, "");
+    var childCS = getComputedStyle(test.childNode, "");
+    is(parentCS.textIndent, "50px",
+       "parent text-indent should be 50px before changing");
+    is(parentCS.letterSpacing, "10px",
+       "parent letter-spacing should be 10px before changing");
+    is(childCS.textIndent, "50px",
+       "child text-indent should be 50px before changing");
+    is(childCS.letterSpacing, "10px",
+       "child letter-spacing should be 10px before changing");
+    test.childCS = childCS;
+}
+
+// For all of these transitions, the transition for margin-left should
+// have a duration of 8s, and the default timing function (ease) and
+// delay (0).
+// This is because we're implementing the proposal in
+// http://lists.w3.org/Archives/Public/www-style/2009Aug/0109.html
+var number_tests = [
+  { style: "-moz-transition: 4s margin, 8s margin-left" },
+  { style: "-moz-transition: 4s margin-left, 8s margin" },
+  { style: "-moz-transition-property: margin-left; " +
+             "-moz-transition-duration: 8s, 2s" },
+  { style: "-moz-transition-property: margin-left, margin-left; " + 
+             "-moz-transition-duration: 2s, 8s" },
+  { style: "-moz-transition-property: margin-left, margin-left, margin-left; " +
+             "-moz-transition-duration: 8s, 2s" },
+  { style: "-moz-transition-property: margin-left; " +
+             "-moz-transition-duration: 8s, 16s" },
+  { style: "-moz-transition-property: margin-left, margin-left; " + 
+             "-moz-transition-duration: 16s, 8s" },
+  { style: "-moz-transition-property: margin-left, margin-left, margin-left; " +
+             "-moz-transition-duration: 8s, 16s" },
+  { style: "-moz-transition-property: text-indent,word-spacing,margin-left; " +
+             "-moz-transition-duration: 8s; " +
+             "-moz-transition-delay: 0, 8s" },
+  { style: "-moz-transition-property: text-indent,word-spacing,margin-left; " +
+             "-moz-transition-duration: 8s, 16s; " +
+             "-moz-transition-delay: 8s, 8s, 0, 8s, 8s, 8s" },
+];
+
+for (var i in number_tests) {
+    var test = number_tests[i];
+    var p = document.createElement("p");
+    p.setAttribute("style", test.style);
+    var t = document.createTextNode(test.style);
+    p.appendChild(t);
+    p.style.marginLeft = "100px";
+    div.appendChild(p);
+    is(getComputedStyle(p, "").marginLeft, "100px",
+       "should be 100px before changing value");
+    test.node = p;
+}
+
+// Test transitions that are also from-display:none, to-display:none, and
+// display:none throughout.
+var from_none_test, to_none_test, always_none_test;
+function make_display_test(initially_none, text)
+{
+    var p = document.createElement("p");
+    p.appendChild(document.createTextNode(text));
+    p.style.textIndent = "0px";
+    p.style.MozTransition = "8s text-indent ease-in-out";
+    if (initially_none)
+        p.style.display = "none";
+    div.appendChild(p);
+    return p;
+}
+from_none_test   = make_display_test(true,  "transition from display:none");
+to_none_test     = make_display_test(false, "transition to display:none");
+always_none_test = make_display_test(true,  "transition always display:none");
+var display_tests = [ from_none_test, to_none_test, always_none_test ];
+
+// FIXME: Test a transition that reverses partway through.
+
+// flush style changes
+var x = getComputedStyle(div, "").color;
+
+// Start our timer as close as possible to when we start the first
+// transition.
+// Do not use setInterval because once it gets off in time, it stays off.
+for (var i = 1; i <= 8; ++i) {
+    setTimeout(process_future_calls, i * 1000, i);
+}
+gStartTime1 = Date.now(); // set before any transitions have started
+
+// Start all the transitions.
+for (var test in tftests) {
+    var p = tftests[test][0];
+    p.style.textIndent = "100px";
+}
+for (var test in interrupt_tests) {
+    var p = interrupt_tests[test][0];
+    p.style.textIndent = "100px";
+}
+for (var d in delay_tests) {
+    var p = delay_tests[d];
+    p.style.marginLeft = "100px";
+}
+reset_test.style.marginLeft = "100px";
+for (var i in descendant_tests) {
+    var test = descendant_tests[i];
+    test.parentNode.style.textIndent = "150px";
+    test.parentNode.style.letterSpacing = "5px";
+}
+for (var i in number_tests) {
+    var test = number_tests[i];
+    test.node.style.marginLeft = "50px";
+}
+from_none_test.style.textIndent = "100px";
+from_none_test.style.display = "";
+to_none_test.style.textIndent = "100px";
+to_none_test.style.display = "none";
+always_none_test.style.textIndent = "100px";
+
+// flush style changes
+x = getComputedStyle(div, "").color;
+
+gStartTime2 = Date.now(); // set after all transitions have started
+gCurrentTime = gStartTime2;
+
+/**
+ * Assert that a transition whose timing function yields the bezier
+ * |func|, running from |start_time| to |end_time| (both in seconds
+ * relative to when the transitions were started) should have produced
+ * computed value |cval| given that the transition was from
+ * |start_value| to |end_value| (both numbers in CSS pixels).
+ */
+function check_transition_value(func, start_time, end_time,
+                                start_value, end_value, cval, desc,
+                                xfail)
+{
+    function value_at(elapsed, error_portion) {
+        var time_portion = (elapsed - start_time) / (end_time - start_time);
+        if (time_portion < 0)
+            time_portion = 0;
+        else if (time_portion > 1)
+            time_portion = 1;
+        var value_portion = func(time_portion) + error_portion;
+        if (value_portion < 0)
+            value_portion = 0;
+        else if (value_portion > 1)
+            value_portion = 1;
+        return (1 - value_portion) * start_value + value_portion * end_value;
+    }
+
+    var time_range; // in seconds
+    var uns_range; // |range| before being sorted (so errors give it
+                   // in the original order
+    if (gCurrentTime == gStartTime2) {
+        // No timers involved
+        time_range = [0, 0];
+        if (start_time < 0) {
+            uns_range = [ value_at(0, -0.01), value_at(0, 0.01) ];
+        } else {
+            var val = value_at(0, 0);
+            uns_range = [val, val];
+        }
+    } else {
+        // seconds
+        // FIXME: Why do we need so much tolerance at the low end of the
+        // range (primarily for Mac)?
+        time_range = [ (gCurrentTime - gStartTime2 - 40) / 1000,
+                       (Date.now() - gStartTime1 + 20) / 1000 ];
+        uns_range = [ value_at(time_range[0], -0.01),
+                      value_at(time_range[1], 0.01) ];
+                    
+    }
+    var range = uns_range.concat(). /* concat to clone array */
+                  sort(function compareNumbers(a,b) { return a - b; });
+    var actual = px_to_num(cval);
+
+    var fn = xfail ? todo : ok;
+
+    fn(range[0] <= actual && actual <= range[1],
+       desc + ": computed value " + cval + " should be between " +
+       uns_range[0].toFixed(6) + "px and " + uns_range[1].toFixed(6) +
+       "px at time between " + time_range[0] + "s and " + time_range[1] + "s.");
+}
+
+function check_tf_test()
+{
+    for (var test in tftests) {
+        var p = tftests[test][0];
+        var tf = tftests[test][1];
+
+        check_transition_value(timingFunctions[tf], 0, 8, 0, 100,
+                               getComputedStyle(p, "").textIndent,
+                               "timing function test for timing function " + tf);
+
+    }
+
+    check_interrupt_tests();
+}
+
+check_tf_test();
+add_future_call(2, check_tf_test);
+add_future_call(4, check_tf_test);
+add_future_call(6, check_tf_test);
+add_future_call(8, check_tf_test);
+
+function check_interrupt_tests()
+{
+    for (var test in interrupt_tests) {
+        var p = interrupt_tests[test][0];
+        var itime = interrupt_tests[test][1];
+
+        check_transition_value(timingFunctions["cubic-bezier(0, 1, 1, 0)"],
+                               0, 8, 0, 100,
+                               getComputedStyle(p, "").textIndent,
+                               "interrupt test for time " + itime + "s");
+    }
+}
+
+// check_interrupt_tests is called from check_tf_test and from
+// where we reset the interrupts
+
+function check_delay_test(time)
+{
+    var tf = timingFunctions["ease-out"];
+    for (var d in delay_tests) {
+        var p = delay_tests[d];
+
+        check_transition_value(tf, Number(d), Number(d) + 4, 0, 100,
+                               getComputedStyle(p, "").marginLeft,
+                               "delay test for delay " + d + "s");
+    }
+}
+
+check_delay_test(0);
+for (var i = 1; i <= 8; ++i) {
+    add_future_call(i, check_delay_test);
+}
+
+function reset_reset_test(time)
+{
+    reset_test.style.marginLeft = "0px";
+}
+function check_reset_test(time)
+{
+    is(getComputedStyle(reset_test, "").marginLeft, "0px",
+       "reset test value at time " + time + "s.");
+}
+setTimeout(reset_reset_test, 1000); // must always run, even if late
+check_reset_test(0);
+for (var i = 1; i <= 8; ++i) {
+    (function(j) {
+        add_future_call(j, function() { check_reset_test(j); });
+    })(i);
+}
+
+check_descendant_tests();
+add_future_call(2, check_descendant_tests);
+add_future_call(6, check_descendant_tests);
+
+function check_descendant_tests() {
+    // text-indent: transition from 50px to 150px
+    // letter-spacing: transition from 10px to 5px
+    var values = {};
+    values["text-indent"] = [ 50, 150 ];
+    values["letter-spacing"] = [ 10, 5 ];
+    var tf = timingFunctions["ease"];
+
+    for (var i in descendant_tests) {
+        var test = descendant_tests[i];
+
+        /* ti=text-indent, ls=letter-spacing */
+        var child_ti_duration = 0;
+        var child_ls_duration = 0;
+        var child_ti_delay = 0;
+        var child_ls_delay = 0;
+
+        if (test.parent_transition != "") {
+            var props = test.parent_transition.split(" ");
+            var duration = parseInt(props[0]);
+            var delay = (props.length > 2) ? parseInt(props[2]) : 0;
+            var property = props[1];
+            if (property == "text-indent") {
+                child_ti_duration = duration;
+                child_ti_delay = delay;
+            } else if (property == "letter-spacing") {
+                child_ls_duration = duration;
+                child_ls_delay = delay;
+            } else {
+                ok(false, "fix this test (unexpected transition-property " +
+                          property + " on parent)");
+            }
+        }
+
+        if (test.child_transition != "") {
+            var props = test.child_transition.split(" ");
+            var duration = parseInt(props[0]);
+            var delay = (props.length > 2) ? parseInt(props[2]) : 0;
+            var property = props[1];
+            if (property != "text-indent" && property != "letter-spacing" &&
+                property != "all") {
+                ok(false, "fix this test (unexpected transition-property " +
+                          property + " on child)");
+            }
+
+            if (property != "letter-spacing" && child_ti_duration == 0) {
+                child_ti_duration = duration;
+                child_ti_delay = delay;
+            }
+            if (property != "text-indent" && child_ls_duration == 0) {
+                child_ls_duration = duration;
+                child_ls_delay = delay;
+            }
+        }
+
+        var time_portions = {
+          "text-indent":
+            { duration: child_ti_duration, delay: child_ti_delay },
+          "letter-spacing":
+            { duration: child_ls_duration, delay: child_ls_delay },
+        };
+
+        for (var prop in {"text-indent": true, "letter-spacing": true}) {
+            var time_portion = time_portions[prop];
+
+            if (time_portion.duration == 0) {
+                time_portion.duration = 0.01;
+                time_portion.delay = -1;
+            }
+
+            check_transition_value(tf, time_portion.delay,
+                                   time_portion.delay + time_portion.duration,
+                                   values[prop][0], values[prop][1],
+                                   test.childCS.getPropertyValue(prop),
+                                   "descendant test, property " + prop);
+        }
+    }
+}
+
+function check_number_tests()
+{
+    var tf = timingFunctions["ease"];
+    for (var d in number_tests) {
+        var test = number_tests[d];
+        var p = test.node;
+
+        check_transition_value(tf, 0, 8, 100, 50,
+                               getComputedStyle(p, "").marginLeft,
+                               "number of transitions test for style " +
+                                 test.style);
+    }
+}
+
+check_number_tests(0);
+add_future_call(2, check_number_tests);
+add_future_call(4, check_number_tests);
+add_future_call(6, check_number_tests);
+add_future_call(8, check_number_tests);
+
+function check_display_tests(time)
+{
+    var tf = timingFunctions["ease-in-out"];
+    for (var i in display_tests) {
+        var p = display_tests[i];
+
+        check_transition_value(tf, 0, 8, 0, 100,
+                               getComputedStyle(p, "").textIndent,
+                               "display test for test with " +
+                                 p.childNodes[0].data,
+        // TODO: Making transitions work on 'display:none' elements is
+        // still not implemented.
+                               (time != 8));
+    }
+}
+
+check_display_tests(0);
+add_future_call(2, function() { check_display_tests(2); });
+add_future_call(4, function() { check_display_tests(4); });
+add_future_call(6, function() { check_display_tests(6); });
+add_future_call(8, function() { check_display_tests(8); });
+
+</script>
+</pre>
+</body>
+</html>
--- a/xpcom/ds/TimeStamp.h
+++ b/xpcom/ds/TimeStamp.h
@@ -206,16 +206,28 @@ public:
     NS_ASSERTION(!aOther.IsNull(), "Cannot compute with aOther null value");
     return mValue >= aOther.mValue;
   }
   PRBool operator>(const TimeStamp& aOther) const {
     NS_ASSERTION(!IsNull(), "Cannot compute with a null value");
     NS_ASSERTION(!aOther.IsNull(), "Cannot compute with aOther null value");
     return mValue > aOther.mValue;
   }
+  PRBool operator==(const TimeStamp& aOther) const {
+    // Maybe it's ok to check == with null timestamps?
+    NS_ASSERTION(!IsNull(), "Cannot compute with a null value");
+    NS_ASSERTION(!aOther.IsNull(), "Cannot compute with aOther null value");
+    return mValue == aOther.mValue;
+  }
+  PRBool operator!=(const TimeStamp& aOther) const {
+    // Maybe it's ok to check != with null timestamps?
+    NS_ASSERTION(!IsNull(), "Cannot compute with a null value");
+    NS_ASSERTION(!aOther.IsNull(), "Cannot compute with aOther null value");
+    return mValue != aOther.mValue;
+  }
 
   // Comparing TimeStamps for equality should be discouraged. Adding
   // two TimeStamps, or scaling TimeStamps, is nonsense and must never
   // be allowed.
 
   static NS_HIDDEN_(nsresult) Startup();
   static NS_HIDDEN_(void) Shutdown();