mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1188075 - Speed up inner box-shadow drawing by using a border-image style approach. r=mstange
This commit is contained in:
parent
401677e76c
commit
9e89498adf
@ -40,7 +40,7 @@ inline Color ToColor(const gfxRGBA &aRGBA)
|
||||
Float(aRGBA.b), Float(aRGBA.a));
|
||||
}
|
||||
|
||||
inline gfxRGBA ThebesColor(Color &aColor)
|
||||
inline gfxRGBA ThebesColor(const Color &aColor)
|
||||
{
|
||||
return gfxRGBA(aColor.r, aColor.g, aColor.b, aColor.a);
|
||||
}
|
||||
|
@ -170,16 +170,21 @@ struct BlurCacheKey : public PLDHashEntryHdr {
|
||||
gfxRGBA mShadowColor;
|
||||
BackendType mBackend;
|
||||
RectCornerRadii mCornerRadii;
|
||||
bool mIsInset;
|
||||
|
||||
BlurCacheKey(IntSize aMinimumSize, gfxIntSize aBlurRadius,
|
||||
// Only used for inset blurs
|
||||
bool mHasBorderRadius;
|
||||
gfxIntSize mSpreadRadius;
|
||||
IntSize mInnerMinSize;
|
||||
|
||||
BlurCacheKey(IntSize aMinSize, gfxIntSize aBlurRadius,
|
||||
RectCornerRadii* aCornerRadii, gfxRGBA aShadowColor,
|
||||
BackendType aBackend)
|
||||
: mMinSize(aMinimumSize)
|
||||
, mBlurRadius(aBlurRadius)
|
||||
, mShadowColor(aShadowColor)
|
||||
, mBackend(aBackend)
|
||||
, mCornerRadii(aCornerRadii ? *aCornerRadii : RectCornerRadii())
|
||||
{ }
|
||||
BackendType aBackendType)
|
||||
: BlurCacheKey(aMinSize, IntSize(0, 0),
|
||||
aBlurRadius, IntSize(0, 0),
|
||||
aCornerRadii, aShadowColor,
|
||||
false, false, aBackendType)
|
||||
{}
|
||||
|
||||
explicit BlurCacheKey(const BlurCacheKey* aOther)
|
||||
: mMinSize(aOther->mMinSize)
|
||||
@ -187,6 +192,26 @@ struct BlurCacheKey : public PLDHashEntryHdr {
|
||||
, mShadowColor(aOther->mShadowColor)
|
||||
, mBackend(aOther->mBackend)
|
||||
, mCornerRadii(aOther->mCornerRadii)
|
||||
, mIsInset(aOther->mIsInset)
|
||||
, mHasBorderRadius(aOther->mHasBorderRadius)
|
||||
, mSpreadRadius(aOther->mSpreadRadius)
|
||||
, mInnerMinSize(aOther->mInnerMinSize)
|
||||
{ }
|
||||
|
||||
explicit BlurCacheKey(IntSize aOuterMinSize, IntSize aInnerMinSize,
|
||||
gfxIntSize aBlurRadius, gfxIntSize aSpreadRadius,
|
||||
const RectCornerRadii* aCornerRadii, gfxRGBA aShadowColor,
|
||||
bool aIsInset,
|
||||
bool aHasBorderRadius, BackendType aBackendType)
|
||||
: mMinSize(aOuterMinSize)
|
||||
, mBlurRadius(aBlurRadius)
|
||||
, mShadowColor(aShadowColor)
|
||||
, mBackend(aBackendType)
|
||||
, mCornerRadii(aCornerRadii ? *aCornerRadii : RectCornerRadii())
|
||||
, mIsInset(aIsInset)
|
||||
, mHasBorderRadius(aHasBorderRadius)
|
||||
, mSpreadRadius(aSpreadRadius)
|
||||
, mInnerMinSize(aInnerMinSize)
|
||||
{ }
|
||||
|
||||
static PLDHashNumber
|
||||
@ -202,26 +227,42 @@ struct BlurCacheKey : public PLDHashEntryHdr {
|
||||
hash = AddToHash(hash, HashBytes(&aKey->mShadowColor.a, sizeof(gfxFloat)));
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
hash = AddToHash(hash, aKey->mCornerRadii[i].width, aKey->mCornerRadii[i].height);
|
||||
hash = AddToHash(hash, aKey->mCornerRadii[i].width, aKey->mCornerRadii[i].height);
|
||||
}
|
||||
|
||||
hash = AddToHash(hash, (uint32_t)aKey->mBackend);
|
||||
|
||||
if (aKey->mIsInset) {
|
||||
hash = AddToHash(hash, aKey->mSpreadRadius.width, aKey->mSpreadRadius.height);
|
||||
hash = AddToHash(hash, aKey->mInnerMinSize.width, aKey->mInnerMinSize.height);
|
||||
hash = AddToHash(hash, HashBytes(&aKey->mHasBorderRadius, sizeof(bool)));
|
||||
}
|
||||
return hash;
|
||||
}
|
||||
|
||||
bool KeyEquals(KeyTypePointer aKey) const
|
||||
bool
|
||||
KeyEquals(KeyTypePointer aKey) const
|
||||
{
|
||||
if (aKey->mMinSize == mMinSize &&
|
||||
aKey->mBlurRadius == mBlurRadius &&
|
||||
aKey->mCornerRadii == mCornerRadii &&
|
||||
aKey->mShadowColor == mShadowColor &&
|
||||
aKey->mBackend == mBackend) {
|
||||
|
||||
if (mIsInset) {
|
||||
return (mHasBorderRadius == aKey->mHasBorderRadius) &&
|
||||
(mInnerMinSize == aKey->mInnerMinSize) &&
|
||||
(mSpreadRadius == aKey->mSpreadRadius);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
static KeyTypePointer KeyToPointer(KeyType aKey)
|
||||
|
||||
static KeyTypePointer
|
||||
KeyToPointer(KeyType aKey)
|
||||
{
|
||||
return &aKey;
|
||||
}
|
||||
@ -291,6 +332,27 @@ class BlurCache final : public nsExpirationTracker<BlurCacheData,4>
|
||||
return blur;
|
||||
}
|
||||
|
||||
BlurCacheData* LookupInsetBoxShadow(const IntSize aOuterMinSize,
|
||||
const IntSize aInnerMinSize,
|
||||
const gfxIntSize& aBlurRadius,
|
||||
const gfxIntSize& aSpreadRadius,
|
||||
const RectCornerRadii* aCornerRadii,
|
||||
const gfxRGBA& aShadowColor,
|
||||
const bool& aHasBorderRadius,
|
||||
BackendType aBackendType)
|
||||
{
|
||||
BlurCacheKey key(aOuterMinSize, aInnerMinSize,
|
||||
aBlurRadius, aSpreadRadius,
|
||||
aCornerRadii, aShadowColor,
|
||||
true, aHasBorderRadius, aBackendType);
|
||||
BlurCacheData* blur = mHashEntries.Get(key);
|
||||
if (blur) {
|
||||
MarkUsed(blur);
|
||||
}
|
||||
|
||||
return blur;
|
||||
}
|
||||
|
||||
// Returns true if we successfully register the blur in the cache, false
|
||||
// otherwise.
|
||||
bool RegisterEntry(BlurCacheData* aValue)
|
||||
@ -431,7 +493,7 @@ CreateBlurMask(const IntSize& aRectSize,
|
||||
}
|
||||
|
||||
static already_AddRefed<SourceSurface>
|
||||
CreateBoxShadow(DrawTarget& aDT, SourceSurface* aBlurMask, const gfxRGBA& aShadowColor)
|
||||
CreateBoxShadow(SourceSurface* aBlurMask, const gfxRGBA& aShadowColor)
|
||||
{
|
||||
IntSize blurredSize = aBlurMask->GetSize();
|
||||
gfxPlatform* platform = gfxPlatform::GetPlatform();
|
||||
@ -447,7 +509,7 @@ CreateBoxShadow(DrawTarget& aDT, SourceSurface* aBlurMask, const gfxRGBA& aShado
|
||||
return boxShadowDT->Snapshot();
|
||||
}
|
||||
|
||||
SourceSurface*
|
||||
static SourceSurface*
|
||||
GetBlur(DrawTarget& aDT,
|
||||
const IntSize& aRectSize,
|
||||
const gfxIntSize& aBlurRadius,
|
||||
@ -480,7 +542,7 @@ GetBlur(DrawTarget& aDT,
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RefPtr<SourceSurface> boxShadow = CreateBoxShadow(aDT, blurMask, aShadowColor);
|
||||
RefPtr<SourceSurface> boxShadow = CreateBoxShadow(blurMask, aShadowColor);
|
||||
if (!boxShadow) {
|
||||
return nullptr;
|
||||
}
|
||||
@ -541,6 +603,68 @@ DrawCorner(DrawTarget& aDT, SourceSurface* aSurface,
|
||||
aDT.DrawSurface(aSurface, aDest, aSrc);
|
||||
}
|
||||
|
||||
static void
|
||||
DrawBoxShadows(DrawTarget& aDestDrawTarget, SourceSurface* aSourceBlur,
|
||||
Rect aDstOuter, Rect aDstInner, Rect aSrcOuter, Rect aSrcInner,
|
||||
Rect aSkipRect)
|
||||
{
|
||||
// Corners: top left, top right, bottom left, bottom right
|
||||
DrawCorner(aDestDrawTarget, aSourceBlur,
|
||||
RectWithEdgesTRBL(aDstOuter.Y(), aDstInner.X(),
|
||||
aDstInner.Y(), aDstOuter.X()),
|
||||
RectWithEdgesTRBL(aSrcOuter.Y(), aSrcInner.X(),
|
||||
aSrcInner.Y(), aSrcOuter.X()),
|
||||
aSkipRect);
|
||||
|
||||
DrawCorner(aDestDrawTarget, aSourceBlur,
|
||||
RectWithEdgesTRBL(aDstOuter.Y(), aDstOuter.XMost(),
|
||||
aDstInner.Y(), aDstInner.XMost()),
|
||||
RectWithEdgesTRBL(aSrcOuter.Y(), aSrcOuter.XMost(),
|
||||
aSrcInner.Y(), aSrcInner.XMost()),
|
||||
aSkipRect);
|
||||
|
||||
DrawCorner(aDestDrawTarget, aSourceBlur,
|
||||
RectWithEdgesTRBL(aDstInner.YMost(), aDstInner.X(),
|
||||
aDstOuter.YMost(), aDstOuter.X()),
|
||||
RectWithEdgesTRBL(aSrcInner.YMost(), aSrcInner.X(),
|
||||
aSrcOuter.YMost(), aSrcOuter.X()),
|
||||
aSkipRect);
|
||||
|
||||
DrawCorner(aDestDrawTarget, aSourceBlur,
|
||||
RectWithEdgesTRBL(aDstInner.YMost(), aDstOuter.XMost(),
|
||||
aDstOuter.YMost(), aDstInner.XMost()),
|
||||
RectWithEdgesTRBL(aSrcInner.YMost(), aSrcOuter.XMost(),
|
||||
aSrcOuter.YMost(), aSrcInner.XMost()),
|
||||
aSkipRect);
|
||||
|
||||
// Edges: top, left, right, bottom
|
||||
RepeatOrStretchSurface(aDestDrawTarget, aSourceBlur,
|
||||
RectWithEdgesTRBL(aDstOuter.Y(), aDstInner.XMost(),
|
||||
aDstInner.Y(), aDstInner.X()),
|
||||
RectWithEdgesTRBL(aSrcOuter.Y(), aSrcInner.XMost(),
|
||||
aSrcInner.Y(), aSrcInner.X()),
|
||||
aSkipRect);
|
||||
RepeatOrStretchSurface(aDestDrawTarget, aSourceBlur,
|
||||
RectWithEdgesTRBL(aDstInner.Y(), aDstInner.X(),
|
||||
aDstInner.YMost(), aDstOuter.X()),
|
||||
RectWithEdgesTRBL(aSrcInner.Y(), aSrcInner.X(),
|
||||
aSrcInner.YMost(), aSrcOuter.X()),
|
||||
aSkipRect);
|
||||
RepeatOrStretchSurface(aDestDrawTarget, aSourceBlur,
|
||||
RectWithEdgesTRBL(aDstInner.Y(), aDstOuter.XMost(),
|
||||
aDstInner.YMost(), aDstInner.XMost()),
|
||||
RectWithEdgesTRBL(aSrcInner.Y(), aSrcOuter.XMost(),
|
||||
aSrcInner.YMost(), aSrcInner.XMost()),
|
||||
aSkipRect);
|
||||
RepeatOrStretchSurface(aDestDrawTarget, aSourceBlur,
|
||||
RectWithEdgesTRBL(aDstInner.YMost(), aDstInner.XMost(),
|
||||
aDstOuter.YMost(), aDstInner.X()),
|
||||
RectWithEdgesTRBL(aSrcInner.YMost(), aSrcInner.XMost(),
|
||||
aSrcOuter.YMost(), aSrcInner.X()),
|
||||
aSkipRect);
|
||||
}
|
||||
|
||||
|
||||
/***
|
||||
* We draw a blurred a rectangle by only blurring a smaller rectangle and
|
||||
* splitting the rectangle into 9 parts.
|
||||
@ -598,60 +722,8 @@ gfxAlphaBoxBlur::BlurRectangle(gfxContext* aDestinationCtx,
|
||||
// The target rect is smaller than the minimal size so just draw the surface
|
||||
destDrawTarget.DrawSurface(boxShadow, dstInner, srcInner);
|
||||
} else {
|
||||
// Corners: top left, top right, bottom left, bottom right
|
||||
DrawCorner(destDrawTarget, boxShadow,
|
||||
RectWithEdgesTRBL(dstOuter.Y(), dstInner.X(),
|
||||
dstInner.Y(), dstOuter.X()),
|
||||
RectWithEdgesTRBL(srcOuter.Y(), srcInner.X(),
|
||||
srcInner.Y(), srcOuter.X()),
|
||||
skipRect);
|
||||
|
||||
DrawCorner(destDrawTarget, boxShadow,
|
||||
RectWithEdgesTRBL(dstOuter.Y(), dstOuter.XMost(),
|
||||
dstInner.Y(), dstInner.XMost()),
|
||||
RectWithEdgesTRBL(srcOuter.Y(), srcOuter.XMost(),
|
||||
srcInner.Y(), srcInner.XMost()),
|
||||
skipRect);
|
||||
|
||||
DrawCorner(destDrawTarget, boxShadow,
|
||||
RectWithEdgesTRBL(dstInner.YMost(), dstInner.X(),
|
||||
dstOuter.YMost(), dstOuter.X()),
|
||||
RectWithEdgesTRBL(srcInner.YMost(), srcInner.X(),
|
||||
srcOuter.YMost(), srcOuter.X()),
|
||||
skipRect);
|
||||
|
||||
DrawCorner(destDrawTarget, boxShadow,
|
||||
RectWithEdgesTRBL(dstInner.YMost(), dstOuter.XMost(),
|
||||
dstOuter.YMost(), dstInner.XMost()),
|
||||
RectWithEdgesTRBL(srcInner.YMost(), srcOuter.XMost(),
|
||||
srcOuter.YMost(), srcInner.XMost()),
|
||||
skipRect);
|
||||
|
||||
// Edges: top, left, right, bottom
|
||||
RepeatOrStretchSurface(destDrawTarget, boxShadow,
|
||||
RectWithEdgesTRBL(dstOuter.Y(), dstInner.XMost(),
|
||||
dstInner.Y(), dstInner.X()),
|
||||
RectWithEdgesTRBL(srcOuter.Y(), srcInner.XMost(),
|
||||
srcInner.Y(), srcInner.X()),
|
||||
skipRect);
|
||||
RepeatOrStretchSurface(destDrawTarget, boxShadow,
|
||||
RectWithEdgesTRBL(dstInner.Y(), dstInner.X(),
|
||||
dstInner.YMost(), dstOuter.X()),
|
||||
RectWithEdgesTRBL(srcInner.Y(), srcInner.X(),
|
||||
srcInner.YMost(), srcOuter.X()),
|
||||
skipRect);
|
||||
RepeatOrStretchSurface(destDrawTarget, boxShadow,
|
||||
RectWithEdgesTRBL(dstInner.Y(), dstOuter.XMost(),
|
||||
dstInner.YMost(), dstInner.XMost()),
|
||||
RectWithEdgesTRBL(srcInner.Y(), srcOuter.XMost(),
|
||||
srcInner.YMost(), srcInner.XMost()),
|
||||
skipRect);
|
||||
RepeatOrStretchSurface(destDrawTarget, boxShadow,
|
||||
RectWithEdgesTRBL(dstInner.YMost(), dstInner.XMost(),
|
||||
dstOuter.YMost(), dstInner.X()),
|
||||
RectWithEdgesTRBL(srcInner.YMost(), srcInner.XMost(),
|
||||
srcOuter.YMost(), srcInner.X()),
|
||||
skipRect);
|
||||
DrawBoxShadows(destDrawTarget, boxShadow, dstOuter, dstInner,
|
||||
srcOuter, srcInner, skipRect);
|
||||
|
||||
// Middle part
|
||||
RepeatOrStretchSurface(destDrawTarget, boxShadow,
|
||||
@ -682,3 +754,252 @@ gfxAlphaBoxBlur::BlurRectangle(gfxContext* aDestinationCtx,
|
||||
destDrawTarget.PopClip();
|
||||
}
|
||||
|
||||
static already_AddRefed<Path>
|
||||
GetBoxShadowInsetPath(DrawTarget* aDrawTarget,
|
||||
const Rect aOuterRect, const Rect aInnerRect,
|
||||
const bool aHasBorderRadius, const RectCornerRadii& aInnerClipRadii)
|
||||
{
|
||||
/***
|
||||
* We create an inset path by having two rects.
|
||||
*
|
||||
* -----------------------
|
||||
* | ________________ |
|
||||
* | | | |
|
||||
* | | | |
|
||||
* | ------------------ |
|
||||
* |_____________________|
|
||||
*
|
||||
* The outer rect and the inside rect. The path
|
||||
* creates a frame around the content where we draw the inset shadow.
|
||||
*/
|
||||
RefPtr<PathBuilder> builder =
|
||||
aDrawTarget->CreatePathBuilder(FillRule::FILL_EVEN_ODD);
|
||||
AppendRectToPath(builder, aOuterRect, true);
|
||||
|
||||
if (aHasBorderRadius) {
|
||||
AppendRoundedRectToPath(builder, aInnerRect, aInnerClipRadii, false);
|
||||
} else {
|
||||
AppendRectToPath(builder, aInnerRect, false);
|
||||
}
|
||||
return builder->Finish();
|
||||
}
|
||||
|
||||
static void
|
||||
ComputeRectsForInsetBoxShadow(gfxIntSize aBlurRadius,
|
||||
gfxIntSize aSpreadRadius,
|
||||
Rect& aOutOuterRect,
|
||||
Rect& aOutInnerRect,
|
||||
Margin& aOutPathMargins,
|
||||
const Rect& aDestRect,
|
||||
const Rect& aShadowClipRect)
|
||||
{
|
||||
gfxIntSize marginSize = aBlurRadius + aSpreadRadius;
|
||||
aOutPathMargins.SizeTo(marginSize.height, marginSize.width, marginSize.height, marginSize.width);
|
||||
aOutPathMargins += aOutPathMargins;
|
||||
|
||||
aOutOuterRect.x = 0;
|
||||
aOutInnerRect.x = marginSize.width;
|
||||
|
||||
aOutOuterRect.y = 0;
|
||||
aOutInnerRect.y = marginSize.height;
|
||||
|
||||
// + 1 for the middle edges so we can sample them
|
||||
aOutInnerRect.width = aOutPathMargins.LeftRight() + 1;
|
||||
aOutInnerRect.height = aOutPathMargins.TopBottom() + 1;
|
||||
|
||||
// The outer path rect needs to be 1 blur radius past the inner edges
|
||||
aOutOuterRect.width = aOutInnerRect.XMost() + marginSize.width;
|
||||
aOutOuterRect.height = aOutInnerRect.YMost() + marginSize.height;
|
||||
|
||||
if ((aOutOuterRect.width >= aDestRect.width) ||
|
||||
(aOutOuterRect.height >= aDestRect.height) ||
|
||||
(aOutInnerRect.width >= aShadowClipRect.width) ||
|
||||
(aOutInnerRect.height >= aShadowClipRect.height))
|
||||
{
|
||||
aOutOuterRect.width = aDestRect.width;
|
||||
aOutOuterRect.height = aDestRect.height;
|
||||
aOutInnerRect.width = aShadowClipRect.width;
|
||||
aOutInnerRect.height = aShadowClipRect.height;
|
||||
aOutPathMargins.SizeTo(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
FillDestinationPath(gfxContext* aDestinationCtx,
|
||||
const Rect aDestinationRect,
|
||||
const Rect aShadowClipRect,
|
||||
const Color& aShadowColor,
|
||||
const bool aHasBorderRadius,
|
||||
const RectCornerRadii& aInnerClipRadii)
|
||||
{
|
||||
// When there is no blur radius, fill the path onto the destination
|
||||
// surface.
|
||||
aDestinationCtx->SetColor(ThebesColor(aShadowColor));
|
||||
DrawTarget* destDrawTarget = aDestinationCtx->GetDrawTarget();
|
||||
RefPtr<Path> shadowPath = GetBoxShadowInsetPath(destDrawTarget, aDestinationRect,
|
||||
aShadowClipRect, aHasBorderRadius,
|
||||
aInnerClipRadii);
|
||||
|
||||
aDestinationCtx->SetPath(shadowPath);
|
||||
aDestinationCtx->Fill();
|
||||
}
|
||||
|
||||
void
|
||||
CacheInsetBlur(const IntSize aMinOuterSize,
|
||||
const IntSize aMinInnerSize,
|
||||
const gfxIntSize& aBlurRadius,
|
||||
const gfxIntSize& aSpreadRadius,
|
||||
const RectCornerRadii* aCornerRadii,
|
||||
const gfxRGBA& aShadowColor,
|
||||
const bool& aHasBorderRadius,
|
||||
BackendType aBackendType,
|
||||
IntMargin aExtendBy,
|
||||
SourceSurface* aBoxShadow)
|
||||
{
|
||||
BlurCacheKey key(aMinOuterSize, aMinInnerSize,
|
||||
aBlurRadius, aSpreadRadius,
|
||||
aCornerRadii, aShadowColor,
|
||||
true, aHasBorderRadius, aBackendType);
|
||||
BlurCacheData* data = new BlurCacheData(aBoxShadow, aExtendBy, key);
|
||||
if (!gBlurCache->RegisterEntry(data)) {
|
||||
delete data;
|
||||
}
|
||||
}
|
||||
|
||||
already_AddRefed<mozilla::gfx::SourceSurface>
|
||||
gfxAlphaBoxBlur::GetInsetBlur(Rect& aOuterRect,
|
||||
Rect& aInnerRect,
|
||||
const gfxIntSize& aBlurRadius,
|
||||
const gfxIntSize& aSpreadRadius,
|
||||
const RectCornerRadii& aInnerClipRadii,
|
||||
const Color& aShadowColor,
|
||||
const bool& aHasBorderRadius,
|
||||
IntPoint& aOutTopLeft,
|
||||
gfxContext* aDestinationCtx)
|
||||
|
||||
{
|
||||
if (!gBlurCache) {
|
||||
gBlurCache = new BlurCache();
|
||||
}
|
||||
|
||||
gfxIntSize outerRectSize = RoundedToInt(aOuterRect).Size();
|
||||
gfxIntSize innerRectSize = RoundedToInt(aInnerRect).Size();
|
||||
DrawTarget* destDrawTarget = aDestinationCtx->GetDrawTarget();
|
||||
|
||||
BlurCacheData* cached =
|
||||
gBlurCache->LookupInsetBoxShadow(outerRectSize, innerRectSize, aBlurRadius, aSpreadRadius,
|
||||
&aInnerClipRadii, ThebesColor(aShadowColor),
|
||||
aHasBorderRadius, destDrawTarget->GetBackendType());
|
||||
|
||||
if (cached) {
|
||||
IntMargin extends = cached->mExtendDest;
|
||||
aOutTopLeft.x = extends.left;
|
||||
aOutTopLeft.y = extends.top;
|
||||
// So we don't forget the actual cached blur
|
||||
RefPtr<SourceSurface> cachedBlur = cached->mBlur;
|
||||
return cachedBlur.forget();
|
||||
}
|
||||
|
||||
// Dirty rect and skip rect are null for the min inset shadow.
|
||||
// When rendering inset box shadows, we respect the spread radius by changing
|
||||
// the shape of the unblurred shadow, and can pass a spread radius of zero here.
|
||||
gfxIntSize zeroSpread(0, 0);
|
||||
gfxContext* minGfxContext = Init(ThebesRect(aOuterRect), zeroSpread, aBlurRadius, nullptr, nullptr);
|
||||
if (!minGfxContext) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
DrawTarget* minDrawTarget = minGfxContext->GetDrawTarget();
|
||||
RefPtr<Path> maskPath = GetBoxShadowInsetPath(minDrawTarget, aOuterRect,
|
||||
aInnerRect, aHasBorderRadius,
|
||||
aInnerClipRadii);
|
||||
|
||||
minGfxContext->SetColor(ThebesColor(aShadowColor));
|
||||
minGfxContext->SetPath(maskPath);
|
||||
minGfxContext->Fill();
|
||||
|
||||
RefPtr<SourceSurface> minMask = DoBlur(minDrawTarget, &aOutTopLeft);
|
||||
if (!minMask) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
RefPtr<SourceSurface> minInsetBlur = CreateBoxShadow(minMask, ThebesColor(aShadowColor));
|
||||
if (!minInsetBlur) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
IntMargin extendBy(aOutTopLeft.y, 0, 0, aOutTopLeft.x);
|
||||
CacheInsetBlur(outerRectSize, innerRectSize,
|
||||
aBlurRadius, aSpreadRadius,
|
||||
&aInnerClipRadii, ThebesColor(aShadowColor),
|
||||
aHasBorderRadius, destDrawTarget->GetBackendType(),
|
||||
extendBy, minInsetBlur);
|
||||
return minInsetBlur.forget();
|
||||
}
|
||||
|
||||
/***
|
||||
* Blur an inset box shadow by doing:
|
||||
* 1) Create a minimal box shadow path that creates a frame.
|
||||
* 2) Draw the box shadow portion over the destination surface.
|
||||
* 3) The "inset" part is created by a clip rect that properly clips
|
||||
* the alpha mask so that it has clean edges. We still create the full
|
||||
* proper alpha mask, but let the clip deal with the clean edges.
|
||||
*
|
||||
* All parameters should already be in device pixels.
|
||||
*/
|
||||
void
|
||||
gfxAlphaBoxBlur::BlurInsetBox(gfxContext* aDestinationCtx,
|
||||
const Rect aDestinationRect,
|
||||
const Rect aShadowClipRect,
|
||||
const gfxIntSize aBlurRadius,
|
||||
const gfxIntSize aSpreadRadius,
|
||||
const Color& aShadowColor,
|
||||
bool aHasBorderRadius,
|
||||
const RectCornerRadii& aInnerClipRadii,
|
||||
const Rect aSkipRect)
|
||||
{
|
||||
// Blur inset shadows ALWAYS have a 0 spread radius.
|
||||
if ((aBlurRadius.width <= 0 && aBlurRadius.height <= 0)) {
|
||||
FillDestinationPath(aDestinationCtx, aDestinationRect, aShadowClipRect,
|
||||
aShadowColor, aHasBorderRadius, aInnerClipRadii);
|
||||
return;
|
||||
}
|
||||
|
||||
DrawTarget* destDrawTarget = aDestinationCtx->GetDrawTarget();
|
||||
Rect outerRect;
|
||||
Rect innerRect;
|
||||
Margin pathMargins;
|
||||
ComputeRectsForInsetBoxShadow(aBlurRadius, aSpreadRadius,
|
||||
outerRect, innerRect,
|
||||
pathMargins,
|
||||
aDestinationRect,
|
||||
aShadowClipRect);
|
||||
|
||||
IntPoint topLeft;
|
||||
RefPtr<SourceSurface> minInsetBlur = GetInsetBlur(outerRect, innerRect,
|
||||
aBlurRadius, aSpreadRadius,
|
||||
aInnerClipRadii, aShadowColor,
|
||||
aHasBorderRadius,
|
||||
topLeft, aDestinationCtx);
|
||||
if (!minInsetBlur) {
|
||||
return;
|
||||
}
|
||||
|
||||
Rect destRectOuter(aDestinationRect);
|
||||
Rect destRectInner(destRectOuter);
|
||||
destRectInner.Deflate(pathMargins);
|
||||
|
||||
Rect srcRectOuter(outerRect);
|
||||
srcRectOuter.MoveBy(abs(topLeft.x), abs(topLeft.y));
|
||||
Rect srcRectInner(srcRectOuter);
|
||||
srcRectInner.Deflate(pathMargins);
|
||||
|
||||
if (srcRectOuter.IsEqualInterior(srcRectInner)) {
|
||||
destDrawTarget->DrawSurface(minInsetBlur, destRectOuter, srcRectOuter);
|
||||
} else {
|
||||
DrawBoxShadows(*destDrawTarget, minInsetBlur,
|
||||
destRectOuter, destRectInner,
|
||||
srcRectOuter, srcRectInner,
|
||||
aSkipRect);
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ struct gfxRGBA;
|
||||
namespace mozilla {
|
||||
namespace gfx {
|
||||
class AlphaBoxBlur;
|
||||
struct Color;
|
||||
struct RectCornerRadii;
|
||||
class SourceSurface;
|
||||
class DrawTarget;
|
||||
@ -135,9 +136,44 @@ public:
|
||||
|
||||
static void ShutdownBlurCache();
|
||||
|
||||
|
||||
/***
|
||||
* Blurs an inset box shadow according to a given path.
|
||||
* This is equivalent to calling Init(), drawing the inset path,
|
||||
* and calling paint. Do not call Init() if using this method.
|
||||
*
|
||||
* @param aDestinationCtx The destination to blur to.
|
||||
* @param aDestinationRect The destination rect in device pixels
|
||||
* @param aShadowClipRect The destiniation inner rect of the
|
||||
* inset path in device pixels.
|
||||
* @param aBlurRadius The standard deviation of the blur.
|
||||
* @param aSpreadRadius The spread radius in device pixels.
|
||||
* @param aShadowColor The color of the blur.
|
||||
* @param aHasBorderRadius If this element also has a border radius
|
||||
* @param aInnerClipRadii Corner radii for the inside rect if it is a rounded rect.
|
||||
* @param aSKipRect An area in device pixels we don't have to paint in.
|
||||
*/
|
||||
void BlurInsetBox(gfxContext* aDestinationCtx,
|
||||
const mozilla::gfx::Rect aDestinationRect,
|
||||
const mozilla::gfx::Rect aShadowClipRect,
|
||||
const gfxIntSize aBlurRadius,
|
||||
const gfxIntSize aSpreadRadius,
|
||||
const mozilla::gfx::Color& aShadowColor,
|
||||
const bool aHasBorderRadius,
|
||||
const RectCornerRadii& aInnerClipRadii,
|
||||
const mozilla::gfx::Rect aSkipRect);
|
||||
|
||||
protected:
|
||||
already_AddRefed<mozilla::gfx::SourceSurface>
|
||||
GetInsetBlur(mozilla::gfx::Rect& aOuterRect,
|
||||
mozilla::gfx::Rect& aInnerRect,
|
||||
const gfxIntSize& aBlurRadius,
|
||||
const gfxIntSize& aSpreadRadius,
|
||||
const RectCornerRadii& aInnerClipRadii,
|
||||
const mozilla::gfx::Color& aShadowColor,
|
||||
const bool& aHasBorderRadius,
|
||||
mozilla::gfx::IntPoint& aOutTopLeft,
|
||||
gfxContext* aDestinationCtx);
|
||||
|
||||
/**
|
||||
* The context of the temporary alpha surface.
|
||||
*/
|
||||
|
@ -1581,29 +1581,22 @@ nsCSSRendering::PaintBoxShadowInner(nsPresContext* aPresContext,
|
||||
gfxContext* renderContext = aRenderingContext.ThebesContext();
|
||||
DrawTarget* drawTarget = renderContext->GetDrawTarget();
|
||||
nsContextBoxBlur blurringArea;
|
||||
gfxContext* shadowContext =
|
||||
blurringArea.Init(shadowPaintRect, 0, blurRadius, twipsPerPixel,
|
||||
renderContext, aDirtyRect, &skipGfxRect);
|
||||
if (!shadowContext)
|
||||
continue;
|
||||
DrawTarget* shadowDT = shadowContext->GetDrawTarget();
|
||||
|
||||
// shadowContext is owned by either blurringArea or aRenderingContext.
|
||||
MOZ_ASSERT(shadowContext == renderContext ||
|
||||
shadowContext == blurringArea.GetContext());
|
||||
|
||||
// Set the shadow color; if not specified, use the foreground color
|
||||
Color shadowColor = Color::FromABGR(shadowItem->mHasColor ?
|
||||
shadowItem->mColor :
|
||||
aForFrame->StyleColor()->mColor);
|
||||
renderContext->Save();
|
||||
renderContext->SetColor(ThebesColor(shadowColor));
|
||||
|
||||
// Clip the context to the area of the frame's padding rect, so no part of the
|
||||
// shadow is painted outside. Also cut out anything beyond where the inset shadow
|
||||
// will be.
|
||||
Rect shadowGfxRect = NSRectToRect(paddingRect, twipsPerPixel);
|
||||
shadowGfxRect.Round();
|
||||
|
||||
// Set the shadow color; if not specified, use the foreground color
|
||||
Color shadowColor = Color::FromABGR(shadowItem->mHasColor ?
|
||||
shadowItem->mColor :
|
||||
aForFrame->StyleColor()->mColor);
|
||||
|
||||
renderContext->Save();
|
||||
|
||||
// This clips the outside border radius.
|
||||
// clipRectRadii is the border radius inside the inset shadow.
|
||||
if (hasBorderRadius) {
|
||||
RefPtr<Path> roundedRect =
|
||||
MakePathForRoundedRect(*drawTarget, shadowGfxRect, innerRadii);
|
||||
@ -1612,22 +1605,13 @@ nsCSSRendering::PaintBoxShadowInner(nsPresContext* aPresContext,
|
||||
renderContext->Clip(shadowGfxRect);
|
||||
}
|
||||
|
||||
// Fill the surface minus the area within the frame that we should
|
||||
// not paint in, and blur and apply it.
|
||||
RefPtr<PathBuilder> builder =
|
||||
shadowDT->CreatePathBuilder(FillRule::FILL_EVEN_ODD);
|
||||
AppendRectToPath(builder, shadowPaintGfxRect, true);
|
||||
if (hasBorderRadius) {
|
||||
AppendRoundedRectToPath(builder, shadowClipGfxRect, clipRectRadii, false);
|
||||
} else {
|
||||
AppendRectToPath(builder, shadowClipGfxRect, false);
|
||||
}
|
||||
RefPtr<Path> path = builder->Finish();
|
||||
shadowContext->SetPath(path);
|
||||
shadowContext->Fill();
|
||||
shadowContext->NewPath();
|
||||
|
||||
blurringArea.DoPaint();
|
||||
nsContextBoxBlur insetBoxBlur;
|
||||
gfxRect destRect = nsLayoutUtils::RectToGfxRect(shadowPaintRect, twipsPerPixel);
|
||||
insetBoxBlur.InsetBoxBlur(renderContext, ToRect(destRect),
|
||||
shadowClipGfxRect, shadowColor,
|
||||
blurRadius, spreadDistanceAppUnits,
|
||||
twipsPerPixel, hasBorderRadius,
|
||||
clipRectRadii, ToRect(skipGfxRect));
|
||||
renderContext->Restore();
|
||||
}
|
||||
}
|
||||
@ -5322,27 +5306,12 @@ nsContextBoxBlur::Init(const nsRect& aRect, nscoord aSpreadRadius,
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
gfxFloat scaleX = 1;
|
||||
gfxFloat scaleY = 1;
|
||||
gfxIntSize blurRadius;
|
||||
gfxIntSize spreadRadius;
|
||||
GetBlurAndSpreadRadius(aDestinationCtx, aAppUnitsPerDevPixel,
|
||||
aBlurRadius, aSpreadRadius,
|
||||
blurRadius, spreadRadius);
|
||||
|
||||
// Do blurs in device space when possible.
|
||||
// Chrome/Skia always does the blurs in device space
|
||||
// and will sometimes get incorrect results (e.g. rotated blurs)
|
||||
gfxMatrix transform = aDestinationCtx->CurrentMatrix();
|
||||
// XXX: we could probably handle negative scales but for now it's easier just to fallback
|
||||
if (transform.HasNonAxisAlignedTransform() || transform._11 <= 0.0 || transform._22 <= 0.0) {
|
||||
transform = gfxMatrix();
|
||||
} else {
|
||||
scaleX = transform._11;
|
||||
scaleY = transform._22;
|
||||
}
|
||||
|
||||
// compute a large or smaller blur radius
|
||||
gfxIntSize blurRadius = ComputeBlurRadius(aBlurRadius, aAppUnitsPerDevPixel, scaleX, scaleY);
|
||||
gfxIntSize spreadRadius = gfxIntSize(std::min(int32_t(aSpreadRadius * scaleX / aAppUnitsPerDevPixel),
|
||||
int32_t(MAX_SPREAD_RADIUS)),
|
||||
std::min(int32_t(aSpreadRadius * scaleY / aAppUnitsPerDevPixel),
|
||||
int32_t(MAX_SPREAD_RADIUS)));
|
||||
mDestinationCtx = aDestinationCtx;
|
||||
|
||||
// If not blurring, draw directly onto the destination device
|
||||
@ -5360,6 +5329,7 @@ nsContextBoxBlur::Init(const nsRect& aRect, nscoord aSpreadRadius,
|
||||
nsLayoutUtils::RectToGfxRect(aDirtyRect, aAppUnitsPerDevPixel);
|
||||
dirtyRect.RoundOut();
|
||||
|
||||
gfxMatrix transform = aDestinationCtx->CurrentMatrix();
|
||||
rect = transform.TransformBounds(rect);
|
||||
|
||||
mPreTransformed = !transform.IsIdentity();
|
||||
@ -5386,8 +5356,9 @@ nsContextBoxBlur::Init(const nsRect& aRect, nscoord aSpreadRadius,
|
||||
void
|
||||
nsContextBoxBlur::DoPaint()
|
||||
{
|
||||
if (mContext == mDestinationCtx)
|
||||
if (mContext == mDestinationCtx) {
|
||||
return;
|
||||
}
|
||||
|
||||
gfxContextMatrixAutoSaveRestore saveMatrix(mDestinationCtx);
|
||||
|
||||
@ -5485,3 +5456,99 @@ nsContextBoxBlur::BlurRectangle(gfxContext* aDestinationCtx,
|
||||
dirtyRect,
|
||||
skipRect);
|
||||
}
|
||||
|
||||
/* static */ void
|
||||
nsContextBoxBlur::GetBlurAndSpreadRadius(gfxContext* aDestinationCtx,
|
||||
int32_t aAppUnitsPerDevPixel,
|
||||
nscoord aBlurRadius,
|
||||
nscoord aSpreadRadius,
|
||||
gfxIntSize& aOutBlurRadius,
|
||||
gfxIntSize& aOutSpreadRadius,
|
||||
bool aConstrainSpreadRadius)
|
||||
{
|
||||
gfxFloat scaleX = 1;
|
||||
gfxFloat scaleY = 1;
|
||||
|
||||
// Do blurs in device space when possible.
|
||||
// Chrome/Skia always does the blurs in device space
|
||||
// and will sometimes get incorrect results (e.g. rotated blurs)
|
||||
gfxMatrix transform = aDestinationCtx->CurrentMatrix();
|
||||
// XXX: we could probably handle negative scales but for now it's easier just to fallback
|
||||
if (transform.HasNonAxisAlignedTransform() || transform._11 <= 0.0 || transform._22 <= 0.0) {
|
||||
transform = gfxMatrix();
|
||||
} else {
|
||||
scaleX = transform._11;
|
||||
scaleY = transform._22;
|
||||
}
|
||||
|
||||
// compute a large or smaller blur radius
|
||||
aOutBlurRadius = ComputeBlurRadius(aBlurRadius, aAppUnitsPerDevPixel, scaleX, scaleY);
|
||||
aOutSpreadRadius =
|
||||
gfxIntSize(int32_t(aSpreadRadius * scaleX / aAppUnitsPerDevPixel),
|
||||
int32_t(aSpreadRadius * scaleY / aAppUnitsPerDevPixel));
|
||||
|
||||
|
||||
if (aConstrainSpreadRadius) {
|
||||
aOutSpreadRadius.width = std::min(aOutSpreadRadius.width, int32_t(MAX_SPREAD_RADIUS));
|
||||
aOutSpreadRadius.height = std::min(aOutSpreadRadius.height, int32_t(MAX_SPREAD_RADIUS));
|
||||
}
|
||||
}
|
||||
|
||||
/* static */ bool
|
||||
nsContextBoxBlur::InsetBoxBlur(gfxContext* aDestinationCtx,
|
||||
Rect aDestinationRect,
|
||||
Rect aShadowClipRect,
|
||||
Color& aShadowColor,
|
||||
nscoord aBlurRadiusAppUnits,
|
||||
nscoord aSpreadDistanceAppUnits,
|
||||
int32_t aAppUnitsPerDevPixel,
|
||||
bool aHasBorderRadius,
|
||||
RectCornerRadii& aInnerClipRectRadii,
|
||||
Rect aSkipRect)
|
||||
{
|
||||
if (aDestinationRect.IsEmpty()) {
|
||||
mContext = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
gfxIntSize blurRadius;
|
||||
gfxIntSize spreadRadius;
|
||||
// Convert the blur and spread radius to device pixels
|
||||
bool constrainSpreadRadius = false;
|
||||
GetBlurAndSpreadRadius(aDestinationCtx, aAppUnitsPerDevPixel,
|
||||
aBlurRadiusAppUnits, aSpreadDistanceAppUnits,
|
||||
blurRadius, spreadRadius, constrainSpreadRadius);
|
||||
|
||||
// The blur and spread radius are scaled already, so scale all
|
||||
// input data to the blur. This way, we don't have to scale the min
|
||||
// inset blur to the invert of the dest context, then rescale it back
|
||||
// when we draw to the destination surface.
|
||||
gfxSize scale = aDestinationCtx->CurrentMatrix().ScaleFactors(true);
|
||||
Matrix currentMatrix = ToMatrix(aDestinationCtx->CurrentMatrix());
|
||||
|
||||
Rect transformedDestRect = currentMatrix.TransformBounds(aDestinationRect);
|
||||
Rect transformedShadowClipRect = currentMatrix.TransformBounds(aShadowClipRect);
|
||||
Rect transformedSkipRect = currentMatrix.TransformBounds(aSkipRect);
|
||||
|
||||
transformedDestRect.Round();
|
||||
transformedShadowClipRect.Round();
|
||||
transformedSkipRect.RoundIn();
|
||||
|
||||
for (size_t i = 0; i < 4; i++) {
|
||||
aInnerClipRectRadii[i].width = std::floor(scale.width * aInnerClipRectRadii[i].width);
|
||||
aInnerClipRectRadii[i].height = std::floor(scale.height * aInnerClipRectRadii[i].height);
|
||||
}
|
||||
|
||||
{
|
||||
gfxContextAutoSaveRestore autoRestore(aDestinationCtx);
|
||||
aDestinationCtx->SetMatrix(gfxMatrix());
|
||||
|
||||
mAlphaBoxBlur.BlurInsetBox(aDestinationCtx, transformedDestRect,
|
||||
transformedShadowClipRect,
|
||||
blurRadius, spreadRadius,
|
||||
aShadowColor,
|
||||
aHasBorderRadius,
|
||||
aInnerClipRectRadii, transformedSkipRect);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ class nsRenderingContext;
|
||||
namespace mozilla {
|
||||
|
||||
namespace gfx {
|
||||
struct Color;
|
||||
class DrawTarget;
|
||||
} // namespace gfx
|
||||
|
||||
@ -944,7 +945,48 @@ public:
|
||||
const nsRect& aDirtyRect,
|
||||
const gfxRect& aSkipRect);
|
||||
|
||||
/**
|
||||
* Draws a blurred inset box shadow shape onto the destination surface.
|
||||
* Like BlurRectangle, this is equivalent to calling Init(),
|
||||
* drawing a rectangle onto the returned surface
|
||||
* and then calling DoPaint, but may let us optimize better in the
|
||||
* backend.
|
||||
*
|
||||
* @param aDestinationCtx The destination to blur to.
|
||||
* @param aDestinationRect The rectangle to blur in app units.
|
||||
* @param aShadowClipRect The inside clip rect that creates the path.
|
||||
* @param aShadowColor The color of the blur
|
||||
* @param aBlurRadiusAppUnits The blur radius in app units
|
||||
* @param aSpreadRadiusAppUnits The spread radius in app units.
|
||||
* @param aAppUnitsPerDevPixel The number of app units in a device pixel,
|
||||
* for conversion. Most of the time you'll
|
||||
* pass this from the current PresContext if
|
||||
* available.
|
||||
* @param aHasBorderRadius If this inset box blur has a border radius
|
||||
* @param aInnerClipRectRadii The clip rect radii used for the inside rect's path.
|
||||
* @param aSkipRect An area in device pixels (NOT app units!) to avoid
|
||||
* blurring over, to prevent unnecessary work.
|
||||
*/
|
||||
bool InsetBoxBlur(gfxContext* aDestinationCtx,
|
||||
mozilla::gfx::Rect aDestinationRect,
|
||||
mozilla::gfx::Rect aShadowClipRect,
|
||||
mozilla::gfx::Color& aShadowColor,
|
||||
nscoord aBlurRadiusAppUnits,
|
||||
nscoord aSpreadRadiusAppUnits,
|
||||
int32_t aAppUnitsPerDevPixel,
|
||||
bool aHasBorderRadius,
|
||||
RectCornerRadii& aInnerClipRectRadii,
|
||||
mozilla::gfx::Rect aSkipRect);
|
||||
|
||||
protected:
|
||||
static void GetBlurAndSpreadRadius(gfxContext* aContext,
|
||||
int32_t aAppUnitsPerDevPixel,
|
||||
nscoord aBlurRadius,
|
||||
nscoord aSpreadRadius,
|
||||
gfxIntSize& aOutBlurRadius,
|
||||
gfxIntSize& aOutSpreadRadius,
|
||||
bool aConstrainSpreadRadius = true);
|
||||
|
||||
gfxAlphaBoxBlur mAlphaBoxBlur;
|
||||
nsRefPtr<gfxContext> mContext;
|
||||
gfxContext* mDestinationCtx;
|
||||
@ -952,7 +994,6 @@ protected:
|
||||
/* This is true if the blur already has it's content transformed
|
||||
* by mDestinationCtx's transform */
|
||||
bool mPreTransformed;
|
||||
|
||||
};
|
||||
|
||||
#endif /* nsCSSRendering_h___ */
|
||||
|
@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<style>
|
||||
#outerDiv {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: lime;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
#middleBlur {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
margin-left: 100px;
|
||||
margin-top: 100px;
|
||||
background: black;
|
||||
box-shadow: inset 0 0 20px 100px lime;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="outerDiv">
|
||||
<div id="middleBlur">
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<style>
|
||||
#thediv {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: black;
|
||||
box-shadow: inset 0 0 20px 200px lime;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="thediv"></div>
|
@ -21,6 +21,7 @@ random-if(d2d) == boxshadow-threecorners.html boxshadow-threecorners-ref.html
|
||||
== boxshadow-skiprect.html boxshadow-skiprect-ref.html
|
||||
== boxshadow-opacity.html boxshadow-opacity-ref.html
|
||||
== boxshadow-color-rounding.html boxshadow-color-rounding-ref.html
|
||||
== boxshadow-color-rounding-middle.html boxshadow-color-rounding-middle-ref.html
|
||||
|
||||
== overflow-not-scrollable-1.html overflow-not-scrollable-1-ref.html
|
||||
== overflow-not-scrollable-1.html overflow-not-scrollable-1-ref2.html
|
||||
|
Loading…
Reference in New Issue
Block a user