Bug 1210796 - Part 1a: Add GetUnanimatedComputedStyle into nsIDOMWindowUtils to use in animationinspector of devtools. r=birtles,r=heycam draft
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Tue, 18 Apr 2017 12:15:47 +0900
changeset 564114 2df4cd749dc9ee28e11c17e11d0883a5d50d88b5
parent 564113 475794a9ca872990485b4309749853547288feb7
child 564115 719249dd0df6d4f7671bdb0464ae0c8b426d86c0
push id54524
push userbmo:dakatsuka@mozilla.com
push dateTue, 18 Apr 2017 09:24:06 +0000
reviewersbirtles, heycam
bugs1210796
milestone55.0a1
Bug 1210796 - Part 1a: Add GetUnanimatedComputedStyle into nsIDOMWindowUtils to use in animationinspector of devtools. r=birtles,r=heycam In this patch, we implement nsIDOMWindowUtils::GetUnanimatedComputedStyle which returns computed value of given CSS property without animation rule. This method is used from the DevTools animation inspector to fill in keyframe values when the property value is null (indicating that the underlying/base value is being used). In order to implement this, we extend nsComputedDOMStyle constructor to fetch the computed style minus animation style (i.e. the base style). This is somewhat complicated by the fact that for discrete animation. StyleAnimationValue::ExtractComputedValue may return ‘unset’, ‘initial’ or ‘inherit’. For example, if the author uses the 'unset' 'initial' or 'inherit' keyword for a discrete property (e.g. 'align-content’), ExtractComputedValue returns the keywords as-is. Furthermore, if the user does not set any specific keyword, ExtractComputedValue returns ‘unset’. We use this new nsComputedDOMStyle mechanism to resolve these keywords into a valid keyword for computed style in the same way as other properties (e.g. ‘opacity’). MozReview-Commit-ID: HffJ9SCDf2k
dom/base/nsDOMWindowUtils.cpp
dom/base/test/test_domwindowutils.html
dom/interfaces/base/nsIDOMWindowUtils.idl
layout/style/nsCSSPseudoElements.cpp
layout/style/nsCSSPseudoElements.h
layout/style/nsComputedDOMStyle.cpp
layout/style/nsComputedDOMStyle.h
--- a/dom/base/nsDOMWindowUtils.cpp
+++ b/dom/base/nsDOMWindowUtils.cpp
@@ -2777,16 +2777,82 @@ nsDOMWindowUtils::GetAnimationTypeForLon
       break;
     case eStyleAnimType_None:
       aResult.AssignLiteral("none");
       break;
   }
   return NS_OK;
 }
 
