// 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 <algorithm>
#include <memory>

#include "base/check.h"
#include "base/i18n/rtl.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/views/frame/browser_root_view.h"
#include "chrome/browser/ui/views/tabs/dragging/tab_drag_context.h"
#include "chrome/browser/ui/views/tabs/dragging/tab_drag_controller.h"
#include "chrome/browser/ui/views/tabs/hovercard/tab_hover_card_controller.h"
#include "chrome/browser/ui/views/tabs/shared/drop_arrow.h"
#include "chrome/browser/ui/views/tabs/shared/tab_strip_types.h"
#include "chrome/browser/ui/views/tabs/tab.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_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_strip_layout_types.h"
#include "chrome/browser/ui/views/tabs/tab_style_views.h"
#include "chrome/browser/ui/views/tabs/z_orderable_tab_container_element.h"
#include "components/tab_groups/tab_group_id.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/mojom/menu_source_type.mojom.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/mouse_watcher_view_host.h"
#include "ui/views/rect_based_targeting_utils.h"
#include "ui/views/view_utils.h"

namespace {
// 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.
constexpr int kDragHandleExtension = 6;
}  // 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) {
  StopObserving();
  tab_container()->OnTabCloseAnimationCompleted(
      views::AsViewClass<Tab>(slot_view()));
}

void TabContainerImpl::RemoveTabDelegate::AnimationCanceled(
    const gfx::Animation* animation) {
  AnimationEnded(animation);
}

TabContainerImpl::TabContainerImpl(
    TabContainerController& controller,
    TabHoverCardController* hover_card_controller,
    TabDragPositioningDelegateBase* drag_position_delegate,
    TabSlotController& tab_slot_controller)
    : controller_(controller),
      hover_card_controller_(hover_card_controller),
      drag_position_delegate_(drag_position_delegate),
      tab_slot_controller_(tab_slot_controller),
      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));
  GetViewAccessibility().SetRole(ax::mojom::Role::kTabList);
  GetViewAccessibility().SetIsMultiselectable(true);

  bounds_animator_.AddObserver(this);

  overall_bounds_view_->SetVisible(false);
}

TabContainerImpl::~TabContainerImpl() {
  // Cancel Animation may destroy objects that are in the zorder_cache. since
  // there are no more paints needed, just clear this cache first to prevent
  // dangling pointers.
  z_ordered_children_cache_.clear();

  // 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;
}

std::vector<Tab*> TabContainerImpl::AddTabs(
    std::vector<TabInsertionParams> tabs_params) {
  std::vector<Tab*> added_tabs;

  // First add the tab to the view model, this is done because AddChildView sets
  // some tooltip information which tries to calculate the hit test, which needs
  // information about its adjacent tabs which it gets from the view model.
  for (auto& param : tabs_params) {
    AddTabToViewModel(param.tab.get(), param.model_index, param.pinned);
  }

  for (auto& param : tabs_params) {
    const size_t child_index = GetChildIndexForSlotView(param.tab.get());
    Tab* tab_ptr = AddChildViewAt(std::move(param.tab), child_index);
    MarkZOrderCacheDirty();
    added_tabs.push_back(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()) {
    for (auto& param : tabs_params) {
      StartInsertTabAnimation(param.model_index);
    }
  } else {
    CompleteAnimationAndLayout();
  }

  return added_tabs;
}

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);
  // TODO(shibalik): Replace by a content changed observer method after
  // group features is enabled. This handles the case where a tab is moved
  // within a group.
  if (tab->group().has_value()) {
    group_views_[tab->group().value()]->header()->OnGroupContentsChanged();
  }

  AnimateToIdealBounds();

  UpdateAccessibleTabIndices();
}

