blob: 64ef4dfc81fde082ae0f449620a67a94e54b518c [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "ui/views/layout/layout_manager_base.h"
#include <utility>
#include "base/auto_reset.h"
#include "base/check_op.h"
#include "base/containers/contains.h"
#include "base/dcheck_is_on.h"
#include "base/memory/raw_ptr.h"
#include "base/trace_event/trace_event.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
#if DCHECK_IS_ON()
#include <algorithm>
#endif
namespace views {
namespace {
// Adjusts |child_available_size| by adding the difference between the host
// view's size and the size available to it.
SizeBounds AdjustAvailableSizeForParentAvailableSize(
const View* host,
const SizeBounds& child_available_size) {
SizeBounds available_size = child_available_size;
if (host && host->parent() && available_size != SizeBounds()) {
SizeBounds host_additional_size = host->parent()->GetAvailableSize(host);
host_additional_size.Enlarge(-host->width(), -host->height());
if (host_additional_size.width().is_bounded()) {
available_size.width() += host_additional_size.width();
}
if (host_additional_size.height().is_bounded()) {
available_size.height() += host_additional_size.height();
}
}
return available_size;
}
bool IncludeInLayout(const View* view) {
return !view->GetProperty(kViewIgnoredByLayoutKey);
}
} // anonymous namespace
LayoutManagerBase::~LayoutManagerBase() = default;
gfx::Size LayoutManagerBase::GetPreferredSize(const View* host) const {
DCHECK_EQ(host_view_, host);
if (!cached_preferred_size_) {
cached_preferred_size_ = CalculateProposedLayout(SizeBounds()).host_size;
}
return *cached_preferred_size_;
}
gfx::Size LayoutManagerBase::GetPreferredSize(
const View* host,
const SizeBounds& available_size) const {
DCHECK_EQ(host_view_, host);
if (available_size.width().is_bounded()) {
return CalculateProposedLayout(available_size).host_size;
}
return GetPreferredSize(host);
}
gfx::Size LayoutManagerBase::GetMinimumSize(const View* host) const {
DCHECK_EQ(host_view_, host);
if (!cached_minimum_size_) {
cached_minimum_size_ = CalculateProposedLayout(SizeBounds(0, 0)).host_size;
}
return *cached_minimum_size_;
}
int LayoutManagerBase::GetPreferredHeightForWidth(const View* host,
int width) const {
if (!cached_height_for_width_ || cached_height_for_width_->width() != width) {
const int height = CalculateProposedLayout(SizeBounds(width, SizeBound()))
.host_size.height();
cached_height_for_width_ = gfx::Size(width, height);
}
return cached_height_for_width_->height();
}
SizeBounds LayoutManagerBase::GetAvailableSize(const View* host,
const View* view) const {
DCHECK_EQ(host_view_, host);
if (!cached_layout_size_) {
GetProposedLayout(host->size());
}
if (cached_layout_size_) {
for (const auto& child_layout : cached_layout_.child_layouts) {
if (child_layout.child_view == view) {
return AdjustAvailableSizeForParentAvailableSize(
host, child_layout.available_size);
}
}
}
return SizeBounds();
}
void LayoutManagerBase::Layout(View* host) {
DCHECK_EQ(host_view_, host);
TRACE_EVENT1("ui", "LayoutManagerBase::Layout", "class",
host->GetClassName());
// A handful of views will cause invalidations while they are being
// positioned, which can result in loops or loss of layout data during layout
// application. Therefore we protect the layout manager from spurious
// invalidations during the layout process.
base::AutoReset<bool> setter(&suppress_invalidate_, true);
LayoutImpl();
}
void LayoutManagerBase::OnViewPropertyChanged(View* observed_view,
const void* key,
int64_t old_value) {
if (key != kViewIgnoredByLayoutKey) {
return;
}
CHECK_EQ(GetRootLayoutManager(), this);
const bool include = IncludeInLayout(observed_view);
if (include != child_infos_[observed_view].included_in_layout) {
base::AutoReset<bool> setter(&suppress_invalidate_, true);
PropagateChildViewIncludedInLayout(observed_view, include);
InvalidateHost(false);
}
}
std::vector<raw_ptr<View, VectorExperimental>>
LayoutManagerBase::GetChildViewsInPaintOrder(const View* host) const {
DCHECK_EQ(host_view_, host);
return LayoutManager::GetChildViewsInPaintOrder(host);
}
ProposedLayout LayoutManagerBase::GetProposedLayout(
const gfx::Size& host_size) const {
if (cached_layout_size_ != host_size) {
#if (DCHECK_IS_ON())
// This calculation must not be re-entrant.
DCHECK(!calculating_layout_);
base::AutoReset<bool> calculating_layout(&calculating_layout_, true);
#endif
cached_layout_size_ = host_size;
cached_layout_ = CalculateProposedLayout(SizeBounds(host_size));
}
return cached_layout_;
}
ProposedLayout LayoutManagerBase::GetProposedLayout(
const SizeBounds& size_bounds,
PassKeyType) const {
return CalculateProposedLayout(size_bounds);
}
LayoutManagerBase::LayoutManagerBase() = default;
SizeBounds LayoutManagerBase::GetAvailableHostSize() const {
DCHECK(host_view());
const auto* const parent = host_view()->parent();
return parent ? parent->GetAvailableSize(host_view()) : SizeBounds();
}
bool LayoutManagerBase::IsChildIncludedInLayout(const View* child,
bool include_hidden) const {
const auto it = child_infos_.find(child);
// During callbacks when a child is removed we can get in a state where a view
// in the child list of the host view is not in |child_infos_|. In that case,
// the view is being removed and is not part of the layout.
if (it == child_infos_.end()) {
return false;
}
return it->second.included_in_layout &&
(include_hidden || it->second.can_be_visible);
}
bool LayoutManagerBase::CanBeVisible(const View* child) const {
const auto it = child_infos_.find(child);
// During callbacks when a child is removed we can get in a state where a view
// in the child list of the host view is not in |child_infos_|. In that case,
// the view is being removed and is not part of the layout.
if (it == child_infos_.end()) {
return false;
}
return it->second.can_be_visible;
}
void LayoutManagerBase::LayoutImpl() {
ApplyLayout(GetProposedLayout(host_view_->size()));
}
void LayoutManagerBase::ApplyLayout(const ProposedLayout& layout) {
const SizeBounds new_available_size = GetAvailableHostSize();
for (auto& child_layout : layout.child_layouts) {
DCHECK_EQ(host_view_, child_layout.child_view->parent());
// Since we have a non-const reference to the parent here, we can safely use
// a non-const reference to the child.
View* const child_view = child_layout.child_view;
// Should not be attempting to modify a child view that has been removed.
DCHECK(host_view()->GetIndexOf(child_view).has_value());
if (child_view->GetVisible() != child_layout.visible) {
SetViewVisibility(child_view, child_layout.visible);
}
// If the child view is not visible and we haven't bothered to specify
// bounds, don't bother setting them (which would cause another cascade of
// events that wouldn't do anything useful).
if (new_available_size != cached_available_size_ || child_layout.visible ||
!child_layout.bounds.IsEmpty()) {
const bool size_changed =
child_view->bounds().size() != child_layout.bounds.size();
if (child_view->bounds() != child_layout.bounds) {
child_view->SetBoundsRect(child_layout.bounds);
}
// Child layouts which are not invalid will not be laid out by the default
// View::Layout() implementation, but if there is an available size
// constraint it's important that the child view be laid out. So we'll do
// it here.
// TODO(dfried): figure out a better way to handle this.
if (!size_changed && child_layout.available_size != SizeBounds()) {
child_view->DeprecatedLayoutImmediately();
}
}
}
cached_available_size_ = new_available_size;
}
void LayoutManagerBase::InvalidateHost(bool mark_layouts_changed) {
if (mark_layouts_changed) {
DCHECK(!suppress_invalidate_);
if (host_view()) {
// This has the side-effect of also invalidating all of the layouts
// associated with the host view, from the view's layout through its
// owned layouts (and their owned layouts).
host_view()->InvalidateLayout();
} else {
// Even without a host view, ensure that the layout tree is invalidated.
GetRootLayoutManager()->InvalidateLayout();
}
} else if (host_view()) {
// Suppress invalidation at the root layout, so host view layout does not
// propagate down the layout manager tree.
LayoutManagerBase* const root_layout = GetRootLayoutManager();
base::AutoReset<bool> setter(&root_layout->suppress_invalidate_, true);
host_view()->InvalidateLayout();
}
}
bool LayoutManagerBase::OnChildViewIncludedInLayoutSet(View* child_view,
bool ignored) {
OnLayoutChanged();
return false;
}
bool LayoutManagerBase::OnViewAdded(View* host, View* view) {
OnLayoutChanged();
return false;
}
bool LayoutManagerBase::OnViewRemoved(View* host, View* view) {
OnLayoutChanged();
return false;
}
bool LayoutManagerBase::OnViewVisibilitySet(View* host,
View* view,
bool visible) {
OnLayoutChanged();
return false;
}
void LayoutManagerBase::OnInstalled(View* host) {}
void LayoutManagerBase::OnLayoutChanged() {
cached_minimum_size_.reset();
cached_preferred_size_.reset();
cached_height_for_width_.reset();
cached_layout_size_.reset();
}
void LayoutManagerBase::InvalidateLayout() {
if (!suppress_invalidate_) {
base::AutoReset<bool> setter(&suppress_invalidate_, true);
PropagateInvalidateLayout();
}
}
void LayoutManagerBase::Installed(View* host_view) {
DCHECK(host_view);
DCHECK(!host_view_);
DCHECK(child_infos_.empty());
DCHECK_EQ(GetRootLayoutManager(), this);
for (View* child : host_view->children()) {
view_observations_.AddObservation(child);
}
base::AutoReset<bool> setter(&suppress_invalidate_, true);
PropagateInstalled(host_view);
}
void LayoutManagerBase::ViewAdded(View* host, View* view) {
DCHECK_EQ(host_view_, host);
DCHECK(!base::Contains(child_infos_, view));
DCHECK_EQ(GetRootLayoutManager(), this);
view_observations_.AddObservation(view);
base::AutoReset<bool> setter(&suppress_invalidate_, true);
const bool invalidate = PropagateViewAdded(host, view);
if (invalidate || view->GetVisible()) {
InvalidateHost(false);
}
}
void LayoutManagerBase::ViewRemoved(View* host, View* view) {
DCHECK_EQ(host_view_, host);
DCHECK(base::Contains(child_infos_, view));
DCHECK_EQ(GetRootLayoutManager(), this);
auto it = child_infos_.find(view);
CHECK(it != child_infos_.end());
const bool removed_visible =
it->second.can_be_visible && it->second.included_in_layout;
view_observations_.RemoveObservation(view);
base::AutoReset<bool> setter(&suppress_invalidate_, true);
const bool invalidate = PropagateViewRemoved(host, view);
if (invalidate || removed_visible) {
InvalidateHost(false);
}
}
void LayoutManagerBase::ViewVisibilitySet(View* host,
View* view,
bool old_visibility,
bool new_visibility) {
DCHECK_EQ(host_view_, host);
auto it = child_infos_.find(view);
CHECK(it != child_infos_.end());
if (it->second.can_be_visible == new_visibility) {
return;
}
base::AutoReset<bool> setter(&suppress_invalidate_, true);
const bool invalidate =
PropagateViewVisibilitySet(host, view, new_visibility);
if (invalidate || it->second.included_in_layout) {
InvalidateHost(false);
}
}
void LayoutManagerBase::AddOwnedLayoutInternal(
std::unique_ptr<LayoutManagerBase> owned_layout) {
DCHECK(owned_layout);
DCHECK(!owned_layout->parent_layout_);
// View observations should only exist on the root.
#if DCHECK_IS_ON()
// If `owned_layout` knows about views the root doesn't, something is going to
// go wrong.
const LayoutManagerBase* const root_layout = GetRootLayoutManager();
std::ranges::for_each(
owned_layout->view_observations_.sources(), [&](View* source) {
DCHECK(root_layout->view_observations_.IsObservingSource(source));
});
#endif
owned_layout->view_observations_.RemoveAllObservations();
// Inform `owned_layout` about `host_view_` and its children.
if (host_view_) {
base::AutoReset<bool> setter(&suppress_invalidate_, true);
owned_layout->PropagateInstalled(host_view_);
for (View* child_view : host_view_->children()) {
const ChildInfo& child_info = child_infos_.find(child_view)->second;
owned_layout->PropagateChildViewIncludedInLayout(
child_view, child_info.included_in_layout);
owned_layout->PropagateViewVisibilitySet(host_view_, child_view,
child_info.can_be_visible);
}
}
// Attach `owned_layout` to us.
owned_layout->parent_layout_ = this;
owned_layouts_.emplace_back(std::move(owned_layout));
}
LayoutManagerBase* LayoutManagerBase::GetRootLayoutManager() {
LayoutManagerBase* result = this;
while (result->parent_layout_) {
result = result->parent_layout_;
}
return result;
}
bool LayoutManagerBase::PropagateChildViewIncludedInLayout(View* child_view,
bool include) {
child_infos_[child_view].included_in_layout = include;
bool result = false;
for (auto& owned_layout : owned_layouts_) {
result |=
owned_layout->PropagateChildViewIncludedInLayout(child_view, include);
}
result |= OnChildViewIncludedInLayoutSet(child_view, include);
return result;
}
bool LayoutManagerBase::PropagateViewAdded(View* host, View* view) {
child_infos_.emplace(view,
ChildInfo{.can_be_visible = view->GetVisible(),
.included_in_layout = IncludeInLayout(view)});
bool result = false;
for (auto& owned_layout : owned_layouts_) {
result |= owned_layout->PropagateViewAdded(host, view);
}
result |= OnViewAdded(host, view);
return result;
}
bool LayoutManagerBase::PropagateViewRemoved(View* host, View* view) {
child_infos_.erase(view);
bool result = false;
for (auto& owned_layout : owned_layouts_) {
result |= owned_layout->PropagateViewRemoved(host, view);
}
result |= OnViewRemoved(host, view);
return result;
}
bool LayoutManagerBase::PropagateViewVisibilitySet(View* host,
View* view,
bool visible) {
child_infos_[view].can_be_visible = visible;
bool result = false;
for (auto& owned_layout : owned_layouts_) {
result |= owned_layout->PropagateViewVisibilitySet(host, view, visible);
}
result |= OnViewVisibilitySet(host, view, visible);
return result;
}
void LayoutManagerBase::PropagateInstalled(View* host) {
host_view_ = host;
for (View* child : host->children()) {
child_infos_.emplace(
child, ChildInfo{.can_be_visible = child->GetVisible(),
.included_in_layout = IncludeInLayout(child)});
}
for (auto& owned_layout : owned_layouts_) {
owned_layout->PropagateInstalled(host);
}
OnInstalled(host);
}
void LayoutManagerBase::PropagateInvalidateLayout() {
for (auto& owned_layout : owned_layouts_) {
owned_layout->PropagateInvalidateLayout();
}
OnLayoutChanged();
}
ManualLayoutUtil::ManualLayoutUtil(LayoutManagerBase* layout_manager)
: layout_manager_(layout_manager) {}
ManualLayoutUtil::~ManualLayoutUtil() = default;
void ManualLayoutUtil::SetViewHidden(View* child_view, bool hidden) {
// If the child view is visible but we are not allowing visibility, update its
// visibility. This doesn't necessarily invalidate anything.
if (child_view->GetVisible() && hidden) {
layout_manager_->SetViewVisibility(child_view, false);
}
// If the view cannot be visible, it should be ignored by the layout, and
// vice-versa. This may invalidate layout data.
const auto it = layout_manager_->child_infos_.find(child_view);
CHECK(it != layout_manager_->child_infos_.end());
const bool can_be_visible = !hidden;
if (can_be_visible != it->second.can_be_visible) {
layout_manager_->PropagateViewVisibilitySet(layout_manager_->host_view(),
child_view, can_be_visible);
}
}
ManualLayoutUtil::TemporaryExclusion
ManualLayoutUtil::TemporarilyExcludeFromLayout(View* child_view) {
const auto it = layout_manager_->child_infos_.find(child_view);
CHECK(it != layout_manager_->child_infos_.end());
CHECK_EQ(IncludeInLayout(child_view), it->second.included_in_layout)
<< "Cannot nest exclusions for the same child view.";
// If this view is already excluded, this is a no-op.
if (it->second.included_in_layout) {
// Write a temporary exclusion into the layout.
layout_manager_->PropagateChildViewIncludedInLayout(child_view, false);
}
return TemporaryExclusion(new TemporaryExclusionData(this, child_view));
}
void ManualLayoutUtil::TemporaryExclusionDeleter::operator()(
TemporaryExclusionData* data) const {
if (data) {
data->first->EndTemporaryExclusion(data->second);
delete data;
}
}
void ManualLayoutUtil::EndTemporaryExclusion(View* child_view) {
// If the child view would still be excluded, this is a no-op.
const bool included = IncludeInLayout(child_view);
if (!included) {
return;
}
// Restore inclusion of the view.
CHECK(base::Contains(layout_manager_->child_infos_, child_view));
layout_manager_->PropagateChildViewIncludedInLayout(child_view, true);
}
} // namespace views