| // Copyright 2022 The Chromium Authors |
| // 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/compound_tab_container.h" |
| |
| #include <memory> |
| |
| #include "base/functional/bind.h" |
| #include "base/trace_event/trace_event.h" |
| #include "base/types/to_address.h" |
| #include "chrome/browser/ui/tabs/tab_style.h" |
| #include "chrome/browser/ui/tabs/tab_types.h" |
| #include "chrome/browser/ui/ui_features.h" |
| #include "chrome/browser/ui/views/tabs/tab.h" |
| #include "chrome/browser/ui/views/tabs/tab_container_impl.h" |
| #include "chrome/browser/ui/views/tabs/tab_hover_card_controller.h" |
| #include "chrome/browser/ui/views/tabs/tab_scrolling_animation.h" |
| #include "chrome/browser/ui/views/tabs/tab_slot_animation_delegate.h" |
| #include "chrome/browser/ui/views/tabs/tab_slot_view.h" |
| #include "chrome/browser/ui/views/tabs/tab_strip_controller.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/base/models/list_selection_model.h" |
| #include "ui/gfx/geometry/rect_conversions.h" |
| #include "ui/views/controls/scroll_view.h" |
| #include "ui/views/layout/layout_types.h" |
| #include "ui/views/rect_based_targeting_utils.h" |
| #include "ui/views/view.h" |
| #include "ui/views/view_utils.h" |
| |
| namespace { |
| class PinnedTabContainerController final : public TabContainerController { |
| public: |
| explicit PinnedTabContainerController( |
| TabContainerController& base_controller, |
| CompoundTabContainer& compound_tab_container) |
| : base_controller_(base_controller), |
| compound_tab_container_(compound_tab_container) {} |
| |
| ~PinnedTabContainerController() override = default; |
| |
| bool IsValidModelIndex(int index) const override { |
| return base_controller_->IsValidModelIndex(index) && |
| index < NumPinnedTabsInModel(); |
| } |
| |
| std::optional<int> GetActiveIndex() const override { |
| const std::optional<int> active_index = base_controller_->GetActiveIndex(); |
| if (active_index.has_value() && !IsValidModelIndex(active_index.value())) { |
| return std::nullopt; |
| } |
| return base_controller_->GetActiveIndex(); |
| } |
| |
| int NumPinnedTabsInModel() const override { |
| return base_controller_->NumPinnedTabsInModel(); |
| } |
| |
| void OnDropIndexUpdate(const std::optional<int> index, |
| const bool drop_before) override { |
| base_controller_->OnDropIndexUpdate(index, drop_before); |
| } |
| |
| bool IsGroupCollapsed(const tab_groups::TabGroupId& group) const override { |
| NOTREACHED(); // Pinned container can't have groups. |
| } |
| |
| std::optional<int> GetFirstTabInGroup( |
| const tab_groups::TabGroupId& group) const override { |
| NOTREACHED(); // Pinned container can't have groups. |
| } |
| |
| gfx::Range ListTabsInGroup( |
| const tab_groups::TabGroupId& group) const override { |
| NOTREACHED(); // Pinned container can't have groups. |
| } |
| |
| bool CanExtendDragHandle() const override { |
| return base_controller_->CanExtendDragHandle(); |
| } |
| |
| const views::View* GetTabClosingModeMouseWatcherHostView() const override { |
| return base_controller_->GetTabClosingModeMouseWatcherHostView(); |
| } |
| |
| bool IsAnimatingInTabStrip() const override { |
| return base_controller_->IsAnimatingInTabStrip(); |
| } |
| |
| bool IsBrowserClosing() const override { |
| return base_controller_->IsBrowserClosing(); |
| } |
| |
| void UpdateAnimationTarget(TabSlotView* tab_slot_view, |
| gfx::Rect target_bounds) override { |
| compound_tab_container_->UpdateAnimationTarget(tab_slot_view, target_bounds, |
| TabPinned::kPinned); |
| } |
| |
| private: |
| const raw_ref<TabContainerController> base_controller_; |
| const raw_ref<CompoundTabContainer> compound_tab_container_; |
| }; |
| |
| class UnpinnedTabContainerController final : public TabContainerController { |
| public: |
| explicit UnpinnedTabContainerController( |
| TabContainerController& base_controller, |
| CompoundTabContainer& compound_tab_container) |
| : base_controller_(base_controller), |
| compound_tab_container_(compound_tab_container) {} |
| |
| ~UnpinnedTabContainerController() override = default; |
| |
| bool IsValidModelIndex(int index) const override { |
| return ContainerToModelIndex(index).has_value(); |
| } |
| |
| std::optional<int> GetActiveIndex() const override { |
| const std::optional<int> base_model_active_index = |
| base_controller_->GetActiveIndex(); |
| if (base_model_active_index.has_value()) { |
| return ModelToContainerIndex(base_model_active_index.value()); |
| } |
| return std::nullopt; |
| } |
| |
| int NumPinnedTabsInModel() const override { return 0; } |
| |
| void OnDropIndexUpdate(const std::optional<int> index, |
| const bool drop_before) override { |
| // We can't use ContainerIndexToModelIndex here because `index` might be |
| // after the last tab (i.e. the drop would open a new tab at the end of the |
| // tabstrip). |
| std::optional<int> model_index = std::nullopt; |
| if (index.has_value() && index > 0) { |
| model_index = index.value() + base_controller_->NumPinnedTabsInModel(); |
| // The adjusted index must be either a valid index in the model, or be the |
| // next index after the end of the model. |
| CHECK(base_controller_->IsValidModelIndex(model_index.value()) || |
| base_controller_->IsValidModelIndex(model_index.value() - 1)); |
| } |
| |
| base_controller_->OnDropIndexUpdate(model_index, drop_before); |
| } |
| |
| bool IsGroupCollapsed(const tab_groups::TabGroupId& group) const override { |
| return base_controller_->IsGroupCollapsed(group); |
| } |
| |
| std::optional<int> GetFirstTabInGroup( |
| const tab_groups::TabGroupId& group) const override { |
| const std::optional<int> model_index = |
| base_controller_->GetFirstTabInGroup(group); |
| if (!model_index) { |
| return std::nullopt; |
| } |
| return ModelToContainerIndex(model_index.value()); |
| } |
| |
| gfx::Range ListTabsInGroup( |
| const tab_groups::TabGroupId& group) const override { |
| const gfx::Range model_range = base_controller_->ListTabsInGroup(group); |
| return gfx::Range(ModelToContainerIndex(model_range.start()).value(), |
| ModelToContainerIndex(model_range.end() - 1).value()); |
| } |
| |
| bool CanExtendDragHandle() const override { |
| return base_controller_->CanExtendDragHandle(); |
| } |
| |
| const views::View* GetTabClosingModeMouseWatcherHostView() const override { |
| return base_controller_->GetTabClosingModeMouseWatcherHostView(); |
| } |
| |
| bool IsAnimatingInTabStrip() const override { |
| return base_controller_->IsAnimatingInTabStrip(); |
| } |
| |
| bool IsBrowserClosing() const override { |
| return base_controller_->IsBrowserClosing(); |
| } |
| |
| void UpdateAnimationTarget(TabSlotView* tab_slot_view, |
| gfx::Rect target_bounds) override { |
| compound_tab_container_->UpdateAnimationTarget(tab_slot_view, target_bounds, |
| TabPinned::kUnpinned); |
| } |
| |
| private: |
| std::optional<int> ModelToContainerIndex(int model_index) const { |
| if (model_index < base_controller_->NumPinnedTabsInModel() || |
| !base_controller_->IsValidModelIndex(model_index)) { |
| return std::nullopt; |
| } |
| return model_index - base_controller_->NumPinnedTabsInModel(); |
| } |
| |
| std::optional<int> ContainerToModelIndex(int container_index) const { |
| if (container_index < 0) { |
| return std::nullopt; |
| } |
| const int model_index = |
| container_index + base_controller_->NumPinnedTabsInModel(); |
| if (!base_controller_->IsValidModelIndex(model_index)) { |
| return std::nullopt; |
| } |
| return model_index; |
| } |
| |
| const raw_ref<TabContainerController> base_controller_; |
| const raw_ref<CompoundTabContainer> compound_tab_container_; |
| }; |
| |
| // Animates tabs being pinned or unpinned, then hands them back to |
| // `tab_container_`. |
| class PinUnpinAnimationDelegate : public TabSlotAnimationDelegate { |
| public: |
| PinUnpinAnimationDelegate(TabContainer* tab_container, TabSlotView* slot_view) |
| : TabSlotAnimationDelegate(tab_container, slot_view) {} |
| PinUnpinAnimationDelegate(const PinUnpinAnimationDelegate&) = delete; |
| PinUnpinAnimationDelegate& operator=(const PinUnpinAnimationDelegate&) = |
| delete; |
| ~PinUnpinAnimationDelegate() override = default; |
| |
| void AnimationEnded(const gfx::Animation* animation) override { |
| TabSlotAnimationDelegate::AnimationEnded(animation); |
| tab_container()->ReturnTabSlotView(slot_view()); |
| } |
| }; |
| } // namespace |
| |
| CompoundTabContainer::CompoundTabContainer( |
| TabContainerController& controller, |
| TabHoverCardController* hover_card_controller, |
| TabDragContextBase* drag_context, |
| TabSlotController& tab_slot_controller, |
| views::View* scroll_contents_view) |
| : controller_(controller), |
| pinned_tab_container_controller_( |
| std::make_unique<PinnedTabContainerController>(controller, *this)), |
| pinned_tab_container_(*AddChildView(std::make_unique<TabContainerImpl>( |
| *(pinned_tab_container_controller_.get()), |
| hover_card_controller, |
| drag_context, |
| tab_slot_controller, |
| scroll_contents_view))), |
| unpinned_tab_container_controller_( |
| std::make_unique<UnpinnedTabContainerController>(controller, *this)), |
| unpinned_tab_container_(*AddChildView(std::make_unique<TabContainerImpl>( |
| *(unpinned_tab_container_controller_.get()), |
| hover_card_controller, |
| drag_context, |
| tab_slot_controller, |
| scroll_contents_view))), |
| hover_card_controller_(hover_card_controller), |
| scroll_contents_view_(scroll_contents_view), |
| bounds_animator_(this) { |
| SetEventTargeter(std::make_unique<views::ViewTargeter>(this)); |
| } |
| |
| CompoundTabContainer::~CompoundTabContainer() { |
| // Tabs call back up to the TabStrip during animation end and destruction. |
| // Ensure that happens now so we aren't in a half-destructed state when they |
| // do so. |
| CancelAnimation(); |
| RemoveChildViewT(base::to_address(pinned_tab_container_)); |
| RemoveChildViewT(base::to_address(unpinned_tab_container_)); |
| } |
| |
| void CompoundTabContainer::SetAvailableWidthCallback( |
| base::RepeatingCallback<int()> available_width_callback) { |
| // Store this ourselves, and let our child containers fall back to calling |
| // GetAvailableSize. |
| available_width_callback_ = available_width_callback; |
| } |
| |
| std::vector<Tab*> CompoundTabContainer::AddTabs( |
| std::vector<TabInsertionParams> tabs_params) { |
| std::vector<Tab*> added_tabs; |
| |
| // Assume all tabs are either pinned or unpinned |
| if (!tabs_params.empty()) { |
| if (tabs_params[0].pinned == TabPinned::kPinned) { |
| for (const TabInsertionParams& param : tabs_params) { |
| CHECK_EQ(param.pinned, TabPinned::kPinned); |
| CHECK_LE(param.model_index, NumPinnedTabs()); |
| } |
| |
| added_tabs = pinned_tab_container_->AddTabs(std::move(tabs_params)); |
| } else { |
| for (auto& param : tabs_params) { |
| CHECK_EQ(param.pinned, TabPinned::kUnpinned); |
| CHECK_GE(param.model_index, NumPinnedTabs()); |
| param.model_index -= NumPinnedTabs(); |
| } |
| added_tabs = unpinned_tab_container_->AddTabs(std::move(tabs_params)); |
| } |
| } |
| |
| return added_tabs; |
| } |
| |
| void CompoundTabContainer::MoveTab(int from_model_index, int to_model_index) { |
| const bool prev_pinned = from_model_index < NumPinnedTabs(); |
| // The tab's TabData has already been updated at this point to reflect its |
| // next pinned status. Consistency with `to_model_index` is verified below. |
| const bool next_pinned = GetTabAtModelIndex(from_model_index)->data().pinned; |
| |
| // If the tab was pinned/unpinned as part of this move, we will need to |
| // transfer it between our TabContainers. |
| if (prev_pinned != next_pinned) { |
| TransferTabBetweenContainers(from_model_index, to_model_index); |
| } else if (prev_pinned) { |
| CHECK_LT(to_model_index, NumPinnedTabs()); |
| pinned_tab_container_->MoveTab(from_model_index, to_model_index); |
| } else { // !prev_pinned |
| CHECK_GE(to_model_index, NumPinnedTabs()); |
| unpinned_tab_container_->MoveTab(from_model_index - NumPinnedTabs(), |
| to_model_index - NumPinnedTabs()); |
| } |
| } |
| |
| void CompoundTabContainer::RemoveTab(int index, bool was_active) { |
| CHECK(IsValidViewModelIndex(index)) << index; |
| if (index < NumPinnedTabs()) { |
| pinned_tab_container_->RemoveTab(index, was_active); |
| } else { |
| unpinned_tab_container_->RemoveTab(index - NumPinnedTabs(), was_active); |
| } |
| } |
| |
| void CompoundTabContainer::SetTabPinned(int model_index, TabPinned pinned) { |
| // This method does not support reorders, so the tab must already be at a |
| // location that can hold either a pinned or an unpinned tab, i.e. the border |
| // between the pinned and unpinned subsets. |
| CHECK_EQ(model_index, |
| pinned == TabPinned::kPinned ? NumPinnedTabs() : NumPinnedTabs() - 1) |
| << "Cannot " << (pinned == TabPinned::kPinned ? "pin" : "unpin") |
| << " the tab at model index " << model_index << " when there are " |
| << NumPinnedTabs() << " pinned tabs without moving that tab." |
| << " Use MoveTab to move and (un)pin a tab at the same time."; |
| // The tab's data must already have been updated. |
| DCHECK_EQ(pinned == TabPinned::kPinned, |
| GetTabAtModelIndex(model_index)->data().pinned); |
| TransferTabBetweenContainers(model_index, model_index); |
| } |
| |
| void CompoundTabContainer::SetActiveTab( |
| std::optional<size_t> prev_active_index, |
| std::optional<size_t> new_active_index) { |
| std::optional<size_t> prev_pinned_active_index; |
| std::optional<size_t> new_pinned_active_index; |
| std::optional<size_t> prev_unpinned_active_index; |
| std::optional<size_t> new_unpinned_active_index; |
| if (prev_active_index.has_value()) { |
| if (prev_active_index < static_cast<size_t>(NumPinnedTabs())) { |
| prev_pinned_active_index = prev_active_index; |
| } else { |
| prev_unpinned_active_index = prev_active_index.value() - NumPinnedTabs(); |
| } |
| } |
| if (new_active_index.has_value()) { |
| if (new_active_index < static_cast<size_t>(NumPinnedTabs())) { |
| new_pinned_active_index = new_active_index; |
| } else { |
| new_unpinned_active_index = new_active_index.value() - NumPinnedTabs(); |
| } |
| } |
| |
| pinned_tab_container_->SetActiveTab(prev_pinned_active_index, |
| new_pinned_active_index); |
| unpinned_tab_container_->SetActiveTab(prev_unpinned_active_index, |
| new_unpinned_active_index); |
| } |
| |
| Tab* CompoundTabContainer::RemoveTabFromViewModel(int model_index) { |
| // TODO(crbug.com/40882151): This only needs to be implemented in |
| // TabContainerImpl. |
| NOTREACHED(); |
| } |
| |
| Tab* CompoundTabContainer::AddTabToViewModel(Tab* tab, |
| int model_index, |
| TabPinned pinned) { |
| // TODO(crbug.com/40882151): This only needs to be implemented in |
| // TabContainerImpl. |
| NOTREACHED(); |
| } |
| |
| void CompoundTabContainer::ReturnTabSlotView(TabSlotView* view) { |
| GetTabContainerFor(view).ReturnTabSlotView(view); |
| } |
| |
| void CompoundTabContainer::ScrollTabToVisible(int model_index) { |
| // TODO(crbug.com/40060338): Implement. I guess. |
| } |
| |
| void CompoundTabContainer::ScrollTabContainerByOffset(int offset) { |
| std::optional<gfx::Rect> visible_content_rect = GetVisibleContentRect(); |
| if (!visible_content_rect.has_value() || offset == 0) { |
| return; |
| } |
| |
| // If tabcontainer is scrolled towards trailing tab, the start edge should |
| // have the x coordinate of the right bound. If it is scrolled towards the |
| // leading tab it should have the x coordinate of the left bound. |
| int start_edge = |
| (offset > 0) ? visible_content_rect->right() : visible_content_rect->x(); |
| |
| AnimateScrollToShowXCoordinate(start_edge, start_edge + offset); |
| } |
| |
| void CompoundTabContainer::OnGroupCreated(const tab_groups::TabGroupId& group) { |
| unpinned_tab_container_->OnGroupCreated(group); |
| } |
| |
| void CompoundTabContainer::OnGroupEditorOpened( |
| const tab_groups::TabGroupId& group) { |
| unpinned_tab_container_->OnGroupEditorOpened(group); |
| } |
| |
| void CompoundTabContainer::OnGroupMoved(const tab_groups::TabGroupId& group) { |
| unpinned_tab_container_->OnGroupMoved(group); |
| } |
| |
| void CompoundTabContainer::OnGroupContentsChanged( |
| const tab_groups::TabGroupId& group) { |
| unpinned_tab_container_->OnGroupContentsChanged(group); |
| } |
| |
| void CompoundTabContainer::OnGroupVisualsChanged( |
| const tab_groups::TabGroupId& group, |
| const tab_groups::TabGroupVisualData* old_visuals, |
| const tab_groups::TabGroupVisualData* new_visuals) { |
| unpinned_tab_container_->OnGroupVisualsChanged(group, old_visuals, |
| new_visuals); |
| } |
| |
| void CompoundTabContainer::ToggleTabGroup( |
| const tab_groups::TabGroupId& group, |
| bool is_collapsing, |
| ToggleTabGroupCollapsedStateOrigin origin) { |
| unpinned_tab_container_->ToggleTabGroup(group, is_collapsing, origin); |
| } |
| |
| void CompoundTabContainer::OnGroupClosed(const tab_groups::TabGroupId& group) { |
| unpinned_tab_container_->OnGroupClosed(group); |
| } |
| |
| void CompoundTabContainer::UpdateTabGroupVisuals( |
| tab_groups::TabGroupId group_id) { |
| unpinned_tab_container_->UpdateTabGroupVisuals(group_id); |
| } |
| |
| void CompoundTabContainer::NotifyTabstripBubbleOpened() { |
| unpinned_tab_container_->NotifyTabstripBubbleOpened(); |
| } |
| |
| void CompoundTabContainer::NotifyTabstripBubbleClosed() { |
| unpinned_tab_container_->NotifyTabstripBubbleClosed(); |
| } |
| |
| void CompoundTabContainer::OnSplitCreated(const std::vector<int>& indices) { |
| OnSplitChanged(indices, &TabContainer::OnSplitCreated); |
| } |
| |
| void CompoundTabContainer::OnSplitRemoved(const std::vector<int>& indices) { |
| OnSplitChanged(indices, &TabContainer::OnSplitRemoved); |
| } |
| |
| void CompoundTabContainer::OnSplitContentsChanged( |
| const std::vector<int>& indices) { |
| OnSplitChanged(indices, &TabContainer::OnSplitContentsChanged); |
| } |
| |
| std::optional<int> CompoundTabContainer::GetModelIndexOf( |
| const TabSlotView* slot_view) const { |
| const std::optional<int> unpinned_index = |
| unpinned_tab_container_->GetModelIndexOf(slot_view); |
| if (unpinned_index.has_value()) { |
| return unpinned_index.value() + NumPinnedTabs(); |
| } |
| return pinned_tab_container_->GetModelIndexOf(slot_view); |
| } |
| |
| Tab* CompoundTabContainer::GetTabAtModelIndex(int index) const { |
| CHECK_LT(index, GetTabCount()); |
| const int num_pinned_tabs = NumPinnedTabs(); |
| if (index < num_pinned_tabs) { |
| return pinned_tab_container_->GetTabAtModelIndex(index); |
| } |
| return unpinned_tab_container_->GetTabAtModelIndex(index - num_pinned_tabs); |
| } |
| |
| int CompoundTabContainer::GetTabCount() const { |
| return pinned_tab_container_->GetTabCount() + |
| unpinned_tab_container_->GetTabCount(); |
| } |
| |
| std::optional<int> CompoundTabContainer::GetModelIndexOfFirstNonClosingTab( |
| Tab* tab) const { |
| if (tab->data().pinned) { |
| const std::optional<int> pinned_index = |
| pinned_tab_container_->GetModelIndexOfFirstNonClosingTab(tab); |
| |
| // If there are no non-closing pinned tabs after `tab`, return the first |
| // non-closing unpinned tab, if there is one (if the unpinned container is |
| // empty or only has closing tabs, GetTabCount will be 0). |
| if (!pinned_index.has_value() && |
| unpinned_tab_container_->GetTabCount() > 0) { |
| return NumPinnedTabs(); |
| } |
| return pinned_index; |
| } else { |
| const std::optional<int> unpinned_index = |
| unpinned_tab_container_->GetModelIndexOfFirstNonClosingTab(tab); |
| if (unpinned_index.has_value()) { |
| return unpinned_index.value() + NumPinnedTabs(); |
| } |
| return std::nullopt; |
| } |
| } |
| |
| void CompoundTabContainer::UpdateHoverCard( |
| Tab* tab, |
| TabSlotController::HoverCardUpdateType update_type) { |
| // Some operations (including e.g. starting a drag) can cause the tab focus |
| // to change at the same time as the tabstrip is starting to animate; the |
| // hover card should not be visible at this time. |
| // See crbug.com/1220840 for an example case. |
| if (controller_->IsAnimatingInTabStrip()) { |
| tab = nullptr; |
| update_type = TabSlotController::HoverCardUpdateType::kAnimating; |
| } |
| |
| if (!hover_card_controller_) { |
| return; |
| } |
| |
| hover_card_controller_->UpdateHoverCard(tab, update_type); |
| } |
| |
| void CompoundTabContainer::HandleLongTap(ui::GestureEvent* const event) { |
| TabContainer* const tab_container = GetTabContainerAt(event->location()); |
| if (!tab_container) { |
| return; |
| } |
| |
| ConvertEventToTarget(tab_container, event); |
| tab_container->HandleLongTap(event); |
| } |
| |
| bool CompoundTabContainer::IsRectInContentArea(const gfx::Rect& rect) { |
| if (pinned_tab_container_->IsRectInContentArea(ToEnclosingRect( |
| ConvertRectToTarget(this, base::to_address(pinned_tab_container_), |
| gfx::RectF(rect))))) { |
| return true; |
| } |
| |
| return unpinned_tab_container_->IsRectInContentArea( |
| ToEnclosingRect(ConvertRectToTarget( |
| this, base::to_address(unpinned_tab_container_), gfx::RectF(rect)))); |
| } |
| |
| std::optional<ZOrderableTabContainerElement> |
| CompoundTabContainer::GetLeadingElementForZOrdering() const { |
| // TODO(crbug.com/40882151): This only needs to be implemented in |
| // TabContainerImpl. |
| NOTREACHED(); |
| } |
| std::optional<ZOrderableTabContainerElement> |
| CompoundTabContainer::GetTrailingElementForZOrdering() const { |
| // TODO(crbug.com/40882151): This only needs to be implemented in |
| // TabContainerImpl. |
| NOTREACHED(); |
| } |
| |
| void CompoundTabContainer::OnTabSlotAnimationProgressed(TabSlotView* view) { |
| GetTabContainerFor(view).OnTabSlotAnimationProgressed(view); |
| } |
| |
| void CompoundTabContainer::OnTabCloseAnimationCompleted(Tab* tab) { |
| // TODO(crbug.com/40882151): This only needs to be implemented in |
| // TabContainerImpl. |
| NOTREACHED(); |
| } |
| |
| void CompoundTabContainer::InvalidateIdealBounds() { |
| pinned_tab_container_->InvalidateIdealBounds(); |
| unpinned_tab_container_->InvalidateIdealBounds(); |
| } |
| |
| void CompoundTabContainer::AnimateToIdealBounds() { |
| // `pinned_tab_container_` must plan its animation first so |
| // `unpinned_tab_container_` has up-to-date available width. |
| pinned_tab_container_->AnimateToIdealBounds(); |
| unpinned_tab_container_->AnimateToIdealBounds(); |
| |
| // Animate the pinning or unpinning tabs too. |
| for (views::View* child : children()) { |
| Tab* tab = views::AsViewClass<Tab>(child); |
| if (!tab) { |
| continue; |
| } |
| |
| const std::optional<int> model_index = GetModelIndexOf(tab); |
| // The tab may have been closed during a pin/unpin animation, in which case |
| // it a) has no model index and b) is already animating to its correct |
| // bounds because that will have been updated in `UpdateAnimationTarget()`. |
| if (!model_index.has_value()) { |
| continue; |
| } |
| |
| AnimateTabTo(tab, GetIdealBounds(model_index.value())); |
| } |
| } |
| |
| bool CompoundTabContainer::IsAnimating() const { |
| return bounds_animator_.IsAnimating() || |
| pinned_tab_container_->IsAnimating() || |
| unpinned_tab_container_->IsAnimating(); |
| } |
| |
| void CompoundTabContainer::CancelAnimation() { |
| pinned_tab_container_->CancelAnimation(); |
| unpinned_tab_container_->CancelAnimation(); |
| } |
| |
| void CompoundTabContainer::CompleteAnimationAndLayout() { |
| bounds_animator_.Complete(); |
| pinned_tab_container_->CompleteAnimationAndLayout(); |
| unpinned_tab_container_->CompleteAnimationAndLayout(); |
| DeprecatedLayoutImmediately(); |
| } |
| |
| int CompoundTabContainer::GetAvailableWidthForTabContainer() const { |
| // Falls back to views::View::GetAvailableSize() when |
| // `available_width_callback_` is not defined, e.g. when tab scrolling is |
| // disabled. |
| return available_width_callback_ |
| ? available_width_callback_.Run() |
| : parent()->GetAvailableSize(this).width().value(); |
| } |
| |
| void CompoundTabContainer::EnterTabClosingMode( |
| std::optional<int> override_width, |
| CloseTabSource source) { |
| if (override_width.has_value()) { |
| override_width = override_width.value() - |
| pinned_tab_container_->GetPreferredSize().width(); |
| } |
| |
| // The pinned container can't be in closing mode, as pinned tabs don't resize. |
| unpinned_tab_container_->EnterTabClosingMode(override_width, source); |
| } |
| |
| void CompoundTabContainer::ExitTabClosingMode() { |
| // The pinned container can't be in closing mode, as pinned tabs don't resize. |
| unpinned_tab_container_->ExitTabClosingMode(); |
| } |
| |
| void CompoundTabContainer::SetTabSlotVisibility() { |
| // TODO(crbug.com/40060338): Impl |
| } |
| |
| bool CompoundTabContainer::InTabClose() { |
| // The pinned container can't be in closing mode, as pinned tabs don't resize. |
| return unpinned_tab_container_->InTabClose(); |
| } |
| |
| TabGroupViews* CompoundTabContainer::GetGroupViews( |
| tab_groups::TabGroupId group_id) const { |
| return unpinned_tab_container_->GetGroupViews(group_id); |
| } |
| |
| const std::map<tab_groups::TabGroupId, std::unique_ptr<TabGroupViews>>& |
| CompoundTabContainer::get_group_views_for_testing() const { |
| // Only the unpinned container can have groups. |
| return unpinned_tab_container_->get_group_views_for_testing(); // IN-TEST |
| } |
| |
| std::map<tab_groups::TabGroupId, TabGroupHeader*> |
| CompoundTabContainer::GetGroupHeaders() const { |
| return unpinned_tab_container_->GetGroupHeaders(); |
| } |
| |
| gfx::Rect CompoundTabContainer::GetIdealBounds(int model_index) const { |
| // Ideal bounds for pinned tabs are fine as-is. |
| if (model_index < NumPinnedTabs()) { |
| return pinned_tab_container_->GetIdealBounds(model_index); |
| } |
| |
| return ConvertUnpinnedContainerIdealBoundsToLocal( |
| unpinned_tab_container_->GetIdealBounds(model_index - NumPinnedTabs())); |
| } |
| |
| gfx::Rect CompoundTabContainer::GetIdealBounds( |
| tab_groups::TabGroupId group) const { |
| return ConvertUnpinnedContainerIdealBoundsToLocal( |
| unpinned_tab_container_->GetIdealBounds(group)); |
| } |
| |
| gfx::Size CompoundTabContainer::GetMinimumSize() const { |
| return GetCombinedSizeForTabContainerSizes( |
| pinned_tab_container_->GetMinimumSize(), |
| unpinned_tab_container_->GetMinimumSize()); |
| } |
| |
| views::SizeBounds CompoundTabContainer::GetAvailableSize( |
| const views::View* child) const { |
| if (child == base::to_address(pinned_tab_container_)) { |
| return views::SizeBounds(GetAvailableWidthForTabContainer(), |
| views::SizeBound()); |
| } |
| |
| CHECK_EQ(child, base::to_address(unpinned_tab_container_)); |
| return views::SizeBounds(GetAvailableWidthForUnpinnedTabContainer(), |
| views::SizeBound()); |
| } |
| |
| gfx::Size CompoundTabContainer::CalculatePreferredSize( |
| const views::SizeBounds& available_size) const { |
| return GetCombinedSizeForTabContainerSizes( |
| pinned_tab_container_->GetPreferredSize(), |
| unpinned_tab_container_->GetPreferredSize()); |
| } |
| |
| views::View* CompoundTabContainer::GetTooltipHandlerForPoint( |
| const gfx::Point& point) { |
| TabContainer* const sub_container = GetTabContainerAt(point); |
| return sub_container ? sub_container->GetTooltipHandlerForPoint( |
| ConvertPointToTarget(this, sub_container, point)) |
| : this; |
| } |
| |
| void CompoundTabContainer::Layout(PassKey) { |
| // Pinned container gets however much space it wants. |
| pinned_tab_container_->SetBoundsRect( |
| gfx::Rect(pinned_tab_container_->GetPreferredSize())); |
| |
| // Unpinned container can have whatever is left over. |
| const int unpinned_container_leading_x = std::max( |
| 0, pinned_tab_container_->width() - TabStyle::Get()->GetTabOverlap()); |
| const int available_width = width() - unpinned_container_leading_x; |
| |
| const gfx::Size pref_size = unpinned_tab_container_->GetPreferredSize(); |
| |
| unpinned_tab_container_->SetBounds( |
| unpinned_container_leading_x, 0, |
| std::min(available_width, pref_size.width()), pref_size.height()); |
| } |
| |
| void CompoundTabContainer::PaintChildren(const views::PaintInfo& paint_info) { |
| TRACE_EVENT1("views", "View::PaintChildren", "class", GetClassName()); |
| |
| // N.B. We override PaintChildren only to define paint order for our children. |
| // We do this instead of GetChildrenInZOrder for consistency with |
| // TabContainerImpl. |
| |
| // Paint our containers first, ordered based on their overlapping elements. |
| // I.e., the last tab in `pinned_tab_container_` will overlap the first tab |
| // (or group header) in `unpinned_tab_container_`, and to paint them in the |
| // right order, we have to paint their containers in the same order. |
| // N.B. if either are nullopt, it doesn't matter what order we paint in |
| // because that whole container must be empty and therefore won't paint |
| // anything at all. |
| std::optional<ZOrderableTabContainerElement> trailing_pinned_element = |
| pinned_tab_container_->GetTrailingElementForZOrdering(); |
| std::optional<ZOrderableTabContainerElement> leading_unpinned_element = |
| unpinned_tab_container_->GetLeadingElementForZOrdering(); |
| if (trailing_pinned_element < leading_unpinned_element) { |
| pinned_tab_container_->Paint(paint_info); |
| unpinned_tab_container_->Paint(paint_info); |
| } else { |
| unpinned_tab_container_->Paint(paint_info); |
| pinned_tab_container_->Paint(paint_info); |
| } |
| |
| // Then paint all tabs animating between pinned and unpinned, ordered based on |
| // their individual z-values. |
| std::vector<ZOrderableTabContainerElement> orderable_children; |
| for (views::View* const child : children()) { |
| if (!ZOrderableTabContainerElement::CanOrderView(child)) { |
| continue; |
| } |
| orderable_children.emplace_back(child); |
| } |
| |
| // Sort in ascending order by z-value. Stable sort breaks ties by child index. |
| std::stable_sort(orderable_children.begin(), orderable_children.end()); |
| |
| for (const ZOrderableTabContainerElement& child : orderable_children) { |
| child.view()->Paint(paint_info); |
| } |
| } |
| |
| void CompoundTabContainer::ChildPreferredSizeChanged(views::View* child) { |
| PreferredSizeChanged(); |
| } |
| |
| std::optional<BrowserRootView::DropIndex> CompoundTabContainer::GetDropIndex( |
| const ui::DropTargetEvent& event) { |
| TabContainer* sub_drop_target = GetTabContainerForDrop(event.location()); |
| CHECK(sub_drop_target); |
| CHECK(sub_drop_target->GetDropTarget( |
| ConvertPointToTarget(this, sub_drop_target, event.location()))); |
| |
| // Convert to `sub_drop_target`'s local coordinate space. |
| const gfx::Point loc_in_sub_target = ConvertPointToTarget( |
| this, sub_drop_target->GetViewForDrop(), event.location()); |
| const ui::DropTargetEvent adjusted_event = ui::DropTargetEvent( |
| event.data(), gfx::PointF(loc_in_sub_target), |
| gfx::PointF(loc_in_sub_target), event.source_operations()); |
| |
| if (sub_drop_target == base::to_address(pinned_tab_container_)) { |
| // Pinned tab container shares an index and coordinate space, so no |
| // adjustments needed. |
| return sub_drop_target->GetDropIndex(adjusted_event); |
| } else { |
| // For the unpinned container, we need to transform the output to the |
| // correct index space. |
| const std::optional<BrowserRootView::DropIndex> sub_target_index = |
| sub_drop_target->GetDropIndex(adjusted_event); |
| return BrowserRootView::DropIndex{ |
| .index = sub_target_index->index + NumPinnedTabs(), |
| .relative_to_index = sub_target_index->relative_to_index, |
| .group_inclusion = sub_target_index->group_inclusion}; |
| } |
| } |
| |
| BrowserRootView::DropTarget* CompoundTabContainer::GetDropTarget( |
| gfx::Point loc_in_local_coords) { |
| TabContainer* const sub_drop_target = |
| GetTabContainerForDrop(loc_in_local_coords); |
| |
| if (sub_drop_target == nullptr || |
| !sub_drop_target->GetDropTarget( |
| ConvertPointToTarget(this, sub_drop_target, loc_in_local_coords))) { |
| return nullptr; |
| } |
| return this; |
| } |
| |
| views::View* CompoundTabContainer::GetViewForDrop() { |
| return this; |
| } |
| |
| void CompoundTabContainer::HandleDragUpdate( |
| const std::optional<BrowserRootView::DropIndex>& index) { |
| // Update `current_text_drop_target_`. |
| TabContainer* next_drop_target = nullptr; |
| if (index.has_value()) { |
| next_drop_target = base::to_address(index->index < NumPinnedTabs() |
| ? pinned_tab_container_ |
| : unpinned_tab_container_); |
| } |
| if (next_drop_target != current_text_drop_target_) { |
| if (current_text_drop_target_) { |
| current_text_drop_target_->HandleDragExited(); |
| } |
| current_text_drop_target_ = next_drop_target; |
| } |
| |
| if (current_text_drop_target_ == nullptr) { // I.e. if `index` is nullopt. |
| return; |
| } |
| |
| // Forward to `current_text_drop_target_`, adjusting if needed. |
| if (current_text_drop_target_ == base::to_address(pinned_tab_container_)) { |
| pinned_tab_container_->HandleDragUpdate(index); |
| } else { |
| BrowserRootView::DropIndex adjusted_index{ |
| .index = index->index - NumPinnedTabs(), |
| .relative_to_index = index->relative_to_index, |
| .group_inclusion = index->group_inclusion}; |
| unpinned_tab_container_->HandleDragUpdate(adjusted_index); |
| } |
| } |
| |
| void CompoundTabContainer::HandleDragExited() { |
| if (current_text_drop_target_) { |
| current_text_drop_target_->HandleDragExited(); |
| current_text_drop_target_ = nullptr; |
| } |
| } |
| |
| views::View* CompoundTabContainer::TargetForRect(views::View* root, |
| const gfx::Rect& rect) { |
| CHECK_EQ(root, this); |
| |
| if (!views::UsePointBasedTargeting(rect)) { |
| return views::ViewTargeterDelegate::TargetForRect(root, rect); |
| } |
| |
| const gfx::Point point(rect.CenterPoint()); |
| TabContainer* const sub_container = GetTabContainerAt(point); |
| if (sub_container == nullptr) { |
| return this; |
| } |
| |
| return sub_container->GetEventHandlerForRect(ToEnclosingRect( |
| ConvertRectToTarget(this, sub_container, gfx::RectF(rect)))); |
| } |
| |
| void CompoundTabContainer::UpdateAnimationTarget(TabSlotView* tab_slot_view, |
| gfx::Rect target_bounds, |
| TabPinned pinned) { |
| if (pinned == TabPinned::kUnpinned) { |
| target_bounds = ConvertUnpinnedContainerIdealBoundsToLocal(target_bounds); |
| } |
| |
| if (tab_slot_view->parent() != this) { |
| controller_->UpdateAnimationTarget(tab_slot_view, target_bounds); |
| return; |
| } |
| |
| // We should only have tabs to deal with here, as groups cannot be pinned. |
| DCHECK(views::IsViewClass<Tab>(tab_slot_view)); |
| if (bounds_animator_.IsAnimating(tab_slot_view)) { |
| bounds_animator_.SetTargetBounds(tab_slot_view, target_bounds); |
| } |
| } |
| |
| int CompoundTabContainer::NumPinnedTabs() const { |
| return pinned_tab_container_->GetTabCount(); |
| } |
| |
| bool CompoundTabContainer::IsValidViewModelIndex(int index) const { |
| const int total_num_tabs = pinned_tab_container_->GetTabCount() + |
| unpinned_tab_container_->GetTabCount(); |
| return index >= 0 && index < total_num_tabs; |
| } |
| |
| void CompoundTabContainer::TransferTabBetweenContainers(int from_model_index, |
| int to_model_index) { |
| // If the tab at `from_model_index` is already being transferred, complete |
| // all pending transfers before we embark upon this one to avoid conflicts. |
| if (bounds_animator_.IsAnimating(GetTabAtModelIndex(from_model_index))) { |
| // We are out of sync with the model right now (because we're handling a |
| // model update), so we need to be careful here. We can complete our |
| // directly managed animations, but we can't ask the sub-containers to do |
| // the same, as their ideal bounds calculations assume the model and |
| // viewmodel are in sync. |
| bounds_animator_.Complete(); |
| } |
| |
| const bool prev_pinned = from_model_index < NumPinnedTabs(); |
| const bool next_pinned = !prev_pinned; |
| |
| const int before_num_pinned_tabs = NumPinnedTabs(); |
| const int after_num_pinned_tabs = |
| before_num_pinned_tabs + (next_pinned ? 1 : -1); |
| |
| if (next_pinned) { |
| // We are going from `unpinned_tab_container_` to `pinned_tab_container_`. |
| // Indices must be valid for those containers. If `from_model_index` == |
| // `to_model_index`, we're pinning the first unpinned tab. |
| CHECK_GE(from_model_index, before_num_pinned_tabs); |
| CHECK_LT(to_model_index, after_num_pinned_tabs); |
| } else { |
| // We are going from `pinned_tab_container_` to `unpinned_tab_container_`. |
| // Indices must be valid for those containers. If `from_model_index` == |
| // `to_model_index`, we're unpinning the last pinned tab. |
| CHECK_LT(from_model_index, before_num_pinned_tabs); |
| CHECK_GE(to_model_index, after_num_pinned_tabs); |
| } |
| |
| TabContainer& from_container = |
| *(prev_pinned ? pinned_tab_container_ : unpinned_tab_container_); |
| const int from_container_index = |
| prev_pinned ? from_model_index |
| : from_model_index - before_num_pinned_tabs; |
| TabContainer& to_container = |
| *(next_pinned ? pinned_tab_container_ : unpinned_tab_container_); |
| const int to_container_index = |
| next_pinned ? to_model_index : to_model_index - after_num_pinned_tabs; |
| |
| // Take `tab` ourselves, so we can animate it. Save and restore its bounds to |
| // ensure it doesn't move visually from its current starting bounds. |
| const gfx::RectF initial_tab_bounds = ConvertRectToTarget( |
| &from_container, this, |
| gfx::RectF( |
| from_container.GetTabAtModelIndex(from_container_index)->bounds())); |
| Tab* const tab = AddChildViewRaw( |
| from_container.RemoveTabFromViewModel(from_container_index)); |
| tab->SetBoundsRect(ToEnclosingRect(initial_tab_bounds)); |
| |
| // Let `to_container` update its layout data structures. |
| to_container.AddTabToViewModel( |
| tab, to_container_index, |
| next_pinned ? TabPinned::kPinned : TabPinned::kUnpinned); |
| |
| AnimateToIdealBounds(); |
| } |
| |
| void CompoundTabContainer::AnimateTabTo(Tab* tab, gfx::Rect ideal_bounds) { |
| if (bounds_animator_.IsAnimating(tab)) { |
| bounds_animator_.SetTargetBounds(tab, ideal_bounds); |
| } else { |
| bounds_animator_.SetAnimationDuration( |
| gfx::Animation::RichAnimationDuration(base::Milliseconds(200))); |
| bounds_animator_.AnimateViewTo(tab, ideal_bounds, |
| std::make_unique<PinUnpinAnimationDelegate>( |
| &GetTabContainerFor(tab), tab)); |
| } |
| } |
| |
| gfx::Rect CompoundTabContainer::ConvertUnpinnedContainerIdealBoundsToLocal( |
| gfx::Rect ideal_bounds) const { |
| const int unpinned_container_ideal_leading_x = |
| GetUnpinnedContainerIdealLeadingX(); |
| ideal_bounds.Offset(unpinned_container_ideal_leading_x, 0); |
| return ideal_bounds; |
| } |
| |
| TabContainer& CompoundTabContainer::GetTabContainerFor( |
| TabSlotView* view) const { |
| if (view->GetTabSlotViewType() == TabSlotView::ViewType::kTabGroupHeader) { |
| return unpinned_tab_container_.get(); |
| } |
| |
| Tab* tab = views::AsViewClass<Tab>(view); |
| return tab->data().pinned ? pinned_tab_container_.get() |
| : unpinned_tab_container_.get(); |
| } |
| |
| TabContainer* CompoundTabContainer::GetTabContainerForDrop( |
| gfx::Point point_in_local_coords) const { |
| const int cutoff_x = (pinned_tab_container_->bounds().right() + |
| unpinned_tab_container_->bounds().x()) / |
| 2; |
| |
| if (point_in_local_coords.x() < cutoff_x) { |
| return base::to_address(pinned_tab_container_); |
| } |
| return base::to_address(unpinned_tab_container_); |
| } |
| |
| TabContainer* CompoundTabContainer::GetTabContainerAt( |
| gfx::Point point_in_local_coords) const { |
| const bool in_pinned = |
| pinned_tab_container_->bounds().Contains(point_in_local_coords); |
| const bool in_unpinned = |
| unpinned_tab_container_->bounds().Contains(point_in_local_coords); |
| |
| if (in_pinned && in_unpinned) { |
| const int cutoff_x = (pinned_tab_container_->bounds().right() + |
| unpinned_tab_container_->bounds().x()) / |
| 2; |
| if (point_in_local_coords.x() < cutoff_x) { |
| return base::to_address(pinned_tab_container_); |
| } |
| return base::to_address(unpinned_tab_container_); |
| } |
| |
| if (in_pinned) { |
| return base::to_address(pinned_tab_container_); |
| } |
| if (in_unpinned) { |
| return base::to_address(unpinned_tab_container_); |
| } |
| |
| // `point_in_local_coords` might be in neither sub container if our layout is |
| // (transiently) stale, e.g. during window creation. |
| return nullptr; |
| } |
| |
| int CompoundTabContainer::GetUnpinnedContainerIdealLeadingX() const { |
| return NumPinnedTabs() > 0 |
| ? pinned_tab_container_->GetIdealBounds(NumPinnedTabs() - 1) |
| .right() - |
| TabStyle::Get()->GetTabOverlap() |
| : 0; |
| } |
| |
| int CompoundTabContainer::GetAvailableWidthForUnpinnedTabContainer() const { |
| // The unpinned container gets the width the pinned container doesn't want. |
| return GetAvailableWidthForTabContainer() - |
| GetUnpinnedContainerIdealLeadingX(); |
| } |
| |
| gfx::Size CompoundTabContainer::GetCombinedSizeForTabContainerSizes( |
| const gfx::Size pinned_size, |
| const gfx::Size unpinned_size) const { |
| gfx::Size largest_container = pinned_size; |
| largest_container.SetToMax(unpinned_size); |
| |
| const int width_with_overlap = pinned_size.width() + unpinned_size.width() - |
| TabStyle::Get()->GetTabOverlap(); |
| return gfx::Size(std::max(width_with_overlap, largest_container.width()), |
| largest_container.height()); |
| } |
| |
| std::optional<gfx::Rect> CompoundTabContainer::GetVisibleContentRect() const { |
| const views::ScrollView* const scroll_container = |
| views::ScrollView::GetScrollViewForContents(scroll_contents_view_); |
| if (!scroll_container) { |
| return std::nullopt; |
| } |
| |
| return scroll_container->GetVisibleRect(); |
| } |
| |
| void CompoundTabContainer::AnimateScrollToShowXCoordinate( |
| const int start_edge, |
| const int target_edge) { |
| if (tab_scrolling_animation_) { |
| tab_scrolling_animation_->Stop(); |
| } |
| |
| gfx::Rect start_rect(start_edge, 0, 0, 0); |
| gfx::Rect target_rect(target_edge, 0, 0, 0); |
| |
| tab_scrolling_animation_ = std::make_unique<TabScrollingAnimation>( |
| scroll_contents_view_, bounds_animator_.container(), start_rect, |
| target_rect); |
| tab_scrolling_animation_->Start(); |
| } |
| |
| void CompoundTabContainer::OnSplitChanged(const std::vector<int>& indices, |
| SplitChangedCallback callback) { |
| CHECK(!indices.empty()); |
| int pinned_count = NumPinnedTabs(); |
| |
| // All the indices are expected to either be in the pinned or unpinned |
| // container and so checking just the first index. |
| if (indices[0] < pinned_count) { |
| return ((*pinned_tab_container_).*callback)(indices); |
| } |
| |
| if (pinned_count == 0) { |
| return ((*unpinned_tab_container_).*callback)(indices); |
| } |
| |
| std::vector<int> unpinned_indices; |
| unpinned_indices.reserve(indices.size()); |
| |
| std::transform(indices.begin(), indices.end(), |
| std::back_inserter(unpinned_indices), |
| [pinned_count](int index) { return index - pinned_count; }); |
| |
| return ((*unpinned_tab_container_).*callback)(unpinned_indices); |
| } |
| |
| BEGIN_METADATA(CompoundTabContainer) |
| END_METADATA |