diff --git a/infra/PATCHES.md b/infra/PATCHES.md index d63382460..77e23b7a6 100644 --- a/infra/PATCHES.md +++ b/infra/PATCHES.md @@ -101,9 +101,15 @@ Always Show Component Extensions Patch > https://github.com/iridium-browser/irid Increase default key length for newly-generated RSA keys from 1024 to 2048 Patch > https://github.com/iridium-browser/iridium-browser/commit/d016769081706d591188b5b2929c5fc2efd8ef20 -Enable UI Features: Side Search, Side Panel Journeys, Chrome Labs, Extensions Access Menu, Tab Hover Cards, Tab Outlines in Low Contrast Themes, More Prominent Active Tab Title in Dark Mode, WebUI Tab Strip, Drag and Drop Tabs on Wayland, Tab Groups Saving > https://github.com/Alex313031/Thorium/blob/main/chrome/browser/ui/ui_features.cc +Enable UI Features: Side Search, Side Panel Journeys, Chrome Labs, Extensions Access Menu, Tab Hover Cards, WebUI Tab Strip, Drag and Drop Tabs on Wayland, Tab Groups Saving > https://github.com/Alex313031/Thorium/blob/main/chrome/browser/ui/ui_features.cc - Made by me. +Tab Outlines in Low Contrast Themes, More Prominent Active Tab Title in Dark Mode: Restore after they removed it in M113 > +https://chromium-review.googlesource.com/c/chromium/src/+/4578380 \ +https://chromium-review.googlesource.com/c/chromium/src/+/4578188 \ + + - Made by me. + Enable Precompiling of Inline Scripts in HTML - https://github.com/Alex313031/thorium/commit/8d237b76adff2ab4e89147b18ee1d0ab7bb29fb6 - Modified by me. diff --git a/src/chrome/browser/ui/tabs/tab_style.h b/src/chrome/browser/ui/tabs/tab_style.h new file mode 100644 index 000000000..8bf7f6771 --- /dev/null +++ b/src/chrome/browser/ui/tabs/tab_style.h @@ -0,0 +1,201 @@ +// Copyright 2023 The Chromium Authors, Alex313031 +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef CHROME_BROWSER_UI_TABS_TAB_STYLE_H_ +#define CHROME_BROWSER_UI_TABS_TAB_STYLE_H_ + +#include + +#include "chrome/browser/ui/color/chrome_color_id.h" +#include "chrome/browser/ui/tabs/tab_types.h" +#include "third_party/skia/include/core/SkColor.h" +#include "ui/color/color_id.h" +#include "ui/gfx/color_palette.h" +#include "ui/gfx/font_list.h" +#include "ui/gfx/geometry/insets.h" +#include "ui/gfx/geometry/rect_f.h" +#include "ui/gfx/geometry/size.h" + +namespace gfx { +class Canvas; +} + +class SkPath; + +// Holds the basic logic for rendering tabs, including preferred sizes, paths, +// etc. +class TabStyle { + public: + // The different types of path GetPath() can return. Different paths are used + // in different situations, but most (excluding |kClip|) are roughly the same + // shape. + enum class PathType { + // Interior fill outline. Extends halfway into the border so there are no + // gaps between border and fill. + kFill, + // Center of the border path. The path is guaranteed to fit into the tab + // bounds, including the stroke thickness. + kBorder, + // The hit test region. May be extended into a rectangle that touches the + // top of the bounding box when the window is maximized, for Fitts' Law. + kHitTest, + // The area inside the tab where children can be rendered, used to clip + // child views. Does not have to be the same shape as the border. + kInteriorClip, + // The path used for focus rings. + kHighlight, + }; + + // How we want the resulting path scaled. + enum class RenderUnits { + // The path is in pixels, and should have its internal area nicely aligned + // to pixel boundaries. + kPixels, + // The path is in DIPs. It will likely be calculated in pixels and then + // scaled back down. + kDips + }; + + enum class ShowHoverStyle { kSubtle, kPronounced }; + + enum class HideHoverStyle { + kGradual, // The hover should fade out. + kImmediate, // The hover should cut off, with no fade out. + }; + + // If we want to draw vertical separators between tabs, these are the leading + // and trailing separator stroke rectangles. + struct SeparatorBounds { + gfx::RectF leading; + gfx::RectF trailing; + }; + + // Contains values 0..1 representing the opacity of the corresponding + // separators. These are physical and not logical, so "left" is the left + // separator in both LTR and RTL. + struct SeparatorOpacities { + float left = 0, right = 0; + }; + + // Colors for various parts of the tab derived by TabStyle. + struct TabColors { + SkColor foreground_color = gfx::kPlaceholderColor; + SkColor background_color = gfx::kPlaceholderColor; + ui::ColorId focus_ring_color = kColorTabFocusRingInactive; + ui::ColorId close_button_focus_ring_color = + kColorTabCloseButtonFocusRingInactive; + + TabColors() = default; + TabColors(SkColor foreground_color, + SkColor background_color, + ui::ColorId focus_ring_color, + ui::ColorId close_button_focus_ring_color) + : foreground_color(foreground_color), + background_color(background_color), + focus_ring_color(focus_ring_color), + close_button_focus_ring_color(close_button_focus_ring_color) {} + bool operator==(const TabColors& other) const { + return std::tie(foreground_color, background_color, focus_ring_color, + close_button_focus_ring_color) == + std::tie(other.foreground_color, other.background_color, + other.focus_ring_color, + other.close_button_focus_ring_color); + } + }; + + TabStyle(const TabStyle&) = delete; + TabStyle& operator=(const TabStyle&) = delete; + virtual ~TabStyle(); + + // Gets the specific |path_type| associated with the specific |tab|. + // If |force_active| is true, applies an active appearance on the tab (usually + // involving painting an optional stroke) even if the tab is not the active + // tab. + virtual SkPath GetPath( + PathType path_type, + float scale, + bool force_active = false, + RenderUnits render_units = RenderUnits::kPixels) const = 0; + + // Returns the insets to use for laying out tab contents. + virtual gfx::Insets GetContentsInsets() const = 0; + + // Returns the z-value of the tab, which should be used to paint them in + // ascending order. Return values are in the range (0, + // TabStyle::GetMaximumZValue()). + virtual float GetZValue() const = 0; + + // Returns the current opacity of the "active" portion of the tab's state. + virtual float GetActiveOpacity() const = 0; + + // Returns whichever of (active, inactive) the tab appears more like given the + // active opacity. + virtual TabActive GetApparentActiveState() const = 0; + + // Derives and returns colors for the tab. See TabColors, above. + virtual TabColors CalculateColors() const = 0; + + // Returns the appropriate fonts for the current theme and active state. + virtual const gfx::FontList& GetFontList() const = 0; + + // Paints the tab. + virtual void PaintTab(gfx::Canvas* canvas) const = 0; + + // Sets the center of the radial highlight in the hover animation. + virtual void SetHoverLocation(const gfx::Point& location) = 0; + + // Shows the hover animation. + virtual void ShowHover(ShowHoverStyle style) = 0; + + // Hides the hover animation. + virtual void HideHover(HideHoverStyle style) = 0; + + // Opacity of the active tab background painted over inactive selected tabs. + static constexpr float kSelectedTabOpacity = 0.75f; + + // Returns the preferred width of a single Tab, assuming space is + // available. + static int GetStandardWidth(); + + // Returns the width for pinned tabs. Pinned tabs always have this width. + static int GetPinnedWidth(); + + // Returns the overlap between adjacent tabs. + static int GetTabOverlap(); + + // Get the space only partially occupied by a tab that we should + // consider to be padding rather than part of the body of the tab for + // interaction purposes. + static gfx::Insets GetTabInternalPadding(); + + // Gets the size of the separator drawn between tabs, if any. + static gfx::Size GetSeparatorSize(); + + // Returns, for a tab of height |height|, how far the window top drag handle + // can extend down into inactive tabs or the new tab button. This behavior + // is not used in all cases. + static int GetDragHandleExtension(int height); + + // Gets the preferred size for tab previews, which could be screencaps, hero + // or og:image images, etc. + static gfx::Size GetPreviewImageSize(); + + // Returns the radius of the outer corners of the tab shape. + static int GetCornerRadius(); + + // The largest valid value of TabStyle::GetZValue(). Currently, + // GM2TabStyle::GetZValue is the only implementation, and it can't return + // values larger than 7. + static constexpr float kMaximumZValue = 7.0f; + + protected: + // Avoid implicitly-deleted constructor. + TabStyle() = default; + + // Returns how far from the leading and trailing edges of a tab the contents + // should actually be laid out. + static int GetContentsHorizontalInsetSize(); +}; + +#endif // CHROME_BROWSER_UI_TABS_TAB_STYLE_H_ diff --git a/src/chrome/browser/ui/views/tabs/tab.cc b/src/chrome/browser/ui/views/tabs/tab.cc index 46438c424..e26d51299 100644 --- a/src/chrome/browser/ui/views/tabs/tab.cc +++ b/src/chrome/browser/ui/views/tabs/tab.cc @@ -212,6 +212,7 @@ Tab::Tab(TabSlotController* controller) 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. @@ -817,6 +818,7 @@ void Tab::ActiveStateChanged() { UpdateTabIconNeedsAttentionBlocked(); UpdateForegroundColors(); alert_indicator_button_->OnParentTabButtonColorChanged(); + title_->SetFontList(tab_style_->GetFontList()); Layout(); } diff --git a/src/chrome/browser/ui/views/tabs/tab_strip.cc b/src/chrome/browser/ui/views/tabs/tab_strip.cc new file mode 100644 index 000000000..365e68136 --- /dev/null +++ b/src/chrome/browser/ui/views/tabs/tab_strip.cc @@ -0,0 +1,2251 @@ +// Copyright 2023 The Chromium Authors, Alex313031 +// 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_strip.h" + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "base/compiler_specific.h" +#include "base/containers/adapters.h" +#include "base/containers/contains.h" +#include "base/containers/flat_map.h" +#include "base/cxx17_backports.h" +#include "base/feature_list.h" +#include "base/functional/bind.h" +#include "base/functional/callback.h" +#include "base/i18n/rtl.h" +#include "base/memory/raw_ptr.h" +#include "base/memory/weak_ptr.h" +#include "base/metrics/histogram.h" +#include "base/metrics/histogram_functions.h" +#include "base/metrics/histogram_macros.h" +#include "base/metrics/user_metrics.h" +#include "base/numerics/safe_conversions.h" +#include "base/observer_list.h" +#include "base/ranges/algorithm.h" +#include "base/scoped_observation.h" +#include "base/stl_util.h" +#include "base/strings/string_piece.h" +#include "base/strings/utf_string_conversions.h" +#include "base/time/time.h" +#include "base/timer/elapsed_timer.h" +#include "build/build_config.h" +#include "build/chromeos_buildflags.h" +#include "chrome/browser/defaults.h" +#include "chrome/browser/themes/theme_properties.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/tabs/tab_group_theme.h" +#include "chrome/browser/ui/tabs/tab_strip_model.h" +#include "chrome/browser/ui/tabs/tab_types.h" +#include "chrome/browser/ui/ui_features.h" +#include "chrome/browser/ui/view_ids.h" +#include "chrome/browser/ui/views/frame/browser_view.h" +#include "chrome/browser/ui/views/tabs/browser_tab_strip_controller.h" +#include "chrome/browser/ui/views/tabs/compound_tab_container.h" +#include "chrome/browser/ui/views/tabs/tab.h" +#include "chrome/browser/ui/views/tabs/tab_container_impl.h" +#include "chrome/browser/ui/views/tabs/tab_drag_controller.h" +#include "chrome/browser/ui/views/tabs/tab_group_header.h" +#include "chrome/browser/ui/views/tabs/tab_group_highlight.h" +#include "chrome/browser/ui/views/tabs/tab_group_underline.h" +#include "chrome/browser/ui/views/tabs/tab_group_views.h" +#include "chrome/browser/ui/views/tabs/tab_hover_card_controller.h" +#include "chrome/browser/ui/views/tabs/tab_slot_view.h" +#include "chrome/browser/ui/views/tabs/tab_strip_controller.h" +#include "chrome/browser/ui/views/tabs/tab_strip_layout_helper.h" +#include "chrome/browser/ui/views/tabs/tab_strip_layout_types.h" +#include "chrome/browser/ui/views/tabs/tab_strip_observer.h" +#include "chrome/browser/ui/views/tabs/tab_strip_types.h" +#include "chrome/browser/ui/views/tabs/tab_style_views.h" +#include "chrome/browser/ui/views/tabs/z_orderable_tab_container_element.h" +#include "chrome/browser/ui/views/touch_uma/touch_uma.h" +#include "chrome/browser/ui/web_applications/app_browser_controller.h" +#include "chrome/grit/generated_resources.h" +#include "chrome/grit/theme_resources.h" +#include "components/crash/core/common/crash_key.h" +#include "components/tab_groups/tab_group_color.h" +#include "components/tab_groups/tab_group_id.h" +#include "components/tab_groups/tab_group_visual_data.h" +#include "ui/base/clipboard/clipboard.h" +#include "ui/base/clipboard/clipboard_constants.h" +#include "ui/base/dragdrop/drag_drop_types.h" +#include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h" +#include "ui/base/interaction/element_identifier.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/resource/resource_bundle.h" +#include "ui/base/theme_provider.h" +#include "ui/color/color_provider.h" +#include "ui/display/display.h" +#include "ui/gfx/animation/throb_animation.h" +#include "ui/gfx/animation/tween.h" +#include "ui/gfx/geometry/rect_conversions.h" +#include "ui/gfx/geometry/size.h" +#include "ui/gfx/geometry/skia_conversions.h" +#include "ui/gfx/native_widget_types.h" +#include "ui/gfx/range/range.h" +#include "ui/views/accessibility/view_accessibility.h" +#include "ui/views/cascading_property.h" +#include "ui/views/controls/scroll_view.h" +#include "ui/views/interaction/element_tracker_views.h" +#include "ui/views/view_class_properties.h" +#include "ui/views/view_observer.h" +#include "ui/views/view_utils.h" +#include "ui/views/widget/root_view.h" +#include "ui/views/widget/widget.h" +#include "ui/views/window/non_client_view.h" + +#if BUILDFLAG(IS_WIN) +#include "ui/display/win/screen_win.h" +#include "ui/gfx/win/hwnd_util.h" +#include "ui/views/win/hwnd_util.h" +#endif + +#if defined(USE_AURA) +#include "ui/aura/window.h" +#endif + +namespace { + +ui::mojom::DragEventSource EventSourceFromEvent(const ui::LocatedEvent& event) { + return event.IsGestureEvent() ? ui::mojom::DragEventSource::kTouch + : ui::mojom::DragEventSource::kMouse; +} + +std::unique_ptr MakeTabContainer( + TabStrip* tab_strip, + TabHoverCardController* hover_card_controller, + TabDragContext* drag_context) { + if (base::FeatureList::IsEnabled(features::kSplitTabStrip)) { + return std::make_unique( + raw_ref::from_ptr(tab_strip), + hover_card_controller, drag_context, *tab_strip, tab_strip); + } + return std::make_unique( + *tab_strip, hover_card_controller, drag_context, *tab_strip, tab_strip); +} + +void UpdateDragEventSourceCrashKey( + absl::optional event_source) { + static crash_reporter::CrashKeyString<8> key("tabdrag-event-source"); + if (!event_source.has_value()) { + key.Clear(); + return; + } + key.Set(*event_source == ui::mojom::DragEventSource::kTouch ? "touch" + : "mouse"); +} + +} // namespace + +/////////////////////////////////////////////////////////////////////////////// +// TabStrip::TabDragContextImpl +// +class TabStrip::TabDragContextImpl : public TabDragContext, + public views::BoundsAnimatorObserver { + public: + METADATA_HEADER(TabDragContextImpl); + explicit TabDragContextImpl(TabStrip* tab_strip) + : tab_strip_(tab_strip), bounds_animator_(this) { + SetCanProcessEventsWithinSubtree(false); + + bounds_animator_.AddObserver(this); + } + // If a window is closed during a drag session, all our tabs will be taken + // from us before our destructor is even called. + ~TabDragContextImpl() override = default; + + gfx::Size CalculatePreferredSize() const override { + int max_child_x = 0; + for (views::View* child : children()) { + if (!views::IsViewClass(child)) + continue; + max_child_x = std::max(max_child_x, child->bounds().right()); + } + + return gfx::Size(max_child_x, GetLayoutConstant(TAB_HEIGHT)); + } + + bool OnMouseDragged(const ui::MouseEvent& event) override { + ContinueDrag(this, event); + return true; + } + + void OnMouseReleased(const ui::MouseEvent& event) override { + EndDrag(END_DRAG_COMPLETE); + } + + void OnMouseCaptureLost() override { EndDrag(END_DRAG_CAPTURE_LOST); } + + void OnGestureEvent(ui::GestureEvent* event) override { + switch (event->type()) { + case ui::ET_GESTURE_SCROLL_END: + case ui::ET_SCROLL_FLING_START: + case ui::ET_GESTURE_END: + EndDrag(END_DRAG_COMPLETE); + break; + + case ui::ET_GESTURE_LONG_TAP: { + EndDrag(END_DRAG_CANCEL); + break; + } + + case ui::ET_GESTURE_SCROLL_UPDATE: + ContinueDrag(this, *event); + break; + + case ui::ET_GESTURE_TAP_DOWN: + EndDrag(END_DRAG_CANCEL); + break; + + default: + break; + } + event->SetHandled(); + + // TabDragContext gets event capture as soon as a drag session begins, which + // precludes TabStrip from ever getting events like tap or long tap. Forward + // this on to TabStrip so it can respond to those events. + tab_strip_->OnGestureEvent(event); + } + + bool IsDragStarted() const { + return drag_controller_ && drag_controller_->started_drag(); + } + + void TabWasAdded() { + if (drag_controller_) + drag_controller_->TabWasAdded(); + } + + void OnTabWillBeRemoved(content::WebContents* contents) { + if (drag_controller_) + drag_controller_->OnTabWillBeRemoved(contents); + } + + bool CanRemoveTabIfDragging(content::WebContents* contents) const { + return drag_controller_ ? drag_controller_->CanRemoveTabDuringDrag(contents) + : true; + } + + void MaybeStartDrag(TabSlotView* source, + const ui::LocatedEvent& event, + const ui::ListSelectionModel& original_selection) { + std::vector dragging_views; + int x = source->GetMirroredXInView(event.x()); + int y = event.y(); + + // Build the set of selected tabs to drag and calculate the offset from the + // source. + ui::ListSelectionModel selection_model; + if (source->GetTabSlotViewType() == + TabSlotView::ViewType::kTabGroupHeader) { + dragging_views.push_back(source); + + const gfx::Range grouped_tabs = + tab_strip_->controller_->ListTabsInGroup(source->group().value()); + for (auto index = grouped_tabs.start(); index < grouped_tabs.end(); + ++index) { + dragging_views.push_back(GetTabAt(index)); + // Set |selection_model| if and only if the original selection does not + // match the group exactly. See TabDragController::Init() for details + // on how |selection_model| is used. + if (!original_selection.IsSelected(index)) + selection_model = original_selection; + } + if (grouped_tabs.length() != original_selection.size()) + selection_model = original_selection; + } else { + for (int i = 0; i < GetTabCount(); ++i) { + Tab* other_tab = GetTabAt(i); + if (tab_strip_->IsTabSelected(other_tab)) { + dragging_views.push_back(other_tab); + if (other_tab == source) + x += GetSizeNeededForViews(dragging_views) - other_tab->width(); + } + } + if (!original_selection.IsSelected( + tab_strip_->GetModelIndexOf(source).value())) + selection_model = original_selection; + } + + DCHECK(!dragging_views.empty()); + DCHECK(base::Contains(dragging_views, source)); + + // Delete the existing DragController before creating a new one. We do this + // as creating the DragController remembers the WebContents delegates and we + // need to make sure the existing DragController isn't still a delegate. + drag_controller_.reset(); + + DCHECK(event.type() == ui::ET_MOUSE_PRESSED || + event.type() == ui::ET_GESTURE_TAP_DOWN || + event.type() == ui::ET_GESTURE_SCROLL_BEGIN); + + drag_controller_ = std::make_unique(); + drag_controller_->Init(this, source, dragging_views, gfx::Point(x, y), + event.x(), std::move(selection_model), + EventSourceFromEvent(event)); + + UpdateDragEventSourceCrashKey(drag_controller_->event_source()); + if (drag_controller_set_callback_) + std::move(drag_controller_set_callback_).Run(drag_controller_.get()); + } + + void ContinueDrag(views::View* view, const ui::LocatedEvent& event) { + if (drag_controller_.get() && + drag_controller_->event_source() == EventSourceFromEvent(event)) { + gfx::Point screen_location(event.location()); + views::View::ConvertPointToScreen(view, &screen_location); + + // Note: |tab_strip_| can be destroyed during drag, also destroying + // |this|. + base::WeakPtr weak_ptr(weak_factory_.GetWeakPtr()); + drag_controller_->Drag(screen_location); + + if (!weak_ptr) + return; + } + } + + bool EndDrag(EndDragReason reason) { + if (!drag_controller_.get()) + return false; + bool started_drag = drag_controller_->started_drag(); + drag_controller_->EndDrag(reason); + return started_drag; + } + + bool IsTabStripCloseable() const { + // Allow the close in two scenarios: + // . The user is not actively dragging the tabstrip. + // . In the process of reverting the drag, and the last tab is being + // removed (so that it can be inserted back into the source tabstrip). + return !IsDragSessionActive() || + drag_controller_->IsRemovingLastTabForRevert(); + } + + // TabDragContext: + Tab* GetTabAt(int i) const override { return tab_strip_->tab_at(i); } + + absl::optional GetIndexOf(const TabSlotView* view) const override { + return tab_strip_->GetModelIndexOf(view); + } + + int GetTabCount() const override { return tab_strip_->GetTabCount(); } + + bool IsTabPinned(const Tab* tab) const override { + return tab_strip_->IsTabPinned(tab); + } + + int GetPinnedTabCount() const override { + return tab_strip_->GetModelPinnedTabCount(); + } + + TabGroupHeader* GetTabGroupHeader( + const tab_groups::TabGroupId& group) const override { + return tab_strip_->group_header(group); + } + + TabStripModel* GetTabStripModel() override { + return static_cast( + tab_strip_->controller_.get()) + ->model(); + } + + TabDragController* GetDragController() override { + return drag_controller_.get(); + } + + void OwnDragController( + std::unique_ptr controller) override { + DCHECK(controller); + DCHECK(!drag_controller_); + drag_controller_ = std::move(controller); + if (drag_controller_set_callback_) + std::move(drag_controller_set_callback_).Run(drag_controller_.get()); + } + + void DestroyDragController() override { drag_controller_.reset(); } + + std::unique_ptr ReleaseDragController() override { + return std::move(drag_controller_); + } + + void SetDragControllerCallbackForTesting( + base::OnceCallback callback) override { + drag_controller_set_callback_ = std::move(callback); + } + + void UpdateAnimationTarget(TabSlotView* tab_slot_view, + const gfx::Rect& target_bounds) override { + if (bounds_animator_.IsAnimating(tab_slot_view)) + bounds_animator_.SetTargetBounds(tab_slot_view, target_bounds); + } + + bool IsDragSessionActive() const override { + return drag_controller_ != nullptr; + } + + bool IsAnimatingDragEnd() const override { + // The drag is ending if we're animating tabs back to the TabContainer, or + // if the TabDragController is in the kStopped state. + return (drag_controller_ == nullptr && bounds_animator_.IsAnimating()) || + (drag_controller_ && !drag_controller_->active()); + } + + void CompleteEndDragAnimations() override { + // Finishing animations will return tabs to the TabContainer via + // ResetDraggingStateDelegate::AnimationEnded. + bounds_animator_.Complete(); + } + + bool IsActiveDropTarget() const override { + for (int i = 0; i < GetTabCount(); ++i) { + const Tab* const tab = GetTabAt(i); + if (tab->dragging()) + return true; + } + return false; + } + + int GetActiveTabWidth() const override { + return tab_strip_->GetActiveTabWidth(); + } + + int GetTabDragAreaWidth() const override { + // There are two cases here (with tab scrolling enabled): + // 1) If the tab strip is not wider than the tab strip region (and thus + // not scrollable), returning the available width for tabs rather than the + // actual width for tabs allows tabs to be dragged past the current bounds + // of the tabstrip, anywhere along the tab strip region. + // N.B. The available width for tabs in this case needs to ignore tab + // closing mode. + // 2) If the tabstrip is wider than the tab strip region (and thus is + // scrollable), returning the tabstrip width allows tabs to be dragged + // anywhere within the tabstrip, not just in the leftmost region of it. + return std::max( + tab_strip_->tab_container_->GetAvailableWidthForTabContainer(), + tab_strip_->width()); + } + + int TabDragAreaBeginX() const override { + return tab_strip_->GetMirroredXWithWidthInView(0, GetTabDragAreaWidth()); + } + + int TabDragAreaEndX() const override { + return TabDragAreaBeginX() + GetTabDragAreaWidth(); + } + + int GetHorizontalDragThreshold() const override { + constexpr int kHorizontalMoveThreshold = 16; // DIPs. + + double ratio = static_cast(tab_strip_->GetInactiveTabWidth()) / + TabStyle::GetStandardWidth(); + return base::ClampRound(ratio * kHorizontalMoveThreshold); + } + + int GetInsertionIndexForDraggedBounds( + const gfx::Rect& dragged_bounds, + std::vector dragged_views, + int num_dragged_tabs, + absl::optional group) const override { + // If the strip has no tabs, the only position to insert at is 0. + if (!GetTabCount()) + return 0; + + // If we're dragging a group by its header, the first element of + // |dragged_views| is a group header, and the second one is the first tab + // in that group. + const int first_dragged_tab_model_index = + tab_strip_->GetModelIndexOf(dragged_views[group.has_value() ? 1 : 0]) + .value(); + const int index = + CalculateInsertionIndex(dragged_bounds, first_dragged_tab_model_index, + num_dragged_tabs, std::move(group)); + + const Tab* last_visible_tab = tab_strip_->GetLastVisibleTab(); + int last_insertion_point = + last_visible_tab ? (GetIndexOf(last_visible_tab).value() + 1) : 0; + + // Clamp the insertion point to keep it within the visible region. + last_insertion_point = std::max(0, last_insertion_point - num_dragged_tabs); + + // Ensure the first dragged tab always stays in the visible index range. + return std::min(index, last_insertion_point); + } + + std::vector CalculateBoundsForDraggedViews( + const std::vector& views) override { + DCHECK(!views.empty()); + + std::vector bounds; + const int overlap = TabStyle::GetTabOverlap(); + int x = 0; + for (const TabSlotView* view : views) { + const int width = view->width(); + bounds.emplace_back(x, 0, width, view->height()); + x += width - overlap; + } + + return bounds; + } + + void SetBoundsForDrag(const std::vector& views, + const std::vector& bounds) override { + tab_strip_->tab_container_->CancelAnimation(); + DCHECK_EQ(views.size(), bounds.size()); + for (size_t i = 0; i < views.size(); ++i) + views[i]->SetBoundsRect(bounds[i]); + + // Ensure that the tab strip and its parent views are correctly re-laid out + // after repositioning dragged tabs. This avoids visual/layout issues such + // as https://crbug.com/1151092. + PreferredSizeChanged(); + + // Reset the layout size as we've effectively laid out a different size. + // This ensures a layout happens after the drag is done. + tab_strip_->tab_container_->InvalidateIdealBounds(); + if (views.at(0)->group().has_value()) + tab_strip_->tab_container_->UpdateTabGroupVisuals( + views.at(0)->group().value()); + } + + void StartedDragging(const std::vector& views) override { + // Let the controller know that the user started dragging tabs. + tab_strip_->controller_->OnStartedDragging( + views.size() == static_cast(tab_strip_->GetModelCount())); + + // No tabs should be dragging at this point. + for (int i = 0; i < GetTabCount(); ++i) + DCHECK(!GetTabAt(i)->dragging()); + + tab_strip_->tab_container_->CompleteAnimationAndLayout(); + + for (TabSlotView* dragged_view : views) { + AddChildView(dragged_view); + dragged_view->set_dragging(true); + } + + // If this is a header drag, start painting the group highlight. + TabGroupHeader* header = views::AsViewClass(views[0]); + if (header) { + tab_strip_->tab_container_->GetGroupViews(header->group().value()) + ->highlight() + ->SetVisible(true); + // Make sure the bounds of the group views are up to date right now + // instead of waiting for subsequent drag events - if we are dragging a + // window by a group header, we won't get any more events. See + // https://crbug.com/1344774. + tab_strip_->tab_container_->UpdateTabGroupVisuals( + header->group().value()); + } + + tab_strip_->tab_container_->SetTabSlotVisibility(); + tab_strip_->SchedulePaint(); + } + + void DraggedTabsDetached() override { + // Let the controller know that the user is not dragging this tabstrip's + // tabs anymore. + tab_strip_->controller_->OnStoppedDragging(); + } + + void StoppedDragging(const std::vector& views) override { + // Let the controller know that the user stopped dragging tabs. + tab_strip_->controller_->OnStoppedDragging(); + UpdateDragEventSourceCrashKey({}); + + // Animate the dragged views to their ideal positions. We'll hand them back + // to TabContainer when the animation ends. + for (TabSlotView* view : views) { + gfx::Rect ideal_bounds; + + TabGroupHeader* header = views::AsViewClass(view); + if (header) { + // Disable the group highlight now that the drag is ended. + tab_strip_->tab_container_->GetGroupViews(header->group().value()) + ->highlight() + ->SetVisible(false); + ideal_bounds = + tab_strip_->tab_container_->GetIdealBounds(header->group().value()); + } else { + ideal_bounds = tab_strip_->tab_container_->GetIdealBounds( + tab_strip_->GetModelIndexOf(view).value()); + } + + bounds_animator_.AnimateViewTo( + view, ideal_bounds, + std::make_unique( + *tab_strip_->tab_container_, *view)); + } + } + + void LayoutDraggedViewsAt(const std::vector& views, + TabSlotView* source_view, + const gfx::Point& location, + bool initial_drag) override { + std::vector bounds = CalculateBoundsForDraggedViews(views); + DCHECK_EQ(views.size(), bounds.size()); + + // The index of `source_view` in the TabStrip's viewmodel. + absl::optional source_view_model_index = GetIndexOf(source_view); + // The index of `source_view` as a child of this TabDragContext. + int source_view_index = static_cast( + base::ranges::find(views, source_view) - views.begin()); + + const auto should_animate_tab = [=, &views, this](int index_in_views) { + // If the tab at `index_in_views` is already animating, don't interrupt + // it. + if (bounds_animator_.IsAnimating(views[index_in_views])) + return true; + + // If `source_view_model_index` is nullopt, we are dragging by a header, + // so the tabs are guaranteed to be consecutive already. + if (!source_view_model_index.has_value()) + return false; + + // If the tab isn't at the right model index relative to `source_view`, + // animate it into position. + const int consecutive_model_index = + source_view_model_index.value() - + (source_view_index - static_cast(index_in_views)); + return initial_drag && + GetIndexOf(views[index_in_views]) != consecutive_model_index; + }; + + for (size_t i = 0; i < views.size(); ++i) { + TabSlotView* view = views[i]; + gfx::Rect new_bounds = bounds[i]; + new_bounds.Offset(location.x(), location.y()); + if (should_animate_tab(i)) { + bounds_animator_.SetTargetBounds(views[i], new_bounds); + } else { + view->SetBoundsRect(new_bounds); + } + } + tab_strip_->tab_container_->SetTabSlotVisibility(); + // The rightmost dragged tab may have moved, which would change our + // preferred width. + PreferredSizeChanged(); + + // If the dragged tabs are in a group, we need to update the bounds of the + // corresponding underline and header. + if (views[0]->group()) { + tab_strip_->tab_container_->UpdateTabGroupVisuals( + views[0]->group().value()); + } + } + + // Forces the entire tabstrip to lay out. + void ForceLayout() override { + tab_strip_->InvalidateLayout(); + tab_strip_->tab_container_->CompleteAnimationAndLayout(); + } + + void PaintChildren(const views::PaintInfo& paint_info) override { + std::vector orderable_children; + for (views::View* child : children()) + orderable_children.emplace_back(child); + + // Sort in ascending order by z-value. Stable sort breaks ties by child + // index. + std::stable_sort(orderable_children.begin(), orderable_children.end()); + + for (const ZOrderableTabContainerElement& child : orderable_children) + child.view()->Paint(paint_info); + } + + void OnBoundsAnimatorProgressed(views::BoundsAnimator* animator) override {} + void OnBoundsAnimatorDone(views::BoundsAnimator* animator) override { + // Send the Container a message to simulate a mouse moved event at the + // current mouse position. This tickles the Tab the mouse is currently over + // to show the "hot" state of the close button, or to show the hover card, + // etc. Note that this is not required (and indeed may crash!) during a + // drag session. + if (!IsDragSessionActive()) { + // The widget can apparently be null during shutdown. + views::Widget* widget = GetWidget(); + if (widget) + widget->SynthesizeMouseMoveEvent(); + } + } + + views::ScrollView* GetScrollView() override { + return views::ScrollView::GetScrollViewForContents(tab_strip_); + } + + private: + // Animates tabs after a drag has ended, then hands them back to + // |tab_container_|. + class ResetDraggingStateDelegate : public gfx::AnimationDelegate { + public: + ResetDraggingStateDelegate(TabContainer& tab_container, + TabSlotView& slot_view) + : tab_container_(tab_container), slot_view_(slot_view) { + slot_view_->set_animating(true); + } + ResetDraggingStateDelegate(const ResetDraggingStateDelegate&) = delete; + ResetDraggingStateDelegate& operator=(const ResetDraggingStateDelegate&) = + delete; + ~ResetDraggingStateDelegate() override = default; + + void AnimationProgressed(const gfx::Animation* animation) override { + tab_container_->OnTabSlotAnimationProgressed( + base::to_address(slot_view_)); + } + + void AnimationEnded(const gfx::Animation* animation) override { + AnimationProgressed(animation); + slot_view_->set_animating(false); + slot_view_->set_dragging(false); + tab_container_->ReturnTabSlotView(base::to_address(slot_view_)); + } + + void AnimationCanceled(const gfx::Animation* animation) override { + AnimationEnded(animation); + } + + private: + const raw_ref tab_container_; + const raw_ref slot_view_; + }; + + // Determines the index to move the dragged tabs to. The dragged tabs must + // already be in the tabstrip. |dragged_bounds| is the union of the bounds + // of the dragged tabs and group header, if any. |first_dragged_tab_index| is + // the current model index in this tabstrip of the first dragged tab. The + // dragged tabs must be in the tabstrip already! + int CalculateInsertionIndex( + const gfx::Rect& dragged_bounds, + int first_dragged_tab_index, + int num_dragged_tabs, + absl::optional dragged_group) const { + // This method assumes that the dragged tabs and group are already in the + // tabstrip (i.e. it doesn't support attaching a drag to a new tabstrip). + // This assumption is critical because it means that tab width won't change + // after this method's recommendation is implemented. + + // For each possible insertion index, determine what the ideal bounds of + // the dragged tabs would be at that index. This corresponds to where they + // would slide to if the drag session ended now. We want to insert at the + // index that minimizes the distance between the corresponding ideal bounds + // and the current bounds of the tabs. This is equivalent to minimizing: + // - the distance of the aforementioned slide, + // - the width of the gaps in the tabstrip, or + // - the amount of tab overlap. + int min_distance_index = -1; + int min_distance = std::numeric_limits::max(); + for (int candidate_index = 0; candidate_index <= GetTabCount(); + ++candidate_index) { + if (!IsValidInsertionIndex(candidate_index, first_dragged_tab_index, + num_dragged_tabs, dragged_group)) { + continue; + } + + // If there's a group header here, and we're dragging a group, we might + // end up on either side of that header. Check both cases to find the + // best option. + // TODO(tbergquist): Use this approach to determine if a tab should be + // added to the group. This is calculated elsewhere and may require some + // plumbing and/or duplicated code. + const int left_ideal_x = CalculateIdealX( + candidate_index, first_dragged_tab_index, dragged_bounds); + const int left_distance = std::abs(dragged_bounds.x() - left_ideal_x); + + const int right_ideal_x = + left_ideal_x + CalculateIdealXAdjustmentIfAddedToGroup( + candidate_index, dragged_group); + const int right_distance = std::abs(dragged_bounds.x() - right_ideal_x); + + const int distance = std::min(left_distance, right_distance); + if (distance < min_distance) { + min_distance = distance; + min_distance_index = candidate_index; + } + } + + CHECK_NE(min_distance_index, -1); + + // When moving a tab within a tabstrip, the target index is expressed as if + // the tabs are not in the tabstrip, i.e. it acts like the tabs are first + // removed and then re-inserted at the target index. We need to adjust the + // target index to account for this. + if (min_distance_index > first_dragged_tab_index) + min_distance_index -= num_dragged_tabs; + + return min_distance_index; + } + + // Dragging can't insert tabs into some indices. + bool IsValidInsertionIndex( + int candidate_index, + int first_dragged_tab_index, + int num_dragged_tabs, + absl::optional dragged_group) const { + if (candidate_index == 0) + return true; + + // If |candidate_index| is right after one of the tabs we're dragging, + // inserting here would be nonsensical - we can't insert the dragged tabs + // into the middle of the dragged tabs. That's just silly. + if (candidate_index > first_dragged_tab_index && + candidate_index <= first_dragged_tab_index + num_dragged_tabs) { + return false; + } + + // This might be in the middle of a group, which may or may not be fine. + absl::optional left_group = + GetTabAt(candidate_index - 1)->group(); + absl::optional right_group = + tab_strip_->IsValidModelIndex(candidate_index) + ? GetTabAt(candidate_index)->group() + : absl::nullopt; + if (left_group.has_value() && left_group == right_group) { + // Can't drag a group into another group. + if (dragged_group.has_value()) + return false; + // Can't drag a tab into a collapsed group. + if (tab_strip_->IsGroupCollapsed(left_group.value())) + return false; + } + + return true; + } + + // Determines the x position that the dragged tabs would have if they were + // inserted at |candidate_index|. If there's a group header at that index, + // this assumes the dragged tabs *would not* be inserted into the group, + // and would therefore end up to the left of that header. + int CalculateIdealX(int candidate_index, + int first_dragged_tab_index, + gfx::Rect dragged_bounds) const { + if (candidate_index == 0) + return 0; + + const int tab_overlap = TabStyle::GetTabOverlap(); + + // We'll insert just right of the tab at |candidate_index| - 1. + int ideal_x = + tab_strip_->tab_container_->GetIdealBounds(candidate_index - 1).right(); + + // If the dragged tabs are currently left of |candidate_index|, moving + // them to |candidate_index| would move the tab at |candidate_index| - 1 + // to the left by |num_dragged_tabs| slots. This would change the ideal x + // for the dragged tabs, as well, by the width of the dragged tabs. + if (candidate_index - 1 > first_dragged_tab_index) + ideal_x -= dragged_bounds.width() - tab_overlap; + + return ideal_x - tab_overlap; + } + + // There might be a group starting at |candidate_index|. If there is, + // this determines how the ideal x would change if the dragged tabs were + // added to that group, thereby moving them to that header's right. + int CalculateIdealXAdjustmentIfAddedToGroup( + int candidate_index, + absl::optional dragged_group) const { + // If the tab to the right of |candidate_index| is the first tab in a + // (non-collapsed) group, we are sharing this model index with a group + // header. We might end up on either side of it, so we need to check + // both positions. + if (!dragged_group.has_value() && + tab_strip_->IsValidModelIndex(candidate_index)) { + absl::optional left_group = + tab_strip_->IsValidModelIndex(candidate_index - 1) + ? GetTabAt(candidate_index - 1)->group() + : absl::nullopt; + absl::optional right_group = + GetTabAt(candidate_index)->group(); + if (right_group.has_value() && left_group != right_group) { + if (tab_strip_->IsGroupCollapsed(right_group.value())) + return 0; + const int header_width = + GetTabGroupHeader(*right_group)->bounds().width() - + TabStyle::GetTabOverlap(); + return header_width; + } + } + + return 0; + } + + const raw_ptr tab_strip_; + + // Responsible for animating tabs during drag sessions. + views::BoundsAnimator bounds_animator_; + + // The controller for a drag initiated from a Tab. Valid for the lifetime of + // the drag session. + std::unique_ptr drag_controller_; + + // Only used in tests. + base::OnceCallback drag_controller_set_callback_; + + base::WeakPtrFactory weak_factory_{this}; +}; + +BEGIN_METADATA(TabStrip, TabDragContextImpl, views::View); +END_METADATA + +/////////////////////////////////////////////////////////////////////////////// +// TabStrip, public: + +TabStrip::TabStrip(std::unique_ptr controller) + : controller_(std::move(controller)), + hover_card_controller_(std::make_unique(this)), + drag_context_(*AddChildView(std::make_unique(this))), + tab_container_( + *AddChildViewAt(MakeTabContainer(this, + hover_card_controller_.get(), + base::to_address(drag_context_)), + 0)) { + // TODO(pbos): This is probably incorrect, the background of individual tabs + // depend on their selected state. This should probably be pushed down into + // tabs. + views::SetCascadingColorProviderColor(this, views::kCascadingBackgroundColor, + kColorToolbar); + Init(); + + SetProperty(views::kElementIdentifierKey, kTabStripElementId); +} + +TabStrip::~TabStrip() { + // Eliminate the hover card first to avoid order-of-operation issues. + hover_card_controller_.reset(); + + // Disengage the drag controller before doing any additional cleanup. This + // call can interact with child views so we can't reliably do it during member + // destruction. + // End any ongoing drag session. + drag_context_->DestroyDragController(); + // Immediately clean up that drag session instead of allowing things to + // animate back into place over time. + drag_context_->CompleteEndDragAnimations(); + + // |tab_container_|'s tabs may call back to us or to |drag_context_| from + // their destructors. Delete them first so that if they call back we aren't in + // a weird state. + RemoveChildViewT(base::to_address(tab_container_)); + RemoveChildViewT(base::to_address(drag_context_)); + + CHECK(!IsInObserverList()); +} + +void TabStrip::SetAvailableWidthCallback( + base::RepeatingCallback available_width_callback) { + tab_container_->SetAvailableWidthCallback(available_width_callback); +} + +// static +int TabStrip::GetSizeNeededForViews(const std::vector& views) { + int width = 0; + for (const TabSlotView* view : views) + width += view->width(); + if (!views.empty()) + width -= TabStyle::GetTabOverlap() * (views.size() - 1); + return width; +} + +void TabStrip::AddObserver(TabStripObserver* observer) { + observers_.AddObserver(observer); +} + +void TabStrip::RemoveObserver(TabStripObserver* observer) { + observers_.RemoveObserver(observer); +} + +void TabStrip::SetBackgroundOffset(int background_offset) { + if (background_offset == background_offset_) + return; + background_offset_ = background_offset; + OnPropertyChanged(&background_offset_, views::kPropertyEffectsPaint); +} + +bool TabStrip::IsRectInWindowCaption(const gfx::Rect& rect) { + // `rect` is in the window caption if it doesn't hit any content area. + return !tab_container_->IsRectInContentArea(rect); +} + +bool TabStrip::IsTabStripCloseable() const { + return drag_context_->IsTabStripCloseable(); +} + +bool TabStrip::IsTabStripEditable() const { + return !drag_context_->IsDragSessionActive() && + !drag_context_->IsActiveDropTarget(); +} + +bool TabStrip::IsTabCrashed(int tab_index) const { + return tab_at(tab_index)->data().IsCrashed(); +} + +bool TabStrip::TabHasNetworkError(int tab_index) const { + return tab_at(tab_index)->data().network_state == TabNetworkState::kError; +} + +absl::optional TabStrip::GetTabAlertState(int tab_index) const { + return Tab::GetAlertStateToShow(tab_at(tab_index)->data().alert_state); +} + +void TabStrip::UpdateLoadingAnimations(const base::TimeDelta& elapsed_time) { + for (int i = 0; i < GetTabCount(); i++) + tab_at(i)->StepLoadingAnimation(elapsed_time); +} + +void TabStrip::AddTabAt(int model_index, TabRendererData data) { + DCHECK(IsValidModelIndex(model_index)); + + const bool pinned = data.pinned; + Tab* tab = tab_container_->AddTab( + std::make_unique(this), model_index, + pinned ? TabPinned::kPinned : TabPinned::kUnpinned); + + tab->set_context_menu_controller(&context_menu_controller_); + tab->AddObserver(this); + selected_tabs_.IncrementFrom(model_index); + + // Setting data must come after all state from the model has been updated + // above for the tab. Accessibility, in particular, reacts to data changed + // callbacks. + tab->SetData(std::move(data)); + + for (TabStripObserver& observer : observers_) + observer.OnTabAdded(model_index); + + // At the start of AddTabAt() the model and tabs are out sync. Any queries to + // find a tab given a model index can go off the end of |tabs_|. As such, it + // is important that we complete the drag *after* adding the tab so that the + // model and tabstrip are in sync. + drag_context_->TabWasAdded(); + + Profile* profile = controller_->GetProfile(); + if (profile) { + if (profile->IsGuestSession()) + base::UmaHistogramCounts100("Tab.Count.Guest", GetTabCount()); + else if (profile->IsIncognitoProfile()) + base::UmaHistogramCounts100("Tab.Count.Incognito", GetTabCount()); + } + + if (new_tab_button_pressed_start_time_.has_value()) { + base::UmaHistogramTimes( + "TabStrip.TimeToCreateNewTabFromPress", + base::TimeTicks::Now() - new_tab_button_pressed_start_time_.value()); + new_tab_button_pressed_start_time_.reset(); + } + + LogTabWidthsForTabScrolling(); +} + +void TabStrip::MoveTab(int from_model_index, + int to_model_index, + TabRendererData data) { + DCHECK_GT(GetTabCount(), 0); + + Tab* moving_tab = tab_at(from_model_index); + moving_tab->SetData(std::move(data)); + + tab_container_->MoveTab(from_model_index, to_model_index); + + selected_tabs_.Move(from_model_index, to_model_index, /*length=*/1); + + for (TabStripObserver& observer : observers_) + observer.OnTabMoved(from_model_index, to_model_index); +} + +void TabStrip::RemoveTabAt(content::WebContents* contents, + int model_index, + bool was_active) { + // OnTabWillBeRemoved should have ended any ongoing drags containing + // `contents` already - unless the call is coming from inside the house! (i.e. + // the TabDragController is doing the removing as part of reverting a drag) + DCHECK(drag_context_->CanRemoveTabIfDragging(contents)); + tab_container_->RemoveTab(model_index, was_active); + + UpdateHoverCard(nullptr, HoverCardUpdateType::kTabRemoved); + + selected_tabs_.DecrementFrom(model_index); + + for (TabStripObserver& observer : observers_) + observer.OnTabRemoved(model_index); +} + +void TabStrip::OnTabWillBeRemoved(content::WebContents* contents, + int model_index) { + drag_context_->OnTabWillBeRemoved(contents); +} + +void TabStrip::SetTabData(int model_index, TabRendererData data) { + Tab* tab = tab_at(model_index); + const bool pinned = data.pinned; + const bool pinned_state_changed = tab->data().pinned != pinned; + tab->SetData(std::move(data)); + + if (HoverCardIsShowingForTab(tab)) + UpdateHoverCard(tab, HoverCardUpdateType::kTabDataChanged); + + if (pinned_state_changed) + tab_container_->SetTabPinned( + model_index, pinned ? TabPinned::kPinned : TabPinned::kUnpinned); +} + +void TabStrip::AddTabToGroup(absl::optional group, + int model_index) { + tab_at(model_index)->set_group(group); + + // Expand the group if the tab that is getting grouped is the active tab. This + // can result in the group expanding in a series of actions where the final + // active tab is not in the group. + if (static_cast(model_index) == selected_tabs_.active() && + group.has_value() && IsGroupCollapsed(group.value())) { + ToggleTabGroupCollapsedState( + group.value(), ToggleTabGroupCollapsedStateOrigin::kImplicitAction); + } + + if (group.has_value()) + tab_container_->ExitTabClosingMode(); +} + +void TabStrip::OnGroupCreated(const tab_groups::TabGroupId& group) { + tab_container_->OnGroupCreated(group); +} + +void TabStrip::OnGroupEditorOpened(const tab_groups::TabGroupId& group) { + tab_container_->OnGroupEditorOpened(group); +} + +void TabStrip::OnGroupContentsChanged(const tab_groups::TabGroupId& group) { + tab_container_->OnGroupContentsChanged(group); +} + +void TabStrip::OnGroupVisualsChanged( + const tab_groups::TabGroupId& group, + const tab_groups::TabGroupVisualData* old_visuals, + const tab_groups::TabGroupVisualData* new_visuals) { + tab_container_->OnGroupVisualsChanged(group, old_visuals, new_visuals); +} + +void TabStrip::ToggleTabGroup(const tab_groups::TabGroupId& group, + bool is_collapsing, + ToggleTabGroupCollapsedStateOrigin origin) { + tab_container_->ToggleTabGroup(group, is_collapsing, origin); +} + +void TabStrip::OnGroupMoved(const tab_groups::TabGroupId& group) { + tab_container_->OnGroupMoved(group); +} + +void TabStrip::OnGroupClosed(const tab_groups::TabGroupId& group) { + tab_container_->OnGroupClosed(group); +} + +bool TabStrip::ShouldDrawStrokes() const { + // If the controller says we can't draw strokes, don't. + if (!controller_->CanDrawStrokes()) + return false; + + // The tabstrip normally avoids strokes and relies on the active tab + // contrasting sufficiently with the frame background. When there isn't + // enough contrast, fall back to a stroke. Always compute the contrast ratio + // against the active frame color, to avoid toggling the stroke on and off as + // the window activation state changes. + constexpr float kMinimumContrastRatioForOutlines = 1.3f; + const SkColor background_color = GetTabBackgroundColor( + TabActive::kActive, BrowserFrameActiveState::kActive); + const SkColor frame_color = + controller_->GetFrameColor(BrowserFrameActiveState::kActive); + const float contrast_ratio = + color_utils::GetContrastRatio(background_color, frame_color); + if (contrast_ratio < kMinimumContrastRatioForOutlines) + return true; + + // Don't want to have to run a full feature query every time this function is + // called. + static const bool tab_outlines_in_low_contrast = true; + if (tab_outlines_in_low_contrast) { + constexpr float kMinimumAbsoluteContrastForOutlines = 0.2f; + const float background_luminance = + color_utils::GetRelativeLuminance(background_color); + const float frame_luminance = + color_utils::GetRelativeLuminance(frame_color); + const float contrast_difference = + std::fabs(background_luminance - frame_luminance); + if (contrast_difference < kMinimumAbsoluteContrastForOutlines) + return true; + } + + return false; +} + +void TabStrip::SetSelection(const ui::ListSelectionModel& new_selection) { + DCHECK(new_selection.active().has_value()) + << "We should never transition to a state where no tab is active."; + Tab* const new_active_tab = tab_at(new_selection.active().value()); + Tab* const old_active_tab = selected_tabs_.active().has_value() + ? tab_at(selected_tabs_.active().value()) + : nullptr; + + if (new_active_tab != old_active_tab) { + if (old_active_tab) + old_active_tab->ActiveStateChanged(); + + new_active_tab->ActiveStateChanged(); + + tab_container_->SetActiveTab(selected_tabs_.active(), + new_selection.active()); + } + + for (int selection : new_selection.selected_indices()) { + Tab* const selected_tab = tab_at(selection); + if (selected_tab->group().has_value()) { + const tab_groups::TabGroupId new_group = selected_tab->group().value(); + // If the tab that is about to be selected is in a collapsed group, + // automatically expand the group. + if (IsGroupCollapsed(new_group)) { + ToggleTabGroupCollapsedState( + new_group, ToggleTabGroupCollapsedStateOrigin::kImplicitAction); + } + } + } + + // Use STLSetDifference to get the indices of elements newly selected + // and no longer selected, since selected_indices() is always sorted. + ui::ListSelectionModel::SelectedIndices no_longer_selected = + base::STLSetDifference( + selected_tabs_.selected_indices(), new_selection.selected_indices()); + ui::ListSelectionModel::SelectedIndices newly_selected = + base::STLSetDifference( + new_selection.selected_indices(), selected_tabs_.selected_indices()); + + new_active_tab->NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true); + selected_tabs_ = new_selection; + + UpdateHoverCard(nullptr, HoverCardUpdateType::kSelectionChanged); + + // Notify all tabs whose selected state changed. + for (auto tab_index : + base::STLSetUnion( + no_longer_selected, newly_selected)) { + tab_at(tab_index)->SelectedStateChanged(); + } +} + +void TabStrip::ScrollTowardsTrailingTabs(int offset) { + tab_container_->ScrollTabContainerByOffset(offset); +} + +void TabStrip::ScrollTowardsLeadingTabs(int offset) { + tab_container_->ScrollTabContainerByOffset(-offset); +} + +void TabStrip::OnWidgetActivationChanged(views::Widget* widget, bool active) { + if (active && selected_tabs_.active().has_value()) { + // When the browser window is activated, fire a selection event on the + // currently active tab, to help enable per-tab modes in assistive + // technologies. + tab_at(selected_tabs_.active().value()) + ->NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true); + } + UpdateHoverCard(nullptr, HoverCardUpdateType::kEvent); +} + +void TabStrip::SetTabNeedsAttention(int model_index, bool attention) { + tab_at(model_index)->SetTabNeedsAttention(attention); +} + +absl::optional TabStrip::GetModelIndexOf(const TabSlotView* view) const { + const absl::optional viewmodel_index = + tab_container_->GetModelIndexOf(view); + + // TODO(1392523): The viewmodel (as accessed by + // `tab_container_->GetModelIndexOf(Tab*)`) can be out of sync with the actual + // TabStripModel when multiple tabs are closed at once. We can check + // IsValidModelIndex to avoid crashes or out of bounds issues, but we can't + // avoid returning incorrect indices from this method in that context. + if (viewmodel_index.has_value() && + !IsValidModelIndex(viewmodel_index.value())) + return absl::nullopt; + return viewmodel_index; +} + +int TabStrip::GetTabCount() const { + return tab_container_->GetTabCount(); +} + +int TabStrip::GetModelCount() const { + return controller_->GetCount(); +} + +int TabStrip::GetModelPinnedTabCount() const { + for (size_t i = 0; i < static_cast(controller_->GetCount()); ++i) { + if (!controller_->IsTabPinned(static_cast(i))) + return static_cast(i); + } + + // All tabs are pinned. + return controller_->GetCount(); +} + +TabDragContext* TabStrip::GetDragContext() { + return base::to_address(drag_context_); +} + +bool TabStrip::IsAnimating() const { + return tab_container_->IsAnimating() || drag_context_->IsAnimatingDragEnd(); +} + +void TabStrip::StopAnimating(bool layout) { + if (layout) { + tab_container_->CompleteAnimationAndLayout(); + } else { + tab_container_->CancelAnimation(); + } +} + +absl::optional TabStrip::GetFocusedTabIndex() const { + for (int i = 0; i < GetTabCount(); ++i) { + if (tab_at(i)->HasFocus()) + return i; + } + return absl::nullopt; +} + +views::View* TabStrip::GetTabViewForPromoAnchor(int index_hint) { + return tab_at(base::clamp(index_hint, 0, GetTabCount() - 1)); +} + +views::View* TabStrip::GetDefaultFocusableChild() { + const absl::optional active = GetActiveIndex(); + return active.has_value() ? tab_at(active.value()) : nullptr; +} + +bool TabStrip::IsValidModelIndex(int index) const { + return controller_->IsValidIndex(index); +} + +absl::optional TabStrip::GetActiveIndex() const { + return controller_->GetActiveIndex(); +} + +int TabStrip::NumPinnedTabsInModel() const { + for (size_t i = 0; i < static_cast(controller_->GetCount()); ++i) { + if (!controller_->IsTabPinned(static_cast(i))) + return static_cast(i); + } + + // All tabs are pinned. + return controller_->GetCount(); +} + +void TabStrip::OnDropIndexUpdate(const absl::optional index, + const bool drop_before) { + controller_->OnDropIndexUpdate(index, drop_before); +} + +absl::optional TabStrip::GetFirstTabInGroup( + const tab_groups::TabGroupId& group) const { + return controller_->GetFirstTabInGroup(group); +} + +gfx::Range TabStrip::ListTabsInGroup( + const tab_groups::TabGroupId& group) const { + return controller_->ListTabsInGroup(group); +} + +bool TabStrip::CanExtendDragHandle() const { + return !controller_->IsFrameCondensed() && + !controller_->EverHasVisibleBackgroundTabShapes(); +} + +const views::View* TabStrip::GetTabClosingModeMouseWatcherHostView() const { + return this; +} + +bool TabStrip::IsAnimatingInTabStrip() const { + return IsAnimating(); +} + +void TabStrip::UpdateAnimationTarget(TabSlotView* tab_slot_view, + gfx::Rect target_bounds) { + // TODO(1116121): This may need to do coordinate space transformations if the + // view hierarchy changes so `tab_container_` and `drag_context_` don't share + // spaces. + drag_context_->UpdateAnimationTarget(tab_slot_view, target_bounds); +} + +bool TabStrip::IsGroupCollapsed(const tab_groups::TabGroupId& group) const { + return controller_->IsGroupCollapsed(group); +} + +const ui::ListSelectionModel& TabStrip::GetSelectionModel() const { + return controller_->GetSelectionModel(); +} + +Tab* TabStrip::tab_at(int index) const { + return tab_container_->GetTabAtModelIndex(index); +} + +void TabStrip::SelectTab(Tab* tab, const ui::Event& event) { + const absl::optional maybe_model_index = GetModelIndexOf(tab); + if (!maybe_model_index.has_value()) + return; + + const int model_index = maybe_model_index.value(); + + if (!tab->IsActive()) { + base::UmaHistogramEnumeration("TabStrip.Tab.Views.ActivationAction", + TabActivationTypes::kTab); + + if (tab->group().has_value()) { + base::RecordAction(base::UserMetricsAction("TabGroups_SwitchGroupedTab")); + } + } + + controller_->SelectTab(model_index, event); +} + +void TabStrip::ExtendSelectionTo(Tab* tab) { + absl::optional model_index = GetModelIndexOf(tab); + if (model_index.has_value()) + controller_->ExtendSelectionTo(model_index.value()); +} + +void TabStrip::ToggleSelected(Tab* tab) { + absl::optional model_index = GetModelIndexOf(tab); + if (model_index.has_value()) + controller_->ToggleSelected(model_index.value()); +} + +void TabStrip::AddSelectionFromAnchorTo(Tab* tab) { + absl::optional model_index = GetModelIndexOf(tab); + if (model_index.has_value()) + controller_->AddSelectionFromAnchorTo(model_index.value()); +} + +void TabStrip::CloseTab(Tab* tab, CloseTabSource source) { + const absl::optional index_to_close = + tab_container_->GetModelIndexOfFirstNonClosingTab(tab); + + if (index_to_close.has_value()) + CloseTabInternal(index_to_close.value(), source); +} + +void TabStrip::ToggleTabAudioMute(Tab* tab) { + absl::optional model_index = GetModelIndexOf(tab); + if (model_index.has_value()) + controller_->ToggleTabAudioMute(model_index.value()); +} + +void TabStrip::ShiftTabNext(Tab* tab) { + ShiftTabRelative(tab, 1); +} + +void TabStrip::ShiftTabPrevious(Tab* tab) { + ShiftTabRelative(tab, -1); +} + +void TabStrip::MoveTabFirst(Tab* tab) { + if (tab->closing()) + return; + + const absl::optional start_index = GetModelIndexOf(tab); + if (!start_index.has_value()) + return; + + int target_index = 0; + if (!controller_->IsTabPinned(start_index.value())) { + while (target_index < start_index && controller_->IsTabPinned(target_index)) + ++target_index; + } + + if (!IsValidModelIndex(target_index)) + return; + + if (target_index != start_index) + controller_->MoveTab(start_index.value(), target_index); + + // The tab may unintentionally land in the first group in the tab strip, so we + // remove the group to ensure consistent behavior. Even if the tab is already + // at the front, it should "move" out of its current group. + if (tab->group().has_value()) + controller_->RemoveTabFromGroup(target_index); + + GetViewAccessibility().AnnounceText( + l10n_util::GetStringUTF16(IDS_TAB_AX_ANNOUNCE_MOVED_FIRST)); +} + +void TabStrip::MoveTabLast(Tab* tab) { + if (tab->closing()) + return; + + const absl::optional maybe_start_index = GetModelIndexOf(tab); + if (!maybe_start_index.has_value()) + return; + + const int start_index = maybe_start_index.value(); + + int target_index; + if (controller_->IsTabPinned(start_index)) { + int temp_index = start_index + 1; + while (temp_index < GetTabCount() && controller_->IsTabPinned(temp_index)) + ++temp_index; + target_index = temp_index - 1; + } else { + target_index = GetTabCount() - 1; + } + + if (!IsValidModelIndex(target_index)) + return; + + if (target_index != start_index) + controller_->MoveTab(start_index, target_index); + + // The tab may unintentionally land in the last group in the tab strip, so we + // remove the group to ensure consistent behavior. Even if the tab is already + // at the back, it should "move" out of its current group. + if (tab->group().has_value()) + controller_->RemoveTabFromGroup(target_index); + + GetViewAccessibility().AnnounceText( + l10n_util::GetStringUTF16(IDS_TAB_AX_ANNOUNCE_MOVED_LAST)); +} + +void TabStrip::ToggleTabGroupCollapsedState( + const tab_groups::TabGroupId group, + ToggleTabGroupCollapsedStateOrigin origin) { + int tab_count = GetTabCount(); + controller_->ToggleTabGroupCollapsedState(group, origin); + // If tab count changed, all tab groups are collapsed and we have + // created a new tab. We need to exit closing mode to resize the new + // tab immediately. + // TODO(crbug/1384151): This should be captured along with the + // ToggleTabGroup logic, so other callers to + // TabStripController::ToggleTabGroupCollapsedState see the same + // behavior. + if (tab_count != GetTabCount()) { + tab_container_->ExitTabClosingMode(); + } +} + +void TabStrip::NotifyTabGroupEditorBubbleOpened() { + tab_container_->NotifyTabGroupEditorBubbleOpened(); +} +void TabStrip::NotifyTabGroupEditorBubbleClosed() { + tab_container_->NotifyTabGroupEditorBubbleClosed(); +} + +void TabStrip::ShowContextMenuForTab(Tab* tab, + const gfx::Point& p, + ui::MenuSourceType source_type) { + controller_->ShowContextMenuForTab(tab, p, source_type); +} + +bool TabStrip::IsActiveTab(const Tab* tab) const { + absl::optional model_index = GetModelIndexOf(tab); + return model_index.has_value() && + controller_->IsActiveTab(model_index.value()); +} + +bool TabStrip::IsTabSelected(const Tab* tab) const { + absl::optional model_index = GetModelIndexOf(tab); + return model_index.has_value() && + controller_->IsTabSelected(model_index.value()); +} + +bool TabStrip::IsTabPinned(const Tab* tab) const { + absl::optional model_index = GetModelIndexOf(tab); + return model_index.has_value() && + controller_->IsTabPinned(model_index.value()); +} + +bool TabStrip::IsTabFirst(const Tab* tab) const { + return GetModelIndexOf(tab) == 0; +} + +bool TabStrip::IsFocusInTabs() const { + return GetFocusManager() && Contains(GetFocusManager()->GetFocusedView()); +} + +void TabStrip::MaybeStartDrag( + TabSlotView* source, + const ui::LocatedEvent& event, + const ui::ListSelectionModel& original_selection) { + // Don't accidentally start any drag operations during animations if the + // mouse is down... during an animation tabs are being resized automatically, + // so the View system can misinterpret this easily if the mouse is down that + // the user is dragging. + if (IsAnimating() || controller_->HasAvailableDragActions() == 0) + return; + + // Check that the source is either a valid tab or a tab group header, which + // are the only valid drag targets. + DCHECK(GetModelIndexOf(source).has_value() || + source->GetTabSlotViewType() == + TabSlotView::ViewType::kTabGroupHeader); + + drag_context_->MaybeStartDrag(source, event, original_selection); +} + +void TabStrip::ContinueDrag(views::View* view, const ui::LocatedEvent& event) { + drag_context_->ContinueDrag(view, event); +} + +bool TabStrip::EndDrag(EndDragReason reason) { + return drag_context_->EndDrag(reason); +} + +Tab* TabStrip::GetTabAt(const gfx::Point& point) { + views::View* view = GetEventHandlerForPoint(point); + if (!view) + return nullptr; // No tab contains the point. + + // Walk up the view hierarchy until we find a tab, or the TabStrip. + while (view && view != this && view->GetID() != VIEW_ID_TAB) + view = view->parent(); + + return view && view->GetID() == VIEW_ID_TAB ? static_cast(view) + : nullptr; +} + +const Tab* TabStrip::GetAdjacentTab(const Tab* tab, int offset) { + const absl::optional tab_index = GetModelIndexOf(tab); + if (!tab_index.has_value()) + return nullptr; + const int adjacent_index = tab_index.value() + offset; + return IsValidModelIndex(adjacent_index) ? tab_at(adjacent_index) : nullptr; +} + +void TabStrip::OnMouseEventInTab(views::View* source, + const ui::MouseEvent& event) { + // Record time from cursor entering the tabstrip to first tap on a tab to + // switch. + if (mouse_entered_tabstrip_time_.has_value() && + event.type() == ui::ET_MOUSE_PRESSED && views::IsViewClass(source)) { + UMA_HISTOGRAM_MEDIUM_TIMES( + "TabStrip.TimeToSwitch", + base::TimeTicks::Now() - mouse_entered_tabstrip_time_.value()); + mouse_entered_tabstrip_time_.reset(); + } +} + +void TabStrip::UpdateHoverCard(Tab* tab, HoverCardUpdateType update_type) { + tab_container_->UpdateHoverCard(tab, update_type); +} + +bool TabStrip::ShowDomainInHoverCards() const { +#if BUILDFLAG(IS_CHROMEOS_ASH) + const auto* app_controller = GetBrowser()->app_controller(); + if (app_controller && app_controller->system_app()) + return false; +#endif + return true; +} + +bool TabStrip::HoverCardIsShowingForTab(Tab* tab) { + return hover_card_controller_ && + hover_card_controller_->IsHoverCardShowingForTab(tab); +} + +int TabStrip::GetBackgroundOffset() const { + return background_offset_; +} + +int TabStrip::GetStrokeThickness() const { + return ShouldDrawStrokes() ? 1 : 0; +} + +bool TabStrip::CanPaintThrobberToLayer() const { + // Disable layer-painting of throbbers if dragging or if any tab animation is + // in progress. Also disable in fullscreen: when "immersive" the tab strip + // could be sliding in or out; for other modes, there's no tab strip. + const bool dragging = drag_context_->IsDragStarted(); + const views::Widget* widget = GetWidget(); + return widget && !dragging && !IsAnimating() && !widget->IsFullscreen(); +} + +bool TabStrip::HasVisibleBackgroundTabShapes() const { + return controller_->HasVisibleBackgroundTabShapes(); +} + +bool TabStrip::ShouldPaintAsActiveFrame() const { + return controller_->ShouldPaintAsActiveFrame(); +} + +SkColor TabStrip::GetTabSeparatorColor() const { + return separator_color_; +} + +SkColor TabStrip::GetTabBackgroundColor( + TabActive active, + BrowserFrameActiveState active_state) const { + const auto* cp = GetColorProvider(); + if (!cp) + return gfx::kPlaceholderColor; + + constexpr ChromeColorIds kColorIds[2][2] = { + {kColorTabBackgroundInactiveFrameInactive, + kColorTabBackgroundInactiveFrameActive}, + {kColorTabBackgroundActiveFrameInactive, + kColorTabBackgroundActiveFrameActive}}; + + using State = BrowserFrameActiveState; + const bool tab_active = active == TabActive::kActive; + const bool frame_active = + (active_state == State::kActive) || + ((active_state == State::kUseCurrent) && ShouldPaintAsActiveFrame()); + return cp->GetColor(kColorIds[tab_active][frame_active]); +} + +SkColor TabStrip::GetTabForegroundColor(TabActive active) const { + const ui::ColorProvider* cp = GetColorProvider(); + if (!cp) + return gfx::kPlaceholderColor; + + constexpr ChromeColorIds kColorIds[2][2] = { + {kColorTabForegroundInactiveFrameInactive, + kColorTabForegroundInactiveFrameActive}, + {kColorTabForegroundActiveFrameInactive, + kColorTabForegroundActiveFrameActive}}; + + const bool tab_active = active == TabActive::kActive; + const bool frame_active = ShouldPaintAsActiveFrame(); + return cp->GetColor(kColorIds[tab_active][frame_active]); +} + +// Returns the accessible tab name for the tab. +std::u16string TabStrip::GetAccessibleTabName(const Tab* tab) const { + return GetModelIndexOf(tab).has_value() + ? controller_->GetAccessibleTabName(tab) + : std::u16string(); +} + +absl::optional TabStrip::GetCustomBackgroundId( + BrowserFrameActiveState active_state) const { + if (!TitlebarBackgroundIsTransparent()) + return controller_->GetCustomBackgroundId(active_state); + + constexpr int kBackgroundIdGlass = IDR_THEME_TAB_BACKGROUND_V; + return GetThemeProvider()->HasCustomImage(kBackgroundIdGlass) + ? absl::make_optional(kBackgroundIdGlass) + : absl::nullopt; +} + +float TabStrip::GetHoverOpacityForTab(float range_parameter) const { + return gfx::Tween::FloatValueBetween(range_parameter, hover_opacity_min_, + hover_opacity_max_); +} + +float TabStrip::GetHoverOpacityForRadialHighlight() const { + return radial_highlight_opacity_; +} + +std::u16string TabStrip::GetGroupTitle( + const tab_groups::TabGroupId& group) const { + return controller_->GetGroupTitle(group); +} + +std::u16string TabStrip::GetGroupContentString( + const tab_groups::TabGroupId& group) const { + return controller_->GetGroupContentString(group); +} +tab_groups::TabGroupColorId TabStrip::GetGroupColorId( + const tab_groups::TabGroupId& group) const { + return controller_->GetGroupColorId(group); +} + +SkColor TabStrip::GetPaintedGroupColor( + const tab_groups::TabGroupColorId& color_id) const { + return GetColorProvider()->GetColor( + GetTabGroupTabStripColorId(color_id, ShouldPaintAsActiveFrame())); +} + +void TabStrip::ShiftGroupLeft(const tab_groups::TabGroupId& group) { + ShiftGroupRelative(group, -1); +} + +void TabStrip::ShiftGroupRight(const tab_groups::TabGroupId& group) { + ShiftGroupRelative(group, 1); +} + +const Browser* TabStrip::GetBrowser() const { + return controller_->GetBrowser(); +} + +/////////////////////////////////////////////////////////////////////////////// +// TabStrip, views::View overrides: + +views::SizeBounds TabStrip::GetAvailableSize(const views::View* child) const { + // We can only reach here if SetAvailableWidthCallback() was never called, + // e.g. if tab scrolling is disabled. Defer to our parent. + DCHECK(child == base::to_address(tab_container_)); + return parent()->GetAvailableSize(this); +} + +gfx::Size TabStrip::GetMinimumSize() const { + // `tab_container_` and `drag_context_` overlap (both share TabStrip's + // origin), so we need to be able to cover the union of their bounds. + gfx::Size min_size = tab_container_->GetMinimumSize(); + min_size.SetToMax(drag_context_->GetMinimumSize()); + + return min_size; +} + +gfx::Size TabStrip::CalculatePreferredSize() const { + // `tab_container_` and `drag_context_` overlap (both share TabStrip's + // origin), so we need to be able to cover the union of their bounds. + gfx::Size preferred_size = tab_container_->GetPreferredSize(); + preferred_size.SetToMax(drag_context_->GetPreferredSize()); + + return preferred_size; +} + +void TabStrip::Layout() { + if (base::FeatureList::IsEnabled(features::kScrollableTabStrip)) { + // With tab scrolling, the TabStrip is the contents view of a ScrollView and + // as such is expected to set its own bounds during layout. + // (With great sizing power comes great sizing responsibility). + + // We should never be larger than our preferred width. + const int max_width = GetPreferredSize().width(); + // We should never be smaller than our minimum width. + const int min_width = GetMinimumSize().width(); + // If we can, we should fit within the tab strip region to avoid scrolling. + const int available_width = + tab_container_->GetAvailableWidthForTabContainer(); + // Be as wide as possible subject to the above constraints. + const int width = std::min(max_width, std::max(min_width, available_width)); + SetBounds(0, 0, width, GetLayoutConstant(TAB_HEIGHT)); + } + + if (tab_container_->bounds() != GetLocalBounds()) { + tab_container_->SetBoundsRect(GetLocalBounds()); + } else { + // We still need to layout in this case, as the available width may have + // changed, which can change layout outcomes (e.g. affecting tab + // visibility). See https://crbug.com/1370459. + // TODO(crbug.com/1371301): TabContainer should observe available width + // changes and invalidate its layout when needed. + tab_container_->Layout(); + } + drag_context_->SetBoundsRect(GetLocalBounds()); +} + +void TabStrip::ChildPreferredSizeChanged(views::View* child) { + PreferredSizeChanged(); +} + +BrowserRootView::DropIndex TabStrip::GetDropIndex( + const ui::DropTargetEvent& event) { + // BrowserView should talk directly to |tab_container_| instead of asking us. + NOTREACHED_NORETURN(); +} + +BrowserRootView::DropTarget* TabStrip::GetDropTarget( + gfx::Point loc_in_local_coords) { + return tab_container_->GetDropTarget(loc_in_local_coords); +} + +views::View* TabStrip::GetViewForDrop() { + // BrowserView should talk directly to |tab_container_| instead of asking us. + NOTREACHED_NORETURN(); +} + +/////////////////////////////////////////////////////////////////////////////// +// TabStrip, private: + +void TabStrip::Init() { + SetID(VIEW_ID_TAB_STRIP); + // So we only get enter/exit messages when the mouse enters/exits the whole + // tabstrip, even if it is entering/exiting a specific Tab, too. + SetNotifyEnterExitOnChild(true); +} + +void TabStrip::NewTabButtonPressed(const ui::Event& event) { + new_tab_button_pressed_start_time_ = base::TimeTicks::Now(); + + base::RecordAction(base::UserMetricsAction("NewTab_Button")); + UMA_HISTOGRAM_ENUMERATION("Tab.NewTab", NewTabTypes::NEW_TAB_BUTTON, + NewTabTypes::NEW_TAB_ENUM_COUNT); + if (event.IsMouseEvent()) { + // Prevent the hover card from popping back in immediately. This forces a + // normal fade-in. + if (hover_card_controller_) + hover_card_controller_->PreventImmediateReshow(); + + const ui::MouseEvent& mouse = static_cast(event); + if (mouse.IsOnlyMiddleMouseButton()) { + if (ui::Clipboard::IsSupportedClipboardBuffer( + ui::ClipboardBuffer::kSelection)) { + ui::Clipboard* clipboard = ui::Clipboard::GetForCurrentThread(); + CHECK(clipboard); + std::u16string clipboard_text; + clipboard->ReadText(ui::ClipboardBuffer::kSelection, + /* data_dst = */ nullptr, &clipboard_text); + if (!clipboard_text.empty()) + controller_->CreateNewTabWithLocation(clipboard_text); + } + return; + } + } + + controller_->CreateNewTab(); + if (event.type() == ui::ET_GESTURE_TAP) + TouchUMA::RecordGestureAction(TouchUMA::kGestureNewTabTap); +} + +bool TabStrip::ShouldHighlightCloseButtonAfterRemove() { + return tab_container_->InTabClose(); +} + +bool TabStrip::TitlebarBackgroundIsTransparent() const { +#if BUILDFLAG(IS_WIN) + return false; +#else + return GetWidget()->ShouldWindowContentsBeTransparent(); +#endif // BUILDFLAG(IS_WIN) +} + +int TabStrip::GetActiveTabWidth() const { + return tab_container_->GetActiveTabWidth(); +} + +int TabStrip::GetInactiveTabWidth() const { + return tab_container_->GetInactiveTabWidth(); +} + +const Tab* TabStrip::GetLastVisibleTab() const { + for (int i = GetTabCount() - 1; i >= 0; --i) { + const Tab* tab = tab_at(i); + + // The tab is marked not visible in a collapsed group, but is "visible" in + // the tabstrip if the header is visible. + if (tab->GetVisible() || + (tab->group().has_value() && + group_header(tab->group().value())->GetVisible())) { + return tab; + } + } + // While in normal use the tabstrip should always be wide enough to have at + // least one visible tab, it can be zero-width in tests, meaning we get here. + return nullptr; +} + +void TabStrip::CloseTabInternal(int model_index, CloseTabSource source) { + if (!IsValidModelIndex(model_index)) + return; + + // If we're not allowed to close this tab for whatever reason, we should not + // proceed. + if (!controller_->BeforeCloseTab(model_index, source)) + return; + + if (!tab_container_->InTabClose() && IsAnimating()) { + // Cancel any current animations. We do this as remove uses the current + // ideal bounds and we need to know ideal bounds is in a good state. + tab_container_->CompleteAnimationAndLayout(); + } + + if (GetWidget()) { + // Enter tab closing mode now, but wait to calculate the width constraint + // until RemoveTabAt() is called, since there are code paths that go through + // RemoveTabAt() but not this method that must also set that constraint. + tab_container_->EnterTabClosingMode(absl::nullopt, source); + } + + UpdateHoverCard(nullptr, HoverCardUpdateType::kTabRemoved); + if (tab_at(model_index)->group().has_value()) + base::RecordAction(base::UserMetricsAction("CloseGroupedTab")); + controller_->CloseTab(model_index); +} + +void TabStrip::UpdateContrastRatioValues() { + // There may be no controller in unit tests, and the call to + // GetTabBackgroundColor() below requires one, so bail early if it is absent. + if (!controller_) + return; + + const SkColor inactive_bg = GetTabBackgroundColor( + TabActive::kInactive, BrowserFrameActiveState::kUseCurrent); + const auto get_blend = [inactive_bg](SkColor target, float contrast) { + return color_utils::BlendForMinContrast(inactive_bg, inactive_bg, target, + contrast); + }; + + const SkColor active_bg = GetTabBackgroundColor( + TabActive::kActive, BrowserFrameActiveState::kUseCurrent); + const auto get_hover_opacity = [active_bg, &get_blend](float contrast) { + return get_blend(active_bg, contrast).alpha / 255.0f; + }; + + // The contrast ratio for the hover effect on standard-width tabs. + // In the default color scheme, this corresponds to a hover opacity of 0.4. + constexpr float kStandardWidthContrast = 1.11f; + hover_opacity_min_ = get_hover_opacity(kStandardWidthContrast); + + // The contrast ratio for the hover effect on min-width tabs. + // In the default color scheme, this corresponds to a hover opacity of 0.65. + constexpr float kMinWidthContrast = 1.19f; + hover_opacity_max_ = get_hover_opacity(kMinWidthContrast); + + // The contrast ratio for the radial gradient effect on hovered tabs. + // In the default color scheme, this corresponds to a hover opacity of 0.45. + constexpr float kRadialGradientContrast = 1.13728f; + radial_highlight_opacity_ = get_hover_opacity(kRadialGradientContrast); + + const SkColor inactive_fg = GetTabForegroundColor(TabActive::kInactive); + // The contrast ratio for the separator between inactive tabs. + constexpr float kTabSeparatorContrast = 2.5f; + separator_color_ = get_blend(inactive_fg, kTabSeparatorContrast).color; + + SchedulePaint(); +} + +void TabStrip::ShiftTabRelative(Tab* tab, int offset) { + DCHECK_EQ(1, std::abs(offset)); + const absl::optional maybe_start_index = GetModelIndexOf(tab); + if (!maybe_start_index.has_value()) + return; + + const int start_index = maybe_start_index.value(); + int target_index = start_index + offset; + + if (tab->closing()) + return; + + const auto old_group = tab->group(); + if (!IsValidModelIndex(target_index) || + controller_->IsTabPinned(start_index) != + controller_->IsTabPinned(target_index)) { + // Even if we've reached the boundary of where the tab could go, it may + // still be able to "move" out of its current group. + if (old_group.has_value()) { + AnnounceTabRemovedFromGroup(old_group.value()); + controller_->RemoveTabFromGroup(start_index); + } + return; + } + + // If the tab is at a group boundary and the group is expanded, instead of + // actually moving the tab just change its group membership. + absl::optional target_group = + tab_at(target_index)->group(); + if (old_group != target_group) { + if (old_group.has_value()) { + AnnounceTabRemovedFromGroup(old_group.value()); + controller_->RemoveTabFromGroup(start_index); + return; + } else if (target_group.has_value()) { + // If the tab is at a group boundary and the group is collapsed, treat the + // collapsed group as a tab and find the next available slot for the tab + // to move to. + if (IsGroupCollapsed(target_group.value())) { + int candidate_index = target_index + offset; + while (IsValidModelIndex(candidate_index) && + tab_at(candidate_index)->group() == target_group) { + candidate_index += offset; + } + if (IsValidModelIndex(candidate_index)) { + target_index = candidate_index - offset; + } else { + target_index = offset < 0 ? 0 : GetModelCount() - 1; + } + } else { + // Read before adding the tab to the group so that the group description + // isn't the tab we just added. + AnnounceTabAddedToGroup(target_group.value()); + controller_->AddTabToGroup(start_index, target_group.value()); + views::ElementTrackerViews::GetInstance()->NotifyCustomEvent( + kTabGroupedCustomEventId, tab); + return; + } + } + } + + controller_->MoveTab(start_index, target_index); + GetViewAccessibility().AnnounceText(l10n_util::GetStringUTF16( + ((offset > 0) ^ base::i18n::IsRTL()) ? IDS_TAB_AX_ANNOUNCE_MOVED_RIGHT + : IDS_TAB_AX_ANNOUNCE_MOVED_LEFT)); +} + +void TabStrip::ShiftGroupRelative(const tab_groups::TabGroupId& group, + int offset) { + DCHECK_EQ(1, std::abs(offset)); + gfx::Range tabs_in_group = controller_->ListTabsInGroup(group); + + const int start_index = tabs_in_group.start(); + int target_index = start_index + offset; + + if (offset > 0) + target_index += tabs_in_group.length() - 1; + + if (!IsValidModelIndex(start_index) || !IsValidModelIndex(target_index)) + return; + + // Avoid moving into the middle of another group by accounting for its size. + absl::optional target_group = + tab_at(target_index)->group(); + if (target_group.has_value()) { + target_index += + offset * + (controller_->ListTabsInGroup(target_group.value()).length() - 1); + } + + if (!IsValidModelIndex(target_index)) + return; + + if (controller_->IsTabPinned(start_index) != + controller_->IsTabPinned(target_index)) + return; + + controller_->MoveGroup(group, target_index); +} + +void TabStrip::LogTabWidthsForTabScrolling() { + int active_tab_width = GetActiveTabWidth(); + int inactive_tab_width = GetInactiveTabWidth(); + + if (active_tab_width > 1) { + UMA_HISTOGRAM_EXACT_LINEAR("Tabs.ActiveTabWidth", active_tab_width, 257); + } + if (inactive_tab_width > 1) { + UMA_HISTOGRAM_EXACT_LINEAR("Tabs.InactiveTabWidth", inactive_tab_width, + 257); + } +} + +// TabStrip:TabContextMenuController: +// ---------------------------------------------------------- + +TabStrip::TabContextMenuController::TabContextMenuController(TabStrip* parent) + : parent_(parent) {} + +void TabStrip::TabContextMenuController::ShowContextMenuForViewImpl( + views::View* source, + const gfx::Point& point, + ui::MenuSourceType source_type) { + // We are only intended to be installed as a context-menu handler for tabs, so + // this cast should be safe. + DCHECK(views::IsViewClass(source)); + Tab* const tab = static_cast(source); + if (tab->closing()) + return; + parent_->ShowContextMenuForTab(tab, point, source_type); +} + +void TabStrip::OnMouseEntered(const ui::MouseEvent& event) { + mouse_entered_tabstrip_time_ = base::TimeTicks::Now(); +} + +void TabStrip::OnMouseExited(const ui::MouseEvent& event) { + UpdateHoverCard(nullptr, HoverCardUpdateType::kHover); +} + +void TabStrip::AddedToWidget() { + GetWidget()->AddObserver(this); + paint_as_active_subscription_ = + GetWidget()->RegisterPaintAsActiveChangedCallback(base::BindRepeating( + &TabStrip::UpdateContrastRatioValues, base::Unretained(this))); +} + +void TabStrip::RemovedFromWidget() { + GetWidget()->RemoveObserver(this); + paint_as_active_subscription_ = {}; +} + +void TabStrip::OnThemeChanged() { + views::View::OnThemeChanged(); + UpdateContrastRatioValues(); +} + +void TabStrip::OnGestureEvent(ui::GestureEvent* event) { + switch (event->type()) { + case ui::ET_GESTURE_LONG_TAP: { + tab_container_->HandleLongTap(event); + break; + } + + case ui::ET_GESTURE_TAP: { + const absl::optional active_index = GetActiveIndex(); + Tab* active_tab = tab_at(active_index.value()); + TouchUMA::GestureActionType action = TouchUMA::kGestureTabNoSwitchTap; + if (active_tab->tab_activated_with_last_tap_down()) + action = TouchUMA::kGestureTabSwitchTap; + TouchUMA::RecordGestureAction(action); + break; + } + + default: + break; + } + event->SetHandled(); +} + +void TabStrip::OnViewFocused(views::View* observed_view) { + TabSlotView* slot_view = views::AsViewClass(observed_view); + if (!slot_view) + return; + + absl::optional index = GetModelIndexOf(slot_view); + if (index.has_value()) + controller_->OnKeyboardFocusedTabChanged(index); +} + +void TabStrip::OnViewBlurred(views::View* observed_view) { + controller_->OnKeyboardFocusedTabChanged(absl::nullopt); +} + +void TabStrip::OnTouchUiChanged() { + tab_container_->CompleteAnimationAndLayout(); + PreferredSizeChanged(); +} + +void TabStrip::AnnounceTabAddedToGroup(tab_groups::TabGroupId group_id) { + const std::u16string group_title = GetGroupTitle(group_id); + const std::u16string contents_string = GetGroupContentString(group_id); + GetViewAccessibility().AnnounceText( + group_title.empty() + ? l10n_util::GetStringFUTF16( + IDS_TAB_AX_ANNOUNCE_TAB_ADDED_TO_UNNAMED_GROUP, contents_string) + : l10n_util::GetStringFUTF16( + IDS_TAB_AX_ANNOUNCE_TAB_ADDED_TO_NAMED_GROUP, group_title, + contents_string)); +} + +void TabStrip::AnnounceTabRemovedFromGroup(tab_groups::TabGroupId group_id) { + const std::u16string group_title = GetGroupTitle(group_id); + const std::u16string contents_string = GetGroupContentString(group_id); + GetViewAccessibility().AnnounceText( + group_title.empty() + ? l10n_util::GetStringFUTF16( + IDS_TAB_AX_ANNOUNCE_TAB_REMOVED_FROM_UNNAMED_GROUP, + contents_string) + : l10n_util::GetStringFUTF16( + IDS_TAB_AX_ANNOUNCE_TAB_REMOVED_FROM_NAMED_GROUP, group_title, + contents_string)); +} + +BEGIN_METADATA(TabStrip, views::View) +ADD_PROPERTY_METADATA(int, BackgroundOffset) +ADD_READONLY_PROPERTY_METADATA(int, TabCount) +ADD_READONLY_PROPERTY_METADATA(int, ModelCount) +ADD_READONLY_PROPERTY_METADATA(int, ModelPinnedTabCount) +ADD_READONLY_PROPERTY_METADATA(absl::optional, FocusedTabIndex) +ADD_READONLY_PROPERTY_METADATA(int, StrokeThickness) +ADD_READONLY_PROPERTY_METADATA(SkColor, + TabSeparatorColor, + ui::metadata::SkColorConverter) +ADD_READONLY_PROPERTY_METADATA(float, HoverOpacityForRadialHighlight) +ADD_READONLY_PROPERTY_METADATA(int, ActiveTabWidth) +ADD_READONLY_PROPERTY_METADATA(int, InactiveTabWidth) +END_METADATA diff --git a/src/chrome/browser/ui/views/tabs/tab_style_views.cc b/src/chrome/browser/ui/views/tabs/tab_style_views.cc new file mode 100644 index 000000000..668c40032 --- /dev/null +++ b/src/chrome/browser/ui/views/tabs/tab_style_views.cc @@ -0,0 +1,1056 @@ +// Copyright 2023 The Chromium Authors, Alex313031 +// 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_style_views.h" + +#include +#include + +#include "base/cxx17_backports.h" +#include "base/i18n/rtl.h" +#include "base/memory/raw_ptr.h" +#include "base/numerics/safe_conversions.h" +#include "base/strings/strcat.h" +#include "cc/paint/paint_record.h" +#include "cc/paint/paint_shader.h" +#include "chrome/browser/themes/theme_properties.h" +#include "chrome/browser/ui/color/chrome_color_id.h" +#include "chrome/browser/ui/layout_constants.h" +#include "chrome/browser/ui/tabs/tab_types.h" +#include "chrome/browser/ui/ui_features.h" +#include "chrome/browser/ui/views/frame/browser_non_client_frame_view.h" +#include "chrome/browser/ui/views/tabs/glow_hover_controller.h" +#include "chrome/browser/ui/views/tabs/tab.h" +#include "chrome/browser/ui/views/tabs/tab_close_button.h" +#include "chrome/browser/ui/views/tabs/tab_group_underline.h" +#include "chrome/browser/ui/views/tabs/tab_slot_controller.h" +#include "chrome/grit/theme_resources.h" +#include "components/tab_groups/tab_group_visual_data.h" +#include "third_party/skia/include/core/SkRRect.h" +#include "third_party/skia/include/core/SkScalar.h" +#include "third_party/skia/include/pathops/SkPathOps.h" +#include "ui/base/theme_provider.h" +#include "ui/gfx/canvas.h" +#include "ui/gfx/font_list.h" +#include "ui/gfx/geometry/skia_conversions.h" +#include "ui/gfx/scoped_canvas.h" +#include "ui/views/controls/focus_ring.h" +#include "ui/views/style/typography.h" +#include "ui/views/widget/widget.h" + +namespace { +// How the tab shape path is modified for selected tabs. +using ShapeModifier = int; +// No modification should be done. +constexpr ShapeModifier kNone = 0x00; +// Exclude the lower left arc. +constexpr ShapeModifier kNoLowerLeftArc = 0x01; +// Exclude the lower right arc. +constexpr ShapeModifier kNoLowerRightArc = 0x02; + +// Tab style implementation for the GM2 refresh (Chrome 69). +class GM2TabStyle : public TabStyleViews { + public: + explicit GM2TabStyle(Tab* tab); + GM2TabStyle(const GM2TabStyle&) = delete; + GM2TabStyle& operator=(const GM2TabStyle&) = delete; + + protected: + // TabStyle: + SkPath GetPath( + PathType path_type, + float scale, + bool force_active = false, + RenderUnits render_units = RenderUnits::kPixels) const override; + gfx::Insets GetContentsInsets() const override; + float GetZValue() const override; + float GetActiveOpacity() const override; + TabActive GetApparentActiveState() const override; + TabStyle::TabColors CalculateColors() const override; + const gfx::FontList& GetFontList() const override; + void PaintTab(gfx::Canvas* canvas) const override; + void SetHoverLocation(const gfx::Point& location) override; + void ShowHover(ShowHoverStyle style) override; + void HideHover(HideHoverStyle style) override; + + private: + // Gets the bounds for the leading and trailing separators for a tab. + SeparatorBounds GetSeparatorBounds(float scale) const; + + // Returns the opacities of the separators. If |for_layout| is true, returns + // the "layout" opacities, which ignore the effects of surrounding tabs' hover + // effects and consider only the current tab's state. + SeparatorOpacities GetSeparatorOpacities(bool for_layout) const; + + // Returns a single separator's opacity based on whether it is the + // logically |leading| separator. |for_layout| has the same meaning as in + // GetSeparatorOpacities(). + float GetSeparatorOpacity(bool for_layout, bool leading) const; + + // Helper that returns an interpolated opacity if the tab or its neighbor + // |other_tab| is mid-hover-animation. Used in almost all cases when a + // separator is shown, since hovering is independent of tab state. + // |for_layout| has the same meaning as in GetSeparatorOpacities(). + float GetHoverInterpolatedSeparatorOpacity(bool for_layout, + const Tab* other_tab) const; + + // Returns whether we shoould extend the hit test region for Fitts' Law. + bool ShouldExtendHitTest() const; + + // Returns whether the hover animation is being shown. + bool IsHoverActive() const; + + // Returns the progress (0 to 1) of the hover animation. + double GetHoverAnimationValue() const; + + // Returns the opacity of the hover effect that should be drawn, which may not + // be the same as GetHoverAnimationValue. + float GetHoverOpacity() const; + + // Gets the throb value. A value of 0 indicates no throbbing. + float GetThrobValue() const; + + // Returns the thickness of the stroke drawn around the top and sides of the + // tab. Only active tabs may have a stroke, and not in all cases. If there + // is no stroke, returns 0. If |should_paint_as_active| is true, the tab is + // treated as an active tab regardless of its true current state. + int GetStrokeThickness(bool should_paint_as_active = false) const; + + bool ShouldPaintTabBackgroundColor(TabActive active, + bool has_custom_background) const; + + SkColor GetTabBackgroundColor(TabActive active) const; + + // When selected, non-active, non-hovered tabs are adjacent to each other, + // there are anti-aliasing artifacts in the overlapped lower arc region. This + // returns how to modify the tab shape to eliminate the lower arcs on the + // right or left based on the state of the adjacent tab(s). + ShapeModifier GetShapeModifier(PathType path_type) const; + + // Painting helper functions: + void PaintInactiveTabBackground(gfx::Canvas* canvas) const; + void PaintTabBackground(gfx::Canvas* canvas, + TabActive active, + absl::optional fill_id, + int y_inset) const; + void PaintTabBackgroundFill(gfx::Canvas* canvas, + TabActive active, + bool paint_hover_effect, + absl::optional fill_id, + int y_inset) const; + void PaintBackgroundStroke(gfx::Canvas* canvas, + TabActive active, + SkColor stroke_color) const; + void PaintSeparators(gfx::Canvas* canvas) const; + + // Given a tab of width |width|, returns the radius to use for the corners. + static float GetTopCornerRadiusForWidth(int width); + + // Scales |bounds| by scale and aligns so that adjacent tabs meet up exactly + // during painting. + static gfx::RectF ScaleAndAlignBounds(const gfx::Rect& bounds, + float scale, + int stroke_thickness); + + const raw_ptr tab_; + + std::unique_ptr hover_controller_; + gfx::FontList normal_font_; + gfx::FontList heavy_font_; +}; + +void DrawHighlight(gfx::Canvas* canvas, + const SkPoint& p, + SkScalar radius, + SkColor color) { + // TODO(crbug/1308932): Remove FromColor and make all SkColor4f. + const SkColor4f colors[2] = { + SkColor4f::FromColor(color), + SkColor4f::FromColor(SkColorSetA(color, SK_AlphaTRANSPARENT))}; + cc::PaintFlags flags; + flags.setAntiAlias(true); + flags.setShader(cc::PaintShader::MakeRadialGradient( + p, radius, colors, nullptr, 2, SkTileMode::kClamp)); + canvas->sk_canvas()->drawRect( + SkRect::MakeXYWH(p.x() - radius, p.y() - radius, radius * 2, radius * 2), + flags); +} + +// Updates a target value, returning true if it changed. +template +bool UpdateValue(T* dest, const T& src) { + if (*dest == src) + return false; + *dest = src; + return true; +} + +// GM2TabStyle ----------------------------------------------------------------- + +GM2TabStyle::GM2TabStyle(Tab* tab) + : tab_(tab), + hover_controller_(gfx::Animation::ShouldRenderRichAnimation() + ? new GlowHoverController(tab) + : nullptr), + normal_font_(views::style::GetFont(views::style::CONTEXT_LABEL, + views::style::STYLE_PRIMARY)), + heavy_font_(views::style::GetFont(views::style::CONTEXT_BUTTON_MD, + views::style::STYLE_PRIMARY)) { + // TODO(dfried): create a new STYLE_PROMINENT or similar to use instead of + // repurposing CONTEXT_BUTTON_MD. +} + +SkPath GM2TabStyle::GetPath(PathType path_type, + float scale, + bool force_active, + RenderUnits render_units) const { + const int stroke_thickness = GetStrokeThickness(force_active); + + // We'll do the entire path calculation in aligned pixels. + // TODO(dfried): determine if we actually want to use |stroke_thickness| as + // the inset in this case. + gfx::RectF aligned_bounds = + ScaleAndAlignBounds(tab_->bounds(), scale, stroke_thickness); + + if (path_type == PathType::kInteriorClip) { + // When there is a separator, animate the clip to account for it, in sync + // with the separator's fading. + // TODO(pkasting): Consider crossfading the favicon instead of animating + // the clip, especially if other children get crossfaded. + const auto opacities = GetSeparatorOpacities(true); + constexpr float kChildClipPadding = 2.5f; + aligned_bounds.Inset( + gfx::InsetsF::TLBR(0.0f, kChildClipPadding + opacities.left, 0.0f, + kChildClipPadding + opacities.right)); + } + + // Calculate the corner radii. Note that corner radius is based on original + // tab width (in DIP), not our new, scaled-and-aligned bounds. + const float radius = GetTopCornerRadiusForWidth(tab_->width()) * scale; + float top_radius = radius; + float bottom_radius = radius; + + // Compute |extension| as the width outside the separators. This is a fixed + // value equal to the normal corner radius. + const float extension = GetCornerRadius() * scale; + + // Calculate the bounds of the actual path. + const float left = aligned_bounds.x(); + const float right = aligned_bounds.right(); + float tab_top = aligned_bounds.y(); + float tab_left = left + extension; + float tab_right = right - extension; + + // Overlap the toolbar below us so that gaps don't occur when rendering at + // non-integral display scale factors. + const float extended_bottom = aligned_bounds.bottom(); + const float bottom_extension = + GetLayoutConstant(TABSTRIP_TOOLBAR_OVERLAP) * scale; + float tab_bottom = extended_bottom - bottom_extension; + + // Path-specific adjustments: + const float stroke_adjustment = stroke_thickness * scale; + bool extend_to_top = false; + if (path_type == PathType::kInteriorClip) { + // Inside of the border runs |stroke_thickness| inside the outer edge. + tab_left += stroke_adjustment; + tab_right -= stroke_adjustment; + tab_top += stroke_adjustment; + top_radius -= stroke_adjustment; + } else if (path_type == PathType::kFill || path_type == PathType::kBorder) { + tab_left += 0.5f * stroke_adjustment; + tab_right -= 0.5f * stroke_adjustment; + tab_top += 0.5f * stroke_adjustment; + top_radius -= 0.5f * stroke_adjustment; + tab_bottom -= 0.5f * stroke_adjustment; + bottom_radius -= 0.5f * stroke_adjustment; + } else if (path_type == PathType::kHitTest) { + // Outside border needs to draw its bottom line a stroke width above the + // bottom of the tab, to line up with the stroke that runs across the rest + // of the bottom of the tab bar (when strokes are enabled). + tab_bottom -= stroke_adjustment; + bottom_radius -= stroke_adjustment; + if (ShouldExtendHitTest()) { + extend_to_top = true; + if (tab_->controller()->IsTabFirst(tab_)) { + // The path is not mirrored in RTL and thus we must manually choose the + // correct "leading" edge. + if (base::i18n::IsRTL()) + tab_right = right; + else + tab_left = left; + } + } + } + const ShapeModifier shape_modifier = GetShapeModifier(path_type); + const bool extend_left_to_bottom = shape_modifier & kNoLowerLeftArc; + const bool extend_right_to_bottom = shape_modifier & kNoLowerRightArc; + + SkPath path; + + if (path_type == PathType::kInteriorClip) { + // Clip path is a simple rectangle. + path.addRect(tab_left, tab_top, tab_right, tab_bottom); + } else if (path_type == PathType::kHighlight) { + // The path is a round rect inset by the focus ring thickness. The + // radius is also adjusted by the inset. + const float inset = views::FocusRing::kDefaultHaloThickness + + views::FocusRing::kDefaultHaloInset; + SkRRect rrect = SkRRect::MakeRectXY( + SkRect::MakeLTRB(tab_left + inset, tab_top + inset, tab_right - inset, + tab_bottom - inset), + radius - inset, radius - inset); + path.addRRect(rrect); + } else { + // Avoid mallocs at every new path verb by preallocating an + // empirically-determined amount of space in the verb and point buffers. + const int kMaxPathPoints = 20; + path.incReserve(kMaxPathPoints); + + // We will go clockwise from the lower left. We start in the overlap region, + // preventing a gap between toolbar and tabstrip. + // TODO(dfried): verify that the we actually want to start the stroke for + // the exterior path outside the region; we might end up rendering an + // extraneous descending pixel on displays with odd scaling and nonzero + // stroke width. + + // Start with the left side of the shape. + path.moveTo(left, extended_bottom); + + if (tab_left != left) { + // Draw the left edge of the extension. + // ╭─────────╮ + // │ Content │ + // ┏─╯ ╰─┐ + if (tab_bottom != extended_bottom) + path.lineTo(left, tab_bottom); + + // Draw the bottom-left corner. + // ╭─────────╮ + // │ Content │ + // ┌━╝ ╰─┐ + if (extend_left_to_bottom) { + path.lineTo(tab_left, tab_bottom); + } else { + path.lineTo(tab_left - bottom_radius, tab_bottom); + path.arcTo(bottom_radius, bottom_radius, 0, SkPath::kSmall_ArcSize, + SkPathDirection::kCCW, tab_left, tab_bottom - bottom_radius); + } + } + + // Draw the ascender and top-left curve, if present. + if (extend_to_top) { + // ┎─────────╮ + // ┃ Content │ + // ┌─╯ ╰─┐ + path.lineTo(tab_left, tab_top); + } else { + // ╔─────────╮ + // ┃ Content │ + // ┌─╯ ╰─┐ + path.lineTo(tab_left, tab_top + top_radius); + path.arcTo(top_radius, top_radius, 0, SkPath::kSmall_ArcSize, + SkPathDirection::kCW, tab_left + top_radius, tab_top); + } + + // Draw the top crossbar and top-right curve, if present. + if (extend_to_top) { + // ┌━━━━━━━━━┑ + // │ Content │ + // ┌─╯ ╰─┐ + path.lineTo(tab_right, tab_top); + } else { + // ╭━━━━━━━━━╗ + // │ Content │ + // ┌─╯ ╰─┐ + path.lineTo(tab_right - top_radius, tab_top); + path.arcTo(top_radius, top_radius, 0, SkPath::kSmall_ArcSize, + SkPathDirection::kCW, tab_right, tab_top + top_radius); + } + + if (tab_right != right) { + // Draw the descender and bottom-right corner. + // ╭─────────╮ + // │ Content ┃ + // ┌─╯ ╚━┐ + if (extend_right_to_bottom) { + path.lineTo(tab_right, tab_bottom); + } else { + path.lineTo(tab_right, tab_bottom - bottom_radius); + path.arcTo(bottom_radius, bottom_radius, 0, SkPath::kSmall_ArcSize, + SkPathDirection::kCCW, tab_right + bottom_radius, + tab_bottom); + } + if (tab_bottom != extended_bottom) + path.lineTo(right, tab_bottom); + } + + // Draw anything remaining: the descender, the bottom right horizontal + // stroke, or the right edge of the extension, depending on which + // conditions fired above. + // ╭─────────╮ + // │ Content │ + // ┌─╯ ╰─┓ + path.lineTo(right, extended_bottom); + + if (path_type != PathType::kBorder) + path.close(); + } + + // Convert path to be relative to the tab origin. + gfx::PointF origin(tab_->origin()); + origin.Scale(scale); + path.offset(-origin.x(), -origin.y()); + + // Possibly convert back to DIPs. + if (render_units == RenderUnits::kDips && scale != 1.0f) + path.transform(SkMatrix::Scale(1.0f / scale, 1.0f / scale)); + + return path; +} + +gfx::Insets GM2TabStyle::GetContentsInsets() const { + const int stroke_thickness = GetStrokeThickness(); + const int horizontal_inset = GetContentsHorizontalInsetSize(); + return gfx::Insets::TLBR( + stroke_thickness, horizontal_inset, + stroke_thickness + GetLayoutConstant(TABSTRIP_TOOLBAR_OVERLAP), + horizontal_inset); +} + +float GM2TabStyle::GetZValue() const { + // This will return values so that inactive tabs can be sorted in the + // following order: + // + // o Unselected tabs, in ascending hover animation value order. + // o The single unselected tab being hovered by the mouse, if present. + // o Selected tabs, in ascending hover animation value order. + // o The single selected tab being hovered by the mouse, if present. + // + // Representing the above groupings is accomplished by adding a "weight" to + // the current hover animation value. + // + // 0.0 == z-value Unselected/non hover animating. + // 0.0 < z-value <= 1.0 Unselected/hover animating. + // 2.0 <= z-value <= 3.0 Unselected/mouse hovered tab. + // 4.0 == z-value Selected/non hover animating. + // 4.0 < z-value <= 5.0 Selected/hover animating. + // 6.0 <= z-value <= 7.0 Selected/mouse hovered tab. + // + // This function doesn't handle active tabs, as they are normally painted by a + // different code path (with z-value infinity). + float sort_value = GetHoverAnimationValue(); + if (tab_->IsSelected()) + sort_value += 4.f; + if (tab_->mouse_hovered()) + sort_value += 2.f; + + DCHECK_GE(sort_value, 0.0f); + DCHECK_LE(sort_value, TabStyle::kMaximumZValue); + + return sort_value; +} + +float GM2TabStyle::GetActiveOpacity() const { + if (tab_->IsActive()) + return 1.0f; + if (tab_->IsSelected()) + return kSelectedTabOpacity; + if (tab_->mouse_hovered()) + return GetHoverOpacity(); + return 0.0f; +} + +TabActive GM2TabStyle::GetApparentActiveState() const { + // In some cases, inactive tabs may have background more like active tabs than + // inactive tabs, so colors should be adapted to ensure appropriate contrast. + // In particular, text should have plenty of contrast in all cases, so switch + // to using foreground color designed for active tabs if the tab looks more + // like an active tab than an inactive tab. + return GetActiveOpacity() > 0.5f ? TabActive::kActive : TabActive::kInactive; +} + +TabStyle::TabColors GM2TabStyle::CalculateColors() const { + const TabActive active = GetApparentActiveState(); + const SkColor foreground_color = + tab_->controller()->GetTabForegroundColor(active); + const SkColor background_color = color_utils::AlphaBlend( + GetTabBackgroundColor(TabActive::kActive), + GetTabBackgroundColor(TabActive::kInactive), GetActiveOpacity()); + const ui::ColorId focus_ring_color = (active == TabActive::kActive) + ? kColorTabFocusRingActive + : kColorTabFocusRingInactive; + const ui::ColorId close_button_focus_ring_color = + (active == TabActive::kActive) ? kColorTabCloseButtonFocusRingActive + : kColorTabCloseButtonFocusRingInactive; + return {foreground_color, background_color, focus_ring_color, + close_button_focus_ring_color}; +} + +const gfx::FontList& GM2TabStyle::GetFontList() const { + // Don't want to have to keep re-computing this value. + static const bool prominent_dark_mode_title = true; + + if (prominent_dark_mode_title && tab_->IsActive() && + color_utils::IsDark(GetTabBackgroundColor(TabActive::kActive))) { + return heavy_font_; + } + + return normal_font_; +} + +void GM2TabStyle::PaintTab(gfx::Canvas* canvas) const { + absl::optional active_tab_fill_id; + int active_tab_y_inset = 0; + if (tab_->GetThemeProvider()->HasCustomImage(IDR_THEME_TOOLBAR)) { + active_tab_fill_id = IDR_THEME_TOOLBAR; + active_tab_y_inset = GetStrokeThickness(true); + } + + if (tab_->IsActive()) { + PaintTabBackground(canvas, TabActive::kActive, active_tab_fill_id, + active_tab_y_inset); + } else { + PaintInactiveTabBackground(canvas); + + const float throb_value = GetThrobValue(); + if (throb_value > 0) { + canvas->SaveLayerAlpha(base::ClampRound(throb_value * 0xff), + tab_->GetLocalBounds()); + PaintTabBackground(canvas, TabActive::kActive, active_tab_fill_id, + active_tab_y_inset); + canvas->Restore(); + } + } +} + +void GM2TabStyle::SetHoverLocation(const gfx::Point& location) { + // There's a "glow" that gets drawn over inactive tabs based on the mouse's + // location. There is no glow for the active tab so don't update the hover + // controller and incur a redraw. + if (hover_controller_ && !tab_->IsActive()) + hover_controller_->SetLocation(location); +} + +void GM2TabStyle::ShowHover(ShowHoverStyle style) { + if (!hover_controller_) + return; + + if (style == ShowHoverStyle::kSubtle) { + hover_controller_->SetSubtleOpacityScale( + tab_->controller()->GetHoverOpacityForRadialHighlight()); + } + hover_controller_->Show(style); +} + +void GM2TabStyle::HideHover(HideHoverStyle style) { + if (hover_controller_) + hover_controller_->Hide(style); +} + +TabStyle::SeparatorBounds GM2TabStyle::GetSeparatorBounds(float scale) const { + const gfx::RectF aligned_bounds = + ScaleAndAlignBounds(tab_->bounds(), scale, GetStrokeThickness()); + const int corner_radius = GetCornerRadius() * scale; + gfx::SizeF separator_size(GetSeparatorSize()); + separator_size.Scale(scale); + + SeparatorBounds separator_bounds; + + separator_bounds.leading = + gfx::RectF(aligned_bounds.x() + corner_radius, + aligned_bounds.y() + + (aligned_bounds.height() - separator_size.height()) / 2, + separator_size.width(), separator_size.height()); + + separator_bounds.trailing = separator_bounds.leading; + separator_bounds.trailing.set_x(aligned_bounds.right() - + (corner_radius + separator_size.width())); + + gfx::PointF origin(tab_->bounds().origin()); + origin.Scale(scale); + separator_bounds.leading.Offset(-origin.x(), -origin.y()); + separator_bounds.trailing.Offset(-origin.x(), -origin.y()); + + return separator_bounds; +} + +TabStyle::SeparatorOpacities GM2TabStyle::GetSeparatorOpacities( + bool for_layout) const { + // Adjacent slots should be visually separated from each other. This can be + // achieved in multiple ways: + // - Contrasting background colors for tabs, due to: + // - Active state + // - Selected state + // - Hovered state + // - Theming (affected by all the above, plus the neutral state) + // - Manually painting a separator. + // The separator should be the last resort, if none of the above states + // apply. It's also needed if multiple adjacent views are selected, in which + // case the uniform selected color does not provide enough contrast. + // In addition, separators should smoothly fade in and out between states, + // particularly during the hover animation. + + float leading_opacity = GetSeparatorOpacity(for_layout, true); + float trailing_opacity = GetSeparatorOpacity(for_layout, false); + + // Return the opacities in physical order, rather than logical. + if (base::i18n::IsRTL()) + std::swap(leading_opacity, trailing_opacity); + return {leading_opacity, trailing_opacity}; +} + +float GM2TabStyle::GetSeparatorOpacity(bool for_layout, bool leading) const { + // If the current tab is active, never show the separator. + if (tab_->IsActive()) + return 0.0f; + + const Tab* adjacent_tab = + tab_->controller()->GetAdjacentTab(tab_, leading ? -1 : 1); + + const Tab* left_tab = leading ? adjacent_tab : tab_.get(); + const Tab* right_tab = leading ? tab_.get() : adjacent_tab; + const bool adjacent_to_header = + right_tab && right_tab->group().has_value() && + (!left_tab || left_tab->group() != right_tab->group()); + + // If the current tab is selected, default to hiding the separator. Only show + // the separator if it's adjacent to other selected tabs. + if (tab_->IsSelected()) { + // If the adjacent view is actually a group header, hide the separator since + // group headers normally cannot be selected. Group headers can become + // selected when dragging groups, but in that case it is always the first + // view dragging followed by the active tab (which has a group outline + // instead of a separator). So a separator is still not necessary here. + if (adjacent_to_header) + return 0.0f; + + if (adjacent_tab && adjacent_tab->IsSelected()) + return GetHoverInterpolatedSeparatorOpacity(for_layout, adjacent_tab); + + return 0.0f; + } + + // Otherwise, default to showing the separator, respecting the hover + // animation. Only hide the separator if it's in the first slot, or in + // certain cases if the tab has a visible background (see below). + + // If the tab has a visible background even when not selected or active, there + // are additional cases where the separators can be hidden. + if (tab_->controller()->HasVisibleBackgroundTabShapes()) { + // If the tab with a visible background is in an end slot, hide the + // separator because it doesn't need additional contrast with the tab strip + // or the new tab button. This value isn't interpolated like the others + // because the separator was likely already hidden: if it's animating into + // an end slot, then the tab was probably next to a selected dragging tab + // (see the condition below). + if (!adjacent_tab) + return 0.0f; + + // With visible tab background shapes, a tab next to a group header doesn't + // need the additional contrast of a separator, because it's the tab + // background on top of the tab strip background directly, same as if the + // tab were in an end slot. + if (adjacent_to_header) + return 0.0f; + + // If the adjacent tab is selected, any separator on the current tab will be + // "hidden" beneath the adjacent tab's background. Normally tabs will still + // have a separator, in case the adjacent tab is dragged away and it reveals + // an empty gap. However, tabs with visible backgrounds already have + // sufficient contrast against the empty gap, so this contingency isn't + // needed. Therefore, the separator is hidden only for tabs with visible + // backgrounds. + if (adjacent_tab->IsSelected()) + return 0.0f; + } + + // Do not show the separator if it is to the right of a group header. + // Otherwise, show the separator since the following group header takes up a + // slot. + if (adjacent_to_header) { + if (leading) + return 0.0f; + return GetHoverInterpolatedSeparatorOpacity(for_layout, nullptr); + } + + // If the tab does not have a visible background and is in the first slot, + // do not show the separator. This once was interpolated based on the tab's + // progress through animating into this slot, but that was removed because the + // visual impact was minimal and + if (!adjacent_tab && leading) + return 0.0f; + + return GetHoverInterpolatedSeparatorOpacity(for_layout, adjacent_tab); +} + +float GM2TabStyle::GetHoverInterpolatedSeparatorOpacity( + bool for_layout, + const Tab* other_tab) const { + // Fade out the intervening separator while this tab or an adjacent tab is + // hovered, which prevents sudden opacity changes when scrubbing the mouse + // across the tabstrip. If that adjacent tab is active, don't consider its + // hover animation value, otherwise the separator on this tab will disappear + // while that tab is being dragged. + auto adjacent_hover_value = [for_layout](const Tab* other_tab) { + if (for_layout || !other_tab || other_tab->IsActive()) + return 0.0f; + auto* tab_style = static_cast(other_tab->tab_style()); + return static_cast(tab_style->GetHoverAnimationValue()); + }; + const float hover_value = GetHoverAnimationValue(); + return 1.0f - std::max(hover_value, adjacent_hover_value(other_tab)); +} + +bool GM2TabStyle::ShouldExtendHitTest() const { + const views::Widget* widget = tab_->GetWidget(); + return widget->IsMaximized() || widget->IsFullscreen(); +} + +bool GM2TabStyle::IsHoverActive() const { + if (!hover_controller_) + return false; + return hover_controller_->ShouldDraw(); +} + +double GM2TabStyle::GetHoverAnimationValue() const { + if (!hover_controller_) + return 0.0; + return hover_controller_->GetAnimationValue(); +} + +float GM2TabStyle::GetHoverOpacity() const { + // Opacity boost varies on tab width. The interpolation is nonlinear so + // that most tabs will fall on the low end of the opacity range, but very + // narrow tabs will still stand out on the high end. + const float range_start = static_cast(GetStandardWidth()); + constexpr float kWidthForMaxHoverOpacity = 32.0f; + const float value_in_range = static_cast(tab_->width()); + const float t = base::clamp( + (value_in_range - range_start) / (kWidthForMaxHoverOpacity - range_start), + 0.0f, 1.0f); + return tab_->controller()->GetHoverOpacityForTab(t * t); +} + +float GM2TabStyle::GetThrobValue() const { + const bool is_selected = tab_->IsSelected(); + double val = is_selected ? kSelectedTabOpacity : 0; + + if (IsHoverActive()) { + constexpr float kSelectedTabThrobScale = 0.95f - kSelectedTabOpacity; + const float opacity = GetHoverOpacity(); + const float offset = + is_selected ? (kSelectedTabThrobScale * opacity) : opacity; + val += GetHoverAnimationValue() * offset; + } + + return val; +} + +int GM2TabStyle::GetStrokeThickness(bool should_paint_as_active) const { + absl::optional group = tab_->group(); + if (group.has_value() && tab_->IsActive()) + return TabGroupUnderline::kStrokeThickness; + + if (tab_->IsActive() || should_paint_as_active) + return tab_->controller()->GetStrokeThickness(); + + return 0; +} + +bool GM2TabStyle::ShouldPaintTabBackgroundColor( + TabActive active, + bool has_custom_background) const { + // In the active case, always paint the tab background. The fill image may be + // transparent. + if (active == TabActive::kActive) + return true; + + // In the inactive case, the fill image is guaranteed to be opaque, so it's + // not necessary to paint the background when there is one. + if (has_custom_background) + return false; + + return tab_->GetThemeProvider()->GetDisplayProperty( + ThemeProperties::SHOULD_FILL_BACKGROUND_TAB_COLOR); +} + +SkColor GM2TabStyle::GetTabBackgroundColor(TabActive active) const { + SkColor color = tab_->controller()->GetTabBackgroundColor( + active, BrowserFrameActiveState::kUseCurrent); + + return color; +} + +ShapeModifier GM2TabStyle::GetShapeModifier(PathType path_type) const { + ShapeModifier shape_modifier = kNone; + if (path_type == PathType::kFill && tab_->IsSelected() && !IsHoverActive() && + !tab_->IsActive()) { + auto check_adjacent_tab = [](const Tab* tab, int offset, + ShapeModifier modifier) { + const Tab* adjacent_tab = tab->controller()->GetAdjacentTab(tab, offset); + if (adjacent_tab && adjacent_tab->IsSelected() && + !adjacent_tab->IsMouseHovered()) + return modifier; + return kNone; + }; + shape_modifier |= check_adjacent_tab(tab_, -1, kNoLowerLeftArc); + shape_modifier |= check_adjacent_tab(tab_, 1, kNoLowerRightArc); + } + return shape_modifier; +} + +void GM2TabStyle::PaintInactiveTabBackground(gfx::Canvas* canvas) const { + PaintTabBackground(canvas, TabActive::kInactive, + tab_->controller()->GetCustomBackgroundId( + BrowserFrameActiveState::kUseCurrent), + 0); +} + +void GM2TabStyle::PaintTabBackground(gfx::Canvas* canvas, + TabActive active, + absl::optional fill_id, + int y_inset) const { + // |y_inset| is only set when |fill_id| is being used. + DCHECK(!y_inset || fill_id.has_value()); + + absl::optional group_color = tab_->GetGroupColor(); + + PaintTabBackgroundFill(canvas, active, + active == TabActive::kInactive && IsHoverActive(), + fill_id, y_inset); + + const auto* widget = tab_->GetWidget(); + DCHECK(widget); + const SkColor tab_stroke_color = widget->GetColorProvider()->GetColor( + tab_->controller()->ShouldPaintAsActiveFrame() + ? kColorTabStrokeFrameActive + : kColorTabStrokeFrameInactive); + + PaintBackgroundStroke(canvas, active, group_color.value_or(tab_stroke_color)); + PaintSeparators(canvas); +} + +void GM2TabStyle::PaintTabBackgroundFill(gfx::Canvas* canvas, + TabActive active, + bool paint_hover_effect, + absl::optional fill_id, + int y_inset) const { + const SkPath fill_path = GetPath(PathType::kFill, canvas->image_scale(), + active == TabActive::kActive); + gfx::ScopedCanvas scoped_canvas(canvas); + const float scale = canvas->UndoDeviceScaleFactor(); + + canvas->ClipPath(fill_path, true); + + if (ShouldPaintTabBackgroundColor(active, fill_id.has_value())) { + cc::PaintFlags flags; + flags.setAntiAlias(true); + flags.setColor(GetTabBackgroundColor(active)); + canvas->DrawRect(gfx::ScaleToEnclosingRect(tab_->GetLocalBounds(), scale), + flags); + } + + if (fill_id.has_value()) { + gfx::ScopedCanvas scale_scoper(canvas); + canvas->sk_canvas()->scale(scale, scale); + canvas->TileImageInt( + *tab_->GetThemeProvider()->GetImageSkiaNamed(fill_id.value()), + tab_->GetMirroredX() + tab_->controller()->GetBackgroundOffset(), 0, 0, + y_inset, tab_->width(), tab_->height()); + } + + if (paint_hover_effect) { + SkPoint hover_location(gfx::PointToSkPoint(hover_controller_->location())); + hover_location.scale(SkFloatToScalar(scale)); + const SkScalar kMinHoverRadius = 16; + const SkScalar radius = + std::max(SkFloatToScalar(tab_->width() / 4.f), kMinHoverRadius); + DrawHighlight(canvas, hover_location, radius * scale, + SkColorSetA(GetTabBackgroundColor(TabActive::kActive), + hover_controller_->GetAlpha())); + } +} + +void GM2TabStyle::PaintBackgroundStroke(gfx::Canvas* canvas, + TabActive active, + SkColor stroke_color) const { + const bool is_active = active == TabActive::kActive; + const int stroke_thickness = GetStrokeThickness(is_active); + if (!stroke_thickness) + return; + + SkPath outer_path = + GetPath(TabStyle::PathType::kBorder, canvas->image_scale(), is_active); + gfx::ScopedCanvas scoped_canvas(canvas); + float scale = canvas->UndoDeviceScaleFactor(); + cc::PaintFlags flags; + flags.setAntiAlias(true); + flags.setColor(stroke_color); + flags.setStyle(cc::PaintFlags::kStroke_Style); + flags.setStrokeWidth(stroke_thickness * scale); + canvas->DrawPath(outer_path, flags); +} + +void GM2TabStyle::PaintSeparators(gfx::Canvas* canvas) const { + const auto separator_opacities = GetSeparatorOpacities(false); + if (!separator_opacities.left && !separator_opacities.right) + return; + + gfx::ScopedCanvas scoped_canvas(canvas); + const float scale = canvas->UndoDeviceScaleFactor(); + + TabStyle::SeparatorBounds separator_bounds = GetSeparatorBounds(scale); + + const SkColor separator_base_color = + tab_->controller()->GetTabSeparatorColor(); + const auto separator_color = [separator_base_color](float opacity) { + return SkColorSetA(separator_base_color, + gfx::Tween::IntValueBetween(opacity, SK_AlphaTRANSPARENT, + SK_AlphaOPAQUE)); + }; + + cc::PaintFlags flags; + flags.setAntiAlias(true); + flags.setColor(separator_color(separator_opacities.left)); + canvas->DrawRect(separator_bounds.leading, flags); + flags.setColor(separator_color(separator_opacities.right)); + canvas->DrawRect(separator_bounds.trailing, flags); +} + +// static +float GM2TabStyle::GetTopCornerRadiusForWidth(int width) { + // Get the width of the top of the tab by subtracting the width of the outer + // corners. + const int ideal_radius = GetCornerRadius(); + const int top_width = width - ideal_radius * 2; + + // To maintain a round-rect appearance, ensure at least one third of the top + // of the tab is flat. + const float radius = top_width / 3.f; + return base::clamp(radius, 0, ideal_radius); +} + +// static +gfx::RectF GM2TabStyle::ScaleAndAlignBounds(const gfx::Rect& bounds, + float scale, + int stroke_thickness) { + // Convert to layout bounds. We must inset the width such that the right edge + // of one tab's layout bounds is the same as the left edge of the next tab's; + // this way the two tabs' separators will be drawn at the same coordinate. + gfx::RectF aligned_bounds(bounds); + const int corner_radius = GetCornerRadius(); + // Note: This intentionally doesn't subtract TABSTRIP_TOOLBAR_OVERLAP from the + // bottom inset, because we want to pixel-align the bottom of the stroke, not + // the bottom of the overlap. + auto layout_insets = + gfx::InsetsF::TLBR(stroke_thickness, corner_radius, stroke_thickness, + corner_radius + GetSeparatorSize().width()); + aligned_bounds.Inset(layout_insets); + + // Scale layout bounds from DIP to px. + aligned_bounds.Scale(scale); + + // Snap layout bounds to nearest pixels so we get clean lines. + const float x = std::round(aligned_bounds.x()); + const float y = std::round(aligned_bounds.y()); + // It's important to round the right edge and not the width, since rounding + // both x and width would mean the right edge would accumulate error. + const float right = std::round(aligned_bounds.right()); + const float bottom = std::round(aligned_bounds.bottom()); + aligned_bounds = gfx::RectF(x, y, right - x, bottom - y); + + // Convert back to full bounds. It's OK that the outer corners of the curves + // around the separator may not be snapped to the pixel grid as a result. + aligned_bounds.Inset(-gfx::ScaleInsets(layout_insets, scale)); + return aligned_bounds; +} + +} // namespace + +// static +std::u16string ui::metadata::TypeConverter::ToString( + ui::metadata::ArgType source_value) { + return base::ASCIIToUTF16(base::StrCat( + {"{", color_utils::SkColorToRgbaString(source_value.foreground_color), + ",", color_utils::SkColorToRgbaString(source_value.background_color), + ",", color_utils::SkColorToRgbaString(source_value.focus_ring_color), + ",", + color_utils::SkColorToRgbaString( + source_value.close_button_focus_ring_color), + "}"})); +} + +// static +absl::optional ui::metadata::TypeConverter< + TabStyle::TabColors>::FromString(const std::u16string& source_value) { + std::u16string trimmed_string; + base::TrimString(source_value, u"{ }", &trimmed_string); + std::u16string::const_iterator color_pos = trimmed_string.cbegin(); + const auto foreground_color = SkColorConverter::GetNextColor( + color_pos, trimmed_string.cend(), color_pos); + const auto background_color = SkColorConverter::GetNextColor( + color_pos, trimmed_string.cend(), color_pos); + const auto focus_ring_color = SkColorConverter::GetNextColor( + color_pos, trimmed_string.cend(), color_pos); + const auto close_button_focus_ring_color = + SkColorConverter::GetNextColor(color_pos, trimmed_string.cend()); + return (foreground_color && background_color && focus_ring_color && + close_button_focus_ring_color) + ? absl::make_optional( + foreground_color.value(), background_color.value(), + focus_ring_color.value(), + close_button_focus_ring_color.value()) + : absl::nullopt; +} + +// static +ui::metadata::ValidStrings +ui::metadata::TypeConverter::GetValidStrings() { + return ValidStrings(); +} + +// TabStyle -------------------------------------------------------------------- + +TabStyleViews::~TabStyleViews() = default; + +// static +std::unique_ptr TabStyleViews::CreateForTab(Tab* tab) { + return std::make_unique(tab); +} + +// static +int TabStyleViews::GetMinimumActiveWidth() { + int min_active_width = + TabCloseButton::GetGlyphSize() + GetContentsHorizontalInsetSize() * 2; + if (base::FeatureList::IsEnabled(features::kScrollableTabStrip)) { + return std::max( + min_active_width, + base::GetFieldTrialParamByFeatureAsInt( + features::kScrollableTabStrip, + features::kMinimumTabWidthFeatureParameterName, min_active_width)); + } + return min_active_width; +} + +// static +int TabStyleViews::GetMinimumInactiveWidth() { + // Allow tabs to shrink until they appear to be 16 DIP wide excluding + // outer corners. + constexpr int kInteriorWidth = 16; + // The overlap contains the trailing separator that is part of the interior + // width; avoid double-counting it. + int min_inactive_width = + kInteriorWidth - GetSeparatorSize().width() + GetTabOverlap(); + + if (base::FeatureList::IsEnabled(features::kScrollableTabStrip)) { + return std::max(min_inactive_width, + base::GetFieldTrialParamByFeatureAsInt( + features::kScrollableTabStrip, + features::kMinimumTabWidthFeatureParameterName, + min_inactive_width)); + } + + return min_inactive_width; +}