haikuwebkit/Source/WebCore/display/css/DisplayBoxDecorationPainter...

1562 lines
70 KiB
C++

/*
* Copyright (C) 2020 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "config.h"
#include "DisplayBoxDecorationPainter.h"
#if ENABLE(LAYOUT_FORMATTING_CONTEXT)
#include "BorderEdge.h" // For BoxSideSet.
#include "Color.h"
#include "DisplayBoxDecorationData.h"
#include "DisplayBoxModelBox.h"
#include "DisplayBoxRareGeometry.h"
#include "DisplayPaintingContext.h"
#include "DisplayStyle.h"
#include "DisplayTree.h"
#include "FillLayer.h"
#include "GeometryUtilities.h"
#include "GraphicsContext.h"
#include "IntRect.h"
#include "LayoutPoint.h"
#include "ShadowData.h"
namespace WebCore {
namespace Display {
class BorderPainter {
public:
BorderPainter(const RectEdges<BorderEdge>& edges, const FloatRoundedRect& borderRect, BackgroundBleedAvoidance bleedAvoidance, bool includeLeftEdge, bool includeRightEdge)
: m_edges(edges)
, m_borderRect(borderRect)
, m_bleedAvoidance(bleedAvoidance)
, m_includeLeftEdge(includeLeftEdge)
, m_includeRightEdge(includeRightEdge)
{
}
void paintBorders(PaintingContext&) const;
private:
static bool edgesShareColor(const BorderEdge& firstEdge, const BorderEdge& secondEdge)
{
return firstEdge.color() == secondEdge.color();
}
static bool borderStyleFillsBorderArea(BorderStyle style)
{
return !(style == BorderStyle::Dotted || style == BorderStyle::Dashed || style == BorderStyle::Double);
}
static bool borderStyleHasInnerDetail(BorderStyle style)
{
return style == BorderStyle::Groove || style == BorderStyle::Ridge || style == BorderStyle::Double;
}
static bool styleRequiresClipPolygon(BorderStyle style)
{
return style == BorderStyle::Dotted || style == BorderStyle::Dashed; // These are drawn with a stroke, so we have to clip to get corner miters.
}
static bool borderStyleIsDottedOrDashed(BorderStyle style)
{
return style == BorderStyle::Dotted || style == BorderStyle::Dashed;
}
static bool borderWillArcInnerEdge(const FloatSize& firstRadius, const FloatSize& secondRadius)
{
return !firstRadius.isZero() || !secondRadius.isZero();
}
static bool borderStyleHasUnmatchedColorsAtCorner(BorderStyle, BoxSide, BoxSide adjacentSide);
static bool borderStylesRequireMitre(BoxSide, BoxSide adjacentSide, BorderStyle, BorderStyle adjacentStyle);
static FloatRoundedRect calculateAdjustedInnerBorder(const FloatRoundedRect& innerBorder, BoxSide);
static void calculateBorderStyleColor(BorderStyle, BoxSide, Color&);
FloatRect borderInnerRectAdjustedForBleedAvoidance(const PaintingContext&) const;
bool colorsMatchAtCorner(BoxSide, BoxSide adjacentSide) const;
bool colorNeedsAntiAliasAtCorner(BoxSide, BoxSide adjacentSide) const;
bool willBeOverdrawn(BoxSide, BoxSide adjacentSide) const;
bool joinRequiresMitre(BoxSide, BoxSide adjacentSide, bool allowOverdraw) const;
void clipBorderSidePolygon(PaintingContext&, const FloatRoundedRect& outerBorder, const FloatRoundedRect& innerBorder, BoxSide, bool firstEdgeMatches, bool secondEdgeMatches) const;
void drawLineForBoxSide(PaintingContext&, const FloatRect&, BoxSide, Color, BorderStyle, float adjacentWidth1, float adjacentWidth2, bool antialias) const;
void drawBoxSideFromPath(PaintingContext&, const FloatRect& borderRect, const Path& borderPath, float thickness, float drawThickness, BoxSide, Color, BorderStyle) const;
void paintOneBorderSide(PaintingContext&, const FloatRoundedRect& outerBorder, const FloatRoundedRect& innerBorder, const FloatRect& sideRect, BoxSide, const Path*, bool antialias, const Color* overrideColor) const;
void paintBorderSides(PaintingContext&, const FloatRoundedRect& outerBorder, const FloatRoundedRect& innerBorder, FloatSize innerBorderBleedAdjustment, BoxSideSet, bool antialias, const Color* overrideColor = nullptr) const;
void paintTranslucentBorderSides(PaintingContext&, const FloatRoundedRect& outerBorder, const FloatRoundedRect& innerBorder, FloatSize innerBorderBleedAdjustment, BoxSideSet, bool antialias) const;
const RectEdges<BorderEdge>& m_edges;
const FloatRoundedRect m_borderRect;
const BackgroundBleedAvoidance m_bleedAvoidance;
const bool m_includeLeftEdge;
const bool m_includeRightEdge;
};
// BorderStyle::Outset darkens the bottom and right (and maybe lightens the top and left)
// BorderStyle::Inset darkens the top and left (and maybe lightens the bottom and right)
bool BorderPainter::borderStyleHasUnmatchedColorsAtCorner(BorderStyle style, BoxSide side, BoxSide adjacentSide)
{
// These styles match at the top/left and bottom/right.
if (style == BorderStyle::Inset || style == BorderStyle::Groove || style == BorderStyle::Ridge || style == BorderStyle::Outset) {
BoxSideSet topRightSides = { BoxSideFlag::Top, BoxSideFlag::Right };
BoxSideSet bottomLeftSides = { BoxSideFlag::Bottom, BoxSideFlag::Left };
BoxSideSet usedSides { edgeFlagForSide(side), edgeFlagForSide(adjacentSide) };
return usedSides == topRightSides || usedSides == bottomLeftSides;
}
return false;
}
bool BorderPainter::colorsMatchAtCorner(BoxSide side, BoxSide adjacentSide) const
{
auto& edge = m_edges.at(side);
auto& adjacentEdge = m_edges.at(adjacentSide);
if (edge.shouldRender() != adjacentEdge.shouldRender())
return false;
if (!edgesShareColor(edge, adjacentEdge))
return false;
return !borderStyleHasUnmatchedColorsAtCorner(edge.style(), side, adjacentSide);
}
bool BorderPainter::colorNeedsAntiAliasAtCorner(BoxSide side, BoxSide adjacentSide) const
{
auto& edge = m_edges.at(side);
auto& adjacentEdge = m_edges.at(adjacentSide);
if (edge.color().isOpaque())
return false;
if (edge.shouldRender() != adjacentEdge.shouldRender())
return false;
if (!edgesShareColor(edge, adjacentEdge))
return true;
return borderStyleHasUnmatchedColorsAtCorner(edge.style(), side, adjacentSide);
}
void BorderPainter::calculateBorderStyleColor(BorderStyle style, BoxSide side, Color& color)
{
ASSERT(style == BorderStyle::Inset || style == BorderStyle::Outset);
// This values were derived empirically.
constexpr float baseDarkColorLuminance { 0.014443844f }; // Luminance of SRGBA<uint8_t> { 32, 32, 32 }
constexpr float baseLightColorLuminance { 0.83077f }; // Luminance of SRGBA<uint8_t> { 235, 235, 235 }
enum Operation { Darken, Lighten };
Operation operation = (side == BoxSide::Top || side == BoxSide::Left) == (style == BorderStyle::Inset) ? Darken : Lighten;
// Here we will darken the border decoration color when needed. This will yield a similar behavior as in FF.
if (operation == Darken) {
if (color.luminance() > baseDarkColorLuminance)
color = color.darkened();
} else {
if (color.luminance() < baseLightColorLuminance)
color = color.lightened();
}
}
// This assumes that we draw in order: top, bottom, left, right.
bool BorderPainter::willBeOverdrawn(BoxSide side, BoxSide adjacentSide) const
{
switch (side) {
case BoxSide::Top:
case BoxSide::Bottom: {
auto& edge = m_edges.at(side);
auto& adjacentEdge = m_edges.at(adjacentSide);
if (adjacentEdge.presentButInvisible())
return false;
if (!edgesShareColor(edge, adjacentEdge) && !adjacentEdge.color().isOpaque())
return false;
if (!borderStyleFillsBorderArea(adjacentEdge.style()))
return false;
return true;
}
case BoxSide::Left:
case BoxSide::Right:
// These draw last, so are never overdrawn.
return false;
}
return false;
}
bool BorderPainter::borderStylesRequireMitre(BoxSide side, BoxSide adjacentSide, BorderStyle style, BorderStyle adjacentStyle)
{
if (style == BorderStyle::Double || adjacentStyle == BorderStyle::Double || adjacentStyle == BorderStyle::Groove || adjacentStyle == BorderStyle::Ridge)
return true;
if (borderStyleIsDottedOrDashed(style) != borderStyleIsDottedOrDashed(adjacentStyle))
return true;
if (style != adjacentStyle)
return true;
return borderStyleHasUnmatchedColorsAtCorner(style, side, adjacentSide);
}
FloatRoundedRect BorderPainter::calculateAdjustedInnerBorder(const FloatRoundedRect& innerBorder, BoxSide side)
{
// Expand the inner border as necessary to make it a rounded rect (i.e. radii contained within each edge).
// This function relies on the fact we only get radii not contained within each edge if one of the radii
// for an edge is zero, so we can shift the arc towards the zero radius corner.
auto newRadii = innerBorder.radii();
auto newRect = innerBorder.rect();
float overshoot;
float maxRadii;
switch (side) {
case BoxSide::Top:
overshoot = newRadii.topLeft().width() + newRadii.topRight().width() - newRect.width();
if (overshoot > 0) {
ASSERT(!(newRadii.topLeft().width() && newRadii.topRight().width()));
newRect.setWidth(newRect.width() + overshoot);
if (!newRadii.topLeft().width())
newRect.move(-overshoot, 0);
}
newRadii.setBottomLeft({ });
newRadii.setBottomRight({ });
maxRadii = std::max(newRadii.topLeft().height(), newRadii.topRight().height());
if (maxRadii > newRect.height())
newRect.setHeight(maxRadii);
break;
case BoxSide::Bottom:
overshoot = newRadii.bottomLeft().width() + newRadii.bottomRight().width() - newRect.width();
if (overshoot > 0) {
ASSERT(!(newRadii.bottomLeft().width() && newRadii.bottomRight().width()));
newRect.setWidth(newRect.width() + overshoot);
if (!newRadii.bottomLeft().width())
newRect.move(-overshoot, 0);
}
newRadii.setTopLeft({ });
newRadii.setTopRight({ });
maxRadii = std::max(newRadii.bottomLeft().height(), newRadii.bottomRight().height());
if (maxRadii > newRect.height()) {
newRect.move(0, newRect.height() - maxRadii);
newRect.setHeight(maxRadii);
}
break;
case BoxSide::Left:
overshoot = newRadii.topLeft().height() + newRadii.bottomLeft().height() - newRect.height();
if (overshoot > 0) {
ASSERT(!(newRadii.topLeft().height() && newRadii.bottomLeft().height()));
newRect.setHeight(newRect.height() + overshoot);
if (!newRadii.topLeft().height())
newRect.move(0, -overshoot);
}
newRadii.setTopRight({ });
newRadii.setBottomRight({ });
maxRadii = std::max(newRadii.topLeft().width(), newRadii.bottomLeft().width());
if (maxRadii > newRect.width())
newRect.setWidth(maxRadii);
break;
case BoxSide::Right:
overshoot = newRadii.topRight().height() + newRadii.bottomRight().height() - newRect.height();
if (overshoot > 0) {
ASSERT(!(newRadii.topRight().height() && newRadii.bottomRight().height()));
newRect.setHeight(newRect.height() + overshoot);
if (!newRadii.topRight().height())
newRect.move(0, -overshoot);
}
newRadii.setTopLeft({ });
newRadii.setBottomLeft({ });
maxRadii = std::max(newRadii.topRight().width(), newRadii.bottomRight().width());
if (maxRadii > newRect.width()) {
newRect.move(newRect.width() - maxRadii, 0);
newRect.setWidth(maxRadii);
}
break;
}
return FloatRoundedRect { newRect, newRadii };
}
bool BorderPainter::joinRequiresMitre(BoxSide side, BoxSide adjacentSide, bool allowOverdraw) const
{
auto& edge = m_edges.at(side);
auto& adjacentEdge = m_edges.at(adjacentSide);
if ((edge.isTransparent() && adjacentEdge.isTransparent()) || !adjacentEdge.isPresent())
return false;
if (allowOverdraw && willBeOverdrawn(side, adjacentSide))
return false;
if (!edgesShareColor(edge, adjacentEdge))
return true;
if (borderStylesRequireMitre(side, adjacentSide, edge.style(), adjacentEdge.style()))
return true;
return false;
}
void BorderPainter::drawBoxSideFromPath(PaintingContext& paintingContext, const FloatRect& borderRect, const Path& borderPath, float thickness, float drawThickness, BoxSide side, Color color, BorderStyle borderStyle) const
{
if (thickness <= 0)
return;
if (borderStyle == BorderStyle::Double && thickness < 3)
borderStyle = BorderStyle::Solid;
switch (borderStyle) {
case BorderStyle::None:
case BorderStyle::Hidden:
return;
case BorderStyle::Dotted:
case BorderStyle::Dashed: {
paintingContext.context.setStrokeColor(color);
// The stroke is doubled here because the provided path is the
// outside edge of the border so half the stroke is clipped off.
// The extra multiplier is so that the clipping mask can antialias
// the edges to prevent jaggies.
paintingContext.context.setStrokeThickness(drawThickness * 2 * 1.1f);
paintingContext.context.setStrokeStyle(borderStyle == BorderStyle::Dashed ? DashedStroke : DottedStroke);
// If the number of dashes that fit in the path is odd and non-integral then we
// will have an awkwardly-sized dash at the end of the path. To try to avoid that
// here, we simply make the whitespace dashes ever so slightly bigger.
// FIXME: This could be even better if we tried to manipulate the dash offset
// and possibly the gapLength to get the corners dash-symmetrical.
float dashLength = thickness * ((borderStyle == BorderStyle::Dashed) ? 3.0f : 1.0f);
float gapLength = dashLength;
float numberOfDashes = borderPath.length() / dashLength;
// Don't try to show dashes if we have less than 2 dashes + 2 gaps.
// FIXME: should do this test per side.
if (numberOfDashes >= 4) {
bool evenNumberOfFullDashes = !((int)numberOfDashes % 2);
bool integralNumberOfDashes = !(numberOfDashes - (int)numberOfDashes);
if (!evenNumberOfFullDashes && !integralNumberOfDashes) {
float numberOfGaps = numberOfDashes / 2;
gapLength += (dashLength / numberOfGaps);
}
DashArray lineDash;
lineDash.append(dashLength);
lineDash.append(gapLength);
paintingContext.context.setLineDash(lineDash, dashLength);
}
// FIXME: stroking the border path causes issues with tight corners:
// https://bugs.webkit.org/show_bug.cgi?id=58711
// Also, to get the best appearance we should stroke a path between the two borders.
paintingContext.context.strokePath(borderPath);
return;
}
case BorderStyle::Double: {
// Get the inner border rects for both the outer border line and the inner border line
// FIXME: Rect edges.
float outerBorderTopWidth = m_edges.top().outerWidth();
float innerBorderTopWidth = m_edges.top().innerWidth();
float outerBorderRightWidth = m_edges.right().outerWidth();
float innerBorderRightWidth = m_edges.right().innerWidth();
float outerBorderBottomWidth = m_edges.bottom().outerWidth();
float innerBorderBottomWidth = m_edges.bottom().innerWidth();
float outerBorderLeftWidth = m_edges.left().outerWidth();
float innerBorderLeftWidth = m_edges.left().innerWidth();
// Draw inner border line
{
GraphicsContextStateSaver stateSaver(paintingContext.context);
auto innerClip = roundedInsetBorderForRect(borderRect, m_borderRect.radii(), { innerBorderTopWidth, innerBorderRightWidth, innerBorderBottomWidth, innerBorderLeftWidth }, m_includeLeftEdge, m_includeRightEdge);
paintingContext.context.clipRoundedRect(innerClip);
drawBoxSideFromPath(paintingContext, borderRect, borderPath, thickness, drawThickness, side, color, BorderStyle::Solid);
}
// Draw outer border line
{
GraphicsContextStateSaver stateSaver(paintingContext.context);
auto outerRect = borderRect;
if (m_bleedAvoidance == BackgroundBleedAvoidance::UseTransparencyLayer) {
outerRect.inflate(1);
++outerBorderTopWidth;
++outerBorderBottomWidth;
++outerBorderLeftWidth;
++outerBorderRightWidth;
}
auto outerClip = roundedInsetBorderForRect(outerRect, m_borderRect.radii(), { outerBorderTopWidth, outerBorderRightWidth, outerBorderBottomWidth, outerBorderLeftWidth }, m_includeLeftEdge, m_includeRightEdge);
paintingContext.context.clipOutRoundedRect(outerClip);
drawBoxSideFromPath(paintingContext, borderRect, borderPath, thickness, drawThickness, side, color, BorderStyle::Solid);
}
return;
}
case BorderStyle::Ridge:
case BorderStyle::Groove:
{
BorderStyle s1;
BorderStyle s2;
if (borderStyle == BorderStyle::Groove) {
s1 = BorderStyle::Inset;
s2 = BorderStyle::Outset;
} else {
s1 = BorderStyle::Outset;
s2 = BorderStyle::Inset;
}
// Paint full border
drawBoxSideFromPath(paintingContext, borderRect, borderPath, thickness, drawThickness, side, color, s1);
// Paint inner only
GraphicsContextStateSaver stateSaver(paintingContext.context);
// FIXME: Should have pixel snapped these at display tree generation time.
// FIXME: RectEdges
float topWidth { m_edges.top().widthForPainting() / 2 };
float bottomWidth { m_edges.bottom().widthForPainting() / 2 };
float leftWidth { m_edges.left().widthForPainting() / 2 };
float rightWidth { m_edges.right().widthForPainting() / 2 };
auto clipRect = roundedInsetBorderForRect(borderRect, m_borderRect.radii(), { topWidth, rightWidth, bottomWidth, leftWidth }, m_includeLeftEdge, m_includeRightEdge);
paintingContext.context.clipRoundedRect(clipRect);
drawBoxSideFromPath(paintingContext, borderRect, borderPath, thickness, drawThickness, side, color, s2);
return;
}
case BorderStyle::Inset:
case BorderStyle::Outset:
calculateBorderStyleColor(borderStyle, side, color);
break;
default:
break;
}
paintingContext.context.setStrokeStyle(NoStroke);
paintingContext.context.setFillColor(color);
paintingContext.context.drawRect(borderRect);
}
void BorderPainter::clipBorderSidePolygon(PaintingContext& paintingContext, const FloatRoundedRect& outerBorder, const FloatRoundedRect& innerBorder, BoxSide side, bool firstEdgeMatches, bool secondEdgeMatches) const
{
const FloatRect& outerRect = outerBorder.rect();
const FloatRect& innerRect = innerBorder.rect();
// For each side, create a quad that encompasses all parts of that side that may draw,
// including areas inside the innerBorder.
//
// 0----------------3
// 0 \ / 0
// |\ 1----------- 2 /|
// | 1 1 |
// | | | |
// | | | |
// | 2 2 |
// |/ 1------------2 \|
// 3 / \ 3
// 0----------------3
//
Vector<FloatPoint> quad;
quad.reserveInitialCapacity(4);
switch (side) {
case BoxSide::Top:
quad.uncheckedAppend(outerRect.minXMinYCorner());
quad.uncheckedAppend(innerRect.minXMinYCorner());
quad.uncheckedAppend(innerRect.maxXMinYCorner());
quad.uncheckedAppend(outerRect.maxXMinYCorner());
if (!innerBorder.radii().topLeft().isZero())
findIntersection(outerRect.minXMinYCorner(), innerRect.minXMinYCorner(), innerRect.minXMaxYCorner(), innerRect.maxXMinYCorner(), quad[1]);
if (!innerBorder.radii().topRight().isZero())
findIntersection(outerRect.maxXMinYCorner(), innerRect.maxXMinYCorner(), innerRect.minXMinYCorner(), innerRect.maxXMaxYCorner(), quad[2]);
break;
case BoxSide::Left:
quad.uncheckedAppend(outerRect.minXMinYCorner());
quad.uncheckedAppend(innerRect.minXMinYCorner());
quad.uncheckedAppend(innerRect.minXMaxYCorner());
quad.uncheckedAppend(outerRect.minXMaxYCorner());
if (!innerBorder.radii().topLeft().isZero())
findIntersection(outerRect.minXMinYCorner(), innerRect.minXMinYCorner(), innerRect.minXMaxYCorner(), innerRect.maxXMinYCorner(), quad[1]);
if (!innerBorder.radii().bottomLeft().isZero())
findIntersection(outerRect.minXMaxYCorner(), innerRect.minXMaxYCorner(), innerRect.minXMinYCorner(), innerRect.maxXMaxYCorner(), quad[2]);
break;
case BoxSide::Bottom:
quad.uncheckedAppend(outerRect.minXMaxYCorner());
quad.uncheckedAppend(innerRect.minXMaxYCorner());
quad.uncheckedAppend(innerRect.maxXMaxYCorner());
quad.uncheckedAppend(outerRect.maxXMaxYCorner());
if (!innerBorder.radii().bottomLeft().isZero())
findIntersection(outerRect.minXMaxYCorner(), innerRect.minXMaxYCorner(), innerRect.minXMinYCorner(), innerRect.maxXMaxYCorner(), quad[1]);
if (!innerBorder.radii().bottomRight().isZero())
findIntersection(outerRect.maxXMaxYCorner(), innerRect.maxXMaxYCorner(), innerRect.maxXMinYCorner(), innerRect.minXMaxYCorner(), quad[2]);
break;
case BoxSide::Right:
quad.uncheckedAppend(outerRect.maxXMinYCorner());
quad.uncheckedAppend(innerRect.maxXMinYCorner());
quad.uncheckedAppend(innerRect.maxXMaxYCorner());
quad.uncheckedAppend(outerRect.maxXMaxYCorner());
if (!innerBorder.radii().topRight().isZero())
findIntersection(outerRect.maxXMinYCorner(), innerRect.maxXMinYCorner(), innerRect.minXMinYCorner(), innerRect.maxXMaxYCorner(), quad[1]);
if (!innerBorder.radii().bottomRight().isZero())
findIntersection(outerRect.maxXMaxYCorner(), innerRect.maxXMaxYCorner(), innerRect.maxXMinYCorner(), innerRect.minXMaxYCorner(), quad[2]);
break;
}
// If the border matches both of its adjacent sides, don't anti-alias the clip, and
// if neither side matches, anti-alias the clip.
if (firstEdgeMatches == secondEdgeMatches) {
bool wasAntialiased = paintingContext.context.shouldAntialias();
paintingContext.context.setShouldAntialias(!firstEdgeMatches);
paintingContext.context.clipPath(Path::polygonPathFromPoints(quad), WindRule::NonZero);
paintingContext.context.setShouldAntialias(wasAntialiased);
return;
}
// Square off the end which shouldn't be affected by antialiasing, and clip.
Vector<FloatPoint> firstQuad = {
quad[0],
quad[1],
quad[2],
side == BoxSide::Top || side == BoxSide::Bottom ? FloatPoint(quad[3].x(), quad[2].y()) : FloatPoint(quad[2].x(), quad[3].y()),
quad[3]
};
bool wasAntialiased = paintingContext.context.shouldAntialias();
paintingContext.context.setShouldAntialias(!firstEdgeMatches);
paintingContext.context.clipPath(Path::polygonPathFromPoints(firstQuad), WindRule::NonZero);
Vector<FloatPoint> secondQuad = {
quad[0],
side == BoxSide::Top || side == BoxSide::Bottom ? FloatPoint(quad[0].x(), quad[1].y()) : FloatPoint(quad[1].x(), quad[0].y()),
quad[1],
quad[2],
quad[3]
};
// Antialiasing affects the second side.
paintingContext.context.setShouldAntialias(!secondEdgeMatches);
paintingContext.context.clipPath(Path::polygonPathFromPoints(secondQuad), WindRule::NonZero);
paintingContext.context.setShouldAntialias(wasAntialiased);
}
void BorderPainter::drawLineForBoxSide(PaintingContext& paintingContext, const FloatRect& rect, BoxSide side, Color color, BorderStyle borderStyle, float adjacentWidth1, float adjacentWidth2, bool antialias) const
{
auto drawBorderRect = [&](const FloatRect& rect) {
if (rect.isEmpty())
return;
paintingContext.context.drawRect(rect);
};
auto drawLineFor = [&](const FloatRect& rect, BoxSide side, BorderStyle borderStyle, const FloatSize& adjacent) {
if (rect.isEmpty())
return;
drawLineForBoxSide(paintingContext, rect, side, color, borderStyle, adjacent.width(), adjacent.height(), antialias);
};
auto& edge = m_edges.at(side);
const float deviceScaleFactor = paintingContext.deviceScaleFactor;
float x1 = rect.x();
float x2 = rect.maxX();
float y1 = rect.y();
float y2 = rect.maxY();
float thickness;
float length;
if (side == BoxSide::Top || side == BoxSide::Bottom) {
thickness = y2 - y1;
length = x2 - x1;
} else {
thickness = x2 - x1;
length = y2 - y1;
}
// FIXME: We really would like this check to be an ASSERT as we don't want to draw empty borders. However
// nothing guarantees that the following recursive calls to drawLineForBoxSide will have non-null dimensions.
if (!thickness || !length)
return;
switch (borderStyle) {
case BorderStyle::None:
case BorderStyle::Hidden:
return;
case BorderStyle::Dotted:
case BorderStyle::Dashed: {
bool wasAntialiased = paintingContext.context.shouldAntialias();
StrokeStyle oldStrokeStyle = paintingContext.context.strokeStyle();
paintingContext.context.setShouldAntialias(antialias);
paintingContext.context.setStrokeColor(color);
paintingContext.context.setStrokeThickness(thickness);
paintingContext.context.setStrokeStyle(borderStyle == BorderStyle::Dashed ? DashedStroke : DottedStroke);
paintingContext.context.drawLine({ x1, y1 }, { x2, y2 });
paintingContext.context.setShouldAntialias(wasAntialiased);
paintingContext.context.setStrokeStyle(oldStrokeStyle);
break;
}
case BorderStyle::Double: {
float thirdOfThickness = ceilToDevicePixel(thickness / 3, deviceScaleFactor);
if (!adjacentWidth1 && !adjacentWidth2) {
StrokeStyle oldStrokeStyle = paintingContext.context.strokeStyle();
paintingContext.context.setStrokeStyle(NoStroke);
paintingContext.context.setFillColor(color);
bool wasAntialiased = paintingContext.context.shouldAntialias();
paintingContext.context.setShouldAntialias(antialias);
switch (side) {
case BoxSide::Top:
drawBorderRect({ x1, y1, length, edge.outerWidth() });
drawBorderRect({ x1, y2 - edge.innerWidth(), length, edge.innerWidth() });
break;
case BoxSide::Bottom:
drawBorderRect({ x1, y2 - edge.outerWidth(), length, edge.outerWidth() });
drawBorderRect({ x1, y1, length, edge.innerWidth() });
break;
case BoxSide::Left:
drawBorderRect({ x1, y1, edge.outerWidth(), length });
drawBorderRect({ x2 - edge.innerWidth(), y1, edge.innerWidth(), length });
break;
case BoxSide::Right:
drawBorderRect({ x2 - edge.outerWidth(), y1, edge.outerWidth(), length });
drawBorderRect({ x1, y1, edge.innerWidth(), length });
break;
}
paintingContext.context.setShouldAntialias(wasAntialiased);
paintingContext.context.setStrokeStyle(oldStrokeStyle);
} else {
float adjacent1BigThird = ceilToDevicePixel(adjacentWidth1 / 3, deviceScaleFactor);
float adjacent2BigThird = ceilToDevicePixel(adjacentWidth2 / 3, deviceScaleFactor);
float offset1 = floorToDevicePixel(fabs(adjacentWidth1) * 2 / 3, deviceScaleFactor);
float offset2 = floorToDevicePixel(fabs(adjacentWidth2) * 2 / 3, deviceScaleFactor);
float mitreOffset1 = adjacentWidth1 < 0 ? offset1 : 0;
float mitreOffset2 = adjacentWidth1 > 0 ? offset1 : 0;
float mitreOffset3 = adjacentWidth2 < 0 ? offset2 : 0;
float mitreOffset4 = adjacentWidth2 > 0 ? offset2 : 0;
FloatRect paintBorderRect;
switch (side) {
case BoxSide::Top:
paintBorderRect = snapRectToDevicePixels(LayoutRect(x1 + mitreOffset1, y1, (x2 - mitreOffset3) - (x1 + mitreOffset1), thirdOfThickness), deviceScaleFactor);
drawLineFor(paintBorderRect, side, BorderStyle::Solid, FloatSize(adjacent1BigThird, adjacent2BigThird));
paintBorderRect = snapRectToDevicePixels(LayoutRect(x1 + mitreOffset2, y2 - thirdOfThickness, (x2 - mitreOffset4) - (x1 + mitreOffset2), thirdOfThickness), deviceScaleFactor);
drawLineFor(paintBorderRect, side, BorderStyle::Solid, FloatSize(adjacent1BigThird, adjacent2BigThird));
break;
case BoxSide::Left:
paintBorderRect = snapRectToDevicePixels(LayoutRect(x1, y1 + mitreOffset1, thirdOfThickness, (y2 - mitreOffset3) - (y1 + mitreOffset1)), deviceScaleFactor);
drawLineFor(paintBorderRect, side, BorderStyle::Solid, FloatSize(adjacent1BigThird, adjacent2BigThird));
paintBorderRect = snapRectToDevicePixels(LayoutRect(x2 - thirdOfThickness, y1 + mitreOffset2, thirdOfThickness, (y2 - mitreOffset4) - (y1 + mitreOffset2)), deviceScaleFactor);
drawLineFor(paintBorderRect, side, BorderStyle::Solid, FloatSize(adjacent1BigThird, adjacent2BigThird));
break;
case BoxSide::Bottom:
paintBorderRect = snapRectToDevicePixels(LayoutRect(x1 + mitreOffset2, y1, (x2 - mitreOffset4) - (x1 + mitreOffset2), thirdOfThickness), deviceScaleFactor);
drawLineFor(paintBorderRect, side, BorderStyle::Solid, FloatSize(adjacent1BigThird, adjacent2BigThird));
paintBorderRect = snapRectToDevicePixels(LayoutRect(x1 + mitreOffset1, y2 - thirdOfThickness, (x2 - mitreOffset3) - (x1 + mitreOffset1), thirdOfThickness), deviceScaleFactor);
drawLineFor(paintBorderRect, side, BorderStyle::Solid, FloatSize(adjacent1BigThird, adjacent2BigThird));
break;
case BoxSide::Right:
paintBorderRect = snapRectToDevicePixels(LayoutRect(x1, y1 + mitreOffset2, thirdOfThickness, (y2 - mitreOffset4) - (y1 + mitreOffset2)), deviceScaleFactor);
drawLineFor(paintBorderRect, side, BorderStyle::Solid, FloatSize(adjacent1BigThird, adjacent2BigThird));
paintBorderRect = snapRectToDevicePixels(LayoutRect(x2 - thirdOfThickness, y1 + mitreOffset1, thirdOfThickness, (y2 - mitreOffset3) - (y1 + mitreOffset1)), deviceScaleFactor);
drawLineFor(paintBorderRect, side, BorderStyle::Solid, FloatSize(adjacent1BigThird, adjacent2BigThird));
break;
}
}
break;
}
case BorderStyle::Ridge:
case BorderStyle::Groove: {
BorderStyle s1;
BorderStyle s2;
if (borderStyle == BorderStyle::Groove) {
s1 = BorderStyle::Inset;
s2 = BorderStyle::Outset;
} else {
s1 = BorderStyle::Outset;
s2 = BorderStyle::Inset;
}
float adjacent1BigHalf = ceilToDevicePixel(adjacentWidth1 / 2, deviceScaleFactor);
float adjacent2BigHalf = ceilToDevicePixel(adjacentWidth2 / 2, deviceScaleFactor);
float adjacent1SmallHalf = floorToDevicePixel(adjacentWidth1 / 2, deviceScaleFactor);
float adjacent2SmallHalf = floorToDevicePixel(adjacentWidth2 / 2, deviceScaleFactor);
float offset1 = 0;
float offset2 = 0;
float offset3 = 0;
float offset4 = 0;
if (((side == BoxSide::Top || side == BoxSide::Left) && adjacentWidth1 < 0) || ((side == BoxSide::Bottom || side == BoxSide::Right) && adjacentWidth1 > 0))
offset1 = floorToDevicePixel(adjacentWidth1 / 2, deviceScaleFactor);
if (((side == BoxSide::Top || side == BoxSide::Left) && adjacentWidth2 < 0) || ((side == BoxSide::Bottom || side == BoxSide::Right) && adjacentWidth2 > 0))
offset2 = ceilToDevicePixel(adjacentWidth2 / 2, deviceScaleFactor);
if (((side == BoxSide::Top || side == BoxSide::Left) && adjacentWidth1 > 0) || ((side == BoxSide::Bottom || side == BoxSide::Right) && adjacentWidth1 < 0))
offset3 = floorToDevicePixel(fabs(adjacentWidth1) / 2, deviceScaleFactor);
if (((side == BoxSide::Top || side == BoxSide::Left) && adjacentWidth2 > 0) || ((side == BoxSide::Bottom || side == BoxSide::Right) && adjacentWidth2 < 0))
offset4 = ceilToDevicePixel(adjacentWidth2 / 2, deviceScaleFactor);
float adjustedX = ceilToDevicePixel((x1 + x2) / 2, deviceScaleFactor);
float adjustedY = ceilToDevicePixel((y1 + y2) / 2, deviceScaleFactor);
// Quads can't use the default snapping rect functions.
x1 = roundToDevicePixel(x1, deviceScaleFactor);
x2 = roundToDevicePixel(x2, deviceScaleFactor);
y1 = roundToDevicePixel(y1, deviceScaleFactor);
y2 = roundToDevicePixel(y2, deviceScaleFactor);
switch (side) {
case BoxSide::Top:
drawLineFor(FloatRect(FloatPoint(x1 + offset1, y1), FloatPoint(x2 - offset2, adjustedY)), side, s1, FloatSize(adjacent1BigHalf, adjacent2BigHalf));
drawLineFor(FloatRect(FloatPoint(x1 + offset3, adjustedY), FloatPoint(x2 - offset4, y2)), side, s2, FloatSize(adjacent1SmallHalf, adjacent2SmallHalf));
break;
case BoxSide::Left:
drawLineFor(FloatRect(FloatPoint(x1, y1 + offset1), FloatPoint(adjustedX, y2 - offset2)), side, s1, FloatSize(adjacent1BigHalf, adjacent2BigHalf));
drawLineFor(FloatRect(FloatPoint(adjustedX, y1 + offset3), FloatPoint(x2, y2 - offset4)), side, s2, FloatSize(adjacent1SmallHalf, adjacent2SmallHalf));
break;
case BoxSide::Bottom:
drawLineFor(FloatRect(FloatPoint(x1 + offset1, y1), FloatPoint(x2 - offset2, adjustedY)), side, s2, FloatSize(adjacent1BigHalf, adjacent2BigHalf));
drawLineFor(FloatRect(FloatPoint(x1 + offset3, adjustedY), FloatPoint(x2 - offset4, y2)), side, s1, FloatSize(adjacent1SmallHalf, adjacent2SmallHalf));
break;
case BoxSide::Right:
drawLineFor(FloatRect(FloatPoint(x1, y1 + offset1), FloatPoint(adjustedX, y2 - offset2)), side, s2, FloatSize(adjacent1BigHalf, adjacent2BigHalf));
drawLineFor(FloatRect(FloatPoint(adjustedX, y1 + offset3), FloatPoint(x2, y2 - offset4)), side, s1, FloatSize(adjacent1SmallHalf, adjacent2SmallHalf));
break;
}
break;
}
case BorderStyle::Inset:
case BorderStyle::Outset:
calculateBorderStyleColor(borderStyle, side, color);
FALLTHROUGH;
case BorderStyle::Solid: {
StrokeStyle oldStrokeStyle = paintingContext.context.strokeStyle();
ASSERT(x2 >= x1);
ASSERT(y2 >= y1);
if (!adjacentWidth1 && !adjacentWidth2) {
paintingContext.context.setStrokeStyle(NoStroke);
paintingContext.context.setFillColor(color);
bool wasAntialiased = paintingContext.context.shouldAntialias();
paintingContext.context.setShouldAntialias(antialias);
drawBorderRect(snapRectToDevicePixels(LayoutRect(x1, y1, x2 - x1, y2 - y1), deviceScaleFactor));
paintingContext.context.setShouldAntialias(wasAntialiased);
paintingContext.context.setStrokeStyle(oldStrokeStyle);
return;
}
// FIXME: These roundings should be replaced by ASSERT(device pixel positioned) when all the callers have transitioned to device pixels.
x1 = roundToDevicePixel(x1, deviceScaleFactor);
y1 = roundToDevicePixel(y1, deviceScaleFactor);
x2 = roundToDevicePixel(x2, deviceScaleFactor);
y2 = roundToDevicePixel(y2, deviceScaleFactor);
Vector<FloatPoint> quad;
quad.reserveInitialCapacity(4);
switch (side) {
case BoxSide::Top:
quad.uncheckedAppend({ x1 + std::max<float>(-adjacentWidth1, 0), y1 });
quad.uncheckedAppend({ x1 + std::max<float>( adjacentWidth1, 0), y2 });
quad.uncheckedAppend({ x2 - std::max<float>( adjacentWidth2, 0), y2 });
quad.uncheckedAppend({ x2 - std::max<float>(-adjacentWidth2, 0), y1 });
break;
case BoxSide::Bottom:
quad.uncheckedAppend({ x1 + std::max<float>( adjacentWidth1, 0), y1 });
quad.uncheckedAppend({ x1 + std::max<float>(-adjacentWidth1, 0), y2 });
quad.uncheckedAppend({ x2 - std::max<float>(-adjacentWidth2, 0), y2 });
quad.uncheckedAppend({ x2 - std::max<float>( adjacentWidth2, 0), y1 });
break;
case BoxSide::Left:
quad.uncheckedAppend({ x1, y1 + std::max<float>(-adjacentWidth1, 0) });
quad.uncheckedAppend({ x1, y2 - std::max<float>(-adjacentWidth2, 0) });
quad.uncheckedAppend({ x2, y2 - std::max<float>( adjacentWidth2, 0) });
quad.uncheckedAppend({ x2, y1 + std::max<float>( adjacentWidth1, 0) });
break;
case BoxSide::Right:
quad.uncheckedAppend({ x1, y1 + std::max<float>( adjacentWidth1, 0) });
quad.uncheckedAppend({ x1, y2 - std::max<float>( adjacentWidth2, 0) });
quad.uncheckedAppend({ x2, y2 - std::max<float>(-adjacentWidth2, 0) });
quad.uncheckedAppend({ x2, y1 + std::max<float>(-adjacentWidth1, 0) });
break;
}
paintingContext.context.setStrokeStyle(NoStroke);
paintingContext.context.setFillColor(color);
bool wasAntialiased = paintingContext.context.shouldAntialias();
paintingContext.context.setShouldAntialias(antialias);
paintingContext.context.fillPath(Path::polygonPathFromPoints(quad));
paintingContext.context.setShouldAntialias(wasAntialiased);
paintingContext.context.setStrokeStyle(oldStrokeStyle);
break;
}
}
}
void BorderPainter::paintOneBorderSide(PaintingContext& paintingContext, const FloatRoundedRect& outerBorder, const FloatRoundedRect& innerBorder, const FloatRect& sideRect, BoxSide side, const Path* path, bool antialias, const Color* overrideColor) const
{
auto& edgeToRender = m_edges.at(side);
ASSERT(edgeToRender.widthForPainting());
auto adjacentSides = adjacentSidesForSide(side);
auto& adjacentEdge1 = m_edges.at(adjacentSides.first);
auto& adjacentEdge2 = m_edges.at(adjacentSides.second);
bool mitreAdjacentSide1 = joinRequiresMitre(side, adjacentSides.first, !antialias);
bool mitreAdjacentSide2 = joinRequiresMitre(side, adjacentSides.second, !antialias);
bool adjacentSide1StylesMatch = colorsMatchAtCorner(side, adjacentSides.first);
bool adjacentSide2StylesMatch = colorsMatchAtCorner(side, adjacentSides.second);
const Color& colorToPaint = overrideColor ? *overrideColor : edgeToRender.color();
if (path) {
GraphicsContextStateSaver stateSaver(paintingContext.context);
clipBorderSidePolygon(paintingContext, outerBorder, innerBorder, side, adjacentSide1StylesMatch, adjacentSide2StylesMatch);
if (!innerBorder.isRenderable())
paintingContext.context.clipOutRoundedRect(FloatRoundedRect(calculateAdjustedInnerBorder(innerBorder, side)));
float thickness = std::max(std::max(edgeToRender.widthForPainting(), adjacentEdge1.widthForPainting()), adjacentEdge2.widthForPainting());
drawBoxSideFromPath(paintingContext, outerBorder.rect(), *path, edgeToRender.widthForPainting(), thickness, side, colorToPaint, edgeToRender.style());
return;
}
bool clipForStyle = styleRequiresClipPolygon(edgeToRender.style()) && (mitreAdjacentSide1 || mitreAdjacentSide2);
bool clipAdjacentSide1 = colorNeedsAntiAliasAtCorner(side, adjacentSides.first) && mitreAdjacentSide1;
bool clipAdjacentSide2 = colorNeedsAntiAliasAtCorner(side, adjacentSides.second) && mitreAdjacentSide2;
bool shouldClip = clipForStyle || clipAdjacentSide1 || clipAdjacentSide2;
GraphicsContextStateSaver clipStateSaver(paintingContext.context, shouldClip);
if (shouldClip) {
bool aliasAdjacentSide1 = clipAdjacentSide1 || (clipForStyle && mitreAdjacentSide1);
bool aliasAdjacentSide2 = clipAdjacentSide2 || (clipForStyle && mitreAdjacentSide2);
clipBorderSidePolygon(paintingContext, outerBorder, innerBorder, side, !aliasAdjacentSide1, !aliasAdjacentSide2);
// Since we clipped, no need to draw with a mitre.
mitreAdjacentSide1 = false;
mitreAdjacentSide2 = false;
}
drawLineForBoxSide(paintingContext, sideRect, side, colorToPaint, edgeToRender.style(), mitreAdjacentSide1 ? adjacentEdge1.widthForPainting() : 0, mitreAdjacentSide2 ? adjacentEdge2.widthForPainting() : 0, antialias);
}
void BorderPainter::paintBorderSides(PaintingContext& paintingContext, const FloatRoundedRect& outerBorder, const FloatRoundedRect& innerBorder, FloatSize innerBorderBleedAdjustment, BoxSideSet edgeSet, bool antialias, const Color* overrideColor) const
{
bool renderRadii = outerBorder.isRounded();
Path roundedPath;
if (renderRadii)
roundedPath.addRoundedRect(outerBorder);
auto paintOneSide = [&](BoxSide side) {
auto& edge = m_edges.at(side);
if (!edge.shouldRender() || !edgeSet.contains(edgeFlagForSide(side)))
return;
auto sideRect = outerBorder.rect();
FloatSize firstRadius;
FloatSize secondRadius;
switch (side) {
case BoxSide::Top:
sideRect.setHeight(edge.widthForPainting() + innerBorderBleedAdjustment.height());
firstRadius = innerBorder.radii().topLeft();
secondRadius = innerBorder.radii().topRight();
break;
case BoxSide::Right:
sideRect.shiftXEdgeTo(sideRect.maxX() - edge.widthForPainting() - innerBorderBleedAdjustment.width());
firstRadius = innerBorder.radii().bottomRight();
secondRadius = innerBorder.radii().topRight();
break;
case BoxSide::Bottom:
sideRect.shiftYEdgeTo(sideRect.maxY() - edge.widthForPainting() - innerBorderBleedAdjustment.height());
firstRadius = innerBorder.radii().bottomLeft();
secondRadius = innerBorder.radii().bottomRight();
break;
case BoxSide::Left:
sideRect.setWidth(edge.widthForPainting() + innerBorderBleedAdjustment.width());
firstRadius = innerBorder.radii().bottomLeft();
secondRadius = innerBorder.radii().topLeft();
break;
}
bool usePath = renderRadii && (borderStyleHasInnerDetail(edge.style()) || borderWillArcInnerEdge(firstRadius, secondRadius));
paintOneBorderSide(paintingContext, outerBorder, innerBorder, sideRect, side, usePath ? &roundedPath : nullptr, antialias, overrideColor);
};
paintOneSide(BoxSide::Top);
paintOneSide(BoxSide::Bottom);
paintOneSide(BoxSide::Left);
paintOneSide(BoxSide::Right);
}
void BorderPainter::paintTranslucentBorderSides(PaintingContext& paintingContext, const FloatRoundedRect& outerBorder, const FloatRoundedRect& innerBorder, FloatSize innerBorderBleedAdjustment, BoxSideSet edgesToDraw, bool antialias) const
{
// willBeOverdrawn assumes that we draw in order: top, bottom, left, right.
// This is different from BoxSide enum order.
static constexpr std::array<BoxSide, 4> paintOrderSides = { BoxSide::Top, BoxSide::Bottom, BoxSide::Left, BoxSide::Right };
while (edgesToDraw) {
// Find undrawn edges sharing a color.
Color commonColor;
BoxSideSet commonColorEdgeSet;
for (auto side : paintOrderSides) {
if (!edgesToDraw.contains(edgeFlagForSide(side)))
continue;
auto& edge = m_edges.at(side);
bool includeEdge;
if (commonColorEdgeSet.isEmpty()) {
commonColor = edge.color();
includeEdge = true;
} else
includeEdge = edge.color() == commonColor;
if (includeEdge)
commonColorEdgeSet.add(edgeFlagForSide(side));
}
bool useTransparencyLayer = includesAdjacentEdges(commonColorEdgeSet) && !commonColor.isOpaque();
{
auto transparencyScope = TransparencyLayerScope { paintingContext.context, commonColor.alphaAsFloat(), useTransparencyLayer };
if (useTransparencyLayer)
commonColor = commonColor.opaqueColor();
paintBorderSides(paintingContext, outerBorder, innerBorder, innerBorderBleedAdjustment, commonColorEdgeSet, antialias, &commonColor);
}
edgesToDraw.remove(commonColorEdgeSet);
}
}
static FloatRect shrinkRectByOneDevicePixel(const PaintingContext& paintingContext, const FloatRect& rect)
{
auto shrunkRect = rect;
auto transform = paintingContext.context.getCTM();
shrunkRect.inflateX(-ceilToDevicePixel(1.0f / transform.xScale(), paintingContext.deviceScaleFactor));
shrunkRect.inflateY(-ceilToDevicePixel(1.0f / transform.yScale(), paintingContext.deviceScaleFactor));
return shrunkRect;
}
FloatRect BorderPainter::borderInnerRectAdjustedForBleedAvoidance(const PaintingContext& paintingContext) const
{
if (m_bleedAvoidance != BackgroundBleedAvoidance::BackgroundOverBorder)
return m_borderRect.rect();
// We shrink the rectangle by one device pixel on each side to make it fully overlap the anti-aliased background border
return shrinkRectByOneDevicePixel(paintingContext, m_borderRect.rect());
}
void BorderPainter::paintBorders(PaintingContext& paintingContext) const
{
bool haveAlphaColor = false;
bool haveAllSolidEdges = true;
bool haveAllDoubleEdges = true;
int numEdgesVisible = 4;
bool allEdgesShareColor = true;
std::optional<BoxSide> firstVisibleSide;
BoxSideSet edgesToDraw;
for (auto side : allBoxSides) {
auto& currEdge = m_edges.at(side);
if (currEdge.shouldRender())
edgesToDraw.add(edgeFlagForSide(side));
if (currEdge.presentButInvisible()) {
--numEdgesVisible;
allEdgesShareColor = false;
continue;
}
if (!currEdge.widthForPainting()) {
--numEdgesVisible;
continue;
}
if (!firstVisibleSide)
firstVisibleSide = side;
else if (currEdge.color() != m_edges.at(*firstVisibleSide).color())
allEdgesShareColor = false;
if (!currEdge.color().isOpaque())
haveAlphaColor = true;
if (currEdge.style() != BorderStyle::Solid)
haveAllSolidEdges = false;
if (currEdge.style() != BorderStyle::Double)
haveAllDoubleEdges = false;
}
FloatRoundedRect innerBorderRect = roundedInsetBorderForRect(borderInnerRectAdjustedForBleedAvoidance(paintingContext), m_borderRect.radii(), borderWidths(m_edges), m_includeLeftEdge, m_includeRightEdge);
// FIXME: If no corner intersects the clip region, we can pretend outerBorder is rectangular to improve performance.
if ((haveAllSolidEdges || haveAllDoubleEdges) && allEdgesShareColor && innerBorderRect.isRenderable()) {
// Fast path for drawing all solid edges and all unrounded double edges
if (numEdgesVisible == 4 && (m_borderRect.isRounded() || haveAlphaColor) && (haveAllSolidEdges || (!m_borderRect.isRounded() && !innerBorderRect.isRounded()))) {
Path path;
if (m_borderRect.isRounded() && m_bleedAvoidance != BackgroundBleedAvoidance::UseTransparencyLayer)
path.addRoundedRect(m_borderRect);
else
path.addRect(m_borderRect.rect());
if (haveAllDoubleEdges) {
auto innerThirdRect = m_borderRect.rect();
auto outerThirdRect = m_borderRect.rect();
for (auto side : allBoxSides) {
auto& edge = m_edges.at(side);
float outerWidth = edge.outerWidth();
float innerWidth = edge.innerWidth();
switch (side) {
case BoxSide::Top:
innerThirdRect.shiftYEdgeTo(innerThirdRect.y() + innerWidth);
outerThirdRect.shiftYEdgeTo(outerThirdRect.y() + outerWidth);
break;
case BoxSide::Right:
innerThirdRect.setWidth(innerThirdRect.width() - innerWidth);
outerThirdRect.setWidth(outerThirdRect.width() - outerWidth);
break;
case BoxSide::Bottom:
innerThirdRect.setHeight(innerThirdRect.height() - innerWidth);
outerThirdRect.setHeight(outerThirdRect.height() - outerWidth);
break;
case BoxSide::Left:
innerThirdRect.shiftXEdgeTo(innerThirdRect.x() + innerWidth);
outerThirdRect.shiftXEdgeTo(outerThirdRect.x() + outerWidth);
break;
}
}
auto outerThirdRoundedRect = m_borderRect;
outerThirdRoundedRect.setRect(outerThirdRect);
if (outerThirdRoundedRect.isRounded() && m_bleedAvoidance != BackgroundBleedAvoidance::UseTransparencyLayer)
path.addRoundedRect(outerThirdRoundedRect);
else
path.addRect(outerThirdRoundedRect.rect());
auto innerThirdRoundedRect = innerBorderRect;
innerThirdRoundedRect.setRect(innerThirdRect);
if (innerThirdRoundedRect.isRounded())
path.addRoundedRect(innerThirdRoundedRect);
else
path.addRect(innerThirdRoundedRect.rect());
}
if (innerBorderRect.isRounded())
path.addRoundedRect(innerBorderRect);
else
path.addRect(innerBorderRect.rect());
paintingContext.context.setFillRule(WindRule::EvenOdd);
paintingContext.context.setFillColor(m_edges.top().color());
paintingContext.context.fillPath(path);
return;
}
// Avoid creating transparent layers
if (haveAllSolidEdges && numEdgesVisible != 4 && !m_borderRect.isRounded() && haveAlphaColor) {
Path path;
auto calculateSideRect = [&](const BorderEdge& edge, BoxSide side) {
auto sideRect = m_borderRect.rect();
auto width = edge.widthForPainting();
switch (side) {
case BoxSide::Top:
sideRect.setHeight(width);
break;
case BoxSide::Right:
sideRect.shiftXEdgeTo(sideRect.maxX() - width);
break;
case BoxSide::Bottom:
sideRect.shiftYEdgeTo(sideRect.maxY() - width);
break;
case BoxSide::Left:
sideRect.setWidth(width);
break;
}
return sideRect;
};
for (auto side : allBoxSides) {
auto& edge = m_edges.at(side);
if (edge.shouldRender()) {
auto sideRect = calculateSideRect(edge, side);
path.addRect(sideRect);
}
}
paintingContext.context.setFillRule(WindRule::NonZero);
paintingContext.context.setFillColor(m_edges.at(*firstVisibleSide).color());
paintingContext.context.fillPath(path);
return;
}
}
bool clipToOuterBorder = m_borderRect.isRounded();
GraphicsContextStateSaver stateSaver(paintingContext.context, clipToOuterBorder);
if (clipToOuterBorder) {
// Clip to the inner and outer radii rects.
if (m_bleedAvoidance != BackgroundBleedAvoidance::UseTransparencyLayer)
paintingContext.context.clipRoundedRect(m_borderRect);
if (innerBorderRect.isRenderable())
paintingContext.context.clipOutRoundedRect(innerBorderRect);
}
bool shouldAntialiasLines = !paintingContext.context.getCTM().isIdentityOrTranslationOrFlipped();
// If only one edge visible antialiasing doesn't create seams
bool antialias = shouldAntialiasLines || numEdgesVisible == 1;
auto unadjustedInnerBorder = innerBorderRect;
FloatSize innerBorderBleedAdjustment;
if (m_bleedAvoidance == BackgroundBleedAvoidance::BackgroundOverBorder) {
unadjustedInnerBorder = roundedInsetBorderForRect(m_borderRect.rect(), m_borderRect.radii(), borderWidths(m_edges), m_includeLeftEdge, m_includeRightEdge);
innerBorderBleedAdjustment = innerBorderRect.rect().location() - unadjustedInnerBorder.rect().location();
}
if (haveAlphaColor)
paintTranslucentBorderSides(paintingContext, m_borderRect, innerBorderRect, innerBorderBleedAdjustment, edgesToDraw, antialias); // FIXME.
else
paintBorderSides(paintingContext, m_borderRect, innerBorderRect, innerBorderBleedAdjustment, edgesToDraw, antialias);
}
BoxDecorationPainter::BoxDecorationPainter(const BoxModelBox& box, PaintingContext& paintingContext, bool includeLeftEdge, bool includeRightEdge)
: m_box(box)
, m_borderRect(box.borderRoundedRect())
, m_bleedAvoidance(determineBackgroundBleedAvoidance(box, paintingContext))
, m_includeLeftEdge(includeLeftEdge)
, m_includeRightEdge(includeRightEdge)
{
}
void BoxDecorationPainter::paintBorders(PaintingContext& paintingContext) const
{
auto* boxDecorationData = m_box.boxDecorationData();
if (!boxDecorationData)
return;
if (boxDecorationData->hasBorderImage()) {
// border-image is not affected by border-radius.
// FIXME: Implement border-image painting.
return;
}
auto outerBorder = borderRoundedRect();
BorderPainter painter(boxDecorationData->borderEdges(), outerBorder, m_bleedAvoidance, m_includeLeftEdge, m_includeRightEdge);
painter.paintBorders(paintingContext);
}
void BoxDecorationPainter::paintFillLayer(PaintingContext& paintingContext, const FillLayer& layer, const FillLayerImageGeometry& geometry) const
{
GraphicsContextStateSaver stateSaver(paintingContext.context, false);
auto clipRectForLayer = [](const BoxModelBox& box, const FillLayer& layer) -> UnadjustedAbsoluteFloatRect {
switch (layer.clip()) {
case FillBox::Border:
return box.absoluteBorderBoxRect();
case FillBox::Padding:
return box.absolutePaddingBoxRect();
case FillBox::Content:
return box.absoluteContentBoxRect();
case FillBox::Text:
break;
}
return { };
};
switch (layer.clip()) {
case FillBox::Border:
case FillBox::Padding:
case FillBox::Content: {
stateSaver.save();
paintingContext.context.clip(clipRectForLayer(m_box, layer));
break;
}
case FillBox::Text:
break;
}
// FIXME: Handle background compositing modes.
auto* backgroundImage = layer.image();
CompositeOperator op = CompositeOperator::SourceOver;
if (geometry.destRect().isEmpty())
return;
auto image = backgroundImage->image(nullptr, geometry.tileSize());
if (!image)
return;
// FIXME: call image->updateFromSettings().
ImagePaintingOptions options = {
op == CompositeOperator::SourceOver ? layer.composite() : op,
layer.blendMode(),
DecodingMode::Synchronous,
ImageOrientation::FromImage,
InterpolationQuality::Default
};
paintingContext.context.drawTiledImage(*image, geometry.destRect(), toFloatPoint(geometry.relativePhase()), geometry.tileSize(), geometry.spaceSize(), options);
}
void BoxDecorationPainter::paintBoxShadow(PaintingContext& paintingContext, ShadowStyle shadowStyle) const
{
if (!m_box.style().boxShadow())
return;
auto borderRect = shadowStyle == ShadowStyle::Inset ? innerBorderRoundedRect() : borderRoundedRect();
auto* boxDecorationData = m_box.boxDecorationData();
bool hasBorderRadius = m_box.hasBorderRadius();
bool hasOpaqueBackground = m_box.style().backgroundColor().isOpaque();
auto paintNormalShadow = [&](const ShadowData& shadow) {
// FIXME: Snapping here isn't ideal. It would be better to compute a rect which is border rect + offset + spread, and snap that at tree building time.
auto shadowOffset = roundSizeToDevicePixels({ shadow.x(), shadow.y() }, paintingContext.deviceScaleFactor);
float shadowPaintingExtent = ceilToDevicePixel(shadow.paintingExtent(), paintingContext.deviceScaleFactor);
float shadowSpread = roundToDevicePixel(shadow.spread(), paintingContext.deviceScaleFactor);
int shadowRadius = shadow.radius();
auto fillRect = borderRect;
fillRect.inflate(shadowSpread);
if (fillRect.isEmpty())
return;
auto shadowRect = borderRect.rect();
shadowRect.inflate(shadowPaintingExtent + shadowSpread);
shadowRect.move(shadowOffset);
GraphicsContextStateSaver stateSaver(paintingContext.context);
paintingContext.context.clip(shadowRect);
// Move the fill just outside the clip, adding at least 1 pixel of separation so that the fill does not
// bleed in (due to antialiasing) if the context is transformed.
float xOffset = shadowRect.width() + std::max<float>(0, shadowOffset.width()) + shadowPaintingExtent + 2 * shadowSpread + 1.0f;
auto extraOffset = FloatSize { std::ceil(xOffset), 0 };
shadowOffset -= extraOffset;
fillRect.move(extraOffset);
auto rectToClipOut = borderRect;
auto adjustedFillRect = fillRect;
auto shadowRectOrigin = fillRect.rect().location() + shadowOffset;
auto adjustedShadowOffset = shadowRectOrigin - adjustedFillRect.rect().location();
paintingContext.context.setShadow(adjustedShadowOffset, shadowRadius, shadow.color(), shadow.isWebkitBoxShadow() ? ShadowRadiusMode::Legacy : ShadowRadiusMode::Default);
if (hasBorderRadius) {
// If the box is opaque, it is unnecessary to clip it out. However, doing so saves time
// when painting the shadow. On the other hand, it introduces subpixel gaps along the
// corners. Those are avoided by insetting the clipping path by one pixel.
if (hasOpaqueBackground)
rectToClipOut.inflateWithRadii(-1.0f);
if (!rectToClipOut.isEmpty())
paintingContext.context.clipOutRoundedRect(rectToClipOut);
auto influenceRect = FloatRoundedRect { shadowRect, borderRect.radii() };
influenceRect.expandRadii(2 * shadowPaintingExtent + shadowSpread);
// FIXME: Optimize for clipped-out corners
adjustedFillRect.expandRadii(shadowSpread);
if (!adjustedFillRect.isRenderable())
adjustedFillRect.adjustRadii();
paintingContext.context.fillRoundedRect(adjustedFillRect, Color::black);
} else {
// If the box is opaque, it is unnecessary to clip it out. However, doing so saves time
// when painting the shadow. On the other hand, it introduces subpixel gaps along the
// edges if they are not pixel-aligned. Those are avoided by insetting the clipping path
// by one pixel.
if (hasOpaqueBackground) {
// FIXME: The function to decide on the policy based on the transform should be a named function.
// FIXME: It's not clear if this check is right. What about integral scale factors?
AffineTransform transform = paintingContext.context.getCTM();
if (transform.a() != 1 || (transform.d() != 1 && transform.d() != -1) || transform.b() || transform.c())
rectToClipOut.inflate(-1.0f);
}
if (!rectToClipOut.isEmpty())
paintingContext.context.clipOut(rectToClipOut.rect());
paintingContext.context.fillRect(adjustedFillRect.rect(), Color::black);
}
};
auto paintInsetShadow = [&](const ShadowData& shadow) {
auto shadowOffset = roundSizeToDevicePixels({ shadow.x(), shadow.y() }, paintingContext.deviceScaleFactor);
float shadowPaintingExtent = ceilToDevicePixel(shadow.paintingExtent(), paintingContext.deviceScaleFactor);
float shadowSpread = roundToDevicePixel(shadow.spread(), paintingContext.deviceScaleFactor);
int shadowRadius = shadow.radius();
auto holeRect = borderRect.rect();
holeRect.inflate(-shadowSpread);
if (!m_includeLeftEdge) {
// FIXME: Need to take writing mode into account.
holeRect.shiftXEdgeBy(-(std::max<float>(shadowOffset.width(), 0) + shadowPaintingExtent + shadowSpread));
}
if (!m_includeRightEdge) {
// FIXME: Need to take writing mode into account.
holeRect.setWidth(holeRect.width() - std::min<float>(shadowOffset.width(), 0) + shadowPaintingExtent + shadowSpread);
}
auto roundedHoleRect = FloatRoundedRect { holeRect, borderRect.radii() };
if (shadowSpread && roundedHoleRect.isRounded()) {
auto roundedRectCorrectingForSpread = [&]() {
bool horizontal = true; // FIXME: Handle writing modes.
auto borderWidth = borderWidths(boxDecorationData->borderEdges());
float leftWidth { (!horizontal || m_includeLeftEdge) ? borderWidth.left() + shadowSpread : 0 };
float rightWidth { (!horizontal || m_includeRightEdge) ? borderWidth.right() + shadowSpread : 0 };
float topWidth { (horizontal || m_includeLeftEdge) ? borderWidth.top() + shadowSpread : 0 };
float bottomWidth { (horizontal || m_includeRightEdge) ? borderWidth.bottom() + shadowSpread : 0 };
return roundedInsetBorderForRect(m_borderRect.rect(), m_borderRect.radii(), { topWidth, rightWidth, bottomWidth, leftWidth }, m_includeLeftEdge, m_includeRightEdge);
}();
roundedHoleRect.setRadii(roundedRectCorrectingForSpread.radii());
}
if (roundedHoleRect.isEmpty()) {
if (hasBorderRadius)
paintingContext.context.fillRoundedRect(borderRect, shadow.color());
else
paintingContext.context.fillRect(borderRect.rect(), shadow.color());
return;
}
auto areaCastingShadowInHole = [](const FloatRect& holeRect, float shadowExtent, float shadowSpread, FloatSize shadowOffset) {
auto bounds(holeRect);
bounds.inflate(shadowExtent);
if (shadowSpread < 0)
bounds.inflate(-shadowSpread);
auto offsetBounds = bounds;
offsetBounds.move(-shadowOffset);
return unionRect(bounds, offsetBounds);
};
Color fillColor = shadow.color().opaqueColor();
auto shadowCastingRect = areaCastingShadowInHole(borderRect.rect(), shadowPaintingExtent, shadowSpread, shadowOffset);
GraphicsContextStateSaver stateSaver(paintingContext.context);
if (hasBorderRadius)
paintingContext.context.clipRoundedRect(borderRect);
else
paintingContext.context.clip(borderRect.rect());
float xOffset = shadowCastingRect.width() + std::max<float>(0, shadowOffset.width()) + shadowPaintingExtent - 2 * shadowSpread + 1.0f;
auto extraOffset = FloatSize { std::ceil(xOffset), 0 };
paintingContext.context.translate(extraOffset);
shadowOffset -= extraOffset;
paintingContext.context.setShadow(shadowOffset, shadowRadius, shadow.color(), shadow.isWebkitBoxShadow() ? ShadowRadiusMode::Legacy : ShadowRadiusMode::Default);
paintingContext.context.fillRectWithRoundedHole(shadowCastingRect, roundedHoleRect, fillColor);
};
for (auto* shadow = m_box.style().boxShadow(); shadow; shadow = shadow->next()) {
if (shadow->style() != shadowStyle)
continue;
LayoutSize shadowOffset(shadow->x(), shadow->y());
if (shadowOffset.isZero() && !shadow->radius() && !shadow->spread())
continue;
if (shadow->style() == ShadowStyle::Normal)
paintNormalShadow(*shadow);
else
paintInsetShadow(*shadow);
}
}
void BoxDecorationPainter::paintBackgroundImages(PaintingContext& paintingContext) const
{
const auto& style = m_box.style();
Vector<const FillLayer*, 8> layers;
for (auto* layer = style.backgroundLayers(); layer; layer = layer->next())
layers.append(layer);
auto* boxDecorationData = m_box.boxDecorationData();
ASSERT(boxDecorationData);
auto& layerGeometryList = boxDecorationData->backgroundImageGeometry();
for (int i = layers.size() - 1; i >=0; --i) {
const auto* layer = layers[i];
const auto& geometry = layerGeometryList[i];
paintFillLayer(paintingContext, *layer, geometry);
}
}
FloatRoundedRect BoxDecorationPainter::innerBorderRoundedRect() const
{
return m_box.innerBorderRoundedRect();
}
FloatRoundedRect BoxDecorationPainter::backgroundRoundedRectAdjustedForBleedAvoidance(const PaintingContext& paintingContext) const
{
if (m_bleedAvoidance == BackgroundBleedAvoidance::ShrinkBackground) {
// We shrink the rectangle by one device pixel on each side because the bleed is one pixel maximum.
return roundedRectWithIncludedRadii(shrinkRectByOneDevicePixel(paintingContext, m_borderRect.rect()), m_borderRect.radii(), m_includeLeftEdge, m_includeRightEdge);
}
if (m_bleedAvoidance == BackgroundBleedAvoidance::BackgroundOverBorder)
return innerBorderRoundedRect();
return roundedRectWithIncludedRadii(m_borderRect.rect(), m_borderRect.radii(), m_includeLeftEdge, m_includeRightEdge);
}
void BoxDecorationPainter::paintBackground(PaintingContext& paintingContext) const
{
auto borderBoxRect = m_box.absoluteBorderBoxRect();
const auto& style = m_box.style();
if (style.hasBackground()) {
GraphicsContextStateSaver stateSaver(paintingContext.context, false);
if (m_bleedAvoidance != BackgroundBleedAvoidance::UseTransparencyLayer && m_box.hasBorderRadius()) {
stateSaver.save();
auto outerBorder = backgroundRoundedRectAdjustedForBleedAvoidance(paintingContext);
paintingContext.context.clipRoundedRect(outerBorder);
}
paintingContext.context.fillRect(borderBoxRect, style.backgroundColor());
if (style.hasBackgroundImage())
paintBackgroundImages(paintingContext);
}
}
BackgroundBleedAvoidance BoxDecorationPainter::determineBackgroundBleedAvoidance(const BoxModelBox& box, PaintingContext& paintingContext)
{
if (paintingContext.context.paintingDisabled())
return BackgroundBleedAvoidance::None;
auto& style = box.style();
auto* boxDecorationData = box.boxDecorationData();
auto hasBackgroundAndRoundedBorder = [&] {
if (!boxDecorationData)
return false;
// FIXME: Consult border-image.
return style.hasBackground() && boxDecorationData->hasBorder() && box.hasBorderRadius();
};
if (!hasBackgroundAndRoundedBorder())
return BackgroundBleedAvoidance::None;
auto ctm = paintingContext.context.getCTM();
auto contextScaling = FloatSize { static_cast<float>(ctm.xScale()), static_cast<float>(ctm.yScale()) };
if (boxDecorationData->borderObscuresBackgroundEdge(contextScaling))
return BackgroundBleedAvoidance::ShrinkBackground;
if (boxDecorationData->borderObscuresBackground() && style.backgroundHasOpaqueTopLayer())
return BackgroundBleedAvoidance::BackgroundOverBorder;
return BackgroundBleedAvoidance::UseTransparencyLayer;
}
void BoxDecorationPainter::paintBackgroundAndBorders(PaintingContext& paintingContext) const
{
// FIXME: Table decoration painting is special.
switch (m_bleedAvoidance) {
case BackgroundBleedAvoidance::BackgroundOverBorder:
paintBoxShadow(paintingContext, ShadowStyle::Normal);
paintBorders(paintingContext);
paintBackground(paintingContext);
paintBoxShadow(paintingContext, ShadowStyle::Inset);
break;
case BackgroundBleedAvoidance::UseTransparencyLayer: {
paintBoxShadow(paintingContext, ShadowStyle::Normal);
GraphicsContextStateSaver stateSaver(paintingContext.context);
auto outerBorder = borderRoundedRect();
paintingContext.context.clipRoundedRect(outerBorder);
paintingContext.context.beginTransparencyLayer(1);
paintBackground(paintingContext);
paintBoxShadow(paintingContext, ShadowStyle::Inset);
paintBorders(paintingContext);
paintingContext.context.endTransparencyLayer();
break;
}
case BackgroundBleedAvoidance::ShrinkBackground:
case BackgroundBleedAvoidance::None:
paintBoxShadow(paintingContext, ShadowStyle::Normal);
paintBackground(paintingContext);
paintBoxShadow(paintingContext, ShadowStyle::Inset);
paintBorders(paintingContext);
break;
}
}
} // namespace Display
} // namespace WebCore
#endif // ENABLE(LAYOUT_FORMATTING_CONTEXT)