void TabContainerImpl::RemoveTab(int model_index, bool was_active) {
  UpdateClosingModeOnRemovedTab(model_index, was_active);

  Tab* const tab = GetTabAtModelIndex(model_index);
  if (!tab) {
    return;
  }

  tab->SetClosing(true);

  CloseTabInViewModel(model_index);

  MarkZOrderCacheDirty();

  if (controller_->IsBrowserClosing()) {
    // If all tabs are closing, we should not animated to tab close, but instead
    // immediately close tabs.
    OnTabCloseAnimationCompleted(tab);
  } else {
    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(std::optional<size_t> prev_active_index,
                                    std::optional<size_t> new_active_index) {
  auto maybe_update_group_visuals = [this](std::optional<size_t> tab_index) {
    if (!tab_index.has_value()) {
      return;
    }
    std::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);
  MarkZOrderCacheDirty();

  if (layout_helper_->layout_domain() ==
      LayoutDomain::kInactiveWidthEqualsActiveWidth) {
    // When tabs are wide enough, selecting a new tab cannot change the
    // ideal bounds, so only a repaint is necessary.
    SchedulePaint();
  } else if (controller_->IsAnimatingInTabStrip() || IsDragSessionActive()) {
    // The selection change will have modified the ideal bounds of the tabs. 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();
  }
}

Tab* TabContainerImpl::RemoveTabFromViewModel(int model_index) {
  Tab* const tab = GetTabAtModelIndex(model_index);
  tabs_view_model_.Remove(model_index);
  OnTabRemoved(tab);

  return 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())));
  AddChildViewRaw(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::OnGroupCreated(const tab_groups::TabGroupId& group) {
  auto group_view = std::make_unique<TabGroupViews>(
      this, drag_position_delegate_->GetContext(), *tab_slot_controller_,
      group);
  layout_helper_->InsertGroupHeader(group, group_view->header());
  group_views_[group] = std::move(group_view);
  MarkZOrderCacheDirty();
}

void TabContainerImpl::OnGroupEditorOpened(
    const tab_groups::TabGroupId& group) {
  // The context menu relies on a BrowserWindowInterface object which is not
  // provided in TabStripTest.
  if (tab_slot_controller_->GetBrowserWindowInterface()) {
    group_views_[group]->header()->ShowContextMenuForViewImpl(
        this, gfx::Point(), ui::mojom::MenuSourceType::kNone);
  }
}

void TabContainerImpl::OnGroupMoved(const tab_groups::TabGroupId& group) {
  DCHECK(group_views_[group]);

  layout_helper_->UpdateGroupHeaderIndex(group);

  OrderTabSlotView(group_views_[group]->header());
}

void TabContainerImpl::OnGroupContentsChanged(
    const tab_groups::TabGroupId& group) {
  // If a tab was removed, the underline bounds might be stale.
  group_views_[group]->UpdateBounds();
  group_views_[group]->header()->OnGroupContentsChanged();
  // 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 std::optional<int> active_index = controller_->GetActiveIndex();
  if (active_index.has_value()) {
    GetTabAtModelIndex(active_index.value())->SchedulePaint();
  }

  if (old_visuals && old_visuals->title() != new_visuals->title()) {
    for (int i = 0; i < GetTabCount(); ++i) {
      Tab* tab = GetTabAtModelIndex(i);
      if (tab && tab->group() == group) {
        tab->UpdateAccessibleName();
      }
    }
  }
}

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::kFromMouse
            : CloseTabSource::kFromTouch;

    // Use actual last tab bounds instead of ideal_bounds to match
    // current_group_width timing.
    EnterTabClosingMode(
        tabs_view_model_.view_at(GetTabCount() - 1)->bounds().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());

  // The ZOrder cache holds pointers to the group views, however on deletion
  // the views in the cache will have to be recalculated and reordered. Marking
  // the cache dirty does not evict the group view pointers, and so a dangling
  // pointer would be created if the cache were not also cleared. This is why
  // the cache must also be evicted.
  MarkZOrderCacheDirty();
  z_ordered_children_cache_.clear();

  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::NotifyTabstripBubbleOpened() {
  // Suppress the mouse watching behavior of tab closing mode.
  RemoveMessageLoopObserver();
}

void TabContainerImpl::NotifyTabstripBubbleClosed() {
  // Restore the mouse watching behavior of tab closing mode.
  if (in_tab_close_) {
    AddMessageLoopObserver();
  }
}

void TabContainerImpl::OnSplitCreated(const std::vector<int>& indices) {
  for (const int index : indices) {
    Tab* const tab = GetTabAtModelIndex(index);
    CHECK(tab->split().has_value());
    tab->UpdateInsets();
  }

  MarkZOrderCacheDirty();
  AnimateToIdealBounds();
}

void TabContainerImpl::OnSplitRemoved(const std::vector<int>& indices) {
  for (const int index : indices) {
    Tab* const tab = GetTabAtModelIndex(index);
    CHECK(!tab->split().has_value());

    // Hide hover effects since the split is removed.
    if (!tab->mouse_hovered()) {
      tab->HideHover(TabStyle::HideHoverStyle::kImmediate);
    }
    tab->UpdateInsets();
  }

  MarkZOrderCacheDirty();
  AnimateToIdealBounds();
}

void TabContainerImpl::OnSplitContentsChanged(const std::vector<int>& indices) {
  for (const int index : indices) {
    Tab* const tab = GetTabAtModelIndex(index);
    CHECK(tab->split().has_value());
    tab->UpdateInsets();

    // Tab shape may change due to reordering (eg. reversing split tabs). We
    // should repaint the focus ring to ensure the highlight path matches the
    // new shape.
    if (auto* focus_ring = views::FocusRing::Get(tab)) {
      focus_ring->SchedulePaint();
    }
  }
}

std::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();
}

