// Copyright 2012 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.h"

#include <stddef.h>

#include <algorithm>
#include <memory>
#include <optional>
#include <string_view>
#include <utility>

#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/metrics/user_metrics.h"
#include "base/numerics/safe_conversions.h"
#include "base/scoped_observation.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "cc/paint/paint_flags.h"
#include "chrome/browser/dictation/dictation_keyed_service.h"
#include "chrome/browser/glic/browser_ui/tab_underline_controller.h"
#include "chrome/browser/glic/browser_ui/tab_underline_view.h"
#include "chrome/browser/glic/public/glic_enabling.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/tab_contents/core_tab_helper.h"
#include "chrome/browser/ui/tabs/alert/tab_alert_controller.h"
#include "chrome/browser/ui/tabs/saved_tab_groups/collaboration_messaging_tab_data.h"
#include "chrome/browser/ui/tabs/tab_change_type.h"
#include "chrome/browser/ui/tabs/tab_data.h"
#include "chrome/browser/ui/tabs/tab_style.h"
#include "chrome/browser/ui/thumbnails/thumbnail_image.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/view_ids.h"
#include "chrome/browser/ui/views/event_utils.h"
#include "chrome/browser/ui/views/frame/browser_frame_view.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/tabs/browser_tab_strip_controller.h"
#include "chrome/browser/ui/views/tabs/dragging/tab_drag_controller.h"
#include "chrome/browser/ui/views/tabs/shared/tab_strip_types.h"
#include "chrome/browser/ui/views/tabs/tab/alert_indicator_button.h"
#include "chrome/browser/ui/views/tabs/tab/glow_hover_controller.h"
#include "chrome/browser/ui/views/tabs/tab/tab_accessibility.h"
#include "chrome/browser/ui/views/tabs/tab/tab_close_button.h"
#include "chrome/browser/ui/views/tabs/tab/tab_icon.h"
#include "chrome/browser/ui/views/tabs/tab/tab_title.h"
#include "chrome/browser/ui/views/tabs/tab_slot_controller.h"
#include "chrome/browser/ui/views/tabs/tab_slot_view.h"
#include "chrome/browser/ui/views/tabs/tab_strip.h"
#include "chrome/browser/ui/views/tabs/tab_style_views.h"
#include "chrome/browser/ui/window_metadata/window_metadata_controller.h"
#include "chrome/common/chrome_features.h"
#include "chrome/grit/generated_resources.h"
#include "components/contextual_tasks/public/features.h"
#include "components/tabs/public/tab_alert.h"
#include "components/tabs/public/tab_group.h"
#include "components/tabs/public/tab_interface.h"
#include "third_party/skia/include/core/SkPath.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/list_selection_model.h"
#include "ui/base/pointer/touch_ui_controller.h"
#include "ui/base/theme_provider.h"
#include "ui/compositor/clip_recorder.h"
#include "ui/compositor/compositor.h"
#include "ui/gfx/animation/tween.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/favicon_size.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/border.h"
#include "ui/views/bubble/bubble_border.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/metadata/view_factory.h"
#include "ui/views/rect_based_targeting_utils.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_targeter.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/tooltip_manager.h"
#include "ui/views/widget/widget.h"

#if BUILDFLAG(IS_WIN)
#include "ui/views/win/pen_event_handler_util.h"
#endif

#if defined(USE_AURA)
#endif

#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/ash/boca/on_task/on_task_locked_controller.h"
#endif

using base::UserMetricsAction;
namespace {

// When a non-pinned tab becomes a pinned tab the width of the tab animates. If
// the width of a pinned tab is at least kPinnedTabExtraWidthToRenderAsNormal
// larger than the desired pinned tab width then the tab is rendered as a normal
// tab. This is done to avoid having the title immediately disappear when
// transitioning a tab from normal to pinned tab.
constexpr int kPinnedTabExtraWidthToRenderAsNormal = 30;

// Additional padding of close button to the right of the tab
// indicator when `extra_alert_indicator_padding_` is true.
constexpr int kTabAlertIndicatorCloseButtonPaddingAdjustmentTouchUI = 8;
constexpr int kTabAlertIndicatorCloseButtonPaddingAdjustment = 4;

bool g_show_hover_card_on_mouse_hover = true;

// Helper functions ------------------------------------------------------------

// Returns the coordinate for an object of size `item_size` centered in a region
// of size `size`, biasing towards placing any extra space ahead of the object.
int Center(int size, int item_size) {
  int extra_space = size - item_size;
  // Integer division below truncates, thus effectively "rounding toward zero";
  // to always place extra space ahead of the object, we want to round towards
  // positive infinity, which means we need to bias the division only when the
  // size difference is positive.  (Adding one unconditionally will stack with
  // the truncation if `extra_space` is negative, resulting in off-by-one
  // errors.)
  if (extra_space > 0) {
    ++extra_space;
  }
  return extra_space / 2;
}

#if BUILDFLAG(IS_CHROMEOS)
// Returns true if the tab should be locked for the task and false otherwise.
bool IsLockedForOnTask(BrowserWindowInterface* browser_window_interface) {
  return browser_window_interface
             ? ash::boca::OnTaskLockedController::From(browser_window_interface)
                   ->is_locked_for_on_task()
             : false;
}
#endif  // BUILDFLAG(IS_CHROMEOS)

class TabStyleHighlightPathGenerator : public views::HighlightPathGenerator {
 public:
  explicit TabStyleHighlightPathGenerator(TabStyleViews* tab_style_views)
      : tab_style_views_(tab_style_views) {}
  TabStyleHighlightPathGenerator(const TabStyleHighlightPathGenerator&) =
      delete;
  TabStyleHighlightPathGenerator& operator=(
      const TabStyleHighlightPathGenerator&) = delete;

  // views::HighlightPathGenerator:
  SkPath GetHighlightPath(const views::View* view) override {
    return tab_style_views_->GetPath(TabStyle::PathType::kHighlight, 1.0, {});
  }

 private:
  const raw_ptr<TabStyleViews, AcrossTasksDanglingUntriaged> tab_style_views_;
};

class TabStyleViewDelegateImpl : public TabStyleViewDelegate {
 public:
  explicit TabStyleViewDelegateImpl(const Tab* tab) : tab_(tab) { CHECK(tab_); }
  ~TabStyleViewDelegateImpl() override = default;

  const views::View* GetView() const override { return tab_; }
  bool IsActive() const override { return tab_->IsActive(); }
  bool IsSelected() const override { return tab_->IsSelected(); }
  bool IsHovering() const override { return tab_->IsHovering(); }
  bool IsClosing() const override { return tab_->closing(); }
  std::optional<tab_groups::TabGroupId> GetGroup() const override {
    return tab_->group();
  }
  std::optional<SkColor> GetGroupColor() const override {
    return tab_->GetGroupColor();
  }
  bool IsInFocusedGroup() const override {
    const std::optional<tab_groups::TabGroupId> group = tab_->group();
    return group.has_value() && tab_->controller()->GetFocusedGroup() == group;
  }
  bool IsSplit() const override { return tab_->split().has_value(); }
  std::optional<split_tabs::SplitTabId> GetSplit() const override {
    return tab_->split();
  }
  int GetTabCount() const override { return tab_->controller()->GetTabCount(); }

  bool IsLeftSplitTab() const override {
    if (!tab_->split().has_value()) {
      return false;
    }
    const std::vector<Tab*>& tabs_in_split =
        tab_->controller()->GetTabsInSplit(tab_);
    if (tabs_in_split.size() < 2) {
      return true;
    }
    return tab_ ==
           tabs_in_split[base::i18n::IsRTL() ? tabs_in_split.size() - 1 : 0];
  }