+NS_IMETHODIMP
+nsDOMWindowUtils::GetUnanimatedComputedStyle(nsIDOMElement* aElement,
+                                             const nsAString& aPseudoElement,
+                                             const nsAString& aProperty,
+                                             nsAString& aResult)
+{
+  nsCOMPtr<Element> element = do_QueryInterface(aElement);
+  if (!element) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  nsCSSPropertyID propertyID =
+    nsCSSProps::LookupProperty(aProperty, CSSEnabledState::eForAllContent);
+  if (propertyID == eCSSProperty_UNKNOWN ||
+      nsCSSProps::IsShorthand(propertyID)) {
+    return NS_ERROR_INVALID_ARG;
+  }
+
+  nsIPresShell* shell = GetPresShell();
+  if (!shell) {
+    return NS_ERROR_FAILURE;
+  }
+
+  nsIAtom* pseudo = nsCSSPseudoElements::GetPseudoAtom(aPseudoElement);
+  RefPtr<nsStyleContext> styleContext =
+    nsComputedDOMStyle::GetUnanimatedStyleContextNoFlush(element,
+                                                         pseudo, shell);
+
+  // We will support Servo in bug 1311257.
+  if (shell->StyleSet()->IsServo()) {
+    return NS_ERROR_NOT_IMPLEMENTED;
+  }
+
+  StyleAnimationValue computedValue;
+  if (!StyleAnimationValue::ExtractComputedValue(propertyID,
+                                                 styleContext, computedValue)) {
+    return NS_ERROR_FAILURE;
+  }
+
+  // Note: ExtractComputedValue can return 'unset', 'initial', or 'inherit' in
+  // its "computedValue" outparam, even though these technically aren't valid
+  // computed values. (It has this behavior for discretely-animatable
+  // properties, e.g. 'align-content', when these keywords are explicitly
+  // specified or when there is no specified value.)  But we need to return a
+  // valid computed value -- these keywords won't do.  So we fall back to
+  // nsComputedDOMStyle in this case.
+  if (computedValue.GetUnit() == StyleAnimationValue::eUnit_DiscreteCSSValue &&
+      (computedValue.GetCSSValueValue()->GetUnit() == eCSSUnit_Unset ||
+       computedValue.GetCSSValueValue()->GetUnit() == eCSSUnit_Initial ||
+       computedValue.GetCSSValueValue()->GetUnit() == eCSSUnit_Inherit)) {
+    RefPtr<nsComputedDOMStyle> computedStyle =
+      NS_NewComputedDOMStyle(
+       element, aPseudoElement, shell,
+       nsComputedDOMStyle::AnimationFlag::eWithoutAnimation);
+    computedStyle->GetPropertyValue(propertyID, aResult);
+    return NS_OK;
+  }
+
+  DebugOnly<bool> uncomputeResult =
+    StyleAnimationValue::UncomputeValue(propertyID,
+                                        Move(computedValue), aResult);
+  MOZ_ASSERT(uncomputeResult,
+             "Unable to get specified value from computed value");
+  return NS_OK;
+}
+
 nsresult
 nsDOMWindowUtils::RenderDocument(const nsRect& aRect,
                                  uint32_t aFlags,
                                  nscolor aBackgroundColor,
                                  gfxContext* aThebesContext)
 {
     nsCOMPtr<nsIDocument> doc = GetDocument();
     NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE);
--- a/dom/base/test/test_domwindowutils.html
+++ b/dom/base/test/test_domwindowutils.html
@@ -146,20 +146,140 @@ function test_getAnimationType() {
   SimpleTest.doesThrow(
     () => utils.getAnimationTypeForLonghand("invalid"),
     "NS_ERROR_ILLEGAL_VALUE",
     "Invalid property should throw");
 
   next();
 }
 
