From 97669ec7d87879a29a45c1d3a3753d17c4e55cb4 Mon Sep 17 00:00:00 2001 From: Jonathan Watt Date: Sat, 5 Jul 2014 21:53:04 +0100 Subject: [PATCH] Bug 988808, part 2 - Convert SVG hit-testing to use Moz2D instead of Thebes backed gfxContext. r=Bas --- content/svg/content/src/SVGContentUtils.cpp | 178 +++++++++++++++++- content/svg/content/src/SVGContentUtils.h | 63 +++++++ content/svg/content/src/SVGPathElement.cpp | 2 +- .../content/src/nsSVGPathGeometryElement.cpp | 12 -- .../content/src/nsSVGPathGeometryElement.h | 8 - .../content/test/test_pointer-events-3.xhtml | 4 +- layout/svg/nsSVGPathGeometryFrame.cpp | 81 +++++--- 7 files changed, 299 insertions(+), 49 deletions(-) diff --git a/content/svg/content/src/SVGContentUtils.cpp b/content/svg/content/src/SVGContentUtils.cpp index 4a265ae63eb..f414d99420c 100644 --- a/content/svg/content/src/SVGContentUtils.cpp +++ b/content/svg/content/src/SVGContentUtils.cpp @@ -8,8 +8,10 @@ #include "SVGContentUtils.h" // Keep others in (case-insensitive) order: +#include "gfx2DGlue.h" #include "gfxMatrix.h" #include "gfxPlatform.h" +#include "gfxSVGGlyphs.h" #include "mozilla/gfx/2D.h" #include "mozilla/dom/SVGSVGElement.h" #include "mozilla/RefPtr.h" @@ -23,9 +25,10 @@ #include "nsContentUtils.h" #include "mozilla/gfx/2D.h" #include "mozilla/gfx/Types.h" -#include "gfx2DGlue.h" +#include "nsStyleContext.h" #include "nsSVGPathDataParser.h" #include "SVGPathData.h" +#include "SVGPathElement.h" using namespace mozilla; using namespace mozilla::dom; @@ -58,6 +61,179 @@ SVGContentUtils::ActivateByHyperlink(nsIContent *aContent) static_cast(aContent)->ActivateByHyperlink(); } +enum DashState { + eDashedStroke, + eContinuousStroke, //< all dashes, no gaps + eNoStroke //< all gaps, no dashes +}; + +static DashState +GetStrokeDashData(SVGContentUtils::AutoStrokeOptions* aStrokeOptions, + nsSVGElement* aElement, + const nsStyleSVG* aStyleSVG, + gfxTextContextPaint *aContextPaint) +{ + size_t dashArrayLength; + Float totalLengthOfDashes = 0.0, totalLengthOfGaps = 0.0; + + if (aContextPaint && aStyleSVG->mStrokeDasharrayFromObject) { + const FallibleTArray& dashSrc = aContextPaint->GetStrokeDashArray(); + dashArrayLength = dashSrc.Length(); + if (dashArrayLength <= 0) { + return eContinuousStroke; + } + Float* dashPattern = aStrokeOptions->InitDashPattern(dashArrayLength); + if (!dashPattern) { + return eContinuousStroke; + } + for (size_t i = 0; i < dashArrayLength; i++) { + if (dashSrc[i] < 0.0) { + return eContinuousStroke; // invalid + } + dashPattern[i] = Float(dashSrc[i]); + (i % 2 ? totalLengthOfGaps : totalLengthOfDashes) += dashSrc[i]; + } + } else { + const nsStyleCoord *dasharray = aStyleSVG->mStrokeDasharray; + dashArrayLength = aStyleSVG->mStrokeDasharrayLength; + if (dashArrayLength <= 0) { + return eContinuousStroke; + } + Float pathScale = 1.0; + if (aElement->Tag() == nsGkAtoms::path) { + pathScale = static_cast(aElement)-> + GetPathLengthScale(SVGPathElement::eForStroking); + if (pathScale <= 0) { + return eContinuousStroke; + } + } + Float* dashPattern = aStrokeOptions->InitDashPattern(dashArrayLength); + if (!dashPattern) { + return eContinuousStroke; + } + for (uint32_t i = 0; i < dashArrayLength; i++) { + Float dashLength = + SVGContentUtils::CoordToFloat(aElement, dasharray[i]) * pathScale; + if (dashLength < 0.0) { + return eContinuousStroke; // invalid + } + dashPattern[i] = dashLength; + (i % 2 ? totalLengthOfGaps : totalLengthOfDashes) += dashLength; + } + } + + // Now that aStrokeOptions.mDashPattern is fully initialized we can safely + // set mDashLength: + aStrokeOptions->mDashLength = dashArrayLength; + + if (totalLengthOfDashes <= 0 || totalLengthOfGaps <= 0) { + if (totalLengthOfGaps > 0 && totalLengthOfDashes <= 0) { + return eNoStroke; + } + return eContinuousStroke; + } + + if (aContextPaint && aStyleSVG->mStrokeDashoffsetFromObject) { + aStrokeOptions->mDashOffset = Float(aContextPaint->GetStrokeDashOffset()); + } else { + aStrokeOptions->mDashOffset = + SVGContentUtils::CoordToFloat(aElement, aStyleSVG->mStrokeDashoffset); + } + + return eDashedStroke; +} + +void +SVGContentUtils::GetStrokeOptions(AutoStrokeOptions* aStrokeOptions, + nsSVGElement* aElement, + nsStyleContext* aStyleContext, + gfxTextContextPaint *aContextPaint) +{ + nsRefPtr styleContext; + if (aStyleContext) { + styleContext = aStyleContext; + } else { + styleContext = + nsComputedDOMStyle::GetStyleContextForElementNoFlush(aElement, nullptr, + nullptr); + } + + if (!styleContext) { + return; + } + + const nsStyleSVG* styleSVG = styleContext->StyleSVG(); + + DashState dashState = + GetStrokeDashData(aStrokeOptions, aElement, styleSVG, aContextPaint); + + if (dashState == eNoStroke) { + // Hopefully this will shortcircuit any stroke operations: + aStrokeOptions->mLineWidth = 0; + return; + } + if (dashState == eContinuousStroke) { + // Prevent our caller from wasting time looking at the dash array: + aStrokeOptions->mDashLength = 0; + } + + aStrokeOptions->mLineWidth = + GetStrokeWidth(aElement, styleContext, aContextPaint); + + aStrokeOptions->mMiterLimit = Float(styleSVG->mStrokeMiterlimit); + + switch (styleSVG->mStrokeLinejoin) { + case NS_STYLE_STROKE_LINEJOIN_MITER: + aStrokeOptions->mLineJoin = JoinStyle::MITER; + break; + case NS_STYLE_STROKE_LINEJOIN_ROUND: + aStrokeOptions->mLineJoin = JoinStyle::ROUND; + break; + case NS_STYLE_STROKE_LINEJOIN_BEVEL: + aStrokeOptions->mLineJoin = JoinStyle::BEVEL; + break; + } + + switch (styleSVG->mStrokeLinecap) { + case NS_STYLE_STROKE_LINECAP_BUTT: + aStrokeOptions->mLineCap = CapStyle::BUTT; + break; + case NS_STYLE_STROKE_LINECAP_ROUND: + aStrokeOptions->mLineCap = CapStyle::ROUND; + break; + case NS_STYLE_STROKE_LINECAP_SQUARE: + aStrokeOptions->mLineCap = CapStyle::SQUARE; + break; + } +} + +Float +SVGContentUtils::GetStrokeWidth(nsSVGElement* aElement, + nsStyleContext* aStyleContext, + gfxTextContextPaint *aContextPaint) +{ + nsRefPtr styleContext; + if (aStyleContext) { + styleContext = aStyleContext; + } else { + styleContext = + nsComputedDOMStyle::GetStyleContextForElementNoFlush(aElement, nullptr, + nullptr); + } + + if (!styleContext) { + return 0.0f; + } + + const nsStyleSVG* styleSVG = styleContext->StyleSVG(); + + if (aContextPaint && styleSVG->mStrokeWidthFromObject) { + return aContextPaint->GetStrokeWidth(); + } + + return SVGContentUtils::CoordToFloat(aElement, styleSVG->mStrokeWidth); +} + float SVGContentUtils::GetFontSize(Element *aElement) { diff --git a/content/svg/content/src/SVGContentUtils.h b/content/svg/content/src/SVGContentUtils.h index 7f8a8b3fd54..178dc747beb 100644 --- a/content/svg/content/src/SVGContentUtils.h +++ b/content/svg/content/src/SVGContentUtils.h @@ -10,12 +10,15 @@ #define _USE_MATH_DEFINES #include +#include "mozilla/fallible.h" +#include "mozilla/gfx/2D.h" // for StrokeOptions #include "mozilla/gfx/Matrix.h" #include "mozilla/RangedPtr.h" #include "nsError.h" #include "nsStringFwd.h" #include "gfx2DGlue.h" +class gfxTextContextPaint; class nsIContent; class nsIDocument; class nsIFrame; @@ -58,6 +61,8 @@ IsSVGWhitespace(char16_t aChar) class SVGContentUtils { public: + typedef mozilla::gfx::Float Float; + typedef mozilla::gfx::StrokeOptions StrokeOptions; typedef mozilla::SVGAnimatedPreserveAspectRatio SVGAnimatedPreserveAspectRatio; typedef mozilla::SVGPreserveAspectRatio SVGPreserveAspectRatio; @@ -76,6 +81,64 @@ public: */ static void ActivateByHyperlink(nsIContent *aContent); + /** + * Moz2D's StrokeOptions requires someone else to own its mDashPattern + * buffer, which is a pain when you want to initialize a StrokeOptions object + * in a helper function and pass it out. This sub-class owns the mDashPattern + * buffer so that consumers of such a helper function don't need to worry + * about creating it, passing it in, or deleting it. (An added benefit is + * that in the typical case when stroke-dasharray is short it will avoid + * allocating.) + */ + struct AutoStrokeOptions : public StrokeOptions { + AutoStrokeOptions() + { + MOZ_ASSERT(mDashLength == 0, "InitDashPattern() depends on this"); + } + ~AutoStrokeOptions() { + if (mDashPattern && mDashPattern != mSmallArray) { + delete [] mDashPattern; + } + } + /** + * Creates the buffer to store the stroke-dasharray, assuming out-of-memory + * does not occur. The buffer's address is assigned to mDashPattern and + * returned to the caller as a non-const pointer (so that the caller can + * initialize the values in the buffer, since mDashPattern is const). + */ + Float* InitDashPattern(size_t aDashCount) { + if (aDashCount <= MOZ_ARRAY_LENGTH(mSmallArray)) { + mDashPattern = mSmallArray; + return mSmallArray; + } + static const mozilla::fallible_t fallible = mozilla::fallible_t(); + Float* nonConstArray = new (fallible) Float[aDashCount]; + mDashPattern = nonConstArray; + return nonConstArray; + } + private: + // Most dasharrays will fit in this and save us allocating + Float mSmallArray[16]; + }; + + static void GetStrokeOptions(AutoStrokeOptions* aStrokeOptions, + nsSVGElement* aElement, + nsStyleContext* aStyleContext, + gfxTextContextPaint *aContextPaint); + + /** + * Returns the current computed value of the CSS property 'stroke-width' for + * the given element. aStyleContext may be provided as an optimization. + * aContextPaint is also optional. + * + * Note that this function does NOT take account of the value of the 'stroke' + * and 'stroke-opacity' properties to, say, return zero if they are "none" or + * "0", respectively. + */ + static Float GetStrokeWidth(nsSVGElement* aElement, + nsStyleContext* aStyleContext, + gfxTextContextPaint *aContextPaint); + /* * Get the number of CSS px (user units) per em (i.e. the em-height in user * units) for an nsIContent diff --git a/content/svg/content/src/SVGPathElement.cpp b/content/svg/content/src/SVGPathElement.cpp index 07d0f871963..b05f0e5a469 100644 --- a/content/svg/content/src/SVGPathElement.cpp +++ b/content/svg/content/src/SVGPathElement.cpp @@ -394,7 +394,7 @@ SVGPathElement::BuildPath(PathBuilder* aBuilder) // opacity here. if (style->mStrokeLinecap == NS_STYLE_STROKE_LINECAP_SQUARE) { strokeLineCap = style->mStrokeLinecap; - strokeWidth = GetStrokeWidth(); + strokeWidth = SVGContentUtils::GetStrokeWidth(this, styleContext, nullptr); } } diff --git a/content/svg/content/src/nsSVGPathGeometryElement.cpp b/content/svg/content/src/nsSVGPathGeometryElement.cpp index c63ca8aa862..982c0c753aa 100644 --- a/content/svg/content/src/nsSVGPathGeometryElement.cpp +++ b/content/svg/content/src/nsSVGPathGeometryElement.cpp @@ -111,15 +111,3 @@ nsSVGPathGeometryElement::GetFillRule() return fillRule; } - -Float -nsSVGPathGeometryElement::GetStrokeWidth() -{ - nsRefPtr styleContext = - nsComputedDOMStyle::GetStyleContextForElementNoFlush(this, nullptr, - nullptr); - return styleContext ? - SVGContentUtils::CoordToFloat(this, - styleContext->StyleSVG()->mStrokeWidth) : - 0.0f; -} diff --git a/content/svg/content/src/nsSVGPathGeometryElement.h b/content/svg/content/src/nsSVGPathGeometryElement.h index ccaf852fc22..ccdebaa114a 100644 --- a/content/svg/content/src/nsSVGPathGeometryElement.h +++ b/content/svg/content/src/nsSVGPathGeometryElement.h @@ -77,14 +77,6 @@ public: * this element. */ FillRule GetFillRule(); - - /** - * Returns the current computed value of the CSS property 'stroke-width' for - * this element. (I.e. this does NOT take account of the value of the - * 'stroke' and 'stroke-opacity' properties to, say, return zero if they are - * "none" or "0", respectively.) - */ - Float GetStrokeWidth(); }; #endif diff --git a/content/svg/content/test/test_pointer-events-3.xhtml b/content/svg/content/test/test_pointer-events-3.xhtml index 78e428e70f8..7981002237f 100644 --- a/content/svg/content/test/test_pointer-events-3.xhtml +++ b/content/svg/content/test/test_pointer-events-3.xhtml @@ -22,7 +22,7 @@ function run() var originY = div.offsetTop; var circle = document.getElementById("circle"); - var elementFromPoint = document.elementFromPoint(originX + 150, originY + 51); + var elementFromPoint = document.elementFromPoint(originX + 150, originY + 52); is(elementFromPoint, circle, "Top of circle should hit"); var elementFromPoint = document.elementFromPoint(originX + 249, originY + 150); @@ -44,10 +44,10 @@ function run()
-
+
diff --git a/layout/svg/nsSVGPathGeometryFrame.cpp b/layout/svg/nsSVGPathGeometryFrame.cpp
index 80ea47d41b6..5823719d821 100644
--- a/layout/svg/nsSVGPathGeometryFrame.cpp
+++ b/layout/svg/nsSVGPathGeometryFrame.cpp
@@ -7,6 +7,7 @@
 #include "nsSVGPathGeometryFrame.h"
 
 // Keep others in (case-insensitive) order:
+#include "gfx2DGlue.h"
 #include "gfxContext.h"
 #include "gfxPlatform.h"
 #include "gfxSVGGlyphs.h"
@@ -20,6 +21,7 @@
 #include "nsSVGUtils.h"
 #include "mozilla/ArrayUtils.h"
 #include "SVGAnimatedTransformList.h"
+#include "SVGContentUtils.h"
 #include "SVGGraphicsElement.h"
 
 using namespace mozilla;
@@ -234,48 +236,77 @@ nsSVGPathGeometryFrame::PaintSVG(nsRenderingContext *aContext,
 nsIFrame*
 nsSVGPathGeometryFrame::GetFrameForPoint(const nsPoint &aPoint)
 {
-  gfxMatrix canvasTM = GetCanvasTM(FOR_HIT_TESTING);
-  if (canvasTM.IsSingular()) {
+  gfxMatrix hitTestingTM = GetCanvasTM(FOR_HIT_TESTING);
+  if (hitTestingTM.IsSingular()) {
     return nullptr;
   }
-  uint16_t fillRule, hitTestFlags;
+  FillRule fillRule;
+  uint16_t hitTestFlags;
   if (GetStateBits() & NS_STATE_SVG_CLIPPATH_CHILD) {
     hitTestFlags = SVG_HIT_TEST_FILL;
-    fillRule = StyleSVG()->mClipRule;
+    fillRule = StyleSVG()->mClipRule == NS_STYLE_FILL_RULE_NONZERO
+                 ? FillRule::FILL_WINDING : FillRule::FILL_EVEN_ODD;
   } else {
     hitTestFlags = GetHitTestFlags();
     nsPoint point =
-      nsSVGUtils::TransformOuterSVGPointToChildFrame(aPoint, canvasTM, PresContext());
+      nsSVGUtils::TransformOuterSVGPointToChildFrame(aPoint, hitTestingTM, PresContext());
     if (!hitTestFlags || ((hitTestFlags & SVG_HIT_TEST_CHECK_MRECT) &&
-                          !mRect.Contains(point)))
+                          !mRect.Contains(point))) {
       return nullptr;
-    fillRule = StyleSVG()->mFillRule;
+    }
+    fillRule = StyleSVG()->mFillRule == NS_STYLE_FILL_RULE_NONZERO
+                 ? FillRule::FILL_WINDING : FillRule::FILL_EVEN_ODD;
   }
 
   bool isHit = false;
 
-  nsRefPtr tmpCtx =
-    new gfxContext(gfxPlatform::GetPlatform()->ScreenReferenceSurface());
+  nsSVGPathGeometryElement* content =
+    static_cast(mContent);
 
-  GeneratePath(tmpCtx, ToMatrix(canvasTM));
-  gfxPoint userSpacePoint =
-    tmpCtx->DeviceToUser(gfxPoint(aPoint.x, aPoint.y) / PresContext()->AppUnitsPerCSSPixel());
+  // Using ScreenReferenceDrawTarget() opens us to Moz2D backend specific hit-
+  // testing bugs. Maybe we should use a BackendType::CAIRO DT for hit-testing
+  // so that we get more consistent/backwards compatible results?
+  RefPtr drawTarget =
+    gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget();
+  RefPtr builder =
+    drawTarget->CreatePathBuilder(fillRule);
+  RefPtr path = content->BuildPath(builder);
+  if (!path) {
+    return nullptr; // no path, so we don't paint anything that can be hit
+  }
 
-  if (fillRule == NS_STYLE_FILL_RULE_EVENODD)
-    tmpCtx->SetFillRule(gfxContext::FILL_RULE_EVEN_ODD);
-  else
-    tmpCtx->SetFillRule(gfxContext::FILL_RULE_WINDING);
+  if (!hitTestingTM.IsIdentity()) {
+    // We'll only get here if we don't have a nsDisplayItem that has called us
+    // (for example, if we're a NS_FRAME_IS_NONDISPLAY frame under a clipPath).
+    RefPtr builder =
+      path->TransformedCopyToBuilder(ToMatrix(hitTestingTM), fillRule);
+    path = builder->Finish();
+  }
 
-  if (hitTestFlags & SVG_HIT_TEST_FILL)
-    isHit = tmpCtx->PointInFill(userSpacePoint);
+  int32_t appUnitsPerCSSPx = PresContext()->AppUnitsPerCSSPixel();
+  Point userSpacePoint = Point(Float(aPoint.x) / appUnitsPerCSSPx,
+                               Float(aPoint.y) / appUnitsPerCSSPx);
+
+  if (hitTestFlags & SVG_HIT_TEST_FILL) {
+    isHit = path->ContainsPoint(userSpacePoint, Matrix());
+  }
   if (!isHit && (hitTestFlags & SVG_HIT_TEST_STROKE)) {
-    nsSVGUtils::SetupCairoStrokeGeometry(this, tmpCtx);
-    // tmpCtx's matrix may have transformed by SetupCairoStrokeGeometry
-    // if there is a non-scaling stroke. We need to transform userSpacePoint
-    // so that everything is using the same co-ordinate system.
-    userSpacePoint =
-      nsSVGUtils::GetStrokeTransform(this).Invert().Transform(userSpacePoint);
-    isHit = tmpCtx->PointInStroke(userSpacePoint);
+    SVGContentUtils::AutoStrokeOptions stroke;
+    SVGContentUtils::GetStrokeOptions(&stroke, content, StyleContext(), nullptr);
+    Matrix nonScalingStrokeMatrix =
+      ToMatrix(nsSVGUtils::GetStrokeTransform(this));
+    if (!nonScalingStrokeMatrix.IsIdentity()) {
+      // We need to transform the path back into the appropriate ancestor
+      // coordinate system in order for non-scaled stroke to be correct.
+      // Naturally we also need to transform the point into the same
+      // coordinate system in order to hit-test against the path.
+      nonScalingStrokeMatrix.Invert();
+      userSpacePoint = ToMatrix(hitTestingTM) * nonScalingStrokeMatrix * userSpacePoint;
+      RefPtr builder =
+        path->TransformedCopyToBuilder(nonScalingStrokeMatrix, fillRule);
+      path = builder->Finish();
+    }
+    isHit = path->StrokeContainsPoint(stroke, userSpacePoint, Matrix());
   }
 
   if (isHit && nsSVGUtils::HitTestClip(this, aPoint))