  bool IsRightSplitTab() const override {
    if (!tab_->split().has_value()) {
      return false;
    }
    const std::vector<Tab*>& tabs_in_split =
        tab_->controller()->GetTabsInSplit(tab_);
    if (tabs_in_split.size() < 2) {
      return true;
    }
    return tab_ ==
           tabs_in_split[base::i18n::IsRTL() ? 0 : tabs_in_split.size() - 1];
  }

  const TabStyleViewDelegate* GetAdjacentTab(bool leading) const override {
    const Tab* adjacent =
        tab_->controller()->GetAdjacentTab(tab_, leading ? -1 : 1);
    return (adjacent && adjacent->tab_style_views())
               ? adjacent->tab_style_views()->delegate()
               : nullptr;
  }

  float GetHoverAnimationValue() const override {
    return tab_->GetHoverAnimationValue();
  }

  float GetHoverOpacity() const override { return tab_->GetHoverOpacity(); }

  bool IsHoverAnimationActive() const override {
    return tab_->IsHoverAnimationActive();
  }

  bool IsGlassFrame() const override {
    return tab_->controller()->IsGlassFrame();
  }

  bool IsPinned() const override { return tab_->data().pinned; }

  bool ShouldPaintTabBackgroundColor() const override {
    return tab_->should_fill_background_tab_color();
  }

  int GetStrokeThickness() const override {
    return tab_->controller()->GetStrokeThickness();
  }

  GlowHoverController* GetHoverControllerForTesting() override {
    return const_cast<Tab*>(tab_.get())
        ->GetHoverControllerForTesting();  // IN-TEST
  }

  BrowserFrameView* GetBrowserFrameView() const override {
    BrowserWindowInterface* browser_window_interface =
        tab_->controller()->GetBrowserWindowInterface();
    if (!browser_window_interface) {
      CHECK_IS_TEST();
      return nullptr;
    }
    BrowserView* browser_view =
        BrowserView::GetBrowserViewForBrowser(browser_window_interface);
    if (!browser_view || !browser_view->browser_widget()) {
      return nullptr;
    }
    return browser_view->browser_widget()->GetFrameView();
  }

  BrowserWindowInterface* GetBrowserWindowInterface() const override {
    return tab_->controller()->GetBrowserWindowInterface();
  }

 private:
  const raw_ptr<const Tab> tab_;
};

}  // namespace

// Helper class that observes the tab's close button.
class Tab::TabCloseButtonObserver : public views::ViewObserver {
 public:
  explicit TabCloseButtonObserver(Tab* tab,
                                  views::View* close_button,
                                  TabSlotController* controller)
      : tab_(tab), close_button_(close_button), controller_(controller) {
    DCHECK(close_button_);
    tab_close_button_observation_.Observe(close_button_.get());
  }
  TabCloseButtonObserver(const TabCloseButtonObserver&) = delete;
  TabCloseButtonObserver& operator=(const TabCloseButtonObserver&) = delete;

  ~TabCloseButtonObserver() override {
    DCHECK(tab_close_button_observation_.IsObserving());
    tab_close_button_observation_.Reset();
  }

 private:
  void OnViewFocused(views::View* observed_view) override {
    controller_->UpdateHoverCard(
        tab_, TabSlotController::HoverCardUpdateType::kFocus);
    if (features::IsTabStripDeclutterEnabled()) {
      tab_->InvalidateLayout();
    }
  }

  void OnViewBlurred(views::View* observed_view) override {
    // Only hide hover card if not keyboard navigating.
    if (!controller_->IsFocusInTabStrip()) {
      controller_->UpdateHoverCard(
          nullptr, TabSlotController::HoverCardUpdateType::kFocus);
    }
    if (features::IsTabStripDeclutterEnabled()) {
      tab_->InvalidateLayout();
    }
  }

  base::ScopedObservation<views::View, views::ViewObserver>
      tab_close_button_observation_{this};

  raw_ptr<Tab, DanglingUntriaged> tab_;
  raw_ptr<views::View, DanglingUntriaged> close_button_;
  raw_ptr<TabSlotController, DanglingUntriaged> controller_;
};

// Tab -------------------------------------------------------------------------

// static
void Tab::SetShowHoverCardOnMouseHoverForTesting(bool value) {
  g_show_hover_card_on_mouse_hover = value;
}

Tab::Tab(tabs::TabHandle handle, TabSlotController* controller)
    : HoverCardAnchorTarget(this),
      tab_handle_(handle),
      controller_(controller),
      hover_controller_(gfx::Animation::ShouldRenderRichAnimation()
                            ? std::make_unique<GlowHoverController>(this)
                            : nullptr),
      title_(new TabTitle()),
      title_animation_(this) {
  DCHECK(controller);

  tab_style_views_ = TabStyleViews::Create(CreateStyleDelegate(this),
                                           TabStripOrientation::kHorizontal);

  // So we get don't get enter/exit on children and don't prematurely stop the
  // hover.
  SetNotifyEnterExitOnChild(true);

  SetID(VIEW_ID_TAB);

  AddChildViewRaw(title_.get());

  SetEventTargeter(std::make_unique<views::ViewTargeter>(this));

  icon_ = AddChildView(std::make_unique<TabIcon>());

  alert_indicator_button_ =
      AddChildView(std::make_unique<AlertIndicatorButton>(this));

  BrowserWindowInterface* const browser_window_interface =
      controller_->GetBrowserWindowInterface();

  bool should_create_underline = false;
  if (browser_window_interface) {
    Profile* profile = browser_window_interface->GetProfile();
    should_create_underline =
        (base::FeatureList::IsEnabled(features::kGlicMultitabUnderlines) &&
         glic::GlicEnabling::IsProfileEligible(profile)) ||
        contextual_tasks::IsContextualTasksUIEnabled() ||
        (dictation::DictationKeyedService::Get(profile));
  }

  if (should_create_underline) {
    glic_tab_underline_view_ = AddChildView(
        views::Builder<glic::TabUnderlineView>(
            glic::TabUnderlineView::Factory::Create(
                std::make_unique<glic::TabUnderlineController>(tab_handle_),
                browser_window_interface, tab_handle_))
            .Build());
  }

  // Unretained is safe here because this class outlives its close button, and
  // the controller outlives this Tab.
  close_button_ = AddChildView(std::make_unique<TabCloseButton>(
      base::BindRepeating(&Tab::CloseButtonPressed, base::Unretained(this)),
      base::BindRepeating(&TabSlotController::OnMouseEventInTab,
                          base::Unretained(controller_))));

  // This will cause calls to GetContentsBounds to return only the rectangle
  // inside the tab shape, rather than to its extents.
  UpdateInsets();

#if BUILDFLAG(IS_CHROMEOS)
  showing_close_button_ = !IsLockedForOnTask(browser_window_interface);

  close_button_->SetVisible(showing_close_button_);
#endif

  tab_close_button_observer_ = std::make_unique<TabCloseButtonObserver>(
      this, close_button_, controller_);

  title_animation_.SetDuration(base::Milliseconds(100));

  // Enable keyboard focus.
  SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
  views::FocusRing::Install(this);
  views::HighlightPathGenerator::Install(
      this,
      std::make_unique<TabStyleHighlightPathGenerator>(tab_style_views()));

  SetProperty(views::kElementIdentifierKey, kTabElementId);

  GetViewAccessibility().SetRole(ax::mojom::Role::kTab);
  UpdateAccessibleName();

  // Tab hover cards replace tooltips for tabs.
  SetTooltipText(std::u16string());

  root_name_changed_subscription_ =
      GetViewAccessibility().AddStringAttributeChangedCallback(
          ax::mojom::StringAttribute::kName,
          base::BindRepeating(&Tab::OnAXNameChanged,
                              weak_ptr_factory_.GetWeakPtr()));

  // TabInterface can be null during unit tests
  tabs::TabInterface* const tab_interface = tab_handle_.Get();
  if (tab_interface) {
    tab_data_observer_ = std::make_unique<tabs::TabDataObserver>(tab_interface);
    // base::Unretained(this) is safe because the Tab owns the TabDataObserver
    // so it is guaranteed to outlive the data observer.
    tab_data_change_subscription_ =
        tab_data_observer_->RegisterTabDataChangedCallback(base::BindRepeating(
            &Tab::OnTabDataChanged, base::Unretained(this)));
  }
}