+function test_getUnanimatedComputedStyle() {
+  [
+    {
+      property: "opacity",
+      keyframes: [1, 0],
+      expectedInitialStyle: "1",
+      expectedDuringTransitionStyle: "0",
+      isDiscrete: false,
+    },
+    {
+      property: "clear",
+      keyframes: ["left", "inline-end"],
+      expectedInitialStyle: "none",
+      expectedDuringTransitionStyle: "inline-end",
+      isDiscrete: true,
+    },
+  ].forEach(testcase => {
+    const { property, keyframes, expectedInitialStyle,
+            expectedDuringTransitionStyle, isDiscrete } = testcase;
+
+    [null, "unset", "initial", "inherit"].forEach(initialStyle => {
+      const scriptAnimation = target => {
+        return target.animate({ [property]: keyframes }, 1000);
+      }
+      checkUnanimatedComputedStyle(property, initialStyle, null,
+                                   expectedInitialStyle, expectedInitialStyle,
+                                   scriptAnimation, "script animation");
+
+      const cssAnimationStyle = `@keyframes cssanimation {`
+                                + ` from { ${property}: ${ keyframes[0] }; }`
+                                + ` to { ${property}: ${ keyframes[1] }; } }`;
+      document.styleSheets[0].insertRule(cssAnimationStyle, 0);
+      const cssAnimation = target => {
+        target.style.animation = "cssanimation 1s";
+        return target.getAnimations()[0];
+      }
+      checkUnanimatedComputedStyle(property, initialStyle, null,
+                                   expectedInitialStyle, expectedInitialStyle,
+                                   cssAnimation, "CSS Animations");
+      document.styleSheets[0].deleteRule(0);
+
+      // We don't support discrete animations for CSS Transitions yet.
+      // (bug 1320854)
+      if (!isDiscrete) {
+        const cssTransition = target => {
+          target.style[property] = keyframes[0];
+          target.style.transition =
+            `${ property } 1s`;
+          window.getComputedStyle(target)[property];
+          target.style[property] = keyframes[1];
+          return target.getAnimations()[0];
+        }
+        checkUnanimatedComputedStyle(property, initialStyle, null,
+                                     expectedInitialStyle,
+                                     expectedDuringTransitionStyle,
+                                     cssTransition, "CSS Transitions");
+      }
+
+      document.styleSheets[0].insertRule(cssAnimationStyle, 0);
+      document.styleSheets[0].insertRule(
+        ".pseudo::before { animation: cssanimation 1s; }", 0);
+      const pseudoAnimation = target => {
+        target.classList.add("pseudo");
+        return target.getAnimations({ subtree: true })[0];
+      }
+      checkUnanimatedComputedStyle(property, initialStyle, "::before",
+                                   expectedInitialStyle, expectedInitialStyle,
+                                   pseudoAnimation, "Animation at pseudo");
+      document.styleSheets[0].deleteRule(0);
+      document.styleSheets[0].deleteRule(0);
+    });
+  });
+
+  SimpleTest.doesThrow(
+    () => utils.getUnanimatedComputedStyle(div, null, "background"),
+    "NS_ERROR_INVALID_ARG",
+    "Shorthand property should throw");
+
+  SimpleTest.doesThrow(
+    () => utils.getUnanimatedComputedStyle(div, null, "invalid"),
+    "NS_ERROR_INVALID_ARG",
+    "Invalid property should throw");
+
+  SimpleTest.doesThrow(
+    () => utils.getUnanimatedComputedStyle(null, null, "opacity"),
+    "NS_ERROR_INVALID_ARG",
+    "Null element should throw");
+
+  next();
+}
+
+function checkUnanimatedComputedStyle(property, initialStyle, pseudoType,
+                                      expectedBeforeAnimation,
+                                      expectedDuringAnimation,
+                                      animate, animationType) {
+  const div = document.createElement("div");
+  document.body.appendChild(div);
+
+  if (initialStyle) {
+    div.style[property] = initialStyle;
+  }
+
+  is(utils.getUnanimatedComputedStyle(div, pseudoType, property),
+     expectedBeforeAnimation,
+     `'${ property }' property with '${ initialStyle }' style `
+     + `should be '${ expectedBeforeAnimation }' `
+     + `before animating by ${ animationType }`);
+
+  const animation = animate(div);
+  animation.currentTime = 500;
+  is(utils.getUnanimatedComputedStyle(div, pseudoType, property),
+     expectedDuringAnimation,
+     `'${ property }' property with '${ initialStyle }' style `
+     + `should be '${ expectedDuringAnimation }' `
+     + `even while animating by ${ animationType }`);
+
+  div.remove();
+}
+
 var tests = [
   test_sendMouseEventDefaults,
   test_sendMouseEventOptionals,
-  test_getAnimationType
+  test_getAnimationType,
+  test_getUnanimatedComputedStyle
 ];
 
 function next() {
   if (!tests.length) {
     SimpleTest.finish();
     return;
   }
 
--- a/dom/interfaces/base/nsIDOMWindowUtils.idl
+++ b/dom/interfaces/base/nsIDOMWindowUtils.idl
@@ -1560,16 +1560,27 @@ interface nsIDOMWindowUtils : nsISupport
   /**
    * Returns the animation type of the specified property (e.g. 'coord').
    *
    * @param aProperty A longhand CSS property (e.g. 'background-color').
    */
   AString getAnimationTypeForLonghand(in AString aProperty);
 
   /**
+   * Returns the computed style for the specified property of given pseudo type
+   * on the given element after removing styles from declarative animations.
+   * @param aElement - A target element
+   * @param aPseudoElement - A pseudo type (e.g. '::before' or null)
+   * @param aProperty - A longhand CSS property (e.g. 'background-color')
+   */
+  AString getUnanimatedComputedStyle(in nsIDOMElement aElement,
+                                     in AString aPseudoElement,
+                                     in AString aProperty);
+
+  /**
    * Get the type of the currently focused html input, if any.
    */
   readonly attribute string focusedInputType;
 
   /**
    * Find the view ID for a given element. This is the reverse of
    * findElementWithViewId().
    */
--- a/layout/style/nsCSSPseudoElements.cpp
+++ b/layout/style/nsCSSPseudoElements.cpp
@@ -7,16 +7,17 @@
 
 #include "nsCSSPseudoElements.h"
 
 #include "mozilla/ArrayUtils.h"
 
 #include "nsAtomListUtils.h"
 #include "nsStaticAtom.h"
 #include "nsCSSAnonBoxes.h"
+#include "nsDOMString.h"
 
 using namespace mozilla;
 
 // define storage for all atoms
 #define CSS_PSEUDO_ELEMENT(name_, value_, flags_) \
   nsICSSPseudoElement* nsCSSPseudoElements::name_;
 #include "nsCSSPseudoElementList.h"
 #undef CSS_PSEUDO_ELEMENT
@@ -112,14 +113,47 @@ nsCSSPseudoElements::GetPseudoType(nsIAt
 /* static */ nsIAtom*
 nsCSSPseudoElements::GetPseudoAtom(Type aType)
 {
   NS_ASSERTION(aType < Type::Count, "Unexpected type");
   return *CSSPseudoElements_info[
     static_cast<CSSPseudoElementTypeBase>(aType)].mAtom;
 }
 
+/* static */ nsIAtom*
+nsCSSPseudoElements::GetPseudoAtom(const nsAString& aPseudoElement)
+{
+  if (DOMStringIsNull(aPseudoElement) || aPseudoElement.IsEmpty() ||
+      aPseudoElement.First() != char16_t(':')) {
+    return nullptr;
+  }
+
+  // deal with two-colon forms of aPseudoElt
+  nsAString::const_iterator start, end;
+  aPseudoElement.BeginReading(start);
+  aPseudoElement.EndReading(end);
+  NS_ASSERTION(start != end, "aPseudoElement is not empty!");
+  ++start;
+  bool haveTwoColons = true;
+  if (start == end || *start != char16_t(':')) {
+    --start;
+    haveTwoColons = false;
+  }
+  nsCOMPtr<nsIAtom> pseudo = NS_Atomize(Substring(start, end));
+  MOZ_ASSERT(pseudo);
+
+  // There aren't any non-CSS2 pseudo-elements with a single ':'
+  if (!haveTwoColons &&
+      (!IsPseudoElement(pseudo) || !IsCSS2PseudoElement(pseudo))) {
+    // XXXbz I'd really rather we threw an exception or something, but
+    // the DOM spec sucks.
+    return nullptr;
+  }
+
+  return pseudo;
+}
+
 /* static */ bool
 nsCSSPseudoElements::PseudoElementSupportsUserActionState(const Type aType)
 {
   return PseudoElementHasFlags(aType,
                                CSS_PSEUDO_ELEMENT_SUPPORTS_USER_ACTION_STATE);
 }
--- a/layout/style/nsCSSPseudoElements.h
+++ b/layout/style/nsCSSPseudoElements.h
@@ -87,16 +87,19 @@ public:
 #include "nsCSSPseudoElementList.h"
 #undef CSS_PSEUDO_ELEMENT
 
   static Type GetPseudoType(nsIAtom* aAtom, EnabledState aEnabledState);
 
   // Get the atom for a given Type.  aType must be < CSSPseudoElementType::Count
   static nsIAtom* GetPseudoAtom(Type aType);
 
+  // Get the atom for a given nsAString. (e.g. "::before")
+  static nsIAtom* GetPseudoAtom(const nsAString& aPseudoElement);
+
   static bool PseudoElementContainsElements(const Type aType) {
     return PseudoElementHasFlags(aType, CSS_PSEUDO_ELEMENT_CONTAINS_ELEMENTS);
   }
 
   static bool PseudoElementSupportsStyleAttribute(const Type aType) {
     MOZ_ASSERT(aType < Type::Count);
     return PseudoElementHasFlags(aType,
                                  CSS_PSEUDO_ELEMENT_SUPPORTS_STYLE_ATTRIBUTE);
--- a/layout/style/nsComputedDOMStyle.cpp
+++ b/layout/style/nsComputedDOMStyle.cpp
@@ -7,17 +7,16 @@
 /* DOM object returned from element.getComputedStyle() */
 
 #include "nsComputedDOMStyle.h"
 
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/Preferences.h"
 
 #include "nsError.h"
-#include "nsDOMString.h"
 #include "nsIDOMCSSPrimitiveValue.h"
 #include "nsIFrame.h"
 #include "nsIFrameInlines.h"
 #include "nsStyleContext.h"
 #include "nsIScrollableFrame.h"
 #include "nsContentUtils.h"
 #include "nsIContent.h"
 
@@ -62,20 +61,22 @@ using namespace mozilla::dom;
 
 /*
  * This is the implementation of the readonly CSSStyleDeclaration that is
  * returned by the getComputedStyle() function.
  */
 
 already_AddRefed<nsComputedDOMStyle>
 NS_NewComputedDOMStyle(dom::Element* aElement, const nsAString& aPseudoElt,
-                       nsIPresShell* aPresShell)
+                       nsIPresShell* aPresShell,
+                       nsComputedDOMStyle::AnimationFlag aFlag)
 {
   RefPtr<nsComputedDOMStyle> computedStyle;
-  computedStyle = new nsComputedDOMStyle(aElement, aPseudoElt, aPresShell);
+  computedStyle = new nsComputedDOMStyle(aElement, aPseudoElt,
+                                         aPresShell, aFlag);
   return computedStyle.forget();
 }
 
 static nsDOMCSSValueList*
 GetROCSSValueList(bool aCommaDelimited)
 {
   return new nsDOMCSSValueList(aCommaDelimited, true);
 }
@@ -238,56 +239,32 @@ nsComputedStyleMap::Update()
       mIndexMap[index++] = i;
     }
   }
   mExposedPropertyCount = index;
 }
 
 nsComputedDOMStyle::nsComputedDOMStyle(dom::Element* aElement,
                                        const nsAString& aPseudoElt,
-                                       nsIPresShell* aPresShell)
+                                       nsIPresShell* aPresShell,
+                                       AnimationFlag aFlag)
   : mDocumentWeak(nullptr)
   , mOuterFrame(nullptr)
   , mInnerFrame(nullptr)
   , mPresShell(nullptr)
   , mStyleContextGeneration(0)
   , mExposeVisitedStyle(false)
   , mResolvedStyleContext(false)
