diff --git a/infra/PATCHES.md b/infra/PATCHES.md index 291213018..66407c4aa 100644 --- a/infra/PATCHES.md +++ b/infra/PATCHES.md @@ -144,12 +144,17 @@ Enable HEVC/H.265 Decoding Patch - https://github.com/StaZhu/enable-chromium-hev Allow all HEVC Video Profiles to Play Patch - https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding/blob/main/remove-main-main10-profile-limit.patch +Enable AC3 and EAC3 for HEVC Patch - https://github.com/Muril-o/electron-chromium-codecs + Show the Apps button in Bookmarks Bar by Default Patch - Made by me. Add autocomplete for chrome://flags > https://github.com/bromite/bromite/blob/master/build/patches/Offer-builtin-autocomplete-for-chrome-flags.patch Disable fetching Field Trials/Variations Patch - https://github.com/ungoogled-software/ungoogled-chromium/blob/master/patches/core/bromite/disable-fetching-field-trials.patch +Enable double click to close tab flag - https://github.com/bigfoxtail/brave-core/commit/acec5efcbaa98722611f551712f051fb343af120 + - Found by @gz83, modified by me. + Installer patches to include unstripped and RPATH binaries, with chrome_sandbox (needed for older distros), chromedriver and content-shell being added along with an icon and .desktop file for content-shell. - Created by me. Patches for mini_installer and abseil when using AVX on Windows. Credit goes to @RobRich999 diff --git a/infra/THORIUM_DEV_BOOKMARKS.html b/infra/THORIUM_DEV_BOOKMARKS.html index dedede5a4..d66f9f2ae 100644 --- a/infra/THORIUM_DEV_BOOKMARKS.html +++ b/infra/THORIUM_DEV_BOOKMARKS.html @@ -178,7 +178,6 @@

Bookmarks