Tab::~Tab() {
  // Observer must be unregistered before child views are destroyed.
  tab_close_button_observer_.reset();
  if (controller_->HoverCardIsShowing(this)) {
    controller_->UpdateHoverCard(
        nullptr, TabSlotController::HoverCardUpdateType::kTabRemoved);
  }
}

bool Tab::IsActive() const {
  if (split()) {
    return std::ranges::any_of(controller()->GetTabsInSplit(this),
                               [this](const Tab* split_tab) {
                                 return controller_->IsActiveTab(split_tab);
                               });
  } else {
    return controller_->IsActiveTab(this);
  }
}

void Tab::AnimationEnded(const gfx::Animation* animation) {
  DCHECK_EQ(animation, &title_animation_);
  title_->SetBoundsRect(target_title_bounds_);
}

void Tab::AnimationProgressed(const gfx::Animation* animation) {
  DCHECK_EQ(animation, &title_animation_);
  title_->SetBoundsRect(gfx::Tween::RectValueBetween(
      gfx::Tween::CalculateValue(gfx::Tween::FAST_OUT_SLOW_IN,
                                 animation->GetCurrentValue()),
      start_title_bounds_, target_title_bounds_));
}

bool Tab::GetHitTestMask(SkPath* mask) const {
  // When the window is maximized we don't want to shave off the edges or top
  // shadow of the tab, such that the user can click anywhere along the top
  // edge of the screen to select a tab. Ditto for immersive fullscreen.
  *mask = tab_style_views()->GetPath(
      TabStyle::PathType::kHitTest,
      GetWidget()->GetCompositor()->device_scale_factor(),
      {.render_units = TabStyle::RenderUnits::kDips});
  return true;
}

void Tab::Layout(PassKey) {
  const gfx::Rect contents_rect = GetContentsBounds();

  const bool was_showing_icon = showing_icon_;
  UpdateIconVisibility();

  const int start = contents_rect.x();

  // Position the underline under the tab contents.
  constexpr int kGlicUnderlineYOffset = 8;
  if (glic_tab_underline_view_) {
    gfx::Rect glic_bounds =
        contents_rect + gfx::Vector2d(0, kGlicUnderlineYOffset);
    // Use the full width of the tab in order to accommodate small tab sizes
    // where the width of the contents bounds is 0.
    glic_bounds.set_x(0);
    glic_bounds.set_width(size().width());
    glic_tab_underline_view_->SetBoundsRect(glic_bounds);
  }

  // The bounds for the favicon will include extra width for the attention
  // indicator, but visually it will be smaller at kFaviconSize wide.
  gfx::Rect favicon_bounds(start, contents_rect.y(), 0, 0);
  if (showing_icon_) {
    if (center_icon_) {
      // When centering the favicon, the favicon is allowed to escape the normal
      // contents rect.
      favicon_bounds.set_x(Center(width(), gfx::kFaviconSize));
    } else {
      MaybeAdjustLeftForPinnedTab(&favicon_bounds, gfx::kFaviconSize);
    }
    icon_->ResizeDiscardIndicatorRadiusForWidth(
        width() - 2 * tab_style()->GetBottomCornerRadius());

    // Add space for insets outside the favicon bounds.
    favicon_bounds.Inset(-icon_->GetInsets());
    favicon_bounds.set_size(icon_->GetPreferredSize());
  }
  icon_->SetBoundsRect(favicon_bounds);
  icon_->SetVisible(showing_icon_);

  const int after_title_padding =
      GetLayoutConstant(LayoutConstant::kTabAfterTitlePadding);

  int close_x = contents_rect.right();
  if (showing_close_button_) {
    // The visible size is the button's hover shape size. The actual size
    // includes the border insets for the button.
    const int close_button_visible_size =
        GetLayoutConstant(LayoutConstant::kTabCloseButtonSize);
    const gfx::Size close_button_actual_size =
        close_button_->GetPreferredSize();

    // The close button is vertically centered in the contents_rect.
    const int top =
        contents_rect.y() +
        Center(contents_rect.height(), close_button_actual_size.height());

    // The visible part of the close button should be placed against the
    // right of the contents rect unless the tab is so small that it would
    // overflow the left side of the contents_rect, in that case it will be
    // placed in the middle of the tab.
    const int visible_left =
        (center_icon_ && features::IsTabStripDeclutterEnabled())
            ? Center(width(), close_button_visible_size)
            : std::max(close_x - close_button_visible_size,
                       Center(width(), close_button_visible_size));

    // Offset the new bounds rect by the extra padding in the close button.
    const int non_visible_left_padding =
        (close_button_actual_size.width() - close_button_visible_size) / 2;

    close_button_->SetBoundsRect(
        {gfx::Point(visible_left - non_visible_left_padding, top),
         close_button_actual_size});
    close_x = visible_left - after_title_padding;
  }
  close_button_->SetVisible(showing_close_button_);

  if (showing_alert_indicator_) {
    int right = contents_rect.right();
    if (showing_close_button_) {
      right = close_x;
      if (extra_alert_indicator_padding_) {
        right -= ui::TouchUiController::Get()->touch_ui()
                     ? kTabAlertIndicatorCloseButtonPaddingAdjustmentTouchUI
                     : kTabAlertIndicatorCloseButtonPaddingAdjustment;
      }
    }
    const gfx::Size image_size = alert_indicator_button_->GetPreferredSize();
    gfx::Rect bounds(
        std::max(contents_rect.x(), right - image_size.width()),
        contents_rect.y() + Center(contents_rect.height(), image_size.height()),
        image_size.width(), image_size.height());
    if (center_icon_) {
      // When centering the alert icon, it is allowed to escape the normal
      // contents rect.
      bounds.set_x(Center(width(), bounds.width()));
    } else {
      MaybeAdjustLeftForPinnedTab(&bounds, bounds.width());
    }
    alert_indicator_button_->SetBoundsRect(bounds);
  }
  alert_indicator_button_->UpdateAlertIndicatorAnimation();
  alert_indicator_button_->SetVisible(showing_alert_indicator_);

  // Size the title to fill the remaining width and use all available height.
  bool show_title = ShouldRenderAsNormalTab();

  if (features::IsTabStripDeclutterEnabled() && center_icon_) {
    show_title = false;
  }

  if (show_title) {
    int title_left = start;
    if (showing_icon_) {
      // When computing the spacing from the favicon, don't count the actual
      // icon view width (which will include extra room for the alert
      // indicator), but rather the normal favicon width which is what it will
      // look like.
      const int after_favicon =
          favicon_bounds.x() + icon_->GetInsets().left() + gfx::kFaviconSize +
          GetLayoutConstant(LayoutConstant::kTabPreTitlePadding);
      title_left = std::max(title_left, after_favicon);
    }
    int title_right = contents_rect.right();
    if (showing_alert_indicator_) {
      title_right = alert_indicator_button_->x() - after_title_padding;
    } else if (showing_close_button_) {
      // Allow the title to overlay the close button's empty border padding.
      title_right = close_x - after_title_padding;
    }
    const int title_width = std::max(title_right - title_left, 0);
    // The Label will automatically center the font's cap height within the
    // provided vertical space.
    const gfx::Rect title_bounds(title_left, GetLocalBounds().y(), title_width,
                                 GetLocalBounds().height());
    show_title = title_width > 0;

    if (title_bounds != target_title_bounds_) {
      target_title_bounds_ = title_bounds;
      if (was_showing_icon == showing_icon_ || title_->bounds().IsEmpty() ||
          title_bounds.IsEmpty()) {
        title_animation_.Stop();
        title_->SetBoundsRect(title_bounds);
      } else if (!title_animation_.is_animating()) {
        start_title_bounds_ = title_->bounds();
        title_animation_.Start();
      }
    }
  }
  title_->SetVisible(show_title);

  if (auto* focus_ring = views::FocusRing::Get(this); focus_ring) {
    focus_ring->DeprecatedLayoutImmediately();
  }
}