+  , mAnimationFlag(aFlag)
 {
   MOZ_ASSERT(aElement && aPresShell);
 
   mDocumentWeak = do_GetWeakReference(aPresShell->GetDocument());
-
   mContent = aElement;
-
-  if (!DOMStringIsNull(aPseudoElt) && !aPseudoElt.IsEmpty() &&
-      aPseudoElt.First() == char16_t(':')) {
-    // deal with two-colon forms of aPseudoElt
-    nsAString::const_iterator start, end;
-    aPseudoElt.BeginReading(start);
-    aPseudoElt.EndReading(end);
-    NS_ASSERTION(start != end, "aPseudoElt is not empty!");
-    ++start;
-    bool haveTwoColons = true;
-    if (start == end || *start != char16_t(':')) {
-      --start;
-      haveTwoColons = false;
-    }
-    mPseudo = NS_Atomize(Substring(start, end));
-    MOZ_ASSERT(mPseudo);
-
-    // There aren't any non-CSS2 pseudo-elements with a single ':'
-    if (!haveTwoColons &&
-        (!nsCSSPseudoElements::IsPseudoElement(mPseudo) ||
-         !nsCSSPseudoElements::IsCSS2PseudoElement(mPseudo))) {
-      // XXXbz I'd really rather we threw an exception or something, but
-      // the DOM spec sucks.
-      mPseudo = nullptr;
-    }
-  }
+  mPseudo = nsCSSPseudoElements::GetPseudoAtom(aPseudoElt);
 
   MOZ_ASSERT(aPresShell->GetPresContext());
 }
 
 nsComputedDOMStyle::~nsComputedDOMStyle()
 {
   ClearStyleContext();
 }