std::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 = std::ranges::find(all_tabs, tab);
    while (it < all_tabs.end() && (*it)->closing()) {
      it++;
    }

    if (it == all_tabs.end()) {
      return std::nullopt;
    }
    tab = *it;
  }

  return GetModelIndexOf(tab);
}

void TabContainerImpl::UpdateHoverCard(
    HoverCardAnchorTarget* anchor_target,
    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/40773156 for an example case.
  if (controller_->IsAnimatingInTabStrip()) {
    anchor_target = nullptr;
    update_type = TabSlotController::HoverCardUpdateType::kAnimating;
  }

  if (!hover_card_controller_) {
    return;
  }

  hover_card_controller_->UpdateHoverCard(anchor_target, 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::mojom::MenuSourceType::kTouch);
  }
}

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()) {
    // A hit on an inactive tab is in the content area unless it is in the thin
    // strip mentioned above.
    const std::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(kDragHandleExtension);
      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;
}

std::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 std::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);
}

std::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 std::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::OnTabCloseAnimationCompleted(Tab* tab) {
  DCHECK(tab->closing());
  OnTabRemoved(tab);

  // The ZOrder cache holds pointers to the tab view, however on deletion the
  // views in the cache will have to be recalculated and reordered. Marking the
  // cache dirty does not evict the tab view pointer, and so a dangling pointer
  // would be created if the cache were not also cleared. This is why the cache
  // must also be evicted.
  MarkZOrderCacheDirty();
  z_ordered_children_cache_.clear();

  // Delete `tab`.
  tab->parent()->RemoveChildViewT(tab);
}

void TabContainerImpl::InvalidateIdealBounds() {
  // Invalidation of ideal bounds is handled dynamically by the parent
  // container or other mechanisms.
}

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);
  }

  AnimateViewTo(&*overall_bounds_view_, gfx::Rect(GetIdealTrailingX(), 0));

  // 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_position_delegate_->CompleteEndDragAnimations();
  bounds_animator_.Cancel();
}

void TabContainerImpl::CompleteAnimationAndLayout() {
  CancelAnimation();

  UpdateIdealBounds();
  SnapToIdealBounds();

  SetTabSlotVisibility();
  SchedulePaint();
}

int TabContainerImpl::GetAvailableWidthForTabContainer() const {
  // Falls back to views::View::GetAvailableSize() when
  // `available_width_callback_` is not defined.
  return available_width_callback_
             ? available_width_callback_.Run()
             : parent()->GetAvailableSize(this).width().value();
}

void TabContainerImpl::EnterTabClosingMode(std::optional<int> override_width,
                                           CloseTabSource source) {
  // Tab closing mode only makes sense if the tab was closed as a direct result
  // of a mouse or touch event on the tab.
  if (source == CloseTabSource::kFromNonUIEvent) {
    return;
  }

  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 == CloseTabSource::kFromTouch) {
    StartResizeLayoutTabsFromTouchTimer();
  } else {
    AddMessageLoopObserver();
  }
}

void TabContainerImpl::ExitTabClosingMode() {
  in_tab_close_ = false;
  override_available_width_for_tabs_.reset();
}