pepper_file_io_host.cc - Chromium Code Search
pepper_file_io_host.h - Chromium Code Search
toolbar_view.cc - Chromium Code Search -
audio_low_latency_input_win.cc - Chromium Code Search
chrome_location_bar_model_delegate.cc - Chromium Code Search
file_system_access_safe_move_helper.cc - Chromium Code Search
file_system_access_safe_move_helper.h - Chromium Code Search diff --git a/src/chrome/browser/thorium_flag_entries.h b/src/chrome/browser/thorium_flag_entries.h index ae6f6a9a4..91ec8a9ab 100644 --- a/src/chrome/browser/thorium_flag_entries.h +++ b/src/chrome/browser/thorium_flag_entries.h @@ -65,6 +65,10 @@ "Disable Web Security", "Don't enforce the same-origin policy; meant for website testing only.", kOsDesktop, SINGLE_VALUE_TYPE(switches::kDisableWebSecurity)}, + {"double-click-close-tab", + "Double Click to Close Tab", + "Enables double clicking a tab to close it.", + kOsDesktop, SINGLE_VALUE_TYPE("double-click-close-tab")}, #if BUILDFLAG(IS_LINUX) {"password-store", diff --git a/src/chrome/browser/ui/views/tabs/tab.cc b/src/chrome/browser/ui/views/tabs/tab.cc new file mode 100644 index 000000000..85025bbe2 --- /dev/null +++ b/src/chrome/browser/ui/views/tabs/tab.cc @@ -0,0 +1,1120 @@ +// Copyright 2023 The Chromium Authors, Alex313031, and gz83 +// 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 + +#include +#include +#include +#include + +#include "base/bind.h" +#include "base/command_line.h" +#include "base/debug/alias.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/themes/theme_properties.h" +#include "chrome/browser/ui/browser.h" +#include "chrome/browser/ui/browser_element_identifiers.h" +#include "chrome/browser/ui/color/chrome_color_id.h" +#include "chrome/browser/ui/layout_constants.h" +#include "chrome/browser/ui/tab_contents/core_tab_helper.h" +#include "chrome/browser/ui/tabs/tab_style.h" +#include "chrome/browser/ui/tabs/tab_utils.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/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/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_style_views.h" +#include "chrome/browser/ui/views/touch_uma/touch_uma.h" +#include "chrome/common/chrome_features.h" +#include "chrome/grit/generated_resources.h" +#include "chrome/grit/theme_resources.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/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/non_client_view.h" + +#if defined(USE_AURA) +#include "ui/aura/env.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; + +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(TabStyle* tab_style) + : tab_style_(tab_style) {} + TabStyleHighlightPathGenerator(const TabStyleHighlightPathGenerator&) = + delete; + TabStyleHighlightPathGenerator& operator=( + const TabStyleHighlightPathGenerator&) = delete; + + // views::HighlightPathGenerator: + SkPath GetHighlightPath(const views::View* view) override { + return tab_style_->GetPath(TabStyle::PathType::kHighlight, 1.0); + } + + private: + const raw_ptr tab_style_; +}; + +} // 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 + tab_close_button_observation_{this}; + + raw_ptr tab_; + raw_ptr close_button_; + raw_ptr 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_ = 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()->GetContentsInsets())); + + title_->SetHorizontalAlignment(gfx::ALIGN_TO_HEAD); + title_->SetElideBehavior(gfx::FADE_TAIL); + title_->SetHandlesTooltips(false); + title_->SetAutoColorReadabilityEnabled(false); + title_->SetText(CoreTabHelper::GetDefaultTitle()); + title_->SetFontList(tab_style_->GetFontList()); + // |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(https://crbug.com/1139395): 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); + AddChildView(title_.get()); + + SetEventTargeter(std::make_unique(this)); + + icon_ = AddChildView(std::make_unique()); + + alert_indicator_button_ = + AddChildView(std::make_unique(this)); + + // Unretained is safe here because this class outlives its close button, and + // the controller outlives this Tab. + close_button_ = AddChildView(std::make_unique( + base::BindRepeating(&Tab::CloseButtonPressed, base::Unretained(this)), + base::BindRepeating(&TabSlotController::OnMouseEventInTab, + base::Unretained(controller_)))); + close_button_->SetHasInkDropActionOnClick(true); + + tab_close_button_observer_ = std::make_unique( + 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(tab_style_.get())); + + SetProperty(views::kElementIdentifierKey, kTabElementId); +} + +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()->GetPath( + TabStyle::PathType::kHitTest, + GetWidget()->GetCompositor()->device_scale_factor(), + /* force_active */ false, TabStyle::RenderUnits::kDips); + return true; +} + +void Tab::Layout() { + const gfx::Rect contents_rect = GetContentsBounds(); + + const bool was_showing_icon = showing_icon_; + UpdateIconVisibility(); + + int start = contents_rect.x(); + if (extra_padding_before_content_) { + constexpr int kExtraLeftPaddingToBalanceCloseButtonPadding = 4; + start += kExtraLeftPaddingToBalanceCloseButtonPadding; + } + + // 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_) { + // Height should go to the bottom of the tab for the crashed tab animation + // to pop out of the bottom. + favicon_bounds.set_y(contents_rect.y() + + Center(contents_rect.height(), gfx::kFaviconSize)); + 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); + } + // Add space for insets outside the favicon bounds. + favicon_bounds.Inset(-icon_->GetInsets()); + favicon_bounds.set_size( + gfx::Size(icon_->GetPreferredSize().width(), + contents_rect.height() - favicon_bounds.y())); + } + 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_) { + // If the ratio of the close button size to tab width exceeds the maximum. + // The close button should be as large as possible so that there is a larger + // hit-target for touch events. So the close button bounds extends to the + // edges of the tab. However, the larger hit-target should be active only + // for touch events, and the close-image should show up in the right place. + // So a border is added to the button with necessary padding. The close + // button (Tab::TabCloseButton) makes sure the padding is a hit-target only + // for touch events. + // TODO(pkasting): The padding should maybe be removed, see comments in + // TabCloseButton::TargetForRect(). + const int close_button_size = TabCloseButton::GetGlyphSize(); + const int top = + contents_rect.y() + Center(contents_rect.height(), close_button_size); + // Clamp the close button position to "centered within the tab"; this should + // only have an effect when animating in a new active tab, which might start + // out narrower than the minimum active tab width. + close_x = std::max(contents_rect.right() - close_button_size, + Center(width(), close_button_size)); + const int left = std::min(after_title_padding, close_x); + const int bottom = height() - close_button_size - top; + const int right = std::max(0, width() - (close_x + close_button_size)); + close_button_->SetButtonPadding( + gfx::Insets::TLBR(top, left, bottom, right)); + close_button_->SetBoundsRect( + {gfx::Point(close_x - left, 0), close_button_->GetPreferredSize()}); + } + 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() ? 8 : 6; + } + 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_->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->Layout(); +} + +bool Tab::OnKeyPressed(const ui::KeyEvent& event) { + if (event.key_code() == ui::VKEY_RETURN && !IsSelected()) { + controller_->SelectTab(this, event); + return true; + } + + constexpr int kModifiedFlag = +#if BUILDFLAG(IS_MAC) + ui::EF_COMMAND_DOWN; +#else + ui::EF_CONTROL_DOWN; +#endif + + if (event.type() == ui::ET_KEY_PRESSED && (event.flags() & kModifiedFlag)) { + const bool is_right = event.key_code() == ui::VKEY_RIGHT; + const bool is_left = event.key_code() == ui::VKEY_LEFT; + if (is_right || is_left) { + const bool is_rtl = base::i18n::IsRTL(); + const bool is_next = (is_right && !is_rtl) || (is_left && is_rtl); + if (event.flags() & ui::EF_SHIFT_DOWN) { + if (is_next) + controller()->MoveTabLast(this); + else + controller()->MoveTabFirst(this); + } else if (is_next) { + controller()->ShiftTabNext(this); + } else { + controller()->ShiftTabPrevious(this); + } + return true; + } + } + + return false; +} + +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) { + 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(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(this)); + + if (!closing()) + controller_->MaybeStartDrag(this, cloned_event, original_selection); + } + return true; +} + +bool Tab::OnMouseDragged(const ui::MouseEvent& event) { + controller_->ContinueDrag(this, event); + return true; +} + +void Tab::OnMouseReleased(const ui::MouseEvent& event) { + 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(END_DRAG_COMPLETE)) + 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, CLOSE_TAB_FROM_MOUSE); + } 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, CLOSE_TAB_FROM_MOUSE); + } + } else if (event.IsOnlyLeftMouseButton() && !event.IsShiftDown() && + !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); + } + // Close tab on double click, mirror of IsOnlyMiddleMouseButton + // Based on gz83's work. + } else if (base::CommandLine::ForCurrentProcess()->HasSwitch("double-click-close-tab")) { + if (event.IsOnlyLeftMouseButton() && event.GetClickCount() == 2) { + if (HitTestPoint(event.location())) { + controller_->CloseTab(this, CLOSE_TAB_FROM_MOUSE); + } else if (closing_) { + // We're animating closed and the left 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, CLOSE_TAB_FROM_MOUSE); + } +} + +void Tab::OnMouseCaptureLost() { + controller_->EndDrag(END_DRAG_CAPTURE_LOST); +} + +void Tab::OnMouseMoved(const ui::MouseEvent& event) { + tab_style_->SetHoverLocation(event.location()); + 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) { + if (mouse_hovered_ || !GetWidget()->IsMouseEventsEnabled()) + 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/978134): 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; + tab_style_->ShowHover(TabStyle::ShowHoverStyle::kSubtle); + UpdateForegroundColors(); + Layout(); + 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; + tab_style_->HideHover(TabStyle::HideHoverStyle::kGradual); + UpdateForegroundColors(); + Layout(); +} + +void Tab::OnGestureEvent(ui::GestureEvent* event) { + controller_->UpdateHoverCard(nullptr, + TabSlotController::HoverCardUpdateType::kEvent); + switch (event->type()) { + case ui::ET_GESTURE_TAP_DOWN: { + // 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(this), + parent()); + ui::ListSelectionModel original_selection; + original_selection = controller_->GetSelectionModel(); + tab_activated_with_last_tap_down_ = !IsActive(); + 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(this)); + + if (!closing()) + controller_->MaybeStartDrag(this, cloned_event, original_selection); + break; + } + + default: + break; + } + event->SetHandled(); +} + +std::u16string Tab::GetTooltipText(const gfx::Point& p) const { + // Tab hover cards replace tooltips for tabs. + return std::u16string(); +} + +void Tab::GetAccessibleNodeData(ui::AXNodeData* node_data) { + node_data->role = ax::mojom::Role::kTab; + node_data->AddState(ax::mojom::State::kMultiselectable); + node_data->AddBoolAttribute(ax::mojom::BoolAttribute::kSelected, + IsSelected()); + + std::u16string name = controller_->GetAccessibleTabName(this); + if (!name.empty()) { + node_data->SetNameChecked(name); + } else { + // Under some conditions, |GetAccessibleTabName| returns an empty string. + node_data->SetNameExplicitlyEmpty(); + } +} + +gfx::Size Tab::CalculatePreferredSize() const { + return gfx::Size(TabStyle::GetStandardWidth(), 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()->GetPath( + TabStyle::PathType::kInteriorClip, paint_recording_scale); + + clip_recorder.ClipPathWithAntiAliasing(clip_path); + View::PaintChildren(info); +} + +void Tab::OnPaint(gfx::Canvas* canvas) { + tab_style()->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 {TabStyle::GetPinnedWidth(), TabStyleViews::GetMinimumActiveWidth(), + TabStyleViews::GetMinimumInactiveWidth(), + TabStyle::GetStandardWidth()}; +} + +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); + } +} + +absl::optional Tab::GetGroupColor() const { + if (closing_ || !group().has_value()) + return absl::nullopt; + + return controller_->GetPaintedGroupColor( + controller_->GetGroupColorId(group().value())); +} + +SkColor Tab::GetAlertIndicatorColor(TabAlertState state) const { + const ui::ColorProvider* color_provider = GetColorProvider(); + if (!color_provider) + return gfx::kPlaceholderColor; + + int group; + switch (state) { + case TabAlertState::MEDIA_RECORDING: + case TabAlertState::DESKTOP_CAPTURING: + group = 0; + break; + case TabAlertState::TAB_CAPTURING: + case TabAlertState::PIP_PLAYING: + group = 1; + break; + case TabAlertState::AUDIO_PLAYING: + case TabAlertState::AUDIO_MUTING: + case TabAlertState::BLUETOOTH_CONNECTED: + case TabAlertState::BLUETOOTH_SCAN_ACTIVE: + case TabAlertState::USB_CONNECTED: + case TabAlertState::HID_CONNECTED: + case TabAlertState::SERIAL_CONNECTED: + case TabAlertState::VR_PRESENTING_IN_HEADSET: + group = 2; + break; + } + ui::ColorId color_ids[3][2][2] = { + {{kColorTabAlertMediaRecordingInactiveFrameInactive, + kColorTabAlertMediaRecordingInactiveFrameActive}, + {kColorTabAlertMediaRecordingActiveFrameInactive, + kColorTabAlertMediaRecordingActiveFrameActive}}, + {{kColorTabAlertPipPlayingInactiveFrameInactive, + kColorTabAlertPipPlayingInactiveFrameActive}, + {kColorTabAlertPipPlayingActiveFrameInactive, + kColorTabAlertPipPlayingActiveFrameActive}}, + {{kColorTabAlertAudioPlayingInactiveFrameInactive, + kColorTabAlertAudioPlayingInactiveFrameActive}, + {kColorTabAlertAudioPlayingActiveFrameInactive, + kColorTabAlertAudioPlayingActiveFrameActive}}}; + return color_provider->GetColor( + color_ids[group] + [tab_style_->GetApparentActiveState() == TabActive::kActive] + [controller_->ShouldPaintAsActiveFrame()]); +} + +bool Tab::IsActive() const { + return controller_->IsActiveTab(this); +} + +void Tab::ActiveStateChanged() { + UpdateTabIconNeedsAttentionBlocked(); + UpdateForegroundColors(); + alert_indicator_button_->OnParentTabButtonColorChanged(); + title_->SetFontList(tab_style_->GetFontList()); + Layout(); +} + +void Tab::AlertStateChanged() { + if (controller_->HoverCardIsShowingForTab(this)) + controller_->UpdateHoverCard( + this, TabSlotController::HoverCardUpdateType::kTabDataChanged); + Layout(); +} + +void Tab::SelectedStateChanged() { + UpdateForegroundColors(); +} + +bool Tab::IsSelected() const { + return controller_->IsTabSelected(this); +} + +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(); + + 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(); + + Layout(); + 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::SetFreezingVoteToken( + std::unique_ptr token) { + freezing_token_ = std::move(token); +} + +void Tab::ReleaseFreezingVoteToken() { + freezing_token_.reset(); +} + +// static +std::u16string Tab::GetTooltipText(const std::u16string& title, + absl::optional alert_state) { + if (!alert_state) + return title; + + std::u16string result = title; + if (!result.empty()) + result.append(1, '\n'); + result.append(chrome::GetTabAlertStateText(alert_state.value())); + return result; +} + +// static +absl::optional Tab::GetAlertStateToShow( + const std::vector& alert_states) { + if (alert_states.empty()) + return absl::nullopt; + + return alert_states[0]; +} + +void Tab::MaybeAdjustLeftForPinnedTab(gfx::Rect* bounds, + int visual_width) const { + if (ShouldRenderAsNormalTab()) + return; + const int pinned_width = TabStyle::GetPinnedWidth(); + const int ideal_delta = width() - pinned_width; + const int ideal_x = (pinned_width - visual_width) / 2; + // TODO(crbug.com/533570): This code is broken when the current width is less + // than the pinned width. + bounds->set_x( + bounds->x() + + base::ClampRound( + (1 - static_cast(ideal_delta) / + static_cast(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; + extra_padding_before_content_ = 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; + const bool has_alert_icon = + (alert_indicator_button_ ? alert_indicator_button_->showing_alert_state() + : GetAlertStateToShow(data().alert_state)) + .has_value(); + + 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_padding_before_content_ = true; + 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 = + close_button_->GetPreferredSize().width() - + (touch_ui ? close_button_->GetInsets().right() + : close_button_->GetInsets().width()); + const bool large_enough_for_close_button = + available_width >= (touch_ui ? kTouchMinimumContentsWidthForCloseButtons + : kMinimumContentsWidthForCloseButtons); + + if (IsActive()) { + // Close button is shown on active tabs regardless of the size. + showing_close_button_ = true; + 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_ = 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; + } + } + + // Don't update padding while the tab is closing, to avoid glitchy-looking + // behaviour when the close animation causes the tab to get very small + if (!closing_) { + // The extra padding is intended to visually balance the close button, so + // only include it when the close button is shown or will be shown on hover. + // We also check this for active tabs so that the extra padding doesn't pop + // in and out as you switch tabs. + extra_padding_before_content_ = large_enough_for_close_button; + } + + extra_alert_indicator_padding_ = showing_alert_indicator_ && + showing_close_button_ && + large_enough_for_close_button; +} + +bool Tab::ShouldRenderAsNormalTab() const { + return !data().pinned || (width() >= (TabStyle::GetPinnedWidth() + + 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_->CalculateColors(); + 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); + 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) == + TabAlertState::AUDIO_PLAYING) + base::RecordAction(UserMetricsAction("CloseTab_AudioIndicator")); + else + base::RecordAction(UserMetricsAction("CloseTab_RecordingIndicator")); + + const bool from_mouse = event.type() == ui::ET_MOUSE_RELEASED && + !(event.flags() & ui::EF_FROM_TOUCH); + controller_->CloseTab( + this, from_mouse ? CLOSE_TAB_FROM_MOUSE : CLOSE_TAB_FROM_TOUCH); + if (event.type() == ui::ET_GESTURE_TAP) + TouchUMA::RecordGestureAction(TouchUMA::kGestureTabCloseTap); +} + +BEGIN_METADATA(Tab, TabSlotView) +END_METADATA