@@ -842,16 +819,27 @@ nsComputedDOMStyle::UpdateCurrentStyleSo
                    mPresShell->GetPresContext()->GetRestyleGeneration(),
                  "why should we have flushed style again?");
 
     SetResolvedStyleContext(Move(resolvedStyleContext));
     NS_ASSERTION(mPseudo || !mStyleContext->HasPseudoElementData(),
                  "should not have pseudo-element data");
   }
 
+  if (mAnimationFlag == eWithoutAnimation) {
+    // We will support Servo in bug 1311257.
+    MOZ_ASSERT(mPresShell->StyleSet()->IsGecko(),
+               "eWithoutAnimationRules support Gecko only");
+    nsStyleSet* styleSet = mPresShell->StyleSet()->AsGecko();
+    RefPtr<nsStyleContext> unanimatedStyleContext =
+      styleSet->ResolveStyleByRemovingAnimation(
+        mContent->AsElement(), mStyleContext, eRestyle_AllHintsWithAnimations);
+    SetResolvedStyleContext(Move(unanimatedStyleContext));
+  }
+
   // mExposeVisitedStyle is set to true only by testing APIs that
   // require chrome privilege.
   MOZ_ASSERT(!mExposeVisitedStyle || nsContentUtils::IsCallerChrome(),
              "mExposeVisitedStyle set incorrectly");
   if (mExposeVisitedStyle && mStyleContext->RelevantLinkVisited()) {
     nsStyleContext *styleIfVisited = mStyleContext->GetStyleIfVisited();
     if (styleIfVisited) {
       mStyleContext = styleIfVisited;
--- a/layout/style/nsComputedDOMStyle.h
+++ b/layout/style/nsComputedDOMStyle.h
@@ -64,34 +64,35 @@ public:
 
   NS_DECL_NSIDOMCSSSTYLEDECLARATION_HELPER
   virtual already_AddRefed<CSSValue>
   GetPropertyCSSValue(const nsAString& aProp, mozilla::ErrorResult& aRv)
     override;
   using nsICSSDeclaration::GetPropertyCSSValue;
   virtual void IndexedGetter(uint32_t aIndex, bool& aFound, nsAString& aPropName) override;
 
+  enum AnimationFlag {
+    eWithAnimation,
+    eWithoutAnimation,
+  };
+
   nsComputedDOMStyle(mozilla::dom::Element* aElement,
                      const nsAString& aPseudoElt,
-                     nsIPresShell* aPresShell);
+                     nsIPresShell* aPresShell,
+                     AnimationFlag aFlag = eWithAnimation);
 
   virtual nsINode *GetParentObject() override
   {
     return mContent;
   }
 
   static already_AddRefed<nsStyleContext>
   GetStyleContext(mozilla::dom::Element* aElement, nsIAtom* aPseudo,
                   nsIPresShell* aPresShell);
 
-  enum AnimationFlag {
-    eWithAnimation,
-    eWithoutAnimation,
-  };
-
   static already_AddRefed<nsStyleContext>
   GetStyleContextNoFlush(mozilla::dom::Element* aElement,
                          nsIAtom* aPseudo,
                          nsIPresShell* aPresShell)
   {
     return DoGetStyleContextNoFlush(aElement,
                                     aPseudo,
                                     aPresShell,
@@ -742,19 +743,26 @@ private:
   bool mExposeVisitedStyle;
 
   /**
    * Whether we resolved a style context last time we called
    * UpdateCurrentStyleSources.  Initially false.
    */
   bool mResolvedStyleContext;
 
+  /**
+   * Whether we include animation rules in the computed style.
+   */
+  AnimationFlag mAnimationFlag;
+
 #ifdef DEBUG
   bool mFlushedPendingReflows;
 #endif
 };
 
 already_AddRefed<nsComputedDOMStyle>
 NS_NewComputedDOMStyle(mozilla::dom::Element* aElement,
                        const nsAString& aPseudoElt,
-                       nsIPresShell* aPresShell);
+                       nsIPresShell* aPresShell,
+                       nsComputedDOMStyle::AnimationFlag aFlag =
+                         nsComputedDOMStyle::eWithAnimation);
 
 #endif /* nsComputedDOMStyle_h__ */