// 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(window->bounds().width()), static_cast(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& 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_ != 0) return; base::AutoReset auto_reset(&num_times_occlusion_recomputed_, 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_ <= kMaxRecomputeOcclusion) { const bool exceeded_max_num_times_occlusion_recomputed = num_times_occlusion_recomputed_ == 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); } } } ++num_times_occlusion_recomputed_; if (!found_dirty_root) break; // 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 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_| 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_ == 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