blob: fe3fc4efc010f1c0df6476b1ca8b82a08eaadc4d [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "cc/trees/hit_test_data_builder.h"
#include <cstddef>
#include <cstdint>
#include <optional>
#include <utility>
#include "base/check_op.h"
#include "base/containers/adapters.h"
#include "base/memory/raw_ref.h"
#include "cc/base/math_util.h"
#include "cc/base/region.h"
#include "cc/layers/layer_impl.h"
#include "cc/layers/surface_layer_impl.h"
#include "cc/trees/layer_tree_impl.h"
#include "cc/trees/property_tree.h"
#include "components/viz/common/hit_test/hit_test_region_list.h"
#include "components/viz/common/surfaces/frame_sink_id.h"
#include "components/viz/common/surfaces/surface_id.h"
#include "third_party/skia/include/core/SkScalar.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rrect_f.h"
#include "ui/gfx/geometry/transform.h"
namespace cc {
namespace {
// In `HitTestDataBuilder::Build` we iterate all layers to find all layers that
// overlap OOPIFs, but when the number of layers is greater than
// |kAssumeOverlapThreshold|, it can be inefficient to accumulate layer bounds
// for overlap checking. As a result, we are conservative and make OOPIFs
// kHitTestAsk after the threshold is reached.
constexpr size_t kAssumeOverlapThreshold = 100;
gfx::Rect MapLayerBoundsToScreen(const LayerImpl* layer) {
return MathUtil::MapEnclosingClippedRect(layer->ScreenSpaceTransform(),
gfx::Rect(layer->bounds()));
}
uint32_t GetFlagsForSurfaceLayer(const SurfaceLayerImpl* layer) {
uint32_t flags = viz::HitTestRegionFlags::kHitTestMouse |
viz::HitTestRegionFlags::kHitTestTouch;
if (layer->range().IsValid()) {
flags |= viz::HitTestRegionFlags::kHitTestChildSurface;
} else {
flags |= viz::HitTestRegionFlags::kHitTestMine;
}
if (layer->has_pointer_events_none()) {
flags |= viz::HitTestRegionFlags::kHitTestIgnore;
}
return flags;
}
struct SurfaceHitTestGeometry {
// The hit test rect of the surface with the device scale factor applied.
gfx::RRectF hit_test_rect;
// True if the geometry requires async hit testing.
bool requires_async_hit_test;
};
// Helper function that computes hit test geometry for `surface_layer` and
// determines whether its clip requires async hit testing.
SurfaceHitTestGeometry ComputeSurfaceHitTestGeometry(
const SurfaceLayerImpl& surface_layer,
const EffectTree& effect_tree,
float device_scale_factor) {
// Using the enclosing rect to ensure antialiased boundary pixels cause
// pointer input to be routed to this layer.
gfx::RRectF hit_test_rect(gfx::ScaleToEnclosingRect(
gfx::Rect(surface_layer.bounds()), device_scale_factor));
bool layer_hit_test_region_is_masked =
effect_tree.HitTestMayBeAffectedByMask(surface_layer.effect_tree_index());
if (surface_layer.is_clipped() || layer_hit_test_region_is_masked) {
bool layer_hit_test_region_is_rectangle =
!layer_hit_test_region_is_masked &&
surface_layer.ScreenSpaceTransform().Preserves2dAxisAlignment() &&
effect_tree.ClippedHitTestRegionIsRectangle(
surface_layer.effect_tree_index());
hit_test_rect = gfx::RRectF(
gfx::ScaleToEnclosingRect(surface_layer.visible_layer_rect(),
device_scale_factor, device_scale_factor));
return {hit_test_rect, !layer_hit_test_region_is_rectangle};
}
return {hit_test_rect, false};
}
void PopulateHitTestRegion(viz::HitTestRegion* hit_test_region,
const LayerImpl* layer,
uint32_t flags,
uint32_t async_hit_test_reasons,
const gfx::RRectF& rect,
const viz::SurfaceId& surface_id,
float device_scale_factor) {
hit_test_region->frame_sink_id = surface_id.frame_sink_id();
hit_test_region->flags = flags;
hit_test_region->async_hit_test_reasons = async_hit_test_reasons;
DCHECK_EQ(!!async_hit_test_reasons,
!!(flags & viz::HitTestRegionFlags::kHitTestAsk));
hit_test_region->rect = rect;
// The transform of hit test region maps a point from parent hit test region
// to the local space. This is the inverse of screen space transform. Because
// hit test query wants the point in target to be in Pixel space, we
// counterscale the transform here. Note that the rect is scaled by dsf, so
// the point and the rect are still in the same space.
gfx::Transform surface_to_root_transform = layer->ScreenSpaceTransform();
surface_to_root_transform.Scale(SK_Scalar1 / device_scale_factor,
SK_Scalar1 / device_scale_factor);
surface_to_root_transform.Flatten();
// TODO(sunxd): Avoid losing precision by not using inverse if possible.
// Note: |transform| is set to the identity if |surface_to_root_transform| is
// not invertible, which is what we want.
hit_test_region->transform = surface_to_root_transform.InverseOrIdentity();
}
} // namespace
HitTestDataBuilder::HitTestDataBuilder(const LayerTreeImpl& active_tree)
: active_tree_(active_tree) {}
std::optional<viz::HitTestRegionList> HitTestDataBuilder::Build() && {
std::optional<viz::HitTestRegionList> hit_test_region_list(std::in_place);
hit_test_region_list->flags = viz::HitTestRegionFlags::kHitTestMine |
viz::HitTestRegionFlags::kHitTestMouse |
viz::HitTestRegionFlags::kHitTestTouch;
hit_test_region_list->bounds = active_tree_->GetDeviceViewport();
hit_test_region_list->transform = active_tree_->DrawTransform();
const float device_scale_factor = active_tree_->device_scale_factor();
const EffectTree& effect_tree = active_tree_->property_trees()->effect_tree();
for (const auto* layer : base::Reversed(*active_tree_)) {
const SurfaceLayerImpl* surface_layer = EvaluateLayerAndTrackOverlap(layer);
if (!surface_layer) {
continue;
}
uint32_t flags = GetFlagsForSurfaceLayer(surface_layer);
uint32_t async_hit_test_reasons =
viz::AsyncHitTestReasons::kNotAsyncHitTest;
if (IsSurfaceOverlapped(surface_layer)) {
flags |= viz::HitTestRegionFlags::kHitTestAsk;
async_hit_test_reasons |= viz::AsyncHitTestReasons::kOverlappedRegion;
}
const SurfaceHitTestGeometry geometry = ComputeSurfaceHitTestGeometry(
*surface_layer, effect_tree, device_scale_factor);
if (geometry.requires_async_hit_test) {
flags |= viz::HitTestRegionFlags::kHitTestAsk;
async_hit_test_reasons |= viz::AsyncHitTestReasons::kIrregularClip;
}
const auto& surface_id = surface_layer->range().end();
hit_test_region_list->regions.emplace_back();
PopulateHitTestRegion(&hit_test_region_list->regions.back(), layer, flags,
async_hit_test_reasons, geometry.hit_test_rect,
surface_id, device_scale_factor);
}
return hit_test_region_list;
}
const SurfaceLayerImpl* HitTestDataBuilder::EvaluateLayerAndTrackOverlap(
const LayerImpl* layer) {
if (!layer->is_surface_layer()) {
if (layer->HitTestable()) {
TrackHitTestableNonSurfaceLayer(layer);
}
return nullptr;
}
const auto* surface_layer = static_cast<const SurfaceLayerImpl*>(layer);
// We should not skip a non-hit-testable surface layer if
// - it has pointer-events: none because viz hit test needs to know the
// information to ensure all descendant OOPIFs to ignore hit tests; or
// - it draws content to track overlaps.
if (!layer->HitTestable() && !layer->draws_content() &&
!surface_layer->has_pointer_events_none()) {
return nullptr;
}
// If a surface layer is created not by child frame compositor or the frame
// owner has pointer-events: none property, the surface layer becomes not
// hit testable. We should not generate data for it.
if (!surface_layer->surface_hit_testable() ||
!surface_layer->range().IsValid()) {
// Track overlapping regions that do not have pointer-events: none.
if (!surface_layer->has_pointer_events_none()) {
TrackNonEmittedSurface(surface_layer);
}
return nullptr;
}
return surface_layer;
}
void HitTestDataBuilder::TrackHitTestableNonSurfaceLayer(
const LayerImpl* layer) {
++num_hit_testable_non_surface_layers_;
if (!ShouldAssumeOverlap()) {
overlapping_region_.Union(MapLayerBoundsToScreen(layer));
}
}
void HitTestDataBuilder::TrackNonEmittedSurface(
const SurfaceLayerImpl* surface_layer) {
if (!ShouldAssumeOverlap()) {
overlapping_region_.Union(MapLayerBoundsToScreen(surface_layer));
}
}
bool HitTestDataBuilder::IsSurfaceOverlapped(
const SurfaceLayerImpl* surface_layer) const {
return ShouldAssumeOverlap() ||
overlapping_region_.Intersects(MapLayerBoundsToScreen(surface_layer));
}
bool HitTestDataBuilder::ShouldAssumeOverlap() const {
return num_hit_testable_non_surface_layers_ > kAssumeOverlapThreshold;
}
} // namespace cc