bool Tab::OnKeyPressed(const ui::KeyEvent& event) {
#if BUILDFLAG(IS_MAC)
  if (event.key_code() == ui::VKEY_RETURN && event.IsControlDown()) {
    ShowContextMenu(GetKeyboardContextMenuLocation(),
                    ui::mojom::MenuSourceType::kKeyboard);
    return true;
  }
#endif
  if (event.key_code() == ui::VKEY_RETURN && !IsSelected()) {
    controller_->SelectTab(this, event);
    return true;
  }

  std::optional<event_utils::ReorderDirection> reorder_direction =
      event_utils::GetReorderCommandForKeyboardEvent(event);
  if (!reorder_direction) {
    return false;
  }

  bool move_to_end = event.flags() & ui::EF_SHIFT_DOWN;
  switch (*reorder_direction) {
    case event_utils::ReorderDirection::kPrevious: {
      if (move_to_end) {
        controller()->MoveTabFirst(this);
      } else {
        controller()->ShiftTabPrevious(this);
      }
      break;
    }
    case event_utils::ReorderDirection::kNext: {
      if (move_to_end) {
        controller()->MoveTabLast(this);
      } else {
        controller()->ShiftTabNext(this);
      }
      break;
    }
  }

  return true;
}

bool Tab::OnKeyReleased(const ui::KeyEvent& event) {
  if (event.key_code() == ui::VKEY_SPACE && !IsSelected()) {
    controller_->SelectTab(this, event);
    return true;
  }
  return false;
}

namespace {
bool IsSelectionModifierDown(const ui::MouseEvent& event) {
#if BUILDFLAG(IS_MAC)
  return event.IsCommandDown();
#else
  return event.IsControlDown();
#endif
}
}  // namespace

bool Tab::OnMousePressed(const ui::MouseEvent& event) {
  shift_pressed_on_mouse_down_ = event.IsShiftDown();
  controller_->UpdateHoverCard(nullptr,
                               TabSlotController::HoverCardUpdateType::kEvent);
  controller_->OnMouseEventInTab(this, event);

  // Allow a right click from touch to drag, which corresponds to a long click.
  if (event.IsOnlyLeftMouseButton() ||
      (event.IsOnlyRightMouseButton() && event.flags() & ui::EF_FROM_TOUCH)) {
    ui::ListSelectionModel original_selection =
        controller_->GetSelectionModel();
    // Changing the selection may cause our bounds to change. If that happens
    // the location of the event may no longer be valid. Create a copy of the
    // event in the parents coordinate, which won't change, and recreate an
    // event after changing so the coordinates are correct.
    ui::MouseEvent event_in_parent(event, views::AsViewClass<View>(this),
                                   parent());
    if (event.IsShiftDown() && IsSelectionModifierDown(event)) {
      controller_->AddSelectionFromAnchorTo(this);
      base::RecordAction(
          UserMetricsAction("TabMultiSelect_AddSelectionFromAnchorTo"));
    } else if (event.IsShiftDown()) {
      controller_->ExtendSelectionTo(this);
      base::RecordAction(UserMetricsAction("TabMultiSelect_ExtendSelectionTo"));
    } else if (IsSelectionModifierDown(event)) {
      controller_->ToggleSelected(this);
      base::RecordAction(UserMetricsAction("TabMultiSelect_ToggleSelected"));
      if (!IsSelected()) {
        // Don't allow dragging non-selected tabs.
        return false;
      }
    } else if (!IsSelected()) {
      controller_->SelectTab(this, event);
      base::RecordAction(UserMetricsAction("SwitchTab_Click"));
    }
    ui::MouseEvent cloned_event(event_in_parent, parent(),
                                views::AsViewClass<View>(this));

    if (!closing()) {
      controller_->MaybeStartDrag(this, cloned_event, original_selection);
    }
  }
  return true;
}

bool Tab::OnMouseDragged(const ui::MouseEvent& event) {
  // TODO: ensure ignoring return value is ok.
  std::ignore = controller_->ContinueDrag(this, event);
  return true;
}

void Tab::OnMouseReleased(const ui::MouseEvent& event) {
  base::WeakPtr<Tab> self = weak_ptr_factory_.GetWeakPtr();
  controller_->OnMouseEventInTab(this, event);

  // Notify the drag helper that we're done with any potential drag operations.
  // Clean up the drag helper, which is re-created on the next mouse press.
  // In some cases, ending the drag will schedule the tab for destruction; if
  // so, bail immediately, since our members are already dead and we shouldn't
  // do anything else except drop the tab where it is.
  if (controller_->EndDrag(EndDragReason::kComplete)) {
    shift_pressed_on_mouse_down_ = false;
    return;
  }

  // Close tab on middle click, but only if the button is released over the tab
  // (normal windows behavior is to discard presses of a UI element where the
  // releases happen off the element).
  if (event.IsOnlyMiddleMouseButton()) {
    if (HitTestPoint(event.location())) {
      controller_->CloseTab(this, CloseTabSource::kFromMouse);
    } else if (closing_) {
      // We're animating closed and a middle mouse button was pushed on us but
      // we don't contain the mouse anymore. We assume the user is clicking
      // quicker than the animation and we should close the tab that falls under
      // the mouse.
      gfx::Point location_in_parent = event.location();
      ConvertPointToTarget(this, parent(), &location_in_parent);
      Tab* closest_tab = controller_->GetTabAt(location_in_parent);
      if (closest_tab) {
        controller_->CloseTab(closest_tab, CloseTabSource::kFromMouse);
      }
    }
  } else if (event.IsOnlyLeftMouseButton() &&
             !(event.IsShiftDown() || shift_pressed_on_mouse_down_) &&
             !IsSelectionModifierDown(event)) {
    // If the tab was already selected mouse pressed doesn't change the
    // selection. Reset it now to handle the case where multiple tabs were
    // selected.
    controller_->SelectTab(this, event);
  }
  // If the tab was closed with the animation disabled, the tab may have
  // already been destroyed.
  if (!self) {
    return;
  }
  shift_pressed_on_mouse_down_ = false;
}

