blob: d8e3f736e6e29685aad3be6f9661e2ea6e3d0a64 [file] [log] [blame]
// 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 <array>
#include <limits>
#include <memory>
#include <utility>
#include "base/debug/alias.h"
#include "base/functional/bind.h"
#include "base/i18n/rtl.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/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "cc/paint/paint_flags.h"
#include "cc/paint/paint_recorder.h"
#include "cc/paint/paint_shader.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/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/saved_tab_groups/collaboration_messaging_tab_data.h"
#include "chrome/browser/ui/tabs/tab_style.h"
#include "chrome/browser/ui/tabs/tab_utils.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/chrome_layout_provider.h"
#include "chrome/browser/ui/views/event_utils.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/tabs/alert_indicator_button.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/tab_close_button.h"
#include "chrome/browser/ui/views/tabs/tab_hover_card_bubble_view.h"
#include "chrome/browser/ui/views/tabs/tab_icon.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_strip_layout.h"
#include "chrome/browser/ui/views/tabs/tab_strip_types.h"
#include "chrome/browser/ui/views/tabs/tab_style_views.h"
#include "chrome/common/chrome_features.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/grit/theme_resources.h"
#include "components/collaboration/public/messaging/message.h"
#include "components/grit/components_scaled_resources.h"
#include "components/tab_groups/tab_group_color.h"
#include "components/tab_groups/tab_group_visual_data.h"
#include "third_party/skia/include/core/SkPath.h"
#include "third_party/skia/include/effects/SkGradientShader.h"
#include "third_party/skia/include/pathops/SkPathOps.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.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/resource/resource_bundle.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/color_palette.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/gfx/scoped_canvas.h"
#include "ui/resources/grit/ui_resources.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/image_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/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/widget/tooltip_manager.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/frame_view.h"
#if BUILDFLAG(IS_WIN)
#include "ui/views/win/pen_event_handler_util.h"
#endif
#if defined(USE_AURA)
#include "ui/aura/env.h"
#endif
#if BUILDFLAG(ENABLE_GLIC)
#include "chrome/browser/glic/browser_ui/tab_underline_view.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;
// When the DiscardRingImprovements feature is enabled, increase the radius of
// the discard ring by this amount if there is enough space.
constexpr int kIncreasedDiscardIndicatorRadiusDp = 2;
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;
}
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_;
};
} // 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);
}
void OnViewBlurred(views::View* observed_view) override {
// Only hide hover card if not keyboard navigating.
if (!controller_->IsFocusInTabs()) {
controller_->UpdateHoverCard(
nullptr, TabSlotController::HoverCardUpdateType::kFocus);
}
}
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(TabSlotController* controller)
: controller_(controller),
title_(new views::Label()),
title_animation_(this) {
DCHECK(controller);
tab_style_views_ = TabStyleViews::CreateForTab(this);
// So we get don't get enter/exit on children and don't prematurely stop the
// hover.
SetNotifyEnterExitOnChild(true);
SetID(VIEW_ID_TAB);
// This will cause calls to GetContentsBounds to return only the rectangle
// inside the tab shape, rather than to its extents.
SetBorder(views::CreateEmptyBorder(tab_style_views()->GetContentsInsets()));
title_->SetHorizontalAlignment(gfx::ALIGN_TO_HEAD);
title_->SetElideBehavior(gfx::FADE_TAIL);
title_->SetHandlesTooltips(false);
title_->SetAutoColorReadabilityEnabled(false);
title_->SetText(CoreTabHelper::GetDefaultTitle());
title_->SetBackgroundColor(SK_ColorTRANSPARENT);
// `title_` paints on top of an opaque region (the tab background) of a
// non-opaque layer (the tabstrip's layer), which cannot currently be detected
// by the subpixel-rendering opacity check.
// TODO(crbug.com/40725997): Improve the check so that this case doen't
// need a manual suppression by detecting cases where the text is painted onto
// onto opaque parts of a not-entirely-opaque layer.
title_->SetSkipSubpixelRenderingOpacityCheck(true);
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));
#if BUILDFLAG(ENABLE_GLIC)
if (base::FeatureList::IsEnabled(features::kGlicMultitabUnderlines) &&
controller_->GetBrowser() &&
glic::GlicEnabling::IsProfileEligible(
controller_->GetBrowser()->GetProfile())) {
glic_tab_underline_view_ = AddChildView(
views::Builder<glic::TabUnderlineView>(
glic::TabUnderlineView::Factory::Create(controller->GetBrowser(),
this))
// Needed so that expectations of visibility that
// inform underline updates are correct on first show.
.SetVisible(false)
// `glic_tab_underline_view_` should never receive input events.
.SetCanProcessEventsWithinSubtree(false)
.Build());
}
#endif
// 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_))));
close_button_->SetHasInkDropActionOnClick(true);
#if BUILDFLAG(IS_CHROMEOS)
showing_close_button_ = !controller_->IsLockedForOnTask();
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()));
}
Tab::~Tab() {
// Observer must be unregistered before child views are destroyed.
tab_close_button_observer_.reset();
if (controller_->HoverCardIsShowingForTab(this)) {
controller_->UpdateHoverCard(
nullptr, TabSlotController::HoverCardUpdateType::kTabRemoved);
}
}
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();
#if BUILDFLAG(ENABLE_GLIC)
// 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);
}
#endif
// 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_->EnlargeDiscardIndicatorRadius(
width() - 2 * tab_style()->GetBottomCornerRadius() >=
gfx::kFaviconSize + 2 * kIncreasedDiscardIndicatorRadiusDp
? kIncreasedDiscardIndicatorRadiusDp
: 0);
// 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(TAB_AFTER_TITLE_PADDING);
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(TAB_CLOSE_BUTTON_SIZE);
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 =
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 (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(TAB_PRE_TITLE_PADDING);
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, contents_rect.y(), title_width,
contents_rect.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 (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;
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, static_cast<View*>(this), parent());
if (event.IsShiftDown() && IsSelectionModifierDown(event)) {
controller_->AddSelectionFromAnchorTo(this);
} else if (event.IsShiftDown()) {
controller_->ExtendSelectionTo(this);
} else if (IsSelectionModifierDown(event)) {
controller_->ToggleSelected(this);
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(),
static_cast<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/1326272).
MaybeUpdateHoverStatus(event);
}
void Tab::OnMouseEntered(const ui::MouseEvent& event) {
MaybeUpdateHoverStatus(event);
}
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::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, static_cast<View*>(this),
parent());
ui::ListSelectionModel original_selection;
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(),
static_cast<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();
}
void Tab::ShowHover(TabStyle::ShowHoverStyle style) {
tab_style_views()->ShowHover(style);
UpdateForegroundColors();
DeprecatedLayoutImmediately();
}
void Tab::HideHover(TabStyle::HideHoverStyle style) {
tab_style_views()->HideHover(style);
UpdateForegroundColors();
DeprecatedLayoutImmediately();
}
// 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()) {
GetWidget()->UpdateAccessibleNameForRootView();
}
}
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();
}
gfx::Size Tab::CalculatePreferredSize(
const views::SizeBounds& available_size) const {
return gfx::Size(GetTabSizeInfo().standard_width,
GetLayoutConstant(TAB_HEIGHT));
}
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::kInteriorClip, 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)));
}
void Tab::RemovedFromWidget() {
paint_as_active_subscription_ = {};
}
void Tab::OnFocus() {
View::OnFocus();
controller_->UpdateHoverCard(this,
TabSlotController::HoverCardUpdateType::kFocus);
}
void Tab::OnBlur() {
View::OnBlur();
if (!controller_->IsFocusInTabs()) {
controller_->UpdateHoverCard(
nullptr, TabSlotController::HoverCardUpdateType::kFocus);
}
}
void Tab::OnThemeChanged() {
TabSlotView::OnThemeChanged();
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::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()) {
return std::nullopt;
}
return controller_->GetPaintedGroupColor(
controller_->GetGroupColorId(group().value()));
}
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::ActiveStateChanged() {
UpdateTabIconNeedsAttentionBlocked();
UpdateForegroundColors();
icon_->SetActiveState(IsActive());
alert_indicator_button_->OnParentTabButtonColorChanged();
DeprecatedLayoutImmediately();
}
void Tab::AlertStateChanged() {
if (controller_->HoverCardIsShowingForTab(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();
}
// This function checks for the parameters that influence the accessible name
// change. 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.
bool Tab::ShouldUpdateAccessibleName(TabRendererData& old_data,
TabRendererData& new_data) {
bool has_old_message = old_data.collaboration_messaging &&
old_data.collaboration_messaging->HasMessage();
bool has_new_message = new_data.collaboration_messaging &&
new_data.collaboration_messaging->HasMessage();
bool collaboration_message_changed = has_old_message != has_new_message;
if (!collaboration_message_changed && has_old_message) {
// Old and new data have both have messages, so compare the contents.
collaboration_message_changed =
(old_data.collaboration_messaging->given_name() !=
new_data.collaboration_messaging->given_name()) ||
(old_data.collaboration_messaging->collaboration_event() !=
new_data.collaboration_messaging->collaboration_event());
}
return ((old_data.network_state != new_data.network_state) ||
old_data.crashed_status != new_data.crashed_status ||
old_data.alert_state != new_data.alert_state ||
old_data.should_show_discard_status !=
new_data.should_show_discard_status ||
old_data.discarded_memory_savings !=
new_data.discarded_memory_savings ||
old_data.tab_resource_usage != new_data.tab_resource_usage ||
old_data.pinned != new_data.pinned ||
old_data.title != new_data.title || collaboration_message_changed);
}
void Tab::SetData(TabRendererData data) {
DCHECK(GetWidget());
if (data_ == data) {
return;
}
TabRendererData old(std::move(data_));
data_ = std::move(data);
icon_->SetData(data_);
icon_->SetCanPaintToLayer(controller_->CanPaintThrobberToLayer());
UpdateTabIconNeedsAttentionBlocked();
if (ShouldUpdateAccessibleName(old, data_)) {
UpdateAccessibleName();
}
std::u16string title = data_.title;
if (title.empty() && !data_.should_render_empty_title) {
title = icon_->GetShowingLoadingAnimation()
? l10n_util::GetStringUTF16(IDS_TAB_LOADING_TITLE)
: CoreTabHelper::GetDefaultTitle();
} else {
title = Browser::FormatTitleForDisplay(title);
}
title_->SetText(title);
const auto new_alert_state = GetAlertStateToShow(data_.alert_state);
const auto old_alert_state = GetAlertStateToShow(old.alert_state);
if (new_alert_state != old_alert_state) {
alert_indicator_button_->TransitionToAlertState(new_alert_state);
}
if (old.pinned != data_.pinned) {
showing_alert_indicator_ = false;
}
if (!data_.pinned && old.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 (new_alert_state != old_alert_state || data_.title != old.title) {
TooltipTextChanged();
}
DeprecatedLayoutImmediately();
SchedulePaint();
}
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::SetTabNeedsAttention(bool attention) {
icon_->SetAttention(TabIcon::AttentionType::kTabWantsAttentionStatus,
attention);
SchedulePaint();
}
void Tab::CreateFreezingVote(content::WebContents* contents) {
if (!freezing_vote_.has_value()) {
freezing_vote_.emplace(contents);
}
}
void Tab::ReleaseFreezingVote() {
freezing_vote_.reset();
}
// 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;
}
// static
std::optional<tabs::TabAlert> Tab::GetAlertStateToShow(
const std::vector<tabs::TabAlert>& alert_states) {
if (alert_states.empty()) {
return std::nullopt;
}
return alert_states[0];
}
void Tab::SetShouldShowDiscardIndicator(bool enabled) {
icon_->SetShouldShowDiscardIndicator(enabled);
}
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() {
// 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(TAB_HEIGHT)) {
return;
}
const bool has_favicon = data().show_icon;
bool has_alert_icon =
(alert_indicator_button_ ? alert_indicator_button_->showing_alert_state()
: GetAlertStateToShow(data().alert_state))
.has_value();
#if BUILDFLAG(ENABLE_GLIC)
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;
}
#endif
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 buton 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(TAB_CLOSE_BUTTON_SIZE) +
GetLayoutConstant(TAB_AFTER_TITLE_PADDING);
const bool large_enough_for_close_button =
available_width >= (touch_ui ? kTouchMinimumContentsWidthForCloseButtons
: kMinimumContentsWidthForCloseButtons);
if (IsActive()) {
#if BUILDFLAG(IS_CHROMEOS)
// Hide tab close button for OnTask if locked. Only applicable for non-web
// browser scenarios.
showing_close_button_ = !controller_->IsLockedForOnTask();
#else
// Close button is shown on active tabs regardless of the size.
showing_close_button_ = true;
#endif // BUILDFLAG(IS_CHROMEOS)
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;
}
} 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_ =
#if BUILDFLAG(IS_CHROMEOS)
!controller_->IsLockedForOnTask() &&
#endif
large_enough_for_close_button;
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_) {
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::UpdateTabIconNeedsAttentionBlocked() {
// 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.
if (IsActive()) {
icon_->SetAttention(TabIcon::AttentionType::kBlockedWebContents, false);
} else {
icon_->SetAttention(TabIcon::AttentionType::kBlockedWebContents,
data_.blocked);
}
}
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::CloseButtonPressed(const ui::Event& event) {
if (!alert_indicator_button_ || !alert_indicator_button_->GetVisible()) {
base::RecordAction(UserMetricsAction("CloseTab_NoAlertIndicator"));
} else if (GetAlertStateToShow(data_.alert_state) ==
tabs::TabAlert::kAudioPlaying) {
base::RecordAction(UserMetricsAction("CloseTab_AudioIndicator"));
} else {
base::RecordAction(UserMetricsAction("CloseTab_RecordingIndicator"));
}
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);
}
BEGIN_METADATA(Tab)
END_METADATA