From 82b073ca75ae0c57c11678296138d34bd9aeccbd Mon Sep 17 00:00:00 2001 From: dusan-nikcevic <168220721+dusan-nikcevic@users.noreply.github.com> Date: Fri, 6 Mar 2026 21:30:52 +0100 Subject: [PATCH] helium/ui/layout: expand collapsed vertical tabs on hover --- .../ui/layout/vertical-hover-edit-lock.patch | 264 ++++++++++++++++ .../vertical-hover-overlay-callers.patch | 275 +++++++++++++++++ .../vertical-hover-overlay-setting.patch | 190 ++++++++++++ .../ui/layout/vertical-hover-overlay.patch | 227 ++++++++++++++ .../layout/vertical-hover-reliability.patch | 285 ++++++++++++++++++ patches/series | 5 + 6 files changed, 1246 insertions(+) create mode 100644 patches/helium/ui/layout/vertical-hover-edit-lock.patch create mode 100644 patches/helium/ui/layout/vertical-hover-overlay-callers.patch create mode 100644 patches/helium/ui/layout/vertical-hover-overlay-setting.patch create mode 100644 patches/helium/ui/layout/vertical-hover-overlay.patch create mode 100644 patches/helium/ui/layout/vertical-hover-reliability.patch diff --git a/patches/helium/ui/layout/vertical-hover-edit-lock.patch b/patches/helium/ui/layout/vertical-hover-edit-lock.patch new file mode 100644 index 000000000..946e13245 --- /dev/null +++ b/patches/helium/ui/layout/vertical-hover-edit-lock.patch @@ -0,0 +1,264 @@ +--- a/chrome/browser/ui/tabs/vertical_tab_strip_state_controller.h ++++ b/chrome/browser/ui/tabs/vertical_tab_strip_state_controller.h +@@ -53,6 +53,7 @@ + void SetCollapsed(bool collapsed); + bool IsHoverExpanded() const; + void SetHovered(bool hovered); ++ void SetHoverExpansionLock(bool locked); + + int GetUncollapsedWidth() const; + void SetUncollapsedWidth(int width); +@@ -78,6 +79,7 @@ + bool IsHoverExpandEnabled() const; + void OnHoverExpandEnabledPrefChanged(); + bool ShouldHoldExpandedForHover() const; ++ bool IsHoverExpansionLocked() const; + void ClearHoverHoldState(bool notify); + void StartHoverExpandTimerIfNeeded(); + void OnHoverExpandDelayElapsed(); +@@ -91,6 +93,7 @@ + VerticalTabStripState state_; + bool is_hovered_ = false; + bool held_by_hover_ = false; ++ int hover_expansion_lock_count_ = 0; + base::OneShotTimer hover_expand_timer_; + base::OneShotTimer hover_collapse_timer_; + +--- a/chrome/browser/ui/tabs/vertical_tab_strip_state_controller.cc ++++ b/chrome/browser/ui/tabs/vertical_tab_strip_state_controller.cc +@@ -135,6 +135,23 @@ + StartHoverCollapseTimerIfNeeded(); + } + ++void VerticalTabStripStateController::SetHoverExpansionLock(bool locked) { ++ if (locked) { ++ ++hover_expansion_lock_count_; ++ hover_collapse_timer_.Stop(); ++ return; ++ } ++ ++ if (hover_expansion_lock_count_ == 0) { ++ return; ++ } ++ ++ --hover_expansion_lock_count_; ++ if (!IsHoverExpansionLocked()) { ++ StartHoverCollapseTimerIfNeeded(); ++ } ++} ++ + int VerticalTabStripStateController::GetUncollapsedWidth() const { + return state_.uncollapsed_width; + } +@@ -225,6 +242,10 @@ + state_.collapsed; + } + ++bool VerticalTabStripStateController::IsHoverExpansionLocked() const { ++ return hover_expansion_lock_count_ > 0; ++} ++ + void VerticalTabStripStateController::ClearHoverHoldState(bool notify) { + hover_expand_timer_.Stop(); + hover_collapse_timer_.Stop(); +@@ -260,6 +281,7 @@ + + void VerticalTabStripStateController::StartHoverCollapseTimerIfNeeded() { + if (!held_by_hover_ || is_hovered_ || !state_.collapsed || ++ IsHoverExpansionLocked() || + hover_collapse_timer_.IsRunning()) { + return; + } +--- a/chrome/browser/ui/tabs/vertical_tab_strip_state_controller_unittest.cc ++++ b/chrome/browser/ui/tabs/vertical_tab_strip_state_controller_unittest.cc +@@ -231,6 +231,50 @@ + EXPECT_FALSE(controller()->IsHoverExpanded()); + } + ++TEST_F(VerticalTabStripStateControllerTest, ++ HoverExpansionLockSuppressesCollapse) { ++ pref_service()->SetInteger(prefs::kHeliumLayout, ++ std::to_underlying(HeliumLayoutType::kVertical)); ++ controller()->SetCollapsed(true); ++ controller()->SetHovered(true); ++ task_environment_.FastForwardBy(base::Milliseconds(300)); ++ EXPECT_TRUE(controller()->IsHoverExpanded()); ++ ++ controller()->SetHoverExpansionLock(true); ++ controller()->SetHovered(false); ++ task_environment_.FastForwardBy(base::Milliseconds(500)); ++ EXPECT_TRUE(controller()->IsHoverExpanded()); ++ ++ controller()->SetHoverExpansionLock(false); ++ task_environment_.FastForwardBy(base::Milliseconds(119)); ++ EXPECT_TRUE(controller()->IsHoverExpanded()); ++ task_environment_.FastForwardBy(base::Milliseconds(1)); ++ EXPECT_FALSE(controller()->IsHoverExpanded()); ++} ++ ++TEST_F(VerticalTabStripStateControllerTest, HoverExpansionLockRefCounted) { ++ pref_service()->SetInteger(prefs::kHeliumLayout, ++ std::to_underlying(HeliumLayoutType::kVertical)); ++ controller()->SetCollapsed(true); ++ controller()->SetHovered(true); ++ task_environment_.FastForwardBy(base::Milliseconds(300)); ++ EXPECT_TRUE(controller()->IsHoverExpanded()); ++ ++ controller()->SetHoverExpansionLock(true); ++ controller()->SetHoverExpansionLock(true); ++ controller()->SetHovered(false); ++ task_environment_.FastForwardBy(base::Milliseconds(1000)); ++ EXPECT_TRUE(controller()->IsHoverExpanded()); ++ ++ controller()->SetHoverExpansionLock(false); ++ task_environment_.FastForwardBy(base::Milliseconds(1000)); ++ EXPECT_TRUE(controller()->IsHoverExpanded()); ++ ++ controller()->SetHoverExpansionLock(false); ++ task_environment_.FastForwardBy(base::Milliseconds(120)); ++ EXPECT_FALSE(controller()->IsHoverExpanded()); ++} ++ + TEST_F(VerticalTabStripStateControllerTest, HoverExpandedDisabledByPref) { + pref_service()->SetBoolean(prefs::kHeliumVerticalHoverExpandEnabled, false); + pref_service()->SetInteger(prefs::kHeliumLayout, +--- a/chrome/browser/ui/views/tabs/vertical/vertical_tab_group_header_view.h ++++ b/chrome/browser/ui/views/tabs/vertical/vertical_tab_group_header_view.h +@@ -5,6 +5,7 @@ + #ifndef CHROME_BROWSER_UI_VIEWS_TABS_VERTICAL_VERTICAL_TAB_GROUP_HEADER_VIEW_H_ + #define CHROME_BROWSER_UI_VIEWS_TABS_VERTICAL_VERTICAL_TAB_GROUP_HEADER_VIEW_H_ + ++#include "base/callback_list.h" + #include "base/memory/raw_ptr.h" + #include "chrome/browser/ui/views/tabs/tab_group_editor_bubble_tracker.h" + #include "chrome/browser/ui/views/tabs/tab_strip_types.h" +@@ -64,6 +65,11 @@ + void OnDataChanged( + const tab_groups::TabGroupVisualData* tab_group_visual_data); + ++ base::CallbackListSubscription RegisterOnEditorBubbleOpened( ++ base::RepeatingClosure callback); ++ base::CallbackListSubscription RegisterOnEditorBubbleClosed( ++ base::RepeatingClosure callback); ++ + views::ImageView* collapse_icon_for_testing() { return collapse_icon_; } + + private: +--- a/chrome/browser/ui/views/tabs/vertical/vertical_tab_group_header_view.cc ++++ b/chrome/browser/ui/views/tabs/vertical/vertical_tab_group_header_view.cc +@@ -5,6 +5,7 @@ + #include "chrome/browser/ui/views/tabs/vertical/vertical_tab_group_header_view.h" + + #include ++#include + + #include "chrome/app/vector_icons/vector_icons.h" + #include "chrome/browser/ui/browser_element_identifiers.h" +@@ -187,5 +188,17 @@ + } + } + ++base::CallbackListSubscription ++VerticalTabGroupHeaderView::RegisterOnEditorBubbleOpened( ++ base::RepeatingClosure callback) { ++ return editor_bubble_tracker_.RegisterOnBubbleOpened(std::move(callback)); ++} ++ ++base::CallbackListSubscription ++VerticalTabGroupHeaderView::RegisterOnEditorBubbleClosed( ++ base::RepeatingClosure callback) { ++ return editor_bubble_tracker_.RegisterOnBubbleClosed(std::move(callback)); ++} ++ + BEGIN_METADATA(VerticalTabGroupHeaderView) + END_METADATA +--- a/chrome/browser/ui/views/tabs/vertical/vertical_tab_group_view.h ++++ b/chrome/browser/ui/views/tabs/vertical/vertical_tab_group_view.h +@@ -74,12 +74,15 @@ + + void ResetCollectionNode(); + void OnDataChanged(); ++ void SetHoverExpansionLock(bool locked); + void UpdateChildVisibilityForCollapseState(bool collapsed); + + raw_ptr collection_node_ = nullptr; + + base::CallbackListSubscription node_destroyed_subscription_; + base::CallbackListSubscription data_changed_subscription_; ++ base::CallbackListSubscription editor_bubble_opened_subscription_; ++ base::CallbackListSubscription editor_bubble_closed_subscription_; + + tab_groups::TabGroupVisualData tab_group_visual_data_; + const raw_ptr group_header_ = nullptr; +--- a/chrome/browser/ui/views/tabs/vertical/vertical_tab_group_view.cc ++++ b/chrome/browser/ui/views/tabs/vertical/vertical_tab_group_view.cc +@@ -6,6 +6,7 @@ + + #include + ++#include "base/functional/bind.h" + #include "chrome/browser/ui/browser_window/public/browser_window_features.h" + #include "chrome/browser/ui/browser_window/public/browser_window_interface.h" + #include "chrome/browser/ui/layout_constants.h" +@@ -23,6 +24,7 @@ + #include "components/tabs/public/tab_collection_storage.h" + #include "components/tabs/public/tab_group.h" + #include "components/tabs/public/tab_group_tab_collection.h" ++#include "components/tabs/public/tab_interface.h" + #include "ui/base/metadata/metadata_impl_macros.h" + #include "ui/color/color_provider.h" + #include "ui/gfx/color_utils.h" +@@ -83,6 +85,14 @@ + data_changed_subscription_ = + collection_node_->RegisterDataChangedCallback(base::BindRepeating( + &VerticalTabGroupView::OnDataChanged, base::Unretained(this))); ++ editor_bubble_opened_subscription_ = ++ group_header_->RegisterOnEditorBubbleOpened(base::BindRepeating( ++ &VerticalTabGroupView::SetHoverExpansionLock, ++ base::Unretained(this), true)); ++ editor_bubble_closed_subscription_ = ++ group_header_->RegisterOnEditorBubbleClosed(base::BindRepeating( ++ &VerticalTabGroupView::SetHoverExpansionLock, ++ base::Unretained(this), false)); + OnDataChanged(); + } + +@@ -205,9 +215,38 @@ + void VerticalTabGroupView::ResetCollectionNode() { + node_destroyed_subscription_ = {}; + data_changed_subscription_ = {}; ++ editor_bubble_opened_subscription_ = {}; ++ editor_bubble_closed_subscription_ = {}; + collection_node_ = nullptr; + } + ++void VerticalTabGroupView::SetHoverExpansionLock(bool locked) { ++ if (!collection_node_) { ++ return; ++ } ++ ++ const TabGroup* group = GetTabGroupFromNode(collection_node_); ++ if (!group) { ++ return; ++ } ++ tabs::TabInterface* first_tab = group->GetFirstTab(); ++ if (!first_tab) { ++ return; ++ } ++ ++ BrowserWindowInterface* browser_window = first_tab->GetBrowserWindowInterface(); ++ if (!browser_window) { ++ return; ++ } ++ ++ tabs::VerticalTabStripStateController* state_controller = ++ tabs::VerticalTabStripStateController::From(browser_window); ++ if (!state_controller) { ++ return; ++ } ++ state_controller->SetHoverExpansionLock(locked); ++} ++ + void VerticalTabGroupView::OnDataChanged() { + // If the group is in the process of being closed, then ignore updates. + if (!collection_node_) { diff --git a/patches/helium/ui/layout/vertical-hover-overlay-callers.patch b/patches/helium/ui/layout/vertical-hover-overlay-callers.patch new file mode 100644 index 000000000..39d70cbc7 --- /dev/null +++ b/patches/helium/ui/layout/vertical-hover-overlay-callers.patch @@ -0,0 +1,275 @@ +--- a/chrome/browser/ui/views/frame/layout/browser_view_layout_delegate.h ++++ b/chrome/browser/ui/views/frame/layout/browser_view_layout_delegate.h +@@ -32,6 +32,7 @@ + virtual bool ShouldDrawVerticalTabStrip() const = 0; + virtual bool ShouldDrawToolbarTabStrip() const = 0; + virtual bool IsVerticalTabStripCollapsed() const = 0; ++ virtual bool IsVerticalTabStripHoverExpanded() const = 0; + virtual bool IsVerticalTabStripRightAligned() const = 0; + virtual bool ShouldDrawWebAppFrameToolbar() const = 0; + virtual bool GetBorderlessModeEnabled() const = 0; +--- a/chrome/browser/ui/views/frame/layout/browser_view_layout_delegate_impl.cc ++++ b/chrome/browser/ui/views/frame/layout/browser_view_layout_delegate_impl.cc +@@ -58,6 +58,12 @@ + return browser_view_->IsVerticalTabStripCollapsed(); + } + ++bool BrowserViewLayoutDelegateImpl::IsVerticalTabStripHoverExpanded() const { ++ auto* controller = ++ tabs::VerticalTabStripStateController::From(browser_view_->browser()); ++ return controller && controller->IsHoverExpanded(); ++} ++ + bool BrowserViewLayoutDelegateImpl::IsVerticalTabStripRightAligned() const { + auto* controller = + tabs::VerticalTabStripStateController::From(browser_view_->browser()); +--- a/chrome/browser/ui/views/frame/layout/browser_view_layout_delegate_impl.h ++++ b/chrome/browser/ui/views/frame/layout/browser_view_layout_delegate_impl.h +@@ -25,6 +25,7 @@ + bool ShouldDrawVerticalTabStrip() const override; + bool ShouldDrawToolbarTabStrip() const override; + bool IsVerticalTabStripCollapsed() const override; ++ bool IsVerticalTabStripHoverExpanded() const override; + bool IsVerticalTabStripRightAligned() const override; + bool ShouldDrawWebAppFrameToolbar() const override; + bool GetBorderlessModeEnabled() const override; +--- a/chrome/browser/ui/views/frame/layout/browser_view_tabbed_layout_impl.cc ++++ b/chrome/browser/ui/views/frame/layout/browser_view_tabbed_layout_impl.cc +@@ -252,6 +252,8 @@ + BrowserLayoutParams params = browser_params; + bool needs_exclusion = true; + const TabStripType tab_strip_type = GetTabStripType(); ++ const bool vertical_hover_expanded = ++ delegate().IsVerticalTabStripHoverExpanded(); + const bool vertical_right_aligned = + delegate().IsVerticalTabStripRightAligned(); + +@@ -310,12 +312,20 @@ + int vertical_tab_strip_relative_top = 0; + int vertical_tab_strip_width = + views().vertical_tab_strip_region_view->GetPreferredSize().width(); ++ int vertical_tab_strip_reserved_width = vertical_tab_strip_width; + if (delegate().IsVerticalTabStripCollapsed()) { + // Collapsed tabstrip sits underneath caption buttons when present. ++ vertical_tab_strip_reserved_width = ++ VerticalTabStripRegionView::kCollapsedWidth; ++ if (!vertical_hover_expanded) { ++ vertical_tab_strip_width = vertical_tab_strip_reserved_width; ++ } + vertical_tab_strip_relative_top = + GetCollapsedVerticalTabStripRelativeTop(params); + collapsed_vertical_tab_strip_adjustment = +- vertical_tab_strip_relative_top > 0 ? vertical_tab_strip_width : 0; ++ vertical_tab_strip_relative_top > 0 ++ ? vertical_tab_strip_reserved_width ++ : 0; + } else { + // Un-collapsed tabstrip must be at least as wide as the caption + // buttons, if present. +@@ -334,7 +344,7 @@ + vertical_tab_strip_width, params.visual_client_area.height() - + vertical_tab_strip_relative_top); + params.InsetHorizontal( +- vertical_tab_strip_width, ++ vertical_tab_strip_reserved_width, + /*leading=*/!vertical_right_aligned); + } + layout.AddChild(views().vertical_tab_strip_region_view, +--- a/chrome/browser/ui/views/frame/vertical_tab_strip_region_view.cc ++++ b/chrome/browser/ui/views/frame/vertical_tab_strip_region_view.cc +@@ -71,6 +71,8 @@ + SetPaintToLayer(); + // Because corners may be transparent, this must be set to false. + layer()->SetFillsBoundsOpaquely(false); ++ // Hover expansion should react when entering/leaving any descendant view. ++ SetNotifyEnterExitOnChild(true); + + flex_layout_ = SetLayoutManager(std::make_unique()); + flex_layout_->SetOrientation(views::LayoutOrientation::kVertical) +@@ -102,10 +104,10 @@ + resize_animation_.SetSlideDuration( + gfx::Animation::RichAnimationDuration(base::Milliseconds(250))); + resize_animation_.SetTweenType(gfx::Tween::Type::EASE_IN_OUT_EMPHASIZED); +- resize_animation_.Reset(!state_controller_->IsCollapsed()); +- + target_collapse_state_ = state_controller_->GetState(); +- SetPreferredSize(gfx::Size(target_collapse_state_.collapsed ++ target_visually_collapsed_ = IsVisuallyCollapsed(); ++ resize_animation_.Reset(!target_visually_collapsed_); ++ SetPreferredSize(gfx::Size(target_visually_collapsed_ + ? kCollapsedWidth + : target_collapse_state_.uncollapsed_width, + 0)); +@@ -156,6 +158,16 @@ + bounds().height())); + } + ++void VerticalTabStripRegionView::OnMouseEntered(const ui::MouseEvent& event) { ++ TabStripRegionView::OnMouseEntered(event); ++ state_controller_->SetHovered(true); ++} ++ ++void VerticalTabStripRegionView::OnMouseExited(const ui::MouseEvent& event) { ++ TabStripRegionView::OnMouseExited(event); ++ state_controller_->SetHovered(false); ++} ++ + views::View* VerticalTabStripRegionView::GetDefaultFocusableChild() { + return top_button_container_; + } +@@ -477,14 +489,14 @@ + + void VerticalTabStripRegionView::UpdateInteriorMargin() { + const int padding = GetLayoutConstant( +- state_controller_->IsCollapsed() ++ IsVisuallyCollapsed() + ? LayoutConstant::kVerticalTabStripCollapsedPadding + : LayoutConstant::kVerticalTabStripUncollapsedPadding); + + // When collapsed and under the toolbar, the top padding has to be 0 + // in order to align with webview. + int top_padding = +- state_controller_->IsCollapsed() && has_leading_exclusion_ ++ IsVisuallyCollapsed() && has_leading_exclusion_ + ? 0 + : kRegionVerticalPadding; + +@@ -492,6 +504,11 @@ + gfx::Insets::TLBR(top_padding, 0, padding, 0)); + } + ++bool VerticalTabStripRegionView::IsVisuallyCollapsed() const { ++ return state_controller_->IsCollapsed() && ++ !state_controller_->IsHoverExpanded(); ++} ++ + void VerticalTabStripRegionView::OnCollapsedStateChanged( + tabs::VerticalTabStripStateController* state_controller) { + if (target_collapse_state_.collapsed != state_controller->IsCollapsed()) { +@@ -503,8 +520,9 @@ + UpdateCollapseState(state_controller_->GetState()); + } + ++ const bool is_visually_collapsed = IsVisuallyCollapsed(); + const int padding = GetLayoutConstant( +- state_controller_->IsCollapsed() ++ is_visually_collapsed + ? LayoutConstant::kVerticalTabStripCollapsedPadding + : LayoutConstant::kVerticalTabStripUncollapsedPadding); + +@@ -518,7 +536,22 @@ + UpdateInteriorMargin(); + + if (tab_strip_view_) { +- tab_strip_view_->SetCollapsedState(state_controller->IsCollapsed()); ++ tab_strip_view_->SetCollapsedState(is_visually_collapsed); ++ } ++ ++ if (target_visually_collapsed_ != is_visually_collapsed) { ++ target_visually_collapsed_ = is_visually_collapsed; ++ if (target_visually_collapsed_) { ++ resize_animation_.Hide(); ++ } else { ++ resize_animation_.Show(); ++ } ++ } else if (state_controller_->IsCollapsed() && ++ !resize_animation_.is_animating()) { ++ const int expanded_width = ++ std::clamp(state_controller_->GetUncollapsedWidth(), ++ kUncollapsedMinWidth, kUncollapsedMaxWidth); ++ ResizeToWidth(is_visually_collapsed ? kCollapsedWidth : expanded_width); + } + } + +--- a/chrome/browser/ui/views/frame/vertical_tab_strip_region_view.h ++++ b/chrome/browser/ui/views/frame/vertical_tab_strip_region_view.h +@@ -97,6 +97,8 @@ + // views::View: + void AddedToWidget() override; + void Layout(PassKey) override; ++ void OnMouseEntered(const ui::MouseEvent& event) override; ++ void OnMouseExited(const ui::MouseEvent& event) override; + views::View* GetDefaultFocusableChild() override; + + // TabStripRegionView +@@ -143,6 +145,7 @@ + void ClearTabStripView(views::View* view); + + void UpdateInteriorMargin(); ++ bool IsVisuallyCollapsed() const; + + void OnCollapsedStateChanged( + tabs::VerticalTabStripStateController* state_controller); +@@ -159,7 +162,6 @@ + bool tab_strip_editable_for_testing_ = true; + + raw_ptr top_button_container_ = nullptr; +- raw_ptr top_button_separator_ = nullptr; + raw_ptr tab_strip_view_ = nullptr; + raw_ptr bottom_button_container_ = nullptr; + raw_ptr gemini_button_ = nullptr; +@@ -189,6 +191,7 @@ + // Additionally, the collapsed value may differ from the state controller, in + // which case this is the source of truth only if we are in a drag operation. + tabs::VerticalTabStripState target_collapse_state_; ++ bool target_visually_collapsed_ = false; + + // Animation for collapsing (GetCurrentValue() -> 0) and expanding + // (GetCurrentValue() -> 1). +--- a/chrome/browser/ui/views/frame/vertical_tab_strip_region_view_browsertest.cc ++++ b/chrome/browser/ui/views/frame/vertical_tab_strip_region_view_browsertest.cc +@@ -393,6 +393,42 @@ + WaitForBoundsToMatchPreferredWidth(); + } + ++IN_PROC_BROWSER_TEST_F(VerticalTabStripRegionViewTest, ++ HoverExpandedDoesNotResizeContents) { ++ state_controller()->SetCollapsed(true); ++ ASSERT_TRUE(base::test::RunUntil( ++ [&]() { return !region_view()->is_animating_for_testing(); })); ++ WaitForBoundsToMatchPreferredWidth(); ++ ++ const auto initial_contents_bounds = ++ browser()->GetBrowserView().GetContentsContainerForTest()->bounds(); ++ ++ state_controller()->SetHovered(true); ++ ASSERT_TRUE(base::test::RunUntil( ++ [&]() { return state_controller()->IsHoverExpanded(); })); ++ ASSERT_TRUE(base::test::RunUntil([&]() { ++ return region_view()->GetPreferredSize().width() > ++ VerticalTabStripRegionView::kCollapsedWidth; ++ })); ++ WaitForBoundsToMatchPreferredWidth(); ++ ++ EXPECT_TRUE(state_controller()->IsCollapsed()); ++ EXPECT_EQ(initial_contents_bounds, ++ browser()->GetBrowserView().GetContentsContainerForTest()->bounds()); ++ ++ state_controller()->SetHovered(false); ++ ASSERT_TRUE(base::test::RunUntil( ++ [&]() { return !state_controller()->IsHoverExpanded(); })); ++ ASSERT_TRUE(base::test::RunUntil([&]() { ++ return region_view()->GetPreferredSize().width() == ++ VerticalTabStripRegionView::kCollapsedWidth; ++ })); ++ WaitForBoundsToMatchPreferredWidth(); ++ ++ EXPECT_EQ(initial_contents_bounds, ++ browser()->GetBrowserView().GetContentsContainerForTest()->bounds()); ++} ++ + // Verify that the pinned tabs container will never be larger than the unpinned + // tabs area. + IN_PROC_BROWSER_TEST_F(VerticalTabStripRegionViewTest, +--- a/chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_controller.cc ++++ b/chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_controller.cc +@@ -220,7 +220,8 @@ + bool VerticalTabStripController::IsCollapsed() const { + tabs::VerticalTabStripStateController* state_controller = + tabs::VerticalTabStripStateController::From(browser_view_->browser()); +- return state_controller && state_controller->IsCollapsed(); ++ return state_controller && state_controller->IsCollapsed() && ++ !state_controller->IsHoverExpanded(); + } + + bool VerticalTabStripController::IsContextMenuCommandChecked( diff --git a/patches/helium/ui/layout/vertical-hover-overlay-setting.patch b/patches/helium/ui/layout/vertical-hover-overlay-setting.patch new file mode 100644 index 000000000..260f80e4a --- /dev/null +++ b/patches/helium/ui/layout/vertical-hover-overlay-setting.patch @@ -0,0 +1,190 @@ +--- a/chrome/common/pref_names.h ++++ b/chrome/common/pref_names.h +@@ -1328,6 +1328,10 @@ + inline constexpr char kHeliumVerticalUncollapsedWidth[] = + "helium.browser.vertical_uncollapsed_width"; + ++// A boolean pref set to true if collapsed vertical tabs expand on hover. ++inline constexpr char kHeliumVerticalHoverExpandEnabled[] = ++ "helium.browser.vertical_hover_expand_enabled"; ++ + // A boolean pref set to true if a Home button to open the Home pages should be + // visible on the toolbar. + inline constexpr char kShowHomeButton[] = "browser.show_home_button"; +--- a/chrome/browser/ui/tabs/tab_strip_prefs.cc ++++ b/chrome/browser/ui/tabs/tab_strip_prefs.cc +@@ -44,6 +44,8 @@ + registry->RegisterIntegerPref( + prefs::kHeliumVerticalUncollapsedWidth, + kVerticalTabStripDefaultUncollapsedWidth); ++ registry->RegisterBooleanPref(prefs::kHeliumVerticalHoverExpandEnabled, ++ true); + } + + TabSearchPosition GetTabSearchPosition(const Profile* profile) { +--- a/chrome/browser/extensions/api/settings_private/prefs_util.cc ++++ b/chrome/browser/extensions/api/settings_private/prefs_util.cc +@@ -185,6 +185,8 @@ + settings_api::PrefType::kNumber; + (*s_allowlist)[::prefs::kHeliumVerticalRightAligned] = + settings_api::PrefType::kBoolean; ++ (*s_allowlist)[::prefs::kHeliumVerticalHoverExpandEnabled] = ++ settings_api::PrefType::kBoolean; + + // Miscellaneous + (*s_allowlist)[::embedder_support::kAlternateErrorPagesEnabled] = +--- a/chrome/browser/ui/tabs/vertical_tab_strip_state_controller.h ++++ b/chrome/browser/ui/tabs/vertical_tab_strip_state_controller.h +@@ -74,6 +74,8 @@ + // Update the Collapse Button's Action Item (kActionToggleCollapseVertical) + // based on the Vertical Tab Strip's Collapse State. + void UpdateCollapseActionItem(); ++ bool IsHoverExpandEnabled() const; ++ void OnHoverExpandEnabledPrefChanged(); + void StartHoverExpandTimerIfNeeded(); + void OnHoverExpandDelayElapsed(); + +--- a/chrome/browser/ui/tabs/vertical_tab_strip_state_controller.cc ++++ b/chrome/browser/ui/tabs/vertical_tab_strip_state_controller.cc +@@ -48,6 +48,11 @@ + {prefs::kHeliumLayout, prefs::kHeliumVerticalRightAligned}, + base::BindRepeating(&VerticalTabStripStateController::NotifyStateChanged, + base::Unretained(this))); ++ pref_change_registrar_.Add( ++ prefs::kHeliumVerticalHoverExpandEnabled, ++ base::BindRepeating( ++ &VerticalTabStripStateController::OnHoverExpandEnabledPrefChanged, ++ base::Unretained(this))); + + state_.collapsed = + pref_service_->GetBoolean(prefs::kHeliumVerticalCollapsed); +@@ -106,7 +111,7 @@ + } + + bool VerticalTabStripStateController::IsHoverExpanded() const { +- return state_.collapsed && held_by_hover_; ++ return state_.collapsed && held_by_hover_ && IsHoverExpandEnabled(); + } + + void VerticalTabStripStateController::SetHovered(bool hovered) { +@@ -116,6 +121,9 @@ + is_hovered_ = hovered; + + if (is_hovered_) { ++ if (!IsHoverExpandEnabled()) { ++ return; ++ } + StartHoverExpandTimerIfNeeded(); + return; + } +@@ -194,8 +202,25 @@ + } + } + ++bool VerticalTabStripStateController::IsHoverExpandEnabled() const { ++ return pref_service_->GetBoolean(prefs::kHeliumVerticalHoverExpandEnabled); ++} ++ ++void VerticalTabStripStateController::OnHoverExpandEnabledPrefChanged() { ++ if (!IsHoverExpandEnabled()) { ++ hover_expand_timer_.Stop(); ++ if (held_by_hover_) { ++ held_by_hover_ = false; ++ NotifyStateChanged(); ++ } ++ return; ++ } ++ StartHoverExpandTimerIfNeeded(); ++} ++ + void VerticalTabStripStateController::StartHoverExpandTimerIfNeeded() { +- if (!state_.collapsed || !is_hovered_ || held_by_hover_ || ++ if (!IsHoverExpandEnabled() || !state_.collapsed || !is_hovered_ || ++ held_by_hover_ || + hover_expand_timer_.IsRunning()) { + return; + } +--- a/chrome/browser/ui/tabs/vertical_tab_strip_state_controller_unittest.cc ++++ b/chrome/browser/ui/tabs/vertical_tab_strip_state_controller_unittest.cc +@@ -48,6 +48,9 @@ + prefs::kHeliumVerticalUncollapsedWidth, + kVerticalTabStripDefaultUncollapsedWidth, + user_prefs::PrefRegistrySyncable::SYNCABLE_PREF); ++ pref_service_.registry()->RegisterBooleanPref( ++ prefs::kHeliumVerticalHoverExpandEnabled, true, ++ user_prefs::PrefRegistrySyncable::SYNCABLE_PREF); + SessionID test_session_id = SessionID::FromSerializedValue(kSessionIDValue); + + EXPECT_CALL(mock_browser_window_interface_, GetUnownedUserDataHost) +@@ -188,4 +191,21 @@ + EXPECT_EQ(3, call_count); + } + ++TEST_F(VerticalTabStripStateControllerTest, HoverExpandedDisabledByPref) { ++ controller()->SetCollapsed(true); ++ controller()->SetHovered(true); ++ task_environment_.FastForwardBy(base::Milliseconds(500)); ++ EXPECT_FALSE(controller()->IsHoverExpanded()); ++} ++ ++TEST_F(VerticalTabStripStateControllerTest, DisablingPrefClearsHoverExpanded) { ++ controller()->SetCollapsed(true); ++ controller()->SetHovered(true); ++ task_environment_.FastForwardBy(base::Milliseconds(500)); ++ EXPECT_TRUE(controller()->IsHoverExpanded()); ++ ++ pref_service()->SetBoolean(prefs::kHeliumVerticalHoverExpandEnabled, false); ++ EXPECT_FALSE(controller()->IsHoverExpanded()); ++} ++ + } // namespace tabs +--- a/chrome/app/settings_strings.grdp ++++ b/chrome/app/settings_strings.grdp +@@ -291,6 +291,9 @@ + + Show vertical tabs on right side + ++ ++ Expand collapsed vertical tabs on hover ++ + + Show home button + +--- a/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc ++++ b/chrome/browser/ui/webui/settings/settings_localized_strings_provider.cc +@@ -530,6 +530,7 @@ + {"browserLayoutCompact", IDS_SETTINGS_BROWSER_LAYOUT_COMPACT}, + {"browserLayoutVertical", IDS_SETTINGS_BROWSER_LAYOUT_VERTICAL}, + {"tabStripRightAlign", IDS_SETTINGS_TAB_STRIP_RIGHT_ALIGN}, ++ {"tabStripHoverExpand", IDS_SETTINGS_TAB_STRIP_HOVER_EXPAND}, + {"showHomeButton", IDS_SETTINGS_SHOW_HOME_BUTTON}, + {"showBookmarksBar", IDS_SETTINGS_SHOW_BOOKMARKS_BAR}, + {"tabStripPosition", IDS_SETTINGS_TAB_STRIP_POSITION}, +--- a/chrome/browser/resources/settings/appearance_page/appearance_page.ts ++++ b/chrome/browser/resources/settings/appearance_page/appearance_page.ts +@@ -474,6 +474,10 @@ + return this.showVerticalTabsEnabled_ && layout === BrowserLayout.VERTICAL; + } + ++ private showVerticalHoverExpansionSetting_(layout: number|undefined): boolean { ++ return this.showVerticalTabsEnabled_ && layout === BrowserLayout.VERTICAL; ++ } ++ + private themeChanged_(themeId: string) { + if (this.prefs === undefined || this.systemTheme_ === undefined) { + return; +--- a/chrome/browser/resources/settings/appearance_page/appearance_page.html ++++ b/chrome/browser/resources/settings/appearance_page/appearance_page.html +@@ -114,6 +114,12 @@ + pref="{{prefs.helium.browser.vertical_right_aligned}}" + label="$i18n{tabStripRightAlign}"> + ++ + + +
pref_service_; + PrefChangeRegistrar pref_change_registrar_; + raw_ptr root_action_item_; + + VerticalTabStripState state_; ++ bool is_hovered_ = false; ++ bool held_by_hover_ = false; ++ base::OneShotTimer hover_expand_timer_; + + base::RepeatingCallbackList + on_state_changed_callback_list_; +--- a/chrome/browser/ui/tabs/vertical_tab_strip_state_controller.cc ++++ b/chrome/browser/ui/tabs/vertical_tab_strip_state_controller.cc +@@ -6,6 +6,8 @@ + + #include + ++#include "base/functional/bind.h" ++#include "base/time/time.h" + #include "chrome/browser/profiles/profile.h" + #include "chrome/browser/ui/actions/chrome_action_id.h" + #include "chrome/browser/ui/browser_actions.h" +@@ -24,6 +26,10 @@ + + DEFINE_USER_DATA(VerticalTabStripStateController); + ++namespace { ++constexpr base::TimeDelta kHoverExpandDelay = base::Milliseconds(400); ++} // namespace ++ + VerticalTabStripStateController::VerticalTabStripStateController( + BrowserWindowInterface* browser_window, + PrefService* pref_service, +@@ -88,6 +94,35 @@ bool VerticalTabStripStateController::IsCollapsed() const { + void VerticalTabStripStateController::SetCollapsed(bool collapsed) { + if (state_.collapsed != collapsed) { + state_.collapsed = collapsed; ++ if (!state_.collapsed) { ++ held_by_hover_ = false; ++ hover_expand_timer_.Stop(); ++ } else { ++ held_by_hover_ = false; ++ StartHoverExpandTimerIfNeeded(); ++ } ++ NotifyStateChanged(); ++ } ++} ++ ++bool VerticalTabStripStateController::IsHoverExpanded() const { ++ return state_.collapsed && held_by_hover_; ++} ++ ++void VerticalTabStripStateController::SetHovered(bool hovered) { ++ if (is_hovered_ == hovered) { ++ return; ++ } ++ is_hovered_ = hovered; ++ ++ if (is_hovered_) { ++ StartHoverExpandTimerIfNeeded(); ++ return; ++ } ++ ++ hover_expand_timer_.Stop(); ++ if (held_by_hover_) { ++ held_by_hover_ = false; + NotifyStateChanged(); + } + } +@@ -108,6 +143,13 @@ void VerticalTabStripStateController::SetState( + if (state_.collapsed != state.collapsed || + state_.uncollapsed_width != state.uncollapsed_width) { + state_ = state; ++ if (!state_.collapsed) { ++ held_by_hover_ = false; ++ hover_expand_timer_.Stop(); ++ } else { ++ held_by_hover_ = false; ++ StartHoverExpandTimerIfNeeded(); ++ } + NotifyStateChanged(); + } + } +@@ -152,4 +194,24 @@ void VerticalTabStripStateController::UpdateCollapseActionItem() { + } + } + ++void VerticalTabStripStateController::StartHoverExpandTimerIfNeeded() { ++ if (!state_.collapsed || !is_hovered_ || held_by_hover_ || ++ hover_expand_timer_.IsRunning()) { ++ return; ++ } ++ hover_expand_timer_.Start( ++ FROM_HERE, kHoverExpandDelay, ++ base::BindOnce( ++ &VerticalTabStripStateController::OnHoverExpandDelayElapsed, ++ base::Unretained(this))); ++} ++ ++void VerticalTabStripStateController::OnHoverExpandDelayElapsed() { ++ if (!state_.collapsed || !is_hovered_ || held_by_hover_) { ++ return; ++ } ++ held_by_hover_ = true; ++ NotifyStateChanged(); ++} ++ + } // namespace tabs +--- a/chrome/browser/ui/tabs/vertical_tab_strip_state_controller_unittest.cc ++++ b/chrome/browser/ui/tabs/vertical_tab_strip_state_controller_unittest.cc +@@ -5,7 +5,11 @@ + #include "chrome/browser/ui/tabs/vertical_tab_strip_state_controller.h" + + #include ++#include + ++#include "base/test/task_environment.h" ++#include "base/time/time.h" ++#include "chrome/browser/ui/helium/helium_layout_state_controller.h" + #include "chrome/browser/ui/browser_window/test/mock_browser_window_interface.h" + #include "chrome/browser/ui/tabs/vertical_tab_strip_state.h" + #include "chrome/common/pref_names.h" +@@ -30,8 +34,19 @@ class VerticalTabStripStateControllerTest : public testing::Test { + + void SetUp() override { + testing::Test::SetUp(); ++ pref_service_.registry()->RegisterIntegerPref( ++ prefs::kHeliumLayout, ++ std::to_underlying(HeliumLayoutType::kHorizontal), ++ user_prefs::PrefRegistrySyncable::SYNCABLE_PREF); ++ pref_service_.registry()->RegisterBooleanPref( ++ prefs::kHeliumVerticalRightAligned, false, ++ user_prefs::PrefRegistrySyncable::SYNCABLE_PREF); + pref_service_.registry()->RegisterBooleanPref( +- prefs::kVerticalTabsEnabled, false, ++ prefs::kHeliumVerticalCollapsed, false, ++ user_prefs::PrefRegistrySyncable::SYNCABLE_PREF); ++ pref_service_.registry()->RegisterIntegerPref( ++ prefs::kHeliumVerticalUncollapsedWidth, ++ kVerticalTabStripDefaultUncollapsedWidth, + user_prefs::PrefRegistrySyncable::SYNCABLE_PREF); + SessionID test_session_id = SessionID::FromSerializedValue(kSessionIDValue); + +@@ -58,6 +73,8 @@ class VerticalTabStripStateControllerTest : public testing::Test { + } + + private: ++ base::test::TaskEnvironment task_environment_{ ++ base::test::TaskEnvironment::TimeSource::MOCK_TIME}; + std::unique_ptr controller_; + sync_preferences::TestingPrefServiceSyncable pref_service_; + ui::UnownedUserDataHost unowned_user_data_host_; +@@ -74,11 +91,13 @@ TEST_F(VerticalTabStripStateControllerTest, Initial) { + TEST_F(VerticalTabStripStateControllerTest, VerticalTabsEnabled) { + controller()->SetVerticalTabsEnabled(true); + EXPECT_TRUE(controller()->ShouldDisplayVerticalTabs()); +- EXPECT_TRUE(pref_service()->GetBoolean(prefs::kVerticalTabsEnabled)); ++ EXPECT_EQ(std::to_underlying(HeliumLayoutType::kVertical), ++ pref_service()->GetInteger(prefs::kHeliumLayout)); + + controller()->SetVerticalTabsEnabled(false); + EXPECT_FALSE(controller()->ShouldDisplayVerticalTabs()); +- EXPECT_FALSE(pref_service()->GetBoolean(prefs::kVerticalTabsEnabled)); ++ EXPECT_EQ(std::to_underlying(HeliumLayoutType::kHorizontal), ++ pref_service()->GetInteger(prefs::kHeliumLayout)); + } + + TEST_F(VerticalTabStripStateControllerTest, Collapsed) { +@@ -141,4 +160,32 @@ TEST_F(VerticalTabStripStateControllerTest, State) { + EXPECT_EQ(1, call_count); + } + ++TEST_F(VerticalTabStripStateControllerTest, HoverExpanded) { ++ int call_count = 0; ++ auto subscription = controller()->RegisterOnStateChanged(base::BindRepeating( ++ [](int* call_count, VerticalTabStripStateController* controller) { ++ (*call_count)++; ++ if (*call_count == 2) { ++ EXPECT_TRUE(controller->IsHoverExpanded()); ++ } ++ }, ++ &call_count)); ++ ++ controller()->SetCollapsed(true); ++ EXPECT_FALSE(controller()->IsHoverExpanded()); ++ ++ controller()->SetHovered(true); ++ task_environment_.FastForwardBy(base::Milliseconds(399)); ++ EXPECT_FALSE(controller()->IsHoverExpanded()); ++ EXPECT_EQ(1, call_count); ++ ++ task_environment_.FastForwardBy(base::Milliseconds(1)); ++ EXPECT_TRUE(controller()->IsHoverExpanded()); ++ EXPECT_EQ(2, call_count); ++ ++ controller()->SetHovered(false); ++ EXPECT_FALSE(controller()->IsHoverExpanded()); ++ EXPECT_EQ(3, call_count); ++} ++ + } // namespace tabs diff --git a/patches/helium/ui/layout/vertical-hover-reliability.patch b/patches/helium/ui/layout/vertical-hover-reliability.patch new file mode 100644 index 000000000..ccb4062b8 --- /dev/null +++ b/patches/helium/ui/layout/vertical-hover-reliability.patch @@ -0,0 +1,285 @@ +--- a/chrome/browser/ui/tabs/vertical_tab_strip_state_controller.h ++++ b/chrome/browser/ui/tabs/vertical_tab_strip_state_controller.h +@@ -74,10 +74,15 @@ + // Update the Collapse Button's Action Item (kActionToggleCollapseVertical) + // based on the Vertical Tab Strip's Collapse State. + void UpdateCollapseActionItem(); ++ void OnLayoutOrAlignmentPrefChanged(); + bool IsHoverExpandEnabled() const; + void OnHoverExpandEnabledPrefChanged(); ++ bool ShouldHoldExpandedForHover() const; ++ void ClearHoverHoldState(bool notify); + void StartHoverExpandTimerIfNeeded(); + void OnHoverExpandDelayElapsed(); ++ void StartHoverCollapseTimerIfNeeded(); ++ void OnHoverCollapseDelayElapsed(); + + const raw_ptr pref_service_; + PrefChangeRegistrar pref_change_registrar_; +@@ -87,6 +92,7 @@ + bool is_hovered_ = false; + bool held_by_hover_ = false; + base::OneShotTimer hover_expand_timer_; ++ base::OneShotTimer hover_collapse_timer_; + + base::RepeatingCallbackList + on_state_changed_callback_list_; +--- a/chrome/browser/ui/tabs/vertical_tab_strip_state_controller.cc ++++ b/chrome/browser/ui/tabs/vertical_tab_strip_state_controller.cc +@@ -27,7 +27,8 @@ + DEFINE_USER_DATA(VerticalTabStripStateController); + + namespace { +-constexpr base::TimeDelta kHoverExpandDelay = base::Milliseconds(400); ++constexpr base::TimeDelta kHoverExpandDelay = base::Milliseconds(300); ++constexpr base::TimeDelta kHoverCollapseDelay = base::Milliseconds(120); + } // namespace + + VerticalTabStripStateController::VerticalTabStripStateController( +@@ -46,8 +47,9 @@ + + pref_change_registrar_.AddMultiple( + {prefs::kHeliumLayout, prefs::kHeliumVerticalRightAligned}, +- base::BindRepeating(&VerticalTabStripStateController::NotifyStateChanged, +- base::Unretained(this))); ++ base::BindRepeating( ++ &VerticalTabStripStateController::OnLayoutOrAlignmentPrefChanged, ++ base::Unretained(this))); + pref_change_registrar_.Add( + prefs::kHeliumVerticalHoverExpandEnabled, + base::BindRepeating( +@@ -97,21 +99,21 @@ + } + + void VerticalTabStripStateController::SetCollapsed(bool collapsed) { +- if (state_.collapsed != collapsed) { +- state_.collapsed = collapsed; +- if (!state_.collapsed) { +- held_by_hover_ = false; +- hover_expand_timer_.Stop(); +- } else { +- held_by_hover_ = false; +- StartHoverExpandTimerIfNeeded(); +- } +- NotifyStateChanged(); ++ if (state_.collapsed == collapsed) { ++ return; + } ++ ++ state_.collapsed = collapsed; ++ ClearHoverHoldState(/*notify=*/false); ++ if (state_.collapsed) { ++ StartHoverExpandTimerIfNeeded(); ++ } ++ NotifyStateChanged(); + } + + bool VerticalTabStripStateController::IsHoverExpanded() const { +- return state_.collapsed && held_by_hover_ && IsHoverExpandEnabled(); ++ return state_.collapsed && held_by_hover_ && IsHoverExpandEnabled() && ++ ShouldDisplayVerticalTabs(); + } + + void VerticalTabStripStateController::SetHovered(bool hovered) { +@@ -121,7 +123,8 @@ + is_hovered_ = hovered; + + if (is_hovered_) { +- if (!IsHoverExpandEnabled()) { ++ hover_collapse_timer_.Stop(); ++ if (!ShouldHoldExpandedForHover()) { + return; + } + StartHoverExpandTimerIfNeeded(); +@@ -129,10 +132,7 @@ + } + + hover_expand_timer_.Stop(); +- if (held_by_hover_) { +- held_by_hover_ = false; +- NotifyStateChanged(); +- } ++ StartHoverCollapseTimerIfNeeded(); + } + + int VerticalTabStripStateController::GetUncollapsedWidth() const { +@@ -151,11 +151,8 @@ + if (state_.collapsed != state.collapsed || + state_.uncollapsed_width != state.uncollapsed_width) { + state_ = state; +- if (!state_.collapsed) { +- held_by_hover_ = false; +- hover_expand_timer_.Stop(); +- } else { +- held_by_hover_ = false; ++ ClearHoverHoldState(/*notify=*/false); ++ if (state_.collapsed) { + StartHoverExpandTimerIfNeeded(); + } + NotifyStateChanged(); +@@ -206,24 +203,46 @@ + return pref_service_->GetBoolean(prefs::kHeliumVerticalHoverExpandEnabled); + } + ++void VerticalTabStripStateController::OnLayoutOrAlignmentPrefChanged() { ++ if (!ShouldDisplayVerticalTabs()) { ++ ClearHoverHoldState(/*notify=*/false); ++ } else if (state_.collapsed && is_hovered_) { ++ StartHoverExpandTimerIfNeeded(); ++ } ++ NotifyStateChanged(); ++} ++ + void VerticalTabStripStateController::OnHoverExpandEnabledPrefChanged() { + if (!IsHoverExpandEnabled()) { +- hover_expand_timer_.Stop(); +- if (held_by_hover_) { +- held_by_hover_ = false; +- NotifyStateChanged(); +- } ++ ClearHoverHoldState(/*notify=*/true); + return; + } + StartHoverExpandTimerIfNeeded(); + } + ++bool VerticalTabStripStateController::ShouldHoldExpandedForHover() const { ++ return IsHoverExpandEnabled() && ShouldDisplayVerticalTabs() && ++ state_.collapsed; ++} ++ ++void VerticalTabStripStateController::ClearHoverHoldState(bool notify) { ++ hover_expand_timer_.Stop(); ++ hover_collapse_timer_.Stop(); ++ if (!held_by_hover_) { ++ return; ++ } ++ held_by_hover_ = false; ++ if (notify) { ++ NotifyStateChanged(); ++ } ++} ++ + void VerticalTabStripStateController::StartHoverExpandTimerIfNeeded() { +- if (!IsHoverExpandEnabled() || !state_.collapsed || !is_hovered_ || +- held_by_hover_ || ++ if (!ShouldHoldExpandedForHover() || !is_hovered_ || held_by_hover_ || + hover_expand_timer_.IsRunning()) { + return; + } ++ hover_collapse_timer_.Stop(); + hover_expand_timer_.Start( + FROM_HERE, kHoverExpandDelay, + base::BindOnce( +@@ -232,11 +251,31 @@ + } + + void VerticalTabStripStateController::OnHoverExpandDelayElapsed() { +- if (!state_.collapsed || !is_hovered_ || held_by_hover_) { ++ if (!ShouldHoldExpandedForHover() || !is_hovered_ || held_by_hover_) { + return; + } + held_by_hover_ = true; + NotifyStateChanged(); + } + ++void VerticalTabStripStateController::StartHoverCollapseTimerIfNeeded() { ++ if (!held_by_hover_ || is_hovered_ || !state_.collapsed || ++ hover_collapse_timer_.IsRunning()) { ++ return; ++ } ++ hover_collapse_timer_.Start( ++ FROM_HERE, kHoverCollapseDelay, ++ base::BindOnce( ++ &VerticalTabStripStateController::OnHoverCollapseDelayElapsed, ++ base::Unretained(this))); ++} ++ ++void VerticalTabStripStateController::OnHoverCollapseDelayElapsed() { ++ if (!held_by_hover_ || is_hovered_ || !state_.collapsed) { ++ return; ++ } ++ held_by_hover_ = false; ++ NotifyStateChanged(); ++} ++ + } // namespace tabs +--- a/chrome/browser/ui/tabs/vertical_tab_strip_state_controller_unittest.cc ++++ b/chrome/browser/ui/tabs/vertical_tab_strip_state_controller_unittest.cc +@@ -174,11 +174,13 @@ + }, + &call_count)); + ++ pref_service()->SetInteger(prefs::kHeliumLayout, ++ std::to_underlying(HeliumLayoutType::kVertical)); + controller()->SetCollapsed(true); + EXPECT_FALSE(controller()->IsHoverExpanded()); + + controller()->SetHovered(true); +- task_environment_.FastForwardBy(base::Milliseconds(399)); ++ task_environment_.FastForwardBy(base::Milliseconds(299)); + EXPECT_FALSE(controller()->IsHoverExpanded()); + EXPECT_EQ(1, call_count); + +@@ -187,11 +189,52 @@ + EXPECT_EQ(2, call_count); + + controller()->SetHovered(false); ++ task_environment_.FastForwardBy(base::Milliseconds(119)); ++ EXPECT_TRUE(controller()->IsHoverExpanded()); ++ EXPECT_EQ(2, call_count); ++ ++ task_environment_.FastForwardBy(base::Milliseconds(1)); + EXPECT_FALSE(controller()->IsHoverExpanded()); + EXPECT_EQ(3, call_count); + } + ++TEST_F(VerticalTabStripStateControllerTest, HoverCollapseCancelledOnReenter) { ++ pref_service()->SetInteger(prefs::kHeliumLayout, ++ std::to_underlying(HeliumLayoutType::kVertical)); ++ controller()->SetCollapsed(true); ++ ++ controller()->SetHovered(true); ++ task_environment_.FastForwardBy(base::Milliseconds(300)); ++ EXPECT_TRUE(controller()->IsHoverExpanded()); ++ ++ controller()->SetHovered(false); ++ task_environment_.FastForwardBy(base::Milliseconds(80)); ++ controller()->SetHovered(true); ++ task_environment_.FastForwardBy(base::Milliseconds(1000)); ++ EXPECT_TRUE(controller()->IsHoverExpanded()); ++} ++ ++TEST_F(VerticalTabStripStateControllerTest, ++ SwitchingAwayFromVerticalClearsHoverExpanded) { ++ pref_service()->SetInteger(prefs::kHeliumLayout, ++ std::to_underlying(HeliumLayoutType::kVertical)); ++ controller()->SetCollapsed(true); ++ controller()->SetHovered(true); ++ task_environment_.FastForwardBy(base::Milliseconds(300)); ++ EXPECT_TRUE(controller()->IsHoverExpanded()); ++ ++ pref_service()->SetInteger(prefs::kHeliumLayout, ++ std::to_underlying(HeliumLayoutType::kHorizontal)); ++ EXPECT_FALSE(controller()->IsHoverExpanded()); ++ ++ controller()->SetHovered(false); ++ EXPECT_FALSE(controller()->IsHoverExpanded()); ++} ++ + TEST_F(VerticalTabStripStateControllerTest, HoverExpandedDisabledByPref) { ++ pref_service()->SetBoolean(prefs::kHeliumVerticalHoverExpandEnabled, false); ++ pref_service()->SetInteger(prefs::kHeliumLayout, ++ std::to_underlying(HeliumLayoutType::kVertical)); + controller()->SetCollapsed(true); + controller()->SetHovered(true); + task_environment_.FastForwardBy(base::Milliseconds(500)); +@@ -199,6 +242,8 @@ + } + + TEST_F(VerticalTabStripStateControllerTest, DisablingPrefClearsHoverExpanded) { ++ pref_service()->SetInteger(prefs::kHeliumLayout, ++ std::to_underlying(HeliumLayoutType::kVertical)); + controller()->SetCollapsed(true); + controller()->SetHovered(true); + task_environment_.FastForwardBy(base::Milliseconds(500)); diff --git a/patches/series b/patches/series index 446d0ed50..ac237423b 100644 --- a/patches/series +++ b/patches/series @@ -320,6 +320,11 @@ helium/ui/layout/settings.patch helium/ui/layout/context-menu.patch helium/ui/layout/compact.patch helium/ui/layout/vertical.patch +helium/ui/layout/vertical-hover-overlay.patch +helium/ui/layout/vertical-hover-overlay-callers.patch +helium/ui/layout/vertical-hover-overlay-setting.patch +helium/ui/layout/vertical-hover-reliability.patch +helium/ui/layout/vertical-hover-edit-lock.patch helium/ui/pdf-viewer.patch helium/ui/hide-pip-live-caption-button.patch