void Tab::OnMouseCaptureLost() {
  controller_->EndDrag(EndDragReason::kCaptureLost);
}

void Tab::OnMouseMoved(const ui::MouseEvent& event) {
  controller_->OnMouseEventInTab(this, event);

  // Linux enter/leave events are sometimes flaky, so we don't want to "miss"
  // an enter event and fail to hover the tab.
  //
  // In Windows, we won't miss the enter event but mouse input is disabled after
  // a touch gesture and we could end up ignoring the enter event. If the user
  // subsequently moves the mouse, we need to then hover the tab.
  //
  // Either way, this is effectively a no-op if the tab is already in a hovered
  // state (crbug.com/40840442).
  MaybeUpdateHoverStatus(event);
}

void Tab::OnMouseEntered(const ui::MouseEvent& event) {
  MaybeUpdateHoverStatus(event);
}

void Tab::OnMouseExited(const ui::MouseEvent& event) {
  if (!mouse_hovered_) {
    return;
  }
  mouse_hovered_ = false;
  controller_->HideHover(this, TabStyle::HideHoverStyle::kGradual);
}

void Tab::OnGestureEvent(ui::GestureEvent* event) {
  controller_->UpdateHoverCard(nullptr,
                               TabSlotController::HoverCardUpdateType::kEvent);
  switch (event->type()) {
    case ui::EventType::kGestureTapDown: {
      // TAP_DOWN is only dispatched for the first touch point.
      DCHECK_EQ(1, event->details().touch_points());

      // See comment in OnMousePressed() as to why we copy the event.
      ui::GestureEvent event_in_parent(*event, views::AsViewClass<View>(this),
                                       parent());
      ui::ListSelectionModel original_selection =
          controller_->GetSelectionModel();
      if (!IsSelected()) {
        controller_->SelectTab(this, *event);
      }
      gfx::Point loc(event->location());
      views::View::ConvertPointToScreen(this, &loc);
      ui::GestureEvent cloned_event(event_in_parent, parent(),
                                    views::AsViewClass<View>(this));

      if (!closing()) {
#if BUILDFLAG(IS_WIN)
        // If the pen is down on the tab, let pen events fall through to the
        // default window handler until the pen is raised. This allows the
        // default window handler to execute drag-drop on the window when it's
        // moved by its tab, e.g., when the window has a single tab or when a
        // tab is being detached.
        const bool is_pen = event->details().primary_pointer_type() ==
                            ui::EventPointerType::kPen;
        if (is_pen) {
          views::UseDefaultHandlerForPenEventsUntilPenUp();
        }
#endif
        controller_->MaybeStartDrag(this, cloned_event, original_selection);
      }
      break;
    }

    default:
      break;
  }
  event->SetHandled();
}

gfx::Size Tab::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  return gfx::Size(GetTabSizeInfo().standard_width,
                   GetLayoutConstant(LayoutConstant::kTabHeight));
}

void Tab::PaintChildren(const views::PaintInfo& info) {
  // Clip children based on the tab's fill path.  This has no effect except when
  // the tab is too narrow to completely show even one icon, at which point this
  // serves to clip the favicon.
  ui::ClipRecorder clip_recorder(info.context());
  // The paint recording scale for tabs is consistent along the x and y axis.
  const float paint_recording_scale = info.paint_recording_scale_x();

  const SkPath clip_path = tab_style_views()->GetPath(
      TabStyle::PathType::kHighlight, paint_recording_scale, {});

  clip_recorder.ClipPathWithAntiAliasing(clip_path);
  View::PaintChildren(info);
}

void Tab::OnPaint(gfx::Canvas* canvas) {
  tab_style_views()->PaintTab(canvas);
}

void Tab::AddedToWidget() {
  paint_as_active_subscription_ =
      GetWidget()->RegisterPaintAsActiveChangedCallback(base::BindRepeating(
          &Tab::UpdateForegroundColors, base::Unretained(this)));

  // Populate the data when the tab is added to a widget because the callback
  // may not be called if the data hasn't changed yet, but we create a new tab
  // like when dragging a tab into a new window.
  if (tab_data_observer_) {
    OnTabDataChanged(TabChangeType::kAll, tab_data_observer_->tab_data());
  }
}

void Tab::RemovedFromWidget() {
  paint_as_active_subscription_ = {};
}

void Tab::OnFocus() {
  View::OnFocus();

  controller_->TabKeyboardFocusChangedTo(tab_handle_.Get());

  // Update the accessible name so that screen reader announce the correct
  // memory usage that will be shown on hover card.
  UpdateAccessibleName();
  controller_->UpdateHoverCard(this,
                               TabSlotController::HoverCardUpdateType::kFocus);
  if (features::IsTabStripDeclutterEnabled()) {
    InvalidateLayout();
  }
}

void Tab::OnBlur() {
  View::OnBlur();

  controller_->TabKeyboardFocusChangedTo(nullptr);

  if (!controller_->IsFocusInTabStrip()) {
    controller_->UpdateHoverCard(
        nullptr, TabSlotController::HoverCardUpdateType::kFocus);
  }
  if (features::IsTabStripDeclutterEnabled()) {
    InvalidateLayout();
  }
}

void Tab::OnThemeChanged() {
  TabSlotView::OnThemeChanged();
  if (auto* theme_provider = GetThemeProvider()) {
    should_fill_background_tab_color_ = theme_provider->GetDisplayProperty(
        ThemeProperties::SHOULD_FILL_BACKGROUND_TAB_COLOR);
  }
  UpdateForegroundColors();
}

TabSlotView::ViewType Tab::GetTabSlotViewType() const {
  return TabSlotView::ViewType::kTab;
}

TabSizeInfo Tab::GetTabSizeInfo() const {
  return {tab_style()->GetPinnedWidth(split().has_value()),
          tab_style()->GetMinimumActiveWidth(split().has_value()),
          tab_style()->GetMinimumInactiveWidth(),
          tab_style()->GetStandardWidth(split().has_value())};
}

void Tab::SetGroup(std::optional<tab_groups::TabGroupId> group) {
  TabSlotView::SetGroup(group);
  UpdateAccessibleName();
}

void Tab::SetSplit(std::optional<split_tabs::SplitTabId> split) {
  TabSlotView::SetSplit(split);
  UpdateAccessibleName();
}

// This function updates the accessible name for the tab whenever any of the
// parameters that influence the accessible name change. It ultimately calls
// BrowserView::GetAccessibleTabLabel to get the updated accessible name.
//
// Note: If any new parameters are added or existing ones are removed that
// affect the accessible name, ensure that the corresponding logic in
// BrowserView::GetAccessibleTabLabel is updated accordingly to maintain
// consistency.
void Tab::UpdateAccessibleName() {
  std::u16string name = controller_->GetAccessibleTabName(this);
  if (!name.empty()) {
    GetViewAccessibility().SetName(name);
  } else {
    // Under some conditions, `GetAccessibleTabName` returns an empty string.
    GetViewAccessibility().SetName(
        std::string(), ax::mojom::NameFrom::kAttributeExplicitlyEmpty);
  }
}

void Tab::OnAXNameChanged(ax::mojom::StringAttribute attribute,
                          const std::optional<std::string>& name) {
  if (GetWidget() && IsActive()) {
    GetWidget()->UpdateAccessibleNameForRootView();
  }
}

