| /* |
| * 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(); |
| } |
| |
| } |