| // 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/gfx/range/range.h" |
| #include "ui/views/view_model.h" |
| |
| namespace { |
| |
| // 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) { |
| 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); |
| return slot; |
| } |
| |
| static TabStripLayoutHelper::TabSlot CreateForGroupHeader( |
| tab_groups::TabGroupId group, |
| TabGroupHeader* header, |
| TabPinned pinned) { |
| 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); |
| return slot; |
| } |
| |
| ViewType type; |
| TabSlotView* view; |
| std::unique_ptr<TabAnimation> animation; |
| }; |
| |
| TabStripLayoutHelper::TabStripLayoutHelper(const TabStripController* controller, |
| GetTabsCallback get_tabs_callback) |
| : controller_(controller), |
| get_tabs_callback_(get_tabs_callback), |
| 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() const { |
| std::vector<Tab*> tabs; |
| for (const TabSlot& slot : slots_) { |
| if (slot.type == ViewType::kTab) |
| tabs.push_back(static_cast<Tab*>(slot.view)); |
| } |
| |
| return tabs; |
| } |
| |
| std::vector<TabSlotView*> TabStripLayoutHelper::GetTabSlotViews() const { |
| std::vector<TabSlotView*> views; |
| for (const TabSlot& slot : slots_) |
| views.push_back(slot.view); |
| return views; |
| } |
| |
| 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::InsertTabAt(int model_index, |
| Tab* tab, |
| TabPinned pinned) { |
| const int slot_index = |
| GetSlotInsertionIndexForNewTab(model_index, tab->group()); |
| slots_.insert(slots_.begin() + slot_index, |
| TabSlot::CreateForTab(tab, TabOpen::kOpen, pinned)); |
| } |
| |
| void TabStripLayoutHelper::RemoveTabAt(int model_index, Tab* tab) { |
| TabAnimation* animation = |
| slots_[GetSlotIndexForExistingTab(model_index)].animation.get(); |
| animation->AnimateTo(animation->target_state().WithOpen(TabOpen::kClosed)); |
| animation->CompleteAnimation(); |
| } |
| |
| void TabStripLayoutHelper::EnterTabClosingMode(int available_width) { |
| if (!WidthsConstrainedForClosingMode()) { |
| tab_width_override_ = CalculateTabWidthOverride( |
| GetTabLayoutConstants(), GetCurrentTabWidthConstraints(), |
| available_width); |
| tabstrip_width_override_ = available_width; |
| } |
| } |
| |
| absl::optional<int> TabStripLayoutHelper::ExitTabClosingMode() { |
| if (!WidthsConstrainedForClosingMode()) |
| return absl::nullopt; |
| |
| int available_width = CalculateIdealBounds(absl::nullopt).back().right(); |
| 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; |
| }); |
| if (it != slots_.end()) |
| slots_.erase(it); |
| } |
| |
| void TabStripLayoutHelper::MoveTab( |
| absl::optional<tab_groups::TabGroupId> moving_tab_group, |
| int prev_index, |
| int new_index) { |
| const int prev_slot_index = GetSlotIndexForExistingTab(prev_index); |
| TabSlot moving_tab = std::move(slots_[prev_slot_index]); |
| slots_.erase(slots_.begin() + prev_slot_index); |
| |
| const int new_slot_index = |
| GetSlotInsertionIndexForNewTab(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) { |
| TabAnimation* animation = |
| slots_[GetSlotIndexForExistingTab(model_index)].animation.get(); |
| animation->AnimateTo(animation->target_state().WithPinned(pinned)); |
| animation->CompleteAnimation(); |
| } |
| |
| void TabStripLayoutHelper::InsertGroupHeader(tab_groups::TabGroupId group, |
| TabGroupHeader* header) { |
| gfx::Range tabs_in_group = controller_->ListTabsInGroup(group); |
| const int header_slot_index = |
| GetSlotInsertionIndexForNewTab(tabs_in_group.start(), group); |
| slots_.insert( |
| slots_.begin() + header_slot_index, |
| TabSlot::CreateForGroupHeader(group, header, TabPinned::kUnpinned)); |
| |
| // Set the starting location of the header to something reasonable for the |
| // animation. |
| slots_[header_slot_index].view->SetBoundsRect( |
| GetTabs()[tabs_in_group.start()]->bounds()); |
| } |
| |
| void TabStripLayoutHelper::RemoveGroupHeader(tab_groups::TabGroupId group) { |
| const int slot_index = GetSlotIndexForGroupHeader(group); |
| slots_.erase(slots_.begin() + slot_index); |
| } |
| |
| void TabStripLayoutHelper::UpdateGroupHeaderIndex( |
| tab_groups::TabGroupId group) { |
| const int slot_index = GetSlotIndexForGroupHeader(group); |
| TabSlot header_slot = std::move(slots_[slot_index]); |
| |
| slots_.erase(slots_.begin() + slot_index); |
| absl::optional<int> first_tab = controller_->GetFirstTabInGroup(group); |
| DCHECK(first_tab); |
| const int first_tab_slot_index = |
| GetSlotInsertionIndexForNewTab(first_tab.value(), group); |
| slots_.insert(slots_.begin() + first_tab_slot_index, std::move(header_slot)); |
| } |
| |
| void TabStripLayoutHelper::SetActiveTab(int prev_active_index, |
| int new_active_index) { |
| // Set active state without animating by retargeting the existing animation. |
| if (prev_active_index >= 0) { |
| const int prev_slot_index = GetSlotIndexForExistingTab(prev_active_index); |
| 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 = GetSlotIndexForExistingTab(new_active_index); |
| TabAnimation* animation = slots_[new_slot_index].animation.get(); |
| animation->RetargetTo( |
| animation->target_state().WithActive(TabActive::kActive)); |
| } |
| } |
| |
| int TabStripLayoutHelper::CalculateMinimumWidth() { |
| const std::vector<gfx::Rect> bounds = CalculateIdealBounds(0); |
| |
| return bounds.empty() ? 0 : bounds.back().right(); |
| } |
| |
| int TabStripLayoutHelper::CalculatePreferredWidth() { |
| const std::vector<gfx::Rect> bounds = CalculateIdealBounds(absl::nullopt); |
| |
| return bounds.empty() ? 0 : bounds.back().right(); |
| } |
| |
| int TabStripLayoutHelper::UpdateIdealBounds(int available_width) { |
| const std::vector<gfx::Rect> bounds = CalculateIdealBounds(available_width); |
| DCHECK_EQ(slots_.size(), bounds.size()); |
| |
| views::ViewModelT<Tab>* tabs = get_tabs_callback_.Run(); |
| const int active_tab_model_index = controller_->GetActiveIndex(); |
| const int active_tab_slot_index = |
| controller_->IsValidIndex(active_tab_model_index) |
| ? GetSlotIndexForExistingTab(active_tab_model_index) |
| : TabStripModel::kNoTab; |
| |
| int current_tab_model_index = 0; |
| for (int i = 0; i < static_cast<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: |
| group_header_ideal_bounds_[slot.view->group().value()] = bounds[i]; |
| break; |
| } |
| } |
| |
| return bounds.back().right(); |
| } |
| |
| 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]); |
| } |
| } |
| |
| std::vector<gfx::Rect> TabStripLayoutHelper::CalculateIdealBounds( |
| absl::optional<int> available_width) { |
| absl::optional<int> tabstrip_width = tabstrip_width_override_.has_value() |
| ? tabstrip_width_override_ |
| : available_width; |
| |
| const int active_tab_model_index = controller_->GetActiveIndex(); |
| const int active_tab_slot_index = |
| controller_->IsValidIndex(active_tab_model_index) |
| ? GetSlotIndexForExistingTab(active_tab_model_index) |
| : 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 ? GetSlotIndexForExistingTab(last_pinned_tab_index) |
| : TabStripModel::kNoTab; |
| |
| TabLayoutConstants layout_constants = GetTabLayoutConstants(); |
| std::vector<TabWidthConstraints> tab_widths; |
| for (int i = 0; i < static_cast<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; |
| |
| // A collapsed tab animates close like a closed tab. |
| auto open = (slots_[i].animation->IsClosing() || SlotIsCollapsedTab(i)) |
| ? 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)); |
| } |
| |
| return CalculateTabBounds(layout_constants, tab_widths, tabstrip_width, |
| tab_width_override_); |
| } |
| |
| int TabStripLayoutHelper::GetSlotIndexForExistingTab(int model_index) const { |
| const int original_slot_index = |
| GetFirstSlotIndexForTabModelIndex(model_index); |
| CHECK_LT(original_slot_index, static_cast<int>(slots_.size())) |
| << "model_index = " << model_index |
| << " does not represent an existing tab"; |
| |
| int slot_index = original_slot_index; |
| |
| if (slots_[slot_index].type == ViewType::kTab) { |
| CHECK(!slots_[slot_index].animation->IsClosing()); |
| return slot_index; |
| } |
| |
| // If |slot_index| is a group header we must return the next slot that |
| // is not animating closed. |
| if (slots_[slot_index].type == ViewType::kGroupHeader) { |
| // Skip all slots animating closed. |
| do { |
| slot_index += 1; |
| } while (slot_index < static_cast<int>(slots_.size()) && |
| slots_[slot_index].animation->IsClosing()); |
| |
| // Double check we arrived at a tab. |
| CHECK_LT(slot_index, static_cast<int>(slots_.size())) |
| << "group header at " << original_slot_index |
| << " not followed by an open tab"; |
| CHECK_EQ(slots_[slot_index].type, ViewType::kTab); |
| } |
| |
| return slot_index; |
| } |
| |
| int TabStripLayoutHelper::GetSlotInsertionIndexForNewTab( |
| int new_model_index, |
| absl::optional<tab_groups::TabGroupId> group) const { |
| int slot_index = GetFirstSlotIndexForTabModelIndex(new_model_index); |
| |
| if (slot_index == static_cast<int>(slots_.size())) |
| return slot_index; |
| |
| // If |slot_index| points to a group header and the new tab's |group| |
| // matches, the tab goes to the right of the header to keep it |
| // contiguous. |
| if (slots_[slot_index].type == ViewType::kGroupHeader && |
| static_cast<const TabGroupHeader*>(slots_[slot_index].view)->group() == |
| group) { |
| return slot_index + 1; |
| } |
| |
| return slot_index; |
| } |
| |
| int TabStripLayoutHelper::GetFirstSlotIndexForTabModelIndex( |
| int model_index) const { |
| int current_model_index = 0; |
| |
| // Conceptually we assign a model index to each slot equal to the |
| // number of open tabs preceeding it. Group headers will have the same |
| // index as the tab before it, and each open tab will have the index |
| // of the previous slot plus 1. Closing tabs are not counted, and are |
| // skipped altogether. |
| // |
| // We simply return the first slot that has a matching model index. |
| for (int slot_index = 0; slot_index < static_cast<int>(slots_.size()); |
| ++slot_index) { |
| if (slots_[slot_index].animation->IsClosing()) |
| continue; |
| |
| if (model_index == current_model_index) |
| return slot_index; |
| |
| if (slots_[slot_index].type == ViewType::kTab) |
| current_model_index += 1; |
| } |
| |
| // If there's no slot in |slots_| corresponding to |model_index|, then |
| // |model_index| may represent the first tab past the end of the |
| // tabstrip. In this case we should return the first-past-the-end |
| // index in |slots_|. |
| CHECK_EQ(current_model_index, model_index) << "model_index is too large"; |
| return slots_.size(); |
| } |
| |
| int TabStripLayoutHelper::GetSlotIndexForGroupHeader( |
| tab_groups::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::UpdateCachedTabWidth(int tab_index, |
| int tab_width, |
| bool active) { |
| // If the slot is collapsed, its width should never be reported as the |
| // current active or inactive tab width - it's not even visible. |
| if (SlotIsCollapsedTab(tab_index)) |
| return; |
| 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(); |
| } |
| |
| bool TabStripLayoutHelper::SlotIsCollapsedTab(int i) const { |
| // The slot can only be collapsed if it is a tab and in a collapsed group. |
| // If the slot is indeed a tab and in a group, check the collapsed state of |
| // the group to determine if it is collapsed. |
| const absl::optional<tab_groups::TabGroupId> id = slots_[i].view->group(); |
| return (slots_[i].type == ViewType::kTab && id.has_value()) |
| ? controller_->IsGroupCollapsed(id.value()) |
| : false; |
| } |