/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* utility functions for drawing borders and backgrounds */ #include "nsCSSRendering.h" #include #include "gfx2DGlue.h" #include "gfxContext.h" #include "mozilla/ArrayUtils.h" #include "mozilla/ComputedStyle.h" #include "mozilla/DebugOnly.h" #include "mozilla/StaticPrefs_layout.h" #include "mozilla/gfx/2D.h" #include "mozilla/gfx/Helpers.h" #include "mozilla/gfx/Logging.h" #include "mozilla/gfx/PathHelpers.h" #include "mozilla/HashFunctions.h" #include "mozilla/MathAlgorithms.h" #include "mozilla/PresShell.h" #include "mozilla/StaticPtr.h" #include "mozilla/SVGImageContext.h" #include "gfxFont.h" #include "ScaledFontBase.h" #include "skia/include/core/SkTextBlob.h" #include "BorderConsts.h" #include "nsCanvasFrame.h" #include "nsStyleConsts.h" #include "nsPresContext.h" #include "nsIFrame.h" #include "nsIFrameInlines.h" #include "nsPageSequenceFrame.h" #include "nsPoint.h" #include "nsRect.h" #include "nsFrameManager.h" #include "nsGkAtoms.h" #include "nsCSSAnonBoxes.h" #include "nsIContent.h" #include "mozilla/dom/DocumentInlines.h" #include "nsIScrollableFrame.h" #include "imgIContainer.h" #include "ImageOps.h" #include "nsCSSColorUtils.h" #include "nsITheme.h" #include "nsLayoutUtils.h" #include "nsBlockFrame.h" #include "nsStyleStructInlines.h" #include "nsCSSFrameConstructor.h" #include "nsCSSProps.h" #include "nsContentUtils.h" #include "gfxDrawable.h" #include "nsCSSRenderingBorders.h" #include "mozilla/css/ImageLoader.h" #include "ImageContainer.h" #include "mozilla/ProfilerLabels.h" #include "mozilla/StaticPrefs_layout.h" #include "mozilla/Telemetry.h" #include "gfxUtils.h" #include "gfxGradientCache.h" #include "nsInlineFrame.h" #include "nsRubyTextContainerFrame.h" #include #include "TextDrawTarget.h" using namespace mozilla; using namespace mozilla::css; using namespace mozilla::gfx; using namespace mozilla::image; using mozilla::CSSSizeOrRatio; using mozilla::dom::Document; static int gFrameTreeLockCount = 0; // To avoid storing this data on nsInlineFrame (bloat) and to avoid // recalculating this for each frame in a continuation (perf), hold // a cache of various coordinate information that we need in order // to paint inline backgrounds. struct InlineBackgroundData { InlineBackgroundData() : mFrame(nullptr), mLineContainer(nullptr), mContinuationPoint(0), mUnbrokenMeasure(0), mLineContinuationPoint(0), mPIStartBorderData{}, mBidiEnabled(false), mVertical(false) {} ~InlineBackgroundData() = default; void Reset() { mBoundingBox.SetRect(0, 0, 0, 0); mContinuationPoint = mLineContinuationPoint = mUnbrokenMeasure = 0; mFrame = mLineContainer = nullptr; mPIStartBorderData.Reset(); } /** * Return a continuous rect for (an inline) aFrame relative to the * continuation that draws the left-most part of the background. * This is used when painting backgrounds. */ nsRect GetContinuousRect(nsIFrame* aFrame) { MOZ_ASSERT(static_cast(do_QueryFrame(aFrame))); SetFrame(aFrame); nscoord pos; // an x coordinate if writing-mode is horizontal; // y coordinate if vertical if (mBidiEnabled) { pos = mLineContinuationPoint; // Scan continuations on the same line as aFrame and accumulate the widths // of frames that are to the left (if this is an LTR block) or right // (if it's RTL) of the current one. bool isRtlBlock = (mLineContainer->StyleVisibility()->mDirection == StyleDirection::Rtl); nscoord curOffset = mVertical ? aFrame->GetOffsetTo(mLineContainer).y : aFrame->GetOffsetTo(mLineContainer).x; // If the continuation is fluid we know inlineFrame is not on the same // line. If it's not fluid, we need to test further to be sure. nsIFrame* inlineFrame = aFrame->GetPrevContinuation(); while (inlineFrame && !inlineFrame->GetNextInFlow() && AreOnSameLine(aFrame, inlineFrame)) { nscoord frameOffset = mVertical ? inlineFrame->GetOffsetTo(mLineContainer).y : inlineFrame->GetOffsetTo(mLineContainer).x; if (isRtlBlock == (frameOffset >= curOffset)) { pos += mVertical ? inlineFrame->GetSize().height : inlineFrame->GetSize().width; } inlineFrame = inlineFrame->GetPrevContinuation(); } inlineFrame = aFrame->GetNextContinuation(); while (inlineFrame && !inlineFrame->GetPrevInFlow() && AreOnSameLine(aFrame, inlineFrame)) { nscoord frameOffset = mVertical ? inlineFrame->GetOffsetTo(mLineContainer).y : inlineFrame->GetOffsetTo(mLineContainer).x; if (isRtlBlock == (frameOffset >= curOffset)) { pos += mVertical ? inlineFrame->GetSize().height : inlineFrame->GetSize().width; } inlineFrame = inlineFrame->GetNextContinuation(); } if (isRtlBlock) { // aFrame itself is also to the right of its left edge, so add its // width. pos += mVertical ? aFrame->GetSize().height : aFrame->GetSize().width; // pos is now the distance from the left [top] edge of aFrame to the // right [bottom] edge of the unbroken content. Change it to indicate // the distance from the left [top] edge of the unbroken content to the // left [top] edge of aFrame. pos = mUnbrokenMeasure - pos; } } else { pos = mContinuationPoint; } // Assume background-origin: border and return a rect with offsets // relative to (0,0). If we have a different background-origin, // then our rect should be deflated appropriately by our caller. return mVertical ? nsRect(0, -pos, mFrame->GetSize().width, mUnbrokenMeasure) : nsRect(-pos, 0, mUnbrokenMeasure, mFrame->GetSize().height); } /** * Return a continuous rect for (an inline) aFrame relative to the * continuation that should draw the left[top]-border. This is used when * painting borders and clipping backgrounds. This may NOT be the same * continuous rect as for drawing backgrounds; the continuation with the * left[top]-border might be somewhere in the middle of that rect (e.g. BIDI), * in those cases we need the reverse background order starting at the * left[top]-border continuation. */ nsRect GetBorderContinuousRect(nsIFrame* aFrame, nsRect aBorderArea) { // Calling GetContinuousRect(aFrame) here may lead to Reset/Init which // resets our mPIStartBorderData so we save it ... PhysicalInlineStartBorderData saved(mPIStartBorderData); nsRect joinedBorderArea = GetContinuousRect(aFrame); if (!saved.mIsValid || saved.mFrame != mPIStartBorderData.mFrame) { if (aFrame == mPIStartBorderData.mFrame) { if (mVertical) { mPIStartBorderData.SetCoord(joinedBorderArea.y); } else { mPIStartBorderData.SetCoord(joinedBorderArea.x); } } else if (mPIStartBorderData.mFrame) { // Copy data to a temporary object so that computing the // continous rect here doesn't clobber our normal state. InlineBackgroundData temp = *this; if (mVertical) { mPIStartBorderData.SetCoord( temp.GetContinuousRect(mPIStartBorderData.mFrame).y); } else { mPIStartBorderData.SetCoord( temp.GetContinuousRect(mPIStartBorderData.mFrame).x); } } } else { // ... and restore it when possible. mPIStartBorderData.SetCoord(saved.mCoord); } if (mVertical) { if (joinedBorderArea.y > mPIStartBorderData.mCoord) { joinedBorderArea.y = -(mUnbrokenMeasure + joinedBorderArea.y - aBorderArea.height); } else { joinedBorderArea.y -= mPIStartBorderData.mCoord; } } else { if (joinedBorderArea.x > mPIStartBorderData.mCoord) { joinedBorderArea.x = -(mUnbrokenMeasure + joinedBorderArea.x - aBorderArea.width); } else { joinedBorderArea.x -= mPIStartBorderData.mCoord; } } return joinedBorderArea; } nsRect GetBoundingRect(nsIFrame* aFrame) { SetFrame(aFrame); // Move the offsets relative to (0,0) which puts the bounding box into // our coordinate system rather than our parent's. We do this by // moving it the back distance from us to the bounding box. // This also assumes background-origin: border, so our caller will // need to deflate us if needed. nsRect boundingBox(mBoundingBox); nsPoint point = mFrame->GetPosition(); boundingBox.MoveBy(-point.x, -point.y); return boundingBox; } protected: // This is a coordinate on the inline axis, but is not a true logical inline- // coord because it is always measured from left to right (if horizontal) or // from top to bottom (if vertical), ignoring any bidi RTL directionality. // We'll call this "physical inline start", or PIStart for short. struct PhysicalInlineStartBorderData { nsIFrame* mFrame; // the continuation that may have a left-border nscoord mCoord; // cached GetContinuousRect(mFrame).x or .y bool mIsValid; // true if mCoord is valid void Reset() { mFrame = nullptr; mIsValid = false; } void SetCoord(nscoord aCoord) { mCoord = aCoord; mIsValid = true; } }; nsIFrame* mFrame; nsIFrame* mLineContainer; nsRect mBoundingBox; nscoord mContinuationPoint; nscoord mUnbrokenMeasure; nscoord mLineContinuationPoint; PhysicalInlineStartBorderData mPIStartBorderData; bool mBidiEnabled; bool mVertical; void SetFrame(nsIFrame* aFrame) { MOZ_ASSERT(aFrame, "Need a frame"); NS_ASSERTION(gFrameTreeLockCount > 0, "Can't call this when frame tree is not locked"); if (aFrame == mFrame) { return; } nsIFrame* prevContinuation = GetPrevContinuation(aFrame); if (!prevContinuation || mFrame != prevContinuation) { // Ok, we've got the wrong frame. We have to start from scratch. Reset(); Init(aFrame); return; } // Get our last frame's size and add its width to our continuation // point before we cache the new frame. mContinuationPoint += mVertical ? mFrame->GetSize().height : mFrame->GetSize().width; // If this a new line, update mLineContinuationPoint. if (mBidiEnabled && (aFrame->GetPrevInFlow() || !AreOnSameLine(mFrame, aFrame))) { mLineContinuationPoint = mContinuationPoint; } mFrame = aFrame; } nsIFrame* GetPrevContinuation(nsIFrame* aFrame) { nsIFrame* prevCont = aFrame->GetPrevContinuation(); if (!prevCont && aFrame->HasAnyStateBits(NS_FRAME_PART_OF_IBSPLIT)) { nsIFrame* block = aFrame->GetProperty(nsIFrame::IBSplitPrevSibling()); if (block) { // The {ib} properties are only stored on first continuations NS_ASSERTION(!block->GetPrevContinuation(), "Incorrect value for IBSplitPrevSibling"); prevCont = block->GetProperty(nsIFrame::IBSplitPrevSibling()); NS_ASSERTION(prevCont, "How did that happen?"); } } return prevCont; } nsIFrame* GetNextContinuation(nsIFrame* aFrame) { nsIFrame* nextCont = aFrame->GetNextContinuation(); if (!nextCont && aFrame->HasAnyStateBits(NS_FRAME_PART_OF_IBSPLIT)) { // The {ib} properties are only stored on first continuations aFrame = aFrame->FirstContinuation(); nsIFrame* block = aFrame->GetProperty(nsIFrame::IBSplitSibling()); if (block) { nextCont = block->GetProperty(nsIFrame::IBSplitSibling()); NS_ASSERTION(nextCont, "How did that happen?"); } } return nextCont; } void Init(nsIFrame* aFrame) { mPIStartBorderData.Reset(); mBidiEnabled = aFrame->PresContext()->BidiEnabled(); if (mBidiEnabled) { // Find the line container frame mLineContainer = aFrame; while (mLineContainer && mLineContainer->IsLineParticipant()) { mLineContainer = mLineContainer->GetParent(); } MOZ_ASSERT(mLineContainer, "Cannot find line containing frame."); MOZ_ASSERT(mLineContainer != aFrame, "line container frame " "should be an ancestor of the target frame."); } mVertical = aFrame->GetWritingMode().IsVertical(); // Start with the previous flow frame as our continuation point // is the total of the widths of the previous frames. nsIFrame* inlineFrame = GetPrevContinuation(aFrame); bool changedLines = false; while (inlineFrame) { if (!mPIStartBorderData.mFrame && !(mVertical ? inlineFrame->GetSkipSides().Top() : inlineFrame->GetSkipSides().Left())) { mPIStartBorderData.mFrame = inlineFrame; } nsRect rect = inlineFrame->GetRect(); mContinuationPoint += mVertical ? rect.height : rect.width; if (mBidiEnabled && (changedLines || !AreOnSameLine(aFrame, inlineFrame))) { mLineContinuationPoint += mVertical ? rect.height : rect.width; changedLines = true; } mUnbrokenMeasure += mVertical ? rect.height : rect.width; mBoundingBox.UnionRect(mBoundingBox, rect); inlineFrame = GetPrevContinuation(inlineFrame); } // Next add this frame and subsequent frames to the bounding box and // unbroken width. inlineFrame = aFrame; while (inlineFrame) { if (!mPIStartBorderData.mFrame && !(mVertical ? inlineFrame->GetSkipSides().Top() : inlineFrame->GetSkipSides().Left())) { mPIStartBorderData.mFrame = inlineFrame; } nsRect rect = inlineFrame->GetRect(); mUnbrokenMeasure += mVertical ? rect.height : rect.width; mBoundingBox.UnionRect(mBoundingBox, rect); inlineFrame = GetNextContinuation(inlineFrame); } mFrame = aFrame; } bool AreOnSameLine(nsIFrame* aFrame1, nsIFrame* aFrame2) { if (nsBlockFrame* blockFrame = do_QueryFrame(mLineContainer)) { bool isValid1, isValid2; nsBlockInFlowLineIterator it1(blockFrame, aFrame1, &isValid1); nsBlockInFlowLineIterator it2(blockFrame, aFrame2, &isValid2); return isValid1 && isValid2 && // Make sure aFrame1 and aFrame2 are in the same continuation of // blockFrame. it1.GetContainer() == it2.GetContainer() && // And on the same line in it it1.GetLine().get() == it2.GetLine().get(); } if (nsRubyTextContainerFrame* rtcFrame = do_QueryFrame(mLineContainer)) { nsBlockFrame* block = nsLayoutUtils::FindNearestBlockAncestor(rtcFrame); // Ruby text container can only hold one line of text, so if they // are in the same continuation, they are in the same line. Since // ruby text containers are bidi isolate, they are never split for // bidi reordering, which means being in different continuation // indicates being in different lines. for (nsIFrame* frame = rtcFrame->FirstContinuation(); frame; frame = frame->GetNextContinuation()) { bool isDescendant1 = nsLayoutUtils::IsProperAncestorFrame(frame, aFrame1, block); bool isDescendant2 = nsLayoutUtils::IsProperAncestorFrame(frame, aFrame2, block); if (isDescendant1 && isDescendant2) { return true; } if (isDescendant1 || isDescendant2) { return false; } } MOZ_ASSERT_UNREACHABLE("None of the frames is a descendant of this rtc?"); } MOZ_ASSERT_UNREACHABLE("Do we have any other type of line container?"); return false; } }; static StaticAutoPtr gInlineBGData; // Initialize any static variables used by nsCSSRendering. void nsCSSRendering::Init() { NS_ASSERTION(!gInlineBGData, "Init called twice"); gInlineBGData = new InlineBackgroundData(); } // Clean up any global variables used by nsCSSRendering. void nsCSSRendering::Shutdown() { gInlineBGData = nullptr; } /** * Make a bevel color */ static nscolor MakeBevelColor(mozilla::Side whichSide, StyleBorderStyle style, nscolor aBorderColor) { nscolor colors[2]; nscolor theColor; // Given a background color and a border color // calculate the color used for the shading NS_GetSpecial3DColors(colors, aBorderColor); if ((style == StyleBorderStyle::Outset) || (style == StyleBorderStyle::Ridge)) { // Flip colors for these two border styles switch (whichSide) { case eSideBottom: whichSide = eSideTop; break; case eSideRight: whichSide = eSideLeft; break; case eSideTop: whichSide = eSideBottom; break; case eSideLeft: whichSide = eSideRight; break; } } switch (whichSide) { case eSideBottom: theColor = colors[1]; break; case eSideRight: theColor = colors[1]; break; case eSideTop: theColor = colors[0]; break; case eSideLeft: default: theColor = colors[0]; break; } return theColor; } static bool GetRadii(nsIFrame* aForFrame, const nsStyleBorder& aBorder, const nsRect& aOrigBorderArea, const nsRect& aBorderArea, nscoord aRadii[8]) { bool haveRoundedCorners; nsSize sz = aBorderArea.Size(); nsSize frameSize = aForFrame->GetSize(); if (&aBorder == aForFrame->StyleBorder() && frameSize == aOrigBorderArea.Size()) { haveRoundedCorners = aForFrame->GetBorderRadii(sz, sz, Sides(), aRadii); } else { haveRoundedCorners = nsIFrame::ComputeBorderRadii( aBorder.mBorderRadius, frameSize, sz, Sides(), aRadii); } return haveRoundedCorners; } static bool GetRadii(nsIFrame* aForFrame, const nsStyleBorder& aBorder, const nsRect& aOrigBorderArea, const nsRect& aBorderArea, RectCornerRadii* aBgRadii) { nscoord radii[8]; bool haveRoundedCorners = GetRadii(aForFrame, aBorder, aOrigBorderArea, aBorderArea, radii); if (haveRoundedCorners) { auto d2a = aForFrame->PresContext()->AppUnitsPerDevPixel(); nsCSSRendering::ComputePixelRadii(radii, d2a, aBgRadii); } return haveRoundedCorners; } static nsRect JoinBoxesForBlockAxisSlice(nsIFrame* aFrame, const nsRect& aBorderArea) { // Inflate the block-axis size as if our continuations were laid out // adjacent in that axis. Note that we don't touch the inline size. const auto wm = aFrame->GetWritingMode(); const nsSize dummyContainerSize; LogicalRect borderArea(wm, aBorderArea, dummyContainerSize); nscoord bSize = 0; nsIFrame* f = aFrame->GetNextContinuation(); for (; f; f = f->GetNextContinuation()) { bSize += f->BSize(wm); } borderArea.BSize(wm) += bSize; bSize = 0; f = aFrame->GetPrevContinuation(); for (; f; f = f->GetPrevContinuation()) { bSize += f->BSize(wm); } borderArea.BStart(wm) -= bSize; borderArea.BSize(wm) += bSize; return borderArea.GetPhysicalRect(wm, dummyContainerSize); } /** * Inflate aBorderArea which is relative to aFrame's origin to calculate * a hypothetical non-split frame area for all the continuations. * See "Joining Boxes for 'slice'" in * http://dev.w3.org/csswg/css-break/#break-decoration */ enum InlineBoxOrder { eForBorder, eForBackground }; static nsRect JoinBoxesForSlice(nsIFrame* aFrame, const nsRect& aBorderArea, InlineBoxOrder aOrder) { if (static_cast(do_QueryFrame(aFrame))) { return (aOrder == eForBorder ? gInlineBGData->GetBorderContinuousRect(aFrame, aBorderArea) : gInlineBGData->GetContinuousRect(aFrame)) + aBorderArea.TopLeft(); } return JoinBoxesForBlockAxisSlice(aFrame, aBorderArea); } /* static */ bool nsCSSRendering::IsBoxDecorationSlice(const nsStyleBorder& aStyleBorder) { return aStyleBorder.mBoxDecorationBreak == StyleBoxDecorationBreak::Slice; } /* static */ nsRect nsCSSRendering::BoxDecorationRectForBorder( nsIFrame* aFrame, const nsRect& aBorderArea, Sides aSkipSides, const nsStyleBorder* aStyleBorder) { if (!aStyleBorder) { aStyleBorder = aFrame->StyleBorder(); } // If aSkipSides.IsEmpty() then there are no continuations, or it's // a ::first-letter that wants all border sides on the first continuation. return IsBoxDecorationSlice(*aStyleBorder) && !aSkipSides.IsEmpty() ? ::JoinBoxesForSlice(aFrame, aBorderArea, eForBorder) : aBorderArea; } /* static */ nsRect nsCSSRendering::BoxDecorationRectForBackground( nsIFrame* aFrame, const nsRect& aBorderArea, Sides aSkipSides, const nsStyleBorder* aStyleBorder) { if (!aStyleBorder) { aStyleBorder = aFrame->StyleBorder(); } // If aSkipSides.IsEmpty() then there are no continuations, or it's // a ::first-letter that wants all border sides on the first continuation. return IsBoxDecorationSlice(*aStyleBorder) && !aSkipSides.IsEmpty() ? ::JoinBoxesForSlice(aFrame, aBorderArea, eForBackground) : aBorderArea; } //---------------------------------------------------------------------- // Thebes Border Rendering Code Start /* * Compute the float-pixel radii that should be used for drawing * this border/outline, given the various input bits. */ /* static */ void nsCSSRendering::ComputePixelRadii(const nscoord* aAppUnitsRadii, nscoord aAppUnitsPerPixel, RectCornerRadii* oBorderRadii) { Float radii[8]; for (const auto corner : mozilla::AllPhysicalHalfCorners()) { radii[corner] = Float(aAppUnitsRadii[corner]) / aAppUnitsPerPixel; } (*oBorderRadii)[C_TL] = Size(radii[eCornerTopLeftX], radii[eCornerTopLeftY]); (*oBorderRadii)[C_TR] = Size(radii[eCornerTopRightX], radii[eCornerTopRightY]); (*oBorderRadii)[C_BR] = Size(radii[eCornerBottomRightX], radii[eCornerBottomRightY]); (*oBorderRadii)[C_BL] = Size(radii[eCornerBottomLeftX], radii[eCornerBottomLeftY]); } static Maybe GetBorderIfVisited(const ComputedStyle& aStyle) { Maybe result; // Don't check RelevantLinkVisited here, since we want to take the // same amount of time whether or not it's true. const ComputedStyle* styleIfVisited = aStyle.GetStyleIfVisited(); if (MOZ_LIKELY(!styleIfVisited)) { return result; } result.emplace(*aStyle.StyleBorder()); auto& newBorder = result.ref(); for (const auto side : mozilla::AllPhysicalSides()) { nscolor color = aStyle.GetVisitedDependentColor( nsStyleBorder::BorderColorFieldFor(side)); newBorder.BorderColorFor(side) = StyleColor::FromColor(color); } return result; } ImgDrawResult nsCSSRendering::PaintBorder( nsPresContext* aPresContext, gfxContext& aRenderingContext, nsIFrame* aForFrame, const nsRect& aDirtyRect, const nsRect& aBorderArea, ComputedStyle* aStyle, PaintBorderFlags aFlags, Sides aSkipSides) { AUTO_PROFILER_LABEL("nsCSSRendering::PaintBorder", GRAPHICS); Maybe visitedBorder = GetBorderIfVisited(*aStyle); return PaintBorderWithStyleBorder( aPresContext, aRenderingContext, aForFrame, aDirtyRect, aBorderArea, visitedBorder.refOr(*aStyle->StyleBorder()), aStyle, aFlags, aSkipSides); } Maybe nsCSSRendering::CreateBorderRenderer( nsPresContext* aPresContext, DrawTarget* aDrawTarget, nsIFrame* aForFrame, const nsRect& aDirtyRect, const nsRect& aBorderArea, ComputedStyle* aStyle, bool* aOutBorderIsEmpty, Sides aSkipSides) { Maybe visitedBorder = GetBorderIfVisited(*aStyle); return CreateBorderRendererWithStyleBorder( aPresContext, aDrawTarget, aForFrame, aDirtyRect, aBorderArea, visitedBorder.refOr(*aStyle->StyleBorder()), aStyle, aOutBorderIsEmpty, aSkipSides); } ImgDrawResult nsCSSRendering::CreateWebRenderCommandsForBorder( nsDisplayItem* aItem, nsIFrame* aForFrame, const nsRect& aBorderArea, mozilla::wr::DisplayListBuilder& aBuilder, mozilla::wr::IpcResourceUpdateQueue& aResources, const mozilla::layers::StackingContextHelper& aSc, mozilla::layers::RenderRootStateManager* aManager, nsDisplayListBuilder* aDisplayListBuilder) { const auto* style = aForFrame->Style(); Maybe visitedBorder = GetBorderIfVisited(*style); return nsCSSRendering::CreateWebRenderCommandsForBorderWithStyleBorder( aItem, aForFrame, aBorderArea, aBuilder, aResources, aSc, aManager, aDisplayListBuilder, visitedBorder.refOr(*style->StyleBorder())); } void nsCSSRendering::CreateWebRenderCommandsForNullBorder( nsDisplayItem* aItem, nsIFrame* aForFrame, const nsRect& aBorderArea, mozilla::wr::DisplayListBuilder& aBuilder, mozilla::wr::IpcResourceUpdateQueue& aResources, const mozilla::layers::StackingContextHelper& aSc, const nsStyleBorder& aStyleBorder) { bool borderIsEmpty = false; Maybe br = nsCSSRendering::CreateNullBorderRendererWithStyleBorder( aForFrame->PresContext(), nullptr, aForFrame, nsRect(), aBorderArea, aStyleBorder, aForFrame->Style(), &borderIsEmpty, aForFrame->GetSkipSides()); if (!borderIsEmpty && br) { br->CreateWebRenderCommands(aItem, aBuilder, aResources, aSc); } } ImgDrawResult nsCSSRendering::CreateWebRenderCommandsForBorderWithStyleBorder( nsDisplayItem* aItem, nsIFrame* aForFrame, const nsRect& aBorderArea, mozilla::wr::DisplayListBuilder& aBuilder, mozilla::wr::IpcResourceUpdateQueue& aResources, const mozilla::layers::StackingContextHelper& aSc, mozilla::layers::RenderRootStateManager* aManager, nsDisplayListBuilder* aDisplayListBuilder, const nsStyleBorder& aStyleBorder) { auto& borderImage = aStyleBorder.mBorderImageSource; // First try to create commands for simple borders. if (borderImage.IsNone()) { CreateWebRenderCommandsForNullBorder( aItem, aForFrame, aBorderArea, aBuilder, aResources, aSc, aStyleBorder); return ImgDrawResult::SUCCESS; } // Next we try image and gradient borders. Gradients are not supported at // this very moment. if (!borderImage.IsImageRequestType()) { return ImgDrawResult::NOT_SUPPORTED; } if (aStyleBorder.mBorderImageRepeatH == StyleBorderImageRepeat::Space || aStyleBorder.mBorderImageRepeatV == StyleBorderImageRepeat::Space) { return ImgDrawResult::NOT_SUPPORTED; } uint32_t flags = 0; if (aDisplayListBuilder->IsPaintingToWindow()) { flags |= nsImageRenderer::FLAG_PAINTING_TO_WINDOW; } if (aDisplayListBuilder->ShouldSyncDecodeImages()) { flags |= nsImageRenderer::FLAG_SYNC_DECODE_IMAGES; } bool dummy; image::ImgDrawResult result; Maybe bir = nsCSSBorderImageRenderer::CreateBorderImageRenderer( aForFrame->PresContext(), aForFrame, aBorderArea, aStyleBorder, aItem->GetBounds(aDisplayListBuilder, &dummy), aForFrame->GetSkipSides(), flags, &result); if (!bir) { // We aren't ready. Try to fallback to the null border image if present but // return the draw result for the border image renderer. CreateWebRenderCommandsForNullBorder( aItem, aForFrame, aBorderArea, aBuilder, aResources, aSc, aStyleBorder); return result; } return bir->CreateWebRenderCommands(aItem, aForFrame, aBuilder, aResources, aSc, aManager, aDisplayListBuilder); } static nsCSSBorderRenderer ConstructBorderRenderer( nsPresContext* aPresContext, ComputedStyle* aStyle, DrawTarget* aDrawTarget, nsIFrame* aForFrame, const nsRect& aDirtyRect, const nsRect& aBorderArea, const nsStyleBorder& aStyleBorder, Sides aSkipSides, bool* aNeedsClip) { nsMargin border = aStyleBorder.GetComputedBorder(); // Compute the outermost boundary of the area that might be painted. // Same coordinate space as aBorderArea & aBGClipRect. nsRect joinedBorderArea = nsCSSRendering::BoxDecorationRectForBorder( aForFrame, aBorderArea, aSkipSides, &aStyleBorder); RectCornerRadii bgRadii; ::GetRadii(aForFrame, aStyleBorder, aBorderArea, joinedBorderArea, &bgRadii); PrintAsFormatString(" joinedBorderArea: %d %d %d %d\n", joinedBorderArea.x, joinedBorderArea.y, joinedBorderArea.width, joinedBorderArea.height); // start drawing if (nsCSSRendering::IsBoxDecorationSlice(aStyleBorder)) { if (joinedBorderArea.IsEqualEdges(aBorderArea)) { // No need for a clip, just skip the sides we don't want. border.ApplySkipSides(aSkipSides); } else { // We're drawing borders around the joined continuation boxes so we need // to clip that to the slice that we want for this frame. *aNeedsClip = true; } } else { MOZ_ASSERT(joinedBorderArea.IsEqualEdges(aBorderArea), "Should use aBorderArea for box-decoration-break:clone"); MOZ_ASSERT( aForFrame->GetSkipSides().IsEmpty() || aForFrame->IsTrueOverflowContainer() || aForFrame->IsColumnSetFrame(), // a little broader than column-rule "Should not skip sides for box-decoration-break:clone except " "::first-letter/line continuations or other frame types that " "don't have borders but those shouldn't reach this point. " "Overflow containers do reach this point though, as does " "column-rule drawing (which always involves a columnset)."); border.ApplySkipSides(aSkipSides); } // Convert to dev pixels. nscoord oneDevPixel = aPresContext->DevPixelsToAppUnits(1); Rect joinedBorderAreaPx = NSRectToRect(joinedBorderArea, oneDevPixel); Float borderWidths[4] = { Float(border.top) / oneDevPixel, Float(border.right) / oneDevPixel, Float(border.bottom) / oneDevPixel, Float(border.left) / oneDevPixel}; Rect dirtyRect = NSRectToRect(aDirtyRect, oneDevPixel); StyleBorderStyle borderStyles[4]; nscolor borderColors[4]; // pull out styles, colors for (const auto i : mozilla::AllPhysicalSides()) { borderStyles[i] = aStyleBorder.GetBorderStyle(i); borderColors[i] = aStyleBorder.BorderColorFor(i).CalcColor(*aStyle); } PrintAsFormatString( " borderStyles: %d %d %d %d\n", static_cast(borderStyles[0]), static_cast(borderStyles[1]), static_cast(borderStyles[2]), static_cast(borderStyles[3])); return nsCSSBorderRenderer( aPresContext, aDrawTarget, dirtyRect, joinedBorderAreaPx, borderStyles, borderWidths, bgRadii, borderColors, !aForFrame->BackfaceIsHidden(), *aNeedsClip ? Some(NSRectToRect(aBorderArea, oneDevPixel)) : Nothing()); } ImgDrawResult nsCSSRendering::PaintBorderWithStyleBorder( nsPresContext* aPresContext, gfxContext& aRenderingContext, nsIFrame* aForFrame, const nsRect& aDirtyRect, const nsRect& aBorderArea, const nsStyleBorder& aStyleBorder, ComputedStyle* aStyle, PaintBorderFlags aFlags, Sides aSkipSides) { DrawTarget& aDrawTarget = *aRenderingContext.GetDrawTarget(); PrintAsStringNewline("++ PaintBorder"); // Check to see if we have an appearance defined. If so, we let the theme // renderer draw the border. DO not get the data from aForFrame, since the // passed in ComputedStyle may be different! Always use |aStyle|! StyleAppearance appearance = aStyle->StyleDisplay()->EffectiveAppearance(); if (appearance != StyleAppearance::None) { nsITheme* theme = aPresContext->Theme(); if (theme->ThemeSupportsWidget(aPresContext, aForFrame, appearance)) { return ImgDrawResult::SUCCESS; // Let the theme handle it. } } if (!aStyleBorder.mBorderImageSource.IsNone()) { ImgDrawResult result = ImgDrawResult::SUCCESS; uint32_t irFlags = 0; if (aFlags & PaintBorderFlags::SyncDecodeImages) { irFlags |= nsImageRenderer::FLAG_SYNC_DECODE_IMAGES; } // Creating the border image renderer will request a decode, and we rely on // that happening. Maybe renderer = nsCSSBorderImageRenderer::CreateBorderImageRenderer( aPresContext, aForFrame, aBorderArea, aStyleBorder, aDirtyRect, aSkipSides, irFlags, &result); // renderer was created successfully, which means border image is ready to // be used. if (renderer) { MOZ_ASSERT(result == ImgDrawResult::SUCCESS); return renderer->DrawBorderImage(aPresContext, aRenderingContext, aForFrame, aDirtyRect); } } ImgDrawResult result = ImgDrawResult::SUCCESS; // If we had a border-image, but it wasn't loaded, then we should return // ImgDrawResult::NOT_READY; we'll want to try again if we do a paint with // sync decoding enabled. if (!aStyleBorder.mBorderImageSource.IsNone()) { result = ImgDrawResult::NOT_READY; } nsMargin border = aStyleBorder.GetComputedBorder(); if (0 == border.left && 0 == border.right && 0 == border.top && 0 == border.bottom) { // Empty border area return result; } bool needsClip = false; nsCSSBorderRenderer br = ConstructBorderRenderer( aPresContext, aStyle, &aDrawTarget, aForFrame, aDirtyRect, aBorderArea, aStyleBorder, aSkipSides, &needsClip); if (needsClip) { aDrawTarget.PushClipRect(NSRectToSnappedRect( aBorderArea, aForFrame->PresContext()->AppUnitsPerDevPixel(), aDrawTarget)); } br.DrawBorders(); if (needsClip) { aDrawTarget.PopClip(); } PrintAsStringNewline(); return result; } Maybe nsCSSRendering::CreateBorderRendererWithStyleBorder( nsPresContext* aPresContext, DrawTarget* aDrawTarget, nsIFrame* aForFrame, const nsRect& aDirtyRect, const nsRect& aBorderArea, const nsStyleBorder& aStyleBorder, ComputedStyle* aStyle, bool* aOutBorderIsEmpty, Sides aSkipSides) { if (!aStyleBorder.mBorderImageSource.IsNone()) { return Nothing(); } return CreateNullBorderRendererWithStyleBorder( aPresContext, aDrawTarget, aForFrame, aDirtyRect, aBorderArea, aStyleBorder, aStyle, aOutBorderIsEmpty, aSkipSides); } Maybe nsCSSRendering::CreateNullBorderRendererWithStyleBorder( nsPresContext* aPresContext, DrawTarget* aDrawTarget, nsIFrame* aForFrame, const nsRect& aDirtyRect, const nsRect& aBorderArea, const nsStyleBorder& aStyleBorder, ComputedStyle* aStyle, bool* aOutBorderIsEmpty, Sides aSkipSides) { StyleAppearance appearance = aStyle->StyleDisplay()->EffectiveAppearance(); if (appearance != StyleAppearance::None) { nsITheme* theme = aPresContext->Theme(); if (theme->ThemeSupportsWidget(aPresContext, aForFrame, appearance)) { // The border will be draw as part of the themed background item created // for this same frame. If no themed background item was created then not // drawing also matches that we do without webrender and what // nsDisplayBorder does for themed borders. if (aOutBorderIsEmpty) { *aOutBorderIsEmpty = true; } return Nothing(); } } nsMargin border = aStyleBorder.GetComputedBorder(); if (0 == border.left && 0 == border.right && 0 == border.top && 0 == border.bottom) { // Empty border area if (aOutBorderIsEmpty) { *aOutBorderIsEmpty = true; } return Nothing(); } bool needsClip = false; nsCSSBorderRenderer br = ConstructBorderRenderer( aPresContext, aStyle, aDrawTarget, aForFrame, aDirtyRect, aBorderArea, aStyleBorder, aSkipSides, &needsClip); return Some(br); } Maybe nsCSSRendering::CreateBorderRendererForNonThemedOutline( nsPresContext* aPresContext, DrawTarget* aDrawTarget, nsIFrame* aForFrame, const nsRect& aDirtyRect, const nsRect& aInnerRect, ComputedStyle* aStyle) { // Get our ComputedStyle's color struct. const nsStyleOutline* ourOutline = aStyle->StyleOutline(); if (!ourOutline->ShouldPaintOutline()) { // Empty outline return Nothing(); } nsRect innerRect = aInnerRect; const nsSize effectiveOffset = ourOutline->EffectiveOffsetFor(innerRect); innerRect.Inflate(effectiveOffset); // If the dirty rect is completely inside the border area (e.g., only the // content is being painted), then we can skip out now // XXX this isn't exactly true for rounded borders, where the inside curves // may encroach into the content area. A safer calculation would be to // shorten insideRect by the radius one each side before performing this test. if (innerRect.Contains(aDirtyRect)) { return Nothing(); } const nscoord width = ourOutline->GetOutlineWidth(); StyleBorderStyle outlineStyle; // Themed outlines are handled by our callers, if supported. if (ourOutline->mOutlineStyle.IsAuto()) { if (width == 0) { return Nothing(); // empty outline } // http://dev.w3.org/csswg/css-ui/#outline // "User agents may treat 'auto' as 'solid'." outlineStyle = StyleBorderStyle::Solid; } else { outlineStyle = ourOutline->mOutlineStyle.AsBorderStyle(); } RectCornerRadii outlineRadii; nsRect outerRect = innerRect; outerRect.Inflate(width); const nscoord oneDevPixel = aPresContext->AppUnitsPerDevPixel(); Rect oRect(NSRectToRect(outerRect, oneDevPixel)); const Float outlineWidths[4] = { Float(width) / oneDevPixel, Float(width) / oneDevPixel, Float(width) / oneDevPixel, Float(width) / oneDevPixel}; // convert the radii nscoord twipsRadii[8]; // get the radius for our outline if (aForFrame->GetBorderRadii(twipsRadii)) { RectCornerRadii innerRadii; ComputePixelRadii(twipsRadii, oneDevPixel, &innerRadii); const auto devPxOffset = LayoutDeviceSize::FromAppUnits( effectiveOffset, aPresContext->AppUnitsPerDevPixel()); const Float widths[4] = {outlineWidths[0] + devPxOffset.Height(), outlineWidths[1] + devPxOffset.Width(), outlineWidths[2] + devPxOffset.Height(), outlineWidths[3] + devPxOffset.Width()}; nsCSSBorderRenderer::ComputeOuterRadii(innerRadii, widths, &outlineRadii); } StyleBorderStyle outlineStyles[4] = {outlineStyle, outlineStyle, outlineStyle, outlineStyle}; // This handles treating the initial color as 'currentColor'; if we // ever want 'invert' back we'll need to do a bit of work here too. nscolor outlineColor = aStyle->GetVisitedDependentColor(&nsStyleOutline::mOutlineColor); nscolor outlineColors[4] = {outlineColor, outlineColor, outlineColor, outlineColor}; Rect dirtyRect = NSRectToRect(aDirtyRect, oneDevPixel); return Some(nsCSSBorderRenderer( aPresContext, aDrawTarget, dirtyRect, oRect, outlineStyles, outlineWidths, outlineRadii, outlineColors, !aForFrame->BackfaceIsHidden(), Nothing())); } void nsCSSRendering::PaintNonThemedOutline(nsPresContext* aPresContext, gfxContext& aRenderingContext, nsIFrame* aForFrame, const nsRect& aDirtyRect, const nsRect& aInnerRect, ComputedStyle* aStyle) { Maybe br = CreateBorderRendererForNonThemedOutline( aPresContext, aRenderingContext.GetDrawTarget(), aForFrame, aDirtyRect, aInnerRect, aStyle); if (!br) { return; } // start drawing br->DrawBorders(); PrintAsStringNewline(); } void nsCSSRendering::PaintFocus(nsPresContext* aPresContext, DrawTarget* aDrawTarget, const nsRect& aFocusRect, nscolor aColor) { nscoord oneCSSPixel = nsPresContext::CSSPixelsToAppUnits(1); nscoord oneDevPixel = aPresContext->DevPixelsToAppUnits(1); Rect focusRect(NSRectToRect(aFocusRect, oneDevPixel)); RectCornerRadii focusRadii; { nscoord twipsRadii[8] = {0, 0, 0, 0, 0, 0, 0, 0}; ComputePixelRadii(twipsRadii, oneDevPixel, &focusRadii); } Float focusWidths[4] = { Float(oneCSSPixel) / oneDevPixel, Float(oneCSSPixel) / oneDevPixel, Float(oneCSSPixel) / oneDevPixel, Float(oneCSSPixel) / oneDevPixel}; StyleBorderStyle focusStyles[4] = { StyleBorderStyle::Dotted, StyleBorderStyle::Dotted, StyleBorderStyle::Dotted, StyleBorderStyle::Dotted}; nscolor focusColors[4] = {aColor, aColor, aColor, aColor}; // Because this renders a dotted border, the background color // should not be used. Therefore, we provide a value that will // be blatantly wrong if it ever does get used. (If this becomes // something that CSS can style, this function will then have access // to a ComputedStyle and can use the same logic that PaintBorder // and PaintOutline do.) // // WebRender layers-free mode don't use PaintFocus function. Just assign // the backface-visibility to true for this case. nsCSSBorderRenderer br(aPresContext, aDrawTarget, focusRect, focusRect, focusStyles, focusWidths, focusRadii, focusColors, true, Nothing()); br.DrawBorders(); PrintAsStringNewline(); } // Thebes Border Rendering Code End //---------------------------------------------------------------------- //---------------------------------------------------------------------- /** * Helper for ComputeObjectAnchorPoint; parameters are the same as for * that function, except they're for a single coordinate / a single size * dimension. (so, x/width vs. y/height) */ static void ComputeObjectAnchorCoord(const LengthPercentage& aCoord, const nscoord aOriginBounds, const nscoord aImageSize, nscoord* aTopLeftCoord, nscoord* aAnchorPointCoord) { nscoord extraSpace = aOriginBounds - aImageSize; // The anchor-point doesn't care about our image's size; just the size // of the region we're rendering into. *aAnchorPointCoord = aCoord.Resolve( aOriginBounds, static_cast(NSToCoordRoundWithClamp)); // Adjust aTopLeftCoord by the specified % of the extra space. *aTopLeftCoord = aCoord.Resolve( extraSpace, static_cast(NSToCoordRoundWithClamp)); } void nsImageRenderer::ComputeObjectAnchorPoint(const Position& aPos, const nsSize& aOriginBounds, const nsSize& aImageSize, nsPoint* aTopLeft, nsPoint* aAnchorPoint) { ComputeObjectAnchorCoord(aPos.horizontal, aOriginBounds.width, aImageSize.width, &aTopLeft->x, &aAnchorPoint->x); ComputeObjectAnchorCoord(aPos.vertical, aOriginBounds.height, aImageSize.height, &aTopLeft->y, &aAnchorPoint->y); } // In print / print preview we have multiple canvas frames (one for each page, // and one for the document as a whole). For the topmost one, we really want the // page sequence page background, not the root or body's background. static nsIFrame* GetPageSequenceForCanvas(const nsIFrame* aCanvasFrame) { MOZ_ASSERT(aCanvasFrame->IsCanvasFrame(), "not a canvas frame"); nsPresContext* pc = aCanvasFrame->PresContext(); if (!pc->IsRootPaginatedDocument()) { return nullptr; } auto* ps = pc->PresShell()->GetPageSequenceFrame(); if (NS_WARN_IF(!ps)) { return nullptr; } if (ps->GetParent() != aCanvasFrame) { return nullptr; } return ps; } auto nsCSSRendering::FindEffectiveBackgroundColor(nsIFrame* aFrame, bool aStopAtThemed, bool aPreferBodyToCanvas) -> EffectiveBackgroundColor { MOZ_ASSERT(aFrame); nsPresContext* pc = aFrame->PresContext(); auto BgColorIfNotTransparent = [&](nsIFrame* aFrame) -> Maybe { nscolor c = aFrame->GetVisitedDependentColor(&nsStyleBackground::mBackgroundColor); if (NS_GET_A(c) == 255) { return Some(c); } if (NS_GET_A(c)) { // TODO(emilio): We should maybe just blend with ancestor bg colors and // such, but this is probably good enough for now, matches pre-existing // behavior. const nscolor defaultBg = pc->DefaultBackgroundColor(); MOZ_ASSERT(NS_GET_A(defaultBg) == 255, "PreferenceSheet guarantees this"); return Some(NS_ComposeColors(defaultBg, c)); } return Nothing(); }; for (nsIFrame* frame = aFrame; frame; frame = nsLayoutUtils::GetParentOrPlaceholderForCrossDoc(frame)) { if (auto bg = BgColorIfNotTransparent(frame)) { return {*bg}; } if (aStopAtThemed && frame->IsThemed()) { return {NS_TRANSPARENT, true}; } if (frame->IsCanvasFrame()) { if (aPreferBodyToCanvas && !GetPageSequenceForCanvas(frame)) { if (auto* body = pc->Document()->GetBodyElement()) { if (nsIFrame* f = body->GetPrimaryFrame()) { if (auto bg = BgColorIfNotTransparent(f)) { return {*bg}; } } } } if (nsIFrame* bgFrame = FindBackgroundFrame(frame)) { if (auto bg = BgColorIfNotTransparent(bgFrame)) { return {*bg}; } } } } return {pc->DefaultBackgroundColor()}; } nsIFrame* nsCSSRendering::FindBackgroundStyleFrame(nsIFrame* aForFrame) { const nsStyleBackground* result = aForFrame->StyleBackground(); // Check if we need to do propagation from BODY rather than HTML. if (!result->IsTransparent(aForFrame)) { return aForFrame; } nsIContent* content = aForFrame->GetContent(); // The root element content can't be null. We wouldn't know what // frame to create for aFrame. // Use |OwnerDoc| so it works during destruction. if (!content) { return aForFrame; } Document* document = content->OwnerDoc(); dom::Element* bodyContent = document->GetBodyElement(); // We need to null check the body node (bug 118829) since // there are cases, thanks to the fix for bug 5569, where we // will reflow a document with no body. In particular, if a // SCRIPT element in the head blocks the parser and then has a // SCRIPT that does "document.location.href = 'foo'", then // nsParser::Terminate will call |DidBuildModel| methods // through to the content sink, which will call |StartLayout| // and thus |Initialize| on the pres shell. See bug 119351 // for the ugly details. if (!bodyContent || aForFrame->StyleDisplay()->IsContainAny()) { return aForFrame; } nsIFrame* bodyFrame = bodyContent->GetPrimaryFrame(); if (!bodyFrame || bodyFrame->StyleDisplay()->IsContainAny()) { return aForFrame; } return nsLayoutUtils::GetStyleFrame(bodyFrame); } /** * |FindBackground| finds the correct style data to use to paint the * background. It is responsible for handling the following two * statements in section 14.2 of CSS2: * * The background of the box generated by the root element covers the * entire canvas. * * For HTML documents, however, we recommend that authors specify the * background for the BODY element rather than the HTML element. User * agents should observe the following precedence rules to fill in the * background: if the value of the 'background' property for the HTML * element is different from 'transparent' then use it, else use the * value of the 'background' property for the BODY element. If the * resulting value is 'transparent', the rendering is undefined. * * Thus, in our implementation, it is responsible for ensuring that: * + we paint the correct background on the |nsCanvasFrame| or |nsPageFrame|, * + we don't paint the background on the root element, and * + we don't paint the background on the BODY element in *some* cases, * and for SGML-based HTML documents only. * * |FindBackground| checks whether a background should be painted. If yes, it * returns the resulting ComputedStyle to use for the background information; * Otherwise, it returns nullptr. */ ComputedStyle* nsCSSRendering::FindRootFrameBackground(nsIFrame* aForFrame) { return FindBackgroundStyleFrame(aForFrame)->Style(); } static nsIFrame* FindCanvasBackgroundFrame(const nsIFrame* aForFrame, nsIFrame* aRootElementFrame) { MOZ_ASSERT(aForFrame->IsCanvasFrame(), "not a canvas frame"); if (auto* ps = GetPageSequenceForCanvas(aForFrame)) { return ps; } if (aRootElementFrame) { return nsCSSRendering::FindBackgroundStyleFrame(aRootElementFrame); } // This should always give transparent, so we'll fill it in with the default // color if needed. This seems to happen a bit while a page is being loaded. return const_cast(aForFrame); } // Helper for FindBackgroundFrame. Returns true if aForFrame has a meaningful // background that it should draw (i.e. that it hasn't propagated to another // frame). See documentation for FindBackground. inline bool FrameHasMeaningfulBackground(const nsIFrame* aForFrame, nsIFrame* aRootElementFrame) { MOZ_ASSERT(!aForFrame->IsCanvasFrame(), "FindBackgroundFrame handles canvas frames before calling us, " "so we don't need to consider them here"); if (aForFrame == aRootElementFrame) { // We must have propagated our background to the viewport or canvas. Abort. return false; } // Return true unless the frame is for a BODY element whose background // was propagated to the viewport. nsIContent* content = aForFrame->GetContent(); if (!content || content->NodeInfo()->NameAtom() != nsGkAtoms::body) { return true; // not frame for a "body" element } // It could be a non-HTML "body" element but that's OK, we'd fail the // bodyContent check below if (aForFrame->Style()->GetPseudoType() != PseudoStyleType::NotPseudo || aForFrame->StyleDisplay()->IsContainAny()) { return true; // A pseudo-element frame, or contained. } // We should only look at the background if we're in an HTML document Document* document = content->OwnerDoc(); dom::Element* bodyContent = document->GetBodyElement(); if (bodyContent != content) { return true; // this wasn't the background that was propagated } // This can be called even when there's no root element yet, during frame // construction, via nsLayoutUtils::FrameHasTransparency and // nsContainerFrame::SyncFrameViewProperties. if (!aRootElementFrame || aRootElementFrame->StyleDisplay()->IsContainAny()) { return true; } const nsStyleBackground* htmlBG = aRootElementFrame->StyleBackground(); return !htmlBG->IsTransparent(aRootElementFrame); } nsIFrame* nsCSSRendering::FindBackgroundFrame(const nsIFrame* aForFrame) { nsIFrame* rootElementFrame = aForFrame->PresShell()->FrameConstructor()->GetRootElementStyleFrame(); if (aForFrame->IsCanvasFrame()) { return FindCanvasBackgroundFrame(aForFrame, rootElementFrame); } if (FrameHasMeaningfulBackground(aForFrame, rootElementFrame)) { return const_cast(aForFrame); } return nullptr; } ComputedStyle* nsCSSRendering::FindBackground(const nsIFrame* aForFrame) { if (auto* backgroundFrame = FindBackgroundFrame(aForFrame)) { return backgroundFrame->Style(); } return nullptr; } void nsCSSRendering::BeginFrameTreesLocked() { ++gFrameTreeLockCount; } void nsCSSRendering::EndFrameTreesLocked() { NS_ASSERTION(gFrameTreeLockCount > 0, "Unbalanced EndFrameTreeLocked"); --gFrameTreeLockCount; if (gFrameTreeLockCount == 0) { gInlineBGData->Reset(); } } bool nsCSSRendering::HasBoxShadowNativeTheme(nsIFrame* aFrame, bool& aMaybeHasBorderRadius) { const nsStyleDisplay* styleDisplay = aFrame->StyleDisplay(); nsITheme::Transparency transparency; if (aFrame->IsThemed(styleDisplay, &transparency)) { aMaybeHasBorderRadius = false; // For opaque (rectangular) theme widgets we can take the generic // border-box path with border-radius disabled. return transparency != nsITheme::eOpaque; } aMaybeHasBorderRadius = true; return false; } gfx::sRGBColor nsCSSRendering::GetShadowColor(const StyleSimpleShadow& aShadow, nsIFrame* aFrame, float aOpacity) { // Get the shadow color; if not specified, use the foreground color nscolor shadowColor = aShadow.color.CalcColor(aFrame); sRGBColor color = sRGBColor::FromABGR(shadowColor); color.a *= aOpacity; return color; } nsRect nsCSSRendering::GetShadowRect(const nsRect& aFrameArea, bool aNativeTheme, nsIFrame* aForFrame) { nsRect frameRect = aNativeTheme ? aForFrame->InkOverflowRectRelativeToSelf() + aFrameArea.TopLeft() : aFrameArea; Sides skipSides = aForFrame->GetSkipSides(); frameRect = BoxDecorationRectForBorder(aForFrame, frameRect, skipSides); // Explicitly do not need to account for the spread radius here // Webrender does it for us or PaintBoxShadow will for non-WR return frameRect; } bool nsCSSRendering::GetBorderRadii(const nsRect& aFrameRect, const nsRect& aBorderRect, nsIFrame* aFrame, RectCornerRadii& aOutRadii) { const nscoord oneDevPixel = aFrame->PresContext()->DevPixelsToAppUnits(1); nscoord twipsRadii[8]; NS_ASSERTION( aBorderRect.Size() == aFrame->VisualBorderRectRelativeToSelf().Size(), "unexpected size"); nsSize sz = aFrameRect.Size(); bool hasBorderRadius = aFrame->GetBorderRadii(sz, sz, Sides(), twipsRadii); if (hasBorderRadius) { ComputePixelRadii(twipsRadii, oneDevPixel, &aOutRadii); } return hasBorderRadius; } void nsCSSRendering::PaintBoxShadowOuter(nsPresContext* aPresContext, gfxContext& aRenderingContext, nsIFrame* aForFrame, const nsRect& aFrameArea, const nsRect& aDirtyRect, float aOpacity) { DrawTarget& aDrawTarget = *aRenderingContext.GetDrawTarget(); auto shadows = aForFrame->StyleEffects()->mBoxShadow.AsSpan(); if (shadows.IsEmpty()) { return; } bool hasBorderRadius; // mutually exclusive with hasBorderRadius bool nativeTheme = HasBoxShadowNativeTheme(aForFrame, hasBorderRadius); const nsStyleDisplay* styleDisplay = aForFrame->StyleDisplay(); nsRect frameRect = GetShadowRect(aFrameArea, nativeTheme, aForFrame); // Get any border radius, since box-shadow must also have rounded corners if // the frame does. RectCornerRadii borderRadii; const nscoord oneDevPixel = aPresContext->DevPixelsToAppUnits(1); if (hasBorderRadius) { nscoord twipsRadii[8]; NS_ASSERTION( aFrameArea.Size() == aForFrame->VisualBorderRectRelativeToSelf().Size(), "unexpected size"); nsSize sz = frameRect.Size(); hasBorderRadius = aForFrame->GetBorderRadii(sz, sz, Sides(), twipsRadii); if (hasBorderRadius) { ComputePixelRadii(twipsRadii, oneDevPixel, &borderRadii); } } // We don't show anything that intersects with the frame we're blurring on. So // tell the blurrer not to do unnecessary work there. gfxRect skipGfxRect = ThebesRect(NSRectToRect(frameRect, oneDevPixel)); skipGfxRect.Round(); bool useSkipGfxRect = true; if (nativeTheme) { // Optimize non-leaf native-themed frames by skipping computing pixels // in the padding-box. We assume the padding-box is going to be painted // opaquely for non-leaf frames. // XXX this may not be a safe assumption; we should make this go away // by optimizing box-shadow drawing more for the cases where we don't have a // skip-rect. useSkipGfxRect = !aForFrame->IsLeaf(); nsRect paddingRect = aForFrame->GetPaddingRectRelativeToSelf() + aFrameArea.TopLeft(); skipGfxRect = nsLayoutUtils::RectToGfxRect(paddingRect, oneDevPixel); } else if (hasBorderRadius) { skipGfxRect.Deflate(gfxMargin( std::max(borderRadii[C_TL].height, borderRadii[C_TR].height), 0, std::max(borderRadii[C_BL].height, borderRadii[C_BR].height), 0)); } for (const StyleBoxShadow& shadow : Reversed(shadows)) { if (shadow.inset) { continue; } nsRect shadowRect = frameRect; nsPoint shadowOffset(shadow.base.horizontal.ToAppUnits(), shadow.base.vertical.ToAppUnits()); shadowRect.MoveBy(shadowOffset); nscoord shadowSpread = shadow.spread.ToAppUnits(); if (!nativeTheme) { shadowRect.Inflate(shadowSpread); } // shadowRect won't include the blur, so make an extra rect here that // includes the blur for use in the even-odd rule below. nsRect shadowRectPlusBlur = shadowRect; nscoord blurRadius = shadow.base.blur.ToAppUnits(); shadowRectPlusBlur.Inflate( nsContextBoxBlur::GetBlurRadiusMargin(blurRadius, oneDevPixel)); Rect shadowGfxRectPlusBlur = NSRectToRect(shadowRectPlusBlur, oneDevPixel); shadowGfxRectPlusBlur.RoundOut(); MaybeSnapToDevicePixels(shadowGfxRectPlusBlur, aDrawTarget, true); sRGBColor gfxShadowColor = GetShadowColor(shadow.base, aForFrame, aOpacity); if (nativeTheme) { nsContextBoxBlur blurringArea; // When getting the widget shape from the native theme, we're going // to draw the widget into the shadow surface to create a mask. // We need to ensure that there actually *is* a shadow surface // and that we're not going to draw directly into aRenderingContext. gfxContext* shadowContext = blurringArea.Init( shadowRect, shadowSpread, blurRadius, oneDevPixel, &aRenderingContext, aDirtyRect, useSkipGfxRect ? &skipGfxRect : nullptr, nsContextBoxBlur::FORCE_MASK); if (!shadowContext) continue; MOZ_ASSERT(shadowContext == blurringArea.GetContext()); aRenderingContext.Save(); aRenderingContext.SetColor(gfxShadowColor); // Draw the shape of the frame so it can be blurred. Recall how // nsContextBoxBlur doesn't make any temporary surfaces if blur is 0 and // it just returns the original surface? If we have no blur, we're // painting this fill on the actual content surface (aRenderingContext == // shadowContext) which is why we set up the color and clip before doing // this. // We don't clip the border-box from the shadow, nor any other box. // We assume that the native theme is going to paint over the shadow. // Draw the widget shape gfxContextMatrixAutoSaveRestore save(shadowContext); gfxPoint devPixelOffset = nsLayoutUtils::PointToGfxPoint( shadowOffset, aPresContext->AppUnitsPerDevPixel()); shadowContext->SetMatrixDouble( shadowContext->CurrentMatrixDouble().PreTranslate(devPixelOffset)); nsRect nativeRect = aDirtyRect; nativeRect.MoveBy(-shadowOffset); nativeRect.IntersectRect(frameRect, nativeRect); aPresContext->Theme()->DrawWidgetBackground( shadowContext, aForFrame, styleDisplay->EffectiveAppearance(), aFrameArea, nativeRect, nsITheme::DrawOverflow::No); blurringArea.DoPaint(); aRenderingContext.Restore(); } else { aRenderingContext.Save(); { Rect innerClipRect = NSRectToRect(frameRect, oneDevPixel); if (!MaybeSnapToDevicePixels(innerClipRect, aDrawTarget, true)) { innerClipRect.Round(); } // Clip out the interior of the frame's border edge so that the shadow // is only painted outside that area. RefPtr builder = aDrawTarget.CreatePathBuilder(FillRule::FILL_EVEN_ODD); AppendRectToPath(builder, shadowGfxRectPlusBlur); if (hasBorderRadius) { AppendRoundedRectToPath(builder, innerClipRect, borderRadii); } else { AppendRectToPath(builder, innerClipRect); } RefPtr path = builder->Finish(); aRenderingContext.Clip(path); } // Clip the shadow so that we only get the part that applies to aForFrame. nsRect fragmentClip = shadowRectPlusBlur; Sides skipSides = aForFrame->GetSkipSides(); if (!skipSides.IsEmpty()) { if (skipSides.Left()) { nscoord xmost = fragmentClip.XMost(); fragmentClip.x = aFrameArea.x; fragmentClip.width = xmost - fragmentClip.x; } if (skipSides.Right()) { nscoord xmost = fragmentClip.XMost(); nscoord overflow = xmost - aFrameArea.XMost(); if (overflow > 0) { fragmentClip.width -= overflow; } } if (skipSides.Top()) { nscoord ymost = fragmentClip.YMost(); fragmentClip.y = aFrameArea.y; fragmentClip.height = ymost - fragmentClip.y; } if (skipSides.Bottom()) { nscoord ymost = fragmentClip.YMost(); nscoord overflow = ymost - aFrameArea.YMost(); if (overflow > 0) { fragmentClip.height -= overflow; } } } fragmentClip = fragmentClip.Intersect(aDirtyRect); aRenderingContext.Clip(NSRectToSnappedRect( fragmentClip, aForFrame->PresContext()->AppUnitsPerDevPixel(), aDrawTarget)); RectCornerRadii clipRectRadii; if (hasBorderRadius) { Float spreadDistance = Float(shadowSpread / oneDevPixel); Float borderSizes[4]; borderSizes[eSideLeft] = spreadDistance; borderSizes[eSideTop] = spreadDistance; borderSizes[eSideRight] = spreadDistance; borderSizes[eSideBottom] = spreadDistance; nsCSSBorderRenderer::ComputeOuterRadii(borderRadii, borderSizes, &clipRectRadii); } nsContextBoxBlur::BlurRectangle( &aRenderingContext, shadowRect, oneDevPixel, hasBorderRadius ? &clipRectRadii : nullptr, blurRadius, gfxShadowColor, aDirtyRect, skipGfxRect); aRenderingContext.Restore(); } } } nsRect nsCSSRendering::GetBoxShadowInnerPaddingRect(nsIFrame* aFrame, const nsRect& aFrameArea) { Sides skipSides = aFrame->GetSkipSides(); nsRect frameRect = BoxDecorationRectForBorder(aFrame, aFrameArea, skipSides); nsRect paddingRect = frameRect; nsMargin border = aFrame->GetUsedBorder(); paddingRect.Deflate(border); return paddingRect; } bool nsCSSRendering::ShouldPaintBoxShadowInner(nsIFrame* aFrame) { const Span shadows = aFrame->StyleEffects()->mBoxShadow.AsSpan(); if (shadows.IsEmpty()) { return false; } if (aFrame->IsThemed() && aFrame->GetContent() && !nsContentUtils::IsChromeDoc(aFrame->GetContent()->GetComposedDoc())) { // There's no way of getting hold of a shape corresponding to a // "padding-box" for native-themed widgets, so just don't draw // inner box-shadows for them. But we allow chrome to paint inner // box shadows since chrome can be aware of the platform theme. return false; } return true; } bool nsCSSRendering::GetShadowInnerRadii(nsIFrame* aFrame, const nsRect& aFrameArea, RectCornerRadii& aOutInnerRadii) { // Get any border radius, since box-shadow must also have rounded corners // if the frame does. nscoord twipsRadii[8]; nsRect frameRect = BoxDecorationRectForBorder(aFrame, aFrameArea, aFrame->GetSkipSides()); nsSize sz = frameRect.Size(); nsMargin border = aFrame->GetUsedBorder(); aFrame->GetBorderRadii(sz, sz, Sides(), twipsRadii); const nscoord oneDevPixel = aFrame->PresContext()->DevPixelsToAppUnits(1); RectCornerRadii borderRadii; const bool hasBorderRadius = GetBorderRadii(frameRect, aFrameArea, aFrame, borderRadii); if (hasBorderRadius) { ComputePixelRadii(twipsRadii, oneDevPixel, &borderRadii); Float borderSizes[4] = { Float(border.top) / oneDevPixel, Float(border.right) / oneDevPixel, Float(border.bottom) / oneDevPixel, Float(border.left) / oneDevPixel}; nsCSSBorderRenderer::ComputeInnerRadii(borderRadii, borderSizes, &aOutInnerRadii); } return hasBorderRadius; } void nsCSSRendering::PaintBoxShadowInner(nsPresContext* aPresContext, gfxContext& aRenderingContext, nsIFrame* aForFrame, const nsRect& aFrameArea) { if (!ShouldPaintBoxShadowInner(aForFrame)) { return; } const Span shadows = aForFrame->StyleEffects()->mBoxShadow.AsSpan(); NS_ASSERTION( aForFrame->IsFieldSetFrame() || aFrameArea.Size() == aForFrame->GetSize(), "unexpected size"); nsRect paddingRect = GetBoxShadowInnerPaddingRect(aForFrame, aFrameArea); RectCornerRadii innerRadii; bool hasBorderRadius = GetShadowInnerRadii(aForFrame, aFrameArea, innerRadii); const nscoord oneDevPixel = aPresContext->DevPixelsToAppUnits(1); for (const StyleBoxShadow& shadow : Reversed(shadows)) { if (!shadow.inset) { continue; } // shadowPaintRect: the area to paint on the temp surface // shadowClipRect: the area on the temporary surface within shadowPaintRect // that we will NOT paint in nscoord blurRadius = shadow.base.blur.ToAppUnits(); nsMargin blurMargin = nsContextBoxBlur::GetBlurRadiusMargin(blurRadius, oneDevPixel); nsRect shadowPaintRect = paddingRect; shadowPaintRect.Inflate(blurMargin); // Round the spread radius to device pixels (by truncation). // This mostly matches what we do for borders, except that we don't round // up values between zero and one device pixels to one device pixel. // This way of rounding is symmetric around zero, which makes sense for // the spread radius. int32_t spreadDistance = shadow.spread.ToAppUnits() / oneDevPixel; nscoord spreadDistanceAppUnits = aPresContext->DevPixelsToAppUnits(spreadDistance); nsRect shadowClipRect = paddingRect; shadowClipRect.MoveBy(shadow.base.horizontal.ToAppUnits(), shadow.base.vertical.ToAppUnits()); shadowClipRect.Deflate(spreadDistanceAppUnits, spreadDistanceAppUnits); Rect shadowClipGfxRect = NSRectToRect(shadowClipRect, oneDevPixel); shadowClipGfxRect.Round(); RectCornerRadii clipRectRadii; if (hasBorderRadius) { // Calculate the radii the inner clipping rect will have Float borderSizes[4] = {0, 0, 0, 0}; // See PaintBoxShadowOuter and bug 514670 if (innerRadii[C_TL].width > 0 || innerRadii[C_BL].width > 0) { borderSizes[eSideLeft] = spreadDistance; } if (innerRadii[C_TL].height > 0 || innerRadii[C_TR].height > 0) { borderSizes[eSideTop] = spreadDistance; } if (innerRadii[C_TR].width > 0 || innerRadii[C_BR].width > 0) { borderSizes[eSideRight] = spreadDistance; } if (innerRadii[C_BL].height > 0 || innerRadii[C_BR].height > 0) { borderSizes[eSideBottom] = spreadDistance; } nsCSSBorderRenderer::ComputeInnerRadii(innerRadii, borderSizes, &clipRectRadii); } // Set the "skip rect" to the area within the frame that we don't paint in, // including after blurring. nsRect skipRect = shadowClipRect; skipRect.Deflate(blurMargin); gfxRect skipGfxRect = nsLayoutUtils::RectToGfxRect(skipRect, oneDevPixel); if (hasBorderRadius) { skipGfxRect.Deflate(gfxMargin( std::max(clipRectRadii[C_TL].height, clipRectRadii[C_TR].height), 0, std::max(clipRectRadii[C_BL].height, clipRectRadii[C_BR].height), 0)); } // When there's a blur radius, gfxAlphaBoxBlur leaves the skiprect area // unchanged. And by construction the gfxSkipRect is not touched by the // rendered shadow (even after blurring), so those pixels must be completely // transparent in the shadow, so drawing them changes nothing. DrawTarget* drawTarget = aRenderingContext.GetDrawTarget(); // 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, oneDevPixel); shadowGfxRect.Round(); sRGBColor shadowColor = GetShadowColor(shadow.base, aForFrame, 1.0); aRenderingContext.Save(); // This clips the outside border radius. // clipRectRadii is the border radius inside the inset shadow. if (hasBorderRadius) { RefPtr roundedRect = MakePathForRoundedRect(*drawTarget, shadowGfxRect, innerRadii); aRenderingContext.Clip(roundedRect); } else { aRenderingContext.Clip(shadowGfxRect); } nsContextBoxBlur insetBoxBlur; gfxRect destRect = nsLayoutUtils::RectToGfxRect(shadowPaintRect, oneDevPixel); Point shadowOffset(shadow.base.horizontal.ToAppUnits() / oneDevPixel, shadow.base.vertical.ToAppUnits() / oneDevPixel); insetBoxBlur.InsetBoxBlur( &aRenderingContext, ToRect(destRect), shadowClipGfxRect, shadowColor, blurRadius, spreadDistanceAppUnits, oneDevPixel, hasBorderRadius, clipRectRadii, ToRect(skipGfxRect), shadowOffset); aRenderingContext.Restore(); } } /* static */ nsCSSRendering::PaintBGParams nsCSSRendering::PaintBGParams::ForAllLayers( nsPresContext& aPresCtx, const nsRect& aDirtyRect, const nsRect& aBorderArea, nsIFrame* aFrame, uint32_t aPaintFlags, float aOpacity) { MOZ_ASSERT(aFrame); PaintBGParams result(aPresCtx, aDirtyRect, aBorderArea, aFrame, aPaintFlags, -1, CompositionOp::OP_OVER, aOpacity); return result; } /* static */ nsCSSRendering::PaintBGParams nsCSSRendering::PaintBGParams::ForSingleLayer( nsPresContext& aPresCtx, const nsRect& aDirtyRect, const nsRect& aBorderArea, nsIFrame* aFrame, uint32_t aPaintFlags, int32_t aLayer, CompositionOp aCompositionOp, float aOpacity) { MOZ_ASSERT(aFrame && (aLayer != -1)); PaintBGParams result(aPresCtx, aDirtyRect, aBorderArea, aFrame, aPaintFlags, aLayer, aCompositionOp, aOpacity); return result; } ImgDrawResult nsCSSRendering::PaintStyleImageLayer(const PaintBGParams& aParams, gfxContext& aRenderingCtx) { AUTO_PROFILER_LABEL("nsCSSRendering::PaintStyleImageLayer", GRAPHICS); MOZ_ASSERT(aParams.frame, "Frame is expected to be provided to PaintStyleImageLayer"); const ComputedStyle* sc = FindBackground(aParams.frame); if (!sc) { // We don't want to bail out if moz-appearance is set on a root // node. If it has a parent content node, bail because it's not // a root, otherwise keep going in order to let the theme stuff // draw the background. The canvas really should be drawing the // bg, but there's no way to hook that up via css. if (!aParams.frame->StyleDisplay()->HasAppearance()) { return ImgDrawResult::SUCCESS; } nsIContent* content = aParams.frame->GetContent(); if (!content || content->GetParent()) { return ImgDrawResult::SUCCESS; } sc = aParams.frame->Style(); } return PaintStyleImageLayerWithSC(aParams, aRenderingCtx, sc, *aParams.frame->StyleBorder()); } bool nsCSSRendering::CanBuildWebRenderDisplayItemsForStyleImageLayer( WebRenderLayerManager* aManager, nsPresContext& aPresCtx, nsIFrame* aFrame, const nsStyleBackground* aBackgroundStyle, int32_t aLayer, uint32_t aPaintFlags) { if (!aBackgroundStyle) { return false; } MOZ_ASSERT(aFrame && aLayer >= 0 && (uint32_t)aLayer < aBackgroundStyle->mImage.mLayers.Length()); // We cannot draw native themed backgrounds StyleAppearance appearance = aFrame->StyleDisplay()->EffectiveAppearance(); if (appearance != StyleAppearance::None) { nsITheme* theme = aPresCtx.Theme(); if (theme->ThemeSupportsWidget(&aPresCtx, aFrame, appearance)) { return false; } } // We only support painting gradients and image for a single style image // layer, and we don't support crop-rects. const auto& styleImage = aBackgroundStyle->mImage.mLayers[aLayer].mImage.FinalImage(); if (styleImage.IsImageRequestType()) { imgRequestProxy* requestProxy = styleImage.GetImageRequest(); if (!requestProxy) { return false; } uint32_t imageFlags = imgIContainer::FLAG_NONE; if (aPaintFlags & nsCSSRendering::PAINTBG_SYNC_DECODE_IMAGES) { imageFlags |= imgIContainer::FLAG_SYNC_DECODE; } nsCOMPtr srcImage; requestProxy->GetImage(getter_AddRefs(srcImage)); if (!srcImage || !srcImage->IsImageContainerAvailable(aManager, imageFlags)) { return false; } return true; } if (styleImage.IsGradient()) { return true; } return false; } ImgDrawResult nsCSSRendering::BuildWebRenderDisplayItemsForStyleImageLayer( const PaintBGParams& aParams, mozilla::wr::DisplayListBuilder& aBuilder, mozilla::wr::IpcResourceUpdateQueue& aResources, const mozilla::layers::StackingContextHelper& aSc, mozilla::layers::RenderRootStateManager* aManager, nsDisplayItem* aItem) { MOZ_ASSERT(aParams.frame, "Frame is expected to be provided to " "BuildWebRenderDisplayItemsForStyleImageLayer"); ComputedStyle* sc = FindBackground(aParams.frame); if (!sc) { // We don't want to bail out if moz-appearance is set on a root // node. If it has a parent content node, bail because it's not // a root, otherwise keep going in order to let the theme stuff // draw the background. The canvas really should be drawing the // bg, but there's no way to hook that up via css. if (!aParams.frame->StyleDisplay()->HasAppearance()) { return ImgDrawResult::SUCCESS; } nsIContent* content = aParams.frame->GetContent(); if (!content || content->GetParent()) { return ImgDrawResult::SUCCESS; } sc = aParams.frame->Style(); } return BuildWebRenderDisplayItemsForStyleImageLayerWithSC( aParams, aBuilder, aResources, aSc, aManager, aItem, sc, *aParams.frame->StyleBorder()); } static bool IsOpaqueBorderEdge(const nsStyleBorder& aBorder, mozilla::Side aSide) { if (aBorder.GetComputedBorder().Side(aSide) == 0) return true; switch (aBorder.GetBorderStyle(aSide)) { case StyleBorderStyle::Solid: case StyleBorderStyle::Groove: case StyleBorderStyle::Ridge: case StyleBorderStyle::Inset: case StyleBorderStyle::Outset: break; default: return false; } // If we're using a border image, assume it's not fully opaque, // because we may not even have the image loaded at this point, and // even if we did, checking whether the relevant tile is fully // opaque would be too much work. if (!aBorder.mBorderImageSource.IsNone()) { return false; } StyleColor color = aBorder.BorderColorFor(aSide); // We don't know the foreground color here, so if it's being used // we must assume it might be transparent. return !color.MaybeTransparent(); } /** * Returns true if all border edges are either missing or opaque. */ static bool IsOpaqueBorder(const nsStyleBorder& aBorder) { for (const auto i : mozilla::AllPhysicalSides()) { if (!IsOpaqueBorderEdge(aBorder, i)) { return false; } } return true; } static inline void SetupDirtyRects(const nsRect& aBGClipArea, const nsRect& aCallerDirtyRect, nscoord aAppUnitsPerPixel, /* OUT: */ nsRect* aDirtyRect, gfxRect* aDirtyRectGfx) { aDirtyRect->IntersectRect(aBGClipArea, aCallerDirtyRect); // Compute the Thebes equivalent of the dirtyRect. *aDirtyRectGfx = nsLayoutUtils::RectToGfxRect(*aDirtyRect, aAppUnitsPerPixel); NS_WARNING_ASSERTION(aDirtyRect->IsEmpty() || !aDirtyRectGfx->IsEmpty(), "converted dirty rect should not be empty"); MOZ_ASSERT(!aDirtyRect->IsEmpty() || aDirtyRectGfx->IsEmpty(), "second should be empty if first is"); } static bool IsSVGStyleGeometryBox(StyleGeometryBox aBox) { return (aBox == StyleGeometryBox::FillBox || aBox == StyleGeometryBox::StrokeBox || aBox == StyleGeometryBox::ViewBox); } static bool IsHTMLStyleGeometryBox(StyleGeometryBox aBox) { return (aBox == StyleGeometryBox::ContentBox || aBox == StyleGeometryBox::PaddingBox || aBox == StyleGeometryBox::BorderBox || aBox == StyleGeometryBox::MarginBox); } static StyleGeometryBox ComputeBoxValueForOrigin(nsIFrame* aForFrame, StyleGeometryBox aBox) { // The mapping for mask-origin is from // https://drafts.fxtf.org/css-masking/#the-mask-origin if (!aForFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT)) { // For elements with associated CSS layout box, the values fill-box, // stroke-box and view-box compute to the initial value of mask-origin. if (IsSVGStyleGeometryBox(aBox)) { return StyleGeometryBox::BorderBox; } } else { // For SVG elements without associated CSS layout box, the values // content-box, padding-box, border-box compute to fill-box. if (IsHTMLStyleGeometryBox(aBox)) { return StyleGeometryBox::FillBox; } } return aBox; } static StyleGeometryBox ComputeBoxValueForClip(const nsIFrame* aForFrame, StyleGeometryBox aBox) { // The mapping for mask-clip is from // https://drafts.fxtf.org/css-masking/#the-mask-clip if (aForFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT)) { // For SVG elements without associated CSS layout box, the used values for // content-box and padding-box compute to fill-box and for border-box and // margin-box compute to stroke-box. switch (aBox) { case StyleGeometryBox::ContentBox: case StyleGeometryBox::PaddingBox: return StyleGeometryBox::FillBox; case StyleGeometryBox::BorderBox: case StyleGeometryBox::MarginBox: return StyleGeometryBox::StrokeBox; default: return aBox; } } // For elements with associated CSS layout box, the used values for fill-box // compute to content-box and for stroke-box and view-box compute to // border-box. switch (aBox) { case StyleGeometryBox::FillBox: return StyleGeometryBox::ContentBox; case StyleGeometryBox::StrokeBox: case StyleGeometryBox::ViewBox: return StyleGeometryBox::BorderBox; default: return aBox; } } bool nsCSSRendering::ImageLayerClipState::IsValid() const { // mDirtyRectInDevPx comes from mDirtyRectInAppUnits. mDirtyRectInAppUnits // can not be empty if mDirtyRectInDevPx is not. if (!mDirtyRectInDevPx.IsEmpty() && mDirtyRectInAppUnits.IsEmpty()) { return false; } if (mHasRoundedCorners == mClippedRadii.IsEmpty()) { return false; } return true; } /* static */ void nsCSSRendering::GetImageLayerClip( const nsStyleImageLayers::Layer& aLayer, nsIFrame* aForFrame, const nsStyleBorder& aBorder, const nsRect& aBorderArea, const nsRect& aCallerDirtyRect, bool aWillPaintBorder, nscoord aAppUnitsPerPixel, /* out */ ImageLayerClipState* aClipState) { StyleGeometryBox layerClip = ComputeBoxValueForClip(aForFrame, aLayer.mClip); if (IsSVGStyleGeometryBox(layerClip)) { MOZ_ASSERT(aForFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT)); // The coordinate space of clipArea is svg user space. nsRect clipArea = nsLayoutUtils::ComputeSVGReferenceRect(aForFrame, layerClip); nsRect strokeBox = (layerClip == StyleGeometryBox::StrokeBox) ? clipArea : nsLayoutUtils::ComputeSVGReferenceRect( aForFrame, StyleGeometryBox::StrokeBox); nsRect clipAreaRelativeToStrokeBox = clipArea - strokeBox.TopLeft(); // aBorderArea is the stroke-box area in a coordinate space defined by // the caller. This coordinate space can be svg user space of aForFrame, // the space of aForFrame's reference-frame, or anything else. // // Which coordinate space chosen for aBorderArea is not matter. What // matter is to ensure returning aClipState->mBGClipArea in the consistent // coordiante space with aBorderArea. So we evaluate the position of clip // area base on the position of aBorderArea here. aClipState->mBGClipArea = clipAreaRelativeToStrokeBox + aBorderArea.TopLeft(); SetupDirtyRects(aClipState->mBGClipArea, aCallerDirtyRect, aAppUnitsPerPixel, &aClipState->mDirtyRectInAppUnits, &aClipState->mDirtyRectInDevPx); MOZ_ASSERT(aClipState->IsValid()); return; } if (layerClip == StyleGeometryBox::NoClip) { aClipState->mBGClipArea = aCallerDirtyRect; SetupDirtyRects(aClipState->mBGClipArea, aCallerDirtyRect, aAppUnitsPerPixel, &aClipState->mDirtyRectInAppUnits, &aClipState->mDirtyRectInDevPx); MOZ_ASSERT(aClipState->IsValid()); return; } MOZ_ASSERT(!aForFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT)); // Compute the outermost boundary of the area that might be painted. // Same coordinate space as aBorderArea. Sides skipSides = aForFrame->GetSkipSides(); nsRect clipBorderArea = BoxDecorationRectForBorder(aForFrame, aBorderArea, skipSides, &aBorder); bool haveRoundedCorners = false; LayoutFrameType fType = aForFrame->Type(); if (fType != LayoutFrameType::TableColGroup && fType != LayoutFrameType::TableCol && fType != LayoutFrameType::TableRow && fType != LayoutFrameType::TableRowGroup) { haveRoundedCorners = GetRadii(aForFrame, aBorder, aBorderArea, clipBorderArea, aClipState->mRadii); } bool isSolidBorder = aWillPaintBorder && IsOpaqueBorder(aBorder); if (isSolidBorder && layerClip == StyleGeometryBox::BorderBox) { // If we have rounded corners, we need to inflate the background // drawing area a bit to avoid seams between the border and // background. layerClip = haveRoundedCorners ? StyleGeometryBox::MozAlmostPadding : StyleGeometryBox::PaddingBox; } aClipState->mBGClipArea = clipBorderArea; if (aForFrame->IsScrollFrame() && StyleImageLayerAttachment::Local == aLayer.mAttachment) { // As of this writing, this is still in discussion in the CSS Working Group // http://lists.w3.org/Archives/Public/www-style/2013Jul/0250.html // The rectangle for 'background-clip' scrolls with the content, // but the background is also clipped at a non-scrolling 'padding-box' // like the content. (See below.) // Therefore, only 'content-box' makes a difference here. if (layerClip == StyleGeometryBox::ContentBox) { nsIScrollableFrame* scrollableFrame = do_QueryFrame(aForFrame); // Clip at a rectangle attached to the scrolled content. aClipState->mHasAdditionalBGClipArea = true; aClipState->mAdditionalBGClipArea = nsRect(aClipState->mBGClipArea.TopLeft() + scrollableFrame->GetScrolledFrame()->GetPosition() // For the dir=rtl case: + scrollableFrame->GetScrollRange().TopLeft(), scrollableFrame->GetScrolledRect().Size()); nsMargin padding = aForFrame->GetUsedPadding(); // padding-bottom is ignored on scrollable frames: // https://bugzilla.mozilla.org/show_bug.cgi?id=748518 padding.bottom = 0; padding.ApplySkipSides(skipSides); aClipState->mAdditionalBGClipArea.Deflate(padding); } // Also clip at a non-scrolling, rounded-corner 'padding-box', // same as the scrolled content because of the 'overflow' property. layerClip = StyleGeometryBox::PaddingBox; } // See the comment of StyleGeometryBox::Margin. // Hitting this assertion means we decide to turn on margin-box support for // positioned mask from CSS parser and style system. In this case, you // should *inflate* mBGClipArea by the margin returning from // aForFrame->GetUsedMargin() in the code chunk bellow. MOZ_ASSERT(layerClip != StyleGeometryBox::MarginBox, "StyleGeometryBox::MarginBox rendering is not supported yet.\n"); if (layerClip != StyleGeometryBox::BorderBox && layerClip != StyleGeometryBox::Text) { nsMargin border = aForFrame->GetUsedBorder(); if (layerClip == StyleGeometryBox::MozAlmostPadding) { // Reduce |border| by 1px (device pixels) on all sides, if // possible, so that we don't get antialiasing seams between the // {background|mask} and border. border.top = std::max(0, border.top - aAppUnitsPerPixel); border.right = std::max(0, border.right - aAppUnitsPerPixel); border.bottom = std::max(0, border.bottom - aAppUnitsPerPixel); border.left = std::max(0, border.left - aAppUnitsPerPixel); } else if (layerClip != StyleGeometryBox::PaddingBox) { NS_ASSERTION(layerClip == StyleGeometryBox::ContentBox, "unexpected background-clip"); border += aForFrame->GetUsedPadding(); } border.ApplySkipSides(skipSides); aClipState->mBGClipArea.Deflate(border); if (haveRoundedCorners) { nsIFrame::AdjustBorderRadii(aClipState->mRadii, -border); } } if (haveRoundedCorners) { auto d2a = aForFrame->PresContext()->AppUnitsPerDevPixel(); nsCSSRendering::ComputePixelRadii(aClipState->mRadii, d2a, &aClipState->mClippedRadii); aClipState->mHasRoundedCorners = !aClipState->mClippedRadii.IsEmpty(); } if (!haveRoundedCorners && aClipState->mHasAdditionalBGClipArea) { // Do the intersection here to account for the fast path(?) below. aClipState->mBGClipArea = aClipState->mBGClipArea.Intersect(aClipState->mAdditionalBGClipArea); aClipState->mHasAdditionalBGClipArea = false; } SetupDirtyRects(aClipState->mBGClipArea, aCallerDirtyRect, aAppUnitsPerPixel, &aClipState->mDirtyRectInAppUnits, &aClipState->mDirtyRectInDevPx); MOZ_ASSERT(aClipState->IsValid()); } static void SetupImageLayerClip(nsCSSRendering::ImageLayerClipState& aClipState, gfxContext* aCtx, nscoord aAppUnitsPerPixel, gfxContextAutoSaveRestore* aAutoSR) { if (aClipState.mDirtyRectInDevPx.IsEmpty()) { // Our caller won't draw anything under this condition, so no need // to set more up. return; } if (aClipState.mCustomClip) { // We don't support custom clips and rounded corners, arguably a bug, but // table painting seems to depend on it. return; } // If we have rounded corners, clip all subsequent drawing to the // rounded rectangle defined by bgArea and bgRadii (we don't know // whether the rounded corners intrude on the dirtyRect or not). // Do not do this if we have a caller-provided clip rect -- // as above with bgArea, arguably a bug, but table painting seems // to depend on it. if (aClipState.mHasAdditionalBGClipArea) { gfxRect bgAreaGfx = nsLayoutUtils::RectToGfxRect( aClipState.mAdditionalBGClipArea, aAppUnitsPerPixel); bgAreaGfx.Round(); gfxUtils::ConditionRect(bgAreaGfx); aAutoSR->EnsureSaved(aCtx); aCtx->SnappedClip(bgAreaGfx); } if (aClipState.mHasRoundedCorners) { Rect bgAreaGfx = NSRectToRect(aClipState.mBGClipArea, aAppUnitsPerPixel); bgAreaGfx.Round(); if (bgAreaGfx.IsEmpty()) { // I think it's become possible to hit this since // https://hg.mozilla.org/mozilla-central/rev/50e934e4979b landed. NS_WARNING("converted background area should not be empty"); // Make our caller not do anything. aClipState.mDirtyRectInDevPx.SizeTo(gfxSize(0.0, 0.0)); return; } aAutoSR->EnsureSaved(aCtx); RefPtr roundedRect = MakePathForRoundedRect( *aCtx->GetDrawTarget(), bgAreaGfx, aClipState.mClippedRadii); aCtx->Clip(roundedRect); } } static void DrawBackgroundColor(nsCSSRendering::ImageLayerClipState& aClipState, gfxContext* aCtx, nscoord aAppUnitsPerPixel) { if (aClipState.mDirtyRectInDevPx.IsEmpty()) { // Our caller won't draw anything under this condition, so no need // to set more up. return; } DrawTarget* drawTarget = aCtx->GetDrawTarget(); // We don't support custom clips and rounded corners, arguably a bug, but // table painting seems to depend on it. if (!aClipState.mHasRoundedCorners || aClipState.mCustomClip) { aCtx->NewPath(); aCtx->SnappedRectangle(aClipState.mDirtyRectInDevPx); aCtx->Fill(); return; } Rect bgAreaGfx = NSRectToRect(aClipState.mBGClipArea, aAppUnitsPerPixel); bgAreaGfx.Round(); if (bgAreaGfx.IsEmpty()) { // I think it's become possible to hit this since // https://hg.mozilla.org/mozilla-central/rev/50e934e4979b landed. NS_WARNING("converted background area should not be empty"); // Make our caller not do anything. aClipState.mDirtyRectInDevPx.SizeTo(gfxSize(0.0, 0.0)); return; } aCtx->Save(); gfxRect dirty = ThebesRect(bgAreaGfx).Intersect(aClipState.mDirtyRectInDevPx); aCtx->SnappedClip(dirty); if (aClipState.mHasAdditionalBGClipArea) { gfxRect bgAdditionalAreaGfx = nsLayoutUtils::RectToGfxRect( aClipState.mAdditionalBGClipArea, aAppUnitsPerPixel); bgAdditionalAreaGfx.Round(); gfxUtils::ConditionRect(bgAdditionalAreaGfx); aCtx->SnappedClip(bgAdditionalAreaGfx); } RefPtr roundedRect = MakePathForRoundedRect(*drawTarget, bgAreaGfx, aClipState.mClippedRadii); aCtx->SetPath(roundedRect); aCtx->Fill(); aCtx->Restore(); } enum class ScrollbarColorKind { Thumb, Track, }; static Maybe CalcScrollbarColor(nsIFrame* aFrame, ScrollbarColorKind aKind) { ComputedStyle* scrollbarStyle = nsLayoutUtils::StyleForScrollbar(aFrame); const auto& colors = scrollbarStyle->StyleUI()->mScrollbarColor; if (colors.IsAuto()) { return Nothing(); } const auto& color = aKind == ScrollbarColorKind::Thumb ? colors.AsColors().thumb : colors.AsColors().track; return Some(color.CalcColor(*scrollbarStyle)); } static nscolor GetBackgroundColor(nsIFrame* aFrame, const ComputedStyle* aStyle) { switch (aStyle->StyleDisplay()->EffectiveAppearance()) { case StyleAppearance::ScrollbarthumbVertical: case StyleAppearance::ScrollbarthumbHorizontal: { if (Maybe overrideColor = CalcScrollbarColor(aFrame, ScrollbarColorKind::Thumb)) { return *overrideColor; } break; } case StyleAppearance::ScrollbarVertical: case StyleAppearance::ScrollbarHorizontal: case StyleAppearance::Scrollcorner: { if (Maybe overrideColor = CalcScrollbarColor(aFrame, ScrollbarColorKind::Track)) { return *overrideColor; } break; } default: break; } return aStyle->GetVisitedDependentColor(&nsStyleBackground::mBackgroundColor); } nscolor nsCSSRendering::DetermineBackgroundColor(nsPresContext* aPresContext, const ComputedStyle* aStyle, nsIFrame* aFrame, bool& aDrawBackgroundImage, bool& aDrawBackgroundColor) { auto shouldPaint = aFrame->ComputeShouldPaintBackground(); aDrawBackgroundImage = shouldPaint.mImage; aDrawBackgroundColor = shouldPaint.mColor; const nsStyleBackground* bg = aStyle->StyleBackground(); nscolor bgColor; if (aDrawBackgroundColor) { bgColor = GetBackgroundColor(aFrame, aStyle); if (NS_GET_A(bgColor) == 0) { aDrawBackgroundColor = false; } } else { // If GetBackgroundColorDraw() is false, we are still expected to // draw color in the background of any frame that's not completely // transparent, but we are expected to use white instead of whatever // color was specified. bgColor = NS_RGB(255, 255, 255); if (aDrawBackgroundImage || !bg->IsTransparent(aStyle)) { aDrawBackgroundColor = true; } else { bgColor = NS_RGBA(0, 0, 0, 0); } } // We can skip painting the background color if a background image is opaque. nsStyleImageLayers::Repeat repeat = bg->BottomLayer().mRepeat; bool xFullRepeat = repeat.mXRepeat == StyleImageLayerRepeat::Repeat || repeat.mXRepeat == StyleImageLayerRepeat::Round; bool yFullRepeat = repeat.mYRepeat == StyleImageLayerRepeat::Repeat || repeat.mYRepeat == StyleImageLayerRepeat::Round; if (aDrawBackgroundColor && xFullRepeat && yFullRepeat && bg->BottomLayer().mImage.IsOpaque() && bg->BottomLayer().mBlendMode == StyleBlend::Normal) { aDrawBackgroundColor = false; } return bgColor; } static CompositionOp DetermineCompositionOp( const nsCSSRendering::PaintBGParams& aParams, const nsStyleImageLayers& aLayers, uint32_t aLayerIndex) { if (aParams.layer >= 0) { // When drawing a single layer, use the specified composition op. return aParams.compositionOp; } const nsStyleImageLayers::Layer& layer = aLayers.mLayers[aLayerIndex]; // When drawing all layers, get the compositon op from each image layer. if (aParams.paintFlags & nsCSSRendering::PAINTBG_MASK_IMAGE) { // Always using OP_OVER mode while drawing the bottom mask layer. if (aLayerIndex == (aLayers.mImageCount - 1)) { return CompositionOp::OP_OVER; } return nsCSSRendering::GetGFXCompositeMode(layer.mComposite); } return nsCSSRendering::GetGFXBlendMode(layer.mBlendMode); } ImgDrawResult nsCSSRendering::PaintStyleImageLayerWithSC( const PaintBGParams& aParams, gfxContext& aRenderingCtx, const ComputedStyle* aBackgroundSC, const nsStyleBorder& aBorder) { MOZ_ASSERT(aParams.frame, "Frame is expected to be provided to PaintStyleImageLayerWithSC"); // If we're drawing all layers, aCompositonOp is ignored, so make sure that // it was left at its default value. MOZ_ASSERT(aParams.layer != -1 || aParams.compositionOp == CompositionOp::OP_OVER); // Check to see if we have an appearance defined. If so, we let the theme // renderer draw the background and bail out. // XXXzw this ignores aParams.bgClipRect. StyleAppearance appearance = aParams.frame->StyleDisplay()->EffectiveAppearance(); if (appearance != StyleAppearance::None) { nsITheme* theme = aParams.presCtx.Theme(); if (theme->ThemeSupportsWidget(&aParams.presCtx, aParams.frame, appearance)) { nsRect drawing(aParams.borderArea); theme->GetWidgetOverflow(aParams.presCtx.DeviceContext(), aParams.frame, appearance, &drawing); drawing.IntersectRect(drawing, aParams.dirtyRect); theme->DrawWidgetBackground(&aRenderingCtx, aParams.frame, appearance, aParams.borderArea, drawing); return ImgDrawResult::SUCCESS; } } // For canvas frames (in the CSS sense) we draw the background color using // a solid color item that gets added in nsLayoutUtils::PaintFrame, // or nsSubDocumentFrame::BuildDisplayList (bug 488242). (The solid // color may be moved into nsDisplayCanvasBackground by // PresShell::AddCanvasBackgroundColorItem(), and painted by // nsDisplayCanvasBackground directly.) Either way we don't need to // paint the background color here. bool isCanvasFrame = aParams.frame->IsCanvasFrame(); const bool paintMask = aParams.paintFlags & PAINTBG_MASK_IMAGE; // Determine whether we are drawing background images and/or // background colors. bool drawBackgroundImage = true; bool drawBackgroundColor = !paintMask; nscolor bgColor = NS_RGBA(0, 0, 0, 0); if (!paintMask) { bgColor = DetermineBackgroundColor(&aParams.presCtx, aBackgroundSC, aParams.frame, drawBackgroundImage, drawBackgroundColor); } // Masks shouldn't be suppressed for print. MOZ_ASSERT_IF(paintMask, drawBackgroundImage); const nsStyleImageLayers& layers = paintMask ? aBackgroundSC->StyleSVGReset()->mMask : aBackgroundSC->StyleBackground()->mImage; // If we're drawing a specific layer, we don't want to draw the // background color. if (drawBackgroundColor && aParams.layer >= 0) { drawBackgroundColor = false; } // At this point, drawBackgroundImage and drawBackgroundColor are // true if and only if we are actually supposed to paint an image or // color into aDirtyRect, respectively. if (!drawBackgroundImage && !drawBackgroundColor) { return ImgDrawResult::SUCCESS; } // The 'bgClipArea' (used only by the image tiling logic, far below) // is the caller-provided aParams.bgClipRect if any, or else the area // determined by the value of 'background-clip' in // SetupCurrentBackgroundClip. (Arguably it should be the // intersection, but that breaks the table painter -- in particular, // taking the intersection breaks reftests/bugs/403249-1[ab].) nscoord appUnitsPerPixel = aParams.presCtx.AppUnitsPerDevPixel(); ImageLayerClipState clipState; if (aParams.bgClipRect) { clipState.mBGClipArea = *aParams.bgClipRect; clipState.mCustomClip = true; clipState.mHasRoundedCorners = false; SetupDirtyRects(clipState.mBGClipArea, aParams.dirtyRect, appUnitsPerPixel, &clipState.mDirtyRectInAppUnits, &clipState.mDirtyRectInDevPx); } else { GetImageLayerClip(layers.BottomLayer(), aParams.frame, aBorder, aParams.borderArea, aParams.dirtyRect, (aParams.paintFlags & PAINTBG_WILL_PAINT_BORDER), appUnitsPerPixel, &clipState); } // If we might be using a background color, go ahead and set it now. if (drawBackgroundColor && !isCanvasFrame) { aRenderingCtx.SetColor(sRGBColor::FromABGR(bgColor)); } // If there is no background image, draw a color. (If there is // neither a background image nor a color, we wouldn't have gotten // this far.) if (!drawBackgroundImage) { if (!isCanvasFrame) { DrawBackgroundColor(clipState, &aRenderingCtx, appUnitsPerPixel); } return ImgDrawResult::SUCCESS; } if (layers.mImageCount < 1) { // Return if there are no background layers, all work from this point // onwards happens iteratively on these. return ImgDrawResult::SUCCESS; } MOZ_ASSERT((aParams.layer < 0) || (layers.mImageCount > uint32_t(aParams.layer))); // The background color is rendered over the entire dirty area, // even if the image isn't. if (drawBackgroundColor && !isCanvasFrame) { DrawBackgroundColor(clipState, &aRenderingCtx, appUnitsPerPixel); } // Compute the outermost boundary of the area that might be painted. // Same coordinate space as aParams.borderArea & aParams.bgClipRect. Sides skipSides = aParams.frame->GetSkipSides(); nsRect paintBorderArea = BoxDecorationRectForBackground( aParams.frame, aParams.borderArea, skipSides, &aBorder); nsRect clipBorderArea = BoxDecorationRectForBorder( aParams.frame, aParams.borderArea, skipSides, &aBorder); ImgDrawResult result = ImgDrawResult::SUCCESS; StyleGeometryBox currentBackgroundClip = StyleGeometryBox::BorderBox; const bool drawAllLayers = (aParams.layer < 0); uint32_t count = drawAllLayers ? layers.mImageCount // iterate all image layers. : layers.mImageCount - aParams.layer; // iterate from the bottom layer to // the 'aParams.layer-th' layer. NS_FOR_VISIBLE_IMAGE_LAYERS_BACK_TO_FRONT_WITH_RANGE( i, layers, layers.mImageCount - 1, count) { // NOTE: no Save() yet, we do that later by calling autoSR.EnsureSaved(ctx) // in the cases we need it. gfxContextAutoSaveRestore autoSR; const nsStyleImageLayers::Layer& layer = layers.mLayers[i]; ImageLayerClipState currentLayerClipState = clipState; if (!aParams.bgClipRect) { bool isBottomLayer = (i == layers.mImageCount - 1); if (currentBackgroundClip != layer.mClip || isBottomLayer) { currentBackgroundClip = layer.mClip; if (!isBottomLayer) { currentLayerClipState = {}; // For the bottom layer, we already called GetImageLayerClip above // and it stored its results in clipState. GetImageLayerClip(layer, aParams.frame, aBorder, aParams.borderArea, aParams.dirtyRect, (aParams.paintFlags & PAINTBG_WILL_PAINT_BORDER), appUnitsPerPixel, ¤tLayerClipState); } SetupImageLayerClip(currentLayerClipState, &aRenderingCtx, appUnitsPerPixel, &autoSR); if (!clipBorderArea.IsEqualEdges(aParams.borderArea)) { // We're drawing the background for the joined continuation boxes // so we need to clip that to the slice that we want for this // frame. gfxRect clip = nsLayoutUtils::RectToGfxRect(aParams.borderArea, appUnitsPerPixel); autoSR.EnsureSaved(&aRenderingCtx); aRenderingCtx.SnappedClip(clip); } } } // Skip the following layer preparing and painting code if the current // layer is not selected for drawing. if (aParams.layer >= 0 && i != (uint32_t)aParams.layer) { continue; } nsBackgroundLayerState state = PrepareImageLayer( &aParams.presCtx, aParams.frame, aParams.paintFlags, paintBorderArea, currentLayerClipState.mBGClipArea, layer, nullptr); result &= state.mImageRenderer.PrepareResult(); // Skip the layer painting code if we found the dirty region is empty. if (currentLayerClipState.mDirtyRectInDevPx.IsEmpty()) { continue; } if (!state.mFillArea.IsEmpty()) { CompositionOp co = DetermineCompositionOp(aParams, layers, i); if (co != CompositionOp::OP_OVER) { NS_ASSERTION(aRenderingCtx.CurrentOp() == CompositionOp::OP_OVER, "It is assumed the initial op is OP_OVER, when it is " "restored later"); aRenderingCtx.SetOp(co); } result &= state.mImageRenderer.DrawLayer( &aParams.presCtx, aRenderingCtx, state.mDestArea, state.mFillArea, state.mAnchor + paintBorderArea.TopLeft(), currentLayerClipState.mDirtyRectInAppUnits, state.mRepeatSize, aParams.opacity); if (co != CompositionOp::OP_OVER) { aRenderingCtx.SetOp(CompositionOp::OP_OVER); } } } return result; } ImgDrawResult nsCSSRendering::BuildWebRenderDisplayItemsForStyleImageLayerWithSC( const PaintBGParams& aParams, mozilla::wr::DisplayListBuilder& aBuilder, mozilla::wr::IpcResourceUpdateQueue& aResources, const mozilla::layers::StackingContextHelper& aSc, mozilla::layers::RenderRootStateManager* aManager, nsDisplayItem* aItem, ComputedStyle* aBackgroundSC, const nsStyleBorder& aBorder) { MOZ_ASSERT(!(aParams.paintFlags & PAINTBG_MASK_IMAGE)); nscoord appUnitsPerPixel = aParams.presCtx.AppUnitsPerDevPixel(); ImageLayerClipState clipState; clipState.mBGClipArea = *aParams.bgClipRect; clipState.mCustomClip = true; clipState.mHasRoundedCorners = false; SetupDirtyRects(clipState.mBGClipArea, aParams.dirtyRect, appUnitsPerPixel, &clipState.mDirtyRectInAppUnits, &clipState.mDirtyRectInDevPx); // Compute the outermost boundary of the area that might be painted. // Same coordinate space as aParams.borderArea & aParams.bgClipRect. Sides skipSides = aParams.frame->GetSkipSides(); nsRect paintBorderArea = BoxDecorationRectForBackground( aParams.frame, aParams.borderArea, skipSides, &aBorder); const nsStyleImageLayers& layers = aBackgroundSC->StyleBackground()->mImage; const nsStyleImageLayers::Layer& layer = layers.mLayers[aParams.layer]; // Skip the following layer painting code if we found the dirty region is // empty or the current layer is not selected for drawing. if (clipState.mDirtyRectInDevPx.IsEmpty()) { return ImgDrawResult::SUCCESS; } ImgDrawResult result = ImgDrawResult::SUCCESS; nsBackgroundLayerState state = PrepareImageLayer(&aParams.presCtx, aParams.frame, aParams.paintFlags, paintBorderArea, clipState.mBGClipArea, layer, nullptr); result &= state.mImageRenderer.PrepareResult(); if (!state.mFillArea.IsEmpty()) { result &= state.mImageRenderer.BuildWebRenderDisplayItemsForLayer( &aParams.presCtx, aBuilder, aResources, aSc, aManager, aItem, state.mDestArea, state.mFillArea, state.mAnchor + paintBorderArea.TopLeft(), clipState.mDirtyRectInAppUnits, state.mRepeatSize, aParams.opacity); } return result; } nsRect nsCSSRendering::ComputeImageLayerPositioningArea( nsPresContext* aPresContext, nsIFrame* aForFrame, const nsRect& aBorderArea, const nsStyleImageLayers::Layer& aLayer, nsIFrame** aAttachedToFrame, bool* aOutIsTransformedFixed) { // Compute {background|mask} origin area relative to aBorderArea now as we // may need it to compute the effective image size for a CSS gradient. nsRect positionArea; StyleGeometryBox layerOrigin = ComputeBoxValueForOrigin(aForFrame, aLayer.mOrigin); if (IsSVGStyleGeometryBox(layerOrigin)) { MOZ_ASSERT(aForFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT)); *aAttachedToFrame = aForFrame; positionArea = nsLayoutUtils::ComputeSVGReferenceRect(aForFrame, layerOrigin); nsPoint toStrokeBoxOffset = nsPoint(0, 0); if (layerOrigin != StyleGeometryBox::StrokeBox) { nsRect strokeBox = nsLayoutUtils::ComputeSVGReferenceRect( aForFrame, StyleGeometryBox::StrokeBox); toStrokeBoxOffset = positionArea.TopLeft() - strokeBox.TopLeft(); } // For SVG frames, the return value is relative to the stroke box return nsRect(toStrokeBoxOffset, positionArea.Size()); } MOZ_ASSERT(!aForFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT)); LayoutFrameType frameType = aForFrame->Type(); nsIFrame* geometryFrame = aForFrame; if (MOZ_UNLIKELY(frameType == LayoutFrameType::Scroll && StyleImageLayerAttachment::Local == aLayer.mAttachment)) { nsIScrollableFrame* scrollableFrame = do_QueryFrame(aForFrame); positionArea = nsRect(scrollableFrame->GetScrolledFrame()->GetPosition() // For the dir=rtl case: + scrollableFrame->GetScrollRange().TopLeft(), scrollableFrame->GetScrolledRect().Size()); // The ScrolledRect’s size does not include the borders or scrollbars, // reverse the handling of background-origin // compared to the common case below. if (layerOrigin == StyleGeometryBox::BorderBox) { nsMargin border = geometryFrame->GetUsedBorder(); border.ApplySkipSides(geometryFrame->GetSkipSides()); positionArea.Inflate(border); positionArea.Inflate(scrollableFrame->GetActualScrollbarSizes()); } else if (layerOrigin != StyleGeometryBox::PaddingBox) { nsMargin padding = geometryFrame->GetUsedPadding(); padding.ApplySkipSides(geometryFrame->GetSkipSides()); positionArea.Deflate(padding); NS_ASSERTION(layerOrigin == StyleGeometryBox::ContentBox, "unknown background-origin value"); } *aAttachedToFrame = aForFrame; return positionArea; } if (MOZ_UNLIKELY(frameType == LayoutFrameType::Canvas)) { geometryFrame = aForFrame->PrincipalChildList().FirstChild(); // geometryFrame might be null if this canvas is a page created // as an overflow container (e.g. the in-flow content has already // finished and this page only displays the continuations of // absolutely positioned content). if (geometryFrame) { positionArea = nsPlaceholderFrame::GetRealFrameFor(geometryFrame)->GetRect(); } } else { positionArea = nsRect(nsPoint(0, 0), aBorderArea.Size()); } // See the comment of StyleGeometryBox::MarginBox. // Hitting this assertion means we decide to turn on margin-box support for // positioned mask from CSS parser and style system. In this case, you // should *inflate* positionArea by the margin returning from // geometryFrame->GetUsedMargin() in the code chunk bellow. MOZ_ASSERT(aLayer.mOrigin != StyleGeometryBox::MarginBox, "StyleGeometryBox::MarginBox rendering is not supported yet.\n"); // {background|mask} images are tiled over the '{background|mask}-clip' area // but the origin of the tiling is based on the '{background|mask}-origin' // area. if (layerOrigin != StyleGeometryBox::BorderBox && geometryFrame) { nsMargin border = geometryFrame->GetUsedBorder(); if (layerOrigin != StyleGeometryBox::PaddingBox) { border += geometryFrame->GetUsedPadding(); NS_ASSERTION(layerOrigin == StyleGeometryBox::ContentBox, "unknown background-origin value"); } positionArea.Deflate(border); } nsIFrame* attachedToFrame = aForFrame; if (StyleImageLayerAttachment::Fixed == aLayer.mAttachment) { // If it's a fixed background attachment, then the image is placed // relative to the viewport, which is the area of the root frame // in a screen context or the page content frame in a print context. attachedToFrame = aPresContext->PresShell()->GetRootFrame(); NS_ASSERTION(attachedToFrame, "no root frame"); nsIFrame* pageContentFrame = nullptr; if (aPresContext->IsPaginated()) { pageContentFrame = nsLayoutUtils::GetClosestFrameOfType( aForFrame, LayoutFrameType::PageContent); if (pageContentFrame) { attachedToFrame = pageContentFrame; } // else this is an embedded shell and its root frame is what we want } // If the background is affected by a transform, treat is as if it // wasn't fixed. if (nsLayoutUtils::IsTransformed(aForFrame, attachedToFrame)) { attachedToFrame = aForFrame; *aOutIsTransformedFixed = true; } else { // Set the background positioning area to the viewport's area // (relative to aForFrame) positionArea = nsRect(-aForFrame->GetOffsetTo(attachedToFrame), attachedToFrame->GetSize()); if (!pageContentFrame) { // Subtract the size of scrollbars. nsIScrollableFrame* scrollableFrame = aPresContext->PresShell()->GetRootScrollFrameAsScrollable(); if (scrollableFrame) { nsMargin scrollbars = scrollableFrame->GetActualScrollbarSizes(); positionArea.Deflate(scrollbars); } } // If we have the dynamic toolbar, we need to expand the image area to // include the region under the dynamic toolbar, otherwise we will see a // blank space under the toolbar. if (aPresContext->IsRootContentDocumentCrossProcess() && aPresContext->HasDynamicToolbar()) { positionArea.SizeTo(nsLayoutUtils::ExpandHeightForDynamicToolbar( aPresContext, positionArea.Size())); } } } *aAttachedToFrame = attachedToFrame; return positionArea; } /* static */ nscoord nsCSSRendering::ComputeRoundedSize(nscoord aCurrentSize, nscoord aPositioningSize) { float repeatCount = NS_roundf(float(aPositioningSize) / float(aCurrentSize)); if (repeatCount < 1.0f) { return aPositioningSize; } return nscoord(NS_lround(float(aPositioningSize) / repeatCount)); } // Apply the CSS image sizing algorithm as it applies to background images. // See http://www.w3.org/TR/css3-background/#the-background-size . // aIntrinsicSize is the size that the background image 'would like to be'. // It can be found by calling nsImageRenderer::ComputeIntrinsicSize. static nsSize ComputeDrawnSizeForBackground( const CSSSizeOrRatio& aIntrinsicSize, const nsSize& aBgPositioningArea, const StyleBackgroundSize& aLayerSize, StyleImageLayerRepeat aXRepeat, StyleImageLayerRepeat aYRepeat) { nsSize imageSize; // Size is dictated by cover or contain rules. if (aLayerSize.IsContain() || aLayerSize.IsCover()) { nsImageRenderer::FitType fitType = aLayerSize.IsCover() ? nsImageRenderer::COVER : nsImageRenderer::CONTAIN; imageSize = nsImageRenderer::ComputeConstrainedSize( aBgPositioningArea, aIntrinsicSize.mRatio, fitType); } else { MOZ_ASSERT(aLayerSize.IsExplicitSize()); const auto& width = aLayerSize.explicit_size.width; const auto& height = aLayerSize.explicit_size.height; // No cover/contain constraint, use default algorithm. CSSSizeOrRatio specifiedSize; if (width.IsLengthPercentage()) { specifiedSize.SetWidth( width.AsLengthPercentage().Resolve(aBgPositioningArea.width)); } if (height.IsLengthPercentage()) { specifiedSize.SetHeight( height.AsLengthPercentage().Resolve(aBgPositioningArea.height)); } imageSize = nsImageRenderer::ComputeConcreteSize( specifiedSize, aIntrinsicSize, aBgPositioningArea); } // See https://www.w3.org/TR/css3-background/#background-size . // "If 'background-repeat' is 'round' for one (or both) dimensions, there is a // second // step. The UA must scale the image in that dimension (or both dimensions) // so that it fits a whole number of times in the background positioning // area." // "If 'background-repeat' is 'round' for one dimension only and if // 'background-size' // is 'auto' for the other dimension, then there is a third step: that other // dimension is scaled so that the original aspect ratio is restored." bool isRepeatRoundInBothDimensions = aXRepeat == StyleImageLayerRepeat::Round && aYRepeat == StyleImageLayerRepeat::Round; // Calculate the rounded size only if the background-size computation // returned a correct size for the image. if (imageSize.width && aXRepeat == StyleImageLayerRepeat::Round) { imageSize.width = nsCSSRendering::ComputeRoundedSize( imageSize.width, aBgPositioningArea.width); if (!isRepeatRoundInBothDimensions && aLayerSize.IsExplicitSize() && aLayerSize.explicit_size.height.IsAuto()) { // Restore intrinsic ratio if (aIntrinsicSize.mRatio) { imageSize.height = aIntrinsicSize.mRatio.Inverted().ApplyTo(imageSize.width); } } } // Calculate the rounded size only if the background-size computation // returned a correct size for the image. if (imageSize.height && aYRepeat == StyleImageLayerRepeat::Round) { imageSize.height = nsCSSRendering::ComputeRoundedSize( imageSize.height, aBgPositioningArea.height); if (!isRepeatRoundInBothDimensions && aLayerSize.IsExplicitSize() && aLayerSize.explicit_size.width.IsAuto()) { // Restore intrinsic ratio if (aIntrinsicSize.mRatio) { imageSize.width = aIntrinsicSize.mRatio.ApplyTo(imageSize.height); } } } return imageSize; } /* ComputeSpacedRepeatSize * aImageDimension: the image width/height * aAvailableSpace: the background positioning area width/height * aRepeat: determine whether the image is repeated * Returns the image size plus gap size of app units for use as spacing */ static nscoord ComputeSpacedRepeatSize(nscoord aImageDimension, nscoord aAvailableSpace, bool& aRepeat) { float ratio = static_cast(aAvailableSpace) / aImageDimension; if (ratio < 2.0f) { // If you can't repeat at least twice, then don't repeat. aRepeat = false; return aImageDimension; } aRepeat = true; return (aAvailableSpace - aImageDimension) / (NSToIntFloor(ratio) - 1); } /* static */ nscoord nsCSSRendering::ComputeBorderSpacedRepeatSize(nscoord aImageDimension, nscoord aAvailableSpace, nscoord& aSpace) { int32_t count = aImageDimension ? (aAvailableSpace / aImageDimension) : 0; aSpace = (aAvailableSpace - aImageDimension * count) / (count + 1); return aSpace + aImageDimension; } nsBackgroundLayerState nsCSSRendering::PrepareImageLayer( nsPresContext* aPresContext, nsIFrame* aForFrame, uint32_t aFlags, const nsRect& aBorderArea, const nsRect& aBGClipRect, const nsStyleImageLayers::Layer& aLayer, bool* aOutIsTransformedFixed) { /* * The properties we need to keep in mind when drawing style image * layers are: * * background-image/ mask-image * background-repeat/ mask-repeat * background-attachment * background-position/ mask-position * background-clip/ mask-clip * background-origin/ mask-origin * background-size/ mask-size * background-blend-mode * box-decoration-break * mask-mode * mask-composite * * (background-color applies to the entire element and not to individual * layers, so it is irrelevant to this method.) * * These properties have the following dependencies upon each other when * determining rendering: * * background-image/ mask-image * no dependencies * background-repeat/ mask-repeat * no dependencies * background-attachment * no dependencies * background-position/ mask-position * depends upon background-size/mask-size (for the image's scaled size) * and background-break (for the background positioning area) * background-clip/ mask-clip * no dependencies * background-origin/ mask-origin * depends upon background-attachment (only in the case where that value * is 'fixed') * background-size/ mask-size * depends upon box-decoration-break (for the background positioning area * for resolving percentages), background-image (for the image's intrinsic * size), background-repeat (if that value is 'round'), and * background-origin (for the background painting area, when * background-repeat is 'round') * background-blend-mode * no dependencies * mask-mode * no dependencies * mask-composite * no dependencies * box-decoration-break * no dependencies * * As a result of only-if dependencies we don't strictly do a topological * sort of the above properties when processing, but it's pretty close to one: * * background-clip/mask-clip (by caller) * background-image/ mask-image * box-decoration-break, background-origin/ mask origin * background-attachment (postfix for background-origin if 'fixed') * background-size/ mask-size * background-position/ mask-position * background-repeat/ mask-repeat */ uint32_t irFlags = 0; if (aFlags & nsCSSRendering::PAINTBG_SYNC_DECODE_IMAGES) { irFlags |= nsImageRenderer::FLAG_SYNC_DECODE_IMAGES; } if (aFlags & nsCSSRendering::PAINTBG_TO_WINDOW) { irFlags |= nsImageRenderer::FLAG_PAINTING_TO_WINDOW; } if (aFlags & nsCSSRendering::PAINTBG_HIGH_QUALITY_SCALING) { irFlags |= nsImageRenderer::FLAG_HIGH_QUALITY_SCALING; } // Only do partial bg image drawing in content documents: non-content // documents are viewed as UI of the browser and a partial draw of a bg image // might look weird in that context. if (StaticPrefs::layout_display_partial_background_images() && XRE_IsContentProcess() && !aPresContext->IsChrome()) { irFlags |= nsImageRenderer::FLAG_DRAW_PARTIAL_FRAMES; } nsBackgroundLayerState state(aForFrame, &aLayer.mImage, irFlags); if (!state.mImageRenderer.PrepareImage()) { // There's no image or it's not ready to be painted. if (aOutIsTransformedFixed && StyleImageLayerAttachment::Fixed == aLayer.mAttachment) { nsIFrame* attachedToFrame = aPresContext->PresShell()->GetRootFrame(); NS_ASSERTION(attachedToFrame, "no root frame"); nsIFrame* pageContentFrame = nullptr; if (aPresContext->IsPaginated()) { pageContentFrame = nsLayoutUtils::GetClosestFrameOfType( aForFrame, LayoutFrameType::PageContent); if (pageContentFrame) { attachedToFrame = pageContentFrame; } // else this is an embedded shell and its root frame is what we want } *aOutIsTransformedFixed = nsLayoutUtils::IsTransformed(aForFrame, attachedToFrame); } return state; } // The frame to which the background is attached nsIFrame* attachedToFrame = aForFrame; // Is the background marked 'fixed', but affected by a transform? bool transformedFixed = false; // Compute background origin area relative to aBorderArea now as we may need // it to compute the effective image size for a CSS gradient. nsRect positionArea = ComputeImageLayerPositioningArea( aPresContext, aForFrame, aBorderArea, aLayer, &attachedToFrame, &transformedFixed); if (aOutIsTransformedFixed) { *aOutIsTransformedFixed = transformedFixed; } // For background-attachment:fixed backgrounds, we'll override the area // where the background can be drawn to the viewport. nsRect bgClipRect = aBGClipRect; if (StyleImageLayerAttachment::Fixed == aLayer.mAttachment && !transformedFixed && (aFlags & nsCSSRendering::PAINTBG_TO_WINDOW)) { bgClipRect = positionArea + aBorderArea.TopLeft(); } StyleImageLayerRepeat repeatX = aLayer.mRepeat.mXRepeat; StyleImageLayerRepeat repeatY = aLayer.mRepeat.mYRepeat; // Scale the image as specified for background-size and background-repeat. // Also as required for proper background positioning when background-position // is defined with percentages. CSSSizeOrRatio intrinsicSize = state.mImageRenderer.ComputeIntrinsicSize(); nsSize bgPositionSize = positionArea.Size(); nsSize imageSize = ComputeDrawnSizeForBackground( intrinsicSize, bgPositionSize, aLayer.mSize, repeatX, repeatY); if (imageSize.width <= 0 || imageSize.height <= 0) return state; state.mImageRenderer.SetPreferredSize(intrinsicSize, imageSize); // Compute the anchor point. // // relative to aBorderArea.TopLeft() (which is where the top-left // of aForFrame's border-box will be rendered) nsPoint imageTopLeft; // Compute the position of the background now that the background's size is // determined. nsImageRenderer::ComputeObjectAnchorPoint(aLayer.mPosition, bgPositionSize, imageSize, &imageTopLeft, &state.mAnchor); state.mRepeatSize = imageSize; if (repeatX == StyleImageLayerRepeat::Space) { bool isRepeat; state.mRepeatSize.width = ComputeSpacedRepeatSize( imageSize.width, bgPositionSize.width, isRepeat); if (isRepeat) { imageTopLeft.x = 0; state.mAnchor.x = 0; } else { repeatX = StyleImageLayerRepeat::NoRepeat; } } if (repeatY == StyleImageLayerRepeat::Space) { bool isRepeat; state.mRepeatSize.height = ComputeSpacedRepeatSize( imageSize.height, bgPositionSize.height, isRepeat); if (isRepeat) { imageTopLeft.y = 0; state.mAnchor.y = 0; } else { repeatY = StyleImageLayerRepeat::NoRepeat; } } imageTopLeft += positionArea.TopLeft(); state.mAnchor += positionArea.TopLeft(); state.mDestArea = nsRect(imageTopLeft + aBorderArea.TopLeft(), imageSize); state.mFillArea = state.mDestArea; ExtendMode repeatMode = ExtendMode::CLAMP; if (repeatX == StyleImageLayerRepeat::Repeat || repeatX == StyleImageLayerRepeat::Round || repeatX == StyleImageLayerRepeat::Space) { state.mFillArea.x = bgClipRect.x; state.mFillArea.width = bgClipRect.width; repeatMode = ExtendMode::REPEAT_X; } if (repeatY == StyleImageLayerRepeat::Repeat || repeatY == StyleImageLayerRepeat::Round || repeatY == StyleImageLayerRepeat::Space) { state.mFillArea.y = bgClipRect.y; state.mFillArea.height = bgClipRect.height; /*** * We're repeating on the X axis already, * so if we have to repeat in the Y axis, * we really need to repeat in both directions. */ if (repeatMode == ExtendMode::REPEAT_X) { repeatMode = ExtendMode::REPEAT; } else { repeatMode = ExtendMode::REPEAT_Y; } } state.mImageRenderer.SetExtendMode(repeatMode); state.mImageRenderer.SetMaskOp(aLayer.mMaskMode); state.mFillArea.IntersectRect(state.mFillArea, bgClipRect); return state; } nsRect nsCSSRendering::GetBackgroundLayerRect( nsPresContext* aPresContext, nsIFrame* aForFrame, const nsRect& aBorderArea, const nsRect& aClipRect, const nsStyleImageLayers::Layer& aLayer, uint32_t aFlags) { Sides skipSides = aForFrame->GetSkipSides(); nsRect borderArea = BoxDecorationRectForBackground(aForFrame, aBorderArea, skipSides); nsBackgroundLayerState state = PrepareImageLayer( aPresContext, aForFrame, aFlags, borderArea, aClipRect, aLayer); return state.mFillArea; } // Begin table border-collapsing section // These functions were written to not disrupt the normal ones and yet satisfy // some additional requirements At some point, all functions should be unified // to include the additional functionality that these provide static nscoord RoundIntToPixel(nscoord aValue, nscoord aOneDevPixel, bool aRoundDown = false) { if (aOneDevPixel <= 0) { // We must be rendering to a device that has a resolution greater than // one device pixel! // In that case, aValue is as accurate as it's going to get. return aValue; } nscoord halfPixel = NSToCoordRound(aOneDevPixel / 2.0f); nscoord extra = aValue % aOneDevPixel; nscoord finalValue = (!aRoundDown && (extra >= halfPixel)) ? aValue + (aOneDevPixel - extra) : aValue - extra; return finalValue; } static nscoord RoundFloatToPixel(float aValue, nscoord aOneDevPixel, bool aRoundDown = false) { return RoundIntToPixel(NSToCoordRound(aValue), aOneDevPixel, aRoundDown); } static void SetPoly(const Rect& aRect, Point* poly) { poly[0].x = aRect.x; poly[0].y = aRect.y; poly[1].x = aRect.x + aRect.width; poly[1].y = aRect.y; poly[2].x = aRect.x + aRect.width; poly[2].y = aRect.y + aRect.height; poly[3].x = aRect.x; poly[3].y = aRect.y + aRect.height; } static void DrawDashedSegment(DrawTarget& aDrawTarget, nsRect aRect, nscoord aDashLength, nscolor aColor, int32_t aAppUnitsPerDevPixel, bool aHorizontal) { ColorPattern color(ToDeviceColor(aColor)); DrawOptions drawOptions(1.f, CompositionOp::OP_OVER, AntialiasMode::NONE); StrokeOptions strokeOptions; Float dash[2]; dash[0] = Float(aDashLength) / aAppUnitsPerDevPixel; dash[1] = dash[0]; strokeOptions.mDashPattern = dash; strokeOptions.mDashLength = MOZ_ARRAY_LENGTH(dash); if (aHorizontal) { nsPoint left = (aRect.TopLeft() + aRect.BottomLeft()) / 2; nsPoint right = (aRect.TopRight() + aRect.BottomRight()) / 2; strokeOptions.mLineWidth = Float(aRect.height) / aAppUnitsPerDevPixel; StrokeLineWithSnapping(left, right, aAppUnitsPerDevPixel, aDrawTarget, color, strokeOptions, drawOptions); } else { nsPoint top = (aRect.TopLeft() + aRect.TopRight()) / 2; nsPoint bottom = (aRect.BottomLeft() + aRect.BottomRight()) / 2; strokeOptions.mLineWidth = Float(aRect.width) / aAppUnitsPerDevPixel; StrokeLineWithSnapping(top, bottom, aAppUnitsPerDevPixel, aDrawTarget, color, strokeOptions, drawOptions); } } static void DrawSolidBorderSegment( DrawTarget& aDrawTarget, nsRect aRect, nscolor aColor, int32_t aAppUnitsPerDevPixel, mozilla::Side aStartBevelSide = mozilla::eSideTop, nscoord aStartBevelOffset = 0, mozilla::Side aEndBevelSide = mozilla::eSideTop, nscoord aEndBevelOffset = 0) { ColorPattern color(ToDeviceColor(aColor)); DrawOptions drawOptions(1.f, CompositionOp::OP_OVER, AntialiasMode::NONE); nscoord oneDevPixel = NSIntPixelsToAppUnits(1, aAppUnitsPerDevPixel); // We don't need to bevel single pixel borders if ((aRect.width == oneDevPixel) || (aRect.height == oneDevPixel) || ((0 == aStartBevelOffset) && (0 == aEndBevelOffset))) { // simple rectangle aDrawTarget.FillRect( NSRectToSnappedRect(aRect, aAppUnitsPerDevPixel, aDrawTarget), color, drawOptions); } else { // polygon with beveling Point poly[4]; SetPoly(NSRectToSnappedRect(aRect, aAppUnitsPerDevPixel, aDrawTarget), poly); Float startBevelOffset = NSAppUnitsToFloatPixels(aStartBevelOffset, aAppUnitsPerDevPixel); switch (aStartBevelSide) { case eSideTop: poly[0].x += startBevelOffset; break; case eSideBottom: poly[3].x += startBevelOffset; break; case eSideRight: poly[1].y += startBevelOffset; break; case eSideLeft: poly[0].y += startBevelOffset; } Float endBevelOffset = NSAppUnitsToFloatPixels(aEndBevelOffset, aAppUnitsPerDevPixel); switch (aEndBevelSide) { case eSideTop: poly[1].x -= endBevelOffset; break; case eSideBottom: poly[2].x -= endBevelOffset; break; case eSideRight: poly[2].y -= endBevelOffset; break; case eSideLeft: poly[3].y -= endBevelOffset; } RefPtr builder = aDrawTarget.CreatePathBuilder(); builder->MoveTo(poly[0]); builder->LineTo(poly[1]); builder->LineTo(poly[2]); builder->LineTo(poly[3]); builder->Close(); RefPtr path = builder->Finish(); aDrawTarget.Fill(path, color, drawOptions); } } static void GetDashInfo(nscoord aBorderLength, nscoord aDashLength, nscoord aOneDevPixel, int32_t& aNumDashSpaces, nscoord& aStartDashLength, nscoord& aEndDashLength) { aNumDashSpaces = 0; if (aStartDashLength + aDashLength + aEndDashLength >= aBorderLength) { aStartDashLength = aBorderLength; aEndDashLength = 0; } else { aNumDashSpaces = (aBorderLength - aDashLength) / (2 * aDashLength); // round down nscoord extra = aBorderLength - aStartDashLength - aEndDashLength - (((2 * aNumDashSpaces) - 1) * aDashLength); if (extra > 0) { nscoord half = RoundIntToPixel(extra / 2, aOneDevPixel); aStartDashLength += half; aEndDashLength += (extra - half); } } } void nsCSSRendering::DrawTableBorderSegment( DrawTarget& aDrawTarget, StyleBorderStyle aBorderStyle, nscolor aBorderColor, const nsRect& aBorder, int32_t aAppUnitsPerDevPixel, mozilla::Side aStartBevelSide, nscoord aStartBevelOffset, mozilla::Side aEndBevelSide, nscoord aEndBevelOffset) { bool horizontal = ((eSideTop == aStartBevelSide) || (eSideBottom == aStartBevelSide)); nscoord oneDevPixel = NSIntPixelsToAppUnits(1, aAppUnitsPerDevPixel); if ((oneDevPixel >= aBorder.width) || (oneDevPixel >= aBorder.height) || (StyleBorderStyle::Dashed == aBorderStyle) || (StyleBorderStyle::Dotted == aBorderStyle)) { // no beveling for 1 pixel border, dash or dot aStartBevelOffset = 0; aEndBevelOffset = 0; } switch (aBorderStyle) { case StyleBorderStyle::None: case StyleBorderStyle::Hidden: // NS_ASSERTION(false, "style of none or hidden"); break; case StyleBorderStyle::Dotted: case StyleBorderStyle::Dashed: { nscoord dashLength = (StyleBorderStyle::Dashed == aBorderStyle) ? DASH_LENGTH : DOT_LENGTH; // make the dash length proportional to the border thickness dashLength *= (horizontal) ? aBorder.height : aBorder.width; // make the min dash length for the ends 1/2 the dash length nscoord minDashLength = (StyleBorderStyle::Dashed == aBorderStyle) ? RoundFloatToPixel(((float)dashLength) / 2.0f, aAppUnitsPerDevPixel) : dashLength; minDashLength = std::max(minDashLength, oneDevPixel); nscoord numDashSpaces = 0; nscoord startDashLength = minDashLength; nscoord endDashLength = minDashLength; if (horizontal) { GetDashInfo(aBorder.width, dashLength, aAppUnitsPerDevPixel, numDashSpaces, startDashLength, endDashLength); nsRect rect(aBorder.x, aBorder.y, startDashLength, aBorder.height); DrawSolidBorderSegment(aDrawTarget, rect, aBorderColor, aAppUnitsPerDevPixel); rect.x += startDashLength + dashLength; rect.width = aBorder.width - (startDashLength + endDashLength + dashLength); DrawDashedSegment(aDrawTarget, rect, dashLength, aBorderColor, aAppUnitsPerDevPixel, horizontal); rect.x += rect.width; rect.width = endDashLength; DrawSolidBorderSegment(aDrawTarget, rect, aBorderColor, aAppUnitsPerDevPixel); } else { GetDashInfo(aBorder.height, dashLength, aAppUnitsPerDevPixel, numDashSpaces, startDashLength, endDashLength); nsRect rect(aBorder.x, aBorder.y, aBorder.width, startDashLength); DrawSolidBorderSegment(aDrawTarget, rect, aBorderColor, aAppUnitsPerDevPixel); rect.y += rect.height + dashLength; rect.height = aBorder.height - (startDashLength + endDashLength + dashLength); DrawDashedSegment(aDrawTarget, rect, dashLength, aBorderColor, aAppUnitsPerDevPixel, horizontal); rect.y += rect.height; rect.height = endDashLength; DrawSolidBorderSegment(aDrawTarget, rect, aBorderColor, aAppUnitsPerDevPixel); } } break; default: AutoTArray segments; GetTableBorderSolidSegments( segments, aBorderStyle, aBorderColor, aBorder, aAppUnitsPerDevPixel, aStartBevelSide, aStartBevelOffset, aEndBevelSide, aEndBevelOffset); for (const auto& segment : segments) { DrawSolidBorderSegment( aDrawTarget, segment.mRect, segment.mColor, aAppUnitsPerDevPixel, segment.mStartBevel.mSide, segment.mStartBevel.mOffset, segment.mEndBevel.mSide, segment.mEndBevel.mOffset); } break; } } void nsCSSRendering::GetTableBorderSolidSegments( nsTArray& aSegments, StyleBorderStyle aBorderStyle, nscolor aBorderColor, const nsRect& aBorder, int32_t aAppUnitsPerDevPixel, mozilla::Side aStartBevelSide, nscoord aStartBevelOffset, mozilla::Side aEndBevelSide, nscoord aEndBevelOffset) { const bool horizontal = eSideTop == aStartBevelSide || eSideBottom == aStartBevelSide; const nscoord oneDevPixel = NSIntPixelsToAppUnits(1, aAppUnitsPerDevPixel); switch (aBorderStyle) { case StyleBorderStyle::None: case StyleBorderStyle::Hidden: return; case StyleBorderStyle::Dotted: case StyleBorderStyle::Dashed: MOZ_ASSERT_UNREACHABLE("Caller should have checked"); return; case StyleBorderStyle::Groove: case StyleBorderStyle::Ridge: if ((horizontal && (oneDevPixel >= aBorder.height)) || (!horizontal && (oneDevPixel >= aBorder.width))) { aSegments.AppendElement( SolidBeveledBorderSegment{aBorder, aBorderColor, {aStartBevelSide, aStartBevelOffset}, {aEndBevelSide, aEndBevelOffset}}); } else { nscoord startBevel = (aStartBevelOffset > 0) ? RoundFloatToPixel(0.5f * (float)aStartBevelOffset, aAppUnitsPerDevPixel, true) : 0; nscoord endBevel = (aEndBevelOffset > 0) ? RoundFloatToPixel(0.5f * (float)aEndBevelOffset, aAppUnitsPerDevPixel, true) : 0; mozilla::Side ridgeGrooveSide = (horizontal) ? eSideTop : eSideLeft; // FIXME: In theory, this should use the visited-dependent // background color, but I don't care. nscolor bevelColor = MakeBevelColor(ridgeGrooveSide, aBorderStyle, aBorderColor); nsRect rect(aBorder); nscoord half; if (horizontal) { // top, bottom half = RoundFloatToPixel(0.5f * (float)aBorder.height, aAppUnitsPerDevPixel); rect.height = half; if (eSideTop == aStartBevelSide) { rect.x += startBevel; rect.width -= startBevel; } if (eSideTop == aEndBevelSide) { rect.width -= endBevel; } aSegments.AppendElement( SolidBeveledBorderSegment{rect, bevelColor, {aStartBevelSide, startBevel}, {aEndBevelSide, endBevel}}); } else { // left, right half = RoundFloatToPixel(0.5f * (float)aBorder.width, aAppUnitsPerDevPixel); rect.width = half; if (eSideLeft == aStartBevelSide) { rect.y += startBevel; rect.height -= startBevel; } if (eSideLeft == aEndBevelSide) { rect.height -= endBevel; } aSegments.AppendElement( SolidBeveledBorderSegment{rect, bevelColor, {aStartBevelSide, startBevel}, {aEndBevelSide, endBevel}}); } rect = aBorder; ridgeGrooveSide = (eSideTop == ridgeGrooveSide) ? eSideBottom : eSideRight; // FIXME: In theory, this should use the visited-dependent // background color, but I don't care. bevelColor = MakeBevelColor(ridgeGrooveSide, aBorderStyle, aBorderColor); if (horizontal) { rect.y = rect.y + half; rect.height = aBorder.height - half; if (eSideBottom == aStartBevelSide) { rect.x += startBevel; rect.width -= startBevel; } if (eSideBottom == aEndBevelSide) { rect.width -= endBevel; } aSegments.AppendElement( SolidBeveledBorderSegment{rect, bevelColor, {aStartBevelSide, startBevel}, {aEndBevelSide, endBevel}}); } else { rect.x = rect.x + half; rect.width = aBorder.width - half; if (eSideRight == aStartBevelSide) { rect.y += aStartBevelOffset - startBevel; rect.height -= startBevel; } if (eSideRight == aEndBevelSide) { rect.height -= endBevel; } aSegments.AppendElement( SolidBeveledBorderSegment{rect, bevelColor, {aStartBevelSide, startBevel}, {aEndBevelSide, endBevel}}); } } break; case StyleBorderStyle::Double: // We can only do "double" borders if the thickness of the border // is more than 2px. Otherwise, we fall through to painting a // solid border. if ((aBorder.width > 2 * oneDevPixel || horizontal) && (aBorder.height > 2 * oneDevPixel || !horizontal)) { nscoord startBevel = (aStartBevelOffset > 0) ? RoundFloatToPixel(0.333333f * (float)aStartBevelOffset, aAppUnitsPerDevPixel) : 0; nscoord endBevel = (aEndBevelOffset > 0) ? RoundFloatToPixel(0.333333f * (float)aEndBevelOffset, aAppUnitsPerDevPixel) : 0; if (horizontal) { // top, bottom nscoord thirdHeight = RoundFloatToPixel( 0.333333f * (float)aBorder.height, aAppUnitsPerDevPixel); // draw the top line or rect nsRect topRect(aBorder.x, aBorder.y, aBorder.width, thirdHeight); if (eSideTop == aStartBevelSide) { topRect.x += aStartBevelOffset - startBevel; topRect.width -= aStartBevelOffset - startBevel; } if (eSideTop == aEndBevelSide) { topRect.width -= aEndBevelOffset - endBevel; } aSegments.AppendElement( SolidBeveledBorderSegment{topRect, aBorderColor, {aStartBevelSide, startBevel}, {aEndBevelSide, endBevel}}); // draw the botom line or rect nscoord heightOffset = aBorder.height - thirdHeight; nsRect bottomRect(aBorder.x, aBorder.y + heightOffset, aBorder.width, aBorder.height - heightOffset); if (eSideBottom == aStartBevelSide) { bottomRect.x += aStartBevelOffset - startBevel; bottomRect.width -= aStartBevelOffset - startBevel; } if (eSideBottom == aEndBevelSide) { bottomRect.width -= aEndBevelOffset - endBevel; } aSegments.AppendElement( SolidBeveledBorderSegment{bottomRect, aBorderColor, {aStartBevelSide, startBevel}, {aEndBevelSide, endBevel}}); } else { // left, right nscoord thirdWidth = RoundFloatToPixel( 0.333333f * (float)aBorder.width, aAppUnitsPerDevPixel); nsRect leftRect(aBorder.x, aBorder.y, thirdWidth, aBorder.height); if (eSideLeft == aStartBevelSide) { leftRect.y += aStartBevelOffset - startBevel; leftRect.height -= aStartBevelOffset - startBevel; } if (eSideLeft == aEndBevelSide) { leftRect.height -= aEndBevelOffset - endBevel; } aSegments.AppendElement( SolidBeveledBorderSegment{leftRect, aBorderColor, {aStartBevelSide, startBevel}, {aEndBevelSide, endBevel}}); nscoord widthOffset = aBorder.width - thirdWidth; nsRect rightRect(aBorder.x + widthOffset, aBorder.y, aBorder.width - widthOffset, aBorder.height); if (eSideRight == aStartBevelSide) { rightRect.y += aStartBevelOffset - startBevel; rightRect.height -= aStartBevelOffset - startBevel; } if (eSideRight == aEndBevelSide) { rightRect.height -= aEndBevelOffset - endBevel; } aSegments.AppendElement( SolidBeveledBorderSegment{rightRect, aBorderColor, {aStartBevelSide, startBevel}, {aEndBevelSide, endBevel}}); } break; } // else fall through to solid [[fallthrough]]; case StyleBorderStyle::Solid: aSegments.AppendElement( SolidBeveledBorderSegment{aBorder, aBorderColor, {aStartBevelSide, aStartBevelOffset}, {aEndBevelSide, aEndBevelOffset}}); break; case StyleBorderStyle::Outset: case StyleBorderStyle::Inset: MOZ_ASSERT_UNREACHABLE( "inset, outset should have been converted to groove, ridge"); break; } } // End table border-collapsing section Rect nsCSSRendering::ExpandPaintingRectForDecorationLine( nsIFrame* aFrame, const StyleTextDecorationStyle aStyle, const Rect& aClippedRect, const Float aICoordInFrame, const Float aCycleLength, bool aVertical) { switch (aStyle) { case StyleTextDecorationStyle::Dotted: case StyleTextDecorationStyle::Dashed: case StyleTextDecorationStyle::Wavy: break; default: NS_ERROR("Invalid style was specified"); return aClippedRect; } nsBlockFrame* block = nullptr; // Note that when we paint the decoration lines in relative positioned // box, we should paint them like all of the boxes are positioned as static. nscoord framePosInBlockAppUnits = 0; for (nsIFrame* f = aFrame; f; f = f->GetParent()) { block = do_QueryFrame(f); if (block) { break; } framePosInBlockAppUnits += aVertical ? f->GetNormalPosition().y : f->GetNormalPosition().x; } NS_ENSURE_TRUE(block, aClippedRect); nsPresContext* pc = aFrame->PresContext(); Float framePosInBlock = Float(pc->AppUnitsToGfxUnits(framePosInBlockAppUnits)); int32_t rectPosInBlock = int32_t(NS_round(framePosInBlock + aICoordInFrame)); int32_t extraStartEdge = rectPosInBlock - (rectPosInBlock / int32_t(aCycleLength) * aCycleLength); Rect rect(aClippedRect); if (aVertical) { rect.y -= extraStartEdge; rect.height += extraStartEdge; } else { rect.x -= extraStartEdge; rect.width += extraStartEdge; } return rect; } // Converts a GfxFont to an SkFont // Either returns true if it was successful, or false if something went wrong static bool GetSkFontFromGfxFont(DrawTarget& aDrawTarget, gfxFont* aFont, SkFont& aSkFont) { RefPtr scaledFont = aFont->GetScaledFont(&aDrawTarget); if (!scaledFont) { return false; } ScaledFontBase* fontBase = static_cast(scaledFont.get()); SkTypeface* typeface = fontBase->GetSkTypeface(); if (!typeface) { return false; } aSkFont = SkFont(sk_ref_sp(typeface), SkFloatToScalar(fontBase->GetSize())); return true; } // Computes data used to position the decoration line within a // SkTextBlob, data is returned through aBounds static void GetPositioning( const nsCSSRendering::PaintDecorationLineParams& aParams, const Rect& aRect, Float aOneCSSPixel, Float aCenterBaselineOffset, SkScalar aBounds[]) { /** * How Positioning in Skia Works * Take the letter "n" for example * We set textPos as 0, 0 * This is represented in Skia like so (not to scale) * ^ * -10px | _ __ * | | '_ \ * -5px | | | | | * y-axis | |_| |_| * (0,0) -----------------------> * | 5px 10px * 5px | * | * 10px | * v * 0 on the x axis is a line that touches the bottom of the n * (0,0) is the bottom left-hand corner of the n character * Moving "up" from the n is going in a negative y direction * Moving "down" from the n is going in a positive y direction * * The intercepts that are returned in this arrangement will be * offset by the original point it starts at. (This happens in * the SkipInk function below). * * In Skia, text MUST be laid out such that the next character * in the RunBuffer is further along the x-axis than the previous * character, otherwise there is undefined/strange behavior. */ Float rectThickness = aParams.vertical ? aRect.Width() : aRect.Height(); // the upper and lower lines/edges of the under or over line SkScalar upperLine, lowerLine; if (aParams.decoration == mozilla::StyleTextDecorationLine::OVERLINE) { lowerLine = -aParams.offset + aParams.defaultLineThickness - aCenterBaselineOffset; upperLine = lowerLine - rectThickness; } else { // underlines in vertical text are offset from the center of // the text, and not the baseline // Skia sets the text at it's baseline so we have to offset it // for text in vertical-* writing modes upperLine = -aParams.offset - aCenterBaselineOffset; lowerLine = upperLine + rectThickness; } // set up the bounds, add in a little padding to the thickness of the line // (unless the line is <= 1 CSS pixel thick) Float lineThicknessPadding = aParams.lineSize.height > aOneCSSPixel ? 0.25f * aParams.lineSize.height : 0; // don't allow padding greater than 0.75 CSS pixel lineThicknessPadding = std::min(lineThicknessPadding, 0.75f * aOneCSSPixel); aBounds[0] = upperLine - lineThicknessPadding; aBounds[1] = lowerLine + lineThicknessPadding; } // positions an individual glyph according to the given offset static SkPoint GlyphPosition(const gfxTextRun::DetailedGlyph& aGlyph, const SkPoint& aTextPos, int32_t aAppUnitsPerDevPixel) { SkPoint point = {aGlyph.mOffset.x, aGlyph.mOffset.y}; // convert to device pixels point.fX /= (float)aAppUnitsPerDevPixel; point.fY /= (float)aAppUnitsPerDevPixel; // add offsets point.fX += aTextPos.fX; point.fY += aTextPos.fY; return point; } // returns a count of all the glyphs that will be rendered // excludes ligature continuations, includes the number of individual // glyph records. This includes the number of DetailedGlyphs that a single // CompressedGlyph record points to. This function is necessary because Skia // needs the total length of glyphs to add to it's run buffer before it creates // the RunBuffer object, and this cannot be resized later. static uint32_t CountAllGlyphs( const gfxTextRun* aTextRun, const gfxTextRun::CompressedGlyph* aCompressedGlyph, uint32_t aStringStart, uint32_t aStringEnd) { uint32_t totalGlyphCount = 0; for (const gfxTextRun::CompressedGlyph* cg = aCompressedGlyph + aStringStart; cg < aCompressedGlyph + aStringEnd; ++cg) { totalGlyphCount += cg->IsSimpleGlyph() ? 1 : cg->GetGlyphCount(); } return totalGlyphCount; } static void AddDetailedGlyph(const SkTextBlobBuilder::RunBuffer& aRunBuffer, const gfxTextRun::DetailedGlyph& aGlyph, int aIndex, float aAppUnitsPerDevPixel, SkPoint& aTextPos) { // add glyph ID to the run buffer at i aRunBuffer.glyphs[aIndex] = aGlyph.mGlyphID; // position the glyph correctly using the detailed offsets SkPoint position = GlyphPosition(aGlyph, aTextPos, aAppUnitsPerDevPixel); aRunBuffer.pos[2 * aIndex] = position.fX; aRunBuffer.pos[(2 * aIndex) + 1] = position.fY; // increase aTextPos.fx by the advance aTextPos.fX += ((float)aGlyph.mAdvance / aAppUnitsPerDevPixel); } static void AddSimpleGlyph(const SkTextBlobBuilder::RunBuffer& aRunBuffer, const gfxTextRun::CompressedGlyph& aGlyph, int aIndex, float aAppUnitsPerDevPixel, SkPoint& aTextPos) { aRunBuffer.glyphs[aIndex] = aGlyph.GetSimpleGlyph(); // simple glyphs are offset from 0, so we'll just use textPos aRunBuffer.pos[2 * aIndex] = aTextPos.fX; aRunBuffer.pos[(2 * aIndex) + 1] = aTextPos.fY; // increase aTextPos.fX by the advance aTextPos.fX += ((float)aGlyph.GetSimpleAdvance() / aAppUnitsPerDevPixel); } // Sets up a Skia TextBlob of the specified font, text position, and made up of // the glyphs between aStringStart and aStringEnd. Handles RTL and LTR text // and positions each glyph within the text blob static sk_sp CreateTextBlob( const gfxTextRun* aTextRun, const gfxTextRun::CompressedGlyph* aCompressedGlyph, const SkFont& aFont, const gfxTextRun::PropertyProvider::Spacing* aSpacing, uint32_t aStringStart, uint32_t aStringEnd, float aAppUnitsPerDevPixel, SkPoint& aTextPos, int32_t& aSpacingOffset) { // allocate space for the run buffer, then fill it with the glyphs uint32_t len = CountAllGlyphs(aTextRun, aCompressedGlyph, aStringStart, aStringEnd); if (len <= 0) { return nullptr; } SkTextBlobBuilder builder; const SkTextBlobBuilder::RunBuffer& run = builder.allocRunPos(aFont, len); // RTL text should be read in by glyph starting at aStringEnd - 1 down until // aStringStart. bool isRTL = aTextRun->IsRightToLeft(); uint32_t currIndex = isRTL ? aStringEnd - 1 : aStringStart; // textRun index // currIndex will be advanced by |step| until it reaches |limit|, which is the // final index to be handled (NOT one beyond the final index) int step = isRTL ? -1 : 1; uint32_t limit = isRTL ? aStringStart : aStringEnd - 1; uint32_t i = 0; // index into the SkTextBlob we're building while (true) { // Loop exit test is below, just before we update currIndex. aTextPos.fX += isRTL ? aSpacing[aSpacingOffset].mAfter / aAppUnitsPerDevPixel : aSpacing[aSpacingOffset].mBefore / aAppUnitsPerDevPixel; if (aCompressedGlyph[currIndex].IsSimpleGlyph()) { MOZ_ASSERT(i < len, "glyph count error!"); AddSimpleGlyph(run, aCompressedGlyph[currIndex], i, aAppUnitsPerDevPixel, aTextPos); i++; } else { // if it's detailed, potentially add multiple into run.glyphs uint32_t count = aCompressedGlyph[currIndex].GetGlyphCount(); if (count > 0) { gfxTextRun::DetailedGlyph* detailGlyph = aTextRun->GetDetailedGlyphs(currIndex); for (uint32_t d = isRTL ? count - 1 : 0; count; count--, d += step) { MOZ_ASSERT(i < len, "glyph count error!"); AddDetailedGlyph(run, detailGlyph[d], i, aAppUnitsPerDevPixel, aTextPos); i++; } } } aTextPos.fX += isRTL ? aSpacing[aSpacingOffset].mBefore / aAppUnitsPerDevPixel : aSpacing[aSpacingOffset].mAfter / aAppUnitsPerDevPixel; aSpacingOffset += step; if (currIndex == limit) { break; } currIndex += step; } MOZ_ASSERT(i == len, "glyph count error!"); return builder.make(); } // Given a TextBlob, the bounding lines, and the set of current intercepts this // function adds the intercepts for the current TextBlob into the given set of // previoulsy calculated intercepts. This set is either of length 0, or a // multiple of 2 (since every intersection with a piece of text results in two // intercepts: entering/exiting) static void GetTextIntercepts(const sk_sp& aBlob, const SkScalar aBounds[], nsTArray& aIntercepts) { // It's possible that we'll encounter a Windows exception deep inside // Skia's DirectWrite code while trying to get the intercepts. To avoid // crashing in this case, catch any such exception here and discard the // newly-added (and incompletely filled in) elements. int count = 0; MOZ_SEH_TRY { // https://skia.org/user/api/SkTextBlob_Reference#Text_Blob_Text_Intercepts count = aBlob->getIntercepts(aBounds, nullptr); if (count < 2) { return; } aBlob->getIntercepts(aBounds, aIntercepts.AppendElements(count)); } MOZ_SEH_EXCEPT(EXCEPTION_EXECUTE_HANDLER) { gfxCriticalNote << "Exception occurred getting text intercepts"; aIntercepts.TruncateLength(aIntercepts.Length() - count); } } // This function, given a set of intercepts that represent each intersection // between an under/overline and text, makes a series of calls to // PaintDecorationLineInternal that paints a series of clip rects which // implement the text-decoration-skip-ink property // Logic for where to place each clipped rect, and the length of each rect is // included here static void SkipInk(nsIFrame* aFrame, DrawTarget& aDrawTarget, const nsCSSRendering::PaintDecorationLineParams& aParams, const nsTArray& aIntercepts, Float aPadding, Rect& aRect) { nsCSSRendering::PaintDecorationLineParams clipParams = aParams; int length = aIntercepts.Length(); SkScalar startIntercept = 0; SkScalar endIntercept = 0; // keep track of the direction we are drawing the clipped rects in // for sideways text, our intercepts from the first glyph are actually // decreasing (towards the top edge of the page), so we use a negative // direction Float dir = 1.0f; Float lineStart = aParams.vertical ? aParams.pt.y : aParams.pt.x; Float lineEnd = lineStart + aParams.lineSize.width; if (aParams.sidewaysLeft) { dir = -1.0f; std::swap(lineStart, lineEnd); } for (int i = 0; i <= length; i += 2) { // handle start/end edge cases and set up general case startIntercept = (i > 0) ? (dir * aIntercepts[i - 1]) + lineStart : lineStart - (dir * aPadding); endIntercept = (i < length) ? (dir * aIntercepts[i]) + lineStart : lineEnd + (dir * aPadding); // remove padding at both ends for width // the start of the line is calculated so the padding removes just // enough so that the line starts at its normal position clipParams.lineSize.width = (dir * (endIntercept - startIntercept)) - (2.0 * aPadding); // Don't draw decoration lines that have a smaller width than 1, or half // the line-end padding dimension. if (clipParams.lineSize.width < std::max(aPadding * 0.5, 1.0)) { continue; } // Start the line right after the intercept's location plus room for // padding; snap the rect edges to device pixels for consistent rendering // of dots across separate fragments of a dotted line. if (aParams.vertical) { clipParams.pt.y = aParams.sidewaysLeft ? endIntercept + aPadding : startIntercept + aPadding; aRect.y = std::floor(clipParams.pt.y + 0.5); aRect.SetBottomEdge( std::floor(clipParams.pt.y + clipParams.lineSize.width + 0.5)); } else { clipParams.pt.x = startIntercept + aPadding; aRect.x = std::floor(clipParams.pt.x + 0.5); aRect.SetRightEdge( std::floor(clipParams.pt.x + clipParams.lineSize.width + 0.5)); } nsCSSRendering::PaintDecorationLineInternal(aFrame, aDrawTarget, clipParams, aRect); } } void nsCSSRendering::PaintDecorationLine( nsIFrame* aFrame, DrawTarget& aDrawTarget, const PaintDecorationLineParams& aParams) { NS_ASSERTION(aParams.style != StyleTextDecorationStyle::None, "aStyle is none"); Rect rect = ToRect(GetTextDecorationRectInternal(aParams.pt, aParams)); if (rect.IsEmpty() || !rect.Intersects(aParams.dirtyRect)) { return; } if (aParams.decoration != StyleTextDecorationLine::UNDERLINE && aParams.decoration != StyleTextDecorationLine::OVERLINE && aParams.decoration != StyleTextDecorationLine::LINE_THROUGH) { MOZ_ASSERT_UNREACHABLE("Invalid text decoration value"); return; } // Check if decoration line will skip past ascenders/descenders // text-decoration-skip-ink only applies to overlines/underlines mozilla::StyleTextDecorationSkipInk skipInk = aFrame->StyleText()->mTextDecorationSkipInk; bool skipInkEnabled = skipInk != mozilla::StyleTextDecorationSkipInk::None && aParams.decoration != StyleTextDecorationLine::LINE_THROUGH; if (!skipInkEnabled || aParams.glyphRange.Length() == 0) { PaintDecorationLineInternal(aFrame, aDrawTarget, aParams, rect); return; } // check if the frame is a text frame or not nsTextFrame* textFrame = nullptr; if (aFrame->IsTextFrame()) { textFrame = static_cast(aFrame); } else { PaintDecorationLineInternal(aFrame, aDrawTarget, aParams, rect); return; } // get text run and current text offset (for line wrapping) gfxTextRun* textRun = textFrame->GetTextRun(nsTextFrame::TextRunType::eInflated); // used for conversions from app units to device pixels int32_t appUnitsPerDevPixel = aFrame->PresContext()->AppUnitsPerDevPixel(); // pointer to the array of glyphs for this TextRun gfxTextRun::CompressedGlyph* characterGlyphs = textRun->GetCharacterGlyphs(); // get positioning info SkPoint textPos = {0, aParams.baselineOffset}; SkScalar bounds[] = {0, 0}; Float oneCSSPixel = aFrame->PresContext()->CSSPixelsToDevPixels(1.0f); if (!textRun->UseCenterBaseline()) { GetPositioning(aParams, rect, oneCSSPixel, 0, bounds); } // array for the text intercepts AutoTArray intercepts; // array for spacing data AutoTArray spacing; spacing.SetLength(aParams.glyphRange.Length()); if (aParams.provider != nullptr) { aParams.provider->GetSpacing(aParams.glyphRange, spacing.Elements()); } // loop through each glyph run // in most cases there will only be one bool isRTL = textRun->IsRightToLeft(); int32_t spacingOffset = isRTL ? aParams.glyphRange.Length() - 1 : 0; gfxTextRun::GlyphRunIterator iter(textRun, aParams.glyphRange, isRTL); // For any glyph run where we don't actually do skipping, we'll need to // advance the current position by its width. // (For runs we do process, CreateTextBlob will update the position.) auto currentGlyphRunAdvance = [&]() { return textRun->GetAdvanceWidth( gfxTextRun::Range(iter.StringStart(), iter.StringEnd()), aParams.provider) / appUnitsPerDevPixel; }; for (; !iter.AtEnd(); iter.NextRun()) { if (iter.GlyphRun()->mOrientation == mozilla::gfx::ShapedTextFlags::TEXT_ORIENT_VERTICAL_UPRIGHT || (iter.GlyphRun()->mIsCJK && skipInk == mozilla::StyleTextDecorationSkipInk::Auto)) { // We don't support upright text in vertical modes currently // (see https://bugzilla.mozilla.org/show_bug.cgi?id=1572294), // but we do need to update textPos so that following runs will be // correctly positioned. // We also don't apply skip-ink to CJK text runs because many fonts // have an underline that looks really bad if this is done // (see https://bugzilla.mozilla.org/show_bug.cgi?id=1573249), // when skip-ink is set to 'auto'. textPos.fX += currentGlyphRunAdvance(); continue; } gfxFont* font = iter.GlyphRun()->mFont; // Don't try to apply skip-ink to 'sbix' fonts like Apple Color Emoji, // because old macOS (10.9) may crash trying to retrieve glyph paths // that don't exist. if (font->GetFontEntry()->HasFontTable(TRUETYPE_TAG('s', 'b', 'i', 'x'))) { textPos.fX += currentGlyphRunAdvance(); continue; } // get a Skia version of the glyph run's font SkFont skiafont; if (!GetSkFontFromGfxFont(aDrawTarget, font, skiafont)) { PaintDecorationLineInternal(aFrame, aDrawTarget, aParams, rect); return; } // Create a text blob with correctly positioned glyphs. This also updates // textPos.fX with the advance of the glyphs. sk_sp textBlob = CreateTextBlob(textRun, characterGlyphs, skiafont, spacing.Elements(), iter.StringStart(), iter.StringEnd(), (float)appUnitsPerDevPixel, textPos, spacingOffset); if (!textBlob) { textPos.fX += currentGlyphRunAdvance(); continue; } if (textRun->UseCenterBaseline()) { // writing modes that use a center baseline need to be adjusted on a // font-by-font basis since Skia lines up the text on a alphabetic // baseline, but for some vertical-* writing modes the offset is from the // center. gfxFont::Metrics metrics = font->GetMetrics(nsFontMetrics::eHorizontal); Float centerToBaseline = (metrics.emAscent - metrics.emDescent) / 2.0f; GetPositioning(aParams, rect, oneCSSPixel, centerToBaseline, bounds); } // compute the text intercepts that need to be skipped GetTextIntercepts(textBlob, bounds, intercepts); } bool needsSkipInk = intercepts.Length() > 0; if (needsSkipInk) { // Padding between glyph intercepts and the decoration line: we use the // decoration line thickness, clamped to a minimum of 1px and a maximum // of 0.2em. Float padding = std::min(std::max(aParams.lineSize.height, oneCSSPixel), Float(textRun->GetFontGroup()->GetStyle()->size / 5.0)); SkipInk(aFrame, aDrawTarget, aParams, intercepts, padding, rect); } else { PaintDecorationLineInternal(aFrame, aDrawTarget, aParams, rect); } } void nsCSSRendering::PaintDecorationLineInternal( nsIFrame* aFrame, DrawTarget& aDrawTarget, const PaintDecorationLineParams& aParams, Rect aRect) { Float lineThickness = std::max(NS_round(aParams.lineSize.height), 1.0); DeviceColor color = ToDeviceColor(aParams.color); ColorPattern colorPat(color); StrokeOptions strokeOptions(lineThickness); DrawOptions drawOptions; Float dash[2]; AutoPopClips autoPopClips(&aDrawTarget); mozilla::layout::TextDrawTarget* textDrawer = nullptr; if (aDrawTarget.GetBackendType() == BackendType::WEBRENDER_TEXT) { textDrawer = static_cast(&aDrawTarget); } switch (aParams.style) { case StyleTextDecorationStyle::Solid: case StyleTextDecorationStyle::Double: break; case StyleTextDecorationStyle::Dashed: { autoPopClips.PushClipRect(aRect); Float dashWidth = lineThickness * DOT_LENGTH * DASH_LENGTH; dash[0] = dashWidth; dash[1] = dashWidth; strokeOptions.mDashPattern = dash; strokeOptions.mDashLength = MOZ_ARRAY_LENGTH(dash); strokeOptions.mLineCap = CapStyle::BUTT; aRect = ExpandPaintingRectForDecorationLine( aFrame, aParams.style, aRect, aParams.icoordInFrame, dashWidth * 2, aParams.vertical); // We should continue to draw the last dash even if it is not in the rect. aRect.width += dashWidth; break; } case StyleTextDecorationStyle::Dotted: { autoPopClips.PushClipRect(aRect); Float dashWidth = lineThickness * DOT_LENGTH; if (lineThickness > 2.0) { dash[0] = 0.f; dash[1] = dashWidth * 2.f; strokeOptions.mLineCap = CapStyle::ROUND; } else { dash[0] = dashWidth; dash[1] = dashWidth; } strokeOptions.mDashPattern = dash; strokeOptions.mDashLength = MOZ_ARRAY_LENGTH(dash); aRect = ExpandPaintingRectForDecorationLine( aFrame, aParams.style, aRect, aParams.icoordInFrame, dashWidth * 2, aParams.vertical); // We should continue to draw the last dot even if it is not in the rect. aRect.width += dashWidth; break; } case StyleTextDecorationStyle::Wavy: autoPopClips.PushClipRect(aRect); if (lineThickness > 2.0) { drawOptions.mAntialiasMode = AntialiasMode::SUBPIXEL; } else { // Don't use anti-aliasing here. Because looks like lighter color wavy // line at this case. And probably, users don't think the // non-anti-aliased wavy line is not pretty. drawOptions.mAntialiasMode = AntialiasMode::NONE; } break; default: NS_ERROR("Invalid style value!"); return; } // The block-direction position should be set to the middle of the line. if (aParams.vertical) { aRect.x += lineThickness / 2; } else { aRect.y += lineThickness / 2; } switch (aParams.style) { case StyleTextDecorationStyle::Solid: case StyleTextDecorationStyle::Dotted: case StyleTextDecorationStyle::Dashed: { Point p1 = aRect.TopLeft(); Point p2 = aParams.vertical ? aRect.BottomLeft() : aRect.TopRight(); if (textDrawer) { textDrawer->AppendDecoration(p1, p2, lineThickness, aParams.vertical, color, aParams.style); } else { aDrawTarget.StrokeLine(p1, p2, colorPat, strokeOptions, drawOptions); } return; } case StyleTextDecorationStyle::Double: { /** * We are drawing double line as: * * +-------------------------------------------+ * |XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX| ^ * |XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX| | lineThickness * |XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX| v * | | * | | * |XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX| ^ * |XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX| | lineThickness * |XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX| v * +-------------------------------------------+ */ Point p1a = aRect.TopLeft(); Point p2a = aParams.vertical ? aRect.BottomLeft() : aRect.TopRight(); if (aParams.vertical) { aRect.width -= lineThickness; } else { aRect.height -= lineThickness; } Point p1b = aParams.vertical ? aRect.TopRight() : aRect.BottomLeft(); Point p2b = aRect.BottomRight(); if (textDrawer) { textDrawer->AppendDecoration(p1a, p2a, lineThickness, aParams.vertical, color, StyleTextDecorationStyle::Solid); textDrawer->AppendDecoration(p1b, p2b, lineThickness, aParams.vertical, color, StyleTextDecorationStyle::Solid); } else { aDrawTarget.StrokeLine(p1a, p2a, colorPat, strokeOptions, drawOptions); aDrawTarget.StrokeLine(p1b, p2b, colorPat, strokeOptions, drawOptions); } return; } case StyleTextDecorationStyle::Wavy: { /** * We are drawing wavy line as: * * P: Path, X: Painted pixel * * +---------------------------------------+ * XX|X XXXXXX XXXXXX | * PP|PX XPPPPPPX XPPPPPPX | ^ * XX|XPX XPXXXXXXPX XPXXXXXXPX| | * | XPX XPX XPX XPX XP|X |adv * | XPXXXXXXPX XPXXXXXXPX X|PX | * | XPPPPPPX XPPPPPPX |XPX v * | XXXXXX XXXXXX | XX * +---------------------------------------+ * <---><---> ^ * adv flatLengthAtVertex rightMost * * 1. Always starts from top-left of the drawing area, however, we need * to draw the line from outside of the rect. Because the start * point of the line is not good style if we draw from inside it. * 2. First, draw horizontal line from outside the rect to top-left of * the rect; * 3. Goes down to bottom of the area at 45 degrees. * 4. Slides to right horizontaly, see |flatLengthAtVertex|. * 5. Goes up to top of the area at 45 degrees. * 6. Slides to right horizontaly. * 7. Repeat from 2 until reached to right-most edge of the area. * * In the vertical case, swap horizontal and vertical coordinates and * directions in the above description. */ Float& rectICoord = aParams.vertical ? aRect.y : aRect.x; Float& rectISize = aParams.vertical ? aRect.height : aRect.width; const Float rectBSize = aParams.vertical ? aRect.width : aRect.height; const Float adv = rectBSize - lineThickness; const Float flatLengthAtVertex = std::max((lineThickness - 1.0) * 2.0, 1.0); // Align the start of wavy lines to the nearest ancestor block. const Float cycleLength = 2 * (adv + flatLengthAtVertex); aRect = ExpandPaintingRectForDecorationLine( aFrame, aParams.style, aRect, aParams.icoordInFrame, cycleLength, aParams.vertical); if (textDrawer) { // Undo attempted centering Float& rectBCoord = aParams.vertical ? aRect.x : aRect.y; rectBCoord -= lineThickness / 2; textDrawer->AppendWavyDecoration(aRect, lineThickness, aParams.vertical, color); return; } // figure out if we can trim whole cycles from the left and right edges // of the line, to try and avoid creating an unnecessarily long and // complex path (but don't do this for webrender, ) const Float dirtyRectICoord = aParams.vertical ? aParams.dirtyRect.y : aParams.dirtyRect.x; int32_t skipCycles = floor((dirtyRectICoord - rectICoord) / cycleLength); if (skipCycles > 0) { rectICoord += skipCycles * cycleLength; rectISize -= skipCycles * cycleLength; } rectICoord += lineThickness / 2.0; Point pt(aRect.TopLeft()); Float& ptICoord = aParams.vertical ? pt.y.value : pt.x.value; Float& ptBCoord = aParams.vertical ? pt.x.value : pt.y.value; if (aParams.vertical) { ptBCoord += adv; } Float iCoordLimit = ptICoord + rectISize + lineThickness; const Float dirtyRectIMost = aParams.vertical ? aParams.dirtyRect.YMost() : aParams.dirtyRect.XMost(); skipCycles = floor((iCoordLimit - dirtyRectIMost) / cycleLength); if (skipCycles > 0) { iCoordLimit -= skipCycles * cycleLength; } RefPtr builder = aDrawTarget.CreatePathBuilder(); RefPtr path; ptICoord -= lineThickness; builder->MoveTo(pt); // 1 ptICoord = rectICoord; builder->LineTo(pt); // 2 // In vertical mode, to go "down" relative to the text we need to // decrease the block coordinate, whereas in horizontal we increase // it. So the sense of this flag is effectively inverted. bool goDown = !aParams.vertical; uint32_t iter = 0; while (ptICoord < iCoordLimit) { if (++iter > 1000) { // stroke the current path and start again, to avoid pathological // behavior in cairo with huge numbers of path segments path = builder->Finish(); aDrawTarget.Stroke(path, colorPat, strokeOptions, drawOptions); builder = aDrawTarget.CreatePathBuilder(); builder->MoveTo(pt); iter = 0; } ptICoord += adv; ptBCoord += goDown ? adv : -adv; builder->LineTo(pt); // 3 and 5 ptICoord += flatLengthAtVertex; builder->LineTo(pt); // 4 and 6 goDown = !goDown; } path = builder->Finish(); aDrawTarget.Stroke(path, colorPat, strokeOptions, drawOptions); return; } default: NS_ERROR("Invalid style value!"); } } Rect nsCSSRendering::DecorationLineToPath( const PaintDecorationLineParams& aParams) { NS_ASSERTION(aParams.style != StyleTextDecorationStyle::None, "aStyle is none"); Rect path; // To benefit from RVO, we return this from all return points Rect rect = ToRect(GetTextDecorationRectInternal(aParams.pt, aParams)); if (rect.IsEmpty() || !rect.Intersects(aParams.dirtyRect)) { return path; } if (aParams.decoration != StyleTextDecorationLine::UNDERLINE && aParams.decoration != StyleTextDecorationLine::OVERLINE && aParams.decoration != StyleTextDecorationLine::LINE_THROUGH) { MOZ_ASSERT_UNREACHABLE("Invalid text decoration value"); return path; } if (aParams.style != StyleTextDecorationStyle::Solid) { // For the moment, we support only solid text decorations. return path; } Float lineThickness = std::max(NS_round(aParams.lineSize.height), 1.0); // The block-direction position should be set to the middle of the line. if (aParams.vertical) { rect.x += lineThickness / 2; path = Rect(rect.TopLeft() - Point(lineThickness / 2, 0.0), Size(lineThickness, rect.Height())); } else { rect.y += lineThickness / 2; path = Rect(rect.TopLeft() - Point(0.0, lineThickness / 2), Size(rect.Width(), lineThickness)); } return path; } nsRect nsCSSRendering::GetTextDecorationRect( nsPresContext* aPresContext, const DecorationRectParams& aParams) { NS_ASSERTION(aPresContext, "aPresContext is null"); NS_ASSERTION(aParams.style != StyleTextDecorationStyle::None, "aStyle is none"); gfxRect rect = GetTextDecorationRectInternal(Point(0, 0), aParams); // The rect values are already rounded to nearest device pixels. nsRect r; r.x = aPresContext->GfxUnitsToAppUnits(rect.X()); r.y = aPresContext->GfxUnitsToAppUnits(rect.Y()); r.width = aPresContext->GfxUnitsToAppUnits(rect.Width()); r.height = aPresContext->GfxUnitsToAppUnits(rect.Height()); return r; } gfxRect nsCSSRendering::GetTextDecorationRectInternal( const Point& aPt, const DecorationRectParams& aParams) { NS_ASSERTION(aParams.style <= StyleTextDecorationStyle::Wavy, "Invalid aStyle value"); if (aParams.style == StyleTextDecorationStyle::None) { return gfxRect(0, 0, 0, 0); } bool canLiftUnderline = aParams.descentLimit >= 0.0; gfxFloat iCoord = aParams.vertical ? aPt.y : aPt.x; gfxFloat bCoord = aParams.vertical ? aPt.x : aPt.y; // 'left' and 'right' are relative to the line, so for vertical writing modes // they will actually become top and bottom of the rendered line. // Similarly, aLineSize.width and .height are actually length and thickness // of the line, which runs horizontally or vertically according to aVertical. const gfxFloat left = floor(iCoord + 0.5), right = floor(iCoord + aParams.lineSize.width + 0.5); // We compute |r| as if for a horizontal text run, and then swap vertical // and horizontal coordinates at the end if vertical was requested. gfxRect r(left, 0, right - left, 0); gfxFloat lineThickness = NS_round(aParams.lineSize.height); lineThickness = std::max(lineThickness, 1.0); gfxFloat defaultLineThickness = NS_round(aParams.defaultLineThickness); defaultLineThickness = std::max(defaultLineThickness, 1.0); gfxFloat ascent = NS_round(aParams.ascent); gfxFloat descentLimit = floor(aParams.descentLimit); gfxFloat suggestedMaxRectHeight = std::max(std::min(ascent, descentLimit), 1.0); r.height = lineThickness; if (aParams.style == StyleTextDecorationStyle::Double) { /** * We will draw double line as: * * +-------------------------------------------+ * |XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX| ^ * |XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX| | lineThickness * |XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX| v * | | ^ * | | | gap * | | v * |XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX| ^ * |XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX| | lineThickness * |XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX| v * +-------------------------------------------+ */ gfxFloat gap = NS_round(lineThickness / 2.0); gap = std::max(gap, 1.0); r.height = lineThickness * 2.0 + gap; if (canLiftUnderline) { if (r.Height() > suggestedMaxRectHeight) { // Don't shrink the line height, because the thickness has some meaning. // We can just shrink the gap at this time. r.height = std::max(suggestedMaxRectHeight, lineThickness * 2.0 + 1.0); } } } else if (aParams.style == StyleTextDecorationStyle::Wavy) { /** * We will draw wavy line as: * * +-------------------------------------------+ * |XXXXX XXXXXX XXXXXX | ^ * |XXXXXX XXXXXXXX XXXXXXXX | | lineThickness * |XXXXXXX XXXXXXXXXX XXXXXXXXXX| v * | XXX XXX XXX XXX XX| * | XXXXXXXXXX XXXXXXXXXX X| * | XXXXXXXX XXXXXXXX | * | XXXXXX XXXXXX | * +-------------------------------------------+ */ r.height = lineThickness > 2.0 ? lineThickness * 4.0 : lineThickness * 3.0; if (canLiftUnderline) { if (r.Height() > suggestedMaxRectHeight) { // Don't shrink the line height even if there is not enough space, // because the thickness has some meaning. E.g., the 1px wavy line and // 2px wavy line can be used for different meaning in IME selections // at same time. r.height = std::max(suggestedMaxRectHeight, lineThickness * 2.0); } } } gfxFloat baseline = floor(bCoord + aParams.ascent + 0.5); // Calculate adjusted offset based on writing-mode/orientation and thickness // of decoration line. The input value aParams.offset is the nominal position // (offset from baseline) where we would draw a single, infinitely-thin line; // but for a wavy or double line, we'll need to move the bounding rect of the // decoration outwards from the baseline so that an underline remains below // the glyphs, and an overline above them, despite the increased block-dir // extent of the decoration. // // So adjustments by r.Height() are used to make the wider line styles (wavy // and double) "grow" in the appropriate direction compared to the basic // single line. // // Note that at this point, the decoration rect is being calculated in line- // relative coordinates, where 'x' is line-rightwards, and 'y' is line- // upwards. We'll swap them to be physical coords at the end. gfxFloat offset = 0.0; if (aParams.decoration == StyleTextDecorationLine::UNDERLINE) { offset = aParams.offset; if (canLiftUnderline) { if (descentLimit < -offset + r.Height()) { // If we can ignore the offset and the decoration line is overflowing, // we should align the bottom edge of the decoration line rect if it's // possible. Otherwise, we should lift up the top edge of the rect as // far as possible. gfxFloat offsetBottomAligned = -descentLimit + r.Height(); gfxFloat offsetTopAligned = 0.0; offset = std::min(offsetBottomAligned, offsetTopAligned); } } } else if (aParams.decoration == StyleTextDecorationLine::OVERLINE) { // For overline, we adjust the offset by defaultlineThickness (the default // thickness of a single decoration line) because empirically it looks // better to draw the overline just inside rather than outside the font's // ascent, which is what nsTextFrame passes as aParams.offset (as fonts // don't provide an explicit overline-offset). offset = aParams.offset - defaultLineThickness + r.Height(); } else if (aParams.decoration == StyleTextDecorationLine::LINE_THROUGH) { // To maintain a consistent mid-point for line-through decorations, // we adjust the offset by half of the decoration rect's height. gfxFloat extra = floor(r.Height() / 2.0 + 0.5); extra = std::max(extra, lineThickness); // computes offset for when user specifies a decoration width since // aParams.offset is derived from the font metric's line height gfxFloat decorationThicknessOffset = (lineThickness - defaultLineThickness) / 2.0; offset = aParams.offset - lineThickness + extra + decorationThicknessOffset; } else { MOZ_ASSERT_UNREACHABLE("Invalid text decoration value"); } // Convert line-relative coordinate system (x = line-right, y = line-up) // to physical coords, and move the decoration rect to the calculated // offset from baseline. if (aParams.vertical) { std::swap(r.x, r.y); std::swap(r.width, r.height); // line-upwards in vertical mode = physical-right, so we /add/ offset // to baseline. Except in sideways-lr mode, where line-upwards will be // physical leftwards. if (aParams.sidewaysLeft) { r.x = baseline - floor(offset + 0.5); } else { r.x = baseline + floor(offset - r.Width() + 0.5); } } else { // line-upwards in horizontal mode = physical-up, but our physical coord // system works downwards, so we /subtract/ offset from baseline. r.y = baseline - floor(offset + 0.5); } return r; } #define MAX_BLUR_RADIUS 300 #define MAX_SPREAD_RADIUS 50 static inline gfxPoint ComputeBlurStdDev(nscoord aBlurRadius, int32_t aAppUnitsPerDevPixel, gfxFloat aScaleX, gfxFloat aScaleY) { // http://dev.w3.org/csswg/css3-background/#box-shadow says that the // standard deviation of the blur should be half the given blur value. gfxFloat blurStdDev = gfxFloat(aBlurRadius) / gfxFloat(aAppUnitsPerDevPixel); return gfxPoint( std::min((blurStdDev * aScaleX), gfxFloat(MAX_BLUR_RADIUS)) / 2.0, std::min((blurStdDev * aScaleY), gfxFloat(MAX_BLUR_RADIUS)) / 2.0); } static inline IntSize ComputeBlurRadius(nscoord aBlurRadius, int32_t aAppUnitsPerDevPixel, gfxFloat aScaleX = 1.0, gfxFloat aScaleY = 1.0) { gfxPoint scaledBlurStdDev = ComputeBlurStdDev(aBlurRadius, aAppUnitsPerDevPixel, aScaleX, aScaleY); return gfxAlphaBoxBlur::CalculateBlurRadius(scaledBlurStdDev); } // ----- // nsContextBoxBlur // ----- gfxContext* nsContextBoxBlur::Init(const nsRect& aRect, nscoord aSpreadRadius, nscoord aBlurRadius, int32_t aAppUnitsPerDevPixel, gfxContext* aDestinationCtx, const nsRect& aDirtyRect, const gfxRect* aSkipRect, uint32_t aFlags) { if (aRect.IsEmpty()) { mContext = nullptr; return nullptr; } IntSize blurRadius; IntSize spreadRadius; GetBlurAndSpreadRadius(aDestinationCtx->GetDrawTarget(), aAppUnitsPerDevPixel, aBlurRadius, aSpreadRadius, blurRadius, spreadRadius); mDestinationCtx = aDestinationCtx; // If not blurring, draw directly onto the destination device if (blurRadius.width <= 0 && blurRadius.height <= 0 && spreadRadius.width <= 0 && spreadRadius.height <= 0 && !(aFlags & FORCE_MASK)) { mContext = aDestinationCtx; return mContext; } // Convert from app units to device pixels gfxRect rect = nsLayoutUtils::RectToGfxRect(aRect, aAppUnitsPerDevPixel); gfxRect dirtyRect = nsLayoutUtils::RectToGfxRect(aDirtyRect, aAppUnitsPerDevPixel); dirtyRect.RoundOut(); gfxMatrix transform = aDestinationCtx->CurrentMatrixDouble(); rect = transform.TransformBounds(rect); mPreTransformed = !transform.IsIdentity(); // Create the temporary surface for blurring dirtyRect = transform.TransformBounds(dirtyRect); bool useHardwareAccel = !(aFlags & DISABLE_HARDWARE_ACCELERATION_BLUR); if (aSkipRect) { gfxRect skipRect = transform.TransformBounds(*aSkipRect); mOwnedContext = mAlphaBoxBlur.Init(aDestinationCtx, rect, spreadRadius, blurRadius, &dirtyRect, &skipRect, useHardwareAccel); } else { mOwnedContext = mAlphaBoxBlur.Init(aDestinationCtx, rect, spreadRadius, blurRadius, &dirtyRect, nullptr, useHardwareAccel); } mContext = mOwnedContext.get(); if (mContext) { // we don't need to blur if skipRect is equal to rect // and mContext will be nullptr mContext->Multiply(transform); } return mContext; } void nsContextBoxBlur::DoPaint() { if (mContext == mDestinationCtx) { return; } gfxContextMatrixAutoSaveRestore saveMatrix(mDestinationCtx); if (mPreTransformed) { mDestinationCtx->SetMatrix(Matrix()); } mAlphaBoxBlur.Paint(mDestinationCtx); } gfxContext* nsContextBoxBlur::GetContext() { return mContext; } /* static */ nsMargin nsContextBoxBlur::GetBlurRadiusMargin(nscoord aBlurRadius, int32_t aAppUnitsPerDevPixel) { IntSize blurRadius = ComputeBlurRadius(aBlurRadius, aAppUnitsPerDevPixel); nsMargin result; result.top = result.bottom = blurRadius.height * aAppUnitsPerDevPixel; result.left = result.right = blurRadius.width * aAppUnitsPerDevPixel; return result; } /* static */ void nsContextBoxBlur::BlurRectangle( gfxContext* aDestinationCtx, const nsRect& aRect, int32_t aAppUnitsPerDevPixel, RectCornerRadii* aCornerRadii, nscoord aBlurRadius, const sRGBColor& aShadowColor, const nsRect& aDirtyRect, const gfxRect& aSkipRect) { DrawTarget& aDestDrawTarget = *aDestinationCtx->GetDrawTarget(); if (aRect.IsEmpty()) { return; } Rect shadowGfxRect = NSRectToRect(aRect, aAppUnitsPerDevPixel); if (aBlurRadius <= 0) { ColorPattern color(ToDeviceColor(aShadowColor)); if (aCornerRadii) { RefPtr roundedRect = MakePathForRoundedRect(aDestDrawTarget, shadowGfxRect, *aCornerRadii); aDestDrawTarget.Fill(roundedRect, color); } else { aDestDrawTarget.FillRect(shadowGfxRect, color); } return; } 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->CurrentMatrixDouble(); // 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) { scaleX = transform._11; scaleY = transform._22; aDestinationCtx->SetMatrix(Matrix()); } else { transform = gfxMatrix(); } gfxPoint blurStdDev = ComputeBlurStdDev(aBlurRadius, aAppUnitsPerDevPixel, scaleX, scaleY); gfxRect dirtyRect = nsLayoutUtils::RectToGfxRect(aDirtyRect, aAppUnitsPerDevPixel); dirtyRect.RoundOut(); gfxRect shadowThebesRect = transform.TransformBounds(ThebesRect(shadowGfxRect)); dirtyRect = transform.TransformBounds(dirtyRect); gfxRect skipRect = transform.TransformBounds(aSkipRect); if (aCornerRadii) { aCornerRadii->Scale(scaleX, scaleY); } gfxAlphaBoxBlur::BlurRectangle(aDestinationCtx, shadowThebesRect, aCornerRadii, blurStdDev, aShadowColor, dirtyRect, skipRect); } /* static */ void nsContextBoxBlur::GetBlurAndSpreadRadius( DrawTarget* aDestDrawTarget, int32_t aAppUnitsPerDevPixel, nscoord aBlurRadius, nscoord aSpreadRadius, IntSize& aOutBlurRadius, IntSize& aOutSpreadRadius, bool aConstrainSpreadRadius) { // 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) Matrix transform = aDestDrawTarget->GetTransform(); // XXX: we could probably handle negative scales but for now it's easier just // to fallback gfxFloat scaleX, scaleY; if (transform.HasNonAxisAlignedTransform() || transform._11 <= 0.0 || transform._22 <= 0.0) { scaleX = 1; scaleY = 1; } else { scaleX = transform._11; scaleY = transform._22; } // compute a large or smaller blur radius aOutBlurRadius = ComputeBlurRadius(aBlurRadius, aAppUnitsPerDevPixel, scaleX, scaleY); aOutSpreadRadius = IntSize(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, sRGBColor& aShadowColor, nscoord aBlurRadiusAppUnits, nscoord aSpreadDistanceAppUnits, int32_t aAppUnitsPerDevPixel, bool aHasBorderRadius, RectCornerRadii& aInnerClipRectRadii, Rect aSkipRect, Point aShadowOffset) { if (aDestinationRect.IsEmpty()) { mContext = nullptr; return false; } gfxContextAutoSaveRestore autoRestore(aDestinationCtx); IntSize blurRadius; IntSize spreadRadius; // Convert the blur and spread radius to device pixels bool constrainSpreadRadius = false; GetBlurAndSpreadRadius(aDestinationCtx->GetDrawTarget(), 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. auto scale = aDestinationCtx->CurrentMatrix().ScaleFactors(); Matrix 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) { // If we don't have a rotation, we're pre-transforming all the rects. aDestinationCtx->SetMatrix(Matrix()); } else { // Don't touch anything, we have a rotation. transform = Matrix(); } Rect transformedDestRect = transform.TransformBounds(aDestinationRect); Rect transformedShadowClipRect = transform.TransformBounds(aShadowClipRect); Rect transformedSkipRect = transform.TransformBounds(aSkipRect); transformedDestRect.Round(); transformedShadowClipRect.Round(); transformedSkipRect.RoundIn(); for (size_t i = 0; i < 4; i++) { aInnerClipRectRadii[i].width = std::floor(scale.xScale * aInnerClipRectRadii[i].width); aInnerClipRectRadii[i].height = std::floor(scale.yScale * aInnerClipRectRadii[i].height); } mAlphaBoxBlur.BlurInsetBox(aDestinationCtx, transformedDestRect, transformedShadowClipRect, blurRadius, aShadowColor, aHasBorderRadius ? &aInnerClipRectRadii : nullptr, transformedSkipRect, aShadowOffset); return true; }