blob: 7fd07e0321382dc7ca0cd7669c05e14e31216a55 [file] [log] [blame]
// Copyright 2017 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 "ui/aura/window_occlusion_tracker.h"
#include "base/auto_reset.h"
#include "base/containers/adapters.h"
#include "base/debug/dump_without_crashing.h"
#include "base/stl_util.h"
#include "third_party/skia/include/core/SkRect.h"
#include "third_party/skia/include/core/SkRegion.h"
#include "ui/aura/window_tracker.h"
#include "ui/gfx/geometry/safe_integer_conversions.h"
#include "ui/gfx/transform.h"
namespace aura {
namespace {
// When one of these properties is animated, a window is considered non-occluded
// and cannot occlude other windows.
constexpr ui::LayerAnimationElement::AnimatableProperties
kSkipWindowWhenPropertiesAnimated =
ui::LayerAnimationElement::TRANSFORM |
ui::LayerAnimationElement::BOUNDS | ui::LayerAnimationElement::OPACITY;
// Maximum number of times that MaybeComputeOcclusion() should have to recompute
// occlusion states before they become stable.
//
// TODO(fdoray): This can be changed to 2 once showing/hiding a WebContents
// doesn't cause a call to Show()/Hide() on the aura::Window of a
// RenderWidgetHostViewAura. https://crbug.com/827268
constexpr int kMaxRecomputeOcclusion = 3;
WindowOcclusionTracker* g_tracker = nullptr;
int g_num_pause_occlusion_tracking = 0;
// Returns true if |window| opaquely fills its bounds. |window| must be visible.
bool VisibleWindowIsOpaque(Window* window) {
DCHECK(window->IsVisible());
DCHECK(window->layer());
return !window->transparent() &&
window->layer()->type() != ui::LAYER_NOT_DRAWN &&
window->layer()->GetCombinedOpacity() == 1.0f;
}
// Returns the transform of |window| relative to its root.
// |parent_transform_relative_to_root| is the transform of |window->parent()|
// relative to its root.
gfx::Transform GetWindowTransformRelativeToRoot(
aura::Window* window,
const gfx::Transform& parent_transform_relative_to_root) {
if (window->IsRootWindow())
return gfx::Transform();
// Concatenate |parent_transform_relative_to_root| to the result of
// GetTargetTransformRelativeTo(parent_layer) rather than simply calling
// GetTargetTransformRelativeTo(window->GetRootWindow()->layer()) to avoid
// doing the same computations multiple times when traversing a window
// hierarchy.
ui::Layer* parent_layer = window->parent()->layer();
gfx::Transform transform_relative_to_root;
bool success = window->layer()->GetTargetTransformRelativeTo(
parent_layer, &transform_relative_to_root);
DCHECK(success);
transform_relative_to_root.ConcatTransform(parent_transform_relative_to_root);
return transform_relative_to_root;
}
// Returns the bounds of |window| relative to its |root|.
// |transform_relative_to_root| is the transform of |window| relative to its
// root. If |clipped_bounds| is not null, the returned bounds are clipped by it.
SkIRect GetWindowBoundsInRootWindow(
aura::Window* window,
const gfx::Transform& transform_relative_to_root,
const SkIRect* clipped_bounds) {
DCHECK(transform_relative_to_root.Preserves2dAxisAlignment());
// Compute the unclipped bounds of |window|.
gfx::RectF bounds(0.0f, 0.0f, static_cast<float>(window->bounds().width()),
static_cast<float>(window->bounds().height()));
transform_relative_to_root.TransformRect(&bounds);
SkIRect skirect_bounds = SkIRect::MakeXYWH(
gfx::ToFlooredInt(bounds.x()), gfx::ToFlooredInt(bounds.y()),
gfx::ToFlooredInt(bounds.width()), gfx::ToFlooredInt(bounds.height()));
// If necessary, clip the bounds.
if (clipped_bounds && !skirect_bounds.intersect(*clipped_bounds))
return SkIRect::MakeEmpty();
return skirect_bounds;
}
// Returns true iff the occlusion states in |tracked_windows| match those
// returned by Window::occlusion_state().
bool OcclusionStatesMatch(
const base::flat_map<Window*, Window::OcclusionState>& tracked_windows) {
for (const auto& tracked_window : tracked_windows) {
if (tracked_window.second != tracked_window.first->occlusion_state())
return false;
}
return true;
}
} // namespace
WindowOcclusionTracker::ScopedPauseOcclusionTracking::
ScopedPauseOcclusionTracking() {
++g_num_pause_occlusion_tracking;
}
WindowOcclusionTracker::ScopedPauseOcclusionTracking::
~ScopedPauseOcclusionTracking() {
--g_num_pause_occlusion_tracking;
DCHECK_GE(g_num_pause_occlusion_tracking, 0);
if (g_tracker)
g_tracker->MaybeComputeOcclusion();
}
void WindowOcclusionTracker::Track(Window* window) {
DCHECK(window);
DCHECK(window != window->GetRootWindow());
if (!g_tracker)
g_tracker = new WindowOcclusionTracker();
auto insert_result = g_tracker->tracked_windows_.insert(
{window, Window::OcclusionState::UNKNOWN});
DCHECK(insert_result.second);
if (!window->HasObserver(g_tracker))
window->AddObserver(g_tracker);
if (window->GetRootWindow())
g_tracker->TrackedWindowAddedToRoot(window);
}
WindowOcclusionTracker::WindowOcclusionTracker() = default;
WindowOcclusionTracker::~WindowOcclusionTracker() = default;
WindowOcclusionTracker* WindowOcclusionTracker::GetInstance() {
DCHECK(g_tracker);
return g_tracker;
}
void WindowOcclusionTracker::MaybeComputeOcclusion() {
if (g_num_pause_occlusion_tracking ||
num_times_occlusion_recomputed_in_current_step_ != 0) {
return;
}
base::AutoReset<int> auto_reset(
&num_times_occlusion_recomputed_in_current_step_, 0);
// Recompute occlusion states until either:
// - They are stable, i.e. calling Window::SetOcclusionState() on all tracked
// windows does not provoke changes that could affect occlusion.
// - Occlusion states have been recomputed
// |kMaxComputeOcclusionIterationsBeforeStable|
// times.
// If occlusion states have been recomputed
// |kMaxComputeOcclusionIterationsBeforeStable| times and are still not
// stable, iterate one last time to set the occlusion state of all tracked
// windows based on IsVisible().
while (num_times_occlusion_recomputed_in_current_step_ <=
kMaxRecomputeOcclusion) {
const bool exceeded_max_num_times_occlusion_recomputed =
num_times_occlusion_recomputed_in_current_step_ ==
kMaxRecomputeOcclusion;
bool found_dirty_root = false;
// Compute occlusion states and store them in |tracked_windows_|. Do not
// call Window::SetOcclusionState() in this phase to prevent changes to the
// window tree while it is being traversed.
for (auto& root_window_pair : root_windows_) {
if (root_window_pair.second.dirty) {
found_dirty_root = true;
root_window_pair.second.dirty = false;
if (!exceeded_max_num_times_occlusion_recomputed) {
SkRegion occluded_region;
RecomputeOcclusionImpl(root_window_pair.first, gfx::Transform(),
nullptr, &occluded_region);
}
}
}
if (!found_dirty_root)
break;
++num_times_occlusion_recomputed_;
++num_times_occlusion_recomputed_in_current_step_;
// Call Window::SetOcclusionState() on tracked windows. A WindowDelegate may
// change the window tree in response to this.
WindowTracker tracked_windows_list;
for (const auto& tracked_window : tracked_windows_)
tracked_windows_list.Add(tracked_window.first);
while (!tracked_windows_list.windows().empty()) {
Window* window = tracked_windows_list.Pop();
auto it = tracked_windows_.find(window);
if (it != tracked_windows_.end() &&
it->second != Window::OcclusionState::UNKNOWN) {
// Fallback to VISIBLE/HIDDEN if the maximum number of times that
// occlusion can be recomputed was exceeded.
if (exceeded_max_num_times_occlusion_recomputed) {
it->second = window->IsVisible() ? Window::OcclusionState::VISIBLE
: Window::OcclusionState::HIDDEN;
}
window->SetOcclusionState(it->second);
}
}
}
// Sanity check: Occlusion states in |tracked_windows_| should match those
// returned by Window::occlusion_state().
DCHECK(OcclusionStatesMatch(tracked_windows_));
}
bool WindowOcclusionTracker::RecomputeOcclusionImpl(
Window* window,
const gfx::Transform& parent_transform_relative_to_root,
const SkIRect* clipped_bounds,
SkRegion* occluded_region) {
DCHECK(window);
if (!window->IsVisible()) {
SetWindowAndDescendantsAreOccluded(window, true);
return false;
}
if (WindowIsAnimated(window)) {
SetWindowAndDescendantsAreOccluded(window, false);
return true;
}
// Compute window bounds.
const gfx::Transform transform_relative_to_root =
GetWindowTransformRelativeToRoot(window,
parent_transform_relative_to_root);
if (!transform_relative_to_root.Preserves2dAxisAlignment()) {
// For simplicity, windows that are not axis-aligned are considered
// unoccluded and do not occlude other windows.
SetWindowAndDescendantsAreOccluded(window, false);
return true;
}
const SkIRect window_bounds = GetWindowBoundsInRootWindow(
window, transform_relative_to_root, clipped_bounds);
// Compute children occlusion states.
const SkIRect* clipped_bounds_for_children =
window->layer()->GetMasksToBounds() ? &window_bounds : clipped_bounds;
bool has_visible_child = false;
for (auto* child : base::Reversed(window->children())) {
has_visible_child |=
RecomputeOcclusionImpl(child, transform_relative_to_root,
clipped_bounds_for_children, occluded_region);
}
// Compute window occlusion state.
if (occluded_region->contains(window_bounds)) {
SetOccluded(window, !has_visible_child);
return has_visible_child;
}
SetOccluded(window, false);
if (VisibleWindowIsOpaque(window))
occluded_region->op(window_bounds, SkRegion::kUnion_Op);
return true;
}
void WindowOcclusionTracker::CleanupAnimatedWindows() {
base::EraseIf(animated_windows_, [=](Window* window) {
ui::LayerAnimator* const animator = window->layer()->GetAnimator();
if (animator->IsAnimatingOnePropertyOf(kSkipWindowWhenPropertiesAnimated))
return false;
animator->RemoveObserver(this);
auto root_window_state_it = root_windows_.find(window->GetRootWindow());
if (root_window_state_it != root_windows_.end())
MarkRootWindowAsDirty(&root_window_state_it->second);
return true;
});
}
bool WindowOcclusionTracker::MaybeObserveAnimatedWindow(Window* window) {
// MaybeObserveAnimatedWindow() is called when OnWindowBoundsChanged(),
// OnWindowTransformed() or OnWindowOpacitySet() is called with
// ui::PropertyChangeReason::FROM_ANIMATION. Despite that, if the animation is
// ending, the IsAnimatingOnePropertyOf() call below may return false. It is
// important not to register an observer in that case because it would never
// be notified.
ui::LayerAnimator* const animator = window->layer()->GetAnimator();
if (animator->IsAnimatingOnePropertyOf(kSkipWindowWhenPropertiesAnimated)) {
const auto insert_result = animated_windows_.insert(window);
if (insert_result.second) {
animator->AddObserver(this);
return true;
}
}
return false;
}
void WindowOcclusionTracker::SetWindowAndDescendantsAreOccluded(
Window* window,
bool is_occluded) {
SetOccluded(window, is_occluded);
for (Window* child_window : window->children())
SetWindowAndDescendantsAreOccluded(child_window, is_occluded);
}
void WindowOcclusionTracker::SetOccluded(Window* window, bool is_occluded) {
auto tracked_window = tracked_windows_.find(window);
if (tracked_window == tracked_windows_.end())
return;
if (!window->IsVisible())
tracked_window->second = Window::OcclusionState::HIDDEN;
else if (is_occluded)
tracked_window->second = Window::OcclusionState::OCCLUDED;
else
tracked_window->second = Window::OcclusionState::VISIBLE;
}
bool WindowOcclusionTracker::WindowIsTracked(Window* window) const {
return base::ContainsKey(tracked_windows_, window);
}
bool WindowOcclusionTracker::WindowIsAnimated(Window* window) const {
return base::ContainsKey(animated_windows_, window);
}
template <typename Predicate>
void WindowOcclusionTracker::MarkRootWindowAsDirtyAndMaybeComputeOcclusionIf(
Window* window,
Predicate predicate) {
Window* root_window = window->GetRootWindow();
if (!root_window)
return;
auto root_window_state_it = root_windows_.find(root_window);
// This may be called if a WindowObserver or a LayoutManager changes |window|
// after Window::AddChild() has added it to a new root but before
// OnWindowAddedToRootWindow() is called on |this|. In that case, do nothing
// here and rely on OnWindowAddedToRootWindow() to mark the new root as dirty.
if (root_window_state_it == root_windows_.end()) {
DCHECK(WindowIsTracked(window));
return;
}
if (root_window_state_it->second.dirty)
return;
if (predicate()) {
MarkRootWindowAsDirty(&root_window_state_it->second);
MaybeComputeOcclusion();
}
}
void WindowOcclusionTracker::MarkRootWindowAsDirty(
RootWindowState* root_window_state) {
root_window_state->dirty = true;
// Generate a crash report when a root window is marked as dirty and occlusion
// states have been recomputed |kMaxRecomputeOcclusion| times, because it
// indicates that they are not stabilizing. Don't report it when
// |num_times_occlusion_recomputed_in_current_step_| is greater than
// |kMaxRecomputeOcclusion| to avoid generating multiple reports from the same
// client.
//
// TODO(fdoray): Remove this once we are confident that occlusion states are
// stable after |kMaxRecomputeOcclusion| iterations in production.
// https://crbug.com/813076
if (num_times_occlusion_recomputed_in_current_step_ ==
kMaxRecomputeOcclusion) {
was_occlusion_recomputed_too_many_times_ = true;
base::debug::DumpWithoutCrashing();
}
}
bool WindowOcclusionTracker::WindowOrParentIsAnimated(Window* window) const {
while (window && !WindowIsAnimated(window))
window = window->parent();
return window != nullptr;
}
bool WindowOcclusionTracker::WindowOrDescendantIsTrackedAndVisible(
Window* window) const {
if (!window->IsVisible())
return false;
if (WindowIsTracked(window))
return true;
for (Window* child_window : window->children()) {
if (WindowOrDescendantIsTrackedAndVisible(child_window))
return true;
}
return false;
}
bool WindowOcclusionTracker::WindowOrDescendantIsOpaque(
Window* window,
bool assume_parent_opaque,
bool assume_window_opaque) const {
const bool parent_window_is_opaque =
assume_parent_opaque || !window->parent() ||
window->parent()->layer()->GetCombinedOpacity() == 1.0f;
const bool window_is_opaque =
parent_window_is_opaque &&
(assume_window_opaque || window->layer()->opacity() == 1.0f);
if (!window->IsVisible() || !window->layer() || !window_is_opaque ||
WindowIsAnimated(window)) {
return false;
}
if (!window->transparent() && window->layer()->type() != ui::LAYER_NOT_DRAWN)
return true;
for (Window* child_window : window->children()) {
if (WindowOrDescendantIsOpaque(child_window, true))
return true;
}
return false;
}
bool WindowOcclusionTracker::WindowMoveMayAffectOcclusionStates(
Window* window) const {
return !WindowOrParentIsAnimated(window) &&
(WindowOrDescendantIsOpaque(window) ||
WindowOrDescendantIsTrackedAndVisible(window));
}
void WindowOcclusionTracker::TrackedWindowAddedToRoot(Window* window) {
Window* const root_window = window->GetRootWindow();
DCHECK(root_window);
RootWindowState& root_window_state = root_windows_[root_window];
++root_window_state.num_tracked_windows;
if (root_window_state.num_tracked_windows == 1)
AddObserverToWindowAndDescendants(root_window);
MarkRootWindowAsDirty(&root_window_state);
MaybeComputeOcclusion();
}
void WindowOcclusionTracker::TrackedWindowRemovedFromRoot(Window* window) {
Window* const root_window = window->GetRootWindow();
DCHECK(root_window);
auto root_window_state_it = root_windows_.find(root_window);
DCHECK(root_window_state_it != root_windows_.end());
--root_window_state_it->second.num_tracked_windows;
if (root_window_state_it->second.num_tracked_windows == 0) {
RemoveObserverFromWindowAndDescendants(root_window);
root_windows_.erase(root_window_state_it);
}
}
void WindowOcclusionTracker::RemoveObserverFromWindowAndDescendants(
Window* window) {
if (WindowIsTracked(window)) {
DCHECK(window->HasObserver(this));
} else {
window->RemoveObserver(this);
window->layer()->GetAnimator()->RemoveObserver(this);
animated_windows_.erase(window);
}
for (Window* child_window : window->children())
RemoveObserverFromWindowAndDescendants(child_window);
}
void WindowOcclusionTracker::AddObserverToWindowAndDescendants(Window* window) {
if (WindowIsTracked(window))
DCHECK(window->HasObserver(this));
else
window->AddObserver(this);
for (Window* child_window : window->children())
AddObserverToWindowAndDescendants(child_window);
}
void WindowOcclusionTracker::OnLayerAnimationEnded(
ui::LayerAnimationSequence* sequence) {
CleanupAnimatedWindows();
MaybeComputeOcclusion();
}
void WindowOcclusionTracker::OnLayerAnimationAborted(
ui::LayerAnimationSequence* sequence) {
CleanupAnimatedWindows();
MaybeComputeOcclusion();
}
void WindowOcclusionTracker::OnLayerAnimationScheduled(
ui::LayerAnimationSequence* sequence) {}
void WindowOcclusionTracker::OnWindowHierarchyChanged(
const HierarchyChangeParams& params) {
Window* const window = params.target;
Window* const root_window = window->GetRootWindow();
if (root_window && base::ContainsKey(root_windows_, root_window) &&
!window->HasObserver(this)) {
AddObserverToWindowAndDescendants(window);
}
}
void WindowOcclusionTracker::OnWindowAdded(Window* window) {
MarkRootWindowAsDirtyAndMaybeComputeOcclusionIf(
window, [=]() { return WindowMoveMayAffectOcclusionStates(window); });
}
void WindowOcclusionTracker::OnWillRemoveWindow(Window* window) {
MarkRootWindowAsDirtyAndMaybeComputeOcclusionIf(window, [=]() {
return !WindowOrParentIsAnimated(window) &&
WindowOrDescendantIsOpaque(window);
});
}
void WindowOcclusionTracker::OnWindowVisibilityChanged(Window* window,
bool visible) {
MarkRootWindowAsDirtyAndMaybeComputeOcclusionIf(window, [=]() {
// A child isn't visible when its parent isn't IsVisible(). Therefore, there
// is no need to compute occlusion when Show() or Hide() is called on a
// window with a hidden parent.
return (!window->parent() || window->parent()->IsVisible()) &&
!WindowOrParentIsAnimated(window);
});
}
void WindowOcclusionTracker::OnWindowBoundsChanged(
Window* window,
const gfx::Rect& old_bounds,
const gfx::Rect& new_bounds,
ui::PropertyChangeReason reason) {
// Call MaybeObserveAnimatedWindow() outside the lambda so that the window can
// be marked as animated even when its root is dirty.
const bool animation_started =
(reason == ui::PropertyChangeReason::FROM_ANIMATION) &&
MaybeObserveAnimatedWindow(window);
MarkRootWindowAsDirtyAndMaybeComputeOcclusionIf(window, [=]() {
return animation_started || WindowMoveMayAffectOcclusionStates(window);
});
}
void WindowOcclusionTracker::OnWindowOpacitySet(
Window* window,
ui::PropertyChangeReason reason) {
// Call MaybeObserveAnimatedWindow() outside the lambda so that the window can
// be marked as animated even when its root is dirty.
const bool animation_started =
(reason == ui::PropertyChangeReason::FROM_ANIMATION) &&
MaybeObserveAnimatedWindow(window);
MarkRootWindowAsDirtyAndMaybeComputeOcclusionIf(window, [=]() {
return animation_started || !WindowOrParentIsAnimated(window);
});
}
void WindowOcclusionTracker::OnWindowTransformed(
Window* window,
ui::PropertyChangeReason reason) {
// Call MaybeObserveAnimatedWindow() outside the lambda so that the window can
// be marked as animated even when its root is dirty.
const bool animation_started =
(reason == ui::PropertyChangeReason::FROM_ANIMATION) &&
MaybeObserveAnimatedWindow(window);
MarkRootWindowAsDirtyAndMaybeComputeOcclusionIf(window, [=]() {
return animation_started || WindowMoveMayAffectOcclusionStates(window);
});
}
void WindowOcclusionTracker::OnWindowStackingChanged(Window* window) {
MarkRootWindowAsDirtyAndMaybeComputeOcclusionIf(
window, [=]() { return WindowMoveMayAffectOcclusionStates(window); });
}
void WindowOcclusionTracker::OnWindowDestroyed(Window* window) {
DCHECK(!window->GetRootWindow() || (window == window->GetRootWindow()));
tracked_windows_.erase(window);
// Animations should be completed or aborted before a window is destroyed.
DCHECK(!window->layer()->GetAnimator()->IsAnimatingOnePropertyOf(
kSkipWindowWhenPropertiesAnimated));
// |window| must be removed from |animated_windows_| to prevent an invalid
// access in CleanupAnimatedWindows() if |window| is being destroyed from a
// LayerAnimationObserver after an animation has ended but before |this| has
// been notified.
animated_windows_.erase(window);
}
void WindowOcclusionTracker::OnWindowAddedToRootWindow(Window* window) {
DCHECK(window->GetRootWindow());
if (WindowIsTracked(window))
TrackedWindowAddedToRoot(window);
}
void WindowOcclusionTracker::OnWindowRemovingFromRootWindow(Window* window,
Window* new_root) {
DCHECK(window->GetRootWindow());
if (WindowIsTracked(window))
TrackedWindowRemovedFromRoot(window);
RemoveObserverFromWindowAndDescendants(window);
}
void WindowOcclusionTracker::OnWindowLayerRecreated(Window* window) {
ui::LayerAnimator* animator = window->layer()->GetAnimator();
// Recreating the layer may have stopped animations.
if (animator->IsAnimatingOnePropertyOf(kSkipWindowWhenPropertiesAnimated))
return;
size_t num_removed = animated_windows_.erase(window);
if (num_removed == 0)
return;
animator->RemoveObserver(this);
auto root_window_state_it = root_windows_.find(window->GetRootWindow());
if (root_window_state_it != root_windows_.end()) {
MarkRootWindowAsDirty(&root_window_state_it->second);
MaybeComputeOcclusion();
}
}
} // namespace aura