void TabContainerImpl::SetTabSlotVisibility() {
  UpdateIdealBounds();

  const std::optional<tab_groups::TabGroupId> focused_group =
      controller_->GetFocusedGroup();

  std::set<tab_groups::TabGroupId> visibility_changed_groups;
  bool last_tab_visible = false;
  std::optional<tab_groups::TabGroupId> last_tab_group;
  std::vector<Tab*> tabs = layout_helper_->GetTabs();
  for (auto it = tabs.rbegin(); true; ++it) {
    Tab* tab = it != tabs.rend() ? *it : nullptr;
    if (last_tab_group.has_value() &&
        (!tab || tab->group() != last_tab_group)) {
      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.
      bool should_header_be_visible = last_tab_visible;
      if (focused_group.has_value() && last_tab_group != focused_group) {
        should_header_be_visible = false;
      }
      if (should_header_be_visible != group_view->header()->GetVisible()) {
        visibility_changed_groups.insert(last_tab_group.value());
      }
      group_view->header()->SetVisible(should_header_be_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()->MaybeSetVisible(should_header_be_visible);
      }
    }

    if (!tab) {
      break;
    }

    std::optional<tab_groups::TabGroupId> current_group = tab->group();
    last_tab_visible = ShouldTabBeVisible(tab);
    last_tab_group = tab->closing() ? std::nullopt : current_group;

    // Tabs outside the focused group are implicitly collapsed in focus mode,
    // unless they are pinned.
    bool is_collapsed_by_focus_mode = false;
    if (focused_group.has_value()) {
      if (!tab->data().pinned) {
        is_collapsed_by_focus_mode =
            !current_group.has_value() ||
            current_group.value() != focused_group.value();
      }
    }

    // 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_by_group =
        current_group.has_value() &&
        controller_->IsGroupCollapsed(current_group.value());

    const bool is_collapsed =
        (is_collapsed_by_group || is_collapsed_by_focus_mode) &&
        (tab->bounds().width() <= tab->tab_style()->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_;
}

std::map<tab_groups::TabGroupId, TabGroupHeader*>
TabContainerImpl::GetGroupHeaders() const {
  std::map<tab_groups::TabGroupId, TabGroupHeader*> header_map;
  for (const auto& entry : group_views_) {
    header_map[entry.first] = entry.second->header();
  }

  return header_map;
}

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(PassKey) {
  UpdateIdealBounds();

  // If the tab strip is not currently animating, directly snap all tabs and
  // group headers to their ideal bounds. This avoids unnecessary animations
  // and ensures a steady state for the layout.
  if (!controller_->IsAnimatingInTabStrip()) {
    SnapToIdealBounds();
  }

  SetTabSlotVisibility();
}

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.

  UpdateZOrderCacheIfDirty();
  for (const auto& child : z_ordered_children_cache_) {
    // The compositor handles painting children with layers separately.
    if (!child.view()->layer()) {
      child.view()->Paint(paint_info);
    }
  }
}

gfx::Size TabContainerImpl::GetMinimumSize() const {
  // During animations, our minimum width tightly hugs the current bounds of our
  // children.
  std::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 = override_available_width_for_tabs_.value_or(
        layout_helper_->CalculateMinimumWidth());
  }

  return gfx::Size(minimum_width.value(),
                   GetLayoutConstant(LayoutConstant::kTabStripHeight));
}

gfx::Size TabContainerImpl::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  // During animations, our preferred width tightly hugs the current bounds of
  // our children.
  std::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(LayoutConstant::kTabStripHeight));
}

views::View* TabContainerImpl::GetTooltipHandlerForPoint(
    const gfx::Point& point) {
  if (!GetVisible()) {
    // TODO(crbug.com/468100896): Remove this once the parent view is destroyed
    // instead of hidden. From crbug.com/467968232, early return if the view is
    // hidden to avoid race condition between the horizontal and vertical tab
    // strip.
    return nullptr;
  }

  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;
}

