| // Copyright 2025 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_view.h" |
| |
| #include "base/callback_list.h" |
| #include "base/functional/bind.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "chrome/browser/ui/browser_element_identifiers.h" |
| #include "chrome/browser/ui/browser_window/public/browser_window_interface.h" |
| #include "chrome/browser/ui/color/chrome_color_id.h" |
| #include "chrome/browser/ui/layout_constants.h" |
| #include "chrome/browser/ui/tabs/tab_group_model.h" |
| #include "chrome/browser/ui/tabs/tab_strip_model.h" |
| #include "chrome/browser/ui/views/tabs/tab_hover_card_controller.h" |
| #include "chrome/browser/ui/views/tabs/vertical/tab_collection_node.h" |
| #include "chrome/browser/ui/views/tabs/vertical/vertical_pinned_tab_container_view.h" |
| #include "chrome/browser/ui/views/tabs/vertical/vertical_tab_group_view.h" |
| #include "chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_controller.h" |
| #include "chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_scroll_bar.h" |
| #include "chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_utils.h" |
| #include "chrome/browser/ui/views/tabs/vertical/vertical_unpinned_tab_container_view.h" |
| #include "components/tabs/public/tab_group.h" |
| #include "ui/base/metadata/metadata_impl_macros.h" |
| #include "ui/color/color_id.h" |
| #include "ui/views/background.h" |
| #include "ui/views/controls/scroll_view.h" |
| #include "ui/views/controls/separator.h" |
| #include "ui/views/layout/delegating_layout_manager.h" |
| #include "ui/views/layout/layout_types.h" |
| #include "ui/views/layout/proposed_layout.h" |
| #include "ui/views/view.h" |
| #include "ui/views/view_class_properties.h" |
| #include "ui/views/view_utils.h" |
| |
| class VerticalTabStripView::ActivatedViewTracker : public views::ViewObserver { |
| public: |
| ActivatedViewTracker() = default; |
| ActivatedViewTracker(const ActivatedViewTracker&) = delete; |
| ActivatedViewTracker& operator=(const ActivatedViewTracker&) = delete; |
| ~ActivatedViewTracker() override = default; |
| |
| // ViewObserver: |
| void OnViewIsDeleting(views::View* observed_view) override { |
| SetView(nullptr); |
| } |
| void OnViewRemovedFromWidget(views::View* observed_view) override { |
| SetView(nullptr); |
| } |
| void OnViewBoundsChanged(views::View* observed_view) override { |
| CheckTrackedViewHeight(); |
| } |
| void OnViewPreferredSizeChanged(views::View* observed_view) override { |
| CheckTrackedViewHeight(); |
| } |
| |
| void SetView(views::View* view) { |
| if (view == view_) { |
| return; |
| } |
| observation_.Reset(); |
| on_reached_preferred_height_cb_.Reset(); |
| view_ = view; |
| |
| if (view_) { |
| observation_.Observe(view_.get()); |
| } |
| } |
| views::View* view() { return view_; } |
| |
| // Returns true if the tracked view height matches its preferred height. |
| bool IsViewAtPreferredHeight() { |
| return view_->size().height() == view_->GetPreferredSize().height(); |
| } |
| |
| // Sets a callback that is run when the tracked view's height reaches its |
| // preferred height. |
| void SetOnReachedPreferredHeightCallback( |
| base::OnceClosure on_reached_preferred_height_cb) { |
| on_reached_preferred_height_cb_ = std::move(on_reached_preferred_height_cb); |
| CheckTrackedViewHeight(); |
| } |
| |
| private: |
| void CheckTrackedViewHeight() { |
| CHECK(view_); |
| if (IsViewAtPreferredHeight() && on_reached_preferred_height_cb_) { |
| std::move(on_reached_preferred_height_cb_).Run(); |
| } |
| } |
| |
| raw_ptr<views::View> view_ = nullptr; |
| base::OnceClosure on_reached_preferred_height_cb_; |
| base::ScopedObservation<View, ViewObserver> observation_{this}; |
| }; |
| |
| VerticalTabStripView::VerticalTabStripView(TabCollectionNode* collection_node) |
| : collection_node_(collection_node), |
| activated_view_tracker_(std::make_unique<ActivatedViewTracker>()) { |
| SetLayoutManager(std::make_unique<views::DelegatingLayoutManager>(this)); |
| SetProperty(views::kElementIdentifierKey, kTabStripElementId); |
| |
| pinned_tabs_scroll_view_ = AddChildView(std::make_unique<views::ScrollView>( |
| views::ScrollView::ScrollWithLayers::kEnabled)); |
| SetScrollViewProperties(pinned_tabs_scroll_view_); |
| |
| auto tabs_separator = std::make_unique<views::Separator>(); |
| tabs_separator_ = AddChildView(std::move(tabs_separator)); |
| |
| unpinned_tabs_scroll_view_ = AddChildView(std::make_unique<views::ScrollView>( |
| views::ScrollView::ScrollWithLayers::kEnabled)); |
| SetScrollViewProperties(unpinned_tabs_scroll_view_); |
| |
| collection_node->set_add_child_to_node(base::BindRepeating( |
| &VerticalTabStripView::AddScrollViewContents, base::Unretained(this))); |
| |
| collection_node->set_remove_child_from_node(base::BindRepeating( |
| &VerticalTabStripView::RemoveScrollViewContents, base::Unretained(this))); |
| |
| callback_subscriptions_.emplace_back( |
| collection_node_->RegisterWillDestroyCallback(base::BindOnce( |
| &VerticalTabStripView::ResetCollectionNode, base::Unretained(this)))); |
| |
| SetNotifyEnterExitOnChild(true); |
| UpdateColors(); |
| } |
| |
| VerticalTabStripView::~VerticalTabStripView() = default; |
| |
| views::ProposedLayout VerticalTabStripView::CalculateProposedLayout( |
| const views::SizeBounds& size_bounds) const { |
| views::ProposedLayout layouts; |
| if (!size_bounds.width().is_bounded()) { |
| return layouts; |
| } |
| |
| const int region_horizontal_padding = GetLayoutConstant( |
| is_collapsed_ ? LayoutConstant::kVerticalTabStripCollapsedPadding |
| : LayoutConstant::kVerticalTabStripUncollapsedPadding); |
| |
| const int region_vertical_padding = |
| GetLayoutConstant(LayoutConstant::kVerticalTabStripCollapsedPadding); |
| |
| int y = 0; |
| |
| // Determine container preferred heights. |
| views::SizeBounds pinned_tab_container_size_bounds = |
| size_bounds.Inset(gfx::Insets::TLBR(0, region_horizontal_padding, 0, 0)); |
| const int pinned_preferred_height = |
| pinned_tabs_scroll_view_ |
| ->GetPreferredSize(pinned_tab_container_size_bounds) |
| .height(); |
| const int unpinned_preferred_height = |
| unpinned_tabs_scroll_view_->GetPreferredSize(size_bounds).height(); |
| |
| const bool should_show_separator = pinned_preferred_height != 0 && |
| unpinned_preferred_height != 0 && |
| is_collapsed_; |
| |
| // If the height is bounded, calculate the available space for laying out the |
| // pinned and unpinned containers. |
| int remaining_height = 0; |
| if (size_bounds.height().is_bounded()) { |
| remaining_height = size_bounds.height().value(); |
| if (pinned_preferred_height != 0 && unpinned_preferred_height != 0) { |
| remaining_height -= region_vertical_padding; |
| } |
| if (should_show_separator) { |
| remaining_height -= tabs_separator_->GetPreferredSize().height() + |
| region_vertical_padding; |
| } |
| // Clamp the remaining height to 0 if we have less space. |
| remaining_height = std::max(remaining_height, 0); |
| } |
| |
| // Place the pinned container. |
| int pinned_container_height = pinned_preferred_height; |
| if (size_bounds.height().is_bounded()) { |
| // The pinned container height should not be larger than half the available |
| // space unless the unpinned container will not fill that space. Also make |
| // sure the height is at least the minimum. |
| pinned_container_height = std::max( |
| std::min(pinned_preferred_height, |
| std::max(remaining_height / 2, |
| remaining_height - unpinned_preferred_height)), |
| pinned_tabs_container_view_->GetMinimumSize().height()); |
| remaining_height -= pinned_container_height; |
| } |
| gfx::Rect pinned_container_bounds( |
| region_horizontal_padding, y, |
| pinned_tab_container_size_bounds.width().value(), |
| pinned_container_height); |
| layouts.child_layouts.emplace_back(pinned_tabs_scroll_view_.get(), |
| pinned_tabs_scroll_view_->GetVisible(), |
| pinned_container_bounds); |
| |
| if (pinned_container_bounds.height()) { |
| y += pinned_container_bounds.height(); |
| // Add padding only if there are pinned and unpinned tabs. |
| if (unpinned_preferred_height != 0) { |
| y += region_vertical_padding; |
| } |
| } |
| |
| // Place the tabs separator if visible. |
| if (should_show_separator) { |
| int separator_width = |
| size_bounds.width().value() - 2 * region_horizontal_padding; |
| int separator_x = region_horizontal_padding; |
| if (is_collapsed_) { |
| const int collapsed_separator_width = GetLayoutConstant( |
| LayoutConstant::kVerticalTabStripCollapsedSeparatorWidth); |
| separator_width = collapsed_separator_width; |
| separator_x = (size_bounds.width().value() - separator_width) / 2; |
| } |
| gfx::Rect tabs_separator_bounds( |
| separator_x, y, separator_width, |
| tabs_separator_->GetPreferredSize().height()); |
| layouts.child_layouts.emplace_back(tabs_separator_.get(), true, |
| tabs_separator_bounds); |
| |
| y += tabs_separator_bounds.height() + region_vertical_padding; |
| } else { |
| layouts.child_layouts.emplace_back(tabs_separator_.get(), false, |
| gfx::Rect()); |
| } |
| |
| // Place the unpinned container using the entire available width, we do not |
| // inset the x value by |region_horizontal_padding| here because, when the tab |
| // strip is collapsed, tab groups need to draw the group colored line in this |
| // space. |
| gfx::Rect unpinned_container_bounds(0, y, size_bounds.width().value(), |
| unpinned_preferred_height); |
| if (size_bounds.height().is_bounded()) { |
| unpinned_container_bounds.set_height( |
| std::max(std::min(unpinned_container_bounds.height(), remaining_height), |
| unpinned_tabs_container_view_->GetMinimumSize().height())); |
| } |
| layouts.child_layouts.emplace_back(unpinned_tabs_scroll_view_.get(), |
| unpinned_tabs_scroll_view_->GetVisible(), |
| unpinned_container_bounds); |
| |
| layouts.host_size = gfx::Size(size_bounds.width().value(), |
| unpinned_container_bounds.bottom()); |
| return layouts; |
| } |
| |
| void VerticalTabStripView::AddedToWidget() { |
| paint_as_active_subscription_ = |
| GetWidget()->RegisterPaintAsActiveChangedCallback(base::BindRepeating( |
| &VerticalTabStripView::UpdateColors, base::Unretained(this))); |
| } |
| |
| void VerticalTabStripView::OnMouseEntered(const ui::MouseEvent& event) { |
| mouse_entered_tabstrip_time_ = base::TimeTicks::Now(); |
| has_reported_time_mouse_entered_to_switch_ = false; |
| } |
| |
| void VerticalTabStripView::OnMouseExited(const ui::MouseEvent& event) { |
| if (!collection_node_) { |
| return; |
| } |
| |
| if (TabHoverCardController* hover_card_controller = |
| collection_node_->GetController()->GetHoverCardController(); |
| hover_card_controller) { |
| hover_card_controller->UpdateHoverCard( |
| nullptr, TabSlotController::HoverCardUpdateType::kHover); |
| } |
| } |
| |
| void VerticalTabStripView::OnActiveTabChanged( |
| const tabs::TabInterface* active_tab) { |
| if (collection_node_ && active_tab) { |
| // Expand group if the activated tab is within a collapsed group unless |
| // we are header dragging the collapsed group. |
| if (active_tab->GetGroup().has_value() && |
| !collection_node_->GetController()->GetDragHandler().IsDragging()) { |
| TabCollectionNode* group_node = collection_node_->GetNodeForHandle( |
| active_tab->GetBrowserWindowInterface() |
| ->GetTabStripModel() |
| ->group_model() |
| ->GetTabGroup(active_tab->GetGroup().value()) |
| ->GetCollectionHandle()); |
| CHECK(group_node); |
| |
| auto* group_view = |
| views::AsViewClass<VerticalTabGroupView>(group_node->view()); |
| if (group_view && group_view->IsCollapsed()) { |
| group_view->ToggleCollapsedState( |
| ToggleTabGroupCollapsedStateOrigin::kMenuAction); |
| } |
| } |
| |
| // Scroll to the activated tab if it isn't in the visible viewport. |
| TabCollectionNode* activated_node = |
| collection_node_->GetNodeForHandle(active_tab->GetHandle()); |
| CHECK(activated_node); |
| views::View* const activated_node_view = activated_node->view(); |
| activated_view_tracker_->SetView(activated_node_view); |
| |
| // Views must either be in the pinned or unpinned view trees. |
| DCHECK_NE(pinned_tabs_container_view_->Contains(activated_node_view), |
| unpinned_tabs_container_view_->Contains(activated_node_view)); |
| |
| views::ScrollView* const target_scroll_view = |
| pinned_tabs_container_view_->Contains(activated_node_view) |
| ? pinned_tabs_scroll_view_ |
| : unpinned_tabs_scroll_view_; |
| |
| // Wait for the next successful layout before attempting to handle moving |
| // the activated view into the scroll view viewport. |
| target_scroll_view->RegisterPostLayoutCallback(base::BindRepeating( |
| &VerticalTabStripView::EnsureVisibleInViewportPostActivationAndLayout, |
| base::Unretained(this))); |
| } |
| } |
| |
| void VerticalTabStripView::RecordMousePressedInTab() { |
| if (mouse_entered_tabstrip_time_.has_value() && |
| !has_reported_time_mouse_entered_to_switch_) { |
| DEPRECATED_UMA_HISTOGRAM_MEDIUM_TIMES( |
| "TabStrip.Vertical.TimeToSwitch", |
| base::TimeTicks::Now() - mouse_entered_tabstrip_time_.value()); |
| has_reported_time_mouse_entered_to_switch_ = true; |
| } |
| } |
| |
| VerticalPinnedTabContainerView* VerticalTabStripView::GetPinnedTabsContainer() { |
| return pinned_tabs_container_view_; |
| } |
| |
| VerticalUnpinnedTabContainerView* |
| VerticalTabStripView::GetUnpinnedTabsContainer() { |
| return unpinned_tabs_container_view_; |
| } |
| |
| void VerticalTabStripView::SetCollapsedState(bool is_collapsed) { |
| if (is_collapsed != is_collapsed_) { |
| is_collapsed_ = is_collapsed; |
| InvalidateLayout(); |
| } |
| } |
| |
| void VerticalTabStripView::SetIsAnimatingSize(bool is_animating) { |
| for (views::ScrollView* scroll_view : |
| {pinned_tabs_scroll_view_, unpinned_tabs_scroll_view_}) { |
| if (scroll_view) { |
| static_cast<VerticalTabStripScrollBar*>( |
| scroll_view->vertical_scroll_bar()) |
| ->SetIsAnimatingSize(is_animating); |
| } |
| } |
| } |
| |
| bool VerticalTabStripView::IsPositionInWindowCaption(const gfx::Point& point) { |
| for (views::View* child : children()) { |
| if (!child->GetVisible()) { |
| continue; |
| } |
| |
| gfx::Point point_in_child = point; |
| ConvertPointToTarget(this, child, &point_in_child); |
| if (!child->HitTestPoint(point_in_child)) { |
| continue; |
| } |
| |
| auto* scroll_view = views::AsViewClass<views::ScrollView>(child); |
| if (!scroll_view) { |
| return true; |
| } |
| |
| if (scroll_view->vertical_scroll_bar()->GetVisible()) { |
| gfx::Point point_in_sb = point_in_child; |
| ConvertPointToTarget(scroll_view, scroll_view->vertical_scroll_bar(), |
| &point_in_sb); |
| if (scroll_view->vertical_scroll_bar()->HitTestPoint(point_in_sb)) { |
| return false; |
| } |
| } |
| |
| if (scroll_view->contents()) { |
| gfx::Point point_in_content = point_in_child; |
| ConvertPointToTarget(scroll_view, scroll_view->contents(), |
| &point_in_content); |
| if (scroll_view->contents()->HitTestPoint(point_in_content)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| return true; |
| } |
| |
| views::View* VerticalTabStripView::AddScrollViewContents( |
| std::unique_ptr<views::View> view) { |
| if (auto* container = |
| views::AsViewClass<VerticalUnpinnedTabContainerView>(view.get())) { |
| unpinned_tabs_container_view_ = container; |
| return unpinned_tabs_scroll_view_->SetContents(std::move(view)); |
| } |
| // |view| should only ever be VerticalUnpinnedTabContainerView or |
| // VerticalPinnedTabContainerView. |
| auto* container = |
| views::AsViewClass<VerticalPinnedTabContainerView>(view.get()); |
| CHECK(container); |
| pinned_tabs_container_view_ = container; |
| return pinned_tabs_scroll_view_->SetContents(std::move(view)); |
| } |
| |
| void VerticalTabStripView::RemoveScrollViewContents(views::View* view) { |
| if (views::IsViewClass<VerticalUnpinnedTabContainerView>(view)) { |
| unpinned_tabs_container_view_ = nullptr; |
| unpinned_tabs_scroll_view_->SetContents(nullptr); |
| return; |
| } |
| if (views::IsViewClass<VerticalPinnedTabContainerView>(view)) { |
| pinned_tabs_container_view_ = nullptr; |
| pinned_tabs_scroll_view_->SetContents(nullptr); |
| return; |
| } |
| // |view| should only ever be VerticalUnpinnedTabContainerView or |
| // VerticalPinnedTabContainerView. |
| NOTREACHED(); |
| } |
| |
| void VerticalTabStripView::SetScrollViewProperties( |
| views::ScrollView* scroll_view) { |
| scroll_view->SetUseContentsPreferredSize(true); |
| scroll_view->SetBackgroundColor(std::nullopt); |
| scroll_view->SetHorizontalScrollBarMode( |
| views::ScrollView::ScrollBarMode::kDisabled); |
| scroll_view->SetOverflowGradientMask( |
| views::ScrollView::GradientDirection::kVertical); |
| CHECK(collection_node_); |
| scroll_view->SetVerticalScrollBar(std::make_unique<VerticalTabStripScrollBar>( |
| collection_node_->GetController()->GetStateController())); |
| callback_subscriptions_.emplace_back(scroll_view->AddContentsScrolledCallback( |
| base::BindRepeating(&VerticalTabStripView::HideHoverCardOnScroll, |
| base::Unretained(this)))); |
| } |
| |
| void VerticalTabStripView::ResetCollectionNode() { |
| collection_node_ = nullptr; |
| } |
| |
| void VerticalTabStripView::EnsureVisibleInViewportPostActivationAndLayout( |
| views::ScrollView* scroll_view) { |
| // Explicitly re-register only as needed. |
| scroll_view->RegisterPostLayoutCallback(base::DoNothing()); |
| |
| // Guard against views being removed from the tree between frames. Dragging a |
| // view out of the visible bounds will also trigger a scroll naturally. |
| views::View* const activated_view = activated_view_tracker_->view(); |
| if (!activated_view || !Contains(activated_view) || |
| (collection_node_ && |
| collection_node_->GetController()->GetDragHandler().IsDragging())) { |
| EnableOverflowVisuals(scroll_view); |
| return; |
| } |
| |
| // Handle the case where the scroll view is currently not in an overflow |
| // state. In such a case the activated view will be visible in the scroll |
| // view's viewport without scrolling. |
| if (!scroll_view->IsVerticalContentOverflowing()) { |
| // It may be the case that the activated view is not at its target height |
| // (i.e. it was activated as it is being animated in). In such a case |
| // disable overflow visuals to prevent jank that can occur if content view |
| // bounds are changed in quick succession. |
| if (!activated_view_tracker_->IsViewAtPreferredHeight()) { |
| DisableOverflowVisuals(scroll_view); |
| activated_view_tracker_->SetOnReachedPreferredHeightCallback( |
| base::BindOnce(&VerticalTabStripView:: |
| EnsureVisibleInViewportPostActivationAndLayout, |
| base::Unretained(this), scroll_view)); |
| } else { |
| // Always exit with overflow visuals enabled. |
| EnableOverflowVisuals(scroll_view); |
| } |
| return; |
| } |
| |
| // Get view bounds in its contents coordinates. |
| gfx::Rect activated_view_bounds = |
| GetVerticalTabStripViewTargetBounds(activated_view); |
| |
| // Proceed up the hierarchy until the content view is reached, iteratively |
| // adjusting target view bounds. |
| for (views::View* v = activated_view->parent(); v != scroll_view->contents(); |
| v = v->parent()) { |
| activated_view_bounds = |
| views::View::ConvertRectToTarget(v, v->parent(), activated_view_bounds); |
| } |
| |
| // Get the visible bounds of the content view. |
| const gfx::Rect visible_contents_rect = scroll_view->GetVisibleRect(); |
| |
| // Determine the adjustment required to fit the activated view into the |
| // visible content view bounds. |
| gfx::Rect adjusted_activated_view_bounds = activated_view_bounds; |
| adjusted_activated_view_bounds.AdjustToFit(visible_contents_rect); |
| |
| // Calculate the required scroll offset for the visible content bounds (the |
| // reverse of the activated view adjustment). |
| int diff = activated_view_bounds.y() - adjusted_activated_view_bounds.y(); |
| |
| if (diff != 0) { |
| // Disable overflow visuals to avoid visual artifacts while scrolling, |
| // particularly for views towards the bottom of the scroll view. |
| DisableOverflowVisuals(scroll_view); |
| scroll_view->ScrollByOffset({0, static_cast<float>(diff)}); |
| scroll_view->RegisterPostLayoutCallback(base::BindRepeating( |
| &VerticalTabStripView::EnsureVisibleInViewportPostActivationAndLayout, |
| base::Unretained(this))); |
| } else { |
| EnableOverflowVisuals(scroll_view); |
| } |
| } |
| |
| void VerticalTabStripView::EnableOverflowVisuals( |
| views::ScrollView* scroll_view) { |
| // Override the post-layout callback to prevent any scheduled scroll requests |
| // from running. |
| scroll_view->RegisterPostLayoutCallback(base::DoNothing()); |
| scroll_view->SetDrawOverflowIndicator(true); |
| scroll_view->SetVerticalScrollBarMode( |
| views::ScrollView::ScrollBarMode::kEnabled); |
| |
| // Restore normal VerticalTabStripScrollBar scrollbar behavior. |
| if (auto* scroll_bar = views::AsViewClass<VerticalTabStripScrollBar>( |
| scroll_view->vertical_scroll_bar())) { |
| scroll_bar->SetIsAnimatingSize(false); |
| } |
| |
| // Reset the active view as it is no longer needed after post-activation |
| // adjustment for viewport visibility is complete. |
| activated_view_tracker_->SetView(nullptr); |
| } |
| |
| void VerticalTabStripView::DisableOverflowVisuals( |
| views::ScrollView* scroll_view) { |
| scroll_view->SetDrawOverflowIndicator(false); |
| scroll_view->SetVerticalScrollBarMode( |
| views::ScrollView::ScrollBarMode::kHiddenButEnabled); |
| |
| // Suppress scrollbar visuals to avoid artifacts as views are resized to |
| // target bounds whilst simultaneously scrolling to target. |
| if (auto* scroll_bar = views::AsViewClass<VerticalTabStripScrollBar>( |
| scroll_view->vertical_scroll_bar())) { |
| scroll_bar->SetIsAnimatingSize(true); |
| } |
| } |
| |
| void VerticalTabStripView::UpdateColors() { |
| tabs_separator_->SetColorId(IsFrameActive() ? kColorTabDividerFrameActive |
| : kColorTabDividerFrameInactive); |
| } |
| |
| bool VerticalTabStripView::IsFrameActive() const { |
| return GetWidget() ? GetWidget()->ShouldPaintAsActive() : true; |
| } |
| |
| void VerticalTabStripView::HideHoverCardOnScroll() { |
| if (!collection_node_) { |
| return; |
| } |
| |
| if (TabHoverCardController* hover_card_controller = |
| collection_node_->GetController()->GetHoverCardController(); |
| hover_card_controller && hover_card_controller->IsHoverCardVisible()) { |
| hover_card_controller->UpdateHoverCard( |
| nullptr, TabSlotController::HoverCardUpdateType::kAnimating); |
| } |
| } |
| |
| BEGIN_METADATA(VerticalTabStripView) |
| END_METADATA |