void Tab::SetClosing(bool closing) {
  closing_ = closing;
  ActiveStateChanged();

  if (closing && views::FocusRing::Get(this)) {
    // When closing, sometimes DCHECK fails because
    // cc::Layer::IsPropertyChangeAllowed() returns false. Deleting
    // the focus ring fixes this. TODO(collinbaker): investigate why
    // this happens.
    views::FocusRing::Remove(this);
  }
}

std::optional<SkColor> Tab::GetGroupColor() const {
  if (closing_ || !group().has_value() ||
      controller_->GetFocusedGroup() == group()) {
    return std::nullopt;
  }

  return controller_->GetPaintedGroupColor(
      controller_->GetGroupColorId(group().value()));
}

bool Tab::NeedsToShowThumbnail() const {
  return !IsActive();
}

bool Tab::IsValidHoverCardTarget() const {
  return !closing() && !detached() && !dragging() && GetVisible();
}

views::BubbleAnchor Tab::GetAnchor() {
  return views::BubbleAnchor(this);
}

views::BubbleBorder::Arrow Tab::GetAnchorPosition() const {
  return views::BubbleBorder::Arrow::TOP_LEFT;
}

void Tab::ActiveStateChanged() {
  UpdateTabIconAttention();
  UpdateForegroundColors();
  icon_->SetActiveState(IsActive());
  alert_indicator_button_->OnParentTabButtonColorChanged();
  alert_indicator_button_->UpdateEnabledForMuteToggle();
  DeprecatedLayoutImmediately();
}

bool Tab::ShouldEnableMuteToggle(int required_width) {
  return IsActive() || GetWidthOfLargestSelectableRegion() >= required_width;
}

void Tab::ToggleTabAudioMute() {
  controller()->ToggleTabAudioMute(this);
}

bool Tab::IsApparentlyActive() const {
  return tab_style_views()->IsApparentlyActive();
}

void Tab::AlertStateChanged() {
  if (controller_->HoverCardIsShowing(this)) {
    controller_->UpdateHoverCard(
        this, TabSlotController::HoverCardUpdateType::kTabDataChanged);
  }
  DeprecatedLayoutImmediately();
}

void Tab::SelectedStateChanged() {
  UpdateForegroundColors();
  GetViewAccessibility().SetIsSelected(IsSelected());
}

bool Tab::IsSelected() const {
  return controller_->IsTabSelected(this);
}

bool Tab::IsDiscarded() const {
  return data().is_tab_discarded;
}

bool Tab::HasThumbnail() const {
  return data().thumbnail && data().thumbnail->has_data();
}

void Tab::SetDataForTesting(tabs::TabData data) {
  OnTabDataChanged(TabChangeType::kAll, data);
}

void Tab::StepLoadingAnimation(const base::TimeDelta& elapsed_time) {
  icon_->StepLoadingAnimation(elapsed_time);

  // Update the layering if necessary.
  //
  // TODO(brettw) this design should be changed to be a push state when the tab
  // can't be painted to a layer, rather than continually polling the
  // controller about the state and reevaulating that state in the icon. This
  // is both overly aggressive and wasteful in the common case, and not
  // frequent enough in other cases since the state can be updated and the tab
  // painted before the animation is stepped.
  icon_->SetCanPaintToLayer(controller_->CanPaintThrobberToLayer());
}

void Tab::CreateFreezingVote(FreezingVoteReason reason,
                             content::WebContents* contents) {
  auto& vote = GetFreezingVote(reason);
  if (!vote.has_value()) {
    vote.emplace(contents);
  }
}

void Tab::ReleaseFreezingVote(FreezingVoteReason reason) {
  GetFreezingVote(reason).reset();
}

bool Tab::HasFreezingVote(FreezingVoteReason reason) const {
  switch (reason) {
    case FreezingVoteReason::kCollapsedGroup:
      return collapsed_freezing_vote_.has_value();
    case FreezingVoteReason::kFocusedGroup:
      return focus_mode_freezing_vote_.has_value();
  }
  NOTREACHED();
}

bool Tab::HasFreezingVote() const {
  return collapsed_freezing_vote_.has_value() ||
         focus_mode_freezing_vote_.has_value();
}

void Tab::ShowHover(TabStyle::ShowHoverStyle style) {
  if (hover_controller_) {
    if (style == TabStyle::ShowHoverStyle::kSubtle) {
      hover_controller_->SetSubtleOpacityScale(
          controller()->GetHoverOpacityForRadialHighlight());
    }
    hover_controller_->Show(style);
  }
  UpdateForegroundColors();
  DeprecatedLayoutImmediately();
}

void Tab::HideHover(TabStyle::HideHoverStyle style) {
  if (hover_controller_) {
    hover_controller_->Hide(style);
  }
  UpdateForegroundColors();
  DeprecatedLayoutImmediately();
}

double Tab::GetHoverAnimationValue() const {
  if (!hover_controller_) {
    return IsHoverAnimationActive() ? 1.0 : 0.0;
  }
  return hover_controller_->GetAnimationValue();
}

float Tab::GetHoverOpacity() const {
  const float range_start =
      static_cast<float>(tab_style()->GetStandardWidth(/*is_split*/ false));
  constexpr float kWidthForMaxHoverOpacity = 32.0f;
  const float value_in_range = static_cast<float>(width());
  const float t = std::clamp(
      (value_in_range - range_start) / (kWidthForMaxHoverOpacity - range_start),
      0.0f, 1.0f);
  return controller()->GetHoverOpacityForTab(t * t);
}

bool Tab::IsHoverAnimationActive() const {
  return IsHovering() || (hover_controller_ && hover_controller_->ShouldDraw());
}

bool Tab::IsHovering() const {
  if (mouse_hovered()) {
    return true;
  }

  return std::ranges::any_of(controller()->GetTabsInSplit(this),
                             [](const Tab* split_tab) {
                               return split_tab && split_tab->mouse_hovered();
                             });
}

float Tab::GetZValue() const {
  // This will return values so that inactive tabs can be sorted in the
  // following order:
  //
  // o Unselected tabs, in ascending hover animation value order.
  // o The single unselected tab being hovered by the mouse, if present.
  // o Selected tabs, in ascending hover animation value order.
  // o The single selected tab being hovered by the mouse, if present.
  //
  // Representing the above groupings is accomplished by adding a "weight" to
  // the current hover animation value.
  //
  // 0.0 == z-value         Unselected/non hover animating.
  // 0.0 <  z-value <= 1.0  Unselected/hover animating.
  // 2.0 <= z-value <= 3.0  Unselected/mouse hovered tab.
  // 4.0 == z-value         Selected/non hover animating.
  // 4.0 <  z-value <= 5.0  Selected/hover animating.
  // 6.0 <= z-value <= 7.0  Selected/mouse hovered tab.
  //
  // This function doesn't handle active tabs, as they are normally painted by a
  // different code path (with z-value infinity).
  float sort_value = GetHoverAnimationValue();
  if (IsSelected()) {
    sort_value += 4.f;
  }
  if (IsHovering()) {
    sort_value += 2.f;
  }

  DCHECK_GE(sort_value, 0.0f);
  DCHECK_LE(sort_value, TabStyle::kMaximumZValue);

  return sort_value;
}

// static
std::u16string Tab::GetTooltipText(const std::u16string& title,
                                   std::optional<tabs::TabAlert> alert_state) {
  if (!alert_state) {
    return title;
  }

  std::u16string result = title;
  if (!result.empty()) {
    result.append(1, '\n');
  }
  result.append(
      tabs::TabAlertController::GetTabAlertStateText(alert_state.value()));
  return result;
}