std::optional<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();

  using BrowserRootView::DropIndex::GroupInclusion::kDontIncludeInGroup;
  using BrowserRootView::DropIndex::GroupInclusion::kIncludeInGroup;
  using BrowserRootView::DropIndex::RelativeToIndex::kInsertBeforeIndex;
  using BrowserRootView::DropIndex::RelativeToIndex::kReplaceIndex;

  // 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 = views::AsViewClass<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();

      enum {
        kInsertToLeft,
        kReplace,
        kInsertToRight,
      } location;

      // When hovering over the left or right quarter of a tab, the drop
      // indicator will point between tabs. Otherwise, it will point at the tab.
      const int hot_width = tab->width() / 4;

      if (x >= (tab->x() + tab->width() - hot_width)) {
        location = kInsertToRight;
      } else if (x < tab->x() + hot_width) {
        location = kInsertToLeft;
      } else {
        location = kReplace;
      }

      switch (location) {
        case kInsertToLeft: {
          const bool first_in_group =
              tab->group().has_value() &&
              model_index ==
                  controller_->GetFirstTabInGroup(tab->group().value());
          return BrowserRootView::DropIndex{
              .index = model_index,
              .relative_to_index = kInsertBeforeIndex,
              .group_inclusion =
                  first_in_group ? kIncludeInGroup : kDontIncludeInGroup};
        }

        case kReplace: {
          return BrowserRootView::DropIndex{
              .index = model_index,
              .relative_to_index = kReplaceIndex,
              .group_inclusion = kDontIncludeInGroup};
        }

        case kInsertToRight: {
          return BrowserRootView::DropIndex{
              .index = model_index + 1,
              .relative_to_index = kInsertBeforeIndex,
              .group_inclusion = kDontIncludeInGroup};
        }
      }
    } else {
      TabGroupHeader* const group_header =
          views::AsViewClass<TabGroupHeader>(view);
      const int first_tab_index =
          controller_->GetFirstTabInGroup(group_header->group().value())
              .value();

      if (x < max_x - group_header->width() / 2) {
        return BrowserRootView::DropIndex{
            .index = first_tab_index,
            .relative_to_index = kInsertBeforeIndex,
            .group_inclusion = kDontIncludeInGroup};
      } else {
        return BrowserRootView::DropIndex{
            .index = first_tab_index,
            .relative_to_index = kInsertBeforeIndex,
            .group_inclusion = kIncludeInGroup};
      }
    }
  }

  // The drop isn't over a tab, add it to the end.
  return BrowserRootView::DropIndex{.index = GetTabCount(),
                                    .relative_to_index = kInsertBeforeIndex,
                                    .group_inclusion = kDontIncludeInGroup};
}

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;
}

views::View* TabContainerImpl::GetViewForDrop() {
  return this;
}

void TabContainerImpl::HandleDragUpdate(
    const std::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();
}

void TabContainerImpl::UpdateZOrderCacheForTesting() {
  UpdateZOrderCacheIfDirty();
}

views::ViewModelT<Tab>* TabContainerImpl::GetTabsViewModel() {
  return &tabs_view_model_;
}

void TabContainerImpl::AnimateViewTo(
    View* view,
    const gfx::Rect& target,
    std::unique_ptr<gfx::AnimationDelegate> delegate) {
  bounds_animator_.SetAnimationDuration(
      gfx::Animation::RichAnimationDuration(base::Milliseconds(200)));
  bounds_animator_.AnimateViewTo(view, target, std::move(delegate));
}

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;
  }

  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;
  }

  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));

  // If a drag session is active, inform the parent container that the
  // preferred size of the tab strip might have changed due to the drag
  // operation. This ensures proper resizing and alignment of the tab strip
  // during drag interactions.
  if (drag_position_delegate_->IsDragSessionActive()) {
    PreferredSizeChanged();
  }
}

int TabContainerImpl::CalculateAvailableWidthForTabs() const {
  int available_width = GetAvailableWidthForTabContainer();
  if (override_available_width_for_tabs_.has_value()) {
    int capped = std::min(*override_available_width_for_tabs_, available_width);
    return capped;
  }
  return available_width;
}

