blob: 1106cb73f0ffe789c48d2048006771effa0f01ae [file] [log] [blame]
// Copyright 2019 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/display_lock/display_lock_utilities.h"
#include "third_party/blink/renderer/core/display_lock/display_lock_context.h"
#include "third_party/blink/renderer/core/display_lock/display_lock_document_state.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/dom/flat_tree_traversal.h"
#include "third_party/blink/renderer/core/dom/node.h"
#include "third_party/blink/renderer/core/dom/node_computed_style.h"
#include "third_party/blink/renderer/core/dom/text.h"
#include "third_party/blink/renderer/core/editing/editing_boundary.h"
#include "third_party/blink/renderer/core/editing/editing_utilities.h"
#include "third_party/blink/renderer/core/inspector/inspector_trace_events.h"
#include "third_party/blink/renderer/core/layout/layout_embedded_content.h"
#include "third_party/blink/renderer/core/layout/layout_view.h"
#include <set>
namespace blink {
namespace {
// Returns the frame owner node for the frame that contains the given child, if
// one exists. Returns nullptr otherwise.
Node* GetFrameOwnerNode(const Node* child) {
if (!child || !child->GetDocument().GetFrame() ||
!child->GetDocument().GetFrame()->OwnerLayoutObject()) {
return nullptr;
}
return child->GetDocument().GetFrame()->OwnerLayoutObject()->GetNode();
}
void PopulateAncestorContexts(Node* node,
std::set<DisplayLockContext*>* contexts) {
DCHECK(node);
for (Node& ancestor : FlatTreeTraversal::InclusiveAncestorsOf(*node)) {
auto* ancestor_element = DynamicTo<Element>(ancestor);
if (!ancestor_element)
continue;
if (auto* context = ancestor_element->GetDisplayLockContext())
contexts->insert(context);
}
}
template <typename Lambda>
Element* LockedAncestorPreventingUpdate(const Node& node,
Lambda update_is_prevented) {
for (auto* ancestor =
DisplayLockUtilities::NearestLockedExclusiveAncestor(node);
ancestor;
ancestor =
DisplayLockUtilities::NearestLockedExclusiveAncestor(*ancestor)) {
DCHECK(ancestor->GetDisplayLockContext());
if (update_is_prevented(ancestor->GetDisplayLockContext()))
return ancestor;
}
return nullptr;
}
template <typename Lambda>
Element* LockedAncestorPreventingUpdate(const LayoutObject& object,
Lambda update_is_prevented) {
if (auto* ancestor =
DisplayLockUtilities::NearestLockedExclusiveAncestor(object)) {
if (update_is_prevented(ancestor->GetDisplayLockContext()))
return ancestor;
return LockedAncestorPreventingUpdate(*ancestor, update_is_prevented);
}
return nullptr;
}
} // namespace
bool DisplayLockUtilities::ActivateFindInPageMatchRangeIfNeeded(
const EphemeralRangeInFlatTree& range) {
if (!RuntimeEnabledFeatures::CSSContentVisibilityEnabled())
return false;
DCHECK(!range.IsNull());
DCHECK(!range.IsCollapsed());
if (range.GetDocument()
.GetDisplayLockDocumentState()
.LockedDisplayLockCount() ==
range.GetDocument()
.GetDisplayLockDocumentState()
.DisplayLockBlockingAllActivationCount())
return false;
// Find-in-page matches can't span multiple block-level elements (because the
// text will be broken by newlines between blocks), so first we find the
// block-level element which contains the match.
// This means we only need to traverse up from one node in the range, in this
// case we are traversing from the start position of the range.
Element* enclosing_block =
EnclosingBlock(range.StartPosition(), kCannotCrossEditingBoundary);
// Note that we don't check the `range.EndPosition()` since we just activate
// the beginning of the range. In find-in-page cases, the end position is the
// same since the matches cannot cross block boundaries. However, in
// scroll-to-text, the range might be different, but we still just activate
// the beginning of the range. See
// https://github.com/WICG/display-locking/issues/125 for more details.
DCHECK(enclosing_block);
return enclosing_block->ActivateDisplayLockIfNeeded(
DisplayLockActivationReason::kFindInPage);
}
bool DisplayLockUtilities::ActivateSelectionRangeIfNeeded(
const EphemeralRangeInFlatTree& range) {
if (range.IsNull() || range.IsCollapsed())
return false;
if (!RuntimeEnabledFeatures::CSSContentVisibilityEnabled() ||
range.GetDocument()
.GetDisplayLockDocumentState()
.LockedDisplayLockCount() ==
range.GetDocument()
.GetDisplayLockDocumentState()
.DisplayLockBlockingAllActivationCount())
return false;
UpdateStyleAndLayoutForRangeIfNeeded(range,
DisplayLockActivationReason::kSelection);
HeapHashSet<Member<Element>> elements_to_activate;
for (Node& node : range.Nodes()) {
DCHECK(!node.GetDocument().NeedsLayoutTreeUpdateForNode(node));
const ComputedStyle* style = node.GetComputedStyle();
if (!style || style->UserSelect() == EUserSelect::kNone)
continue;
if (auto* nearest_locked_ancestor = NearestLockedExclusiveAncestor(node))
elements_to_activate.insert(nearest_locked_ancestor);
}
for (Element* element : elements_to_activate) {
element->ActivateDisplayLockIfNeeded(
DisplayLockActivationReason::kSelection);
}
return !elements_to_activate.IsEmpty();
}
const HeapVector<Member<Element>>
DisplayLockUtilities::ActivatableLockedInclusiveAncestors(
const Node& node,
DisplayLockActivationReason reason) {
HeapVector<Member<Element>> elements_to_activate;
if (!RuntimeEnabledFeatures::CSSContentVisibilityEnabled() ||
node.GetDocument()
.GetDisplayLockDocumentState()
.LockedDisplayLockCount() ==
node.GetDocument()
.GetDisplayLockDocumentState()
.DisplayLockBlockingAllActivationCount())
return elements_to_activate;
for (Node& ancestor : FlatTreeTraversal::InclusiveAncestorsOf(node)) {
auto* ancestor_element = DynamicTo<Element>(ancestor);
if (!ancestor_element)
continue;
if (auto* context = ancestor_element->GetDisplayLockContext()) {
if (!context->IsLocked())
continue;
if (!context->IsActivatable(reason)) {
// If we find a non-activatable locked ancestor, then we shouldn't
// activate anything.
elements_to_activate.clear();
return elements_to_activate;
}
elements_to_activate.push_back(ancestor_element);
}
}
return elements_to_activate;
}
DisplayLockUtilities::ScopedForcedUpdate::Impl::Impl(const Node* node,
bool include_self)
: node_(node) {
if (!RuntimeEnabledFeatures::CSSContentVisibilityEnabled())
return;
if (!node_)
return;
auto* owner_node = GetFrameOwnerNode(node);
if (owner_node)
parent_frame_impl_ = MakeGarbageCollected<Impl>(owner_node, true);
node->GetDocument().GetDisplayLockDocumentState().BeginNodeForcedScope(
node, include_self, this);
if (node->GetDocument()
.GetDisplayLockDocumentState()
.LockedDisplayLockCount() == 0)
return;
// Get the right ancestor view. Only use inclusive ancestors if the node
// itself is locked and it prevents self layout, or if |include_self| is true.
// If self layout is not prevented, we don't need to force the subtree layout,
// so use exclusive ancestors in that case.
auto ancestor_view = [node, include_self] {
if (auto* element = DynamicTo<Element>(node)) {
auto* context = element->GetDisplayLockContext();
if (context && include_self)
return FlatTreeTraversal::InclusiveAncestorsOf(*node);
}
return FlatTreeTraversal::AncestorsOf(*node);
}();
// TODO(vmpstr): This is somewhat inefficient, since we would pay the cost
// of traversing the ancestor chain even for nodes that are not in the
// locked subtree. We need to figure out if there is a supplementary
// structure that we can use to quickly identify nodes that are in the
// locked subtree.
for (Node& ancestor : ancestor_view) {
auto* ancestor_node = DynamicTo<Element>(ancestor);
if (!ancestor_node)
continue;
if (auto* context = ancestor_node->GetDisplayLockContext()) {
context->NotifyForcedUpdateScopeStarted();
forced_context_set_.insert(context);
}
}
}
void DisplayLockUtilities::ScopedForcedUpdate::Impl::Destroy() {
if (!node_)
return;
if (RuntimeEnabledFeatures::CSSContentVisibilityEnabled())
node_->GetDocument().GetDisplayLockDocumentState().EndNodeForcedScope(this);
if (parent_frame_impl_)
parent_frame_impl_->Destroy();
for (auto context : forced_context_set_) {
context->NotifyForcedUpdateScopeEnded();
}
}
void DisplayLockUtilities::ScopedForcedUpdate::Impl::
AddForcedUpdateScopeForContext(DisplayLockContext* context) {
auto result = forced_context_set_.insert(context);
if (result.is_new_entry)
context->NotifyForcedUpdateScopeStarted();
}
const Element* DisplayLockUtilities::NearestLockedInclusiveAncestor(
const Node& node) {
auto* element = DynamicTo<Element>(node);
if (!element)
return NearestLockedExclusiveAncestor(node);
if (!RuntimeEnabledFeatures::CSSContentVisibilityEnabled() ||
!node.isConnected() ||
node.GetDocument()
.GetDisplayLockDocumentState()
.LockedDisplayLockCount() == 0 ||
!node.CanParticipateInFlatTree()) {
return nullptr;
}
if (auto* context = element->GetDisplayLockContext()) {
if (context->IsLocked())
return element;
}
return NearestLockedExclusiveAncestor(node);
}
Element* DisplayLockUtilities::NearestLockedInclusiveAncestor(Node& node) {
return const_cast<Element*>(
NearestLockedInclusiveAncestor(static_cast<const Node&>(node)));
}
Element* DisplayLockUtilities::NearestHiddenMatchableInclusiveAncestor(
Element& element) {
if (!RuntimeEnabledFeatures::CSSContentVisibilityEnabled() ||
!element.isConnected() ||
element.GetDocument()
.GetDisplayLockDocumentState()
.LockedDisplayLockCount() == 0 ||
!element.CanParticipateInFlatTree()) {
return nullptr;
}
if (auto* context = element.GetDisplayLockContext()) {
if (context->GetState() == EContentVisibility::kHiddenMatchable) {
return &element;
}
}
// TODO(crbug.com/924550): Once we figure out a more efficient way to
// determine whether we're inside a locked subtree or not, change this.
for (Node& ancestor : FlatTreeTraversal::AncestorsOf(element)) {
auto* ancestor_element = DynamicTo<Element>(ancestor);
if (!ancestor_element)
continue;
if (auto* context = ancestor_element->GetDisplayLockContext()) {
if (context->GetState() == EContentVisibility::kHiddenMatchable) {
return ancestor_element;
}
}
}
return nullptr;
}
Element* DisplayLockUtilities::NearestLockedExclusiveAncestor(
const Node& node) {
if (!RuntimeEnabledFeatures::CSSContentVisibilityEnabled() ||
!node.isConnected() ||
node.GetDocument()
.GetDisplayLockDocumentState()
.LockedDisplayLockCount() == 0 ||
!node.CanParticipateInFlatTree()) {
return nullptr;
}
// TODO(crbug.com/924550): Once we figure out a more efficient way to
// determine whether we're inside a locked subtree or not, change this.
for (Node& ancestor : FlatTreeTraversal::AncestorsOf(node)) {
auto* ancestor_element = DynamicTo<Element>(ancestor);
if (!ancestor_element)
continue;
if (auto* context = ancestor_element->GetDisplayLockContext()) {
if (context->IsLocked())
return ancestor_element;
}
}
return nullptr;
}
Element* DisplayLockUtilities::HighestLockedInclusiveAncestor(
const Node& node) {
if (!RuntimeEnabledFeatures::CSSContentVisibilityEnabled() ||
!node.CanParticipateInFlatTree()) {
return nullptr;
}
auto* node_ptr = const_cast<Node*>(&node);
// If the exclusive result exists, then that's higher than this node, so
// return it.
if (auto* result = HighestLockedExclusiveAncestor(node))
return result;
// Otherwise, we know the node is not in a locked subtree, so the only
// other possibility is that the node itself is locked.
auto* element = DynamicTo<Element>(node_ptr);
if (element && element->GetDisplayLockContext() &&
element->GetDisplayLockContext()->IsLocked()) {
return element;
}
return nullptr;
}
Element* DisplayLockUtilities::HighestLockedExclusiveAncestor(
const Node& node) {
if (!RuntimeEnabledFeatures::CSSContentVisibilityEnabled() ||
!node.CanParticipateInFlatTree()) {
return nullptr;
}
Node* parent = FlatTreeTraversal::Parent(node);
Element* locked_ancestor = nullptr;
while (parent) {
auto* locked_candidate = NearestLockedInclusiveAncestor(*parent);
auto* last_node = parent;
if (locked_candidate) {
locked_ancestor = locked_candidate;
parent = FlatTreeTraversal::Parent(*parent);
} else {
parent = nullptr;
}
if (!parent)
parent = GetFrameOwnerNode(last_node);
}
return locked_ancestor;
}
Element* DisplayLockUtilities::NearestLockedInclusiveAncestor(
const LayoutObject& object) {
auto* node = object.GetNode();
auto* ancestor = object.Parent();
while (ancestor && !node) {
node = ancestor->GetNode();
ancestor = ancestor->Parent();
}
return node ? NearestLockedInclusiveAncestor(*node) : nullptr;
}
Element* DisplayLockUtilities::NearestLockedExclusiveAncestor(
const LayoutObject& object) {
if (auto* node = object.GetNode())
return NearestLockedExclusiveAncestor(*node);
// Since we now navigate to an ancestor, use the inclusive version.
if (auto* parent = object.Parent())
return NearestLockedInclusiveAncestor(*parent);
return nullptr;
}
bool DisplayLockUtilities::IsInUnlockedOrActivatableSubtree(
const Node& node,
DisplayLockActivationReason activation_reason) {
if (!RuntimeEnabledFeatures::CSSContentVisibilityEnabled(
node.GetExecutionContext()) ||
node.GetDocument()
.GetDisplayLockDocumentState()
.LockedDisplayLockCount() == 0 ||
!node.CanParticipateInFlatTree()) {
return true;
}
for (auto* element = NearestLockedExclusiveAncestor(node); element;
element = NearestLockedExclusiveAncestor(*element)) {
if (!element->GetDisplayLockContext()->IsActivatable(activation_reason)) {
return false;
}
}
return true;
}
bool DisplayLockUtilities::IsInLockedSubtreeCrossingFrames(
const Node& source_node) {
if (!RuntimeEnabledFeatures::CSSContentVisibilityEnabled())
return false;
if (LocalFrameView* frame_view = source_node.GetDocument().View()) {
if (frame_view->IsDisplayLocked())
return true;
}
const Node* node = &source_node;
// Since we handled the self-check above, we need to do inclusive checks
// starting from the parent.
node = FlatTreeTraversal::Parent(*node);
// If we don't have a flat-tree parent, get the |source_node|'s owner node
// instead.
if (!node)
node = GetFrameOwnerNode(&source_node);
while (node) {
if (NearestLockedInclusiveAncestor(*node))
return true;
node = GetFrameOwnerNode(node);
}
return false;
}
void DisplayLockUtilities::ElementLostFocus(Element* element) {
if (!RuntimeEnabledFeatures::CSSContentVisibilityEnabled() ||
(element && element->GetDocument()
.GetDisplayLockDocumentState()
.DisplayLockCount() == 0))
return;
for (; element; element = FlatTreeTraversal::ParentElement(*element)) {
auto* context = element->GetDisplayLockContext();
if (context)
context->NotifySubtreeLostFocus();
}
}
void DisplayLockUtilities::ElementGainedFocus(Element* element) {
if (!RuntimeEnabledFeatures::CSSContentVisibilityEnabled() ||
(element && element->GetDocument()
.GetDisplayLockDocumentState()
.DisplayLockCount() == 0))
return;
for (; element; element = FlatTreeTraversal::ParentElement(*element)) {
auto* context = element->GetDisplayLockContext();
if (context)
context->NotifySubtreeGainedFocus();
}
}
void DisplayLockUtilities::SelectionChanged(
const EphemeralRangeInFlatTree& old_selection,
const EphemeralRangeInFlatTree& new_selection) {
if (!RuntimeEnabledFeatures::CSSContentVisibilityEnabled() ||
(!old_selection.IsNull() && old_selection.GetDocument()
.GetDisplayLockDocumentState()
.DisplayLockCount() == 0) ||
(!new_selection.IsNull() && new_selection.GetDocument()
.GetDisplayLockDocumentState()
.DisplayLockCount() == 0))
return;
TRACE_EVENT0("blink", "DisplayLockUtilities::SelectionChanged");
std::set<Node*> old_nodes;
for (Node& node : old_selection.Nodes())
old_nodes.insert(&node);
std::set<Node*> new_nodes;
for (Node& node : new_selection.Nodes())
new_nodes.insert(&node);
std::set<DisplayLockContext*> lost_selection_contexts;
std::set<DisplayLockContext*> gained_selection_contexts;
// Skip common nodes and extract contexts from nodes that lost selection and
// contexts from nodes that gained selection.
// This is similar to std::set_symmetric_difference except that we need to
// know which set the resulting item came from. In this version, we simply do
// the relevant operation on each of the items instead of storing the
// difference.
std::set<Node*>::iterator old_it = old_nodes.begin();
std::set<Node*>::iterator new_it = new_nodes.begin();
while (old_it != old_nodes.end() && new_it != new_nodes.end()) {
// Compare the addresses since that's how the nodes are ordered in the set.
if (*old_it < *new_it) {
PopulateAncestorContexts(*old_it++, &lost_selection_contexts);
} else if (*old_it > *new_it) {
PopulateAncestorContexts(*new_it++, &gained_selection_contexts);
} else {
++old_it;
++new_it;
}
}
while (old_it != old_nodes.end())
PopulateAncestorContexts(*old_it++, &lost_selection_contexts);
while (new_it != new_nodes.end())
PopulateAncestorContexts(*new_it++, &gained_selection_contexts);
// Now do a similar thing with contexts: skip common ones, and mark the ones
// that lost selection or gained selection as such.
std::set<DisplayLockContext*>::iterator lost_it =
lost_selection_contexts.begin();
std::set<DisplayLockContext*>::iterator gained_it =
gained_selection_contexts.begin();
while (lost_it != lost_selection_contexts.end() &&
gained_it != gained_selection_contexts.end()) {
if (*lost_it < *gained_it) {
(*lost_it++)->NotifySubtreeLostSelection();
} else if (*lost_it > *gained_it) {
(*gained_it++)->NotifySubtreeGainedSelection();
} else {
++lost_it;
++gained_it;
}
}
while (lost_it != lost_selection_contexts.end())
(*lost_it++)->NotifySubtreeLostSelection();
while (gained_it != gained_selection_contexts.end())
(*gained_it++)->NotifySubtreeGainedSelection();
}
void DisplayLockUtilities::SelectionRemovedFromDocument(Document& document) {
document.GetDisplayLockDocumentState().NotifySelectionRemoved();
}
Element* DisplayLockUtilities::LockedAncestorPreventingPrePaint(
const LayoutObject& object) {
return LockedAncestorPreventingUpdate(
object, [](DisplayLockContext* context) {
return !context->ShouldPrePaintChildren();
});
}
Element* DisplayLockUtilities::LockedAncestorPreventingLayout(
const LayoutObject& object) {
return LockedAncestorPreventingUpdate(
object, [](DisplayLockContext* context) {
return !context->ShouldLayoutChildren();
});
}
Element* DisplayLockUtilities::LockedAncestorPreventingStyle(const Node& node) {
return LockedAncestorPreventingUpdate(node, [](DisplayLockContext* context) {
return !context->ShouldStyleChildren();
});
}
bool DisplayLockUtilities::UpdateStyleAndLayoutForRangeIfNeeded(
const EphemeralRangeInFlatTree& range,
DisplayLockActivationReason reason) {
if (range.IsNull() || range.IsCollapsed())
return false;
if (!RuntimeEnabledFeatures::CSSContentVisibilityEnabled() ||
range.GetDocument()
.GetDisplayLockDocumentState()
.LockedDisplayLockCount() ==
range.GetDocument()
.GetDisplayLockDocumentState()
.DisplayLockBlockingAllActivationCount())
return false;
HeapVector<Member<DisplayLockContext>> forced_context_list_;
for (Node& node : range.Nodes()) {
for (Element* locked_activatable_ancestor :
DisplayLockUtilities::ActivatableLockedInclusiveAncestors(node,
reason)) {
DCHECK(locked_activatable_ancestor->GetDisplayLockContext());
DCHECK(locked_activatable_ancestor->GetDisplayLockContext()->IsLocked());
auto* context = locked_activatable_ancestor->GetDisplayLockContext();
// TODO(vmpstr): Clean this up not to call
// |NotifyForcedUpdateScopeStarted()| directly.
context->NotifyForcedUpdateScopeStarted();
forced_context_list_.push_back(context);
}
}
if (!forced_context_list_.IsEmpty()) {
range.GetDocument().UpdateStyleAndLayout(
DocumentUpdateReason::kDisplayLock);
}
for (auto context : forced_context_list_) {
context->NotifyForcedUpdateScopeEnded();
}
return !forced_context_list_.IsEmpty();
}
bool DisplayLockUtilities::PrePaintBlockedInParentFrame(LayoutView* view) {
auto* owner = view->GetFrameView()->GetFrame().OwnerLayoutObject();
if (!owner)
return false;
auto* element = NearestLockedInclusiveAncestor(*owner);
while (element) {
if (!element->GetDisplayLockContext()->ShouldPrePaintChildren())
return true;
element = NearestLockedExclusiveAncestor(*element);
}
return false;
}
bool DisplayLockUtilities::IsAutoWithoutLayout(const LayoutObject& object) {
auto* context = object.GetDisplayLockContext();
if (!context)
return false;
return !context->IsLocked() && context->IsAuto() &&
!context->HadLifecycleUpdateSinceLastUnlock();
}
} // namespace blink