Bug 988808, part 2 - Convert SVG hit-testing to use Moz2D instead of Thebes backed gfxContext. r=Bas

This commit is contained in:
Jonathan Watt 2014-07-05 21:53:04 +01:00
parent 8a4d2baf65
commit 191df684dc
7 changed files with 299 additions and 49 deletions

View File

@ -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<SVGAnimationElement*>(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<gfxFloat>& 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<SVGPathElement*>(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<nsStyleContext> 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<nsStyleContext> 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)
{

View File

@ -10,12 +10,15 @@
#define _USE_MATH_DEFINES
#include <math.h>
#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

View File

@ -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);
}
}

View File

@ -111,15 +111,3 @@ nsSVGPathGeometryElement::GetFillRule()
return fillRule;
}
Float
nsSVGPathGeometryElement::GetStrokeWidth()
{
nsRefPtr<nsStyleContext> styleContext =
nsComputedDOMStyle::GetStyleContextForElementNoFlush(this, nullptr,
nullptr);
return styleContext ?
SVGContentUtils::CoordToFloat(this,
styleContext->StyleSVG()->mStrokeWidth) :
0.0f;
}

View File

@ -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

View File

@ -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()
<div id="content">
<div width="100%" height="1" id="div">
</div>
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="400" id="svg">
<circle id="circle" cx="1.5" cy="1.5" r="1" transform="scale(100, 100)"/>
</svg>
</div>
</div>
<pre id="test">

View File

@ -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<gfxContext> tmpCtx =
new gfxContext(gfxPlatform::GetPlatform()->ScreenReferenceSurface());
nsSVGPathGeometryElement* content =
static_cast<nsSVGPathGeometryElement*>(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> drawTarget =
gfxPlatform::GetPlatform()->ScreenReferenceDrawTarget();
RefPtr<PathBuilder> builder =
drawTarget->CreatePathBuilder(fillRule);
RefPtr<Path> 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<PathBuilder> 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<PathBuilder> builder =
path->TransformedCopyToBuilder(nonScalingStrokeMatrix, fillRule);
path = builder->Finish();
}
isHit = path->StrokeContainsPoint(stroke, userSpacePoint, Matrix());
}
if (isHit && nsSVGUtils::HitTestClip(this, aPoint))