blob: 8faf23f2341adaa518ee6b969495d6aaa051c51b [file] [log] [blame]
// 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/tab_container_impl.h"
#include "base/bits.h"
#include "base/containers/adapters.h"
#include "base/cxx20_to_address.h"
#include "base/ranges/algorithm.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/tabs/tab.h"
#include "chrome/browser/ui/views/tabs/tab_drag_context.h"
#include "chrome/browser/ui/views/tabs/tab_drag_controller.h"
#include "chrome/browser/ui/views/tabs/tab_group_header.h"
#include "chrome/browser/ui/views/tabs/tab_group_highlight.h"
#include "chrome/browser/ui/views/tabs/tab_group_underline.h"
#include "chrome/browser/ui/views/tabs/tab_group_views.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_strip.h"
#include "chrome/browser/ui/views/tabs/tab_strip_controller.h"
#include "chrome/browser/ui/views/tabs/tab_style_views.h"
#include "chrome/browser/ui/views/tabs/z_orderable_tab_container_element.h"
#include "chrome/grit/theme_resources.h"
#include "components/tab_groups/tab_group_id.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/display/screen.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/mouse_watcher_view_host.h"
#include "ui/views/rect_based_targeting_utils.h"
#include "ui/views/view_utils.h"
namespace {
// Size of the drop indicator.
int g_drop_indicator_width = 0;
int g_drop_indicator_height = 0;
gfx::ImageSkia* GetDropArrowImage(bool is_down) {
return ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
is_down ? IDR_TAB_DROP_DOWN : IDR_TAB_DROP_UP);
}
} // namespace
///////////////////////////////////////////////////////////////////////////////
// TabContainerImpl::RemoveTabDelegate
//
// AnimationDelegate used when removing a tab. Does the necessary cleanup when
// done.
class TabContainerImpl::RemoveTabDelegate : public TabSlotAnimationDelegate {
public:
RemoveTabDelegate(TabContainer* tab_container, Tab* tab);
RemoveTabDelegate(const RemoveTabDelegate&) = delete;
RemoveTabDelegate& operator=(const RemoveTabDelegate&) = delete;
void AnimationEnded(const gfx::Animation* animation) override;
void AnimationCanceled(const gfx::Animation* animation) override;
};
TabContainerImpl::RemoveTabDelegate::RemoveTabDelegate(
TabContainer* tab_container,
Tab* tab)
: TabSlotAnimationDelegate(tab_container, tab) {}
void TabContainerImpl::RemoveTabDelegate::AnimationEnded(
const gfx::Animation* animation) {
tab_container()->OnTabCloseAnimationCompleted(static_cast<Tab*>(slot_view()));
}
void TabContainerImpl::RemoveTabDelegate::AnimationCanceled(
const gfx::Animation* animation) {
AnimationEnded(animation);
}
TabContainerImpl::TabContainerImpl(
TabContainerController& controller,
TabHoverCardController* hover_card_controller,
TabDragContextBase* drag_context,
TabSlotController& tab_slot_controller,
views::View* scroll_contents_view)
: controller_(controller),
hover_card_controller_(hover_card_controller),
drag_context_(drag_context),
tab_slot_controller_(tab_slot_controller),
scroll_contents_view_(scroll_contents_view),
overall_bounds_view_(*AddChildView(std::make_unique<views::View>())),
bounds_animator_(this),
layout_helper_(std::make_unique<TabStripLayoutHelper>(
controller,
base::BindRepeating(&TabContainerImpl::GetTabsViewModel,
base::Unretained(this)))) {
SetEventTargeter(std::make_unique<views::ViewTargeter>(this));
if (!gfx::Animation::ShouldRenderRichAnimation())
bounds_animator_.SetAnimationDuration(base::TimeDelta());
bounds_animator_.AddObserver(this);
overall_bounds_view_->SetVisible(false);
if (g_drop_indicator_width == 0) {
// Direction doesn't matter, both images are the same size.
gfx::ImageSkia* drop_image = GetDropArrowImage(true);
g_drop_indicator_width = drop_image->width();
g_drop_indicator_height = drop_image->height();
}
}
TabContainerImpl::~TabContainerImpl() {
// The animations may reference the tabs or group views. Shut down the
// animation before we destroy any animated views.
CancelAnimation();
// Since TabGroupViews expects be able to remove the views it creates, clear
// |group_views_| before removing the remaining children below.
group_views_.clear();
// Make sure we unhook ourselves as a message loop observer so that we don't
// crash in the case where the user closes the window after closing a tab
// but before moving the mouse.
RemoveMessageLoopObserver();
RemoveAllChildViews();
}
void TabContainerImpl::SetAvailableWidthCallback(
base::RepeatingCallback<int()> available_width_callback) {
available_width_callback_ = available_width_callback;
}
Tab* TabContainerImpl::AddTab(std::unique_ptr<Tab> tab,
int model_index,
TabPinned pinned) {
Tab* tab_ptr = AddChildView(std::move(tab));
AddTabToViewModel(tab_ptr, model_index, pinned);
OrderTabSlotView(tab_ptr);
// Don't animate the first tab, it looks weird, and don't animate anything
// if the containing window isn't visible yet.
if (GetTabCount() > 1 && GetWidget() && GetWidget()->IsVisible()) {
StartInsertTabAnimation(model_index);
} else {
CompleteAnimationAndLayout();
}
return tab_ptr;
}
void TabContainerImpl::MoveTab(int from_model_index, int to_model_index) {
Tab* tab = GetTabAtModelIndex(from_model_index);
tabs_view_model_.Move(from_model_index, to_model_index);
layout_helper_->MoveTab(tab->group(), from_model_index, to_model_index);
OrderTabSlotView(tab);
layout_helper_->SetTabPinned(to_model_index, tab->data().pinned
? TabPinned::kPinned
: TabPinned::kUnpinned);
AnimateToIdealBounds();
UpdateAccessibleTabIndices();
}
void TabContainerImpl::RemoveTab(int model_index, bool was_active) {
UpdateClosingModeOnRemovedTab(model_index, was_active);
Tab* tab = GetTabAtModelIndex(model_index);
tab->SetClosing(true);
RemoveTabFromViewModel(model_index);
StartRemoveTabAnimation(tab, model_index);
UpdateAccessibleTabIndices();
}
void TabContainerImpl::SetTabPinned(int model_index, TabPinned pinned) {
layout_helper_->SetTabPinned(model_index, pinned);
if (GetWidget() && GetWidget()->IsVisible()) {
ExitTabClosingMode();
AnimateToIdealBounds();
} else {
CompleteAnimationAndLayout();
}
}
void TabContainerImpl::SetActiveTab(absl::optional<size_t> prev_active_index,
absl::optional<size_t> new_active_index) {
auto maybe_update_group_visuals = [this](absl::optional<size_t> tab_index) {
if (!tab_index.has_value())
return;
absl::optional<tab_groups::TabGroupId> group =
GetTabAtModelIndex(tab_index.value())->group();
if (group.has_value())
UpdateTabGroupVisuals(group.value());
};
maybe_update_group_visuals(prev_active_index);
maybe_update_group_visuals(new_active_index);
layout_helper_->SetActiveTab(prev_active_index, new_active_index);
if (GetActiveTabWidth() == GetInactiveTabWidth()) {
// When tabs are wide enough, selecting a new tab cannot change the
// ideal bounds, so only a repaint is necessary.
SchedulePaint();
} else if (IsAnimating() || drag_context_->IsDragSessionActive()) {
// The selection change will have modified the ideal bounds of the tabs
// in |selected_tabs_| and |new_selection|. We need to recompute and
// retarget the animation to these new bounds. Note: This is safe even if
// we're in the midst of mouse-based tab closure--we won't expand the
// tabstrip back to the full window width--because PrepareForCloseAt() will
// have set |override_available_width_for_tabs_| already.
AnimateToIdealBounds();
} else {
// As in the animating case above, the selection change will have
// affected the desired bounds of the tabs, but since we're in a steady
// state we can just snap to the new bounds.
CompleteAnimationAndLayout();
}
if (base::FeatureList::IsEnabled(features::kScrollableTabStrip) &&
new_active_index.has_value())
ScrollTabToVisible(new_active_index.value());
}
std::unique_ptr<Tab> TabContainerImpl::TransferTabOut(int model_index) {
Tab* const tab = GetTabAtModelIndex(model_index);
tabs_view_model_.Remove(model_index);
OnTabRemoved(tab);
return RemoveChildViewT(tab);
}
Tab* TabContainerImpl::AddTabToViewModel(Tab* tab,
int model_index,
TabPinned pinned) {
tabs_view_model_.Add(tab, model_index);
layout_helper_->InsertTabAt(model_index, tab, pinned);
UpdateAccessibleTabIndices();
return tab;
}
void TabContainerImpl::ReturnTabSlotView(TabSlotView* view) {
// Take `view` back now that it's done dragging or pinning.
// If `view` has no parent (vs the expected case where its parent is a
// TabDragContext or CompoundTabContainer), then it's been removed from the
// View hierarchy as part of deletion, triggering animations to end, which in
// turn will bring us here if tabs are being dragged or are pinning. We need
// to update our data structures accordingly and otherwise not interfere.
if (!view->parent()) {
Tab* tab = views::AsViewClass<Tab>(view);
if (tab) {
DCHECK(tab->closing());
OnTabRemoved(tab);
}
return;
}
const gfx::Rect bounds_in_tab_container_coords = gfx::ToEnclosingRect(
ConvertRectToTarget(view->parent(), this, gfx::RectF(view->bounds())));
AddChildView(view);
view->SetBoundsRect(bounds_in_tab_container_coords);
Tab* tab = views::AsViewClass<Tab>(view);
if (tab && tab->closing()) {
// This tab was closed during the drag. It's already been removed from our
// other data structures in RemoveTab(), and TabDragContext animated it
// closed for us, so we can just destroy it.
OnTabCloseAnimationCompleted(tab);
return;
}
OrderTabSlotView(view);
if (view->group())
UpdateTabGroupVisuals(view->group().value());
}
void TabContainerImpl::ScrollTabToVisible(int model_index) {
absl::optional<gfx::Rect> visible_content_rect = GetVisibleContentRect();
if (!visible_content_rect.has_value())
return;
// If the tab strip won't be scrollable after the current tabstrip animations
// complete, scroll animation wouldn't be meaningful.
if (tabs_view_model_.ideal_bounds(GetTabCount() - 1).right() <=
GetAvailableWidthForTabContainer())
return;
gfx::Rect active_tab_ideal_bounds =
tabs_view_model_.ideal_bounds(model_index);
if ((active_tab_ideal_bounds.x() >= visible_content_rect->x()) &&
(active_tab_ideal_bounds.right() <= visible_content_rect->right())) {
return;
}
bool scroll_left = active_tab_ideal_bounds.x() < visible_content_rect->x();
if (scroll_left) {
// Scroll the left edge of |visible_content_rect| to show the left edge of
// the tab at |model_index|. We can leave the width entirely up to the
// ScrollView.
int start_left_edge(visible_content_rect->x());
int target_left_edge(active_tab_ideal_bounds.x());
AnimateScrollToShowXCoordinate(start_left_edge, target_left_edge);
} else {
// Scroll the right edge of |visible_content_rect| to show the right edge
// of the tab at |model_index|. We can leave the width entirely up to the
// ScrollView.
int start_right_edge(visible_content_rect->right());
int target_right_edge(active_tab_ideal_bounds.right());
AnimateScrollToShowXCoordinate(start_right_edge, target_right_edge);
}
}
void TabContainerImpl::ScrollTabContainerByOffset(int offset) {
absl::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 TabContainerImpl::OnGroupCreated(const tab_groups::TabGroupId& group) {
auto group_view = std::make_unique<TabGroupViews>(
this, drag_context_, *tab_slot_controller_, group);
layout_helper_->InsertGroupHeader(group, group_view->header());
group_views_[group] = std::move(group_view);
}
void TabContainerImpl::OnGroupEditorOpened(
const tab_groups::TabGroupId& group) {
// The context menu relies on a Browser object which is not provided in
// TabStripTest.
if (tab_slot_controller_->GetBrowser()) {
group_views_[group]->header()->ShowContextMenuForViewImpl(
this, gfx::Point(), ui::MENU_SOURCE_NONE);
}
}
void TabContainerImpl::OnGroupContentsChanged(
const tab_groups::TabGroupId& group) {
// If a tab was removed, the underline bounds might be stale.
group_views_[group]->UpdateBounds();
// The group header may be in the wrong place if the tab didn't actually
// move in terms of model indices.
OnGroupMoved(group);
AnimateToIdealBounds();
}
void TabContainerImpl::OnGroupVisualsChanged(
const tab_groups::TabGroupId& group,
const tab_groups::TabGroupVisualData* old_visuals,
const tab_groups::TabGroupVisualData* new_visuals) {
GetGroupViews(group)->OnGroupVisualsChanged();
// The group title may have changed size, so update bounds.
// First exit tab closing mode, unless this change was a collapse, in which
// case we want to stay in tab closing mode.
const bool is_collapsing = old_visuals && !old_visuals->is_collapsed() &&
new_visuals->is_collapsed();
if (!is_collapsing)
ExitTabClosingMode();
AnimateToIdealBounds();
// The active tab may need to repaint its group stroke if it's in `group`.
const absl::optional<int> active_index = controller_->GetActiveIndex();
if (active_index.has_value())
GetTabAtModelIndex(active_index.value())->SchedulePaint();
}
void TabContainerImpl::OnGroupMoved(const tab_groups::TabGroupId& group) {
DCHECK(group_views_[group]);
layout_helper_->UpdateGroupHeaderIndex(group);
OrderTabSlotView(group_views_[group]->header());
}
void TabContainerImpl::ToggleTabGroup(
const tab_groups::TabGroupId& group,
bool is_collapsing,
ToggleTabGroupCollapsedStateOrigin origin) {
if (is_collapsing && GetWidget()) {
if (origin != ToggleTabGroupCollapsedStateOrigin::kMouse &&
origin != ToggleTabGroupCollapsedStateOrigin::kGesture) {
return;
}
const int current_group_width = GetGroupViews(group)->GetBounds().width();
// A collapsed group only has the width of its header, which is slightly
// smaller for collapsed groups compared to expanded groups.
const int collapsed_group_width =
GetGroupViews(group)->header()->GetCollapsedHeaderWidth();
const CloseTabSource source =
origin == ToggleTabGroupCollapsedStateOrigin::kMouse
? CloseTabSource::CLOSE_TAB_FROM_MOUSE
: CloseTabSource::CLOSE_TAB_FROM_TOUCH;
EnterTabClosingMode(
tabs_view_model_.ideal_bounds(GetTabCount() - 1).right() -
current_group_width + collapsed_group_width,
source);
} else {
ExitTabClosingMode();
}
}
void TabContainerImpl::OnGroupClosed(const tab_groups::TabGroupId& group) {
bounds_animator_.StopAnimatingView(group_views_.at(group).get()->header());
layout_helper_->RemoveGroupHeader(group);
group_views_.erase(group);
AnimateToIdealBounds();
}
void TabContainerImpl::UpdateTabGroupVisuals(tab_groups::TabGroupId group_id) {
const auto group_views = group_views_.find(group_id);
if (group_views != group_views_.end())
group_views->second->UpdateBounds();
}
void TabContainerImpl::NotifyTabGroupEditorBubbleOpened() {
// Suppress the mouse watching behavior of tab closing mode.
RemoveMessageLoopObserver();
}
void TabContainerImpl::NotifyTabGroupEditorBubbleClosed() {
// Restore the mouse watching behavior of tab closing mode.
if (in_tab_close_)
AddMessageLoopObserver();
}
// TODO(tbergquist): This should really return an optional<size_t>.
absl::optional<int> TabContainerImpl::GetModelIndexOf(
const TabSlotView* slot_view) const {
return tabs_view_model_.GetIndexOfView(slot_view);
}
Tab* TabContainerImpl::GetTabAtModelIndex(int index) const {
return tabs_view_model_.view_at(index);
}
int TabContainerImpl::GetTabCount() const {
return tabs_view_model_.view_size();
}
// TODO(tbergquist): This should really return an optional<size_t>.
absl::optional<int> TabContainerImpl::GetModelIndexOfFirstNonClosingTab(
Tab* tab) const {
if (tab->closing()) {
// If the tab is already closing, close the next tab. We do this so that the
// user can rapidly close tabs by clicking the close button and not have
// the animations interfere with that.
std::vector<Tab*> all_tabs = layout_helper_->GetTabs();
auto it = base::ranges::find(all_tabs, tab);
while (it < all_tabs.end() && (*it)->closing()) {
it++;
}
if (it == all_tabs.end())
return absl::nullopt;
tab = *it;
}
return GetModelIndexOf(tab);
}
void TabContainerImpl::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 TabContainerImpl::HandleLongTap(ui::GestureEvent* event) {
event->target()->ConvertEventToTarget(this, event);
gfx::Point local_point = event->location();
Tab* tab = FindTabHitByPoint(local_point);
if (tab) {
ConvertPointToScreen(this, &local_point);
tab_slot_controller_->ShowContextMenuForTab(tab, local_point,
ui::MENU_SOURCE_TOUCH);
}
}
bool TabContainerImpl::IsRectInContentArea(const gfx::Rect& rect) {
// If there is no control at this location, the hit is in the caption area.
const views::View* v = GetEventHandlerForRect(rect);
if (v == this)
return false;
if (controller_->CanExtendDragHandle()) {
// When the window has a top drag handle, a thin strip at the top of
// inactive tabs and the new tab button can be treated as part of the window
// drag handle, to increase draggability. This region starts 1 DIP above
// the top of the separator.
const int drag_handle_extension =
TabStyle::GetDragHandleExtension(height());
// A hit on an inactive tab is in the content area unless it is in the thin
// strip mentioned above.
const absl::optional<size_t> tab_index = tabs_view_model_.GetIndexOfView(v);
if (tab_index.has_value() && IsValidModelIndex(tab_index.value())) {
Tab* tab = GetTabAtModelIndex(tab_index.value());
gfx::Rect tab_drag_handle = tab->GetMirroredBounds();
tab_drag_handle.set_height(drag_handle_extension);
return tab->IsActive() || !tab_drag_handle.Intersects(rect);
}
}
// |v| is some other view (e.g. a close button in a tab) and therefore |rect|
// is in client area.
return true;
}
absl::optional<ZOrderableTabContainerElement>
TabContainerImpl::GetLeadingElementForZOrdering() const {
// Use `tabs_view_model_` instead of `layout_helper_` to ignore closing tabs
// to prevent discontinuous z-order flips when tab close animations end.
if (GetTabCount() == 0)
return absl::nullopt;
Tab* const leading_tab = tabs_view_model_.view_at(0);
// If `leading_tab` is grouped, it's preceded by its group header.
if (leading_tab->group().has_value()) {
return ZOrderableTabContainerElement(
group_views_.at(leading_tab->group().value())->header());
}
return ZOrderableTabContainerElement(leading_tab);
}
absl::optional<ZOrderableTabContainerElement>
TabContainerImpl::GetTrailingElementForZOrdering() const {
// Use `tabs_view_model_` instead of `layout_helper_` to ignore closing tabs
// to prevent discontinuous z-order flips when tab close animations end.
if (GetTabCount() == 0)
return absl::nullopt;
Tab* const trailing_tab =
tabs_view_model_.view_at(tabs_view_model_.view_size() - 1);
// Tab group headers could be the trailing element, if the group is collapsed.
// However, this method doesn't need to consider that case because it is
// currently only called on the pinned TabContainer in a CompoundTabContainer,
// which can't have tab groups. DCHECK that assumption:
DCHECK(!trailing_tab->group().has_value());
return ZOrderableTabContainerElement(trailing_tab);
}
void TabContainerImpl::OnTabSlotAnimationProgressed(TabSlotView* view) {
if (view && view->group())
UpdateTabGroupVisuals(view->group().value());
}
void TabContainerImpl::InvalidateIdealBounds() {
last_layout_size_ = gfx::Size();
}
void TabContainerImpl::AnimateToIdealBounds() {
UpdateIdealBounds();
UpdateHoverCard(nullptr, TabSlotController::HoverCardUpdateType::kAnimating);
for (int i = 0; i < GetTabCount(); ++i) {
Tab* tab = GetTabAtModelIndex(i);
const gfx::Rect& target_bounds = tabs_view_model_.ideal_bounds(i);
AnimateTabSlotViewTo(tab, target_bounds);
}
for (const auto& header_pair : group_views_) {
TabGroupHeader* const header = header_pair.second->header();
const gfx::Rect& target_bounds =
layout_helper_->group_header_ideal_bounds().at(header_pair.first);
AnimateTabSlotViewTo(header, target_bounds);
}
const gfx::Rect overall_target_bounds = gfx::Rect(GetIdealTrailingX(), 0);
bounds_animator_.AnimateViewTo(base::to_address(overall_bounds_view_),
overall_target_bounds);
// Because the preferred size of the tabstrip depends on the IsAnimating()
// condition, but starting an animation doesn't necessarily invalidate the
// existing preferred size and layout (which may now be incorrect), we need to
// signal this explicitly.
PreferredSizeChanged();
}
bool TabContainerImpl::IsAnimating() const {
return bounds_animator_.IsAnimating();
}
void TabContainerImpl::CancelAnimation() {
drag_context_->CompleteEndDragAnimations();
bounds_animator_.Cancel();
}
void TabContainerImpl::CompleteAnimationAndLayout() {
last_available_width_ = GetAvailableWidthForTabContainer();
last_layout_size_ = size();
CancelAnimation();
UpdateIdealBounds();
SnapToIdealBounds();
SetTabSlotVisibility();
SchedulePaint();
}
int TabContainerImpl::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 TabContainerImpl::EnterTabClosingMode(absl::optional<int> override_width,
CloseTabSource source) {
in_tab_close_ = true;
if (override_width.has_value())
override_available_width_for_tabs_ = override_width;
// Default to freezing tabs in their current state if our caller doesn't have
// a more specific plan.
if (!override_available_width_for_tabs_.has_value())
override_available_width_for_tabs_ = width();
resize_layout_timer_.Stop();
if (source == CLOSE_TAB_FROM_TOUCH)
StartResizeLayoutTabsFromTouchTimer();
else
AddMessageLoopObserver();
}
void TabContainerImpl::ExitTabClosingMode() {
in_tab_close_ = false;
override_available_width_for_tabs_.reset();
}
void TabContainerImpl::SetTabSlotVisibility() {
std::set<tab_groups::TabGroupId> visibility_changed_groups;
bool last_tab_visible = false;
absl::optional<tab_groups::TabGroupId> last_tab_group = absl::nullopt;
std::vector<Tab*> tabs = layout_helper_->GetTabs();
for (Tab* tab : base::Reversed(tabs)) {
absl::optional<tab_groups::TabGroupId> current_group = tab->group();
if (current_group != last_tab_group && last_tab_group.has_value()) {
TabGroupViews* group_view = group_views_.at(last_tab_group.value()).get();
// If we change the visibility of a group header, we must recalculate that
// group's underline bounds.
if (last_tab_visible != group_view->header()->GetVisible())
visibility_changed_groups.insert(last_tab_group.value());
group_view->header()->SetVisible(last_tab_visible);
// Hide underlines if they would underline an invisible tab, but don't
// show underlines if they're hidden during a header drag session.
if (!group_view->header()->dragging())
group_view->underline()->SetVisible(last_tab_visible);
}
last_tab_visible = ShouldTabBeVisible(tab);
last_tab_group = tab->closing() ? absl::nullopt : current_group;
// Collapsed tabs disappear once they've reached their minimum size. This
// is different than very small non-collapsed tabs, because in that case
// the tab (and its favicon) must still be visible.
const bool is_collapsed =
(current_group.has_value() &&
controller_->IsGroupCollapsed(current_group.value()) &&
tab->bounds().width() <= TabStyle::GetTabOverlap());
const bool should_be_visible = is_collapsed ? false : last_tab_visible;
// If we change the visibility of a tab in a group, we must recalculate that
// group's underline bounds.
if (should_be_visible != tab->GetVisible() && tab->group().has_value())
visibility_changed_groups.insert(tab->group().value());
tab->SetVisible(should_be_visible);
}
// Update bounds for any groups containing a modified tab. N.B. this method
// also updates the title and color of the group, but this should always be a
// no-op in practice, as changes to those immediately take effect via other
// notification channels.
for (const auto& group : visibility_changed_groups)
UpdateTabGroupVisuals(group);
}
bool TabContainerImpl::InTabClose() {
return in_tab_close_;
}
TabGroupViews* TabContainerImpl::GetGroupViews(
tab_groups::TabGroupId group_id) const {
auto group_views = group_views_.find(group_id);
CHECK(group_views != group_views_.end());
return group_views->second.get();
}
const std::map<tab_groups::TabGroupId, std::unique_ptr<TabGroupViews>>&
TabContainerImpl::get_group_views_for_testing() const {
return group_views_;
}
int TabContainerImpl::GetActiveTabWidth() const {
return layout_helper_->active_tab_width();
}
int TabContainerImpl::GetInactiveTabWidth() const {
return layout_helper_->inactive_tab_width();
}
gfx::Rect TabContainerImpl::GetIdealBounds(int model_index) const {
return tabs_view_model_.ideal_bounds(model_index);
}
gfx::Rect TabContainerImpl::GetIdealBounds(tab_groups::TabGroupId group) const {
return layout_helper_->group_header_ideal_bounds().at(group);
}
void TabContainerImpl::Layout() {
if (controller_->IsAnimatingInTabStrip()) {
// Hide tabs that have animated at least partially out of the clip region.
SetTabSlotVisibility();
return;
}
// Only do a layout if our size or the available width changed.
const int available_width = GetAvailableWidthForTabContainer();
if (last_layout_size_ == size() && last_available_width_ == available_width)
return;
if (IsDragSessionActive())
return;
CompleteAnimationAndLayout();
}
void TabContainerImpl::PaintChildren(const views::PaintInfo& paint_info) {
// N.B. We override PaintChildren only to define paint order for our children.
// We do this instead of GetChildrenInZOrder because GetChildrenInZOrder is
// called in many more contexts for many more reasons, e.g. whenever views are
// added or removed, and in particular can be called while we are partway
// through creating a tab group and are not in a self-consistent state.
std::vector<ZOrderableTabContainerElement> orderable_children;
for (views::View* 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);
}
gfx::Size TabContainerImpl::GetMinimumSize() const {
// During animations, our minimum width tightly hugs the current bounds of our
// children.
absl::optional<int> minimum_width = GetMidAnimationTrailingX();
if (!minimum_width.has_value()) {
// Otherwise, the tabstrip is in a steady state, so we want to use the width
// that would be spanned by our children after animations complete. This
// allows tabs to resize directly with window resizes instead of mediating
// that through animation.
minimum_width = layout_helper_->CalculateMinimumWidth();
}
return gfx::Size(minimum_width.value(), GetLayoutConstant(TAB_HEIGHT));
}
gfx::Size TabContainerImpl::CalculatePreferredSize() const {
// During animations, our preferred width tightly hugs the current bounds of
// our children.
absl::optional<int> preferred_width = GetMidAnimationTrailingX();
if (!preferred_width.has_value()) {
// Otherwise, the tabstrip is in a steady state, so we want to use the width
// that would be spanned by our children after animations complete. This
// allows tabs to resize directly with window resizes instead of mediating
// that through animation.
preferred_width = override_available_width_for_tabs_.value_or(
layout_helper_->CalculatePreferredWidth());
}
return gfx::Size(preferred_width.value(), GetLayoutConstant(TAB_HEIGHT));
}
views::View* TabContainerImpl::GetTooltipHandlerForPoint(
const gfx::Point& point) {
if (!HitTestPoint(point))
return nullptr;
// Return any view that isn't a Tab or this TabContainer immediately. We don't
// want to interfere.
views::View* v = View::GetTooltipHandlerForPoint(point);
if (v && v != this && !views::IsViewClass<Tab>(v))
return v;
views::View* tab = FindTabHitByPoint(point);
if (tab)
return tab;
return this;
}
BrowserRootView::DropIndex TabContainerImpl::GetDropIndex(
const ui::DropTargetEvent& event) {
// Force animations to stop, otherwise it makes the index calculation tricky.
CompleteAnimationAndLayout();
// If the UI layout is right-to-left, we need to mirror the mouse
// coordinates since we calculate the drop index based on the
// original (and therefore non-mirrored) positions of the tabs.
const int x = GetMirroredXInView(event.x());
std::vector<TabSlotView*> views = layout_helper_->GetTabSlotViews();
// Loop until we find a tab or group header that intersects |event|'s
// location.
for (TabSlotView* view : views) {
const int max_x = view->x() + view->width();
if (x >= max_x)
continue;
if (view->GetTabSlotViewType() == TabSlotView::ViewType::kTab) {
Tab* const tab = static_cast<Tab*>(view);
// Closing tabs should be skipped.
if (tab->closing())
continue;
// GetModelIndexOf is an O(n) operation. Since we will definitely
// return from the loop at this point, it is only called once.
// Hence the loop is still O(n). Calling this every loop iteration
// must be avoided since it will become O(n^2).
const int model_index = GetModelIndexOf(tab).value();
const bool first_in_group =
tab->group().has_value() &&
model_index == controller_->GetFirstTabInGroup(tab->group().value());
// When hovering over the left or right quarter of a tab, the drop
// indicator will point between tabs.
const int hot_width = tab->width() / 4;
if (x >= (max_x - hot_width))
return {model_index + 1, true /* drop_before */,
false /* drop_in_group */};
else if (x < tab->x() + hot_width)
return {model_index, true /* drop_before */, first_in_group};
else
return {model_index, false /* drop_before */,
false /* drop_in_group */};
} else {
TabGroupHeader* const group_header = static_cast<TabGroupHeader*>(view);
const int first_tab_index =
controller_->GetFirstTabInGroup(group_header->group().value())
.value();
if (x < max_x - group_header->width() / 2)
return {first_tab_index, true /* drop_before */,
false /* drop_in_group */};
else
return {first_tab_index, true /* drop_before */,
true /* drop_in_group */};
}
}
// The drop isn't over a tab, add it to the end.
return {GetTabCount(), true, false};
}
views::View* TabContainerImpl::GetViewForDrop() {
return this;
}
BrowserRootView::DropTarget* TabContainerImpl::GetDropTarget(
gfx::Point loc_in_local_coords) {
if (IsDrawn()) {
// Allow the drop as long as the mouse is over tab container or vertically
// before it.
if (loc_in_local_coords.y() < height())
return this;
}
return nullptr;
}
void TabContainerImpl::HandleDragUpdate(
const absl::optional<BrowserRootView::DropIndex>& index) {
SetDropArrow(index);
}
void TabContainerImpl::HandleDragExited() {
SetDropArrow({});
}
views::View* TabContainerImpl::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());
// Return any view that isn't a Tab or this TabStrip immediately. We don't
// want to interfere.
views::View* v = views::ViewTargeterDelegate::TargetForRect(root, rect);
if (v && v != this && !views::IsViewClass<Tab>(v))
return v;
views::View* tab = FindTabHitByPoint(point);
if (tab)
return tab;
return this;
}
void TabContainerImpl::MouseMovedOutOfHost() {
ResizeLayoutTabs();
}
void TabContainerImpl::OnBoundsAnimatorProgressed(
views::BoundsAnimator* animator) {
// The rightmost tab (or the `overall_bounds_view_`) moving might have changed
// our preferred width.
PreferredSizeChanged();
}
void TabContainerImpl::OnBoundsAnimatorDone(views::BoundsAnimator* animator) {
// Send the Container a message to simulate a mouse moved event at the current
// mouse position. This tickles the Tab the mouse is currently over to show
// the "hot" state of the close button, or to show the hover card, etc. Note
// that this is not required (and indeed may crash!) during a drag session.
if (!IsDragSessionActive()) {
// The widget can apparently be null during shutdown.
views::Widget* widget = GetWidget();
if (widget)
widget->SynthesizeMouseMoveEvent();
}
PreferredSizeChanged();
}
// TabContainerImpl::DropArrow:
// ----------------------------------------------------------
TabContainerImpl::DropArrow::DropArrow(const BrowserRootView::DropIndex& index,
bool point_down,
views::Widget* context)
: index_(index), point_down_(point_down) {
arrow_window_ = new views::Widget;
views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP);
params.z_order = ui::ZOrderLevel::kFloatingUIElement;
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
params.accept_events = false;
params.bounds = gfx::Rect(g_drop_indicator_width, g_drop_indicator_height);
params.context = context->GetNativeWindow();
arrow_window_->Init(std::move(params));
arrow_view_ =
arrow_window_->SetContentsView(std::make_unique<views::ImageView>());
arrow_view_->SetImage(GetDropArrowImage(point_down_));
scoped_observation_.Observe(arrow_window_.get());
arrow_window_->Show();
}
TabContainerImpl::DropArrow::~DropArrow() {
// Close eventually deletes the window, which deletes arrow_view too.
if (arrow_window_)
arrow_window_->Close();
}
void TabContainerImpl::DropArrow::SetPointDown(bool down) {
if (point_down_ == down)
return;
point_down_ = down;
arrow_view_->SetImage(GetDropArrowImage(point_down_));
}
void TabContainerImpl::DropArrow::SetWindowBounds(const gfx::Rect& bounds) {
arrow_window_->SetBounds(bounds);
}
void TabContainerImpl::DropArrow::OnWidgetDestroying(views::Widget* widget) {
DCHECK(scoped_observation_.IsObservingSource(arrow_window_.get()));
scoped_observation_.Reset();
arrow_window_ = nullptr;
}
views::ViewModelT<Tab>* TabContainerImpl::GetTabsViewModel() {
return &tabs_view_model_;
}
absl::optional<gfx::Rect> TabContainerImpl::GetVisibleContentRect() {
views::ScrollView* scroll_container =
views::ScrollView::GetScrollViewForContents(scroll_contents_view_);
if (!scroll_container)
return absl::nullopt;
return scroll_container->GetVisibleRect();
}
void TabContainerImpl::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(),
bounds_animator_.GetAnimationDuration(), start_rect, target_rect);
tab_scrolling_animation_->Start();
}
void TabContainerImpl::AnimateTabSlotViewTo(TabSlotView* tab_slot_view,
const gfx::Rect& target_bounds) {
// If we don't own the tab, let our controller handle it.
if (tab_slot_view->parent() != this) {
controller_->UpdateAnimationTarget(tab_slot_view, target_bounds);
return;
}
// Also skip slots already being animated to the same ideal bounds. Calling
// AnimateViewTo() again restarts the animation, which changes the timing of
// how the slot animates, leading to hitches.
if (bounds_animator_.GetTargetBounds(tab_slot_view) == target_bounds)
return;
bounds_animator_.AnimateViewTo(
tab_slot_view, target_bounds,
std::make_unique<TabSlotAnimationDelegate>(this, tab_slot_view));
}
void TabContainerImpl::UpdateIdealBounds() {
// No tabs = no width. This can happen during startup and shutdown, or, if
// CompoundTabContainer is in use, all the tabs are in the other TabContainer.
if (GetTabCount() == 0)
return;
// Update |last_available_width_| in case there is a different amount of
// available width than there was in the last layout (e.g. if the tabstrip
// is currently hidden).
last_available_width_ = GetAvailableWidthForTabContainer();
layout_helper_->UpdateIdealBounds(CalculateAvailableWidthForTabs());
}
void TabContainerImpl::SnapToIdealBounds() {
for (int i = 0; i < GetTabCount(); ++i) {
if (GetTabAtModelIndex(i)->parent() != this)
continue;
GetTabAtModelIndex(i)->SetBoundsRect(tabs_view_model_.ideal_bounds(i));
}
for (const auto& header_pair : group_views_) {
if (header_pair.second->header()->parent() != this)
continue;
header_pair.second->header()->SetBoundsRect(
layout_helper_->group_header_ideal_bounds().at(header_pair.first));
header_pair.second->UpdateBounds();
}
overall_bounds_view_->SetBoundsRect(gfx::Rect(GetIdealTrailingX(), 0));
PreferredSizeChanged();
}
int TabContainerImpl::CalculateAvailableWidthForTabs() const {
return override_available_width_for_tabs_.value_or(
GetAvailableWidthForTabContainer());
}
void TabContainerImpl::StartInsertTabAnimation(int model_index) {
ExitTabClosingMode();
gfx::Rect bounds = GetTabAtModelIndex(model_index)->bounds();
bounds.set_height(GetLayoutConstant(TAB_HEIGHT));
// Adjust the starting bounds of the new tab.
const int tab_overlap = TabStyle::GetTabOverlap();
if (model_index > 0) {
// If we have a tab to our left, start at its right edge.
bounds.set_x(GetTabAtModelIndex(model_index - 1)->bounds().right() -
tab_overlap);
} else if (model_index + 1 < GetTabCount()) {
// Otherwise, if we have a tab to our right, start at its left edge.
bounds.set_x(GetTabAtModelIndex(model_index + 1)->bounds().x());
} else {
NOTREACHED() << "First tab inserted into the tabstrip should not animate.";
}
// Start at the width of the overlap in order to animate at the same speed
// the surrounding tabs are moving, since at this width the subsequent tab
// is naturally positioned at the same X coordinate.
bounds.set_width(tab_overlap);
GetTabAtModelIndex(model_index)->SetBoundsRect(bounds);
// Animate in to the full width.
AnimateToIdealBounds();
}
void TabContainerImpl::StartRemoveTabAnimation(Tab* tab,
int former_model_index) {
if (in_tab_close_ && GetTabCount() > 0 &&
override_available_width_for_tabs_ >
tabs_view_model_.ideal_bounds(GetTabCount() - 1).right()) {
// Tab closing mode is no longer constraining tab widths - they're at full
// size. Exit tab closing mode so that it doesn't artificially inflate our
// bounds.
ExitTabClosingMode();
}
AnimateToIdealBounds();
gfx::Rect target_bounds =
GetTargetBoundsForClosingTab(tab, former_model_index);
// If the tab is being dragged, we don't own it, and can't run animations on
// it. We need to take it back first.
if (tab->dragging()) {
// Don't bother animating if the tab has been detached rather than closed -
// i.e. it's being moved to another tabstrip. At this point it's safe to
// just destroy the tab immediately.
if (tab->detached()) {
OnTabCloseAnimationCompleted(tab);
return;
}
DCHECK(IsDragSessionEnding());
}
if (tab->parent() != this) {
// Notify our parent of the new animation target, since we can't animate
// `tab` ourselves.
controller_->UpdateAnimationTarget(tab, target_bounds);
return;
}
// TODO(pkasting): When closing multiple tabs, we get repeated RemoveTabAt()
// calls, each of which closes a new tab and thus generates different ideal
// bounds. We should update the animations of any other tabs that are
// currently being closed to reflect the new ideal bounds, or else change from
// removing one tab at a time to animating the removal of all tabs at once.
bounds_animator_.AnimateViewTo(
tab, target_bounds, std::make_unique<RemoveTabDelegate>(this, tab));
}
gfx::Rect TabContainerImpl::GetTargetBoundsForClosingTab(
Tab* tab,
int former_model_index) const {
const int tab_overlap = TabStyle::GetTabOverlap();
// Compute the target bounds for animating this tab closed. The tab's left
// edge should stay joined to the right edge of the previous tab, if any.
gfx::Rect target_bounds = tab->bounds();
target_bounds.set_x(
(former_model_index > 0)
? (tabs_view_model_.ideal_bounds(former_model_index - 1).right() -
tab_overlap)
: 0);
// The tab should animate to the width of the overlap in order to close at the
// same speed the surrounding tabs are moving, since at this width the
// subsequent tab is naturally positioned at the same X coordinate.
target_bounds.set_width(tab_overlap);
return target_bounds;
}
int TabContainerImpl::GetIdealTrailingX() const {
// Our ideal width is the trailing x of our rightmost tab's ideal bounds.
return GetTabCount() > 0
? tabs_view_model_.ideal_bounds(GetTabCount() - 1).right()
: 0;
}
absl::optional<int> TabContainerImpl::GetMidAnimationTrailingX() const {
if (!controller_->IsAnimatingInTabStrip() || IsDragSessionActive() ||
IsDragSessionEnding()) {
return absl::nullopt;
}
// During animations not related to a drag session, we want to tightly hug
// our tabs. This allows the NTB to slide smoothly as tabs are opened and
// closed.
int trailing_x = 0;
// The visual order of the tabs can be out of sync with the logical order,
// so we have to check all of them to find the visually trailing-most one.
for (views::View* child : children()) {
trailing_x = std::max(trailing_x, child->bounds().right());
}
return trailing_x;
}
void TabContainerImpl::RemoveTabFromViewModel(int index) {
Tab* tab = GetTabAtModelIndex(index);
bool tab_was_active = tab->IsActive();
UpdateHoverCard(nullptr, TabSlotController::HoverCardUpdateType::kTabRemoved);
tabs_view_model_.Remove(index);
layout_helper_->MarkTabAsClosing(index, tab);
if (tab_was_active)
tab->ActiveStateChanged();
}
void TabContainerImpl::OnTabRemoved(Tab* tab) {
// Remove `tab` from `layout_helper_` so we don't try to lay it out later.
layout_helper_->RemoveTab(tab);
}
void TabContainerImpl::OnTabCloseAnimationCompleted(Tab* tab) {
DCHECK(tab->closing());
OnTabRemoved(tab);
// Delete `tab`.
tab->parent()->RemoveChildViewT(tab);
}
void TabContainerImpl::UpdateClosingModeOnRemovedTab(int model_index,
bool was_active) {
// The tab at |model_index| has already been removed from the model, but is
// still in |tabs_view_model_|. Index math with care!
const int model_count = GetTabCount() - 1;
// If we're closing the last tab, tab closing mode is no longer meaningful.
if (model_count == 0)
ExitTabClosingMode();
// No updates needed if we aren't in tab closing mode or are closing the
// trailingmost tab.
if (!in_tab_close_ || model_index == model_count)
return;
// Update `override_available_width_for_tabs_` so that as the user closes tabs
// with the mouse a tab continues to fall under the mouse.
const Tab* const tab_being_removed = GetTabAtModelIndex(model_index);
int size_delta = tab_being_removed->width();
// When removing an active, non-pinned tab, the next active tab will be
// given the active width (unless it is pinned). Thus the width being
// removed from the container is really the current width of whichever
// inactive tab will be made active.
if (was_active && !tab_being_removed->data().pinned &&
layout_helper_->active_tab_width() >
layout_helper_->inactive_tab_width()) {
const absl::optional<int> next_active_viewmodel_index =
controller_->GetActiveIndex();
// The next active tab may not be in this TabContainer.
if (next_active_viewmodel_index.has_value()) {
// At this point, model's internal state has already been updated.
// `contents` has been detached from model and the active index has
// been updated. But the tab for `contents` isn't removed yet. Thus,
// we need to fix up `next_active_viewmodel_index` based on it.
const bool adjust_for_removed_tab =
model_index <= next_active_viewmodel_index;
const int next_active_index = next_active_viewmodel_index.value() +
(adjust_for_removed_tab ? 1 : 0);
const Tab* const next_active_tab = GetTabAtModelIndex(next_active_index);
if (!next_active_tab->data().pinned)
size_delta = next_active_tab->width();
}
}
override_available_width_for_tabs_ =
tabs_view_model_.ideal_bounds(model_count).right() - size_delta +
TabStyle::GetTabOverlap();
}
void TabContainerImpl::ResizeLayoutTabs() {
// We've been called back after the TabStrip has been emptied out (probably
// just prior to the window being destroyed). We need to do nothing here or
// else GetTabAt below will crash.
if (GetTabCount() == 0)
return;
// It is critically important that this is unhooked here, otherwise we will
// keep spying on messages forever.
RemoveMessageLoopObserver();
ExitTabClosingMode();
int pinned_tab_count = layout_helper_->GetPinnedTabCount();
if (pinned_tab_count == GetTabCount()) {
// Only pinned tabs, we know the tab widths won't have changed (all
// pinned tabs have the same width), so there is nothing to do.
return;
}
// Don't try and avoid layout based on tab sizes. If tabs are small enough
// then the width of the active tab may not change, but other widths may
// have. This is particularly important if we've overflowed (all tabs are at
// the min).
AnimateToIdealBounds();
}
void TabContainerImpl::ResizeLayoutTabsFromTouch() {
// Don't resize if the user is interacting with the tabstrip.
if (!IsDragSessionActive())
ResizeLayoutTabs();
else
StartResizeLayoutTabsFromTouchTimer();
}
void TabContainerImpl::StartResizeLayoutTabsFromTouchTimer() {
// Amount of time we delay before resizing after a close from a touch.
constexpr auto kTouchResizeLayoutTime = base::Seconds(2);
resize_layout_timer_.Stop();
resize_layout_timer_.Start(FROM_HERE, kTouchResizeLayoutTime, this,
&TabContainerImpl::ResizeLayoutTabsFromTouch);
}
bool TabContainerImpl::IsDragSessionActive() const {
// `drag_context_` may be null in tests.
return drag_context_ && drag_context_->IsDragSessionActive();
}
bool TabContainerImpl::IsDragSessionEnding() const {
// `drag_context_` may be null in tests.
return drag_context_ && drag_context_->IsAnimatingDragEnd();
}
void TabContainerImpl::AddMessageLoopObserver() {
if (!mouse_watcher_) {
// Expand the watched region downwards below the bottom of the tabstrip.
// This allows users to move the cursor horizontally, to another tab,
// without accidentally exiting closing mode if they drift verticaally
// slightly out of the tabstrip.
constexpr int kTabStripAnimationVSlop = 40;
// Expand the watched region to the right to cover the NTB. This prevents
// the scenario where the user goes to click on the NTB while they're in
// closing mode, and closing mode exits just as they reach the NTB.
constexpr int kTabStripAnimationHSlop = 60;
mouse_watcher_ = std::make_unique<views::MouseWatcher>(
std::make_unique<views::MouseWatcherViewHost>(
controller_->GetTabClosingModeMouseWatcherHostView(),
gfx::Insets::TLBR(
0, base::i18n::IsRTL() ? kTabStripAnimationHSlop : 0,
kTabStripAnimationVSlop,
base::i18n::IsRTL() ? 0 : kTabStripAnimationHSlop)),
this);
}
mouse_watcher_->Start(GetWidget()->GetNativeWindow());
}
void TabContainerImpl::RemoveMessageLoopObserver() {
mouse_watcher_ = nullptr;
}
void TabContainerImpl::OrderTabSlotView(TabSlotView* slot_view) {
if (slot_view->parent() != this)
return;
// |slot_view| is in the wrong place in children(). Fix it.
std::vector<TabSlotView*> slots = layout_helper_->GetTabSlotViews();
size_t target_slot_index =
base::ranges::find(slots, slot_view) - slots.begin();
// Find the index in children() that corresponds to |target_slot_index|.
size_t view_index = 0;
for (size_t slot_index = 0; slot_index < target_slot_index; ++slot_index) {
// If we don't own this view, skip it *without* advancing in children().
if (slots[slot_index]->parent() != this)
continue;
if (view_index == children().size())
break;
++view_index;
}
ReorderChildView(slot_view, view_index);
}
bool TabContainerImpl::IsPointInTab(
Tab* tab,
const gfx::Point& point_in_tabstrip_coords) {
if (!tab->GetVisible())
return false;
if (tab->parent() != this)
return false;
return tab->HitTestPoint(
View::ConvertPointToTarget(this, tab, point_in_tabstrip_coords));
}
Tab* TabContainerImpl::FindTabHitByPoint(const gfx::Point& point) {
// Check all tabs, even closing tabs. Mouse events need to reach closing tabs
// for users to be able to rapidly middle-click close several tabs.
std::vector<Tab*> all_tabs = layout_helper_->GetTabs();
// The display order doesn't necessarily match the child order, so we iterate
// in display order.
for (size_t i = 0; i < all_tabs.size(); ++i) {
// If we don't first exclude points outside the current tab, the code below
// will return the wrong tab if the next tab is selected, the following tab
// is active, and |point| is in the overlap region between the two.
Tab* tab = all_tabs[i];
if (!IsPointInTab(tab, point))
continue;
// Selected tabs render atop unselected ones, and active tabs render atop
// everything. Check whether the next tab renders atop this one and |point|
// is in the overlap region.
Tab* next_tab = i < (all_tabs.size() - 1) ? all_tabs[i + 1] : nullptr;
if (next_tab &&
(next_tab->IsActive() ||
(next_tab->IsSelected() && !tab->IsSelected())) &&
IsPointInTab(next_tab, point))
return next_tab;
// This is the topmost tab for this point.
return tab;
}
return nullptr;
}
bool TabContainerImpl::ShouldTabBeVisible(const Tab* tab) const {
// When the tabstrip is scrollable, it can grow to accommodate any number of
// tabs, so tabs can never become clipped.
// N.B. Tabs can still be not-visible because they're in a collapsed group,
// but that's handled elsewhere.
// N.B. This is separate from the tab being potentially scrolled offscreen -
// this solely determines whether the tab should be clipped for the
// pre-scrolling overflow behavior.
if (base::FeatureList::IsEnabled(features::kScrollableTabStrip))
return true;
// Detached tabs should always be invisible (as they close).
if (tab->detached())
return false;
// If the tab would be clipped by the trailing edge of the strip, even if the
// tabstrip were resized to its greatest possible width, it shouldn't be
// visible.
int right_edge = tab->bounds().right();
const int tabstrip_right = tab->parent() != this
? drag_context_->GetTabDragAreaWidth()
: GetAvailableWidthForTabContainer();
if (right_edge > tabstrip_right)
return false;
// Non-clipped dragging tabs should always be visible.
if (tab->dragging())
return true;
// Let all non-clipped closing tabs be visible. These will probably finish
// closing before the user changes the active tab, so there's little reason to
// try and make the more complex logic below apply.
if (tab->closing())
return true;
// Now we need to check whether the tab isn't currently clipped, but could
// become clipped if we changed the active tab, widening either this tab or
// the tabstrip portion before it.
// Pinned tabs don't change size when activated, so any tab in the pinned tab
// region is safe.
if (tab->data().pinned)
return true;
// If the active tab is on or before this tab, we're safe.
if (controller_->GetActiveIndex() <= GetModelIndexOf(tab))
return true;
// We need to check what would happen if the active tab were to move to this
// tab or before. If animating, we want to use the target bounds in this
// calculation.
if (IsAnimating())
right_edge = bounds_animator_.GetTargetBounds(tab).right();
return (right_edge + layout_helper_->active_tab_width() -
layout_helper_->inactive_tab_width()) <= tabstrip_right;
}
gfx::Rect TabContainerImpl::GetDropBounds(int drop_index,
bool drop_before,
bool drop_in_group,
bool* is_beneath) {
DCHECK_NE(drop_index, -1);
// The X location the indicator points to.
int center_x = -1;
if (GetTabCount() == 0) {
// If the tabstrip is empty, it doesn't matter where the drop arrow goes.
// The tabstrip can only be transiently empty, e.g. during shutdown.
return gfx::Rect();
}
Tab* tab = GetTabAtModelIndex(std::min(drop_index, GetTabCount() - 1));
const bool first_in_group =
drop_index < GetTabCount() && tab->group().has_value() &&
GetModelIndexOf(tab) ==
controller_->GetFirstTabInGroup(tab->group().value());
const int overlap = TabStyle::GetTabOverlap();
if (!drop_before || !first_in_group || drop_in_group) {
// Dropping between tabs, or between a group header and the group's first
// tab.
center_x = tab->x();
const int width = tab->width();
if (drop_index < GetTabCount())
center_x += drop_before ? (overlap / 2) : (width / 2);
else
center_x += width - (overlap / 2);
} else {
// Dropping before a group header.
TabGroupHeader* const header = group_views_[tab->group().value()]->header();
center_x = header->x() + overlap / 2;
}
// Mirror the center point if necessary.
center_x = GetMirroredXInView(center_x);
// Determine the screen bounds.
gfx::Point drop_loc(center_x - g_drop_indicator_width / 2,
-g_drop_indicator_height);
ConvertPointToScreen(this, &drop_loc);
gfx::Rect drop_bounds(drop_loc.x(), drop_loc.y(), g_drop_indicator_width,
g_drop_indicator_height);
// If the rect doesn't fit on the monitor, push the arrow to the bottom.
display::Screen* screen = display::Screen::GetScreen();
display::Display display = screen->GetDisplayMatching(drop_bounds);
*is_beneath = !display.bounds().Contains(drop_bounds);
if (*is_beneath)
drop_bounds.Offset(0, drop_bounds.height() + height());
return drop_bounds;
}
void TabContainerImpl::SetDropArrow(
const absl::optional<BrowserRootView::DropIndex>& index) {
if (!index) {
controller_->OnDropIndexUpdate(-1, false);
drop_arrow_.reset();
return;
}
// Let the controller know of the index update.
controller_->OnDropIndexUpdate(index->value, index->drop_before);
if (drop_arrow_ && (index == drop_arrow_->index()))
return;
bool is_beneath;
gfx::Rect drop_bounds = GetDropBounds(index->value, index->drop_before,
index->drop_in_group, &is_beneath);
if (!drop_arrow_) {
drop_arrow_ = std::make_unique<DropArrow>(*index, !is_beneath, GetWidget());
} else {
drop_arrow_->set_index(*index);
drop_arrow_->SetPointDown(!is_beneath);
}
// Reposition the window.
drop_arrow_->SetWindowBounds(drop_bounds);
}
void TabContainerImpl::UpdateAccessibleTabIndices() {
const int num_tabs = GetTabCount();
for (int i = 0; i < num_tabs; ++i) {
GetTabAtModelIndex(i)->GetViewAccessibility().OverridePosInSet(i + 1,
num_tabs);
}
}
bool TabContainerImpl::IsValidModelIndex(int model_index) const {
return controller_->IsValidModelIndex(model_index);
}
BEGIN_METADATA(TabContainerImpl, views::View)
ADD_READONLY_PROPERTY_METADATA(int, AvailableWidthForTabContainer)
END_METADATA