blob: 458792fbd938d28e43326675f4afb33bee3104b4 [file] [log] [blame]
// Copyright 2018 The Chromium Authors
// 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_context.h"
#include <string>
#include "base/auto_reset.h"
#include "base/memory/ptr_util.h"
#include "base/metrics/histogram_macros.h"
#include "third_party/blink/renderer/core/accessibility/ax_object_cache.h"
#include "third_party/blink/renderer/core/css/style_change_reason.h"
#include "third_party/blink/renderer/core/css/style_engine.h"
#include "third_party/blink/renderer/core/display_lock/content_visibility_auto_state_change_event.h"
#include "third_party/blink/renderer/core/display_lock/display_lock_document_state.h"
#include "third_party/blink/renderer/core/display_lock/display_lock_utilities.h"
#include "third_party/blink/renderer/core/dom/css_toggle.h"
#include "third_party/blink/renderer/core/dom/document.h"
#include "third_party/blink/renderer/core/dom/dom_exception.h"
#include "third_party/blink/renderer/core/dom/element.h"
#include "third_party/blink/renderer/core/dom/node_computed_style.h"
#include "third_party/blink/renderer/core/editing/frame_selection.h"
#include "third_party/blink/renderer/core/editing/selection_template.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/frame/local_frame_view.h"
#include "third_party/blink/renderer/core/html/html_object_element.h"
#include "third_party/blink/renderer/core/html_element_type_helpers.h"
#include "third_party/blink/renderer/core/inspector/inspector_trace_events.h"
#include "third_party/blink/renderer/core/layout/layout_object.h"
#include "third_party/blink/renderer/core/page/page.h"
#include "third_party/blink/renderer/core/page/page_animator.h"
#include "third_party/blink/renderer/core/paint/paint_layer.h"
#include "third_party/blink/renderer/core/paint/pre_paint_tree_walk.h"
#include "third_party/blink/renderer/core/speculation_rules/document_speculation_rules.h"
#include "third_party/blink/renderer/core/style/toggle_trigger.h"
#include "third_party/blink/renderer/core/view_transition/view_transition_utils.h"
#include "third_party/blink/renderer/platform/graphics/compositing/paint_artifact_compositor.h"
#include "third_party/blink/renderer/platform/heap/garbage_collected.h"
#include "third_party/blink/renderer/platform/instrumentation/use_counter.h"
#include "third_party/blink/renderer/platform/wtf/text/string_builder.h"
namespace blink {
namespace {
namespace rejection_names {
const char* kContainmentNotSatisfied =
"Containment requirement is not satisfied.";
const char* kUnsupportedDisplay =
"Element has unsupported display type (display: contents).";
} // namespace rejection_names
ScrollableArea* GetScrollableArea(Node* node) {
if (!node)
return nullptr;
LayoutBoxModelObject* object =
DynamicTo<LayoutBoxModelObject>(node->GetLayoutObject());
if (!object)
return nullptr;
return object->GetScrollableArea();
}
} // namespace
DisplayLockContext::DisplayLockContext(Element* element)
: element_(element), document_(&element_->GetDocument()) {
document_->GetDisplayLockDocumentState().AddDisplayLockContext(this);
DetermineIfSubtreeHasFocus();
DetermineIfSubtreeHasSelection();
DetermineIfSubtreeHasTopLayerElement();
DetermineIfDescendantIsViewTransitionElement();
}
void DisplayLockContext::SetRequestedState(EContentVisibility state,
const AtomicString& toggle_name) {
if (state_ == state && toggle_name_ == toggle_name)
return;
state_ = state;
toggle_name_ = toggle_name;
base::AutoReset<bool> scope(&set_requested_state_scope_, true);
bool should_lock = false;
uint16_t lock_activation_mask = 0;
switch (state_) {
case EContentVisibility::kVisible:
break;
case EContentVisibility::kAuto:
UseCounter::Count(document_, WebFeature::kContentVisibilityAuto);
had_any_viewport_intersection_notifications_ = false;
should_lock = true;
lock_activation_mask =
static_cast<uint16_t>(DisplayLockActivationReason::kAuto);
break;
case EContentVisibility::kHidden:
UseCounter::Count(document_, WebFeature::kContentVisibilityHidden);
should_lock = true;
lock_activation_mask =
is_hidden_until_found_ || is_details_slot_
? static_cast<uint16_t>(DisplayLockActivationReason::kFindInPage)
: 0u;
break;
}
if (!toggle_name.IsNull()) {
if (should_lock) {
// We have both 'content-visibility' and 'toggle-visibility'. We want
// to combine their effects (i.e., content is hidden if *either* would
// hide it), which means we want to intersect the activation masks.
lock_activation_mask &=
static_cast<uint16_t>(DisplayLockActivationReason::kToggleVisibility);
} else {
// We have 'toggle-visibility', but no 'content-visibility'.
should_lock = true;
lock_activation_mask =
static_cast<uint16_t>(DisplayLockActivationReason::kToggleVisibility);
}
}
if (should_lock) {
RequestLock(lock_activation_mask);
} else {
RequestUnlock();
}
// In a new state, we might need to either start or stop observing viewport
// intersections.
UpdateActivationObservationIfNeeded();
// If we needed a deferred not intersecting signal from 'auto' mode, we can
// set that to false, since the mode has switched to something else. If we're
// switching _to_ 'auto' mode, this should already be false and will be a
// no-op.
DCHECK(state_ != EContentVisibility::kAuto ||
!needs_deferred_not_intersecting_signal_);
needs_deferred_not_intersecting_signal_ = false;
UpdateLifecycleNotificationRegistration();
// Note that we call this here since the |state_| change is a render affecting
// state, but is tracked independently.
NotifyRenderAffectingStateChanged();
// Since our state changed, check if we need to create a scoped force update
// object.
element_->GetDocument().GetDisplayLockDocumentState().ForceLockIfNeeded(
element_.Get());
}
scoped_refptr<const ComputedStyle> DisplayLockContext::AdjustElementStyle(
const ComputedStyle* style) const {
if (IsAlwaysVisible())
return style;
if (IsLocked()) {
ComputedStyleBuilder builder(*style);
builder.SetSkipsContents();
return builder.TakeStyle();
}
return style;
}
void DisplayLockContext::RequestLock(uint16_t activation_mask) {
UpdateActivationMask(activation_mask);
SetRenderAffectingState(RenderAffectingState::kLockRequested, true);
}
void DisplayLockContext::RequestUnlock() {
SetRenderAffectingState(RenderAffectingState::kLockRequested, false);
}
void DisplayLockContext::UpdateActivationMask(uint16_t activatable_mask) {
if (activatable_mask == activatable_mask_)
return;
bool all_activation_was_blocked = !activatable_mask_;
bool all_activation_is_blocked = !activatable_mask;
UpdateDocumentBookkeeping(IsLocked(), all_activation_was_blocked, IsLocked(),
all_activation_is_blocked);
activatable_mask_ = activatable_mask;
}
void DisplayLockContext::UpdateDocumentBookkeeping(
bool was_locked,
bool all_activation_was_blocked,
bool is_locked,
bool all_activation_is_blocked) {
if (!document_)
return;
if (was_locked != is_locked) {
if (is_locked)
document_->GetDisplayLockDocumentState().AddLockedDisplayLock();
else
document_->GetDisplayLockDocumentState().RemoveLockedDisplayLock();
}
bool was_locked_and_blocking = was_locked && all_activation_was_blocked;
bool is_locked_and_blocking = is_locked && all_activation_is_blocked;
if (was_locked_and_blocking != is_locked_and_blocking) {
if (is_locked_and_blocking) {
document_->GetDisplayLockDocumentState()
.IncrementDisplayLockBlockingAllActivation();
} else {
document_->GetDisplayLockDocumentState()
.DecrementDisplayLockBlockingAllActivation();
}
}
}
void DisplayLockContext::UpdateActivationObservationIfNeeded() {
// If we don't have a document, then we don't have an observer so just make
// sure we're marked as not observing anything and early out.
if (!document_) {
is_observed_ = false;
return;
}
// We require observation if we are in 'auto' mode and we're connected to a
// view.
bool should_observe = state_ == EContentVisibility::kAuto &&
toggle_name_.IsNull() && ConnectedToView();
if (is_observed_ == should_observe)
return;
is_observed_ = should_observe;
// Reset viewport intersection notification state, so that if we're observing
// again, the next observation will be synchronous.
had_any_viewport_intersection_notifications_ = false;
if (should_observe) {
document_->GetDisplayLockDocumentState()
.RegisterDisplayLockActivationObservation(element_);
} else {
document_->GetDisplayLockDocumentState()
.UnregisterDisplayLockActivationObservation(element_);
// If we're not listening to viewport intersections, then we can assume
// we're not intersecting:
// 1. We might not be connected, in which case we're not intersecting.
// 2. We might not be in 'auto' mode. which means that this doesn't affect
// anything consequential but acts as a reset should we switch back to
// the 'auto' mode.
SetRenderAffectingState(RenderAffectingState::kIntersectsViewport, false);
}
}
bool DisplayLockContext::NeedsLifecycleNotifications() const {
return needs_deferred_not_intersecting_signal_ ||
render_affecting_state_[static_cast<int>(
RenderAffectingState::kAutoStateUnlockedUntilLifecycle)] ||
has_pending_subtree_checks_ || has_pending_clear_has_top_layer_ ||
has_pending_top_layer_check_;
}
void DisplayLockContext::UpdateLifecycleNotificationRegistration() {
if (!document_ || !document_->View()) {
is_registered_for_lifecycle_notifications_ = false;
return;
}
bool needs_notifications = NeedsLifecycleNotifications();
if (needs_notifications == is_registered_for_lifecycle_notifications_)
return;
is_registered_for_lifecycle_notifications_ = needs_notifications;
if (needs_notifications) {
document_->View()->RegisterForLifecycleNotifications(this);
} else {
document_->View()->UnregisterFromLifecycleNotifications(this);
}
}
void DisplayLockContext::Lock() {
DCHECK(!IsLocked());
is_locked_ = true;
UpdateDocumentBookkeeping(false, !activatable_mask_, true,
!activatable_mask_);
// If we're not connected, then we don't have to do anything else. Otherwise,
// we need to ensure that we update our style to check for containment later,
// layout size based on the options, and also clear the painted output.
if (!ConnectedToView()) {
return;
}
// If there are any pending updates, we cancel them, as the fast updates
// can't detect a locked display.
// See: ../paint/README.md#Transform-update-optimization for more information
document_->View()->RemoveAllPendingUpdates();
// There are two ways we can get locked:
// 1. A new content-visibility property needs us to be locked.
// 2. We're in 'auto' mode and we are not intersecting the viewport.
// In the first case, we are already in style processing, so we don't need to
// invalidate style. However, in the second case we invalidate style so that
// `AdjustElementStyle()` can be called.
if (CanDirtyStyle()) {
element_->SetNeedsStyleRecalc(
kLocalStyleChange,
StyleChangeReasonForTracing::Create(style_change_reason::kDisplayLock));
MarkForStyleRecalcIfNeeded();
}
// TODO(vmpstr): Note when an 'auto' context gets locked, we should clear
// the ancestor scroll anchors. This is a workaround for a behavior that
// happens when the user quickly scrolls (e.g. scrollbar scrolls) into an
// area that only has locked content. We can get into a loop that will
// keep unlocking an element, which may shrink it to be out of the viewport,
// and thus relocking it again. It is is also possible that we selected the
// scroller itself or one of the locked elements as the anchor, so we don't
// actually shift the scroll and the loop continues indefinitely. The user
// can easily get out of the loop by scrolling since that triggers a new
// scroll anchor selection. The work-around for us is also to pick a new
// scroll anchor for the scroller that has a newly-locked context. The
// reason it works is that it causes us to pick an anchor while the element
// is still unlocked, so when it gets relocked we shift the scroll to
// whatever visible content we had. The TODO here is to figure out if there
// is a better way to solve this. In either case, we have to select a new
// scroll anchor to get out of this behavior.
element_->NotifyPriorityScrollAnchorStatusChanged();
// We need to notify the AX cache (if it exists) to update |element_|'s
// children in the AX cache.
if (AXObjectCache* cache = element_->GetDocument().ExistingAXObjectCache())
cache->ChildrenChanged(element_);
// If we have top layer elements in our subtree, we have to detach their
// layout objects, since otherwise they would be hoisted out of our subtree.
DetachDescendantTopLayerElements();
// Schedule ContentVisibilityAutoStateChange event if needed.
// TODO(https://crbug.com/1250716): We shouldn't fire this if it was the
// result of toggle state changing.
ScheduleStateChangeEventIfNeeded();
if (!element_->GetLayoutObject())
return;
// If this element is a scroller, then stash its current scroll offset, so
// that we can restore it when needed.
// Note that this only applies if the element itself is a scroller. Any
// subtree scrollers' scroll offsets are not affected.
StashScrollOffsetIfAvailable();
MarkNeedsRepaintAndPaintArtifactCompositorUpdate();
}
// Should* and Did* function for the lifecycle phases. These functions control
// whether or not to process the lifecycle for self or for children.
// =============================================================================
bool DisplayLockContext::ShouldStyleChildren() const {
return !is_locked_ ||
forced_info_.is_forced(ForcedPhase::kStyleAndLayoutTree) ||
(document_->GetDisplayLockDocumentState()
.ActivatableDisplayLocksForced() &&
IsActivatable(DisplayLockActivationReason::kAny));
}
void DisplayLockContext::DidStyleSelf() {
// If we don't have a style after styling self, it means that we should revert
// to the default state of being visible. This will get updated when we gain
// new style.
if (!element_->GetComputedStyle()) {
SetRequestedState(EContentVisibility::kVisible, g_null_atom);
return;
}
// TODO(vmpstr): This needs to be in the spec.
if (ForceUnlockIfNeeded())
return;
if (!IsLocked() && !IsAlwaysVisible()) {
UpdateActivationObservationIfNeeded();
NotifyRenderAffectingStateChanged();
}
}
void DisplayLockContext::DidStyleChildren() {
if (!element_->ChildNeedsReattachLayoutTree())
return;
auto* parent = element_->GetReattachParent();
if (!parent || parent->ChildNeedsReattachLayoutTree())
return;
element_->MarkAncestorsWithChildNeedsReattachLayoutTree();
}
bool DisplayLockContext::ShouldLayoutChildren() const {
return !is_locked_ || forced_info_.is_forced(ForcedPhase::kLayout) ||
(document_->GetDisplayLockDocumentState()
.ActivatableDisplayLocksForced() &&
IsActivatable(DisplayLockActivationReason::kAny));
}
void DisplayLockContext::DidLayoutChildren() {
// Since we did layout on children already, we'll clear this.
child_layout_was_blocked_ = false;
had_lifecycle_update_since_last_unlock_ = true;
// If we're not locked and we laid out the children, then now is a good time
// to restore the scroll offset.
if (!is_locked_)
RestoreScrollOffsetIfStashed();
}
bool DisplayLockContext::ShouldPrePaintChildren() const {
return !is_locked_ || forced_info_.is_forced(ForcedPhase::kPrePaint) ||
(document_->GetDisplayLockDocumentState()
.ActivatableDisplayLocksForced() &&
IsActivatable(DisplayLockActivationReason::kAny));
}
bool DisplayLockContext::ShouldPaintChildren() const {
// Note that forced updates should never require us to paint, so we don't
// check |forced_info_| here.
return !is_locked_;
}
// End Should* and Did* functions ==============================================
bool DisplayLockContext::IsActivatable(
DisplayLockActivationReason reason) const {
return activatable_mask_ & static_cast<uint16_t>(reason);
}
void DisplayLockContext::CommitForActivation(
DisplayLockActivationReason reason) {
DCHECK(element_);
DCHECK(ConnectedToView());
DCHECK(IsLocked());
DCHECK(ShouldCommitForActivation(DisplayLockActivationReason::kAny));
// The following actions (can) scroll content into view. However, if the
// position of the target is outside of the bounds that would cause the
// auto-context to unlock, then we can scroll into wrong content while the
// context remains lock. To avoid this, unlock it until the next lifecycle.
// If the scroll is successful, then we will gain visibility anyway so the
// context will be unlocked for other reasons.
if (reason == DisplayLockActivationReason::kAccessibility ||
reason == DisplayLockActivationReason::kFindInPage ||
reason == DisplayLockActivationReason::kFragmentNavigation ||
reason == DisplayLockActivationReason::kScrollIntoView ||
reason == DisplayLockActivationReason::kSimulatedClick) {
// Note that because the visibility is only determined at the _end_ of the
// next frame, we need to ensure that we stay unlocked for two frames.
SetKeepUnlockedUntilLifecycleCount(2);
}
if (!toggle_name_.IsNull()) {
CSSToggle* toggle = CSSToggle::FindToggleInScope(*element_, toggle_name_);
DCHECK(toggle) << "should no longer be locked with a toggle state";
ToggleTrigger trigger(toggle_name_, ToggleTriggerMode::kSet,
ToggleTrigger::State(1u));
toggle->FireToggleActivation(*element_, trigger);
}
if (reason == DisplayLockActivationReason::kFindInPage)
document_->MarkHasFindInPageContentVisibilityActiveMatch();
}
void DisplayLockContext::SetKeepUnlockedUntilLifecycleCount(int count) {
DCHECK_GT(count, 0);
keep_unlocked_count_ = std::max(keep_unlocked_count_, count);
SetRenderAffectingState(
RenderAffectingState::kAutoStateUnlockedUntilLifecycle, true);
UpdateLifecycleNotificationRegistration();
ScheduleAnimation();
}
void DisplayLockContext::NotifyIsIntersectingViewport() {
had_any_viewport_intersection_notifications_ = true;
// If we are now intersecting, then we are definitely not nested in a locked
// subtree and we don't need to lock as a result.
needs_deferred_not_intersecting_signal_ = false;
UpdateLifecycleNotificationRegistration();
// If we're not connected, then there is no need to change any state.
// This could be the case if we were disconnected while a viewport
// intersection notification was pending.
if (ConnectedToView())
SetRenderAffectingState(RenderAffectingState::kIntersectsViewport, true);
}
void DisplayLockContext::NotifyIsNotIntersectingViewport() {
had_any_viewport_intersection_notifications_ = true;
if (IsLocked()) {
DCHECK(!needs_deferred_not_intersecting_signal_);
return;
}
// We might have been disconnected while the intersection observation
// notification was pending. Ensure to unregister from lifecycle
// notifications if we're doing that, and early out.
if (!ConnectedToView()) {
needs_deferred_not_intersecting_signal_ = false;
UpdateLifecycleNotificationRegistration();
return;
}
// There are two situations we need to consider here:
// 1. We are off-screen but not nested in any other lock. This means we should
// re-lock (also verify that the reason we're in this state is that we're
// activated).
// 2. We are in a nested locked context. This means we don't actually know
// whether we should lock or not. In order to avoid needless dirty of the
// layout and style trees up to the nested context, we remain unlocked.
// However, we also need to ensure that we relock if we become unnested.
// So, we simply delay this check to the next frame (via LocalFrameView),
// which will call this function again and so we can perform the check
// again.
// Note that we use a signal that we're not painting to defer intersection,
// since even if we're updating the locked ancestor for style or layout, we
// should defer intersection notifications.
auto* locked_ancestor =
DisplayLockUtilities::LockedAncestorPreventingPaint(*element_);
if (locked_ancestor) {
needs_deferred_not_intersecting_signal_ = true;
} else {
needs_deferred_not_intersecting_signal_ = false;
SetRenderAffectingState(RenderAffectingState::kIntersectsViewport, false);
}
UpdateLifecycleNotificationRegistration();
}
bool DisplayLockContext::ShouldCommitForActivation(
DisplayLockActivationReason reason) const {
return IsActivatable(reason) && IsLocked();
}
void DisplayLockContext::UpgradeForcedScope(ForcedPhase old_phase,
ForcedPhase new_phase,
bool emit_warnings) {
// Since we're upgrading, it means we have a bigger phase.
DCHECK_LT(static_cast<int>(old_phase), static_cast<int>(new_phase));
auto old_forced_info = forced_info_;
forced_info_.end(old_phase);
forced_info_.start(new_phase);
if (IsLocked()) {
// Now that the update is forced, we should ensure that style layout, and
// prepaint code can reach it via dirty bits. Note that paint isn't a part
// of this, since |forced_info_| doesn't force paint to happen. See
// ShouldPaint(). Also, we could have forced a lock from SetRequestedState
// during a style update. If that's the case, don't mark style as dirty
// from within style recalc. We rely on `TakeBlockedStyleRecalcChange`
// to be called from self style recalc.
if (CanDirtyStyle() &&
!old_forced_info.is_forced(ForcedPhase::kStyleAndLayoutTree) &&
forced_info_.is_forced(ForcedPhase::kStyleAndLayoutTree)) {
MarkForStyleRecalcIfNeeded();
}
if (!old_forced_info.is_forced(ForcedPhase::kLayout) &&
forced_info_.is_forced(ForcedPhase::kLayout)) {
MarkForLayoutIfNeeded();
}
if (!old_forced_info.is_forced(ForcedPhase::kPrePaint) &&
forced_info_.is_forced(ForcedPhase::kPrePaint)) {
MarkAncestorsForPrePaintIfNeeded();
}
if (emit_warnings && v8::Isolate::GetCurrent()->InContext() && document_ &&
element_ &&
(!IsActivatable(DisplayLockActivationReason::kAny) ||
RuntimeEnabledFeatures::
WarnOnContentVisibilityRenderAccessEnabled())) {
document_->GetDisplayLockDocumentState().IssueForcedRenderWarning(
element_);
}
}
}
void DisplayLockContext::ScheduleStateChangeEventIfNeeded() {
if (state_ == EContentVisibility::kAuto &&
RuntimeEnabledFeatures::ContentVisibilityAutoStateChangeEventEnabled() &&
!state_change_task_pending_) {
document_->GetExecutionContext()
->GetTaskRunner(TaskType::kMiscPlatformAPI)
->PostTask(
FROM_HERE,
WTF::BindOnce(&DisplayLockContext::DispatchStateChangeEventIfNeeded,
WrapPersistent(this)));
state_change_task_pending_ = true;
}
}
void DisplayLockContext::DispatchStateChangeEventIfNeeded() {
DCHECK(state_change_task_pending_);
state_change_task_pending_ = false;
// If we're not connected to view, reset the state that we reported so that we
// can report it again on insertion.
if (!ConnectedToView()) {
last_notified_skipped_state_.reset();
return;
}
if (!last_notified_skipped_state_ ||
*last_notified_skipped_state_ != is_locked_) {
last_notified_skipped_state_ = is_locked_;
element_->DispatchEvent(*ContentVisibilityAutoStateChangeEvent::Create(
event_type_names::kContentvisibilityautostatechange, is_locked_));
}
}
void DisplayLockContext::NotifyForcedUpdateScopeEnded(ForcedPhase phase) {
// Since we do perform updates in a locked display if we're in a forced
// update scope, when ending a forced update scope in a locked display, we
// remove all pending updates, to prevent them from being executed in a
// locked display.
// See: ../paint/README.md#Transform-update-optimization for more information
if (is_locked_) {
document_->View()->RemoveAllPendingUpdates();
}
forced_info_.end(phase);
}
void DisplayLockContext::Unlock() {
DCHECK(IsLocked());
is_locked_ = false;
had_lifecycle_update_since_last_unlock_ = false;
UpdateDocumentBookkeeping(true, !activatable_mask_, false,
!activatable_mask_);
if (!ConnectedToView())
return;
// There are a few ways we can get unlocked:
// 1. A new content-visibility property needs us to be ulocked.
// 2. We're in 'auto' mode and we are intersecting the viewport.
// In the first case, we are already in style processing, so we don't need to
// invalidate style. However, in the second case we invalidate style so that
// `AdjustElementStyle()` can be called.
if (CanDirtyStyle()) {
// Since size containment depends on the activatability state, we should
// invalidate the style for this element, so that the style adjuster can
// properly remove the containment.
element_->SetNeedsStyleRecalc(
kLocalStyleChange,
StyleChangeReasonForTracing::Create(style_change_reason::kDisplayLock));
// Also propagate any dirty bits that we have previously blocked.
// If we're in style recalc, this will be handled by
// `TakeBlockedStyleRecalcChange()` call from self style recalc.
MarkForStyleRecalcIfNeeded();
} else if (SubtreeHasTopLayerElement()) {
// TODO(vmpstr): This seems like a big hammer, but it's unclear to me how we
// can mark the dirty bits from the descendant top layer node up to this
// display lock on the ancestor chain while we're in the middle of style
// recalc. It seems plausible, but we have to be careful.
blocked_child_recalc_change_ = blocked_child_recalc_change_.EnsureAtLeast(
StyleRecalcChange::kRecalcDescendants);
}
// We also need to notify the AX cache (if it exists) to update the childrens
// of |element_| in the AX cache.
if (AXObjectCache* cache = element_->GetDocument().ExistingAXObjectCache())
cache->ChildrenChanged(element_);
// Schedule ContentVisibilityAutoStateChange event if needed.
// TODO(https://crbug.com/1250716): We shouldn't fire this if it was the
// result of toggle state changing.
ScheduleStateChangeEventIfNeeded();
auto* layout_object = element_->GetLayoutObject();
// We might commit without connecting, so there is no layout object yet.
if (!layout_object)
return;
// Now that we know we have a layout object, we should ensure that we can
// reach the rest of the phases as well.
MarkForLayoutIfNeeded();
MarkAncestorsForPrePaintIfNeeded();
MarkNeedsRepaintAndPaintArtifactCompositorUpdate();
MarkNeedsCullRectUpdate();
}
bool DisplayLockContext::CanDirtyStyle() const {
return !set_requested_state_scope_ && !document_->InStyleRecalc();
}
bool DisplayLockContext::MarkForStyleRecalcIfNeeded() {
DCHECK(CanDirtyStyle());
if (IsElementDirtyForStyleRecalc()) {
// Propagate to the ancestors, since the dirty bit in a locked subtree is
// stopped at the locked ancestor.
// See comment in IsElementDirtyForStyleRecalc.
element_->SetNeedsStyleRecalc(
kLocalStyleChange,
StyleChangeReasonForTracing::Create(style_change_reason::kDisplayLock));
element_->MarkAncestorsWithChildNeedsStyleRecalc();
// When we're forcing a lock, which is done in a CanDirtyStyle context, we
// mark the top layers that don't have a computed style as needing a style
// recalc. This is a heuristic since if a top layer doesn't have a computed
// style then it is possibly under a content-visibility skipped subtree. The
// alternative is to figure out exactly which top layer element is under
// this lock and only dirty those, but that seems unnecessary. If the top
// layer element is locked under a different lock, then the dirty bit
// wouldn't propagate anyway.
for (auto top_layer_element : document_->TopLayerElements()) {
if (!top_layer_element->GetComputedStyle()) {
top_layer_element->SetNeedsStyleRecalc(
kLocalStyleChange, StyleChangeReasonForTracing::Create(
style_change_reason::kDisplayLock));
}
}
return true;
}
return false;
}
bool DisplayLockContext::MarkForLayoutIfNeeded() {
if (IsElementDirtyForLayout()) {
// Forces the marking of ancestors to happen, even if
// |DisplayLockContext::ShouldLayout()| returns false.
class ScopedForceLayout {
STACK_ALLOCATED();
public:
explicit ScopedForceLayout(DisplayLockContext* context)
: context_(context) {
context_->forced_info_.start(ForcedPhase::kLayout);
}
~ScopedForceLayout() { context_->forced_info_.end(ForcedPhase::kLayout); }
private:
DisplayLockContext* context_;
} scoped_force(this);
auto* layout_object = element_->GetLayoutObject();
if (child_layout_was_blocked_ || HasStashedScrollOffset()) {
// We've previously blocked a child traversal when doing self-layout for
// the locked element, so we're marking it with child-needs-layout so that
// it will traverse to the locked element and do the child traversal
// again. We don't need to mark it for self-layout (by calling
// |LayoutObject::SetNeedsLayout()|) because the locked element itself
// doesn't need to relayout.
//
// Note that we also make sure to visit the children when we have a
// stashed scroll offset. This is so that we can restore the offset after
// laying out the children. If we try to restore it before the layout, it
// will be ignored since the scroll area may think that it doesn't have
// enough contents.
// TODO(vmpstr): In the scroll offset case, we're doing this just so we
// can reach DisplayLockContext::DidLayoutChildren where we restore the
// offset. If performance becomes an issue, then we should think of a
// different time / opportunity to restore the offset.
layout_object->SetChildNeedsLayout();
child_layout_was_blocked_ = false;
} else {
// Since the dirty layout propagation stops at the locked element, we need
// to mark its ancestors as dirty here so that it will be traversed to on
// the next layout.
layout_object->MarkContainerChainForLayout();
}
return true;
}
return false;
}
bool DisplayLockContext::MarkAncestorsForPrePaintIfNeeded() {
// TODO(vmpstr): We should add a compositing phase for proper bookkeeping.
bool compositing_dirtied = MarkForCompositingUpdatesIfNeeded();
if (IsElementDirtyForPrePaint()) {
auto* layout_object = element_->GetLayoutObject();
if (auto* parent = layout_object->Parent())
parent->SetSubtreeShouldCheckForPaintInvalidation();
// Note that if either we or our descendants are marked as needing this
// update, then ensure to mark self as needing the update. This sets up the
// correct flags for PrePaint to recompute the necessary values and
// propagate the information into the subtree.
if (needs_effective_allowed_touch_action_update_ ||
layout_object->EffectiveAllowedTouchActionChanged() ||
layout_object->DescendantEffectiveAllowedTouchActionChanged()) {
// Note that although the object itself should have up to date value, in
// order to force recalc of the whole subtree, we mark it as needing an
// update.
layout_object->MarkEffectiveAllowedTouchActionChanged();
}
if (needs_blocking_wheel_event_handler_update_ ||
layout_object->BlockingWheelEventHandlerChanged() ||
layout_object->DescendantBlockingWheelEventHandlerChanged()) {
// Note that although the object itself should have up to date value, in
// order to force recalc of the whole subtree, we mark it as needing an
// update.
layout_object->MarkBlockingWheelEventHandlerChanged();
}
return true;
}
return compositing_dirtied;
}
bool DisplayLockContext::MarkNeedsRepaintAndPaintArtifactCompositorUpdate() {
DCHECK(ConnectedToView());
if (auto* layout_object = element_->GetLayoutObject()) {
layout_object->PaintingLayer()->SetNeedsRepaint();
document_->View()->SetPaintArtifactCompositorNeedsUpdate();
return true;
}
return false;
}
bool DisplayLockContext::MarkNeedsCullRectUpdate() {
DCHECK(ConnectedToView());
if (auto* layout_object = element_->GetLayoutObject()) {
layout_object->PaintingLayer()->SetForcesChildrenCullRectUpdate();
return true;
}
return false;
}
bool DisplayLockContext::MarkForCompositingUpdatesIfNeeded() {
if (!ConnectedToView())
return false;
auto* layout_object = element_->GetLayoutObject();
if (!layout_object)
return false;
auto* layout_box = DynamicTo<LayoutBoxModelObject>(layout_object);
if (layout_box && layout_box->HasSelfPaintingLayer()) {
if (needs_compositing_dependent_flag_update_)
layout_box->Layer()->SetNeedsCompositingInputsUpdate();
needs_compositing_dependent_flag_update_ = false;
return true;
}
return false;
}
bool DisplayLockContext::IsElementDirtyForStyleRecalc() const {
// The |element_| checks could be true even if |blocked_child_recalc_change_|
// is empty. The reason for this is that the |blocked_child_recalc_change_| is
// set during the style walk that this display lock blocks. However, we could
// dirty element style and unlock this context (e.g. by c-v auto visibility
// change) before ever having gone through the style calc that would have been
// blocked Also these dirty bits were not propagated to the ancestors, so we
// do need to update the dirty bit state for ancestors.
return element_->IsDirtyForStyleRecalc() ||
element_->ChildNeedsStyleRecalc() ||
element_->ChildNeedsReattachLayoutTree() ||
!blocked_child_recalc_change_.IsEmpty() || SubtreeHasTopLayerElement();
}
bool DisplayLockContext::IsElementDirtyForLayout() const {
if (auto* layout_object = element_->GetLayoutObject()) {
return layout_object->NeedsLayout() || child_layout_was_blocked_ ||
HasStashedScrollOffset();
}
return false;
}
bool DisplayLockContext::IsElementDirtyForPrePaint() const {
if (auto* layout_object = element_->GetLayoutObject()) {
return PrePaintTreeWalk::ObjectRequiresPrePaint(*layout_object) ||
PrePaintTreeWalk::ObjectRequiresTreeBuilderContext(*layout_object) ||
needs_prepaint_subtree_walk_ ||
needs_effective_allowed_touch_action_update_ ||
needs_blocking_wheel_event_handler_update_;
}
return false;
}
void DisplayLockContext::DidMoveToNewDocument(Document& old_document) {
DCHECK(element_);
document_ = &element_->GetDocument();
old_document.GetDisplayLockDocumentState().RemoveDisplayLockContext(this);
document_->GetDisplayLockDocumentState().AddDisplayLockContext(this);
if (is_observed_) {
old_document.GetDisplayLockDocumentState()
.UnregisterDisplayLockActivationObservation(element_);
document_->GetDisplayLockDocumentState()
.RegisterDisplayLockActivationObservation(element_);
}
// Since we're observing the lifecycle updates, ensure that we listen to the
// right document's view.
if (is_registered_for_lifecycle_notifications_) {
if (old_document.View())
old_document.View()->UnregisterFromLifecycleNotifications(this);
if (document_->View())
document_->View()->RegisterForLifecycleNotifications(this);
else
is_registered_for_lifecycle_notifications_ = false;
}
if (IsLocked()) {
old_document.GetDisplayLockDocumentState().RemoveLockedDisplayLock();
document_->GetDisplayLockDocumentState().AddLockedDisplayLock();
if (!IsActivatable(DisplayLockActivationReason::kAny)) {
old_document.GetDisplayLockDocumentState()
.DecrementDisplayLockBlockingAllActivation();
document_->GetDisplayLockDocumentState()
.IncrementDisplayLockBlockingAllActivation();
}
}
DetermineIfSubtreeHasFocus();
DetermineIfSubtreeHasSelection();
DetermineIfSubtreeHasTopLayerElement();
DetermineIfDescendantIsViewTransitionElement();
}
void DisplayLockContext::WillStartLifecycleUpdate(const LocalFrameView& view) {
DCHECK(NeedsLifecycleNotifications());
// We might have delayed processing intersection observation update (signal
// that we were not intersecting) because this context was nested in another
// locked context. At the start of the lifecycle, we should check whether
// that is still true. In other words, this call will check if we're still
// nested. If we are, we won't do anything. If we're not, then we will lock
// this context.
//
// Note that when we are no longer nested and and we have not received any
// notifications from the intersection observer, it means that we are not
// visible.
if (needs_deferred_not_intersecting_signal_)
NotifyIsNotIntersectingViewport();
bool update_registration = false;
// If we're keeping this context unlocked, update the values.
if (keep_unlocked_count_) {
if (--keep_unlocked_count_) {
ScheduleAnimation();
} else {
SetRenderAffectingState(
RenderAffectingState::kAutoStateUnlockedUntilLifecycle, false);
update_registration = true;
}
} else {
DCHECK(!render_affecting_state_[static_cast<int>(
RenderAffectingState::kAutoStateUnlockedUntilLifecycle)]);
}
if (has_pending_subtree_checks_ || has_pending_top_layer_check_) {
DetermineIfSubtreeHasTopLayerElement();
has_pending_top_layer_check_ = false;
update_registration = true;
}
if (has_pending_subtree_checks_) {
DetermineIfSubtreeHasFocus();
DetermineIfSubtreeHasSelection();
has_pending_subtree_checks_ = false;
update_registration = true;
}
if (has_pending_clear_has_top_layer_) {
SetRenderAffectingState(RenderAffectingState::kSubtreeHasTopLayerElement,
false);
has_pending_clear_has_top_layer_ = false;
update_registration = true;
}
if (update_registration)
UpdateLifecycleNotificationRegistration();
}
void DisplayLockContext::NotifyWillDisconnect() {
if (!IsLocked() || !element_ || !element_->GetLayoutObject())
return;
// If we're locked while being disconnected, we need to layout the parent.
// The reason for this is that we might skip the layout if we're empty while
// locked, but it's important to update IsSelfCollapsingBlock property on
// the parent so that it's up to date. This property is updated during
// layout.
if (auto* parent = element_->GetLayoutObject()->Parent())
parent->SetNeedsLayout(layout_invalidation_reason::kDisplayLock);
}
void DisplayLockContext::ElementDisconnected() {
// We remove the style when disconnecting an element, so we should also unlock
// the context.
DCHECK(!element_->GetComputedStyle());
SetRequestedState(EContentVisibility::kVisible, g_null_atom);
if (auto* document_rules =
DocumentSpeculationRules::FromIfExists(*document_)) {
document_rules->DisplayLockedElementDisconnected(element_);
}
// blocked_child_recalc_change_ must be cleared because things can be in an
// inconsistent state when we add the element back (e.g. crbug.com/1262742).
blocked_child_recalc_change_ = StyleRecalcChange();
}
void DisplayLockContext::ElementConnected() {
// When connecting the element, we should not have a style.
DCHECK(!element_->GetComputedStyle());
// We can't check for subtree selection / focus here, since we are likely in
// slot reassignment forbidden scope. However, walking the subtree may need
// this reassignment. This is fine, since the state check can be deferred
// until the beginning of the next frame.
has_pending_subtree_checks_ = true;
UpdateLifecycleNotificationRegistration();
ScheduleAnimation();
}
void DisplayLockContext::DetachLayoutTree() {
// When |element_| is removed from the flat tree, we need to set this context
// to visible.
if (!element_->GetComputedStyle()) {
SetRequestedState(EContentVisibility::kVisible, g_null_atom);
blocked_child_recalc_change_ = StyleRecalcChange();
}
}
void DisplayLockContext::ScheduleTopLayerCheck() {
has_pending_top_layer_check_ = true;
UpdateLifecycleNotificationRegistration();
ScheduleAnimation();
}
void DisplayLockContext::ScheduleAnimation() {
DCHECK(element_);
if (!ConnectedToView() || !document_ || !document_->GetPage())
return;
// Schedule an animation to perform the lifecycle phases.
document_->GetPage()->Animator().ScheduleVisualUpdate(document_->GetFrame());
}
const char* DisplayLockContext::ShouldForceUnlock() const {
DCHECK(element_);
// This function is only called after style, layout tree, or lifecycle
// updates, so the style should be up-to-date, except in the case of nested
// locks, where the style recalc will never actually get to |element_|.
// TODO(vmpstr): We need to figure out what to do here, since we don't know
// what the style is and whether this element has proper containment. However,
// forcing an update from the ancestor locks seems inefficient. For now, we
// just optimistically assume that we have all of the right containment in
// place. See crbug.com/926276 for more information.
if (element_->NeedsStyleRecalc()) {
DCHECK(DisplayLockUtilities::LockedAncestorPreventingStyle(*element_));
return nullptr;
}
if (element_->HasDisplayContentsStyle())
return rejection_names::kUnsupportedDisplay;
auto* style = element_->GetComputedStyle();
DCHECK(style);
// We need style and layout containment in order to properly lock the subtree.
if (!style->ContainsStyle() || !style->ContainsLayout())
return rejection_names::kContainmentNotSatisfied;
// We allow replaced elements without fallback content to be locked. This
// check is similar to the check in DefinitelyNewFormattingContext() in
// element.cc, but in this case we allow object element to get locked.
if (const auto* object_element = DynamicTo<HTMLObjectElement>(*element_)) {
if (!object_element->UseFallbackContent())
return nullptr;
} else if (IsA<HTMLImageElement>(*element_) ||
IsA<HTMLCanvasElement>(*element_) ||
(element_->IsFormControlElement() &&
!element_->IsOutputElement()) ||
element_->IsMediaElement() || element_->IsFrameOwnerElement() ||
element_->IsSVGElement()) {
return nullptr;
}
// From https://www.w3.org/TR/css-contain-1/#containment-layout
// If the element does not generate a principal box (as is the case with
// display: contents or display: none), or if the element is an internal
// table element other than display: table-cell, if the element is an
// internal ruby element, or if the element’s principal box is a
// non-atomic inline-level box, layout containment has no effect.
// (Note we're allowing display:none for display locked elements, and a bit
// more restrictive on ruby - banning <ruby> elements entirely).
auto* html_element = DynamicTo<HTMLElement>(element_.Get());
if ((style->IsDisplayTableType() &&
style->Display() != EDisplay::kTableCell) ||
(!html_element || IsA<HTMLRubyElement>(html_element)) ||
(style->IsDisplayInlineType() && !style->IsDisplayReplacedType())) {
return rejection_names::kContainmentNotSatisfied;
}
return nullptr;
}
bool DisplayLockContext::ForceUnlockIfNeeded() {
// We must have "contain: style layout", and disallow display:contents
// for display locking. Note that we should always guarantee this after
// every style or layout tree update. Otherwise, proceeding with layout may
// cause unexpected behavior. By rejecting the promise, the behavior can be
// detected by script.
// TODO(rakina): If this is after acquire's promise is resolved and update()
// commit() isn't in progress, the web author won't know that the element
// got unlocked. Figure out how to notify the author.
if (ShouldForceUnlock()) {
if (IsLocked()) {
Unlock();
// If we forced unlock, then we need to prevent subsequent calls to
// Lock() until the next frame.
SetRequestedState(EContentVisibility::kVisible, g_null_atom);
}
return true;
}
return false;
}
bool DisplayLockContext::ConnectedToView() const {
return element_ && document_ && element_->isConnected() && document_->View();
}
void DisplayLockContext::NotifySubtreeLostFocus() {
SetRenderAffectingState(RenderAffectingState::kSubtreeHasFocus, false);
}
void DisplayLockContext::NotifySubtreeGainedFocus() {
SetRenderAffectingState(RenderAffectingState::kSubtreeHasFocus, true);
}
void DisplayLockContext::DetermineIfSubtreeHasFocus() {
if (!ConnectedToView()) {
SetRenderAffectingState(RenderAffectingState::kSubtreeHasFocus, false);
return;
}
bool subtree_has_focus = false;
// Iterate up the ancestor chain from the currently focused element. If at any
// time we find our element, then our subtree is focused.
for (auto* focused = document_->FocusedElement(); focused;
focused = FlatTreeTraversal::ParentElement(*focused)) {
if (focused == element_.Get()) {
subtree_has_focus = true;
break;
}
}
SetRenderAffectingState(RenderAffectingState::kSubtreeHasFocus,
subtree_has_focus);
}
void DisplayLockContext::DetermineIfSubtreeHasTopLayerElement() {
if (!ConnectedToView())
return;
ClearHasTopLayerElement();
// Iterate up the ancestor chain from each top layer element.
// Note that this walk is searching for just the |element_| associated with
// this lock. The walk in DisplayLockDocumentState walks from top layer
// elements all the way to the ancestors searching for display locks, so if we
// have nested display locks that walk is more optimal.
for (auto top_layer_element : document_->TopLayerElements()) {
auto* ancestor = top_layer_element.Get();
while ((ancestor = FlatTreeTraversal::ParentElement(*ancestor))) {
if (ancestor == element_) {
NotifyHasTopLayerElement();
return;
}
}
}
}
void DisplayLockContext::DetermineIfDescendantIsViewTransitionElement() {
ResetDescendantIsViewTransitionElement();
if (ConnectedToView()) {
document_->GetDisplayLockDocumentState()
.UpdateViewTransitionElementAncestorLocks();
}
}
void DisplayLockContext::ResetDescendantIsViewTransitionElement() {
SetRenderAffectingState(
RenderAffectingState::kDescendantIsViewTransitionElement, false);
}
void DisplayLockContext::SetDescendantIsViewTransitionElement() {
SetRenderAffectingState(
RenderAffectingState::kDescendantIsViewTransitionElement, true);
}
void DisplayLockContext::ClearHasTopLayerElement() {
// Note that this is asynchronous because it can happen during a layout detach
// which is a bad time to relock a content-visibility auto element (since it
// causes us to potentially access layout objects which are in a state of
// being destroyed).
has_pending_clear_has_top_layer_ = true;
UpdateLifecycleNotificationRegistration();
ScheduleAnimation();
}
void DisplayLockContext::NotifyHasTopLayerElement() {
has_pending_clear_has_top_layer_ = false;
SetRenderAffectingState(RenderAffectingState::kSubtreeHasTopLayerElement,
true);
UpdateLifecycleNotificationRegistration();
}
bool DisplayLockContext::SubtreeHasTopLayerElement() const {
return render_affecting_state_[static_cast<int>(
RenderAffectingState::kSubtreeHasTopLayerElement)];
}
void DisplayLockContext::DetachDescendantTopLayerElements() {
if (!ConnectedToView() || !SubtreeHasTopLayerElement())
return;
// Detach all top layer elements contained by the element inducing this
// display lock.
// Detaching a layout tree can cause further top layer elements to be removed
// from the top layer element's list (in a nested top layer element case --
// since we would remove the ::backdrop pseudo when the layout object
// disappears). This means that we're potentially modifying the list as we're
// traversing it. Instead of doing that, make a copy.
auto top_layer_elements = document_->TopLayerElements();
for (auto top_layer_element : top_layer_elements) {
auto* ancestor = top_layer_element.Get();
while ((ancestor = FlatTreeTraversal::ParentElement(*ancestor))) {
if (ancestor == element_) {
top_layer_element->DetachLayoutTree();
break;
}
}
}
}
void DisplayLockContext::NotifySubtreeGainedSelection() {
SetRenderAffectingState(RenderAffectingState::kSubtreeHasSelection, true);
}
void DisplayLockContext::NotifySubtreeLostSelection() {
SetRenderAffectingState(RenderAffectingState::kSubtreeHasSelection, false);
}
void DisplayLockContext::DetermineIfSubtreeHasSelection() {
if (!ConnectedToView() || !document_->GetFrame()) {
SetRenderAffectingState(RenderAffectingState::kSubtreeHasSelection, false);
return;
}
auto range = ToEphemeralRangeInFlatTree(document_->GetFrame()
->Selection()
.GetSelectionInDOMTree()
.ComputeRange());
bool subtree_has_selection = false;
for (auto& node : range.Nodes()) {
for (auto& ancestor : FlatTreeTraversal::InclusiveAncestorsOf(node)) {
if (&ancestor == element_.Get()) {
subtree_has_selection = true;
break;
}
}
if (subtree_has_selection)
break;
}
SetRenderAffectingState(RenderAffectingState::kSubtreeHasSelection,
subtree_has_selection);
}
void DisplayLockContext::SetRenderAffectingState(RenderAffectingState state,
bool new_flag) {
// If we have forced activatable locks, it is possible that we're within
// find-in-page. We cannot lock an object while doing this, since it may
// invalidate layout and in turn prevent find-in-page from properly finding
// text (and DCHECK). Since layout is clean for this lock (we're unlocked),
// keep the context unlocked until the next lifecycle starts.
if (state == RenderAffectingState::kSubtreeHasSelection && !new_flag &&
document_->GetDisplayLockDocumentState()
.ActivatableDisplayLocksForced()) {
SetKeepUnlockedUntilLifecycleCount(1);
}
render_affecting_state_[static_cast<int>(state)] = new_flag;
NotifyRenderAffectingStateChanged();
}
void DisplayLockContext::NotifyRenderAffectingStateChanged() {
auto state = [this](RenderAffectingState state) {
return render_affecting_state_[static_cast<int>(state)];
};
// Check that we're visible if and only if lock has not been requested.
DCHECK_EQ(IsAlwaysVisible(), !state(RenderAffectingState::kLockRequested));
// We should be locked if the lock has been requested (the above DCHECKs
// verify that this means that we are not 'visible'), and any of the
// following is true:
// - We are not in 'auto' mode (meaning 'hidden') or
// - We have a non-null toggle_name_ (for 'toggle-visibility') or
// - We are in 'auto' mode and nothing blocks locking: viewport is
// not intersecting, subtree doesn't have focus, and subtree doesn't have
// selection, etc. See the condition for the full list.
bool should_be_locked =
state(RenderAffectingState::kLockRequested) &&
(state_ != EContentVisibility::kAuto || !toggle_name_.IsNull() ||
(!state(RenderAffectingState::kIntersectsViewport) &&
!state(RenderAffectingState::kSubtreeHasFocus) &&
!state(RenderAffectingState::kSubtreeHasSelection) &&
!state(RenderAffectingState::kAutoStateUnlockedUntilLifecycle) &&
!state(RenderAffectingState::kAutoUnlockedForPrint) &&
!state(RenderAffectingState::kSubtreeHasTopLayerElement) &&
!state(RenderAffectingState::kDescendantIsViewTransitionElement)));
if (should_be_locked && !IsLocked())
Lock();
else if (!should_be_locked && IsLocked())
Unlock();
}
void DisplayLockContext::Trace(Visitor* visitor) const {
visitor->Trace(element_);
visitor->Trace(document_);
ElementRareDataField::Trace(visitor);
}
void DisplayLockContext::SetShouldUnlockAutoForPrint(bool flag) {
SetRenderAffectingState(RenderAffectingState::kAutoUnlockedForPrint, flag);
}
const char* DisplayLockContext::RenderAffectingStateName(int state) const {
switch (static_cast<RenderAffectingState>(state)) {
case RenderAffectingState::kLockRequested:
return "LockRequested";
case RenderAffectingState::kIntersectsViewport:
return "IntersectsViewport";
case RenderAffectingState::kSubtreeHasFocus:
return "SubtreeHasFocus";
case RenderAffectingState::kSubtreeHasSelection:
return "SubtreeHasSelection";
case RenderAffectingState::kAutoStateUnlockedUntilLifecycle:
return "AutoStateUnlockedUntilLifecycle";
case RenderAffectingState::kAutoUnlockedForPrint:
return "AutoUnlockedForPrint";
case RenderAffectingState::kSubtreeHasTopLayerElement:
return "SubtreeHasTopLayerElement";
case RenderAffectingState::kDescendantIsViewTransitionElement:
return "DescendantIsViewTransitionElement";
case RenderAffectingState::kNumRenderAffectingStates:
break;
}
return "<Invalid State>";
}
String DisplayLockContext::RenderAffectingStateToString() const {
StringBuilder builder;
for (int i = 0;
i < static_cast<int>(RenderAffectingState::kNumRenderAffectingStates);
++i) {
builder.Append(RenderAffectingStateName(i));
builder.Append(": ");
builder.Append(render_affecting_state_[i] ? "true" : "false");
builder.Append("\n");
}
return builder.ToString();
}
void DisplayLockContext::StashScrollOffsetIfAvailable() {
if (auto* area = GetScrollableArea(element_)) {
const ScrollOffset& offset = area->GetScrollOffset();
// Only store the offset if it's non-zero. This is because scroll
// restoration has a small performance implication and restoring to a zero
// offset is the same as not restoring it.
if (!offset.IsZero())
stashed_scroll_offset_.emplace(offset);
}
}
void DisplayLockContext::RestoreScrollOffsetIfStashed() {
if (!stashed_scroll_offset_.has_value())
return;
// Restore the offset and reset the value.
if (auto* area = GetScrollableArea(element_)) {
area->SetScrollOffset(*stashed_scroll_offset_,
mojom::blink::ScrollType::kAnchoring);
stashed_scroll_offset_.reset();
}
}
bool DisplayLockContext::HasStashedScrollOffset() const {
return stashed_scroll_offset_.has_value();
}
} // namespace blink