void Tab::UpdateInsets() {
  SetBorder(views::CreateEmptyBorder(tab_style_views()->GetContentsInsets()));
}

void Tab::MaybeAdjustLeftForPinnedTab(gfx::Rect* bounds,
                                      int visual_width) const {
  if (ShouldRenderAsNormalTab()) {
    return;
  }
  const int pinned_width = GetTabSizeInfo().pinned_tab_width;
  const int ideal_delta = width() - pinned_width;
  const int ideal_x = (pinned_width - visual_width) / 2;
  bounds->set_x(
      bounds->x() +
      base::ClampRound(
          (1 - static_cast<float>(ideal_delta) /
                   static_cast<float>(kPinnedTabExtraWidthToRenderAsNormal)) *
          (ideal_x - bounds->x())));
}

void Tab::UpdateIconVisibility() {
  // When a tab is less than it's minimum inactive width its implied that its
  // collapsed, but some favicon functionality can escape the bounds of the tab
  // causing artifacts. Fix this by explicitly disabling the visibility of the
  // views in the tab if the width is such that it is collapsed.
  if (width() < tab_style()->GetMinimumInactiveWidth()) {
    showing_icon_ = false;
    showing_alert_indicator_ = false;
    showing_close_button_ = false;
    return;
  }

  // TODO(pkasting): This whole function should go away, and we should simply
  // compute child visibility state in Layout().

  // Don't adjust whether we're centering the favicon or adding extra padding
  // during tab closure; let it stay however it was prior to closing the tab.
  // This prevents the icon and text from sliding left at the end of closing
  // a non-narrow tab.
  if (!closing_) {
    center_icon_ = false;
  }

  showing_icon_ = showing_alert_indicator_ = false;
  extra_alert_indicator_padding_ = false;

  if (height() < GetLayoutConstant(LayoutConstant::kTabHeight)) {
    return;
  }

  const bool has_favicon = data().should_display_favicon;
  bool has_alert_icon =
      (alert_indicator_button_ ? alert_indicator_button_->showing_alert_state()
                               : data().alert_state)
          .has_value();
  std::optional<tabs::TabAlert> current_alert_state =
      alert_indicator_button_->showing_alert_state();
  if (glic_tab_underline_view_ &&
      (current_alert_state == tabs::TabAlert::kGlicAccessing ||
       current_alert_state == tabs::TabAlert::kGlicSharing)) {
    // Tab underlines for glic multitab replace `alert_indicator_button` as the
    // UI indicator for sharing. In this case, ensure the alert indicator is
    // hidden.
    has_alert_icon = false;
  }

  is_animating_from_pinned_ &= animating();

  if (data().pinned || is_animating_from_pinned_) {
    // When the tab is pinned, we can show one of the two icons; the alert icon
    // is given priority over the favicon. The close button is never shown.
    showing_alert_indicator_ = has_alert_icon;
    showing_icon_ = has_favicon && !has_alert_icon;
    showing_close_button_ = false;

    // While animating to or from the pinned state, pinned tabs are rendered as
    // normal tabs. Force the extra padding on so the favicon doesn't jitter
    // left and then back right again as it resizes through layout regimes.
    extra_alert_indicator_padding_ = true;
    return;
  }

  int available_width = GetContentsBounds().width();

  const bool touch_ui = ui::TouchUiController::Get()->touch_ui();
  const int favicon_width = gfx::kFaviconSize;
  const int alert_icon_width =
      alert_indicator_button_->GetPreferredSize().width();
  // In case of touch optimized UI, the close button has an extra padding on the
  // left that needs to be considered.
  const int close_button_width =
      GetLayoutConstant(LayoutConstant::kTabCloseButtonSize) +
      GetLayoutConstant(LayoutConstant::kTabAfterTitlePadding);
  const bool large_enough_for_close_button =
      available_width >= (touch_ui ? kTouchMinimumContentsWidthForCloseButtons
                                   : kMinimumContentsWidthForCloseButtons);

  const bool declutter_eligible =
      features::IsTabStripDeclutterEnabled() &&
      !(IsHovering() || HasFocus() ||
        (close_button_ && close_button_->HasFocus()));

#if BUILDFLAG(IS_CHROMEOS)
  // Hide tab close button for OnTask if locked. Only applicable for non-web
  // browser scenarios.
  const bool should_show_close_button =
      !IsLockedForOnTask(controller_->GetBrowserWindowInterface());
#else
  const bool should_show_close_button = true;
#endif  // BUILDFLAG(IS_CHROMEOS)

  if (IsActive()) {
    if (!declutter_eligible) {
      // Close button is shown on active tabs regardless of the size.
      showing_close_button_ = should_show_close_button;
      if (showing_close_button_) {
        available_width -= close_button_width;
      }
    }

    showing_alert_indicator_ =
        has_alert_icon && alert_icon_width <= available_width;
    if (showing_alert_indicator_) {
      available_width -= alert_icon_width;
    }

    showing_icon_ = has_favicon && favicon_width <= available_width;
    if (showing_icon_) {
      available_width -= favicon_width;
    }

    if (declutter_eligible) {
      showing_close_button_ =
          should_show_close_button && ((!has_alert_icon && !has_favicon) ||
                                       close_button_width <= available_width);
      if (showing_close_button_) {
        available_width -= close_button_width;
      }
    }
  } else {
    showing_alert_indicator_ =
        has_alert_icon && alert_icon_width <= available_width;
    if (showing_alert_indicator_) {
      available_width -= alert_icon_width;
    }

    showing_icon_ = has_favicon && favicon_width <= available_width;
    if (showing_icon_) {
      available_width -= favicon_width;
    }

    showing_close_button_ =
        should_show_close_button && large_enough_for_close_button &&
        !(declutter_eligible &&
          available_width <=
              TabStyle::kTabStripDeclutterMaxTabWidthForCloseHide);
    if (showing_close_button_) {
      available_width -= close_button_width;
    }

    // If no other controls are visible, show the alert icon or the favicon
    // even though we don't have enough space. We'll clip the icon in
    // PaintChildren().
    if (!showing_close_button_ && !showing_alert_indicator_ && !showing_icon_) {
      showing_alert_indicator_ = has_alert_icon;
      showing_icon_ = !showing_alert_indicator_ && has_favicon;

      // See comments near top of function on why this conditional is here.
      if (!closing_ && (showing_alert_indicator_ || showing_icon_)) {
        center_icon_ = true;
      }
    }
  }

  if (!closing_ && features::IsTabStripDeclutterEnabled()) {
    const int max_width_to_center_icon =
        GetLayoutConstant(LayoutConstant::kTabPreTitlePadding) +
        GetLayoutConstant(LayoutConstant::kTabAfterTitlePadding);

    const int num_icons_showing =
        showing_icon_ + showing_alert_indicator_ + showing_close_button_;

    if (available_width < max_width_to_center_icon && num_icons_showing == 1) {
      center_icon_ = true;
    }
  }

  extra_alert_indicator_padding_ = showing_alert_indicator_ &&
                                   showing_close_button_ &&
                                   large_enough_for_close_button;
}

bool Tab::ShouldRenderAsNormalTab() const {
  return !data().pinned || (width() >= (GetTabSizeInfo().pinned_tab_width +
                                        kPinnedTabExtraWidthToRenderAsNormal));
}

