| // Copyright 2016 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/intersection_observer/intersection_observation.h" |
| |
| #include "third_party/blink/renderer/core/dom/node_rare_data.h" |
| #include "third_party/blink/renderer/core/frame/local_frame.h" |
| #include "third_party/blink/renderer/core/intersection_observer/element_intersection_observer_data.h" |
| #include "third_party/blink/renderer/core/intersection_observer/intersection_geometry.h" |
| #include "third_party/blink/renderer/core/intersection_observer/intersection_observer.h" |
| #include "third_party/blink/renderer/core/intersection_observer/intersection_observer_controller.h" |
| #include "third_party/blink/renderer/core/intersection_observer/intersection_observer_entry.h" |
| #include "third_party/blink/renderer/core/layout/hit_test_result.h" |
| #include "third_party/blink/renderer/core/layout/layout_box.h" |
| #include "third_party/blink/renderer/core/layout/layout_view.h" |
| #include "third_party/blink/renderer/core/paint/paint_layer.h" |
| |
| #define CHECK_SKIPPED_UPDATE_ON_SCROLL() DCHECK_IS_ON() |
| |
| namespace blink { |
| |
| IntersectionObservation::IntersectionObservation(IntersectionObserver& observer, |
| Element& target) |
| : observer_(observer), target_(&target) {} |
| |
| int64_t IntersectionObservation::ComputeIntersection( |
| unsigned compute_flags, |
| gfx::Vector2dF accumulated_scroll_delta_since_last_update, |
| ComputeIntersectionsContext& context) { |
| if (!CanCompute()) { |
| return 0; |
| } |
| if (compute_flags & kConsumeScrollDelta) { |
| cached_rects_.min_scroll_delta_to_update -= |
| accumulated_scroll_delta_since_last_update; |
| accumulated_scroll_delta_since_last_update = gfx::Vector2dF(); |
| } |
| |
| // If we're processing post-layout deliveries only and we don't have a |
| // post-layout delivery observer, then return early. Likewise, return if we |
| // need to compute non-post-layout-delivery observations but the observer |
| // behavior is post-layout. |
| bool post_layout_delivery_only = compute_flags & kPostLayoutDeliveryOnly; |
| bool is_post_layout_delivery_observer = |
| Observer()->GetDeliveryBehavior() == |
| IntersectionObserver::kDeliverDuringPostLayoutSteps; |
| if (post_layout_delivery_only != is_post_layout_delivery_observer) { |
| return 0; |
| } |
| |
| bool has_pending_update = needs_update_; |
| if (compute_flags & |
| (observer_->RootIsImplicit() ? kImplicitRootObserversNeedUpdate |
| : kExplicitRootObserversNeedUpdate)) { |
| needs_update_ = true; |
| } |
| |
| if (!CanCompute() || !ShouldCompute(compute_flags)) { |
| return 0; |
| } |
| if (MaybeDelayAndReschedule(compute_flags, context)) { |
| return 0; |
| } |
| |
| last_run_time_ = context.GetMonotonicTime(); |
| needs_update_ = false; |
| |
| #if CHECK_SKIPPED_UPDATE_ON_SCROLL() |
| std::optional<IntersectionGeometry::CachedRects> cached_rects_backup; |
| #endif |
| if (!has_pending_update && (compute_flags & kScrollAndVisibilityOnly) && |
| cached_rects_.min_scroll_delta_to_update.x() > |
| accumulated_scroll_delta_since_last_update.x() && |
| cached_rects_.min_scroll_delta_to_update.y() > |
| accumulated_scroll_delta_since_last_update.y()) { |
| #if CHECK_SKIPPED_UPDATE_ON_SCROLL() |
| cached_rects_backup.emplace(cached_rects_); |
| #else |
| // This is equivalent to a full update. |
| return 1; |
| #endif |
| } |
| |
| unsigned geometry_flags = GetIntersectionGeometryFlags(compute_flags); |
| // The policy for honoring margins is the same as that for reporting root |
| // bounds, so this flag can be used for both. |
| bool honor_margins = |
| geometry_flags & IntersectionGeometry::kShouldReportRootBounds; |
| Vector<Length> empty_margin; |
| IntersectionGeometry geometry( |
| observer_->root(), *Target(), |
| honor_margins ? observer_->RootMargin() : empty_margin, |
| observer_->thresholds(), |
| honor_margins ? observer_->TargetMargin() : empty_margin, |
| honor_margins ? observer_->ScrollMargin() : empty_margin, geometry_flags, |
| context.GetRootGeometry(*observer_, compute_flags), &cached_rects_); |
| |
| #if CHECK_SKIPPED_UPDATE_ON_SCROLL() |
| if (cached_rects_backup) { |
| // A skipped update on scroll should generate the same result. |
| CHECK_EQ(last_threshold_index_, geometry.ThresholdIndex()); |
| CHECK_EQ(last_is_visible_, geometry.IsVisible()); |
| cached_rects_ = cached_rects_backup.value(); |
| return 1; |
| } |
| #endif |
| |
| ProcessIntersectionGeometry(geometry, context); |
| return geometry.DidComputeGeometry() ? 1 : 0; |
| } |
| |
| gfx::Vector2dF IntersectionObservation::MinScrollDeltaToUpdate() const { |
| if (cached_rects_.valid) { |
| return cached_rects_.min_scroll_delta_to_update; |
| } |
| return gfx::Vector2dF(); |
| } |
| |
| void IntersectionObservation::TakeRecords( |
| HeapVector<Member<IntersectionObserverEntry>>& entries) { |
| entries.append_range(entries_); |
| entries_.clear(); |
| } |
| |
| void IntersectionObservation::Disconnect() { |
| DCHECK(Observer()); |
| if (target_) { |
| DCHECK(target_->IntersectionObserverData()); |
| ElementIntersectionObserverData* observer_data = |
| target_->IntersectionObserverData(); |
| observer_data->RemoveObservation(*this); |
| IntersectionObserverController* controller = |
| target_->GetDocument().GetIntersectionObserverController(); |
| if (controller) { |
| controller->RemoveTrackedObservation(*this); |
| } |
| } |
| entries_.clear(); |
| observer_.Clear(); |
| } |
| |
| void IntersectionObservation::Trace(Visitor* visitor) const { |
| visitor->Trace(observer_); |
| visitor->Trace(entries_); |
| visitor->Trace(target_); |
| } |
| |
| bool IntersectionObservation::CanUseCachedRectsForTesting( |
| bool scroll_and_visibility_only) const { |
| // This is to avoid the side effects of IntersectionGeometry. |
| IntersectionGeometry::CachedRects cached_rects_copy = cached_rects_; |
| |
| std::optional<IntersectionGeometry::RootGeometry> root_geometry; |
| IntersectionGeometry geometry( |
| observer_->root(), *target_, |
| /* root_margin */ {}, |
| /* thresholds */ {0}, |
| /* target_margin */ {}, |
| /* scroll_margin */ {}, |
| scroll_and_visibility_only |
| ? IntersectionGeometry::kScrollAndVisibilityOnly |
| : 0, |
| root_geometry, &cached_rects_copy); |
| |
| return geometry.CanUseCachedRectsForTesting(); |
| } |
| |
| bool IntersectionObservation::CanCompute() const { |
| return !!target_ && !!observer_ && observer_->RootIsValid() && |
| observer_->GetExecutionContext(); |
| } |
| |
| bool IntersectionObservation::ShouldCompute(unsigned flags) const { |
| if (!needs_update_) { |
| return false; |
| } |
| if (target_->isConnected() && target_->GetDocument().GetFrame() && |
| Observer()->trackVisibility()) { |
| mojom::blink::FrameOcclusionState occlusion_state = |
| target_->GetDocument().GetFrame()->GetOcclusionState(); |
| // If we're tracking visibility, and we aren't currently reporting the |
| // target visible, and we don't have occlusion information from our parent |
| // frame, then postpone computing intersections until a later lifecycle when |
| // the occlusion information is known. |
| if (!last_is_visible_ && |
| occlusion_state == mojom::blink::FrameOcclusionState::kUnknown) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| bool IntersectionObservation::MaybeDelayAndReschedule( |
| unsigned flags, |
| ComputeIntersectionsContext& context) { |
| if (flags & kIgnoreDelay) { |
| return false; |
| } |
| if (last_run_time_.is_null()) { |
| return false; |
| } |
| base::TimeDelta delay = observer_->GetEffectiveDelay() - |
| (context.GetMonotonicTime() - last_run_time_); |
| if (delay.is_positive()) { |
| context.UpdateNextRunDelay(delay); |
| return true; |
| } |
| return false; |
| } |
| |
| unsigned IntersectionObservation::GetIntersectionGeometryFlags( |
| unsigned compute_flags) const { |
| bool report_root_bounds = observer_->AlwaysReportRootBounds() || |
| (compute_flags & kReportImplicitRootBounds) || |
| !observer_->RootIsImplicit(); |
| unsigned geometry_flags = IntersectionGeometry::kShouldConvertToCSSPixels; |
| if (report_root_bounds) |
| geometry_flags |= IntersectionGeometry::kShouldReportRootBounds; |
| if (Observer()->trackVisibility()) |
| geometry_flags |= IntersectionGeometry::kShouldComputeVisibility; |
| if (Observer()->ShouldExposeOccluderNodeId()) { |
| geometry_flags |= IntersectionGeometry::kShouldExposeOccluderNodeId; |
| } |
| if (Observer()->trackFractionOfRoot()) |
| geometry_flags |= IntersectionGeometry::kShouldTrackFractionOfRoot; |
| if (Observer()->UseOverflowClipEdge()) |
| geometry_flags |= IntersectionGeometry::kUseOverflowClipEdge; |
| if (Observer()->IsInternal()) { |
| // TODO(wangxianzhu): Let internal clients decide whether to respect |
| // filters. |
| geometry_flags |= IntersectionGeometry::kRespectFilters; |
| } |
| if (compute_flags & kScrollAndVisibilityOnly) { |
| geometry_flags |= IntersectionGeometry::kScrollAndVisibilityOnly; |
| } |
| return geometry_flags; |
| } |
| |
| void IntersectionObservation::ProcessIntersectionGeometry( |
| const IntersectionGeometry& geometry, |
| ComputeIntersectionsContext& context) { |
| CHECK_LT(geometry.ThresholdIndex(), kNotFound); |
| |
| if (last_threshold_index_ != geometry.ThresholdIndex() || |
| last_is_visible_ != geometry.IsVisible()) { |
| entries_.push_back(MakeGarbageCollected<IntersectionObserverEntry>( |
| geometry, context.GetTimeStamp(*Observer()), Target())); |
| Observer()->ReportUpdates(*this); |
| last_threshold_index_ = geometry.ThresholdIndex(); |
| last_is_visible_ = geometry.IsVisible(); |
| } |
| } |
| |
| } // namespace blink |