blob: 37966983e1a06f3cb1197db8984e714198af8693 [file]
// Copyright 2025 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/vertical/vertical_tab_view.h"
#include <optional>
#include <string>
#include "base/functional/callback_helpers.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/user_metrics.h"
#include "base/metrics/user_metrics_action.h"
#include "base/notimplemented.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/glic/browser_ui/tab_underline_view.h"
#include "chrome/browser/glic/browser_ui/tab_underline_view_controller_impl.h"
#include "chrome/browser/glic/public/glic_enabling.h"
#include "chrome/browser/themes/theme_properties.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_element_identifiers.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.h"
#include "chrome/browser/ui/tabs/alert/tab_alert_controller.h"
#include "chrome/browser/ui/tabs/tab_data.h"
#include "chrome/browser/ui/tabs/tab_muted_utils.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/tabs/tab_style.h"
#include "chrome/browser/ui/tabs/vertical_tab_strip_state_controller.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/frame/themed_background.h"
#include "chrome/browser/ui/views/frame/vertical_tab_strip_region_view.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/vertical/tab_collection_node.h"
#include "chrome/browser/ui/views/tabs/vertical/vertical_split_tab_view.h"
#include "chrome/browser/ui/views/tabs/vertical/vertical_tab_drag_handler.h"
#include "chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_controller.h"
#include "chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_view.h"
#include "chrome/common/buildflags.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/theme_resources.h"
#include "components/contextual_tasks/public/features.h"
#include "components/tabs/public/tab_interface.h"
#include "third_party/skia/include/core/SkPathBuilder.h"
#include "third_party/skia/include/core/SkRRect.h"
#include "ui/accessibility/ax_enums.mojom-shared.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/theme_provider.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/gfx/favicon_size.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/gfx/scoped_canvas.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/delegating_layout_manager.h"
#include "ui/views/layout/layout_types.h"
#include "ui/views/layout/proposed_layout.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_utils.h"
namespace {
constexpr int kIconDesignWidth = 16;
constexpr int kTitleMinWidth = 10;
constexpr int kHorizontalInset = 7;
constexpr int kDefaultPadding = 4;
constexpr int kFocusRingInset = 0.0f;
class VerticalTabHighlightPathGenerator : public views::HighlightPathGenerator {
public:
explicit VerticalTabHighlightPathGenerator(VerticalTabView* tab_view)
: tab_view_(tab_view) {}
// views::HighlightPathGenerator:
SkPath GetHighlightPath(const views::View* view) override {
return tab_view_->GetPath();
}
private:
raw_ptr<VerticalTabView> tab_view_;
};
class VerticalTabTitle : public views::Label {
METADATA_HEADER(VerticalTabTitle, views::Label)
public:
VerticalTabTitle() {
SetHorizontalAlignment(gfx::ALIGN_TO_HEAD);
SetElideBehavior(gfx::FADE_TAIL);
SetHandlesTooltips(false);
SetBackgroundColor(SK_ColorTRANSPARENT);
SetProperty(views::kElementIdentifierKey, kVerticalTabTitleElementId);
}
};
BEGIN_METADATA(VerticalTabTitle)
END_METADATA
bool IsSelectionModifierDown(const ui::MouseEvent& event) {
#if BUILDFLAG(IS_MAC)
return event.IsCommandDown();
#else
return event.IsControlDown();
#endif
}
TabStripUserGestureDetails GetGestureDetail(const ui::Event& event) {
TabStripUserGestureDetails gesture_detail(
TabStripUserGestureDetails::GestureType::kOther, event.time_stamp());
TabStripUserGestureDetails::GestureType type =
TabStripUserGestureDetails::GestureType::kOther;
if (event.type() == ui::EventType::kMousePressed) {
type = TabStripUserGestureDetails::GestureType::kMouse;
} else if (event.type() == ui::EventType::kGestureTapDown) {
type = TabStripUserGestureDetails::GestureType::kTouch;
}
gesture_detail.type = type;
return gesture_detail;
}
} // namespace
VerticalTabView::VerticalTabView(TabCollectionNode* collection_node)
: HoverCardAnchorTarget(this),
collection_node_(collection_node),
tab_style_(TabStyle::Get()),
icon_(AddChildView(std::make_unique<TabIcon>())),
title_(AddChildView(std::make_unique<VerticalTabTitle>())),
alert_indicator_(
AddChildView(std::make_unique<AlertIndicatorButton>(this))),
close_button_(AddChildView(std::make_unique<TabCloseButton>(
base::BindRepeating(&VerticalTabView::CloseButtonPressed,
base::Unretained(this)),
// TODO(crbug.com/467733947): Hook up metrics logging callback.
base::DoNothingAs<void(views::View*, const ui::MouseEvent&)>()))),
hover_controller_(gfx::Animation::ShouldRenderRichAnimation()
? std::make_unique<GlowHoverController>(this)
: nullptr) {
tabs::TabInterface* tab = const_cast<tabs::TabInterface*>(GetTabInterface());
BrowserWindowInterface* browser_window = tab->GetBrowserWindowInterface();
if (browser_window &&
((base::FeatureList::IsEnabled(features::kGlicMultitabUnderlines) &&
glic::GlicEnabling::IsProfileEligible(browser_window->GetProfile())) ||
base::FeatureList::IsEnabled(contextual_tasks::kContextualTasks))) {
glic_tab_underline_view_ = AddChildView(
views::Builder<glic::TabUnderlineView>(
glic::TabUnderlineView::Factory::Create(
std::make_unique<glic::TabUnderlineViewControllerImpl>(),
browser_window->GetBrowserForMigrationOnly(), tab->GetHandle()))
.Build());
glic_tab_underline_view_->SetOrientation(
glic::TabUnderlineView::Orientation::kVertical);
glic_tab_underline_view_->SetProperty(views::kViewIgnoredByLayoutKey, true);
glic_tab_underline_view_->SetBoundsRect(
gfx::Rect(0, 0, 2 * glic::TabUnderlineView::kEffectThickness,
GetLayoutConstant(LayoutConstant::kVerticalTabHeight)));
}
// Ordered vector of children to be rendered in the tab.
tab_children_configs_ = {
TabChildConfig(close_button_, kIconDesignWidth, kDefaultPadding,
/*align_leading=*/false,
/*expand=*/false),
TabChildConfig(alert_indicator_, kIconDesignWidth, kDefaultPadding,
/*align_leading=*/false,
/*expand=*/false, /*decorate_on_collapse=*/true),
TabChildConfig(icon_, kIconDesignWidth, kHorizontalInset,
/*align_leading=*/true,
/*expand=*/false),
TabChildConfig(title_, kTitleMinWidth, kDefaultPadding,
/*align_leading=*/true,
/*expand=*/true)};
SetProperty(views::kElementIdentifierKey, kTabElementId);
SetLayoutManager(std::make_unique<views::DelegatingLayoutManager>(this));
SetEventTargeter(std::make_unique<views::ViewTargeter>(this));
// So we get don't get enter/exit on children and don't prematurely stop the
// hover.
SetNotifyEnterExitOnChild(true);
// Add accessibility and focus ring
SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
views::FocusRing::Install(this);
auto* focus_ring = views::FocusRing::Get(this);
focus_ring->SetHaloInset(kFocusRingInset);
focus_ring->SetOutsetFocusRingDisabled(true);
views::HighlightPathGenerator::Install(
this, std::make_unique<VerticalTabHighlightPathGenerator>(this));
GetViewAccessibility().SetRole(ax::mojom::Role::kTab);
GetViewAccessibility().SetName(
std::string(), ax::mojom::NameFrom::kAttributeExplicitlyEmpty);
ax_name_changed_subscription_ =
GetViewAccessibility().AddStringAttributeChangedCallback(
ax::mojom::StringAttribute::kName,
base::BindRepeating(&VerticalTabView::OnAXNameChanged,
base::Unretained(this)));
node_destroyed_subscription_ =
collection_node_->RegisterWillDestroyCallback(base::BindOnce(
&VerticalTabView::ResetCollectionNode, base::Unretained(this)));
data_changed_subscription_ =
collection_node_->RegisterDataChangedCallback(base::BindRepeating(
&VerticalTabView::OnDataChanged, base::Unretained(this)));
CHECK(collection_node_->GetController());
auto* state_controller =
collection_node_->GetController()->GetStateController();
CHECK(state_controller);
collapsed_state_changed_subscription_ =
state_controller->RegisterOnCollapseChanged(base::BindRepeating(
&VerticalTabView::OnCollapsedStateChanged, base::Unretained(this)));
collapsed_ = state_controller->IsCollapsed();
set_context_menu_controller(this);
}
VerticalTabView::~VerticalTabView() = default;
void VerticalTabView::StepLoadingAnimation(
const base::TimeDelta& elapsed_time) {
// TODO(crbug.com/467710547): Paint favicon to a layer when tab strip isn't
// animating or when dragging isn't in progress or in full screen mode.
icon_->StepLoadingAnimation(elapsed_time);
}
void VerticalTabView::CreateFreezingVote() {
if (!freezing_vote_.has_value()) {
freezing_vote_.emplace(GetTabInterface()->GetContents());
}
}
void VerticalTabView::ReleaseFreezingVote() {
freezing_vote_.reset();
}
void VerticalTabView::UpdateHovered(bool hovered) {
if (hovered_ == hovered) {
return;
}
hovered_ = hovered;
if (hover_controller_ && !split_) {
if (hovered_) {
hover_controller_->SetSubtleOpacityScale(radial_highlight_opacity_);
hover_controller_->Show(TabStyle::ShowHoverStyle::kSubtle);
} else {
hover_controller_->Hide(TabStyle::HideHoverStyle::kGradual);
}
}
UpdateColors();
UpdateThemeColors();
InvalidateLayout();
}
bool VerticalTabView::IsHoverAnimationActive() const {
if (split_) {
auto* split_view = views::AsViewClass<VerticalSplitTabView>(parent());
// Ask the parent if its hover animation is running.
return split_view &&
(hovered_ || (split_view->hover_controller() &&
split_view->hover_controller()->ShouldDraw()));
}
return hovered_ || (hover_controller_ && hover_controller_->ShouldDraw());
}
std::optional<SkColor> VerticalTabView::GetBackgroundColor() {
if (active_ || IsHoverAnimationActive() ||
GetThemeProvider()->GetDisplayProperty(
ThemeProperties::SHOULD_FILL_BACKGROUND_TAB_COLOR)) {
return tab_style_->GetCurrentTabBackgroundColor(
GetSelectionState(), IsHoverAnimationActive(), GetHoverAnimationValue(),
IsFrameActive(), GetColorProvider());
}
return std::nullopt;
}
SkPath VerticalTabView::GetPath() const {
const SkScalar corner_radius = SkIntToScalar(
GetLayoutConstant(LayoutConstant::kVerticalTabCornerRadius) +
(split_ ? GetInsets().height() : 0));
return SkPath::RRect(SkRRect::MakeRectXY(gfx::RectToSkRect(GetLocalBounds()),
corner_radius, corner_radius));
}
void VerticalTabView::Layout(PassKey) {
LayoutSuperclass<views::View>(this);
alert_indicator_->UpdateAlertIndicatorAnimation();
}
bool VerticalTabView::OnKeyPressed(const ui::KeyEvent& event) {
CHECK(collection_node_);
if (event.key_code() == ui::VKEY_RETURN && !selected_) {
collection_node_->GetController()->SelectTab(GetTabInterface(),
GetGestureDetail(event));
return true;
}
std::optional<event_utils::ReorderDirection> reorder_direction =
event_utils::GetReorderCommandForKeyboardEvent(
event, views::LayoutOrientation::kVertical);
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) {
collection_node_->GetController()->MoveTabFirst(GetTabInterface());
} else {
collection_node_->GetController()->ShiftTabPrevious(GetTabInterface());
}
break;
}
case event_utils::ReorderDirection::kNext: {
if (move_to_end) {
collection_node_->GetController()->MoveTabLast(GetTabInterface());
} else {
collection_node_->GetController()->ShiftTabNext(GetTabInterface());
}
break;
}
}
return true;
}
bool VerticalTabView::OnKeyReleased(const ui::KeyEvent& event) {
CHECK(collection_node_);
if (event.key_code() == ui::VKEY_SPACE && !selected_) {
collection_node_->GetController()->SelectTab(GetTabInterface(),
GetGestureDetail(event));
return true;
}
return false;
}
bool VerticalTabView::OnMousePressed(const ui::MouseEvent& event) {
CHECK(collection_node_);
auto* controller = collection_node_->GetController();
shift_pressed_on_mouse_down_ = event.IsShiftDown();
RecordMousePressedInTab();
UpdateHoverCard(nullptr, TabSlotController::HoverCardUpdateType::kEvent);
if (event.IsOnlyLeftMouseButton() ||
(event.IsOnlyRightMouseButton() && event.flags() & ui::EF_FROM_TOUCH)) {
if (event.IsShiftDown() && IsSelectionModifierDown(event)) {
controller->AddSelectionFromAnchorTo(GetTabInterface());
} else if (event.IsShiftDown()) {
controller->ExtendSelectionTo(GetTabInterface());
} else if (IsSelectionModifierDown(event)) {
controller->ToggleSelected(GetTabInterface());
if (!selected_) {
return false;
}
} else if (!selected_) {
controller->SelectTab(GetTabInterface(), GetGestureDetail(event));
base::RecordAction(base::UserMetricsAction("SwitchTab_Click"));
}
// Potentially start the drag for the mouse press.
// Follow-up mouse-movement events will update the drag controller and
// eventually kick off the drag-loop.
controller->GetDragHandler().InitializeDrag(*collection_node_, event);
}
return true;
}
void VerticalTabView::OnMouseReleased(const ui::MouseEvent& event) {
CHECK(collection_node_);
auto* controller = collection_node_->GetController();
base::WeakPtr<VerticalTabView> self = weak_ptr_factory_.GetWeakPtr();
if (event.IsOnlyMiddleMouseButton()) {
if (HitTestPoint(event.location())) {
controller->CloseTab(GetTabInterface());
}
} else if (event.IsOnlyLeftMouseButton() &&
!(event.IsShiftDown() || shift_pressed_on_mouse_down_) &&
!IsSelectionModifierDown(event)) {
controller->SelectTab(GetTabInterface(), GetGestureDetail(event));
}
// Cancel the initialized drag (noop if not started). This is considered
// a cancel because the drag handler assumes mouse capture when the drag
// loop starts.
controller->GetDragHandler().EndDrag(EndDragReason::kCancel);
if (!self) {
return;
}
shift_pressed_on_mouse_down_ = false;
}
void VerticalTabView::OnMouseMoved(const ui::MouseEvent& event) {
// Hover state is handled by the parent if it is split.
if (split_) {
return;
}
// Linux enter/leave events are sometimes flaky, so we don't want to "miss"
// an enter event and fail to hover the tab.
UpdateHovered(true);
}
void VerticalTabView::OnMouseEntered(const ui::MouseEvent& event) {
CHECK(collection_node_);
UpdateHoverCard(this, TabSlotController::HoverCardUpdateType::kHover);
// Hover state is handled by the parent if it is split.
if (split_) {
return;
}
UpdateHovered(true);
}
void VerticalTabView::OnMouseExited(const ui::MouseEvent& event) {
CHECK(collection_node_);
// Hover state is handled by the parent if it is split.
if (split_) {
return;
}
UpdateHovered(false);
}
bool VerticalTabView::OnMouseDragged(const ui::MouseEvent& event) {
// Protect against key presses when the tab is animating out. Drag events may
// call this function after the node has been deleted.
if (!collection_node_) {
return false;
}
auto* controller = collection_node_->GetController();
CHECK(controller);
return controller->GetDragHandler().ContinueDrag(*this, event);
}
void VerticalTabView::OnGestureEvent(ui::GestureEvent* event) {
CHECK(collection_node_);
UpdateHoverCard(nullptr, TabSlotController::HoverCardUpdateType::kEvent);
switch (event->type()) {
case ui::EventType::kGestureTapDown: {
auto* controller = collection_node_->GetController();
CHECK(controller);
// TAP_DOWN is only dispatched for the first touch point.
CHECK_EQ(1, event->details().touch_points());
if (!selected_) {
controller->SelectTab(GetTabInterface(), GetGestureDetail(*event));
}
event->SetHandled();
break;
}
default:
break;
}
}
void VerticalTabView::OnPaint(gfx::Canvas* canvas) {
// Split pinned tabs have a merged background that is rendered in
// `VerticalSplitTabView`.
if (pinned_ && split_) {
return;
}
if (active_tab_fill_id_.has_value() || inactive_tab_fill_id_.has_value()) {
PaintTabBackgroundWithImages(canvas, active_tab_fill_id_,
inactive_tab_fill_id_);
} else {
PaintTabBackgroundFill(canvas, GetSelectionState(),
IsHoverAnimationActive(), std::nullopt);
}
views::View::OnPaint(canvas);
}
void VerticalTabView::PaintTabBackgroundWithImages(
gfx::Canvas* canvas,
std::optional<int> active_tab_fill_id,
std::optional<int> inactive_tab_fill_id) const {
const TabStyle::TabSelectionState current_state = GetSelectionState();
if (current_state == TabStyle::TabSelectionState::kActive) {
PaintTabBackgroundFill(canvas, TabStyle::TabSelectionState::kActive,
/*hovered=*/false, active_tab_fill_id);
} else {
PaintTabBackgroundFill(canvas, TabStyle::TabSelectionState::kInactive,
/*hovered=*/false, inactive_tab_fill_id);
const float opacity = GetCurrentActiveOpacity();
if (opacity > 0) {
canvas->SaveLayerAlpha(base::ClampRound<uint8_t>(opacity * 0xff),
GetLocalBounds());
PaintTabBackgroundFill(canvas, TabStyle::TabSelectionState::kActive,
/*hovered=*/false, active_tab_fill_id);
canvas->Restore();
}
}
}
float VerticalTabView::GetCurrentActiveOpacity() const {
const TabStyle::TabSelectionState selection_state = GetSelectionState();
if (selection_state == TabStyle::TabSelectionState::kActive) {
return 1.0f;
}
const float base_opacity =
selection_state == TabStyle::TabSelectionState::kSelected
? tab_style()->GetSelectedTabOpacity()
: 0.0f;
if (!IsHoverAnimationActive()) {
return base_opacity;
}
return std::lerp(base_opacity, GetHoverOpacity(), GetHoverAnimationValue());
}
void VerticalTabView::PaintTabBackgroundFill(
gfx::Canvas* canvas,
TabStyle::TabSelectionState selection_state,
bool hovered,
std::optional<int> fill_id) const {
if (ShouldPaintTabBackgroundColor(selection_state, fill_id.has_value(),
hovered)) {
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setColor(tab_style_->GetCurrentTabBackgroundColor(
GetSelectionState(), IsHoverAnimationActive(), GetHoverAnimationValue(),
IsFrameActive(), GetColorProvider()));
canvas->DrawRect(GetContentsBounds(), flags);
}
if (fill_id.has_value()) {
gfx::ImageSkia* image =
GetThemeProvider()->GetImageSkiaNamed(fill_id.value());
canvas->TileImageInt(*image, 0, 0, 0, 0, width(), height());
}
}
bool VerticalTabView::ShouldPaintTabBackgroundColor(
TabStyle::TabSelectionState selection_state,
bool has_custom_background,
bool hovered) const {
if (selection_state == TabStyle::TabSelectionState::kActive) {
return true;
}
if (has_custom_background) {
return false;
}
if (hovered) {
return true;
}
return GetThemeProvider()->GetDisplayProperty(
ThemeProperties::SHOULD_FILL_BACKGROUND_TAB_COLOR);
}
void VerticalTabView::AddedToWidget() {
paint_as_active_subscription_ =
GetWidget()->RegisterPaintAsActiveChangedCallback(base::BindRepeating(
&VerticalTabView::UpdateColors, base::Unretained(this)));
OnDataChanged();
// Recompute accessible name when the structure changes.
UpdateAccessibleName();
// Recompute the hovered state as mouse events are not processed if a view
// removed from the widget and added.
if (!split_) {
UpdateHovered(IsMouseHovered());
}
}
void VerticalTabView::RemovedFromWidget() {
paint_as_active_subscription_ = {};
UpdateHovered(false);
}
void VerticalTabView::OnFocus() {
views::View::OnFocus();
if (collection_node_ && collection_node_->GetController()) {
collection_node_->GetController()->TabKeyboardFocusChangedTo(
GetTabInterface());
}
UpdateHoverCard(this, TabSlotController::HoverCardUpdateType::kFocus);
}
void VerticalTabView::OnBlur() {
views::View::OnBlur();
if (collection_node_ && collection_node_->GetController()) {
collection_node_->GetController()->TabKeyboardFocusChangedTo(nullptr);
}
}
void VerticalTabView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
SetClipPath(GetPath());
}
void VerticalTabView::OnThemeChanged() {
views::View::OnThemeChanged();
UpdateThemeColors();
UpdateColors();
}
gfx::Rect VerticalTabView::GetChildBounds(const gfx::Rect& container,
const TabChildConfig& config,
const bool center) const {
int preferred_width;
int preferred_height;
if (config.expand) {
preferred_width = container.width() - config.padding;
// The only expandable view is the views::Label. Just get the line height to
// make calculating bounds cheaper.
views::Label* label = views::AsViewClass<views::Label>(config.view);
CHECK(label);
preferred_height = label->GetLineHeight();
} else {
const gfx::Size preferred_size = config.view->GetPreferredSize();
preferred_width = preferred_size.width();
preferred_height = preferred_size.height();
}
// Some icons have larger sizes to account for decoration. Make a distinction
// between the design width and the actual width.
const int design_width =
config.expand ? container.width() - config.padding : config.min_width;
int x = container.x();
if (center) {
x += 0.5 * (container.width() - preferred_width);
} else if (config.align_leading) {
x += 0.5 * (design_width - preferred_width);
} else {
x += container.width() - 0.5 * (design_width + preferred_width);
}
const int y = container.y() + 0.5 * (container.height() - preferred_height);
return gfx::Rect(x, y, preferred_width, preferred_height);
}
absl::flat_hash_map<views::View*, bool>
VerticalTabView::CalculateChildVisibilities() const {
absl::flat_hash_map<views::View*, bool> child_visibility_map;
child_visibility_map[title_] = !pinned_;
child_visibility_map[alert_indicator_] =
alert_indicator_->showing_alert_state().has_value();
if (glic_tab_underline_view_ && (alert_indicator_->showing_alert_state() ==
tabs::TabAlert::kGlicAccessing ||
alert_indicator_->showing_alert_state() ==
tabs::TabAlert::kGlicSharing)) {
child_visibility_map[alert_indicator_] = false;
}
child_visibility_map[icon_] =
!pinned_ || !child_visibility_map[alert_indicator_];
if (pinned_) {
child_visibility_map[close_button_] = false;
} else if (collapsed_) {
child_visibility_map[close_button_] = active_ && hovered_;
} else {
child_visibility_map[close_button_] = active_ || hovered_;
}
return child_visibility_map;
}
views::ProposedLayout VerticalTabView::CalculateProposedLayout(
const views::SizeBounds& size_bounds) const {
auto child_visibility_map = CalculateChildVisibilities();
const int width = size_bounds.width().value_or(
VerticalTabStripRegionView::kUncollapsedMaxWidth);
const int height =
GetLayoutConstant(pinned_ ? LayoutConstant::kVerticalTabPinnedHeight
: LayoutConstant::kVerticalTabHeight);
views::ProposedLayout layouts;
layouts.host_size = gfx::Size(width, height);
gfx::Rect bounds_remaining = gfx::Rect(0, 0, width, height);
bounds_remaining.Inset(gfx::Insets::VH(0, kHorizontalInset));
// If the tab is collapsed but animating with a wider width then we shouldn't
// center the contents.
const bool is_centered =
(collapsed_ && width < VerticalTabStripRegionView::kCollapsedWidth) ||
pinned_;
int placed_children = 0;
for (const auto& child : tab_children_configs_) {
const bool can_render_child =
is_centered
? (placed_children == 0)
: (child.min_width + child.padding < bounds_remaining.width() ||
placed_children < 2);
if (child_visibility_map[child.view] && can_render_child) {
layouts.child_layouts.emplace_back(
child.view.get(), child_visibility_map[child.view],
GetChildBounds(bounds_remaining, child, is_centered));
if (!is_centered) {
bounds_remaining.Inset(
child.align_leading
? gfx::Insets().set_left(child.padding + child.min_width)
: gfx::Insets().set_right(child.padding + child.min_width));
}
placed_children += 1;
} else if (child.decorate_on_collapse) {
layouts.child_layouts.emplace_back(
child.view.get(), child_visibility_map[child.view],
gfx::Rect(width / 2, height / 2, 0, 0));
} else {
layouts.child_layouts.emplace_back(
child.view.get(), child_visibility_map[child.view],
gfx::Rect(bounds_remaining.x(), bounds_remaining.y(), 0, 0));
}
}
return layouts;
}
bool VerticalTabView::GetHitTestMask(SkPath* mask) const {
*mask = GetPath();
return true;
}
bool VerticalTabView::ShouldEnableMuteToggle(int required_width) {
// TODO(crbug.com/454686636): Determine if there is enough space to activate
// the tab in collapsed, pinned, or split states.
return true;
}
void VerticalTabView::ToggleTabAudioMute() {
// The TabAlertIndicator can call this function even after the node has been
// deleted, so prevent calling if collection node doesnt exist.
if (!collection_node_) {
return;
}
content::WebContents* const contents = GetTabInterface()->GetContents();
bool mute = !contents->IsAudioMuted();
base::UmaHistogramBoolean("Media.Audio.TabAudioMuted", mute);
SetTabAudioMuted(contents, mute, TabMutedReason::kAudioIndicator,
/*extension_id=*/std::string());
}
bool VerticalTabView::IsApparentlyActive() const {
if (active_) {
return true;
}
if (hovered_) {
return GetHoverOpacity() > 0.5f;
}
return selected_;
}
void VerticalTabView::AlertStateChanged() {
// TODO(crbug.com/457525548): Update hover card.
InvalidateLayout();
}
void VerticalTabView::ShowContextMenuForViewImpl(
views::View* source,
const gfx::Point& point,
ui::mojom::MenuSourceType source_type) {
if (collection_node_) {
if (auto* controller = collection_node_->GetController()) {
controller->ShowContextMenuForNode(collection_node_, source, point,
source_type);
}
}
}
bool VerticalTabView::IsActive() const {
return active_;
}
bool VerticalTabView::IsValid() const {
return collection_node_ && !IsDragging();
}
views::BubbleBorder::Arrow VerticalTabView::GetAnchorPosition() const {
if (pinned_ && !collapsed_) {
return views::BubbleBorder::Arrow::TOP_LEFT;
}
return views::BubbleBorder::Arrow::LEFT_TOP;
}
const views::View* VerticalTabView::GetAnchorView() const {
if (split_ && !collapsed_) {
return parent();
}
return HoverCardAnchorTarget::GetAnchorView();
}
void VerticalTabView::ResetCollectionNode() {
CHECK(collection_node_);
TabHoverCardController* hover_card_controller =
collection_node_->GetController()->GetHoverCardController();
if (hover_card_controller && hover_card_controller->target_tab() == this) {
hover_card_controller->UpdateHoverCard(
nullptr, TabSlotController::HoverCardUpdateType::kTabRemoved);
}
// Reset the active/selected/hovered states so the tab animates out without a
// background.
active_ = false;
selected_ = false;
// Update the callbacks for the buttons so that we dont call anything that
// needs the node.
close_button_->SetCallback(base::RepeatingClosure(base::DoNothing()));
collection_node_ = nullptr;
}
void VerticalTabView::UpdateAccessibleName() {
CHECK(collection_node_);
std::u16string name =
tabs::GetAccessibleTabLabel(GetTabInterface(), /*is_for_tab=*/true);
if (!name.empty()) {
GetViewAccessibility().SetName(name);
} else {
GetViewAccessibility().SetName(
std::string(), ax::mojom::NameFrom::kAttributeExplicitlyEmpty);
}
}
void VerticalTabView::OnAXNameChanged(ax::mojom::StringAttribute attribute,
const std::optional<std::string>& name) {
if (GetWidget() && active_) {
GetWidget()->UpdateAccessibleNameForRootView();
}
}
void VerticalTabView::OnCollapsedStateChanged(
tabs::VerticalTabStripStateController* controller) {
collapsed_ = controller->IsCollapsed();
}
void VerticalTabView::OnDataChanged() {
CHECK(collection_node_);
tabs::TabInterface* tab = const_cast<tabs::TabInterface*>(GetTabInterface());
CHECK(tab);
const TabStripModel* tab_strip_model =
tab->GetBrowserWindowInterface()->GetTabStripModel();
int index = tab_strip_model->GetIndexOfTab(tab);
CHECK(index != TabStripModel::kNoTab);
active_ = tab_strip_model->IsTabInForeground(index);
split_ = tab->IsSplit();
pinned_ = tab->IsPinned();
SetSelection(tab->IsSelected());
UpdateTabData(tab);
UpdateColors();
InvalidateLayout();
}
void VerticalTabView::SetSelection(bool selected) {
if (selected_ == selected) {
return;
}
selected_ = selected;
GetViewAccessibility().SetIsSelected(selected_);
}
void VerticalTabView::UpdateTabData(tabs::TabInterface* tab) {
tabs::TabData new_data = tabs::TabData::FromTabInterface(tab);
tabs::TabData old_data = std::move(tab_data_);
tab_data_ = std::move(new_data);
if (tabs::ShouldUpdateAccessibleName(old_data, tab_data_)) {
UpdateAccessibleName();
}
icon_->SetData(tab_data_);
icon_->SetActiveState(tab->IsActivated());
icon_->SetAttention(TabIcon::AttentionType::kBlockedWebContents,
!tab->IsActivated() && tab->IsBlocked());
icon_->SetAttention(TabIcon::AttentionType::kTabWantsAttentionStatus,
tab_data_.needs_attention);
UpdateTitle();
alert_indicator_->TransitionToAlertState(tab_data_.alert_state);
SetHoverCardDataFrom(tab_data_);
}
void VerticalTabView::UpdateTitle() {
std::u16string title = tab_data_.title;
if (tab_data_.should_render_loading_title) {
title = icon_->GetShowingLoadingAnimation()
? l10n_util::GetStringUTF16(IDS_TAB_LOADING_TITLE)
: CoreTabHelper::GetDefaultTitle();
} else {
title = Browser::FormatTitleForDisplay(title);
}
title_->SetText(title);
}
void VerticalTabView::UpdateBorder() {
if (pinned_) {
if (split_) {
// Insets for border handled by the `VerticalSplitTabView`.
SetBorder(views::CreateEmptyBorder(gfx::Insets(GetLayoutConstant(
LayoutConstant::kVerticalTabPinnedBorderThickness))));
} else {
SetBorder(views::CreateRoundedRectBorder(
GetLayoutConstant(LayoutConstant::kVerticalTabPinnedBorderThickness),
GetLayoutConstant(LayoutConstant::kVerticalTabCornerRadius),
IsFrameActive() ? kColorVerticalTabPinnedOutline
: kColorTabDividerFrameInactive));
}
} else if (GetBorder()) {
SetBorder(nullptr);
}
}
void VerticalTabView::UpdateThemeColors() {
if (!collection_node_) {
return;
}
std::optional<int> active_tab_fill_id;
if (GetThemeProvider()->HasCustomImage(IDR_THEME_TOOLBAR)) {
active_tab_fill_id = IDR_THEME_TOOLBAR;
}
const tabs::TabInterface* tab_interface = GetTabInterface();
if (!tab_interface) {
return;
}
BrowserFrameView* const browser_frame_view =
BrowserView::GetBrowserViewForBrowser(
tab_interface->GetBrowserWindowInterface())
->browser_widget()
->GetFrameView();
const std::optional<int> inactive_tab_fill_id =
browser_frame_view ? browser_frame_view->GetCustomBackgroundId(
BrowserFrameActiveState::kUseCurrent)
: std::nullopt;
active_tab_fill_id_ = active_tab_fill_id;
inactive_tab_fill_id_ = inactive_tab_fill_id;
}
void VerticalTabView::UpdateColors() {
UpdateContrastRatioValues();
TabStyle::TabColors colors = tab_style_->CalculateTargetColors(
GetSelectionState(), IsApparentlyActive(), hovered_, IsFrameActive(),
GetColorProvider());
title_->SetEnabledColor(colors.foreground_color);
close_button_->SetColors(colors);
alert_indicator_->OnParentTabButtonColorChanged();
UpdateBorder();
// TODO(crbug.com/465159185): Update focus ring colors.
SchedulePaint();
}
void VerticalTabView::UpdateContrastRatioValues() {
auto [hover_opacity_min, hover_opacity_max, radial_highlight_opacity, _] =
tab_style_->GetContrastRatioValues(IsFrameActive(), GetColorProvider());
hover_opacity_min_ = hover_opacity_min;
hover_opacity_max_ = hover_opacity_max;
radial_highlight_opacity_ = radial_highlight_opacity;
}
void VerticalTabView::CloseButtonPressed(const ui::Event& event) {
CHECK(collection_node_);
if (active_) {
base::RecordAction(base::UserMetricsAction("CloseTab_Active"));
} else {
base::RecordAction(base::UserMetricsAction("CloseTab_Inactive"));
}
CHECK(alert_indicator_);
if (!alert_indicator_->GetVisible()) {
base::RecordAction(base::UserMetricsAction("CloseTab_NoAlertIndicator"));
} else if (tab_data_.alert_state.has_value()) {
tabs::TabAlertController::RecordCloseTabMetrics(
tab_data_.alert_state.value());
}
if (split_) {
auto* split_view = views::AsViewClass<VerticalSplitTabView>(parent());
base::RecordAction(base::UserMetricsAction(this == split_view->children()[0]
? "CloseTab_StartTabInSplit"
: "CloseTab_EndTabInSplit"));
}
// Hide the interactive close button while the tab is animating out.
if (close_button_) {
close_button_->SetVisible(false);
}
collection_node_->GetController()->CloseTab(GetTabInterface());
}
void VerticalTabView::RecordMousePressedInTab() {
views::View* parent_view = parent();
while (parent_view &&
!views::IsViewClass<VerticalTabStripView>(parent_view)) {
parent_view = parent_view->parent();
}
auto* tab_strip_view = views::AsViewClass<VerticalTabStripView>(parent_view);
CHECK(tab_strip_view);
tab_strip_view->RecordMousePressedInTab();
}
double VerticalTabView::GetHoverAnimationValue() const {
if (split_) {
if (auto* split_view = views::AsViewClass<VerticalSplitTabView>(parent())) {
return split_view->GetHoverAnimationValue();
}
}
return hover_controller_ ? hover_controller_->GetAnimationValue()
: (hovered_ ? 1.0 : 0.0);
}
float VerticalTabView::GetHoverOpacity() const {
// Opacity boost varies on tab width. The interpolation is nonlinear so
// that most tabs will fall on the low end of the opacity range, but very
// narrow tabs will still stand out on the high end.
// TODO(crbug.com/457525745): Determine what the min and max widths should be.
constexpr float kWidthForMinHoverOpacity = 216.0f;
constexpr float kWidthForMaxHoverOpacity = 32.0f;
const float value_in_range = static_cast<float>(width());
const float t =
std::clamp((kWidthForMinHoverOpacity - value_in_range) /
(kWidthForMinHoverOpacity - kWidthForMaxHoverOpacity),
0.0f, 1.0f);
return gfx::Tween::FloatValueBetween(t * t, hover_opacity_min_,
hover_opacity_max_);
}
bool VerticalTabView::IsFrameActive() const {
return GetWidget() ? GetWidget()->ShouldPaintAsActive() : true;
}
TabStyle::TabSelectionState VerticalTabView::GetSelectionState() const {
return active_ ? TabStyle::TabSelectionState::kActive
: (selected_ ? TabStyle::TabSelectionState::kSelected
: TabStyle::TabSelectionState::kInactive);
}
bool VerticalTabView::IsDragging() const {
return collection_node_ && collection_node_->GetController() &&
collection_node_->GetController()->GetDragHandler().IsViewDragging(
*this);
}
const tabs::TabInterface* VerticalTabView::GetTabInterface() const {
return std::get<const tabs::TabInterface*>(collection_node_->GetNodeData());
}
void VerticalTabView::UpdateHoverCard(HoverCardAnchorTarget* target,
int hover_card_update_type) {
CHECK(collection_node_);
if (TabHoverCardController* hover_card_controller =
collection_node_->GetController()->GetHoverCardController()) {
hover_card_controller->UpdateHoverCard(
target, static_cast<TabSlotController::HoverCardUpdateType>(
hover_card_update_type));
}
}
BEGIN_METADATA(VerticalTabView)
END_METADATA