blob: 6f5a05c157f77aacb7d8936074819beddf4d23c9 [file] [log] [blame]
// Copyright 2019 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/tab_strip_layout_helper.h"
#include <algorithm>
#include <memory>
#include <set>
#include <utility>
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/tabs/tab_style.h"
#include "chrome/browser/ui/tabs/tab_types.h"
#include "chrome/browser/ui/views/tabs/tab.h"
#include "chrome/browser/ui/views/tabs/tab_animation.h"
#include "chrome/browser/ui/views/tabs/tab_animation_state.h"
#include "chrome/browser/ui/views/tabs/tab_group_header.h"
#include "chrome/browser/ui/views/tabs/tab_slot_view.h"
#include "chrome/browser/ui/views/tabs/tab_strip_controller.h"
#include "chrome/browser/ui/views/tabs/tab_strip_layout_types.h"
#include "chrome/browser/ui/views/tabs/tab_style_views.h"
#include "ui/base/material_design/material_design_controller.h"
#include "ui/views/view_model.h"
namespace {
// TODO(965227): Align animation ticks to compositor events.
constexpr base::TimeDelta kTickInterval =
base::TimeDelta::FromMilliseconds(1000 / 60.0);
// The types of TabSlotView that can be referenced by TabSlot.
enum class ViewType {
kTab,
kGroupHeader,
};
TabLayoutConstants GetTabLayoutConstants() {
return {GetLayoutConstant(TAB_HEIGHT), TabStyle::GetTabOverlap()};
}
} // namespace
struct TabStripLayoutHelper::TabSlot {
static TabStripLayoutHelper::TabSlot CreateForTab(
Tab* tab,
TabOpen open,
TabPinned pinned,
base::OnceClosure removed_callback) {
TabStripLayoutHelper::TabSlot slot;
slot.type = ViewType::kTab;
slot.view = tab;
TabAnimationState initial_state = TabAnimationState::ForIdealTabState(
open, pinned, TabActive::kInactive, 0);
slot.animation = std::make_unique<TabAnimation>(
initial_state, std::move(removed_callback));
return slot;
}
static TabStripLayoutHelper::TabSlot CreateForGroupHeader(
TabGroupId group,
TabGroupHeader* header,
TabPinned pinned,
base::OnceClosure removed_callback) {
TabStripLayoutHelper::TabSlot slot;
slot.type = ViewType::kGroupHeader;
slot.view = header;
TabAnimationState initial_state = TabAnimationState::ForIdealTabState(
TabOpen::kOpen, pinned, TabActive::kInactive, 0);
slot.animation = std::make_unique<TabAnimation>(
initial_state, std::move(removed_callback));
return slot;
}
ViewType type;
TabSlotView* view;
std::unique_ptr<TabAnimation> animation;
};
TabStripLayoutHelper::TabStripLayoutHelper(
const TabStripController* controller,
GetTabsCallback get_tabs_callback,
GetGroupHeadersCallback get_group_headers_callback,
base::RepeatingClosure on_animation_progressed)
: controller_(controller),
get_tabs_callback_(get_tabs_callback),
get_group_headers_callback_(get_group_headers_callback),
on_animation_progressed_(on_animation_progressed),
active_tab_width_(TabStyle::GetStandardWidth()),
inactive_tab_width_(TabStyle::GetStandardWidth()),
first_non_pinned_tab_index_(0),
first_non_pinned_tab_x_(0) {}
TabStripLayoutHelper::~TabStripLayoutHelper() = default;
std::vector<Tab*> TabStripLayoutHelper::GetTabs() {
std::vector<Tab*> tabs;
for (const TabSlot& slot : slots_) {
if (slot.type == ViewType::kTab)
tabs.push_back(static_cast<Tab*>(slot.view));
}
return tabs;
}
bool TabStripLayoutHelper::IsAnimating() const {
return animation_timer_.IsRunning();
}
int TabStripLayoutHelper::GetPinnedTabCount() const {
views::ViewModelT<Tab>* tabs = get_tabs_callback_.Run();
int pinned_count = 0;
while (pinned_count < tabs->view_size() &&
tabs->view_at(pinned_count)->data().pinned) {
pinned_count++;
}
return pinned_count;
}
void TabStripLayoutHelper::InsertTabAtNoAnimation(
int model_index,
Tab* tab,
base::OnceClosure tab_removed_callback,
TabPinned pinned) {
const int slot_index =
GetSlotIndexForTabModelIndex(model_index, tab->group());
slots_.insert(slots_.begin() + slot_index,
TabSlot::CreateForTab(tab, TabOpen::kOpen, pinned,
std::move(tab_removed_callback)));
}
void TabStripLayoutHelper::InsertTabAt(int model_index,
Tab* tab,
base::OnceClosure tab_removed_callback,
TabPinned pinned) {
const int slot_index =
GetSlotIndexForTabModelIndex(model_index, tab->group());
slots_.insert(slots_.begin() + slot_index,
TabSlot::CreateForTab(tab, TabOpen::kClosed, pinned,
std::move(tab_removed_callback)));
AnimateSlot(slot_index, slots_[slot_index].animation->target_state().WithOpen(
TabOpen::kOpen));
}
void TabStripLayoutHelper::RemoveTabNoAnimation(int model_index, Tab* tab) {
TabAnimation* animation =
slots_[GetSlotIndexForTabModelIndex(model_index, tab->group())]
.animation.get();
animation->AnimateTo(animation->target_state().WithOpen(TabOpen::kClosed));
animation->CompleteAnimation();
}
void TabStripLayoutHelper::RemoveTab(int model_index, Tab* tab) {
int slot_index = GetSlotIndexForTabModelIndex(model_index, tab->group());
if (WidthsConstrainedForClosingMode()) {
if (slot_index == int{slots_.size()} - 1) {
// Removing the last tab. Constrain available width to place the next
// tab to the left under the cursor.
if (!tabstrip_width_override_.has_value()) {
// We are currently constrained by tab_width_override_. Express the
// equivalent constraint with tabstrip_width_override_.
tabstrip_width_override_ = LayoutTabs(base::nullopt);
}
tab_width_override_.reset();
} else {
// Removing a tab other than the last tab. Constrain the width of each
// tab to place the next tab to the right under the cursor.
if (!tab_width_override_.has_value()) {
// We are currently constrained by tabstrip_width_override_. Express
// the equivalent constraint with tab_width_override_.
tab_width_override_ = CalculateTabWidthOverride(
GetTabLayoutConstants(), GetCurrentTabWidthConstraints(),
tabstrip_width_override_.value());
}
tabstrip_width_override_.reset();
}
}
AnimateSlot(slot_index, slots_[slot_index].animation->target_state().WithOpen(
TabOpen::kClosed));
}
void TabStripLayoutHelper::EnterTabClosingMode(int available_width) {
if (!WidthsConstrainedForClosingMode()) {
tab_width_override_ = CalculateTabWidthOverride(
GetTabLayoutConstants(), GetCurrentTabWidthConstraints(),
available_width);
tabstrip_width_override_ = available_width;
}
}
base::Optional<int> TabStripLayoutHelper::ExitTabClosingMode() {
if (!WidthsConstrainedForClosingMode())
return base::nullopt;
int available_width = LayoutTabs(base::nullopt);
tab_width_override_.reset();
tabstrip_width_override_.reset();
return available_width;
}
void TabStripLayoutHelper::OnTabDestroyed(Tab* tab) {
auto it =
std::find_if(slots_.begin(), slots_.end(), [tab](const TabSlot& slot) {
return slot.type == ViewType::kTab && slot.view == tab;
});
// Remove the tab from |slots_| if it is still there. It will have already
// been removed if the tab was destroyed by |RemoveClosedTabs|.
if (it != slots_.end())
slots_.erase(it);
}
void TabStripLayoutHelper::MoveTab(base::Optional<TabGroupId> moving_tab_group,
int prev_index,
int new_index) {
const int prev_slot_index =
GetSlotIndexForTabModelIndex(prev_index, moving_tab_group);
TabSlot moving_tab = std::move(slots_[prev_slot_index]);
slots_.erase(slots_.begin() + prev_slot_index);
const int new_slot_index =
GetSlotIndexForTabModelIndex(new_index, moving_tab_group);
slots_.insert(slots_.begin() + new_slot_index, std::move(moving_tab));
if (moving_tab_group.has_value())
UpdateGroupHeaderIndex(moving_tab_group.value());
}
void TabStripLayoutHelper::SetTabPinned(int model_index, TabPinned pinned) {
// TODO(958173): Animate state change.
views::ViewModelT<Tab>* tabs = get_tabs_callback_.Run();
TabAnimation* animation =
slots_[GetSlotIndexForTabModelIndex(model_index,
tabs->view_at(model_index)->group())]
.animation.get();
animation->AnimateTo(animation->target_state().WithPinned(pinned));
animation->CompleteAnimation();
}
void TabStripLayoutHelper::InsertGroupHeader(
TabGroupId group,
TabGroupHeader* header,
base::OnceClosure header_removed_callback) {
// TODO(958173): Animate open.
std::vector<int> tabs_in_group = controller_->ListTabsInGroup(group);
const int header_slot_index =
GetSlotIndexForTabModelIndex(tabs_in_group[0], group);
slots_.insert(
slots_.begin() + header_slot_index,
TabSlot::CreateForGroupHeader(group, header, TabPinned::kUnpinned,
std::move(header_removed_callback)));
}
void TabStripLayoutHelper::RemoveGroupHeader(TabGroupId group) {
// TODO(958173): Animate closed.
const int slot_index = GetSlotIndexForGroupHeader(group);
slots_[slot_index].animation->NotifyCloseCompleted();
slots_.erase(slots_.begin() + slot_index);
}
void TabStripLayoutHelper::UpdateGroupHeaderIndex(TabGroupId group) {
const int slot_index = GetSlotIndexForGroupHeader(group);
TabSlot header_slot = std::move(slots_[slot_index]);
slots_.erase(slots_.begin() + slot_index);
std::vector<int> tabs_in_group = controller_->ListTabsInGroup(group);
const int first_tab_slot_index =
GetSlotIndexForTabModelIndex(tabs_in_group[0], group);
slots_.insert(slots_.begin() + first_tab_slot_index, std::move(header_slot));
}
void TabStripLayoutHelper::SetActiveTab(int prev_active_index,
int new_active_index) {
views::ViewModelT<Tab>* tabs = get_tabs_callback_.Run();
// Set active state without animating by retargeting the existing animation.
if (prev_active_index >= 0) {
const int prev_slot_index = GetSlotIndexForTabModelIndex(
prev_active_index, tabs->view_at(prev_active_index)->group());
TabAnimation* animation = slots_[prev_slot_index].animation.get();
animation->RetargetTo(
animation->target_state().WithActive(TabActive::kInactive));
}
if (new_active_index >= 0) {
const int new_slot_index = GetSlotIndexForTabModelIndex(
new_active_index, tabs->view_at(new_active_index)->group());
TabAnimation* animation = slots_[new_slot_index].animation.get();
animation->RetargetTo(
animation->target_state().WithActive(TabActive::kActive));
}
}
void TabStripLayoutHelper::CompleteAnimations() {
CompleteAnimationsWithoutDestroyingTabs();
RemoveClosedTabs();
}
void TabStripLayoutHelper::CompleteAnimationsWithoutDestroyingTabs() {
for (TabSlot& slot : slots_)
slot.animation->CompleteAnimation();
animation_timer_.Stop();
}
void TabStripLayoutHelper::UpdateIdealBounds(int available_width) {
int tabstrip_width = tabstrip_width_override_.value_or(available_width);
DCHECK_LE(tabstrip_width, available_width);
views::ViewModelT<Tab>* tabs = get_tabs_callback_.Run();
std::map<TabGroupId, TabGroupHeader*> group_headers =
get_group_headers_callback_.Run();
const int active_tab_model_index = controller_->GetActiveIndex();
const int active_tab_slot_index =
controller_->IsValidIndex(active_tab_model_index)
? GetSlotIndexForTabModelIndex(
active_tab_model_index,
tabs->view_at(active_tab_model_index)->group())
: TabStripModel::kNoTab;
const int pinned_tab_count = GetPinnedTabCount();
const int last_pinned_tab_index = pinned_tab_count - 1;
const int last_pinned_tab_slot_index =
pinned_tab_count > 0 ? GetSlotIndexForTabModelIndex(
last_pinned_tab_index,
tabs->view_at(last_pinned_tab_index)->group())
: TabStripModel::kNoTab;
TabLayoutConstants layout_constants = GetTabLayoutConstants();
std::vector<TabWidthConstraints> tab_widths;
for (int i = 0; i < int{slots_.size()}; i++) {
auto active =
i == active_tab_slot_index ? TabActive::kActive : TabActive::kInactive;
auto pinned = i <= last_pinned_tab_slot_index ? TabPinned::kPinned
: TabPinned::kUnpinned;
auto open =
slots_[i].animation->IsClosing() ? TabOpen::kClosed : TabOpen::kOpen;
TabAnimationState ideal_animation_state =
TabAnimationState::ForIdealTabState(open, pinned, active, 0);
TabSizeInfo size_info = slots_[i].view->GetTabSizeInfo();
tab_widths.push_back(TabWidthConstraints(ideal_animation_state,
layout_constants, size_info));
}
const std::vector<gfx::Rect> bounds = CalculateTabBounds(
layout_constants, tab_widths, tabstrip_width, tab_width_override_);
DCHECK_EQ(slots_.size(), bounds.size());
int current_tab_model_index = 0;
for (int i = 0; i < int{bounds.size()}; ++i) {
const TabSlot& slot = slots_[i];
switch (slot.type) {
case ViewType::kTab:
if (!slot.animation->IsClosing()) {
tabs->set_ideal_bounds(current_tab_model_index, bounds[i]);
UpdateCachedTabWidth(i, bounds[i].width(),
i == active_tab_slot_index);
++current_tab_model_index;
}
break;
case ViewType::kGroupHeader:
slot.view->SetBoundsRect(bounds[i]);
break;
}
}
}
void TabStripLayoutHelper::UpdateIdealBoundsForPinnedTabs() {
views::ViewModelT<Tab>* tabs = get_tabs_callback_.Run();
const int pinned_tab_count = GetPinnedTabCount();
first_non_pinned_tab_index_ = pinned_tab_count;
first_non_pinned_tab_x_ = 0;
TabLayoutConstants layout_constants = GetTabLayoutConstants();
if (pinned_tab_count > 0) {
std::vector<TabWidthConstraints> tab_widths;
for (int tab_index = 0; tab_index < pinned_tab_count; tab_index++) {
TabAnimationState ideal_animation_state =
TabAnimationState::ForIdealTabState(
TabOpen::kOpen, TabPinned::kPinned, TabActive::kInactive, 0);
TabSizeInfo size_info = tabs->view_at(tab_index)->GetTabSizeInfo();
tab_widths.push_back(TabWidthConstraints(ideal_animation_state,
layout_constants, size_info));
}
const std::vector<gfx::Rect> tab_bounds =
CalculatePinnedTabBounds(layout_constants, tab_widths);
for (int i = 0; i < pinned_tab_count; ++i)
tabs->set_ideal_bounds(i, tab_bounds[i]);
}
}
int TabStripLayoutHelper::LayoutTabs(base::Optional<int> available_width) {
base::Optional<int> tabstrip_width = tabstrip_width_override_.has_value()
? tabstrip_width_override_
: available_width;
if (DCHECK_IS_ON() && tabstrip_width_override_.has_value() &&
available_width.has_value()) {
DCHECK_LE(tabstrip_width_override_.value(), available_width.value());
}
std::vector<gfx::Rect> bounds = CalculateTabBounds(
GetTabLayoutConstants(), GetCurrentTabWidthConstraints(), tabstrip_width,
tab_width_override_);
if (DCHECK_IS_ON()) {
views::ViewModelT<Tab>* tabs = get_tabs_callback_.Run();
std::map<TabGroupId, TabGroupHeader*> group_headers =
get_group_headers_callback_.Run();
int num_closing_tabs = 0;
for (Tab* tab : GetTabs()) {
if (tab->closing())
++num_closing_tabs;
}
DCHECK_EQ(bounds.size(),
tabs->view_size() + num_closing_tabs + group_headers.size());
}
int trailing_x = 0;
const int active_tab_model_index = controller_->GetActiveIndex();
int current_tab_model_index = 0;
for (size_t i = 0; i < bounds.size(); i++) {
switch (slots_[i].type) {
case ViewType::kTab: {
Tab* tab = static_cast<Tab*>(slots_[i].view);
if (!tab->dragging()) {
tab->SetBoundsRect(bounds[i]);
trailing_x = std::max(trailing_x, bounds[i].right());
// TODO(958173): We shouldn't need to update the cached widths here,
// since they're also updated in UpdateIdealBounds. However, tests
// will fail without this line; we should investigate why.
UpdateCachedTabWidth(
current_tab_model_index, bounds[i].width(),
current_tab_model_index == active_tab_model_index);
}
if (!slots_[i].animation->IsClosing())
++current_tab_model_index;
break;
}
case ViewType::kGroupHeader: {
slots_[i].view->SetBoundsRect(bounds[i]);
trailing_x = std::max(trailing_x, bounds[i].right());
break;
}
}
}
return trailing_x;
}
int TabStripLayoutHelper::GetSlotIndexForTabModelIndex(
int model_index,
base::Optional<TabGroupId> group) const {
int current_model_index = 0;
for (size_t i = 0; i < slots_.size(); i++) {
const bool model_space_index =
slots_[i].type == ViewType::kTab && !slots_[i].animation->IsClosing();
if (current_model_index == model_index) {
if (model_space_index) {
return i;
} else if (slots_[i].type == ViewType::kGroupHeader) {
// If the tab is in the header's group, then it should be to its right,
// so as to be contiguous with the group. If not, it goes to its left.
return static_cast<TabGroupHeader*>(slots_[i].view)->group() == group
? i + 1
: i;
}
}
if (model_space_index)
++current_model_index;
}
DCHECK_EQ(model_index, current_model_index);
return slots_.size();
}
int TabStripLayoutHelper::GetSlotIndexForGroupHeader(TabGroupId group) const {
for (size_t i = 0; i < slots_.size(); i++) {
if (slots_[i].type == ViewType::kGroupHeader &&
static_cast<TabGroupHeader*>(slots_[i].view)->group() == group) {
return i;
}
}
NOTREACHED();
return 0;
}
std::vector<TabWidthConstraints>
TabStripLayoutHelper::GetCurrentTabWidthConstraints() const {
TabLayoutConstants layout_constants = GetTabLayoutConstants();
std::vector<TabWidthConstraints> result;
for (const TabSlot& slot : slots_) {
result.push_back(slot.animation->GetCurrentTabWidthConstraints(
layout_constants, slot.view->GetTabSizeInfo()));
}
return result;
}
void TabStripLayoutHelper::AnimateSlot(int slots_index,
TabAnimationState target_state) {
slots_[slots_index].animation->AnimateTo(target_state);
if (!gfx::Animation::ShouldRenderRichAnimation()) {
slots_[slots_index].animation->CompleteAnimation();
on_animation_progressed_.Run();
return;
}
if (!animation_timer_.IsRunning()) {
animation_timer_.Start(FROM_HERE, kTickInterval, this,
&TabStripLayoutHelper::TickAnimations);
// Tick animations immediately so that the animation starts from the
// beginning instead of kTickInterval ms into the animation.
TickAnimations();
}
}
void TabStripLayoutHelper::TickAnimations() {
bool all_animations_completed = true;
for (const TabSlot& slot : slots_)
all_animations_completed &= slot.animation->GetTimeRemaining().is_zero();
RemoveClosedTabs();
if (all_animations_completed)
animation_timer_.Stop();
on_animation_progressed_.Run();
}
void TabStripLayoutHelper::RemoveClosedTabs() {
for (auto it = slots_.begin(); it != slots_.end();) {
if (it->animation->IsClosed()) {
// Remove the closed tab from |slots_| before invoking the callback so
// that this is in a consistent state and can be reentered.
TabSlot removed_slot = std::move(*it);
it = slots_.erase(it);
removed_slot.animation->NotifyCloseCompleted();
} else {
it++;
}
}
}
void TabStripLayoutHelper::UpdateCachedTabWidth(int tab_index,
int tab_width,
bool active) {
if (active)
active_tab_width_ = tab_width;
else
inactive_tab_width_ = tab_width;
}
bool TabStripLayoutHelper::WidthsConstrainedForClosingMode() {
return tab_width_override_.has_value() ||
tabstrip_width_override_.has_value();
}