blob: b0a4536c6a7bae07c4ef3c0534242e227914c6b8 [file] [log] [blame]
/*
* Copyright (C) 2021 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 "InlineBoxPainter.h"
#include "GraphicsContext.h"
#include "InlineIteratorLineBox.h"
#include "LegacyInlineFlowBox.h"
#include "PaintInfo.h"
#include "RenderBlockFlow.h"
#include "RenderInline.h"
#include "RenderLayer.h"
#include "RenderView.h"
namespace WebCore {
InlineBoxPainter::InlineBoxPainter(const LegacyInlineFlowBox& inlineBox, PaintInfo& paintInfo, const LayoutPoint& paintOffset)
: InlineBoxPainter(*InlineIterator::inlineBoxFor(inlineBox), paintInfo, paintOffset)
{
}
#if ENABLE(LAYOUT_FORMATTING_CONTEXT)
InlineBoxPainter::InlineBoxPainter(const LayoutIntegration::InlineContent& inlineContent, const InlineDisplay::Box& box, PaintInfo& paintInfo, const LayoutPoint& paintOffset)
: InlineBoxPainter(*InlineIterator::inlineBoxFor(inlineContent, box), paintInfo, paintOffset)
{
}
#endif
InlineBoxPainter::InlineBoxPainter(const InlineIterator::InlineBox& inlineBox, PaintInfo& paintInfo, const LayoutPoint& paintOffset)
: m_inlineBox(inlineBox)
, m_paintInfo(paintInfo)
, m_paintOffset(paintOffset)
, m_renderer(m_inlineBox.renderer())
, m_isFirstLineBox(m_inlineBox.lineBox()->isFirst())
, m_isRootInlineBox(m_inlineBox.isRootInlineBox())
, m_isHorizontal(m_inlineBox.isHorizontal())
{
}
InlineBoxPainter::~InlineBoxPainter() = default;
void InlineBoxPainter::paint()
{
if (m_paintInfo.phase == PaintPhase::Outline || m_paintInfo.phase == PaintPhase::SelfOutline) {
if (renderer().style().visibility() != Visibility::Visible || !renderer().hasOutline() || m_isRootInlineBox)
return;
auto& inlineFlow = downcast<RenderInline>(renderer());
RenderBlock* containingBlock = nullptr;
bool containingBlockPaintsContinuationOutline = inlineFlow.continuation() || inlineFlow.isContinuation();
if (containingBlockPaintsContinuationOutline) {
// FIXME: See https://bugs.webkit.org/show_bug.cgi?id=54690. We currently don't reconnect inline continuations
// after a child removal. As a result, those merged inlines do not get seperated and hence not get enclosed by
// anonymous blocks. In this case, it is better to bail out and paint it ourself.
RenderBlock* enclosingAnonymousBlock = renderer().containingBlock();
if (!enclosingAnonymousBlock->isAnonymousBlock())
containingBlockPaintsContinuationOutline = false;
else {
containingBlock = enclosingAnonymousBlock->containingBlock();
for (auto* box = &renderer(); box != containingBlock; box = &box->parent()->enclosingBoxModelObject()) {
if (box->hasSelfPaintingLayer()) {
containingBlockPaintsContinuationOutline = false;
break;
}
}
}
}
if (containingBlockPaintsContinuationOutline) {
// Add ourselves to the containing block of the entire continuation so that it can
// paint us atomically.
containingBlock->addContinuationWithOutline(downcast<RenderInline>(renderer().element()->renderer()));
} else if (!inlineFlow.isContinuation())
m_paintInfo.outlineObjects->add(&inlineFlow);
return;
}
if (m_paintInfo.phase == PaintPhase::Mask) {
paintMask();
return;
}
paintDecorations();
}
static LayoutRect clipRectForNinePieceImageStrip(const InlineIterator::InlineBox& box, const NinePieceImage& image, const LayoutRect& paintRect)
{
LayoutRect clipRect(paintRect);
auto& style = box.renderer().style();
LayoutBoxExtent outsets = style.imageOutsets(image);
auto [hasClosedLeftEdge, hasClosedRightEdge] = box.hasClosedLeftAndRightEdge();
if (box.isHorizontal()) {
clipRect.setY(paintRect.y() - outsets.top());
clipRect.setHeight(paintRect.height() + outsets.top() + outsets.bottom());
if (hasClosedLeftEdge) {
clipRect.setX(paintRect.x() - outsets.left());
clipRect.setWidth(paintRect.width() + outsets.left());
}
if (hasClosedRightEdge)
clipRect.setWidth(clipRect.width() + outsets.right());
} else {
clipRect.setX(paintRect.x() - outsets.left());
clipRect.setWidth(paintRect.width() + outsets.left() + outsets.right());
if (hasClosedLeftEdge) {
clipRect.setY(paintRect.y() - outsets.top());
clipRect.setHeight(paintRect.height() + outsets.top());
}
if (hasClosedRightEdge)
clipRect.setHeight(clipRect.height() + outsets.bottom());
}
return clipRect;
}
void InlineBoxPainter::paintMask()
{
if (!m_paintInfo.shouldPaintWithinRoot(renderer()) || renderer().style().visibility() != Visibility::Visible || m_paintInfo.phase != PaintPhase::Mask)
return;
// Move x/y to our coordinates.
auto localRect = LayoutRect { m_inlineBox.visualRect() };
LayoutPoint adjustedPaintOffset = m_paintOffset + localRect.location();
const NinePieceImage& maskNinePieceImage = renderer().style().maskBoxImage();
StyleImage* maskBoxImage = renderer().style().maskBoxImage().image();
// Figure out if we need to push a transparency layer to render our mask.
bool pushTransparencyLayer = false;
bool compositedMask = renderer().hasLayer() && renderer().layer()->hasCompositedMask();
bool flattenCompositingLayers = renderer().view().frameView().paintBehavior().contains(PaintBehavior::FlattenCompositingLayers);
CompositeOperator compositeOp = CompositeOperator::SourceOver;
if (!compositedMask || flattenCompositingLayers) {
if ((maskBoxImage && renderer().style().maskLayers().hasImage()) || renderer().style().maskLayers().next())
pushTransparencyLayer = true;
compositeOp = CompositeOperator::DestinationIn;
if (pushTransparencyLayer) {
m_paintInfo.context().setCompositeOperation(CompositeOperator::DestinationIn);
m_paintInfo.context().beginTransparencyLayer(1.0f);
compositeOp = CompositeOperator::SourceOver;
}
}
LayoutRect paintRect = LayoutRect(adjustedPaintOffset, localRect.size());
paintFillLayers(Color(), renderer().style().maskLayers(), paintRect, compositeOp);
bool hasBoxImage = maskBoxImage && maskBoxImage->canRender(&renderer(), renderer().style().effectiveZoom());
if (!hasBoxImage || !maskBoxImage->isLoaded()) {
if (pushTransparencyLayer)
m_paintInfo.context().endTransparencyLayer();
return; // Don't paint anything while we wait for the image to load.
}
bool hasSingleLine = !m_inlineBox.previousInlineBox() && !m_inlineBox.nextInlineBox();
if (hasSingleLine)
renderer().paintNinePieceImage(m_paintInfo.context(), LayoutRect(adjustedPaintOffset, localRect.size()), renderer().style(), maskNinePieceImage, compositeOp);
else {
// We have a mask image that spans multiple lines.
// We need to adjust _tx and _ty by the width of all previous lines.
LayoutUnit logicalOffsetOnLine;
for (auto box = m_inlineBox.previousInlineBox(); box; box.traversePreviousInlineBox())
logicalOffsetOnLine += box->logicalWidth();
LayoutUnit totalLogicalWidth = logicalOffsetOnLine;
for (auto box = m_inlineBox.iterator(); box; box.traverseNextInlineBox())
totalLogicalWidth += box->logicalWidth();
LayoutUnit stripX = adjustedPaintOffset.x() - (isHorizontal() ? logicalOffsetOnLine : 0_lu);
LayoutUnit stripY = adjustedPaintOffset.y() - (isHorizontal() ? 0_lu : logicalOffsetOnLine);
LayoutUnit stripWidth = isHorizontal() ? totalLogicalWidth : localRect.width();
LayoutUnit stripHeight = isHorizontal() ? localRect.height() : totalLogicalWidth;
LayoutRect clipRect = clipRectForNinePieceImageStrip(m_inlineBox, maskNinePieceImage, paintRect);
GraphicsContextStateSaver stateSaver(m_paintInfo.context());
m_paintInfo.context().clip(clipRect);
renderer().paintNinePieceImage(m_paintInfo.context(), LayoutRect(stripX, stripY, stripWidth, stripHeight), renderer().style(), maskNinePieceImage, compositeOp);
}
if (pushTransparencyLayer)
m_paintInfo.context().endTransparencyLayer();
}
void InlineBoxPainter::paintDecorations()
{
if (!m_paintInfo.shouldPaintWithinRoot(renderer()) || renderer().style().visibility() != Visibility::Visible || m_paintInfo.phase != PaintPhase::Foreground)
return;
if (!m_isRootInlineBox && !renderer().hasVisibleBoxDecorations())
return;
auto& style = this->style();
// You can use p::first-line to specify a background. If so, the root inline boxes for
// a line may actually have to paint a background.
if (m_isRootInlineBox && (!m_isFirstLineBox || &style == &renderer().style()))
return;
// Move x/y to our coordinates.
auto localRect = LayoutRect { m_inlineBox.visualRect() };
LayoutPoint adjustedPaintoffset = m_paintOffset + localRect.location();
GraphicsContext& context = m_paintInfo.context();
LayoutRect paintRect = LayoutRect(adjustedPaintoffset, localRect.size());
// Shadow comes first and is behind the background and border.
if (!renderer().boxShadowShouldBeAppliedToBackground(adjustedPaintoffset, BackgroundBleedNone, m_inlineBox))
paintBoxShadow(ShadowStyle::Normal, paintRect);
auto color = style.visitedDependentColor(CSSPropertyBackgroundColor);
auto compositeOp = renderer().document().compositeOperatorForBackgroundColor(color, renderer());
color = style.colorByApplyingColorFilter(color);
paintFillLayers(color, style.backgroundLayers(), paintRect, compositeOp);
paintBoxShadow(ShadowStyle::Inset, paintRect);
// :first-line cannot be used to put borders on a line. Always paint borders with our
// non-first-line style.
if (m_isRootInlineBox || !renderer().style().hasVisibleBorderDecoration())
return;
const NinePieceImage& borderImage = renderer().style().borderImage();
StyleImage* borderImageSource = borderImage.image();
bool hasBorderImage = borderImageSource && borderImageSource->canRender(&renderer(), style.effectiveZoom());
if (hasBorderImage && !borderImageSource->isLoaded())
return; // Don't paint anything while we wait for the image to load.
bool hasSingleLine = !m_inlineBox.previousInlineBox() && !m_inlineBox.nextInlineBox();
if (!hasBorderImage || hasSingleLine) {
auto [hasClosedLeftEdge, hasClosedRightEdge] = m_inlineBox.hasClosedLeftAndRightEdge();
renderer().paintBorder(m_paintInfo, paintRect, style, BackgroundBleedNone, hasClosedLeftEdge, hasClosedRightEdge);
return;
}
// We have a border image that spans multiple lines.
// We need to adjust tx and ty by the width of all previous lines.
// Think of border image painting on inlines as though you had one long line, a single continuous
// strip. Even though that strip has been broken up across multiple lines, you still paint it
// as though you had one single line. This means each line has to pick up the image where
// the previous line left off.
// FIXME: What the heck do we do with RTL here? The math we're using is obviously not right,
// but it isn't even clear how this should work at all.
LayoutUnit logicalOffsetOnLine;
for (auto box = m_inlineBox.previousInlineBox(); box; box.traversePreviousInlineBox())
logicalOffsetOnLine += box->logicalWidth();
LayoutUnit totalLogicalWidth = logicalOffsetOnLine;
for (auto box = m_inlineBox.iterator(); box; box.traverseNextInlineBox())
totalLogicalWidth += box->logicalWidth();
LayoutUnit stripX = adjustedPaintoffset.x() - (isHorizontal() ? logicalOffsetOnLine : 0_lu);
LayoutUnit stripY = adjustedPaintoffset.y() - (isHorizontal() ? 0_lu : logicalOffsetOnLine);
LayoutUnit stripWidth = isHorizontal() ? totalLogicalWidth : localRect.width();
LayoutUnit stripHeight = isHorizontal() ? localRect.height() : totalLogicalWidth;
LayoutRect clipRect = clipRectForNinePieceImageStrip(m_inlineBox, borderImage, paintRect);
GraphicsContextStateSaver stateSaver(context);
context.clip(clipRect);
renderer().paintBorder(m_paintInfo, LayoutRect(stripX, stripY, stripWidth, stripHeight), style);
}
void InlineBoxPainter::paintFillLayers(const Color& color, const FillLayer& fillLayer, const LayoutRect& rect, CompositeOperator op)
{
Vector<const FillLayer*, 8> layers;
for (auto* layer = &fillLayer; layer; layer = layer->next())
layers.append(layer);
layers.reverse();
for (auto* layer : layers)
paintFillLayer(color, *layer, rect, op);
}
void InlineBoxPainter::paintFillLayer(const Color& color, const FillLayer& fillLayer, const LayoutRect& rect, CompositeOperator op)
{
auto* image = fillLayer.image();
bool hasFillImage = image && image->canRender(&renderer(), renderer().style().effectiveZoom());
bool hasFillImageOrBorderRadious = hasFillImage || renderer().style().hasBorderRadius();
bool hasSingleLine = !m_inlineBox.previousInlineBox() && !m_inlineBox.nextInlineBox();
if (!hasFillImageOrBorderRadious || hasSingleLine || m_isRootInlineBox) {
renderer().paintFillLayerExtended(m_paintInfo, color, fillLayer, rect, BackgroundBleedNone, m_inlineBox, { }, op);
return;
}
#if ENABLE(CSS_BOX_DECORATION_BREAK)
if (renderer().style().boxDecorationBreak() == BoxDecorationBreak::Clone) {
GraphicsContextStateSaver stateSaver(m_paintInfo.context());
m_paintInfo.context().clip({ rect.location(), m_inlineBox.visualRectIgnoringBlockDirection().size() });
renderer().paintFillLayerExtended(m_paintInfo, color, fillLayer, rect, BackgroundBleedNone, m_inlineBox, { }, op);
return;
}
#endif
// We have a fill image that spans multiple lines.
// We need to adjust tx and ty by the width of all previous lines.
// Think of background painting on inlines as though you had one long line, a single continuous
// strip. Even though that strip has been broken up across multiple lines, you still paint it
// as though you had one single line. This means each line has to pick up the background where
// the previous line left off.
LayoutUnit logicalOffsetOnLine;
LayoutUnit totalLogicalWidth;
if (renderer().style().direction() == TextDirection::LTR) {
for (auto box = m_inlineBox.previousInlineBox(); box; box.traversePreviousInlineBox())
logicalOffsetOnLine += box->logicalWidth();
totalLogicalWidth = logicalOffsetOnLine;
for (auto box = m_inlineBox.iterator(); box; box.traverseNextInlineBox())
totalLogicalWidth += box->logicalWidth();
} else {
for (auto box = m_inlineBox.nextInlineBox(); box; box.traverseNextInlineBox())
logicalOffsetOnLine += box->logicalWidth();
totalLogicalWidth = logicalOffsetOnLine;
for (auto box = m_inlineBox.iterator(); box; box.traversePreviousInlineBox())
totalLogicalWidth += box->logicalWidth();
}
LayoutRect backgroundImageStrip {
rect.x() - (isHorizontal() ? logicalOffsetOnLine : 0_lu),
rect.y() - (isHorizontal() ? 0_lu : logicalOffsetOnLine),
isHorizontal() ? totalLogicalWidth : LayoutUnit(m_inlineBox.visualRectIgnoringBlockDirection().width()),
isHorizontal() ? LayoutUnit(m_inlineBox.visualRectIgnoringBlockDirection().height()) : totalLogicalWidth
};
GraphicsContextStateSaver stateSaver(m_paintInfo.context());
m_paintInfo.context().clip(FloatRect { rect });
renderer().paintFillLayerExtended(m_paintInfo, color, fillLayer, rect, BackgroundBleedNone, m_inlineBox, backgroundImageStrip, op);
}
void InlineBoxPainter::paintBoxShadow(ShadowStyle shadowStyle, const LayoutRect& paintRect)
{
bool hasSingleLine = !m_inlineBox.previousInlineBox() && !m_inlineBox.nextInlineBox();
if (hasSingleLine || m_isRootInlineBox) {
renderer().paintBoxShadow(m_paintInfo, paintRect, style(), shadowStyle);
return;
}
// FIXME: We can do better here in the multi-line case. We want to push a clip so that the shadow doesn't
// protrude incorrectly at the edges, and we want to possibly include shadows cast from the previous/following lines
auto [hasClosedLeftEdge, hasClosedRightEdge] = m_inlineBox.hasClosedLeftAndRightEdge();
renderer().paintBoxShadow(m_paintInfo, paintRect, style(), shadowStyle, hasClosedLeftEdge, hasClosedRightEdge);
}
const RenderStyle& InlineBoxPainter::style() const
{
return m_isFirstLineBox ? renderer().firstLineStyle() : renderer().style();
}
}