void TabContainerImpl::StartInsertTabAnimation(int model_index) {
  ExitTabClosingMode();

  gfx::Rect bounds = GetTabAtModelIndex(model_index)->bounds();
  bounds.set_height(GetLayoutConstant(LayoutConstant::kTabStripHeight));

  // Adjust the starting bounds of the new tab.
  const int tab_overlap = TabStyle::Get()->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 {
    CHECK_LT(model_index + 1, GetTabCount())
        << "First tab inserted into the tabstrip should not animate.";
    // Otherwise, if we have a tab to our right, start at its left edge.
    bounds.set_x(GetTabAtModelIndex(model_index + 1)->bounds().x());
  }

  // 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) {
  // Update ideal bounds before using them to check if we should stay in tab
  // closing mode. See crbug.com/40838229.
  UpdateIdealBounds();
  if (in_tab_close_ && GetTabCount() > 0 &&
      override_available_width_for_tabs_.value_or(0) >=
          layout_helper_->CalculatePreferredWidth()) {
    // 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.

  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::Get()->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;
}

std::optional<int> TabContainerImpl::GetMidAnimationTrailingX() const {
  if (!controller_->IsAnimatingInTabStrip() || IsDragSessionActive() ||
      IsDragSessionEnding()) {
    return std::nullopt;
  }

  // During animations not related to a drag session, we want to tightly hug
  // our tabs. The `overall_bounds_view_` is animated smoothly to the ideal
  // trailing X and is free from the rounding jitter of individual child
  // views.
  return overall_bounds_view_->bounds().right();
}

void TabContainerImpl::CloseTabInViewModel(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::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 visible_tab_count = GetVisibleTabCount();

  // If we're closing the last visible tab, tab closing mode is no longer
  // meaningful.
  if (visible_tab_count <= 1) {
    ExitTabClosingMode();
    return;
  }

  const int last_visible_tab_index = GetLastVisibleTabModelIndex();
  // Closing the trailingmost visible tab allows remaining visible tabs to
  // expand.
  if (!in_tab_close_ || model_index >= last_visible_tab_index) {
    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_->layout_domain() ==
          LayoutDomain::kInactiveWidthBelowActiveWidth) {
    const std::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();
      }
    }
  }

  const int current_width =
      override_available_width_for_tabs_.value_or(GetIdealTrailingX());
  override_available_width_for_tabs_ =
      current_width - size_delta + TabStyle::Get()->GetTabOverlap();
}

bool TabContainerImpl::IsTabVisible(const Tab* tab) const {
  if (!tab) {
    return false;
  }
  if (tab->data().pinned) {
    return true;
  }
  std::optional<tab_groups::TabGroupId> focused_group =
      controller_->GetFocusedGroup();
  if (focused_group.has_value()) {
    return tab->group() == focused_group;
  }
  if (tab->group().has_value() &&
      controller_->IsGroupCollapsed(tab->group().value())) {
    return false;
  }
  return true;
}

int TabContainerImpl::GetVisibleTabCount() const {
  int count = 0;
  for (int i = 0; i < GetTabCount(); ++i) {
    if (IsTabVisible(GetTabAtModelIndex(i))) {
      ++count;
    }
  }
  return count;
}

