blob: 448dba97eb7aa349b32c44cd0d78171092a28fcc [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_PAINT_OBJECT_PAINT_PROPERTIES_H_
#define THIRD_PARTY_BLINK_RENDERER_CORE_PAINT_OBJECT_PAINT_PROPERTIES_H_
#include <array>
#include <memory>
#include <utility>
#include <variant>
#include "base/dcheck_is_on.h"
#include "base/memory/ptr_util.h"
#include "base/memory/scoped_refptr.h"
#include "third_party/blink/renderer/core/core_export.h"
#include "third_party/blink/renderer/core/paint/sparse_vector.h"
#include "third_party/blink/renderer/platform/graphics/paint/clip_paint_property_node.h"
#include "third_party/blink/renderer/platform/graphics/paint/effect_paint_property_node.h"
#include "third_party/blink/renderer/platform/graphics/paint/scroll_paint_property_node.h"
#include "third_party/blink/renderer/platform/graphics/paint/transform_paint_property_node.h"
#include "third_party/blink/renderer/platform/wtf/allocator/allocator.h"
namespace blink {
// This class is for storing the paint property nodes created by a
// LayoutObject. The object owns each of the property nodes directly and RefPtrs
// are only used to harden against use-after-free bugs. These paint properties
// are built/updated by PaintPropertyTreeBuilder during the PrePaint lifecycle
// step.
//
// [update & clear implementation note] This class has Update[property](...) and
// Clear[property]() helper functions for efficiently creating and updating
// properties. The update functions returns a 3-state result to indicate whether
// the value or the existence of the node has changed. They use a create-or-
// update pattern of re-using existing properties for efficiency:
// 1. It avoids extra allocations.
// 2. It preserves existing child->parent pointers.
// The clear functions return true if an existing node is removed. Property
// nodes store parent pointers but not child pointers and these return values
// are important for catching property tree structure changes which require
// updating descendant's parent pointers.
class CORE_EXPORT ObjectPaintProperties {
USING_FAST_MALLOC(ObjectPaintProperties);
public:
#if DCHECK_IS_ON()
~ObjectPaintProperties() { DCHECK(!is_immutable_); }
#endif
static std::unique_ptr<ObjectPaintProperties> Create() {
return std::unique_ptr<ObjectPaintProperties>(new ObjectPaintProperties);
}
private:
// Use the public Create() method for instantiation.
ObjectPaintProperties() = default;
public:
// Preprocessor macro declarations.
//
// The following defines 3 functions and one variable:
// - Foo(): a getter for the property.
// - UpdateFoo(): an update function.
// - ClearFoo(): a clear function
// - foo_: the variable itself.
//
// Note that clear* functions return true if the property tree structure
// changes (an existing node was deleted), and false otherwise. See the
// class-level comment ("update & clear implementation note") for details
// about why this is needed for efficient updates.
#define ADD_NODE(type, function, field_id) \
public: \
static_assert(field_id >= NodeId::kFirst##type); \
static_assert(field_id <= NodeId::kLast##type); \
\
const type##PaintPropertyNode* function() const { \
return GetNode<type##PaintPropertyNode>(field_id); \
} \
\
PaintPropertyChangeType Update##function( \
const type##PaintPropertyNodeOrAlias& parent, \
type##PaintPropertyNode::State&& state, \
const type##PaintPropertyNode::AnimationState& animation_state = \
type##PaintPropertyNode::AnimationState()) { \
return Update<type##PaintPropertyNode, type##PaintPropertyNodeOrAlias>( \
field_id, parent, std::move(state), animation_state); \
} \
\
bool Clear##function() { return nodes_.ClearField(field_id); } \
// (End of ADD_NODE definition)
#define ADD_ALIAS_NODE(type, function, field_id) \
public: \
static_assert(field_id >= NodeId::kFirst##type); \
static_assert(field_id <= NodeId::kLast##type); \
\
const type##PaintPropertyNodeOrAlias* function() const { \
return GetNode<type##PaintPropertyNodeAlias>(field_id); \
} \
\
PaintPropertyChangeType Update##function( \
const type##PaintPropertyNodeOrAlias& parent) { \
return UpdateAlias<type##PaintPropertyNodeAlias>(field_id, parent); \
} \
\
bool Clear##function() { return nodes_.ClearField(field_id); } \
// (End of ADD_ALIAS_NODE definition)
#define ADD_TRANSFORM(function, field_id) \
ADD_NODE(Transform, function, field_id)
#define ADD_EFFECT(function, field_id) ADD_NODE(Effect, function, field_id)
#define ADD_CLIP(function, field_id) ADD_NODE(Clip, function, field_id)
// Identifier used for indexing into the sparse vector of nodes. NOTE: when
// adding a new node to this list, make sure to do the following. Update
// the kLast<NodeType> value to reflect the value you added, and renumber all
// higher value enums. The HasNodeTypeInRange() method assumes that all nodes
// of NodeType are bounded between kFirst<NodeType>() and kLast<NodeType>, and
// there are no other types of nodes in that range.
enum class NodeId : unsigned {
// Transforms
kPaintOffsetTranslation = 0,
kStickyTranslation = 1,
kAnchorPositionScrollTranslation = 2,
kTranslate = 3,
kRotate = 4,
kScale = 5,
kOffset = 6,
kTransform = 7,
kPerspective = 8,
kReplacedContentTransform = 9,
kScrollTranslation = 10,
kTransformAlias = 11,
kFirstTransform = kPaintOffsetTranslation,
kLastTransform = kTransformAlias,
kScroll = 12,
kFirstScroll = kScroll,
kLastScroll = kScroll,
// Effects
kElementCaptureEffect = 13,
kEffect = 14,
kFilter = 15,
kMask = 16,
kClipPathMask = 17,
kVerticalScrollbarEffect = 18,
kHorizontalScrollbarEffect = 19,
kScrollCorner = 20,
kEffectAlias = 21,
kFirstEffect = kElementCaptureEffect,
kLastEffect = kEffectAlias,
// Clips
kClipPathClip = 22,
kMaskClip = 23,
kCssClip = 24,
kOverflowControlsClip = 25,
kBackgroundClip = 26,
kPixelMovingFilterClipExpander = 27,
kInnerBorderRadiusClip = 28,
kOverflowClip = 29,
kCssClipFixedPosition = 30,
kClipAlias = 31,
kFirstClip = kClipPathClip,
kLastClip = kClipAlias,
// Should be updated whenever a higher value NodeType is added.
kNumFields = kLastClip + 1
};
// Transform node method declarations.
//
// The hierarchy of the transform subtree created by a LayoutObject is as
// follows:
// [ PaintOffsetTranslation ]
// | Normally paint offset is accumulated without creating a node until
// | we see, for example, transform or position:fixed.
// |
// +-[ StickyTranslation ]
// / This applies the sticky offset induced by position:sticky.
// |
// +-[ AnchorPositionScrollTranslation ]
// / This applies the scrolling offset induced by CSS anchor positioning.
// |
// +-[ Translate ]
// | The transform from CSS 'translate' (including the effects of
// / 'transform-origin').
// |
// +-[ Rotate ]
// | The transform from CSS 'rotate' (including the effects of
// / 'transform-origin').
// |
// +-[ Scale ]
// | The transform from CSS 'scale' (including the effects of
// / 'transform-origin').
// |
// +-[ Offset ]
// | The transform from the longhand properties that comprise the CSS
// / 'offset' shorthand (including the effects of 'transform-origin').
// |
// +-[ Transform ]
// | The transform from CSS 'transform' (including the effects of
// | 'transform-origin').
// |
// | For SVG, this also includes 'translate', 'rotate', 'scale',
// | 'offset-*' (instead of the nodes above) and the effects of
// | some characteristics of the SVG viewport and the "SVG
// | additional translation" (for the x and y attributes on
// | svg:use).
// |
// | This is the local border box space (see
// | FragmentData::LocalBorderBoxProperties); the nodes below influence
// | the transform for the children but not the LayoutObject itself.
// |
// +-[ Perspective ]
// | The space created by CSS perspective.
// +-[ ReplacedContentTransform ]
// Additional transform for replaced elements to implement object-fit.
// (Replaced elements don't scroll.)
// OR
// +-[ ScrollTranslation ]
// The space created by overflow clip. The translation equals the
// offset between the scrolling contents and the scrollable area of
// the container, both originated from the top-left corner, so it is
// the scroll position (instead of scroll offset) of the
// ScrollableArea.
//
// ... +-[ TransformIsolationNode ]
// This serves as a parent to subtree transforms on an element with
// paint containment. It induces a PaintOffsetTranslation node and
// is the deepest child of any transform tree on the contain: paint
// element.
//
// This hierarchy is related to the order of transform operations in
// https://drafts.csswg.org/css-transforms-2/#accumulated-3d-transformation-matrix-computation
bool HasTransformNode() const {
return HasNodeTypeInRange(NodeId::kFirstTransform, NodeId::kLastTransform);
}
bool HasCSSTransformPropertyNode() const {
return Translate() || Rotate() || Scale() || Offset() || Transform();
}
std::array<const TransformPaintPropertyNode*, 5>
AllCSSTransformPropertiesOutsideToInside() const {
return {Translate(), Rotate(), Scale(), Offset(), Transform()};
}
ADD_TRANSFORM(PaintOffsetTranslation, NodeId::kPaintOffsetTranslation)
ADD_TRANSFORM(StickyTranslation, NodeId::kStickyTranslation)
ADD_TRANSFORM(AnchorPositionScrollTranslation,
NodeId::kAnchorPositionScrollTranslation)
ADD_TRANSFORM(Translate, NodeId::kTranslate)
ADD_TRANSFORM(Rotate, NodeId::kRotate)
ADD_TRANSFORM(Scale, NodeId::kScale)
ADD_TRANSFORM(Offset, NodeId::kOffset)
ADD_TRANSFORM(Transform, NodeId::kTransform)
ADD_TRANSFORM(Perspective, NodeId::kPerspective)
ADD_TRANSFORM(ReplacedContentTransform, NodeId::kReplacedContentTransform)
ADD_TRANSFORM(ScrollTranslation, NodeId::kScrollTranslation)
using ScrollPaintPropertyNodeOrAlias = ScrollPaintPropertyNode;
ADD_ALIAS_NODE(Transform, TransformIsolationNode, NodeId::kTransformAlias)
ADD_NODE(Scroll, Scroll, NodeId::kScroll)
// Effect node method declarations.
//
// The hierarchy of the effect subtree created by a LayoutObject is as
// follows:
// [ ElementCaptureEffect ]
// | Isolated group to force an element to be painted separately.
// +-[ Effect ]
// | Isolated group to apply various CSS effects, including opacity,
// / mix-blend-mode, backdrop-filter, and for isolation if a mask needs
// / to be applied or backdrop-dependent children are present.
// +-[ Mask ]
// | | Isolated group for painting the CSS mask or the mask-based CSS
// | | clip-path. This node will have SkBlendMode::kDstIn and shall paint
// | | last, i.e. after masked contents.
// | +-[ ClipPathMask ]
// | Isolated group for painting the mask-based CSS clip-path. This node
// | will have SkBlendMode::kDstIn and shall paint last, i.e. after
// | clipped contents. If there is no Mask node, then this node is a
// | direct child of the Effect node.
// +-[ Filter ]
// | Isolated group for CSS filter. This is separate from Effect in case
// | there are masks which should be applied to the output of the filter
// | instead of the input.
// +-[ VerticalScrollbarEffect / HorizontalScrollbarEffect / ScrollCorner ]
// | Overlay Scrollbars on Aura and Android need effect node for fade
// | animation. Also used in ViewTransitions to separate out scrollbars
// | from the root snapshot.
//
// ... +-[ EffectIsolationNode ]
// This serves as a parent to subtree effects on an element with paint
// containment, It is the deepest child of any effect tree on the
// contain: paint element.
bool HasEffectNode() const {
return HasNodeTypeInRange(NodeId::kFirstEffect, NodeId::kLastEffect);
}
ADD_EFFECT(ElementCaptureEffect, NodeId::kElementCaptureEffect)
ADD_EFFECT(Effect, NodeId::kEffect)
ADD_EFFECT(Filter, NodeId::kFilter)
ADD_EFFECT(Mask, NodeId::kMask)
ADD_EFFECT(ClipPathMask, NodeId::kClipPathMask)
ADD_EFFECT(VerticalScrollbarEffect, NodeId::kVerticalScrollbarEffect)
ADD_EFFECT(HorizontalScrollbarEffect, NodeId::kHorizontalScrollbarEffect)
ADD_EFFECT(ScrollCornerEffect, NodeId::kScrollCorner)
ADD_ALIAS_NODE(Effect, EffectIsolationNode, NodeId::kEffectAlias)
// Clip node declarations.
//
// The hierarchy of the clip subtree created by a LayoutObject is as follows:
// [ ViewTransitionClip ]
// | Clip created only when there is an active ViewTransition. This is used
// | to clip the element's painting to a subset close to the viewport.
// | See https://drafts.csswg.org/css-view-transitions-1/
// | #compute-the-interest-rectangle-algorithm for details.
// +-[ ClipPathClip ]
// | Clip created by path-based CSS clip-path. Only exists if the
// / clip-path is "simple" that can be applied geometrically. This and
// / the ClipPathMask effect node are mutually exclusive.
// +-[ MaskClip ]
// | Clip created by CSS mask or mask-based CSS clip-path.
// | It serves two purposes:
// | 1. Cull painting of the masked subtree. Because anything outside of
// | the mask is never visible, it is pointless to paint them.
// | 2. Raster clip of the masked subtree. Because the mask implemented
// | as SkBlendMode::kDstIn, pixels outside of mask's bound will be
// | intact when they shall be masked out. This clip ensures no pixels
// | leak out.
// +-[ CssClip ]
// | Clip created by CSS clip. CSS clip applies to all descendants, this
// | node only applies to containing block descendants. For descendants
// | not contained by this object, use [ css clip fixed position ].
// +-[ OverflowControlsClip ]
// | Clip created by overflow clip to clip overflow controls
// | (scrollbars, resizer, scroll corner) that would overflow the box.
// +-[ BackgroundClip ]
// | Clip created for CompositeBackgroundAttachmentFixed background
// | according to CSS background-clip.
// +-[ PixelMovingFilterClipExpander ]
// | Clip created by pixel-moving filter. Instead of intersecting with
// | the current clip, this clip expands the current clip to include all
// / pixels in the filtered content that may affect the pixels in the
// / current clip.
// +-[ InnerBorderRadiusClip ]
// | Clip created by a rounded border with overflow clip. This clip is
// | not inset by scrollbars.
// +-[ OverflowClip ]
// Clip created by overflow clip and is inset by the scrollbar.
// [ CssClipFixedPosition ]
// Clip created by CSS clip. Only exists if the current clip includes
// some clip that doesn't apply to our fixed position descendants.
//
// ... +-[ ClipIsolationNode ]
// This serves as a parent to subtree clips on an element with paint
// containment. It is the deepest child of any clip tree on the contain:
// paint element.
bool HasClipNode() const {
return HasNodeTypeInRange(NodeId::kFirstClip, NodeId::kLastClip);
}
ADD_CLIP(ClipPathClip, NodeId::kClipPathClip)
ADD_CLIP(MaskClip, NodeId::kMaskClip)
ADD_CLIP(CssClip, NodeId::kCssClip)
ADD_CLIP(OverflowControlsClip, NodeId::kOverflowControlsClip)
ADD_CLIP(BackgroundClip, NodeId::kBackgroundClip)
ADD_CLIP(PixelMovingFilterClipExpander,
NodeId::kPixelMovingFilterClipExpander)
ADD_CLIP(InnerBorderRadiusClip, NodeId::kInnerBorderRadiusClip)
ADD_CLIP(OverflowClip, NodeId::kOverflowClip)
ADD_CLIP(CssClipFixedPosition, NodeId::kCssClipFixedPosition)
ADD_ALIAS_NODE(Clip, ClipIsolationNode, NodeId::kClipAlias)
#undef ADD_CLIP
#undef ADD_EFFECT
#undef ADD_TRANSFORM
#undef ADD_NODE
#undef ADD_ALIAS_NODE
// Debug-only state change validation method implementations.
//
// Used by find_properties_needing_update.h for verifying state doesn't
// change.
#if DCHECK_IS_ON()
void SetImmutable() const { is_immutable_ = true; }
bool IsImmutable() const { return is_immutable_; }
void SetMutable() const { is_immutable_ = false; }
void Validate() {
DCHECK(!ScrollTranslation() || !ReplacedContentTransform())
<< "Replaced elements don't scroll so there should never be both a "
"scroll translation and a replaced content transform.";
DCHECK(!ClipPathClip() || !ClipPathMask())
<< "ClipPathClip and ClipPathshould be mutually exclusive.";
DCHECK((!TransformIsolationNode() && !ClipIsolationNode() &&
!EffectIsolationNode()) ||
(TransformIsolationNode() && ClipIsolationNode() &&
EffectIsolationNode()))
<< "Isolation nodes have to be created for all of transform, clip, and "
"effect trees.";
}
void AddTransformNodesToPrinter(PropertyTreePrinter& printer) const {
AddNodesToPrinter(NodeId::kFirstTransform, NodeId::kLastTransform, printer);
}
void AddClipNodesToPrinter(PropertyTreePrinter& printer) const {
AddNodesToPrinter(NodeId::kFirstClip, NodeId::kLastClip, printer);
}
void AddEffectNodesToPrinter(PropertyTreePrinter& printer) const {
AddNodesToPrinter(NodeId::kFirstEffect, NodeId::kLastEffect, printer);
}
void AddScrollNodesToPrinter(PropertyTreePrinter& printer) const {
AddNodesToPrinter(NodeId::kFirstScroll, NodeId::kLastScroll, printer);
}
#endif
// Direct update method implementations.
PaintPropertyChangeType DirectlyUpdateTransformAndOrigin(
TransformPaintPropertyNode::TransformAndOrigin&& transform_and_origin,
const TransformPaintPropertyNode::AnimationState& animation_state) {
CHECK(nodes_.HasField(NodeId::kTransform));
return GetNode<TransformPaintPropertyNode>(NodeId::kTransform)
->DirectlyUpdateTransformAndOrigin(std::move(transform_and_origin),
animation_state);
}
PaintPropertyChangeType DirectlyUpdateOpacity(
float opacity,
const EffectPaintPropertyNode::AnimationState& animation_state) {
const bool has_effect = nodes_.HasField(NodeId::kEffect);
// TODO(yotha): Remove this check once we make sure crbug.com/1370268
// is fixed.
DCHECK(has_effect);
if (!has_effect) {
return PaintPropertyChangeType::kNodeAddedOrRemoved;
}
return GetNode<EffectPaintPropertyNode>(NodeId::kEffect)
->DirectlyUpdateOpacity(opacity, animation_state);
}
private:
using NodeList = SparseVector<NodeId, scoped_refptr<PaintPropertyNode>>;
template <typename NodeType, typename ParentType>
PaintPropertyChangeType Update(
NodeId node_id,
const ParentType& parent,
NodeType::State&& state,
const NodeType::AnimationState& animation_state =
NodeType::AnimationState()) {
// First, check if we need to add a new node.
if (!nodes_.HasField(node_id)) {
nodes_.SetField(node_id, NodeType::Create(parent, std::move(state)));
#if DCHECK_IS_ON()
DCHECK(!is_immutable_) << "Sparse node added while immutable.";
#endif
return PaintPropertyChangeType::kNodeAddedOrRemoved;
}
// If not, we just need to update the existing node.
auto* node = GetNode<NodeType>(node_id);
const PaintPropertyChangeType changed =
node->Update(parent, std::move(state), animation_state);
#if DCHECK_IS_ON()
DCHECK(!is_immutable_ || changed == PaintPropertyChangeType::kUnchanged)
<< "Value changed while immutable.";
#endif
return changed;
}
template <typename AliasType, typename ParentType>
PaintPropertyChangeType UpdateAlias(NodeId node_id,
const ParentType& parent) {
// First, check if we need to add a new alias.
if (!nodes_.HasField(node_id)) {
nodes_.SetField(node_id, AliasType::Create(parent));
#if DCHECK_IS_ON()
DCHECK(!is_immutable_) << "Sparse node added while immutable.";
#endif
return PaintPropertyChangeType::kNodeAddedOrRemoved;
}
// If not, we just need to update the existing alias.
auto* node = GetNode<AliasType>(node_id);
DCHECK(node->IsParentAlias());
const PaintPropertyChangeType changed = node->SetParent(parent);
#if DCHECK_IS_ON()
DCHECK(!is_immutable_ || changed == PaintPropertyChangeType::kUnchanged)
<< "Parent changed while immutable. New state:\n"
<< *node;
#endif
return changed;
}
template <typename NodeType>
const NodeType* GetNode(NodeId node_id) const {
if (nodes_.HasField(node_id)) {
return static_cast<const NodeType*>(nodes_.GetField(node_id).get());
}
return nullptr;
}
template <typename NodeType>
NodeType* GetNode(NodeId node_id) {
return const_cast<NodeType*>(
static_cast<const ObjectPaintProperties&>(*this).GetNode<NodeType>(
node_id));
}
bool HasNodeTypeInRange(NodeId first_id, NodeId last_id) const {
for (NodeId i = first_id; i < last_id;
i = static_cast<NodeId>(static_cast<int>(i) + 1)) {
if (nodes_.HasField(i)) {
return true;
}
}
return false;
}
#if DCHECK_IS_ON()
void AddNodesToPrinter(NodeId first_id,
NodeId last_id,
PropertyTreePrinter& printer) const {
for (NodeId i = first_id; i <= last_id;
i = static_cast<NodeId>(static_cast<int>(i) + 1)) {
if (nodes_.HasField(i)) {
printer.AddNode(nodes_.GetField(i).get());
}
}
}
#endif
NodeList nodes_;
#if DCHECK_IS_ON()
mutable bool is_immutable_ = false;
#endif
};
} // namespace blink
#endif // THIRD_PARTY_BLINK_RENDERER_CORE_PAINT_OBJECT_PAINT_PROPERTIES_H_