/* -*- 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 <ctime>

#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 <algorithm>
#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<nsInlineFrame*>(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->IsFrameOfType(nsIFrame::eLineParticipant)) {
        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<InlineBackgroundData> 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<nsInlineFrame*>(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<nsStyleBorder> GetBorderIfVisited(const ComputedStyle& aStyle) {
  Maybe<nsStyleBorder> 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<nsStyleBorder> visitedBorder = GetBorderIfVisited(*aStyle);
  return PaintBorderWithStyleBorder(
      aPresContext, aRenderingContext, aForFrame, aDirtyRect, aBorderArea,
      visitedBorder.refOr(*aStyle->StyleBorder()), aStyle, aFlags, aSkipSides);
}

Maybe<nsCSSBorderRenderer> nsCSSRendering::CreateBorderRenderer(
    nsPresContext* aPresContext, DrawTarget* aDrawTarget, nsIFrame* aForFrame,
    const nsRect& aDirtyRect, const nsRect& aBorderArea, ComputedStyle* aStyle,
    bool* aOutBorderIsEmpty, Sides aSkipSides) {
  Maybe<nsStyleBorder> 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<nsStyleBorder> 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<nsCSSBorderRenderer> 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<nsCSSBorderImageRenderer> 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<int>(borderStyles[0]),
      static_cast<int>(borderStyles[1]), static_cast<int>(borderStyles[2]),
      static_cast<int>(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<nsCSSBorderImageRenderer> 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<nsCSSBorderRenderer> 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<nsCSSBorderRenderer>
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<nsCSSBorderRenderer>
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<nsCSSBorderRenderer> 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<nscoord (*)(float)>(NSToCoordRoundWithClamp));
  // Adjust aTopLeftCoord by the specified % of the extra space.
  *aTopLeftCoord = aCoord.Resolve(
      extraSpace, static_cast<nscoord (*)(float)>(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> {
    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<nsIFrame*>(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 <html> 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<nsIFrame*>(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<PathBuilder> builder =
            aDrawTarget.CreatePathBuilder(FillRule::FILL_EVEN_ODD);
        AppendRectToPath(builder, shadowGfxRectPlusBlur);
        if (hasBorderRadius) {
          AppendRoundedRectToPath(builder, innerClipRect, borderRadii);
        } else {
          AppendRectToPath(builder, innerClipRect);
        }
        RefPtr<Path> 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<const StyleBoxShadow> 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<const StyleBoxShadow> 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<Path> 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()) {
    if (styleImage.IsRect()) {
      return false;
    }

    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<imgIContainer> 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 ComputeBoxValue(nsIFrame* aForFrame,
                                        StyleGeometryBox aBox) {
  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-clip.
    if (IsSVGStyleGeometryBox(aBox)) {
      return StyleGeometryBox::BorderBox;
    }
  } else {
    // For SVG elements without associated CSS layout box, the values
    // content-box, padding-box, border-box and margin-box compute to fill-box.
    if (IsHTMLStyleGeometryBox(aBox)) {
      return StyleGeometryBox::FillBox;
    }
  }

  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 = ComputeBoxValue(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::ComputeGeometryBox(aForFrame, layerClip);

    nsRect strokeBox = (layerClip == StyleGeometryBox::StrokeBox)
                           ? clipArea
                           : nsLayoutUtils::ComputeGeometryBox(
                                 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<Path> 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<Path> roundedRect =
      MakePathForRoundedRect(*drawTarget, bgAreaGfx, aClipState.mClippedRadii);
  aCtx->SetPath(roundedRect);
  aCtx->Fill();
  aCtx->Restore();
}

enum class ScrollbarColorKind {
  Thumb,
  Track,
};

static Maybe<nscolor> 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<nscolor> overrideColor =
              CalcScrollbarColor(aFrame, ScrollbarColorKind::Thumb)) {
        return *overrideColor;
      }
      break;
    }
    case StyleAppearance::ScrollbarVertical:
    case StyleAppearance::ScrollbarHorizontal:
    case StyleAppearance::Scrollcorner: {
      if (Maybe<nscolor> 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, &currentLayerClipState);
        }
        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 = ComputeBoxValue(aForFrame, aLayer.mOrigin);

  if (IsSVGStyleGeometryBox(layerOrigin)) {
    MOZ_ASSERT(aForFrame->HasAnyStateBits(NS_FRAME_SVG_LAYOUT));
    *aAttachedToFrame = aForFrame;

    positionArea = nsLayoutUtils::ComputeGeometryBox(aForFrame, layerOrigin);

    nsPoint toStrokeBoxOffset = nsPoint(0, 0);
    if (layerOrigin != StyleGeometryBox::StrokeBox) {
      nsRect strokeBox = nsLayoutUtils::ComputeGeometryBox(
          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<float>(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<PathBuilder> builder = aDrawTarget.CreatePathBuilder();
    builder->MoveTo(poly[0]);
    builder->LineTo(poly[1]);
    builder->LineTo(poly[2]);
    builder->LineTo(poly[3]);
    builder->Close();
    RefPtr<Path> 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<SolidBeveledBorderSegment, 3> 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<SolidBeveledBorderSegment>& 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> scaledFont = aFont->GetScaledFont(&aDrawTarget);
  if (!scaledFont) {
    return false;
  }

  ScaledFontBase* fontBase = static_cast<ScaledFontBase*>(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<const SkTextBlob> 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<const SkTextBlob>& aBlob,
                              const SkScalar aBounds[],
                              nsTArray<SkScalar>& 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<SkScalar>& 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<nsTextFrame*>(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<SkScalar, 256> intercepts;

  // array for spacing data
  AutoTArray<gfxTextRun::PropertyProvider::Spacing, 64> 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<const SkTextBlob> 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<mozilla::layout::TextDrawTarget*>(&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<PathBuilder> builder = aDrawTarget.CreatePathBuilder();
      RefPtr<Path> 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<Path> 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;
}
