| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "core/paint/BackgroundImageGeometry.h" |
| |
| #include "core/frame/FrameView.h" |
| #include "core/layout/LayoutBox.h" |
| #include "core/layout/LayoutBoxModelObject.h" |
| #include "core/layout/LayoutView.h" |
| #include "core/layout/compositing/CompositedLayerMapping.h" |
| #include "core/paint/PaintLayer.h" |
| #include "platform/LayoutUnit.h" |
| #include "platform/geometry/LayoutRect.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| // Return the amount of space to leave between image tiles for the |
| // background-repeat: space property. |
| inline LayoutUnit getSpaceBetweenImageTiles(LayoutUnit areaSize, |
| LayoutUnit tileSize) { |
| int numberOfTiles = (areaSize / tileSize).toInt(); |
| LayoutUnit space(-1); |
| |
| if (numberOfTiles > 1) { |
| // Spec doesn't specify rounding, so use the same method as for |
| // background-repeat: round. |
| space = (areaSize - numberOfTiles * tileSize) / (numberOfTiles - 1); |
| } |
| |
| return space; |
| } |
| |
| bool fixedBackgroundPaintsInLocalCoordinates( |
| const LayoutObject& obj, |
| const GlobalPaintFlags globalPaintFlags) { |
| if (!obj.isLayoutView()) |
| return false; |
| |
| const LayoutView& view = toLayoutView(obj); |
| |
| if (globalPaintFlags & GlobalPaintFlattenCompositingLayers) |
| return false; |
| |
| PaintLayer* rootLayer = view.layer(); |
| if (!rootLayer || rootLayer->compositingState() == NotComposited) |
| return false; |
| |
| return rootLayer->compositedLayerMapping() |
| ->backgroundLayerPaintsFixedRootBackground(); |
| } |
| |
| LayoutSize calculateFillTileSize(const LayoutBoxModelObject& obj, |
| const FillLayer& fillLayer, |
| const LayoutSize& positioningAreaSize) { |
| StyleImage* image = fillLayer.image(); |
| EFillSizeType type = fillLayer.size().type; |
| |
| LayoutSize imageIntrinsicSize = |
| image->imageSize(obj, obj.style()->effectiveZoom(), positioningAreaSize); |
| switch (type) { |
| case SizeLength: { |
| LayoutSize tileSize(positioningAreaSize); |
| |
| Length layerWidth = fillLayer.size().size.width(); |
| Length layerHeight = fillLayer.size().size.height(); |
| |
| if (layerWidth.isFixed()) |
| tileSize.setWidth(LayoutUnit(layerWidth.value())); |
| else if (layerWidth.isPercentOrCalc()) |
| tileSize.setWidth( |
| valueForLength(layerWidth, positioningAreaSize.width())); |
| |
| if (layerHeight.isFixed()) |
| tileSize.setHeight(LayoutUnit(layerHeight.value())); |
| else if (layerHeight.isPercentOrCalc()) |
| tileSize.setHeight( |
| valueForLength(layerHeight, positioningAreaSize.height())); |
| |
| // If one of the values is auto we have to use the appropriate |
| // scale to maintain our aspect ratio. |
| if (layerWidth.isAuto() && !layerHeight.isAuto()) { |
| if (imageIntrinsicSize.height()) { |
| LayoutUnit adjustedWidth = imageIntrinsicSize.width() * |
| tileSize.height() / |
| imageIntrinsicSize.height(); |
| if (imageIntrinsicSize.width() >= 1 && adjustedWidth < 1) |
| adjustedWidth = LayoutUnit(1); |
| tileSize.setWidth(adjustedWidth); |
| } |
| } else if (!layerWidth.isAuto() && layerHeight.isAuto()) { |
| if (imageIntrinsicSize.width()) { |
| LayoutUnit adjustedHeight = imageIntrinsicSize.height() * |
| tileSize.width() / |
| imageIntrinsicSize.width(); |
| if (imageIntrinsicSize.height() >= 1 && adjustedHeight < 1) |
| adjustedHeight = LayoutUnit(1); |
| tileSize.setHeight(adjustedHeight); |
| } |
| } else if (layerWidth.isAuto() && layerHeight.isAuto()) { |
| // If both width and height are auto, use the image's intrinsic size. |
| tileSize = imageIntrinsicSize; |
| } |
| |
| tileSize.clampNegativeToZero(); |
| return tileSize; |
| } |
| case SizeNone: { |
| // If both values are 'auto' then the intrinsic width and/or height of the |
| // image should be used, if any. |
| if (!imageIntrinsicSize.isEmpty()) |
| return imageIntrinsicSize; |
| |
| // If the image has neither an intrinsic width nor an intrinsic height, |
| // its size is determined as for 'contain'. |
| type = Contain; |
| } |
| case Contain: |
| case Cover: { |
| float horizontalScaleFactor = |
| imageIntrinsicSize.width() |
| ? positioningAreaSize.width().toFloat() / |
| imageIntrinsicSize.width() |
| : 1.0f; |
| float verticalScaleFactor = imageIntrinsicSize.height() |
| ? positioningAreaSize.height().toFloat() / |
| imageIntrinsicSize.height() |
| : 1.0f; |
| // Force the dimension that determines the size to exactly match the |
| // positioningAreaSize in that dimension, so that rounding of floating |
| // point approximation to LayoutUnit do not shrink the image to smaller |
| // than the positioningAreaSize. |
| if (type == Contain) { |
| if (horizontalScaleFactor < verticalScaleFactor) |
| return LayoutSize( |
| positioningAreaSize.width(), |
| LayoutUnit(std::max( |
| 1.0f, imageIntrinsicSize.height() * horizontalScaleFactor))); |
| return LayoutSize(LayoutUnit(std::max(1.0f, imageIntrinsicSize.width() * |
| verticalScaleFactor)), |
| positioningAreaSize.height()); |
| } |
| if (horizontalScaleFactor > verticalScaleFactor) |
| return LayoutSize( |
| positioningAreaSize.width(), |
| LayoutUnit(std::max( |
| 1.0f, imageIntrinsicSize.height() * horizontalScaleFactor))); |
| return LayoutSize(LayoutUnit(std::max(1.0f, imageIntrinsicSize.width() * |
| verticalScaleFactor)), |
| positioningAreaSize.height()); |
| } |
| } |
| |
| NOTREACHED(); |
| return LayoutSize(); |
| } |
| |
| IntPoint accumulatedScrollOffsetForFixedBackground( |
| const LayoutBoxModelObject& object, |
| const LayoutBoxModelObject* container) { |
| IntPoint result; |
| if (&object == container) |
| return result; |
| for (const LayoutBlock* block = object.containingBlock(); block; |
| block = block->containingBlock()) { |
| if (block->hasOverflowClip()) |
| result += block->scrolledContentOffset(); |
| if (block == container) |
| break; |
| } |
| return result; |
| } |
| |
| // When we match the sub-pixel fraction of the destination rect in a dimension, |
| // we snap the same way. This commonly occurs when the background is meant to |
| // fill the padding box but there's a border (which in Blink is always stored as |
| // an integer). Otherwise we floor to avoid growing our tile size. Often these |
| // tiles are from a sprite map, and bleeding adjacent sprites is visually worse |
| // than clipping the intended one. |
| LayoutSize applySubPixelHeuristicToImageSize(const LayoutSize& size, |
| const LayoutRect& destination) { |
| LayoutSize snappedSize = |
| LayoutSize(size.width().fraction() == destination.width().fraction() |
| ? snapSizeToPixel(size.width(), destination.x()) |
| : size.width().floor(), |
| size.height().fraction() == destination.height().fraction() |
| ? snapSizeToPixel(size.height(), destination.y()) |
| : size.height().floor()); |
| return snappedSize; |
| } |
| |
| } // anonymous namespace |
| |
| void BackgroundImageGeometry::setNoRepeatX(LayoutUnit xOffset) { |
| int roundedOffset = roundToInt(xOffset); |
| m_destRect.move(std::max(roundedOffset, 0), 0); |
| setPhaseX(LayoutUnit(-std::min(roundedOffset, 0))); |
| m_destRect.setWidth(m_tileSize.width() + std::min(roundedOffset, 0)); |
| setSpaceSize(LayoutSize(LayoutUnit(), spaceSize().height())); |
| } |
| |
| void BackgroundImageGeometry::setNoRepeatY(LayoutUnit yOffset) { |
| int roundedOffset = roundToInt(yOffset); |
| m_destRect.move(0, std::max(roundedOffset, 0)); |
| setPhaseY(LayoutUnit(-std::min(roundedOffset, 0))); |
| m_destRect.setHeight(m_tileSize.height() + std::min(roundedOffset, 0)); |
| setSpaceSize(LayoutSize(spaceSize().width(), LayoutUnit())); |
| } |
| |
| void BackgroundImageGeometry::setRepeatX(const FillLayer& fillLayer, |
| LayoutUnit unsnappedTileWidth, |
| LayoutUnit snappedAvailableWidth, |
| LayoutUnit unsnappedAvailableWidth, |
| LayoutUnit extraOffset) { |
| // We would like to identify the phase as a fraction of the image size in the |
| // absence of snapping, then re-apply it to the snapped values. This is to |
| // handle large positions. |
| if (unsnappedTileWidth) { |
| LayoutUnit computedXPosition = roundedMinimumValueForLength( |
| fillLayer.xPosition(), unsnappedAvailableWidth); |
| if (fillLayer.backgroundXOrigin() == RightEdge) { |
| float numberOfTilesInPosition = |
| (snappedAvailableWidth - computedXPosition + extraOffset).toFloat() / |
| unsnappedTileWidth.toFloat(); |
| float fractionalPositionWithinTile = |
| numberOfTilesInPosition - truncf(numberOfTilesInPosition); |
| setPhaseX(LayoutUnit( |
| roundf(fractionalPositionWithinTile * tileSize().width()))); |
| } else { |
| float numberOfTilesInPosition = |
| (computedXPosition + extraOffset).toFloat() / |
| unsnappedTileWidth.toFloat(); |
| float fractionalPositionWithinTile = |
| 1.0f - (numberOfTilesInPosition - truncf(numberOfTilesInPosition)); |
| setPhaseX(LayoutUnit( |
| roundf(fractionalPositionWithinTile * tileSize().width()))); |
| } |
| } else { |
| setPhaseX(LayoutUnit()); |
| } |
| setSpaceSize(LayoutSize(LayoutUnit(), spaceSize().height())); |
| } |
| |
| void BackgroundImageGeometry::setRepeatY(const FillLayer& fillLayer, |
| LayoutUnit unsnappedTileHeight, |
| LayoutUnit snappedAvailableHeight, |
| LayoutUnit unsnappedAvailableHeight, |
| LayoutUnit extraOffset) { |
| // We would like to identify the phase as a fraction of the image size in the |
| // absence of snapping, then re-apply it to the snapped values. This is to |
| // handle large positions. |
| if (unsnappedTileHeight) { |
| LayoutUnit computedYPosition = roundedMinimumValueForLength( |
| fillLayer.yPosition(), unsnappedAvailableHeight); |
| if (fillLayer.backgroundYOrigin() == BottomEdge) { |
| float numberOfTilesInPosition = |
| (snappedAvailableHeight - computedYPosition + extraOffset).toFloat() / |
| unsnappedTileHeight.toFloat(); |
| float fractionalPositionWithinTile = |
| numberOfTilesInPosition - truncf(numberOfTilesInPosition); |
| setPhaseY(LayoutUnit( |
| roundf(fractionalPositionWithinTile * tileSize().height()))); |
| } else { |
| float numberOfTilesInPosition = |
| (computedYPosition + extraOffset).toFloat() / |
| unsnappedTileHeight.toFloat(); |
| float fractionalPositionWithinTile = |
| 1.0f - (numberOfTilesInPosition - truncf(numberOfTilesInPosition)); |
| setPhaseY(LayoutUnit( |
| roundf(fractionalPositionWithinTile * tileSize().height()))); |
| } |
| } else { |
| setPhaseY(LayoutUnit()); |
| } |
| setSpaceSize(LayoutSize(spaceSize().width(), LayoutUnit())); |
| } |
| |
| void BackgroundImageGeometry::setSpaceX(LayoutUnit space, |
| LayoutUnit availableWidth, |
| LayoutUnit extraOffset) { |
| LayoutUnit computedXPosition = |
| roundedMinimumValueForLength(Length(), availableWidth); |
| setSpaceSize(LayoutSize(space.round(), spaceSize().height().toInt())); |
| LayoutUnit actualWidth = tileSize().width() + space; |
| setPhaseX(actualWidth |
| ? LayoutUnit(roundf( |
| actualWidth - |
| fmodf((computedXPosition + extraOffset), actualWidth))) |
| : LayoutUnit()); |
| } |
| |
| void BackgroundImageGeometry::setSpaceY(LayoutUnit space, |
| LayoutUnit availableHeight, |
| LayoutUnit extraOffset) { |
| LayoutUnit computedYPosition = |
| roundedMinimumValueForLength(Length(), availableHeight); |
| setSpaceSize(LayoutSize(spaceSize().width().toInt(), space.round())); |
| LayoutUnit actualHeight = tileSize().height() + space; |
| setPhaseY(actualHeight |
| ? LayoutUnit(roundf( |
| actualHeight - |
| fmodf((computedYPosition + extraOffset), actualHeight))) |
| : LayoutUnit()); |
| } |
| |
| void BackgroundImageGeometry::useFixedAttachment( |
| const LayoutPoint& attachmentPoint) { |
| LayoutPoint alignedPoint = attachmentPoint; |
| m_phase.move(std::max(alignedPoint.x() - m_destRect.x(), LayoutUnit()), |
| std::max(alignedPoint.y() - m_destRect.y(), LayoutUnit())); |
| setPhase(LayoutPoint(roundedIntPoint(m_phase))); |
| } |
| |
| void BackgroundImageGeometry::calculate( |
| const LayoutBoxModelObject& obj, |
| const LayoutBoxModelObject* paintContainer, |
| const GlobalPaintFlags globalPaintFlags, |
| const FillLayer& fillLayer, |
| const LayoutRect& paintRect) { |
| LayoutUnit left; |
| LayoutUnit top; |
| LayoutSize positioningAreaSize; |
| bool isLayoutView = obj.isLayoutView(); |
| const LayoutBox* rootBox = nullptr; |
| if (isLayoutView) { |
| // It is only possible reach here when root element has a box. |
| Element* documentElement = obj.document().documentElement(); |
| DCHECK(documentElement); |
| DCHECK(documentElement->layoutObject()); |
| DCHECK(documentElement->layoutObject()->isBox()); |
| rootBox = toLayoutBox(documentElement->layoutObject()); |
| } |
| const LayoutBoxModelObject& positioningBox = |
| isLayoutView ? static_cast<const LayoutBoxModelObject&>(*rootBox) : obj; |
| |
| // Determine the background positioning area and set destRect to the |
| // background painting area. destRect will be adjusted later if the |
| // background is non-repeating. |
| // FIXME: transforms spec says that fixed backgrounds behave like scroll |
| // inside transforms. |
| bool fixedAttachment = fillLayer.attachment() == FixedBackgroundAttachment; |
| |
| if (RuntimeEnabledFeatures::fastMobileScrollingEnabled()) { |
| // As a side effect of an optimization to blit on scroll, we do not honor |
| // the CSS property "background-attachment: fixed" because it may result in |
| // rendering artifacts. Note, these artifacts only appear if we are blitting |
| // on scroll of a page that has fixed background images. |
| fixedAttachment = false; |
| } |
| |
| if (!fixedAttachment) { |
| setDestRect(paintRect); |
| |
| LayoutUnit right; |
| LayoutUnit bottom; |
| // Scroll and Local. |
| if (fillLayer.origin() != BorderFillBox) { |
| left = LayoutUnit(positioningBox.borderLeft()); |
| right = LayoutUnit(positioningBox.borderRight()); |
| top = LayoutUnit(positioningBox.borderTop()); |
| bottom = LayoutUnit(positioningBox.borderBottom()); |
| if (fillLayer.origin() == ContentFillBox) { |
| left += positioningBox.paddingLeft(); |
| right += positioningBox.paddingRight(); |
| top += positioningBox.paddingTop(); |
| bottom += positioningBox.paddingBottom(); |
| } |
| } |
| |
| if (isLayoutView) { |
| // The background of the box generated by the root element covers the |
| // entire canvas and will be painted by the view object, but the we should |
| // still use the root element box for positioning. |
| positioningAreaSize = |
| rootBox->size() - LayoutSize(left + right, top + bottom), |
| rootBox->location(); |
| // The input paint rect is specified in root element local coordinate |
| // (i.e. a transform is applied on the context for painting), and is |
| // expanded to cover the whole canvas. Since left/top is relative to the |
| // paint rect, we need to offset them back. |
| left -= paintRect.x(); |
| top -= paintRect.y(); |
| } else { |
| positioningAreaSize = |
| paintRect.size() - LayoutSize(left + right, top + bottom); |
| } |
| } else { |
| setHasNonLocalGeometry(); |
| |
| LayoutRect viewportRect = obj.viewRect(); |
| if (fixedBackgroundPaintsInLocalCoordinates(obj, globalPaintFlags)) { |
| viewportRect.setLocation(LayoutPoint()); |
| } else { |
| if (FrameView* frameView = obj.view()->frameView()) |
| viewportRect.setLocation(frameView->scrollPosition()); |
| // Compensate the translations created by ScrollRecorders. |
| // TODO(trchen): Fix this for SP phase 2. crbug.com/529963. |
| viewportRect.moveBy( |
| accumulatedScrollOffsetForFixedBackground(obj, paintContainer)); |
| } |
| |
| if (paintContainer) |
| viewportRect.moveBy( |
| LayoutPoint(-paintContainer->localToAbsolute(FloatPoint()))); |
| |
| setDestRect(viewportRect); |
| positioningAreaSize = destRect().size(); |
| } |
| |
| LayoutSize fillTileSize( |
| calculateFillTileSize(positioningBox, fillLayer, positioningAreaSize)); |
| // It's necessary to apply the heuristic here prior to any further |
| // calculations to avoid incorrectly using sub-pixel values that won't be |
| // present in the painted tile. |
| setTileSize(applySubPixelHeuristicToImageSize(fillTileSize, m_destRect)); |
| |
| EFillRepeat backgroundRepeatX = fillLayer.repeatX(); |
| EFillRepeat backgroundRepeatY = fillLayer.repeatY(); |
| LayoutUnit unsnappedAvailableWidth = |
| positioningAreaSize.width() - fillTileSize.width(); |
| LayoutUnit unsnappedAvailableHeight = |
| positioningAreaSize.height() - fillTileSize.height(); |
| positioningAreaSize = |
| LayoutSize(snapSizeToPixel(positioningAreaSize.width(), m_destRect.x()), |
| snapSizeToPixel(positioningAreaSize.height(), m_destRect.y())); |
| LayoutUnit availableWidth = positioningAreaSize.width() - tileSize().width(); |
| LayoutUnit availableHeight = |
| positioningAreaSize.height() - tileSize().height(); |
| |
| LayoutUnit computedXPosition = |
| roundedMinimumValueForLength(fillLayer.xPosition(), availableWidth); |
| if (backgroundRepeatX == RoundFill && |
| positioningAreaSize.width() > LayoutUnit() && |
| fillTileSize.width() > LayoutUnit()) { |
| int nrTiles = std::max( |
| 1, roundToInt(positioningAreaSize.width() / fillTileSize.width())); |
| LayoutUnit roundedWidth = positioningAreaSize.width() / nrTiles; |
| |
| // Maintain aspect ratio if background-size: auto is set |
| if (fillLayer.size().size.height().isAuto() && |
| backgroundRepeatY != RoundFill) { |
| fillTileSize.setHeight(fillTileSize.height() * roundedWidth / |
| fillTileSize.width()); |
| } |
| fillTileSize.setWidth(roundedWidth); |
| |
| setTileSize(applySubPixelHeuristicToImageSize(fillTileSize, m_destRect)); |
| setPhaseX(tileSize().width() |
| ? LayoutUnit(roundf( |
| tileSize().width() - |
| fmodf((computedXPosition + left), tileSize().width()))) |
| : LayoutUnit()); |
| setSpaceSize(LayoutSize()); |
| } |
| |
| LayoutUnit computedYPosition = |
| roundedMinimumValueForLength(fillLayer.yPosition(), availableHeight); |
| if (backgroundRepeatY == RoundFill && |
| positioningAreaSize.height() > LayoutUnit() && |
| fillTileSize.height() > LayoutUnit()) { |
| int nrTiles = std::max( |
| 1, roundToInt(positioningAreaSize.height() / fillTileSize.height())); |
| LayoutUnit roundedHeight = positioningAreaSize.height() / nrTiles; |
| // Maintain aspect ratio if background-size: auto is set |
| if (fillLayer.size().size.width().isAuto() && |
| backgroundRepeatX != RoundFill) { |
| fillTileSize.setWidth(fillTileSize.width() * roundedHeight / |
| fillTileSize.height()); |
| } |
| fillTileSize.setHeight(roundedHeight); |
| |
| setTileSize(applySubPixelHeuristicToImageSize(fillTileSize, m_destRect)); |
| setPhaseY(tileSize().height() |
| ? LayoutUnit(roundf( |
| tileSize().height() - |
| fmodf((computedYPosition + top), tileSize().height()))) |
| : LayoutUnit()); |
| setSpaceSize(LayoutSize()); |
| } |
| |
| if (backgroundRepeatX == RepeatFill) { |
| setRepeatX(fillLayer, fillTileSize.width(), availableWidth, |
| unsnappedAvailableWidth, left); |
| } else if (backgroundRepeatX == SpaceFill && |
| tileSize().width() > LayoutUnit()) { |
| LayoutUnit space = getSpaceBetweenImageTiles(positioningAreaSize.width(), |
| tileSize().width()); |
| if (space >= LayoutUnit()) |
| setSpaceX(space, availableWidth, left); |
| else |
| backgroundRepeatX = NoRepeatFill; |
| } |
| if (backgroundRepeatX == NoRepeatFill) { |
| LayoutUnit xOffset = fillLayer.backgroundXOrigin() == RightEdge |
| ? availableWidth - computedXPosition |
| : computedXPosition; |
| setNoRepeatX(left + xOffset); |
| } |
| |
| if (backgroundRepeatY == RepeatFill) { |
| setRepeatY(fillLayer, fillTileSize.height(), availableHeight, |
| unsnappedAvailableHeight, top); |
| } else if (backgroundRepeatY == SpaceFill && |
| tileSize().height() > LayoutUnit()) { |
| LayoutUnit space = getSpaceBetweenImageTiles(positioningAreaSize.height(), |
| tileSize().height()); |
| if (space >= LayoutUnit()) |
| setSpaceY(space, availableHeight, top); |
| else |
| backgroundRepeatY = NoRepeatFill; |
| } |
| if (backgroundRepeatY == NoRepeatFill) { |
| LayoutUnit yOffset = fillLayer.backgroundYOrigin() == BottomEdge |
| ? availableHeight - computedYPosition |
| : computedYPosition; |
| setNoRepeatY(top + yOffset); |
| } |
| |
| if (fixedAttachment) |
| useFixedAttachment(paintRect.location()); |
| |
| // Clip the final output rect to the paint rect |
| m_destRect.intersect(paintRect); |
| |
| // Snap as-yet unsnapped values. |
| setDestRect(LayoutRect(pixelSnappedIntRect(m_destRect))); |
| } |
| |
| } // namespace blink |