blob: 73e9dc4f3cba4e86b89a2bebc0706a14a01a5484 [file] [log] [blame]
// Copyright (c) 2012 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 "chrome/browser/ui/views/tabs/stacked_tab_strip_layout.h"
#include <stdio.h>
#include "base/check_op.h"
#include "base/cxx17_backports.h"
#include "base/metrics/user_metrics.h"
#include "base/numerics/ranges.h"
#include "base/strings/string_number_conversions.h"
using base::UserMetricsAction;
StackedTabStripLayout::StackedTabStripLayout(const gfx::Size& size,
int overlap,
int stacked_padding,
int max_stacked_count,
views::ViewModelBase* view_model)
: size_(size),
overlap_(overlap),
stacked_padding_(stacked_padding),
max_stacked_count_(max_stacked_count),
view_model_(view_model) {}
StackedTabStripLayout::~StackedTabStripLayout() {
}
void StackedTabStripLayout::SetXAndPinnedCount(int x, int pinned_tab_count) {
first_tab_x_ = x;
x_ = x;
pinned_tab_count_ = pinned_tab_count;
pinned_tab_to_non_pinned_tab_ = 0;
if (!requires_stacking() || tab_count() == pinned_tab_count) {
ResetToIdealState();
return;
}
if (pinned_tab_count > 0) {
pinned_tab_to_non_pinned_tab_ = x - ideal_x(pinned_tab_count - 1);
first_tab_x_ = ideal_x(0);
}
SetIdealBoundsAt(active_index(), ConstrainActiveX(ideal_x(active_index())));
LayoutByTabOffsetAfter(active_index());
LayoutByTabOffsetBefore(active_index());
}
void StackedTabStripLayout::SetWidth(int width) {
if (width_ == width)
return;
width_ = width;
if (!requires_stacking()) {
ResetToIdealState();
return;
}
// TODO(tdanderson): Audit other places in this class which make a similar
// pattern of calls to this in order to re-layout the tabs, and refactor
// into a helper function as appropriate.
SetIdealBoundsAt(active_index(), ConstrainActiveX(ideal_x(active_index())));
LayoutByTabOffsetBefore(active_index());
LayoutByTabOffsetAfter(active_index());
AdjustStackedTabs();
}
void StackedTabStripLayout::SetActiveIndex(int index) {
int old = active_index();
active_index_ = index;
if (old == active_index() || !requires_stacking())
return;
SetIdealBoundsAt(active_index(), ConstrainActiveX(ideal_x(active_index())));
LayoutByTabOffsetBefore(active_index());
LayoutByTabOffsetAfter(active_index());
AdjustStackedTabs();
}
void StackedTabStripLayout::DragActiveTab(int delta) {
if (delta == 0 || !requires_stacking())
return;
base::RecordAction(UserMetricsAction("StackedTab_DragActiveTab"));
int initial_x = ideal_x(active_index());
// If we're at a particular edge and start dragging, expose all the tabs after
// the tab (or before when dragging to the left).
if (delta > 0 && initial_x == GetMinX(active_index())) {
LayoutByTabOffsetAfter(active_index());
AdjustStackedTabs();
} else if (delta < 0 && initial_x == GetMaxX(active_index())) {
LayoutByTabOffsetBefore(active_index());
AdjustStackedTabs();
}
int x = delta > 0 ?
std::min(initial_x + delta, GetMaxDragX(active_index())) :
std::max(initial_x + delta, GetMinDragX(active_index()));
if (x != initial_x) {
SetIdealBoundsAt(active_index(), x);
if (delta > 0) {
PushTabsAfter(active_index(), (x - initial_x));
LayoutForDragBefore(active_index());
} else {
PushTabsBefore(active_index(), initial_x - x);
LayoutForDragAfter(active_index());
}
delta -= (x - initial_x);
}
if (delta > 0)
ExpandTabsBefore(active_index(), delta);
else if (delta < 0)
ExpandTabsAfter(active_index(), -delta);
AdjustStackedTabs();
}
void StackedTabStripLayout::SizeToFit() {
if (!tab_count())
return;
if (!requires_stacking()) {
ResetToIdealState();
return;
}
if (ideal_x(0) != first_tab_x_) {
// Tabs have been dragged to the right. Pull in the tabs from left to right
// to fill in space.
int delta = ideal_x(0) - first_tab_x_;
int i = 0;
for (; i < pinned_tab_count_; ++i) {
gfx::Rect pinned_bounds(view_model_->ideal_bounds(i));
pinned_bounds.set_x(ideal_x(i) - delta);
view_model_->set_ideal_bounds(i, pinned_bounds);
}
for (; delta > 0 && i < tab_count() - 1; ++i) {
const int exposed = tab_offset() - (ideal_x(i + 1) - ideal_x(i));
SetIdealBoundsAt(i, ideal_x(i) - delta);
delta -= exposed;
}
AdjustStackedTabs();
return;
}
const int max_x = width_ - size_.width();
if (ideal_x(tab_count() - 1) == max_x)
return;
// Tabs have been dragged to the left. Pull in tabs from right to left to fill
// in space.
SetIdealBoundsAt(tab_count() - 1, max_x);
for (int i = tab_count() - 2; i > pinned_tab_count_ &&
ideal_x(i + 1) - ideal_x(i) > tab_offset(); --i) {
SetIdealBoundsAt(i, ideal_x(i + 1) - tab_offset());
}
AdjustStackedTabs();
}
void StackedTabStripLayout::AddTab(int index, int add_types, int start_x) {
if (add_types & kAddTypeActive)
active_index_ = index;
else if (active_index_ >= index)
active_index_++;
if (add_types & kAddTypePinned)
pinned_tab_count_++;
x_ = start_x;
if (!requires_stacking() || normal_tab_count() <= 1) {
ResetToIdealState();
return;
}
int active_x = ideal_x(active_index());
if (add_types & kAddTypeActive) {
active_x = (index + 1 == tab_count()) ?
width_ - size_.width() : ideal_x(index + 1);
}
SetIdealBoundsAt(active_index(), ConstrainActiveX(active_x));
LayoutByTabOffsetAfter(active_index());
LayoutByTabOffsetBefore(active_index());
AdjustStackedTabs();
if ((add_types & kAddTypeActive) == 0)
MakeVisible(index);
}
void StackedTabStripLayout::RemoveTab(int index, int start_x, int old_x) {
if (index == active_index_)
active_index_ = std::min(active_index_, tab_count() - 1);
else if (index < active_index_)
active_index_--;
bool removed_pinned_tab = index < pinned_tab_count_;
if (removed_pinned_tab) {
pinned_tab_count_--;
DCHECK_GE(pinned_tab_count_, 0);
}
int delta = start_x - x_;
x_ = start_x;
if (!requires_stacking()) {
ResetToIdealState();
return;
}
if (removed_pinned_tab) {
for (int i = pinned_tab_count_; i < tab_count(); ++i)
SetIdealBoundsAt(i, ideal_x(i) + delta);
}
// TODO(tdanderson): Investigate whether the call to
// SetActiveBoundsAndLayoutFromActiveTab() should be replaced by
// LayoutByTabOffsetBefore() / LayoutByTabOffsetAfter() similar to the
// behavior in other stacked tab operations.
SetActiveBoundsAndLayoutFromActiveTab();
AdjustStackedTabs();
}
void StackedTabStripLayout::MoveTab(int from,
int to,
int new_active_index,
int start_x,
int pinned_tab_count) {
x_ = start_x;
pinned_tab_count_ = pinned_tab_count;
active_index_ = new_active_index;
if (!requires_stacking() || tab_count() == pinned_tab_count_) {
ResetToIdealState();
} else {
SetIdealBoundsAt(active_index(),
ConstrainActiveX(ideal_x(active_index())));
LayoutByTabOffsetAfter(active_index());
LayoutByTabOffsetBefore(active_index());
AdjustStackedTabs();
}
pinned_tab_to_non_pinned_tab_ = pinned_tab_count > 0 ?
start_x - ideal_x(pinned_tab_count - 1) : 0;
first_tab_x_ = pinned_tab_count > 0 ? ideal_x(0) : start_x;
}
bool StackedTabStripLayout::IsStacked(int index) const {
if (index == active_index() || tab_count() == pinned_tab_count_ ||
index < pinned_tab_count_)
return false;
if (index > active_index())
return ideal_x(index) != ideal_x(index - 1) + tab_offset();
return ideal_x(index + 1) != ideal_x(index) + tab_offset();
}
void StackedTabStripLayout::SetActiveTabLocation(int x) {
if (!requires_stacking())
return;
const int index = active_index();
if (index <= pinned_tab_count_)
return;
x = ConstrainActiveX(x);
if (x == ideal_x(index))
return;
SetIdealBoundsAt(index, x);
LayoutByTabOffsetBefore(index);
LayoutByTabOffsetAfter(index);
}
#if !defined(NDEBUG)
std::string StackedTabStripLayout::BoundsString() const {
std::string result;
for (int i = 0; i < view_model_->view_size(); ++i) {
if (!result.empty())
result += " ";
if (i == active_index())
result += "[";
result += base::NumberToString(view_model_->ideal_bounds(i).x());
if (i == active_index())
result += "]";
}
return result;
}
#endif
void StackedTabStripLayout::Reset(int x,
int width,
int pinned_tab_count,
int active_index) {
x_ = x;
width_ = width;
pinned_tab_count_ = pinned_tab_count;
pinned_tab_to_non_pinned_tab_ = pinned_tab_count > 0 ?
x - ideal_x(pinned_tab_count - 1) : 0;
first_tab_x_ = pinned_tab_count > 0 ? ideal_x(0) : x;
active_index_ = active_index;
ResetToIdealState();
}
void StackedTabStripLayout::ResetToIdealState() {
if (tab_count() == pinned_tab_count_)
return;
if (!requires_stacking()) {
SetIdealBoundsAt(pinned_tab_count_, x_);
LayoutByTabOffsetAfter(pinned_tab_count_);
return;
}
if (normal_tab_count() == 1) {
// TODO: might want to shrink the tab here.
SetIdealBoundsAt(pinned_tab_count_, 0);
return;
}
int available_width = width_ - x_;
int leading_count = active_index() - pinned_tab_count_;
int trailing_count = tab_count() - active_index();
if (width_for_count(leading_count + 1) + max_stacked_width() <
available_width) {
SetIdealBoundsAt(pinned_tab_count_, x_);
LayoutByTabOffsetAfter(pinned_tab_count_);
} else if (width_for_count(trailing_count) + max_stacked_width() <
available_width) {
SetIdealBoundsAt(tab_count() - 1, width_ - size_.width());
LayoutByTabOffsetBefore(tab_count() - 1);
} else {
int index = active_index();
do {
int stacked_padding =
stacked_padding_for_count(index - pinned_tab_count_);
SetIdealBoundsAt(index, x_ + stacked_padding);
LayoutByTabOffsetAfter(index);
LayoutByTabOffsetBefore(index);
index--;
} while (index >= pinned_tab_count_ && ideal_x(pinned_tab_count_) != x_ &&
ideal_x(tab_count() - 1) != width_ - size_.width());
}
AdjustStackedTabs();
}
void StackedTabStripLayout::MakeVisible(int index) {
// Currently no need to support tabs opening before |index| visible.
if (index <= active_index() || !requires_stacking() || !IsStacked(index))
return;
const int ideal_delta = width_for_count(index - active_index()) - overlap_;
if (ideal_x(index) - ideal_x(active_index()) == ideal_delta)
return;
// Move the active tab to the left so that all tabs between the active tab
// and |index| (inclusive) can be made visible.
// This is a broken clamp function that successfully returns a bogus value
// when invalid inputs are provided, rather than crashing.
// TODO(https://crbug.com/1231839): Migrate this call to use base::clamp()
// from base/cxx17_backports.h, and fix all the broken tests that result.
const int active_x = base::BrokenClampThatShouldNotBeUsed(
ideal_x(index) - ideal_delta, GetMinX(active_index()),
ideal_x(active_index()));
SetIdealBoundsAt(active_index(), active_x);
LayoutByTabOffsetBefore(active_index());
LayoutByTabOffsetAfter(active_index());
AdjustStackedTabs();
if (ideal_x(index) - ideal_x(active_index()) == ideal_delta)
return;
// If we get here active_index() is left aligned. Push |index| as far to
// the right as possible, forming a stack immediately to the right of the
// active tab if necessary.
const int x = std::min(GetMaxX(index), active_x + ideal_delta);
SetIdealBoundsAt(index, x);
LayoutByTabOffsetAfter(index);
for (int next_x = x, i = index - 1; i > active_index(); --i) {
next_x = std::max(GetMinXCompressed(i), next_x - tab_offset());
SetIdealBoundsAt(i, next_x);
}
LayoutUsingCurrentAfter(active_index());
AdjustStackedTabs();
}
int StackedTabStripLayout::ConstrainActiveX(int x) const {
// This is a broken clamp function that successfully returns a bogus value
// when invalid inputs are provided, rather than crashing.
// TODO(https://crbug.com/1231839): Migrate this call to use base::clamp()
// from base/cxx17_backports.h, and fix all the broken tests that result.
return base::BrokenClampThatShouldNotBeUsed(x, GetMinX(active_index()),
GetMaxX(active_index()));
}
void StackedTabStripLayout::SetActiveBoundsAndLayoutFromActiveTab() {
int x = ConstrainActiveX(ideal_x(active_index()));
SetIdealBoundsAt(active_index(), x);
LayoutUsingCurrentBefore(active_index());
LayoutUsingCurrentAfter(active_index());
AdjustStackedTabs();
}
void StackedTabStripLayout::LayoutByTabOffsetAfter(int index) {
for (int i = index + 1; i < tab_count(); ++i) {
int max_x = width_ - size_.width() -
stacked_padding_for_count(tab_count() - i - 1);
int x = std::min(max_x,
view_model_->ideal_bounds(i - 1).x() + tab_offset());
SetIdealBoundsAt(i, x);
}
}
void StackedTabStripLayout::LayoutByTabOffsetBefore(int index) {
for (int i = index - 1; i >= pinned_tab_count_; --i) {
int min_x = x_ + stacked_padding_for_count(i - pinned_tab_count_);
int x = std::max(min_x, ideal_x(i + 1) - (tab_offset()));
SetIdealBoundsAt(i, x);
}
}
void StackedTabStripLayout::LayoutUsingCurrentAfter(int index) {
for (int i = index + 1; i < tab_count(); ++i) {
int x = std::min(ideal_x(i), ideal_x(i - 1) + tab_offset());
int min_x = width_ - width_for_count(tab_count() - i);
SetIdealBoundsAt(i, base::clamp(x, min_x, GetMaxX(i)));
}
}
void StackedTabStripLayout::LayoutUsingCurrentBefore(int index) {
for (int i = index - 1; i >= pinned_tab_count_; --i) {
int x = std::max(ideal_x(i), ideal_x(i + 1) - tab_offset());
int max_x = x_ + width_for_count(i - pinned_tab_count_);
if (i > pinned_tab_count_)
max_x -= overlap_;
SetIdealBoundsAt(i,
std::min({x, ideal_x(i + 1) - stacked_padding_, max_x}));
}
}
void StackedTabStripLayout::PushTabsAfter(int index, int delta) {
for (int i = index + 1; i < tab_count(); ++i)
SetIdealBoundsAt(i, std::min(ideal_x(i) + delta, GetMaxDragX(i)));
}
void StackedTabStripLayout::PushTabsBefore(int index, int delta) {
for (int i = index - 1; i > pinned_tab_count_; --i)
SetIdealBoundsAt(i, std::max(ideal_x(i) - delta, GetMinDragX(i)));
}
void StackedTabStripLayout::LayoutForDragAfter(int index) {
for (int i = index + 1; i < tab_count(); ++i) {
const int min_x = ideal_x(i - 1) + stacked_padding_;
const int max_x = ideal_x(i - 1) + tab_offset();
SetIdealBoundsAt(i, base::clamp(ideal_x(i), min_x, max_x));
}
}
void StackedTabStripLayout::LayoutForDragBefore(int index) {
for (int i = index - 1; i >= pinned_tab_count_; --i) {
const int max_x = ideal_x(i + 1) - stacked_padding_;
const int min_x = ideal_x(i + 1) - tab_offset();
SetIdealBoundsAt(i, base::clamp(ideal_x(i), min_x, max_x));
}
if (pinned_tab_count_ == 0)
return;
// Pull in the pinned tabs.
const int delta = (pinned_tab_count_ > 1) ? ideal_x(1) - ideal_x(0) : 0;
for (int i = pinned_tab_count_ - 1; i >= 0; --i) {
gfx::Rect pinned_bounds(view_model_->ideal_bounds(i));
if (i == pinned_tab_count_ - 1)
pinned_bounds.set_x(ideal_x(i + 1) - pinned_tab_to_non_pinned_tab_);
else
pinned_bounds.set_x(ideal_x(i + 1) - delta);
view_model_->set_ideal_bounds(i, pinned_bounds);
}
}
void StackedTabStripLayout::ExpandTabsBefore(int index, int delta) {
for (int i = index - 1; i >= pinned_tab_count_ && delta > 0; --i) {
const int max_x = ideal_x(active_index()) -
stacked_padding_for_count(active_index() - i);
int to_resize = std::min(delta, max_x - ideal_x(i));
if (to_resize <= 0)
continue;
SetIdealBoundsAt(i, ideal_x(i) + to_resize);
delta -= to_resize;
LayoutForDragBefore(i);
}
}
void StackedTabStripLayout::ExpandTabsAfter(int index, int delta) {
if (index == tab_count() - 1)
return; // Nothing to expand.
for (int i = index + 1; i < tab_count() && delta > 0; ++i) {
const int min_compressed =
ideal_x(active_index()) + stacked_padding_for_count(i - active_index());
const int to_resize = std::min(ideal_x(i) - min_compressed, delta);
if (to_resize <= 0)
continue;
SetIdealBoundsAt(i, ideal_x(i) - to_resize);
delta -= to_resize;
LayoutForDragAfter(i);
}
}
void StackedTabStripLayout::AdjustStackedTabs() {
if (!requires_stacking() || tab_count() <= pinned_tab_count_ + 1)
return;
AdjustLeadingStackedTabs();
AdjustTrailingStackedTabs();
}
void StackedTabStripLayout::AdjustLeadingStackedTabs() {
int index = pinned_tab_count_ + 1;
while (index < active_index() &&
ideal_x(index) - ideal_x(index - 1) <= stacked_padding_ &&
ideal_x(index) <= x_ + max_stacked_width()) {
index++;
}
if (ideal_x(index) - ideal_x(index - 1) <= stacked_padding_ &&
ideal_x(index) <= x_ + max_stacked_width()) {
index++;
}
if (index <= pinned_tab_count_ + max_stacked_count_ - 1)
return;
int max_stacked = index;
int x = x_;
index = pinned_tab_count_;
for (; index < max_stacked - max_stacked_count_ - 1; ++index)
SetIdealBoundsAt(index, x);
for (; index < max_stacked; ++index, x += stacked_padding_)
SetIdealBoundsAt(index, x);
}
void StackedTabStripLayout::AdjustTrailingStackedTabs() {
int index = tab_count() - 1;
int max_stacked_x = width_ - size_.width() - max_stacked_width();
while (index > active_index() &&
ideal_x(index) - ideal_x(index - 1) <= stacked_padding_ &&
ideal_x(index - 1) >= max_stacked_x) {
index--;
}
if (index > active_index() &&
ideal_x(index) - ideal_x(index - 1) <= stacked_padding_ &&
ideal_x(index - 1) >= max_stacked_x) {
index--;
}
if (index >= tab_count() - max_stacked_count_)
return;
int first_stacked = index;
int x = width_ - size_.width() -
std::min(tab_count() - first_stacked, max_stacked_count_) *
stacked_padding_;
for (; index < first_stacked + max_stacked_count_;
++index, x += stacked_padding_) {
SetIdealBoundsAt(index, x);
}
for (; index < tab_count(); ++index)
SetIdealBoundsAt(index, x);
}
void StackedTabStripLayout::SetIdealBoundsAt(int index, int x) {
view_model_->set_ideal_bounds(index, gfx::Rect(gfx::Point(x, 0), size_));
}
int StackedTabStripLayout::GetMinX(int index) const {
int leading_count = index - pinned_tab_count_;
int trailing_count = tab_count() - index;
return std::max(x_ + stacked_padding_for_count(leading_count),
width_ - width_for_count(trailing_count));
}
int StackedTabStripLayout::GetMaxX(int index) const {
int trailing_offset = stacked_padding_for_count(tab_count() - index - 1);
int leading_size = width_for_count(index - pinned_tab_count_) + x_;
if (index > pinned_tab_count_)
leading_size -= overlap_;
return std::min(width_ - trailing_offset - size_.width(), leading_size);
}
int StackedTabStripLayout::GetMinDragX(int index) const {
return x_ + stacked_padding_for_count(index - pinned_tab_count_);
}
int StackedTabStripLayout::GetMaxDragX(int index) const {
const int trailing_offset =
stacked_padding_for_count(tab_count() - index - 1);
return width_ - trailing_offset - size_.width();
}
int StackedTabStripLayout::GetMinXCompressed(int index) const {
DCHECK_GT(index, active_index());
return std::max(
width_ - width_for_count(tab_count() - index),
ideal_x(active_index()) +
stacked_padding_for_count(index - active_index()));
}