blob: 2519088ebccd1bcdc087030b0029f9426d6aa680 [file] [log] [blame]
// 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 "third_party/blink/renderer/core/paint/box_painter_base.h"
#include "base/optional.h"
#include "third_party/blink/renderer/core/animation/element_animations.h"
#include "third_party/blink/renderer/core/css/background_color_paint_image_generator.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/node_computed_style.h"
#include "third_party/blink/renderer/core/frame/settings.h"
#include "third_party/blink/renderer/core/inspector/inspector_trace_events.h"
#include "third_party/blink/renderer/core/layout/layout_progress.h"
#include "third_party/blink/renderer/core/paint/background_image_geometry.h"
#include "third_party/blink/renderer/core/paint/box_border_painter.h"
#include "third_party/blink/renderer/core/paint/image_element_timing.h"
#include "third_party/blink/renderer/core/paint/nine_piece_image_painter.h"
#include "third_party/blink/renderer/core/paint/paint_info.h"
#include "third_party/blink/renderer/core/paint/paint_layer.h"
#include "third_party/blink/renderer/core/paint/paint_timing_detector.h"
#include "third_party/blink/renderer/core/paint/rounded_border_geometry.h"
#include "third_party/blink/renderer/core/paint/rounded_inner_rect_clipper.h"
#include "third_party/blink/renderer/core/style/border_edge.h"
#include "third_party/blink/renderer/core/style/computed_style.h"
#include "third_party/blink/renderer/core/style/shadow_list.h"
#include "third_party/blink/renderer/core/style/style_fetched_image.h"
#include "third_party/blink/renderer/platform/geometry/layout_rect.h"
#include "third_party/blink/renderer/platform/graphics/bitmap_image.h"
#include "third_party/blink/renderer/platform/graphics/graphics_context_state_saver.h"
#include "third_party/blink/renderer/platform/graphics/paint/paint_controller.h"
#include "third_party/blink/renderer/platform/graphics/scoped_interpolation_quality.h"
#include "third_party/blink/renderer/platform/runtime_enabled_features.h"
namespace blink {
void BoxPainterBase::PaintFillLayers(const PaintInfo& paint_info,
const Color& c,
const FillLayer& fill_layer,
const PhysicalRect& rect,
BackgroundImageGeometry& geometry,
BackgroundBleedAvoidance bleed) {
FillLayerOcclusionOutputList reversed_paint_list;
bool should_draw_background_in_separate_buffer =
CalculateFillLayerOcclusionCulling(reversed_paint_list, fill_layer);
// TODO(trchen): We can optimize out isolation group if we have a
// non-transparent background color and the bottom layer encloses all other
// layers.
GraphicsContext& context = paint_info.context;
if (should_draw_background_in_separate_buffer)
context.BeginLayer();
for (auto it = reversed_paint_list.rbegin(); it != reversed_paint_list.rend();
++it) {
PaintFillLayer(paint_info, c, **it, rect, bleed, geometry);
}
if (should_draw_background_in_separate_buffer)
context.EndLayer();
}
namespace {
void ApplySpreadToShadowShape(FloatRoundedRect& shadow_shape, float spread) {
if (spread == 0)
return;
if (spread >= 0)
shadow_shape.ExpandRadii(spread);
else
shadow_shape.ShrinkRadii(-spread);
if (!shadow_shape.IsRenderable())
shadow_shape.AdjustRadii();
shadow_shape.ConstrainRadii();
}
} // namespace
void BoxPainterBase::PaintNormalBoxShadow(const PaintInfo& info,
const PhysicalRect& paint_rect,
const ComputedStyle& style,
PhysicalBoxSides sides_to_include,
bool background_is_skipped) {
if (!style.BoxShadow())
return;
GraphicsContext& context = info.context;
FloatRoundedRect border = RoundedBorderGeometry::PixelSnappedRoundedBorder(
style, paint_rect, sides_to_include);
bool has_border_radius = style.HasBorderRadius();
bool has_opaque_background =
!background_is_skipped &&
style.VisitedDependentColor(GetCSSPropertyBackgroundColor()).Alpha() ==
255;
GraphicsContextStateSaver state_saver(context, false);
const ShadowList* shadow_list = style.BoxShadow();
for (wtf_size_t i = shadow_list->Shadows().size(); i--;) {
const ShadowData& shadow = shadow_list->Shadows()[i];
if (shadow.Style() != ShadowStyle::kNormal)
continue;
FloatSize shadow_offset(shadow.X(), shadow.Y());
float shadow_blur = shadow.Blur();
float shadow_spread = shadow.Spread();
if (shadow_offset.IsZero() && !shadow_blur && !shadow_spread)
continue;
const Color& shadow_color = shadow.GetColor().Resolve(
style.VisitedDependentColor(GetCSSPropertyColor()),
style.UsedColorScheme());
FloatRect fill_rect = border.Rect();
fill_rect.Inflate(shadow_spread);
if (fill_rect.IsEmpty())
continue;
// Save the state and clip, if not already done.
// The clip does not depend on any shadow-specific properties.
if (!state_saver.Saved()) {
state_saver.Save();
if (has_border_radius) {
FloatRoundedRect rect_to_clip_out = border;
// 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 CSS pixel.
if (has_opaque_background)
rect_to_clip_out.InflateWithRadii(-1);
if (!rect_to_clip_out.IsEmpty())
context.ClipOutRoundedRect(rect_to_clip_out);
} else {
// This IntRect is correct even with fractional shadows, because it is
// used for the rectangle of the box itself, which is always
// pixel-aligned.
FloatRect rect_to_clip_out = border.Rect();
// 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 CSS pixel.
if (has_opaque_background)
rect_to_clip_out.Inflate(-1);
if (!rect_to_clip_out.IsEmpty())
context.ClipOut(rect_to_clip_out);
}
}
// Draw only the shadow. If the color of the shadow is transparent we will
// set an empty draw looper.
DrawLooperBuilder draw_looper_builder;
draw_looper_builder.AddShadow(shadow_offset, shadow_blur, shadow_color,
DrawLooperBuilder::kShadowRespectsTransforms,
DrawLooperBuilder::kShadowIgnoresAlpha);
context.SetDrawLooper(draw_looper_builder.DetachDrawLooper());
if (has_border_radius) {
FloatRoundedRect rounded_fill_rect = border;
rounded_fill_rect.Inflate(shadow_spread);
ApplySpreadToShadowShape(rounded_fill_rect, shadow_spread);
context.FillRoundedRect(rounded_fill_rect, Color::kBlack);
} else {
context.FillRect(fill_rect, Color::kBlack);
}
}
}
void BoxPainterBase::PaintInsetBoxShadowWithBorderRect(
const PaintInfo& info,
const PhysicalRect& border_rect,
const ComputedStyle& style,
PhysicalBoxSides sides_to_include) {
if (!style.BoxShadow())
return;
auto bounds = RoundedBorderGeometry::PixelSnappedRoundedInnerBorder(
style, border_rect, sides_to_include);
PaintInsetBoxShadow(info, bounds, style, sides_to_include);
}
void BoxPainterBase::PaintInsetBoxShadowWithInnerRect(
const PaintInfo& info,
const PhysicalRect& inner_rect,
const ComputedStyle& style) {
if (!style.BoxShadow())
return;
auto bounds = RoundedBorderGeometry::PixelSnappedRoundedInnerBorder(
style, inner_rect, LayoutRectOutsets());
PaintInsetBoxShadow(info, bounds, style);
}
namespace {
inline FloatRect AreaCastingShadowInHole(const FloatRect& hole_rect,
const ShadowData& shadow) {
FloatRect bounds(hole_rect);
bounds.Inflate(shadow.Blur());
if (shadow.Spread() < 0)
bounds.Inflate(-shadow.Spread());
FloatRect offset_bounds = bounds;
offset_bounds.MoveBy(-shadow.Location());
return UnionRect(bounds, offset_bounds);
}
void AdjustInnerRectForSideClipping(FloatRect& inner_rect,
const ShadowData& shadow,
PhysicalBoxSides sides_to_include) {
if (!sides_to_include.left) {
float extend_by = std::max(shadow.X(), 0.0f) + shadow.Blur();
inner_rect.Move(-extend_by, 0);
inner_rect.SetWidth(inner_rect.Width() + extend_by);
}
if (!sides_to_include.top) {
float extend_by = std::max(shadow.Y(), 0.0f) + shadow.Blur();
inner_rect.Move(0, -extend_by);
inner_rect.SetHeight(inner_rect.Height() + extend_by);
}
if (!sides_to_include.right) {
float shrink_by = std::min(shadow.X(), 0.0f) - shadow.Blur();
inner_rect.SetWidth(inner_rect.Width() - shrink_by);
}
if (!sides_to_include.bottom) {
float shrink_by = std::min(shadow.Y(), 0.0f) - shadow.Blur();
inner_rect.SetHeight(inner_rect.Height() - shrink_by);
}
}
} // namespace
void BoxPainterBase::PaintInsetBoxShadow(const PaintInfo& info,
const FloatRoundedRect& bounds,
const ComputedStyle& style,
PhysicalBoxSides sides_to_include) {
GraphicsContext& context = info.context;
const ShadowList* shadow_list = style.BoxShadow();
for (wtf_size_t i = shadow_list->Shadows().size(); i--;) {
const ShadowData& shadow = shadow_list->Shadows()[i];
if (shadow.Style() != ShadowStyle::kInset)
continue;
if (!shadow.X() && !shadow.Y() && !shadow.Blur() && !shadow.Spread())
continue;
const Color& shadow_color = shadow.GetColor().Resolve(
style.VisitedDependentColor(GetCSSPropertyColor()),
style.UsedColorScheme());
FloatRect inner_rect(bounds.Rect());
inner_rect.Inflate(-shadow.Spread());
if (inner_rect.IsEmpty()) {
context.FillRoundedRect(bounds, shadow_color);
continue;
}
AdjustInnerRectForSideClipping(inner_rect, shadow, sides_to_include);
FloatRoundedRect inner_rounded_rect(inner_rect, bounds.GetRadii());
GraphicsContextStateSaver state_saver(context);
if (bounds.IsRounded()) {
context.ClipRoundedRect(bounds);
ApplySpreadToShadowShape(inner_rounded_rect, -shadow.Spread());
} else {
context.Clip(bounds.Rect());
}
DrawLooperBuilder draw_looper_builder;
draw_looper_builder.AddShadow(ToFloatSize(shadow.Location()), shadow.Blur(),
shadow_color,
DrawLooperBuilder::kShadowRespectsTransforms,
DrawLooperBuilder::kShadowIgnoresAlpha);
context.SetDrawLooper(draw_looper_builder.DetachDrawLooper());
Color fill_color(shadow_color.Red(), shadow_color.Green(),
shadow_color.Blue());
FloatRect outer_rect = AreaCastingShadowInHole(bounds.Rect(), shadow);
context.FillRectWithRoundedHole(outer_rect, inner_rounded_rect, fill_color);
}
}
bool BoxPainterBase::ShouldForceWhiteBackgroundForPrintEconomy(
const Document& document,
const ComputedStyle& style) {
return document.Printing() &&
style.PrintColorAdjust() == EPrintColorAdjust::kEconomy &&
(!document.GetSettings() ||
!document.GetSettings()->GetShouldPrintBackgrounds());
}
bool BoxPainterBase::CalculateFillLayerOcclusionCulling(
FillLayerOcclusionOutputList& reversed_paint_list,
const FillLayer& fill_layer) {
bool is_non_associative = false;
for (auto* current_layer = &fill_layer; current_layer;
current_layer = current_layer->Next()) {
reversed_paint_list.push_back(current_layer);
// Stop traversal when an opaque layer is encountered.
// FIXME : It would be possible for the following occlusion culling test to
// be more aggressive on layers with no repeat by testing whether the image
// covers the layout rect. Testing that here would imply duplicating a lot
// of calculations that are currently done in
// LayoutBoxModelObject::paintFillLayer. A more efficient solution might be
// to move the layer recursion into paintFillLayer, or to compute the layer
// geometry here and pass it down.
// TODO(trchen): Need to check compositing mode as well.
if (current_layer->GetBlendMode() != BlendMode::kNormal)
is_non_associative = true;
// TODO(trchen): A fill layer cannot paint if the calculated tile size is
// empty. This occlusion check can be wrong.
if (current_layer->ClipOccludesNextLayers() &&
current_layer->ImageOccludesNextLayers(*document_, style_)) {
if (current_layer->Clip() == EFillBox::kBorder)
is_non_associative = false;
break;
}
}
return is_non_associative;
}
BoxPainterBase::FillLayerInfo::FillLayerInfo(
const Document& doc,
const ComputedStyle& style,
bool is_scroll_container,
Color bg_color,
const FillLayer& layer,
BackgroundBleedAvoidance bleed_avoidance,
RespectImageOrientationEnum respect_image_orientation,
PhysicalBoxSides sides_to_include,
bool is_inline,
bool is_painting_scrolling_background)
: image(layer.GetImage()),
color(bg_color),
respect_image_orientation(respect_image_orientation),
sides_to_include(sides_to_include),
is_bottom_layer(!layer.Next()),
is_border_fill(layer.Clip() == EFillBox::kBorder),
is_clipped_with_local_scrolling(is_scroll_container &&
layer.Attachment() ==
EFillAttachment::kLocal) {
// When printing backgrounds is disabled or using economy mode,
// change existing background colors and images to a solid white background.
// If there's no bg color or image, leave it untouched to avoid affecting
// transparency. We don't try to avoid loading the background images,
// because this style flag is only set when printing, and at that point
// we've already loaded the background images anyway. (To avoid loading the
// background images we'd have to do this check when applying styles rather
// than while layout.)
if (BoxPainterBase::ShouldForceWhiteBackgroundForPrintEconomy(doc, style)) {
// Note that we can't reuse this variable below because the bgColor might
// be changed.
bool should_paint_background_color = is_bottom_layer && color.Alpha();
if (image || should_paint_background_color) {
color = Color::kWhite;
image = nullptr;
}
}
// Background images are not allowed at the inline level in forced colors
// mode when forced-color-adjust is auto. This ensures that the inline images
// are not painted on top of the forced colors mode backplate.
if (doc.InForcedColorsMode() && is_inline &&
style.ForcedColorAdjust() != EForcedColorAdjust::kNone)
image = nullptr;
const bool has_rounded_border =
style.HasBorderRadius() && !sides_to_include.IsEmpty();
// BorderFillBox radius clipping is taken care of by
// BackgroundBleedClip{Only,Layer}
is_rounded_fill =
has_rounded_border && !is_painting_scrolling_background &&
!(is_border_fill && BleedAvoidanceIsClipping(bleed_avoidance));
is_printing = doc.Printing();
should_paint_image = image && image->CanRender();
should_paint_color =
is_bottom_layer && color.Alpha() &&
(!should_paint_image || !layer.ImageOccludesNextLayers(doc, style));
should_paint_color_with_paint_worklet_image =
should_paint_color &&
RuntimeEnabledFeatures::CompositeBGColorAnimationEnabled() &&
style.HasCurrentBackgroundColorAnimation();
}
namespace {
// Given the |size| that the whole image should draw at, and the input phase
// requested by the content, and the space between repeated tiles, return a
// rectangle with |size| and a location that respects the phase but is no more
// than one size + space in magnitude. In practice, this means that if there is
// no repeating the returned rect would contain the destination_offset
// location. The destination_offset passed here must exactly match the location
// of the subset in a following call to ComputeSubsetForBackground.
FloatRect ComputePhaseForBackground(const FloatPoint& destination_offset,
const FloatSize& size,
const FloatPoint& phase,
const FloatSize& spacing) {
const FloatSize step_per_tile(size + spacing);
return FloatRect(
FloatPoint(
destination_offset.X() + fmodf(-phase.X(), step_per_tile.Width()),
destination_offset.Y() + fmodf(-phase.Y(), step_per_tile.Height())),
size);
}
// Compute the image subset, in intrinsic image coordinates, that gets mapped
// onto the |subset|, when the whole image would be drawn with phase and size
// given by |phase_and_size|. Assumes |phase_and_size| contains |subset|. The
// location of the requested subset should be the painting snapped location, or
// whatever was used as a destination_offset in ComputePhaseForBackground.
//
// It is used to undo the offset added in ComputePhaseForBackground. The size
// of requested subset should be the unsnapped size so that the computed
// scale and location in the source image can be correctly determined.
FloatRect ComputeSubsetForBackground(const FloatRect& phase_and_size,
const FloatRect& subset,
const FloatSize& intrinsic_size) {
// TODO(schenney): Re-enable this after determining why it fails for
// CAP, and maybe other cases.
// DCHECK(phase_and_size.Contains(subset));
const FloatSize scale(phase_and_size.Width() / intrinsic_size.Width(),
phase_and_size.Height() / intrinsic_size.Height());
return FloatRect((subset.X() - phase_and_size.X()) / scale.Width(),
(subset.Y() - phase_and_size.Y()) / scale.Height(),
subset.Width() / scale.Width(),
subset.Height() / scale.Height());
}
FloatRect SnapSourceRectIfNearIntegral(const FloatRect src_rect) {
// Round to avoid filtering pulling in neighboring pixels, for the
// common case of sprite maps, but only if we're close to an integral size.
// "Close" in this context means we will allow floating point inaccuracy,
// when converted to layout units, to be at most one LayoutUnit::Epsilon and
// still snap.
if (std::abs(std::round(src_rect.X()) - src_rect.X()) <=
LayoutUnit::Epsilon() &&
std::abs(std::round(src_rect.Y()) - src_rect.Y()) <=
LayoutUnit::Epsilon() &&
std::abs(std::round(src_rect.MaxX()) - src_rect.MaxX()) <=
LayoutUnit::Epsilon() &&
std::abs(std::round(src_rect.MaxY()) - src_rect.MaxY()) <=
LayoutUnit::Epsilon()) {
return FloatRect(RoundedIntRect(src_rect));
}
return src_rect;
}
// The unsnapped_subset_size should be the target painting area implied by the
// content, without any snapping applied. It is necessary to correctly
// compute the subset of the source image to paint into the destination.
// The snapped_paint_rect should be the target destination for painting into.
// The phase is never snapped.
// The tile_size is the total image size. The mapping from this size
// to the unsnapped_dest_rect size defines the scaling of the image for
// sprite computation.
void DrawTiledBackground(GraphicsContext& context,
Image* image,
const FloatSize& unsnapped_subset_size,
const FloatRect& snapped_paint_rect,
const FloatPoint& phase,
const FloatSize& tile_size,
SkBlendMode op,
const FloatSize& repeat_spacing,
bool has_filter_property,
RespectImageOrientationEnum respect_orientation) {
DCHECK(!tile_size.IsEmpty());
// Use the intrinsic size of the image if it has one, otherwise force the
// generated image to be the tile size.
FloatSize intrinsic_tile_size(image->Size());
// image-resolution information is baked into the given parameters, but we
// need oriented size. That requires explicitly applying orientation here.
if (respect_orientation &&
image->CurrentFrameOrientation().UsesWidthAsHeight()) {
intrinsic_tile_size = intrinsic_tile_size.TransposedSize();
}
FloatSize scale(1, 1);
if (!image->HasIntrinsicSize() ||
// TODO(crbug.com/1042783): This is not checking for real empty image
// (for which we have checked and skipped the whole FillLayer), but for
// that a subpixel image size is rounded to empty, to avoid infinite tile
// scale that would be calculated in the |else| part.
// We should probably support subpixel size here.
intrinsic_tile_size.IsEmpty()) {
intrinsic_tile_size = tile_size;
} else {
scale = FloatSize(tile_size.Width() / intrinsic_tile_size.Width(),
tile_size.Height() / intrinsic_tile_size.Height());
}
const FloatRect one_tile_rect = ComputePhaseForBackground(
snapped_paint_rect.Location(), tile_size, phase, repeat_spacing);
// Check and see if a single draw of the image can cover the entire area we
// are supposed to tile. The dest_rect_for_subset must use the same
// location that was used in ComputePhaseForBackground and the unsnapped
// destination rect in order to correctly evaluate the subset size and
// location in the presence of border snapping and zoom.
FloatRect dest_rect_for_subset(snapped_paint_rect.Location(),
unsnapped_subset_size);
if (one_tile_rect.Contains(dest_rect_for_subset)) {
FloatRect visible_src_rect = ComputeSubsetForBackground(
one_tile_rect, dest_rect_for_subset, intrinsic_tile_size);
visible_src_rect = SnapSourceRectIfNearIntegral(visible_src_rect);
// When respecting image orientation, the drawing code expects the source
// rect to be in the unrotated image space, but we have computed it here in
// the rotated space in order to position and size the background. Undo the
// src rect rotation if necessary.
if (respect_orientation && !image->HasDefaultOrientation()) {
visible_src_rect = image->CorrectSrcRectForImageOrientation(
intrinsic_tile_size, visible_src_rect);
}
context.DrawImage(image, Image::kSyncDecode, snapped_paint_rect,
&visible_src_rect, has_filter_property, op,
respect_orientation);
return;
}
// At this point we have decided to tile the image to fill the dest rect.
// Note that this tile rect uses the image's pre-scaled size.
FloatRect tile_rect(FloatPoint(), intrinsic_tile_size);
// Farther down the pipeline we will use the scaled tile size to determine
// which dimensions to clamp or repeat in. We do not want to repeat when the
// tile size rounds to match the dest in a given dimension, to avoid having
// a single row or column repeated when the developer almost certainly
// intended the image to not repeat (this generally occurs under zoom).
//
// So detect when we do not want to repeat and set the scale to round the
// values in that dimension.
if (fabs(tile_size.Width() - snapped_paint_rect.Width()) <= 0.5) {
scale.SetWidth(snapped_paint_rect.Width() / intrinsic_tile_size.Width());
}
if (fabs(tile_size.Height() - snapped_paint_rect.Height()) <= 0.5) {
scale.SetHeight(snapped_paint_rect.Height() / intrinsic_tile_size.Height());
}
// This call takes the unscaled image, applies the given scale, and paints
// it into the snapped_dest_rect using phase from one_tile_rect and the
// given repeat spacing. Note the phase is already scaled.
context.DrawImageTiled(image, snapped_paint_rect, tile_rect, scale,
one_tile_rect.Location(), repeat_spacing, op,
respect_orientation);
}
// Returning false meaning that we need to fall back to the main thread for the
// background color animation.
bool GetBGColorPaintWorkletParams(const BoxPainterBase::FillLayerInfo& info,
const Document* document,
Node* node,
Vector<Color>* animated_colors,
Vector<double>* offsets) {
if (!info.should_paint_color_with_paint_worklet_image)
return false;
BackgroundColorPaintImageGenerator* generator =
document->GetFrame()->GetBackgroundColorPaintImageGenerator();
return generator->GetBGColorPaintWorkletParams(node, animated_colors,
offsets);
}
void FillRectWithPaintWorklet(const Document* document,
const BoxPainterBase::FillLayerInfo& info,
Node* node,
const FloatRoundedRect& dest_rect,
GraphicsContext& context,
const Vector<Color>& animated_colors,
const Vector<double>& offsets) {
FloatRect src_rect(FloatPoint(), dest_rect.Rect().Size());
BackgroundColorPaintImageGenerator* generator =
document->GetFrame()->GetBackgroundColorPaintImageGenerator();
scoped_refptr<Image> paint_worklet_image =
generator->Paint(src_rect.Size(), node, animated_colors, offsets);
context.DrawImageRRect(
paint_worklet_image.get(), Image::kSyncDecode, dest_rect, src_rect,
node && node->ComputedStyleRef().HasFilterInducingProperty(),
SkBlendMode::kSrcOver, info.respect_image_orientation);
}
inline bool PaintFastBottomLayer(const Document* document,
Node* node,
const PaintInfo& paint_info,
const BoxPainterBase::FillLayerInfo& info,
const PhysicalRect& rect,
const FloatRoundedRect& border_rect,
BackgroundImageGeometry& geometry,
Image* image,
SkBlendMode composite_op) {
// Painting a background image from an ancestor onto a cell is a complex case.
if (geometry.CellUsingContainerBackground())
return false;
// Complex cases not handled on the fast path.
if (!info.is_bottom_layer || !info.is_border_fill)
return false;
// Transparent layer, nothing to paint.
if (!info.should_paint_color && !info.should_paint_image)
return true;
// Compute the destination rect for painting the color here because we may
// need it for computing the image painting rect for optimization.
GraphicsContext& context = paint_info.context;
FloatRoundedRect color_border =
info.is_rounded_fill ? border_rect
: FloatRoundedRect(PixelSnappedIntRect(rect));
// When the layer has an image, figure out whether it is covered by a single
// tile. The border for painting images may not be the same as the color due
// to optimizations for the image painting destination that avoid painting
// under the border.
FloatRect image_tile;
FloatRoundedRect image_border;
if (info.should_paint_image) {
// Avoid image shaders when printing (poorly supported in PDF).
if (info.is_rounded_fill && info.is_printing)
return false;
// Compute the dest rect we will be using for images.
image_border =
info.is_rounded_fill
? color_border
: FloatRoundedRect(FloatRect(geometry.SnappedDestRect()));
if (!image_border.Rect().IsEmpty()) {
// We cannot optimize if the tile is too small.
if (geometry.TileSize().width < image_border.Rect().Width() ||
geometry.TileSize().height < image_border.Rect().Height())
return false;
// Phase calculation uses the actual painted location, given by the
// border-snapped destination rect.
image_tile = ComputePhaseForBackground(
FloatPoint(geometry.SnappedDestRect().offset),
FloatSize(geometry.TileSize()), geometry.Phase(),
FloatSize(geometry.SpaceSize()));
// Force the image tile to LayoutUnit precision, which is the precision
// it was calculated in. This avoids bleeding due to values very close to
// integers.
// The test images/sprite-no-bleed.html fails on two of the sub-cases
// due to this rounding still not being enough to make the Contains check
// pass. The best way to fix this would be to remove the paint rect offset
// from the tile computation, because we effectively add it in
// ComputePhaseForBackground then remove it in ComputeSubsetForBackground.
image_tile =
FloatRect(PhysicalRect::FastAndLossyFromFloatRect(image_tile));
// We cannot optimize if the tile is misaligned.
if (!image_tile.Contains(image_border.Rect()))
return false;
}
}
// At this point we're committed to the fast path: the destination (r)rect
// fits within a single tile, and we can paint it using direct draw(R)Rect()
// calls.
base::Optional<RoundedInnerRectClipper> clipper;
if (info.is_rounded_fill && !color_border.IsRenderable()) {
// When the rrect is not renderable, we resort to clipping.
// RoundedInnerRectClipper handles this case via discrete, corner-wise
// clipping.
clipper.emplace(context, rect, color_border);
color_border.SetRadii(FloatRoundedRect::Radii());
image_border.SetRadii(FloatRoundedRect::Radii());
}
// Paint the color if needed.
if (info.should_paint_color) {
Vector<Color> animated_colors;
Vector<double> offsets;
if (GetBGColorPaintWorkletParams(info, document, node, &animated_colors,
&offsets)) {
FillRectWithPaintWorklet(document, info, node, color_border, context,
animated_colors, offsets);
} else {
context.FillRoundedRect(color_border, info.color);
}
}
// Paint the image if needed.
if (!info.should_paint_image || !image || image_tile.IsEmpty())
return true;
// Generated images will be created at the desired tile size, so assume their
// intrinsic size is the requested tile size.
bool has_intrinsic_size = image->HasIntrinsicSize();
const FloatSize intrinsic_tile_size =
!has_intrinsic_size
? image_tile.Size()
: FloatSize(image->Size(info.respect_image_orientation));
// Subset computation needs the same location as was used with
// ComputePhaseForBackground above, but needs the unsnapped destination
// size to correctly calculate sprite subsets in the presence of zoom. But if
// this is a generated image sized according to the tile size (which is a
// snapped value), use the snapped dest rect instead.
FloatRect dest_rect_for_subset(
FloatPoint(geometry.SnappedDestRect().offset),
!has_intrinsic_size ? FloatSize(geometry.SnappedDestRect().size)
: FloatSize(geometry.UnsnappedDestRect().size));
// Content providers almost always choose source pixels at integer locations,
// so snap to integers. This is particuarly important for sprite maps.
// Calculation up to this point, in LayoutUnits, can lead to small variations
// from integer size, so it is safe to round without introducing major issues.
const FloatRect unrounded_subset = ComputeSubsetForBackground(
image_tile, dest_rect_for_subset, intrinsic_tile_size);
FloatRect src_rect = SnapSourceRectIfNearIntegral(unrounded_subset);
// If we have snapped the image size to 0, revert the rounding.
if (src_rect.IsEmpty())
src_rect = unrounded_subset;
// When respecting image orientation, the drawing code expects the source rect
// to be in the unrotated image space, but we have computed it here in the
// rotated space in order to position and size the background. Undo the src
// rect rotation if necessaary.
if (info.respect_image_orientation && !image->HasDefaultOrientation()) {
src_rect =
image->CorrectSrcRectForImageOrientation(intrinsic_tile_size, src_rect);
}
TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("devtools.timeline"), "PaintImage",
"data",
inspector_paint_image_event::Data(
node, *info.image, FloatRect(image->Rect()),
FloatRect(image_border.Rect())));
// Since there is no way for the developer to specify decode behavior, use
// kSync by default.
context.DrawImageRRect(
image, Image::kSyncDecode, image_border, src_rect,
node && node->ComputedStyleRef().HasFilterInducingProperty(),
composite_op, info.respect_image_orientation);
if (node && info.image && info.image->IsImageResource()) {
PaintTimingDetector::NotifyBackgroundImagePaint(
*node, *image, To<StyleFetchedImage>(*info.image),
paint_info.context.GetPaintController().CurrentPaintChunkProperties(),
RoundedIntRect(image_border.Rect()));
LocalDOMWindow* window = node->GetDocument().domWindow();
DCHECK(window);
ImageElementTiming::From(*window).NotifyBackgroundImagePainted(
*node, To<StyleFetchedImage>(*info.image),
context.GetPaintController().CurrentPaintChunkProperties(),
RoundedIntRect(image_border.Rect()));
}
return true;
}
// Inset the background rect by a "safe" amount: 1/2 border-width for opaque
// border styles, 1/6 border-width for double borders.
FloatRoundedRect BackgroundRoundedRectAdjustedForBleedAvoidance(
const ComputedStyle& style,
const PhysicalRect& border_rect,
bool object_has_multiple_boxes,
PhysicalBoxSides sides_to_include,
FloatRoundedRect background_rounded_rect) {
// TODO(fmalita): we should be able to fold these parameters into
// BoxBorderInfo or BoxDecorationData and avoid calling getBorderEdgeInfo
// redundantly here.
BorderEdge edges[4];
style.GetBorderEdgeInfo(edges, sides_to_include);
// Use the most conservative inset to avoid mixed-style corner issues.
float fractional_inset = 1.0f / 2;
for (auto& edge : edges) {
if (edge.BorderStyle() == EBorderStyle::kDouble) {
fractional_inset = 1.0f / 6;
break;
}
}
FloatRectOutsets insets(
-fractional_inset *
edges[static_cast<unsigned>(BoxSide::kTop)].UsedWidth(),
-fractional_inset *
edges[static_cast<unsigned>(BoxSide::kRight)].UsedWidth(),
-fractional_inset *
edges[static_cast<unsigned>(BoxSide::kBottom)].UsedWidth(),
-fractional_inset *
edges[static_cast<unsigned>(BoxSide::kLeft)].UsedWidth());
FloatRect inset_rect(background_rounded_rect.Rect());
inset_rect.Expand(insets);
FloatRoundedRect::Radii inset_radii(background_rounded_rect.GetRadii());
inset_radii.Shrink(-insets.Top(), -insets.Bottom(), -insets.Left(),
-insets.Right());
return FloatRoundedRect(inset_rect, inset_radii);
}
FloatRoundedRect RoundedBorderRectForClip(
const ComputedStyle& style,
const BoxPainterBase::FillLayerInfo& info,
const FillLayer& bg_layer,
const PhysicalRect& rect,
bool object_has_multiple_boxes,
const PhysicalSize& flow_box_size,
BackgroundBleedAvoidance bleed_avoidance,
LayoutRectOutsets border_padding_insets) {
if (!info.is_rounded_fill)
return FloatRoundedRect();
FloatRoundedRect border = RoundedBorderGeometry::PixelSnappedRoundedBorder(
style, rect, info.sides_to_include);
if (object_has_multiple_boxes) {
FloatRoundedRect segment_border =
RoundedBorderGeometry::PixelSnappedRoundedBorder(
style,
PhysicalRect(PhysicalOffset(),
PhysicalSize(FlooredIntSize(flow_box_size))),
info.sides_to_include);
border.SetRadii(segment_border.GetRadii());
}
if (info.is_border_fill &&
bleed_avoidance == kBackgroundBleedShrinkBackground) {
border = BackgroundRoundedRectAdjustedForBleedAvoidance(
style, rect, object_has_multiple_boxes, info.sides_to_include, border);
}
// Clip to the padding or content boxes as necessary.
// Use FastAndLossyFromFloatRect because we know it has been pixel snapped.
PhysicalRect border_rect =
PhysicalRect::FastAndLossyFromFloatRect(border.Rect());
if (bg_layer.Clip() == EFillBox::kContent) {
border = RoundedBorderGeometry::PixelSnappedRoundedInnerBorder(
style, border_rect, border_padding_insets, info.sides_to_include);
} else if (bg_layer.Clip() == EFillBox::kPadding) {
border = RoundedBorderGeometry::PixelSnappedRoundedInnerBorder(
style, border_rect, info.sides_to_include);
}
return border;
}
void PaintFillLayerBackground(const Document* document,
GraphicsContext& context,
const BoxPainterBase::FillLayerInfo& info,
Node* node,
Image* image,
SkBlendMode composite_op,
const BackgroundImageGeometry& geometry,
const PhysicalRect& scrolled_paint_rect) {
// Paint the color first underneath all images, culled if background image
// occludes it.
// TODO(trchen): In the !bgLayer.hasRepeatXY() case, we could improve the
// culling test by verifying whether the background image covers the entire
// painting area.
if (info.is_bottom_layer && info.color.Alpha() && info.should_paint_color) {
IntRect background_rect(PixelSnappedIntRect(scrolled_paint_rect));
Vector<Color> animated_colors;
Vector<double> offsets;
if (GetBGColorPaintWorkletParams(info, document, node, &animated_colors,
&offsets)) {
FillRectWithPaintWorklet(document, info, node,
FloatRoundedRect(background_rect), context,
animated_colors, offsets);
} else {
context.FillRect(background_rect, info.color);
}
}
// No progressive loading of the background image.
// NOTE: This method can be called with no image in situations when a bad
// resource locator is given such as "//:0", so still check for image.
if (info.should_paint_image && !geometry.SnappedDestRect().IsEmpty() &&
!geometry.TileSize().IsEmpty() && image) {
TRACE_EVENT1(TRACE_DISABLED_BY_DEFAULT("devtools.timeline"), "PaintImage",
"data",
inspector_paint_image_event::Data(
node, *info.image, FloatRect(image->Rect()),
FloatRect(scrolled_paint_rect)));
DrawTiledBackground(
context, image, FloatSize(geometry.UnsnappedDestRect().size),
FloatRect(geometry.SnappedDestRect()), geometry.Phase(),
FloatSize(geometry.TileSize()), composite_op,
FloatSize(geometry.SpaceSize()),
node && node->ComputedStyleRef().HasFilterInducingProperty(),
info.respect_image_orientation);
if (node && info.image && info.image->IsImageResource()) {
PaintTimingDetector::NotifyBackgroundImagePaint(
*node, *image, To<StyleFetchedImage>(*info.image),
context.GetPaintController().CurrentPaintChunkProperties(),
EnclosingIntRect(geometry.SnappedDestRect()));
LocalDOMWindow* window = node->GetDocument().domWindow();
DCHECK(window);
ImageElementTiming::From(*window).NotifyBackgroundImagePainted(
*node, To<StyleFetchedImage>(*info.image),
context.GetPaintController().CurrentPaintChunkProperties(),
EnclosingIntRect(geometry.SnappedDestRect()));
}
}
}
LayoutRectOutsets AdjustOutsetsForEdgeInclusion(
const LayoutRectOutsets outsets,
const BoxPainterBase::FillLayerInfo& info) {
LayoutRectOutsets adjusted = outsets;
if (!info.sides_to_include.top)
adjusted.SetTop(LayoutUnit());
if (!info.sides_to_include.right)
adjusted.SetRight(LayoutUnit());
if (!info.sides_to_include.bottom)
adjusted.SetBottom(LayoutUnit());
if (!info.sides_to_include.left)
adjusted.SetLeft(LayoutUnit());
return adjusted;
}
bool ShouldApplyBlendOperation(const BoxPainterBase::FillLayerInfo& info,
const FillLayer& layer) {
// For a mask layer, don't use the operator if this is the bottom layer.
return !info.is_bottom_layer || layer.GetType() != EFillLayerType::kMask;
}
} // anonymous namespace
LayoutRectOutsets BoxPainterBase::AdjustedBorderOutsets(
const FillLayerInfo& info) const {
return AdjustOutsetsForEdgeInclusion(ComputeBorders(), info);
}
void BoxPainterBase::PaintFillLayer(const PaintInfo& paint_info,
const Color& color,
const FillLayer& bg_layer,
const PhysicalRect& rect,
BackgroundBleedAvoidance bleed_avoidance,
BackgroundImageGeometry& geometry,
bool object_has_multiple_boxes,
const PhysicalSize& flow_box_size) {
GraphicsContext& context = paint_info.context;
if (rect.IsEmpty())
return;
const FillLayerInfo info =
GetFillLayerInfo(color, bg_layer, bleed_avoidance,
IsPaintingScrollingBackground(paint_info));
// If we're not actually going to paint anything, abort early.
if (!info.should_paint_image && !info.should_paint_color)
return;
GraphicsContextStateSaver clip_with_scrolling_state_saver(
context, info.is_clipped_with_local_scrolling);
auto scrolled_paint_rect =
AdjustRectForScrolledContent(paint_info, info, rect);
const auto did_adjust_paint_rect = scrolled_paint_rect != rect;
scoped_refptr<Image> image;
SkBlendMode composite_op = SkBlendMode::kSrcOver;
base::Optional<ScopedInterpolationQuality> interpolation_quality_context;
if (info.should_paint_image) {
geometry.Calculate(paint_info.PaintContainer(), paint_info.phase,
paint_info.GetGlobalPaintFlags(), bg_layer,
scrolled_paint_rect);
image = info.image->GetImage(
geometry.ImageClient(), geometry.ImageDocument(), geometry.ImageStyle(),
FloatSize(geometry.TileSize()));
interpolation_quality_context.emplace(context,
geometry.ImageInterpolationQuality());
if (ShouldApplyBlendOperation(info, bg_layer)) {
composite_op = WebCoreCompositeToSkiaComposite(bg_layer.Composite(),
bg_layer.GetBlendMode());
}
}
LayoutRectOutsets border = ComputeBorders();
LayoutRectOutsets padding = ComputePadding();
LayoutRectOutsets border_padding_insets = -(border + padding);
FloatRoundedRect border_rect = RoundedBorderRectForClip(
style_, info, bg_layer, rect, object_has_multiple_boxes, flow_box_size,
bleed_avoidance, border_padding_insets);
// Fast path for drawing simple color backgrounds. Do not use the fast
// path with images if the dest rect has been adjusted for scrolling
// backgrounds because correcting the dest rect for scrolling reduces the
// accuracy of the destination rects. Also disable the fast path for images
// if we are shrinking the background for bleed avoidance, because this
// adjusts the border rects in a way that breaks the optimization.
bool disable_fast_path =
info.should_paint_image &&
(bleed_avoidance == kBackgroundBleedShrinkBackground ||
did_adjust_paint_rect);
if (!disable_fast_path &&
PaintFastBottomLayer(document_, node_, paint_info, info, rect,
border_rect, geometry, image.get(), composite_op)) {
return;
}
base::Optional<RoundedInnerRectClipper> clip_to_border;
if (info.is_rounded_fill)
clip_to_border.emplace(context, rect, border_rect);
if (bg_layer.Clip() == EFillBox::kText) {
PaintFillLayerTextFillBox(context, info, image.get(), composite_op,
geometry, rect, scrolled_paint_rect,
object_has_multiple_boxes);
return;
}
GraphicsContextStateSaver background_clip_state_saver(context, false);
switch (bg_layer.Clip()) {
case EFillBox::kPadding:
case EFillBox::kContent: {
if (info.is_rounded_fill)
break;
// Clip to the padding or content boxes as necessary.
PhysicalRect clip_rect = scrolled_paint_rect;
clip_rect.Contract(AdjustOutsetsForEdgeInclusion(border, info));
if (bg_layer.Clip() == EFillBox::kContent)
clip_rect.Contract(AdjustOutsetsForEdgeInclusion(padding, info));
background_clip_state_saver.Save();
// TODO(chrishtr): this should be pixel-snapped.
context.Clip(FloatRect(clip_rect));
break;
}
case EFillBox::kBorder:
break;
case EFillBox::kText: // fall through
default:
NOTREACHED();
break;
}
PaintFillLayerBackground(document_, context, info, node_, image.get(),
composite_op, geometry, scrolled_paint_rect);
}
void BoxPainterBase::PaintFillLayerTextFillBox(
GraphicsContext& context,
const BoxPainterBase::FillLayerInfo& info,
Image* image,
SkBlendMode composite_op,
const BackgroundImageGeometry& geometry,
const PhysicalRect& rect,
const PhysicalRect& scrolled_paint_rect,
bool object_has_multiple_boxes) {
// First figure out how big the mask has to be. It should be no bigger
// than what we need to actually render, so we should intersect the dirty
// rect with the border box of the background.
IntRect mask_rect = PixelSnappedIntRect(rect);
// We draw the background into a separate layer, to be later masked with
// yet another layer holding the text content.
GraphicsContextStateSaver background_clip_state_saver(context, false);
background_clip_state_saver.Save();
context.Clip(mask_rect);
context.BeginLayer(1, composite_op);
PaintFillLayerBackground(document_, context, info, node_, image,
SkBlendMode::kSrcOver, geometry,
scrolled_paint_rect);
// Create the text mask layer and draw the text into the mask. We do this by
// painting using a special paint phase that signals to InlineTextBoxes that
// they should just add their contents to the clip.
context.BeginLayer(1, SkBlendMode::kDstIn);
PaintTextClipMask(context, mask_rect, scrolled_paint_rect.offset,
object_has_multiple_boxes);
context.EndLayer(); // Text mask layer.
context.EndLayer(); // Background layer.
}
void BoxPainterBase::PaintBorder(const ImageResourceObserver& obj,
const Document& document,
Node* node,
const PaintInfo& info,
const PhysicalRect& rect,
const ComputedStyle& style,
BackgroundBleedAvoidance bleed_avoidance,
PhysicalBoxSides sides_to_include) {
// border-image is not affected by border-radius.
if (NinePieceImagePainter::Paint(info.context, obj, document, node, rect,
style, style.BorderImage())) {
return;
}
const BoxBorderPainter border_painter(rect, style, bleed_avoidance,
sides_to_include);
border_painter.PaintBorder(info, rect);
}
void BoxPainterBase::PaintMaskImages(const PaintInfo& paint_info,
const PhysicalRect& paint_rect,
const ImageResourceObserver& obj,
BackgroundImageGeometry& geometry,
PhysicalBoxSides sides_to_include) {
if (!style_.HasMask() || style_.Visibility() != EVisibility::kVisible)
return;
PaintFillLayers(paint_info, Color::kTransparent, style_.MaskLayers(),
paint_rect, geometry);
NinePieceImagePainter::Paint(paint_info.context, obj, *document_, node_,
paint_rect, style_, style_.MaskBoxImage(),
sides_to_include);
}
bool BoxPainterBase::ShouldSkipPaintUnderInvalidationChecking(
const LayoutBox& box) {
DCHECK(RuntimeEnabledFeatures::PaintUnderInvalidationCheckingEnabled());
// Disable paint under-invalidation checking for cases that under-invalidation
// is intensional and/or harmless.
// A box having delayed-invalidation may change before it's actually
// invalidated. Note that we still report harmless under-invalidation of
// non-delayed-invalidation animated background, which should be ignored.
if (box.ShouldDelayFullPaintInvalidation())
return true;
// We always paint a MediaSliderPart using the latest data (buffered ranges,
// current time and duration) which may be different from the cached data.
if (box.StyleRef().EffectiveAppearance() == kMediaSliderPart)
return true;
// We paint an indeterminate progress based on the position calculated from
// the animation progress. Harmless under-invalidatoin may happen during a
// paint that is not scheduled for animation.
if (box.IsProgress() && !To<LayoutProgress>(box).IsDeterminate())
return true;
return false;
}
} // namespace blink