| // 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. |
| |
| #ifndef THIRD_PARTY_BLINK_RENDERER_CORE_DISPLAY_LOCK_DISPLAY_LOCK_CONTEXT_H_ |
| #define THIRD_PARTY_BLINK_RENDERER_CORE_DISPLAY_LOCK_DISPLAY_LOCK_CONTEXT_H_ |
| |
| #include <utility> |
| |
| #include "third_party/blink/renderer/core/core_export.h" |
| #include "third_party/blink/renderer/core/css/style_engine.h" |
| #include "third_party/blink/renderer/core/css/style_recalc_change.h" |
| #include "third_party/blink/renderer/core/dom/element_rare_data_field.h" |
| #include "third_party/blink/renderer/core/frame/local_frame_view.h" |
| #include "third_party/blink/renderer/core/scroll/scroll_types.h" |
| #include "third_party/blink/renderer/core/style/computed_style_base_constants.h" |
| #include "third_party/blink/renderer/platform/runtime_enabled_features.h" |
| #include "third_party/blink/renderer/platform/scheduler/public/post_cancellable_task.h" |
| #include "third_party/blink/renderer/platform/wtf/allocator/allocator.h" |
| #include "third_party/blink/renderer/platform/wtf/text/wtf_string.h" |
| |
| namespace blink { |
| |
| class Document; |
| class Element; |
| |
| enum class DisplayLockActivationReason { |
| // Accessibility driven activation |
| kAccessibility = 1 << 0, |
| // Activation as a result of find-in-page |
| kFindInPage = 1 << 1, |
| // Fragment link navigation |
| kFragmentNavigation = 1 << 2, |
| // Script invoked focus(). |
| kScriptFocus = 1 << 3, |
| // scrollIntoView() |
| kScrollIntoView = 1 << 4, |
| // User / script selection |
| kSelection = 1 << 5, |
| // Simulated click (Node::DispatchSimulatedClick) |
| kSimulatedClick = 1 << 6, |
| // User focus (e.g. tab navigation) |
| kUserFocus = 1 << 7, |
| // Intersection observer activation |
| kViewportIntersection = 1 << 8, |
| |
| // Shorthands |
| kViewport = static_cast<uint16_t>(kSelection) | |
| static_cast<uint16_t>(kUserFocus) | |
| static_cast<uint16_t>(kViewportIntersection) | |
| static_cast<uint16_t>(kAccessibility), |
| kAny = static_cast<uint16_t>(kAccessibility) | |
| static_cast<uint16_t>(kFindInPage) | |
| static_cast<uint16_t>(kFragmentNavigation) | |
| static_cast<uint16_t>(kScriptFocus) | |
| static_cast<uint16_t>(kScrollIntoView) | |
| static_cast<uint16_t>(kSelection) | |
| static_cast<uint16_t>(kSimulatedClick) | |
| static_cast<uint16_t>(kUserFocus) | |
| static_cast<uint16_t>(kViewportIntersection), |
| }; |
| |
| // Instead of specifying an underlying type, which would propagate throughout |
| // forward declarations, we static assert that the activation reasons enum is |
| // small-ish. |
| static_assert(static_cast<uint32_t>(DisplayLockActivationReason::kAny) < |
| std::numeric_limits<uint16_t>::max(), |
| "DisplayLockActivationReason is too large"); |
| |
| class CORE_EXPORT DisplayLockContext final |
| : public GarbageCollected<DisplayLockContext>, |
| public LocalFrameView::LifecycleNotificationObserver, |
| public ElementRareDataField { |
| public: |
| // Note the order of the phases matters. Each phase implies all previous ones |
| // as well. |
| enum class ForcedPhase { kNone, kStyleAndLayoutTree, kLayout, kPrePaint }; |
| |
| explicit DisplayLockContext(Element*); |
| ~DisplayLockContext() = default; |
| |
| // Called by style to update the current state of content-visibility. |
| void SetRequestedState(EContentVisibility state); |
| // Called by style to adjust the element's style based on the current state. |
| const ComputedStyle* AdjustElementStyle(const ComputedStyle*) const; |
| |
| // Is called by the intersection observer callback to inform us of the |
| // intersection state. |
| void NotifyIsIntersectingViewport(); |
| void NotifyIsNotIntersectingViewport(); |
| |
| // Lifecycle state functions. |
| ALWAYS_INLINE bool ShouldStyleChildren() const { |
| return !is_locked_ || |
| forced_info_.is_forced(ForcedPhase::kStyleAndLayoutTree) || |
| (IsActivatable(DisplayLockActivationReason::kAny) && |
| ActivatableDisplayLocksForced()) || |
| (IsActivatable(DisplayLockActivationReason::kAccessibility) && |
| document_->ExistingAXObjectCache()); |
| } |
| |
| void DidStyleSelf(); |
| void DidStyleChildren(); |
| ALWAYS_INLINE bool ShouldLayoutChildren() const { |
| return !is_locked_ || forced_info_.is_forced(ForcedPhase::kLayout) || |
| (IsActivatable(DisplayLockActivationReason::kAny) && |
| ActivatableDisplayLocksForced()) || |
| (IsActivatable(DisplayLockActivationReason::kAccessibility) && |
| document_->ExistingAXObjectCache() && |
| document_->GetStyleEngine().SkippedContainerRecalc()); |
| } |
| void DidLayoutChildren(); |
| ALWAYS_INLINE bool ShouldPrePaintChildren() const { |
| return !is_locked_ || forced_info_.is_forced(ForcedPhase::kPrePaint) || |
| (IsActivatable(DisplayLockActivationReason::kAny) && |
| ActivatableDisplayLocksForced()); |
| } |
| ALWAYS_INLINE bool ShouldPaintChildren() const { return !is_locked_; } |
| |
| // Returns true if the last style recalc traversal was blocked at this |
| // element. |
| bool StyleTraversalWasBlocked() const { |
| return !blocked_child_recalc_change_.IsEmpty(); |
| } |
| |
| // Returns true if the contents of the associated element should be visible |
| // from and activatable by a specified reason. Note that passing |
| // kAny will return true if the lock is activatable for any |
| // reason. |
| ALWAYS_INLINE bool IsActivatable(DisplayLockActivationReason reason) const { |
| return activatable_mask_ & static_cast<uint16_t>(reason); |
| } |
| |
| // Trigger commit because of activation from tab order, url fragment, |
| // find-in-page, scrolling, etc. |
| // The reason is specified for metrics. |
| void CommitForActivation(DisplayLockActivationReason reason); |
| |
| bool ShouldCommitForActivation(DisplayLockActivationReason reason) const; |
| |
| // Returns true if this context is locked. |
| bool IsLocked() const { return is_locked_; } |
| |
| EContentVisibility GetState() { return state_; } |
| |
| // This is called when the element with which this context is associated is |
| // moved to a new document. Used to listen to the lifecycle update from the |
| // right document's view. |
| void DidMoveToNewDocument(Document& old_document); |
| |
| // LifecycleNotificationObserver overrides. |
| void WillStartLifecycleUpdate(const LocalFrameView&) override; |
| |
| // Inform the display lock that it prevented a style change. This is used to |
| // invalidate style when we need to update it in the future. |
| void NotifyChildStyleRecalcWasBlocked(const StyleRecalcChange& change) { |
| blocked_child_recalc_change_ = blocked_child_recalc_change_.Combine(change); |
| } |
| |
| StyleRecalcChange TakeBlockedStyleRecalcChange() { |
| return std::exchange(blocked_child_recalc_change_, StyleRecalcChange()); |
| } |
| |
| void NotifyReattachLayoutTreeWasBlocked() { |
| blocked_child_recalc_change_ = |
| blocked_child_recalc_change_.ForceReattachLayoutTree(); |
| } |
| |
| void NotifyChildLayoutWasBlocked() { child_layout_was_blocked_ = true; } |
| |
| void NotifyCompositingDescendantDependentFlagUpdateWasBlocked() { |
| needs_compositing_dependent_flag_update_ = true; |
| } |
| |
| // Notify this element will be disconnected. |
| void NotifyWillDisconnect(); |
| |
| // Called when the element disconnects or connects. |
| void ElementDisconnected(); |
| void ElementConnected(); |
| |
| void DetachLayoutTree(); |
| |
| void NotifySubtreeLostFocus(); |
| void NotifySubtreeGainedFocus(); |
| |
| void NotifySubtreeLostSelection(); |
| void NotifySubtreeGainedSelection(); |
| |
| void SetNeedsPrePaintSubtreeWalk( |
| bool needs_effective_allowed_touch_action_update, |
| bool needs_blocking_wheel_event_handler_update) { |
| needs_effective_allowed_touch_action_update_ = |
| needs_effective_allowed_touch_action_update; |
| needs_blocking_wheel_event_handler_update_ = |
| needs_blocking_wheel_event_handler_update; |
| needs_prepaint_subtree_walk_ = true; |
| } |
| |
| void DidForceActivatableDisplayLocks() { |
| if (IsLocked() && IsActivatable(DisplayLockActivationReason::kAny)) { |
| MarkForStyleRecalcIfNeeded(); |
| MarkForLayoutIfNeeded(); |
| MarkAncestorsForPrePaintIfNeeded(); |
| } |
| } |
| |
| bool HadAnyViewportIntersectionNotifications() const { |
| return had_any_viewport_intersection_notifications_; |
| } |
| |
| // GC functions. |
| void Trace(Visitor*) const override; |
| |
| // Debugging functions. |
| String RenderAffectingStateToString() const; |
| |
| bool IsAuto() const { return state_ == EContentVisibility::kAuto; } |
| bool HadLifecycleUpdateSinceLastUnlock() const { |
| return had_lifecycle_update_since_last_unlock_; |
| } |
| |
| // We unlock auto locks for printing, which is set here. |
| void SetShouldUnlockAutoForPrint(bool); |
| |
| void SetIsHiddenUntilFoundElement(bool is_hidden_until_found) { |
| is_hidden_until_found_ = is_hidden_until_found; |
| } |
| |
| void SetIsDetailsSlotElement(bool is_details_slot) { |
| is_details_slot_ = is_details_slot; |
| } |
| |
| bool HasElement() const { return element_ != nullptr; } |
| |
| // Top layer implementation. |
| void NotifyHasTopLayerElement(); |
| void ClearHasTopLayerElement(); |
| |
| void ScheduleTopLayerCheck(); |
| |
| // State control for view transition element render affecting state. |
| void ResetDescendantIsViewTransitionElement(); |
| void SetDescendantIsViewTransitionElement(); |
| |
| private: |
| // Give access to |NotifyForcedUpdateScopeStarted()| and |
| // |NotifyForcedUpdateScopeEnded()|. |
| friend class DisplayLockUtilities; |
| |
| // Test friends. |
| friend class DisplayLockContextRenderingTest; |
| friend class DisplayLockContextTest; |
| |
| // Request that this context be locked. Called when style determines that the |
| // subtree rooted at this element should be skipped, unless things like |
| // viewport intersection prevent it from doing so. |
| void RequestLock(uint16_t activation_mask); |
| // Request that this context be unlocked. Called when style determines that |
| // the subtree rooted at this element should be rendered. |
| void RequestUnlock(); |
| |
| // Called in |DisplayLockUtilities| to notify the state of scope. |
| void NotifyForcedUpdateScopeStarted(ForcedPhase phase, bool emit_warnings) { |
| UpgradeForcedScope(ForcedPhase::kNone, phase, emit_warnings); |
| } |
| void NotifyForcedUpdateScopeEnded(ForcedPhase phase); |
| void UpgradeForcedScope(ForcedPhase old_phase, |
| ForcedPhase new_phase, |
| bool emit_warnings); |
| |
| // Records the locked context counts on the document as well as context that |
| // block all activation. |
| void UpdateDocumentBookkeeping(bool was_locked, |
| bool all_activation_was_blocked, |
| bool is_locked, |
| bool all_activation_is_blocked); |
| |
| // Set which reasons activate, as a mask of DisplayLockActivationReason enums. |
| void UpdateActivationMask(uint16_t activatable_mask); |
| |
| // Clear the activated flag. |
| void ResetActivation(); |
| |
| // Returns true if activatable display locks are being currently forced. |
| bool ActivatableDisplayLocksForced() const; |
| |
| // The following functions propagate dirty bits from the locked element up to |
| // the ancestors in order to be reached, and update dirty bits for the element |
| // as well if needed. They return true if the element or its subtree were |
| // dirty, and false otherwise. |
| bool MarkForStyleRecalcIfNeeded(); |
| bool MarkForLayoutIfNeeded(); |
| bool MarkAncestorsForPrePaintIfNeeded(); |
| bool MarkNeedsRepaintAndPaintArtifactCompositorUpdate(); |
| bool MarkNeedsCullRectUpdate(); |
| bool MarkForCompositingUpdatesIfNeeded(); |
| |
| bool IsElementDirtyForStyleRecalc() const; |
| bool IsElementDirtyForLayout() const; |
| bool IsElementDirtyForPrePaint() const; |
| |
| // Helper to schedule an animation to delay lifecycle updates for the next |
| // frame. |
| void ScheduleAnimation(); |
| |
| // Checks whether we should force unlock the lock (due to not meeting |
| // containment/display requirements), returns a string from rejection_names |
| // if we should, nullptr if not. Note that this can only be called if the |
| // style is clean. It checks the layout object if it exists. Otherwise, |
| // falls back to checking computed style. |
| const char* ShouldForceUnlock() const; |
| |
| // Unlocks the lock if the element doesn't meet requirements |
| // (containment/display type). Returns true if we did unlock. |
| bool ForceUnlockIfNeeded(); |
| |
| // Returns true if the element is connected to a document that has a view. |
| // If we're not connected, or if we're connected but the document doesn't |
| // have a view (e.g. templates) we shouldn't do style calculations etc and |
| // when acquiring this lock should immediately resolve the acquire promise. |
| bool ConnectedToView() const; |
| |
| // Registers or unregisters the element for intersection observations in the |
| // document. This is used to activate on visibily changes. This can be safely |
| // called even if changes are not required, since it will only act if a |
| // register/unregister is required. |
| void UpdateActivationObservationIfNeeded(); |
| |
| // Determines whether or not we need lifecycle notifications. |
| bool NeedsLifecycleNotifications() const; |
| // Updates the lifecycle notification registration based on whether we need |
| // the notifications. |
| void UpdateLifecycleNotificationRegistration(); |
| |
| // Locks the context. |
| void Lock(); |
| // Unlocks the context. |
| void Unlock(); |
| |
| // Determines if the subtree has focus. This is a linear walk from the focused |
| // element to its root element. |
| void DetermineIfSubtreeHasFocus(); |
| |
| // Determines if the subtree has selection. This will walk from each of the |
| // selected notes up to its root looking for `element_`. |
| void DetermineIfSubtreeHasSelection(); |
| |
| // Determines if the subtree has a top layer element. This is a walk from each |
| // top layer node up the ancestor chain looking for `element_`. |
| void DetermineIfSubtreeHasTopLayerElement(); |
| |
| // Determines if there are view transition elements in the subtree of this |
| // element. |
| void DetermineIfDescendantIsViewTransitionElement(); |
| |
| // Detaching the layout tree from the top layers nested under this lock. |
| void DetachDescendantTopLayerElements(); |
| |
| // Keep this context unlocked until the beginning of lifecycle. Effectively |
| // keeps this context unlocked for the next `count` frames. It also schedules |
| // a frame to ensure the lifecycle happens. Only affects locks with 'auto' |
| // setting. |
| void SetKeepUnlockedUntilLifecycleCount(int count); |
| |
| // Returns true if the context can dirty element's style in the current |
| // processing. Note that this returns false if the document is doing a style |
| // recalc, or if we're currently setting a new requested state which happens |
| // in style adjustment. |
| bool CanDirtyStyle() const; |
| |
| // When a scroller becomes locked, we store off its current scroll offset, to |
| // avoid losing the offset when the scroller becomes unlocked in the future. |
| // The following functions enable this functionality. |
| void StashScrollOffsetIfAvailable(); |
| void RestoreScrollOffsetIfStashed(); |
| bool HasStashedScrollOffset() const; |
| |
| bool SubtreeHasTopLayerElement() const; |
| |
| void ScheduleStateChangeEventIfNeeded(); |
| void DispatchStateChangeEventIfNeeded(); |
| |
| WeakMember<Element> element_; |
| WeakMember<Document> document_; |
| EContentVisibility state_ = EContentVisibility::kVisible; |
| |
| // A struct to keep track of forced unlocks, and reasons for it. |
| struct UpdateForcedInfo { |
| bool is_forced(ForcedPhase phase) const { |
| switch (phase) { |
| case ForcedPhase::kNone: |
| NOTREACHED(); |
| return false; |
| case ForcedPhase::kStyleAndLayoutTree: |
| return style_update_forced_ || layout_update_forced_ || |
| prepaint_update_forced_; |
| case ForcedPhase::kLayout: |
| return layout_update_forced_ || prepaint_update_forced_; |
| case ForcedPhase::kPrePaint: |
| return prepaint_update_forced_; |
| } |
| } |
| |
| void start(ForcedPhase phase) { |
| switch (phase) { |
| case ForcedPhase::kNone: |
| break; |
| case ForcedPhase::kStyleAndLayoutTree: |
| ++style_update_forced_; |
| break; |
| case ForcedPhase::kLayout: |
| ++layout_update_forced_; |
| break; |
| case ForcedPhase::kPrePaint: |
| ++prepaint_update_forced_; |
| } |
| } |
| |
| void end(ForcedPhase phase) { |
| switch (phase) { |
| case ForcedPhase::kNone: |
| break; |
| case ForcedPhase::kStyleAndLayoutTree: |
| DCHECK(style_update_forced_); |
| --style_update_forced_; |
| break; |
| case ForcedPhase::kLayout: |
| DCHECK(layout_update_forced_); |
| --layout_update_forced_; |
| break; |
| case ForcedPhase::kPrePaint: |
| DCHECK(prepaint_update_forced_); |
| --prepaint_update_forced_; |
| } |
| } |
| |
| private: |
| // Each of the forced modes includes forcing phases before. For instance, |
| // layout_update_forced_ == 1 would also ensure that style and layout tree |
| // are up to date. |
| int style_update_forced_ = 0; |
| int layout_update_forced_ = 0; |
| int prepaint_update_forced_ = 0; |
| }; |
| |
| UpdateForcedInfo forced_info_; |
| |
| StyleRecalcChange blocked_child_recalc_change_; |
| |
| bool needs_effective_allowed_touch_action_update_ = false; |
| bool needs_blocking_wheel_event_handler_update_ = false; |
| bool needs_prepaint_subtree_walk_ = false; |
| bool needs_compositing_dependent_flag_update_ = false; |
| |
| // Will be true if child traversal was blocked on a previous layout run on the |
| // locked element. We need to keep track of this to ensure that on the next |
| // layout run where the descendants of the locked element are allowed to be |
| // traversed into, we will traverse to the children of the locked element. |
| bool child_layout_was_blocked_ = false; |
| |
| // Tracks whether the element associated with this lock is being tracked by a |
| // document level intersection observer. |
| bool is_observed_ = false; |
| |
| uint16_t activatable_mask_ = |
| static_cast<uint16_t>(DisplayLockActivationReason::kAny); |
| |
| // Is set to true if we are registered for lifecycle notifications. |
| bool is_registered_for_lifecycle_notifications_ = false; |
| |
| // This is set to true when we have delayed locking ourselves due to viewport |
| // intersection (or lack thereof) because we were nested in a locked subtree. |
| // In that case, we register for lifecycle notifications and check every time |
| // if we are still nested. |
| bool needs_deferred_not_intersecting_signal_ = false; |
| |
| // Lock has been requested. |
| bool is_locked_ = false; |
| |
| // If true, this lock is kept unlocked at least until the beginning of the |
| // lifecycle. If nothing else is keeping it unlocked, then it will be locked |
| // again at the start of the lifecycle. |
| bool keep_unlocked_until_lifecycle_ = false; |
| |
| // This is set to true if we're in the 'auto' mode and had our first |
| // intersection / non-intersection notification. This is reset to false if the |
| // 'auto' mode is added again (after being removed). |
| bool had_any_viewport_intersection_notifications_ = false; |
| |
| enum class RenderAffectingState : int { |
| kLockRequested, |
| kIntersectsViewport, |
| kSubtreeHasFocus, |
| kSubtreeHasSelection, |
| kAutoStateUnlockedUntilLifecycle, |
| kAutoUnlockedForPrint, |
| kSubtreeHasTopLayerElement, |
| kDescendantIsViewTransitionElement, |
| kNumRenderAffectingStates |
| }; |
| void SetRenderAffectingState(RenderAffectingState state, bool flag); |
| void NotifyRenderAffectingStateChanged(); |
| const char* RenderAffectingStateName(int state) const; |
| |
| bool render_affecting_state_[static_cast<int>( |
| RenderAffectingState::kNumRenderAffectingStates)] = {false}; |
| int keep_unlocked_count_ = 0; |
| |
| bool had_lifecycle_update_since_last_unlock_ = false; |
| |
| // Tracks whether we're updating requested state, which can only happen from |
| // the style adjuster. Note that this is different from a InStyleRecalc check |
| // since we can also force update style outside of this call (via ensure |
| // computed style). |
| bool set_requested_state_scope_ = false; |
| |
| std::optional<ScrollOffset> stashed_scroll_offset_; |
| |
| // When we use content-visibility:hidden for the <details> element's content |
| // slot or the hidden=until-found attribute, then this lock must activate |
| // during find-in-page. |
| bool is_details_slot_ = false; |
| |
| // When an element has the hidden=until-found attribute, it gets the a |
| // presentational style of content-visibility:hidden, and we also want to |
| // activate this lock during find-in-page. |
| bool is_hidden_until_found_ = false; |
| |
| // If we have pending subtree checks, it means we should check for selection |
| // and focus at the start of the next frame. |
| bool has_pending_subtree_checks_ = false; |
| |
| // If true, we need to clear the fact that we have a top layer at the start of |
| // the next frame. |
| bool has_pending_clear_has_top_layer_ = false; |
| |
| // If true, we need to check if this subtree has any top layer elements at the |
| // start of the next frame. |
| bool has_pending_top_layer_check_ = false; |
| |
| // This is set to the last value for which ContentVisibilityAutoStateChange |
| // event has been dispatched (if any). |
| std::optional<bool> last_notified_skipped_state_; |
| |
| // If true, there is a pending task that will dispatch a state change event if |
| // needed. |
| bool state_change_task_pending_ = false; |
| }; |
| |
| } // namespace blink |
| |
| #endif // THIRD_PARTY_BLINK_RENDERER_CORE_DISPLAY_LOCK_DISPLAY_LOCK_CONTEXT_H_ |