void Tab::UpdateTabIconAttention() {
  // Only show the blocked attention indicator on non-active tabs. For active
  // tabs, the user sees the dialog blocking the tab, so there's no point to it
  // and it would be distracting.
  icon_->SetAttention(TabIcon::AttentionType::kBlockedWebContents,
                      !IsActive() && data_.blocked);

  icon_->SetAttention(TabIcon::AttentionType::kTabWantsAttentionStatus,
                      data_.needs_attention);
}

int Tab::GetWidthOfLargestSelectableRegion() const {
  // Assume the entire region to the left of the alert indicator and/or close
  // buttons is available for click-to-select.  If neither are visible, the
  // entire tab region is available.
  const int indicator_left = alert_indicator_button_->GetVisible()
                                 ? alert_indicator_button_->x()
                                 : width();
  const int close_button_left =
      close_button_->GetVisible() ? close_button_->x() : width();
  return std::min(indicator_left, close_button_left);
}

void Tab::UpdateForegroundColors() {
  TabStyle::TabColors colors = tab_style_views()->CalculateTargetColors();
  title_->SetEnabledColor(colors.foreground_color);
  close_button_->SetColors(colors);
  alert_indicator_button_->OnParentTabButtonColorChanged();
  // There may be no focus ring when the tab is closing.
  if (auto* focus_ring = views::FocusRing::Get(this); focus_ring) {
    focus_ring->SetColorId(colors.focus_ring_color);
    focus_ring->SetOutsetFocusRingDisabled(true);
  }
  SchedulePaint();
}

void Tab::MaybeUpdateHoverStatus(const ui::MouseEvent& event) {
  // During system-DnD-based tab dragging we sometimes receive mouse events, but
  // we shouldn't update the hover status during a drag.
  if (mouse_hovered_ || !GetWidget()->IsMouseEventsEnabled() ||
      TabDragController::IsActive()) {
    return;
  }

#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
  // Move the hit test area for hovering up so that it is not overlapped by tab
  // hover cards when they are shown.
  // TODO(crbug.com/41467565): Once Linux/CrOS widget transparency is solved,
  // remove that case.
  constexpr int kHoverCardOverlap = 6;
  if (event.location().y() >= height() - kHoverCardOverlap) {
    return;
  }
#endif

  mouse_hovered_ = true;
  controller_->ShowHover(this, TabStyle::ShowHoverStyle::kSubtle);
  if (g_show_hover_card_on_mouse_hover) {
    controller_->UpdateHoverCard(
        this, TabSlotController::HoverCardUpdateType::kHover);
  }
}

void Tab::CloseButtonPressed(const ui::Event& event) {
  if (IsActive()) {
    base::RecordAction(UserMetricsAction("CloseTab_Active"));
  } else {
    base::RecordAction(UserMetricsAction("CloseTab_Inactive"));
  }

  const tabs::TabData& tab_data = tab_data_observer_->tab_data();
  if (!alert_indicator_button_ || !alert_indicator_button_->GetVisible()) {
    base::RecordAction(UserMetricsAction("CloseTab_NoAlertIndicator"));
  } else if (auto alert_state = tab_data.alert_state; alert_state.has_value()) {
    tabs::TabAlertController::RecordCloseTabMetrics(alert_state.value());
  }

  const std::vector<Tab*>& tabs_in_split = controller()->GetTabsInSplit(this);
  if (tabs_in_split.size() > 0) {
    CHECK(tabs_in_split.size() == 2);
    base::RecordAction(UserMetricsAction(this == tabs_in_split[0]
                                             ? "CloseTab_StartTabInSplit"
                                             : "CloseTab_EndTabInSplit"));
  }

  const bool from_mouse = event.type() == ui::EventType::kMouseReleased &&
                          !(event.flags() & ui::EF_FROM_TOUCH);
  controller_->CloseTab(this, from_mouse ? CloseTabSource::kFromMouse
                                         : CloseTabSource::kFromTouch);
}

void Tab::OnTabDataChanged(TabChangeType tab_change_type,
                           const tabs::TabData& tab_data) {
  // Update the accessible name when the hovered tab's memory usage changes
  // so screen readers are in sync with the hover card. Skips updating
  // other visual UI elements (title, favicon, alert buttons) because those
  // states usually do not change when memory is updated.
  if (tab_change_type == TabChangeType::kResourceUsageOnly) {
    if (IsActive() || HasFocus()) {
      UpdateAccessibleName();
    }
    return;
  }

  if (tab_data == data_) {
    return;
  }

  tabs::TabData old(std::move(data_));
  data_ = tab_data;
  icon_->SetData(data_);
  icon_->SetCanPaintToLayer(controller_->CanPaintThrobberToLayer());
  UpdateTabIconAttention();
  if (tabs::ShouldUpdateAccessibleName(old, data_)) {
    UpdateAccessibleName();
  }
  std::optional<tabs::TabAlert> previous_alert_state = old.alert_state;
  const bool was_pinned = old.pinned;
  bool did_title_change = false;
  const std::u16string_view old_title = title_->GetText();
  std::u16string title = data_.title;
  if (data_.should_render_loading_title) {
    title = icon_->GetShowingLoadingAnimation()
                ? l10n_util::GetStringUTF16(IDS_TAB_LOADING_TITLE)
                : CoreTabHelper::GetDefaultTitle();
  } else {
    title = WindowMetadataController::FormatTitleForDisplay(title);
  }
  did_title_change = title != old_title;
  if (did_title_change) {
    title_->SetText(title);
  }

  const bool did_alert_state_change = previous_alert_state != data_.alert_state;
  if (did_alert_state_change) {
    alert_indicator_button_->TransitionToAlertState(data_.alert_state);
  }

  const bool did_pin_state_change = was_pinned != data_.pinned;
  if (did_pin_state_change) {
    showing_alert_indicator_ = false;
    // Transitioning from a pinned tab to a normal tab.
    if (was_pinned && !data_.pinned) {
      is_animating_from_pinned_ = true;
      // We must set this to true early, because we don't want to set
      // `is_animating_from_pinned_` to false if we lay out before the animation
      // begins.
      set_animating(true);
    }
  }

  if (did_alert_state_change || did_title_change) {
    TooltipTextChanged();
  }
  SetHoverCardDataFrom(data_);
  if (controller_->HoverCardIsShowing(this)) {
    controller_->UpdateHoverCard(
        this, TabSlotController::HoverCardUpdateType::kTabDataChanged);
  }

  // Since tab group naming can be based on the name of the first tab in the
  // group, update the tab group name if this tab is the first in the group.
  if (did_title_change && group().has_value()) {
    const tab_groups::TabGroupId group_id = group().value();
    TabGroup* const tab_group = controller_->GetTabGroup(group_id);
    if (tab_group->GetFirstTab() == tab_handle_.Get()) {
      controller_->OnGroupContentsChanged(group_id);
    }
  }

  DeprecatedLayoutImmediately();
  SchedulePaint();
}

// static
std::unique_ptr<TabStyleViewDelegate> Tab::CreateStyleDelegate(const Tab* tab) {
  return std::make_unique<TabStyleViewDelegateImpl>(tab);
}

std::optional<performance_manager::freezing::FreezingVote>&
Tab::GetFreezingVote(FreezingVoteReason reason) {
  switch (reason) {
    case FreezingVoteReason::kCollapsedGroup:
      return collapsed_freezing_vote_;
    case FreezingVoteReason::kFocusedGroup:
      return focus_mode_freezing_vote_;
  }
  NOTREACHED();
}

BEGIN_METADATA(Tab)
END_METADATA