int TabContainerImpl::GetLastVisibleTabModelIndex() const {
  for (int i = GetTabCount() - 1; i >= 0; --i) {
    if (IsTabVisible(GetTabAtModelIndex(i))) {
      return i;
    }
  }
  return -1;
}

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_position_delegate_` may be null in tests.
  return drag_position_delegate_ &&
         drag_position_delegate_->IsDragSessionActive();
}

bool TabContainerImpl::IsDragSessionEnding() const {
  // `drag_position_delegate_` may be null in tests.
  return drag_position_delegate_ &&
         drag_position_delegate_->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.
  ReorderChildView(slot_view, GetChildIndexForSlotView(slot_view));
  MarkZOrderCacheDirty();
}

size_t TabContainerImpl::GetChildIndexForSlotView(
    const TabSlotView* slot_view) const {
  const std::vector<TabSlotView*> slots = layout_helper_->GetTabSlotViews();
  const auto slot_it = std::ranges::find(slots, slot_view);
  CHECK(slot_it != slots.end());
  const size_t target_slot_index = slot_it - 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;
  }
  return 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;
  }

  const gfx::Point point_in_tab_coords =
      View::ConvertPointToTarget(this, tab, point_in_tabstrip_coords);
  return tab->HitTestPoint(point_in_tab_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 {
  // 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_position_delegate_->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.
  const Tab* active_tab =
      GetTabAtModelIndex(controller_->GetActiveIndex().value());
  int active_tab_width = active_tab->width();
  if (IsAnimating()) {
    right_edge = bounds_animator_.GetTargetBounds(tab).right();
    active_tab_width = bounds_animator_.GetTargetBounds(active_tab).width();
  }
  return right_edge + active_tab_width - tab->width() <= tabstrip_right;
}

gfx::Rect TabContainerImpl::GetDropBounds(
    const BrowserRootView::DropIndex& drop_index,
    DropArrow::Direction* direction) {
  DCHECK_NE(drop_index.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.index, GetTabCount() - 1));
  const bool first_in_group =
      drop_index.index < GetTabCount() && tab->group().has_value() &&
      GetModelIndexOf(tab) ==
          controller_->GetFirstTabInGroup(tab->group().value());

  const int overlap = tab->tab_style()->GetTabOverlap();
  const bool drop_before =
      drop_index.relative_to_index ==
      BrowserRootView::DropIndex::RelativeToIndex::kInsertBeforeIndex;
  const bool drop_in_group =
      drop_index.group_inclusion ==
      BrowserRootView::DropIndex::GroupInclusion::kIncludeInGroup;
  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.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 - DropArrow::kSize / 2, -DropArrow::kSize);
  ConvertPointToScreen(this, &drop_loc);
  gfx::Rect drop_bounds(drop_loc.x(), drop_loc.y(), DropArrow::kSize,
                        DropArrow::kSize);

  // If the rect doesn't fit on the monitor, push the arrow to the bottom.
  display::Screen* screen = display::Screen::Get();
  gfx::Rect display_bounds =
      screen->GetDisplayNearestView(GetWidget()->GetNativeView()).bounds();

  DropArrow::MaybeAdjustDisplayBounds(display_bounds);

  const bool is_beneath = drop_bounds.y() < display_bounds.y();
  *direction =
      is_beneath ? DropArrow::Direction::kUp : DropArrow::Direction::kDown;
  if (is_beneath) {
    drop_bounds.Offset(0, drop_bounds.height() + height());
  }

  return drop_bounds;
}

void TabContainerImpl::SetDropArrow(
    const std::optional<BrowserRootView::DropIndex>& index) {
  if (!index) {
    controller_->OnDropIndexUpdate(std::nullopt, false);
    drop_arrow_.reset();
    return;
  }

  // Let the controller know of the index update.
  const bool drop_before =
      index->relative_to_index ==
      BrowserRootView::DropIndex::RelativeToIndex::kInsertBeforeIndex;
  controller_->OnDropIndexUpdate(index->index, drop_before);

  if (drop_arrow_ && (index == drop_arrow_->index())) {
    return;
  }

  if (!drop_arrow_) {
    drop_arrow_ = std::make_unique<DropArrow>(
        *index, GetWidget()->GetNativeWindow(),
        base::BindRepeating(&TabContainerImpl::GetDropBounds,
                            base::Unretained(this)));
  } else {
    drop_arrow_->SetIndex(*index);
  }
}

void TabContainerImpl::UpdateAccessibleTabIndices() {
  const int num_tabs = GetTabCount();
  for (int i = 0; i < num_tabs; ++i) {
    GetTabAtModelIndex(i)->GetViewAccessibility().SetPosInSet(i + 1);
    GetTabAtModelIndex(i)->GetViewAccessibility().SetSetSize(num_tabs);
  }
}

bool TabContainerImpl::IsValidModelIndex(int model_index) const {
  return controller_->IsValidModelIndex(model_index);
}

void TabContainerImpl::UpdateZOrderCacheIfDirty() {
  if (!z_order_cache_dirty_) {
    return;
  }
  z_ordered_children_cache_.clear();
  z_ordered_children_cache_.reserve(children().size());
  for (views::View* child : children()) {
    if (!ZOrderableTabContainerElement::CanOrderView(child)) {
      continue;
    }
    z_ordered_children_cache_.emplace_back(child);
  }
  std::stable_sort(z_ordered_children_cache_.begin(),
                   z_ordered_children_cache_.end());
  z_order_cache_dirty_ = false;
}

BEGIN_METADATA(TabContainerImpl)
ADD_READONLY_PROPERTY_METADATA(int, AvailableWidthForTabContainer)
END_METADATA
