1 // Copyright 2017 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "chrome/browser/ui/views/tabs/tab_strip.h"
6 
7 #include <stddef.h>
8 
9 #include <algorithm>
10 #include <iterator>
11 #include <string>
12 #include <utility>
13 #include <vector>
14 
15 #include "base/bind.h"
16 #include "base/callback.h"
17 #include "base/compiler_specific.h"
18 #include "base/containers/adapters.h"
19 #include "base/containers/flat_map.h"
20 #include "base/feature_list.h"
21 #include "base/i18n/rtl.h"
22 #include "base/memory/weak_ptr.h"
23 #include "base/metrics/histogram.h"
24 #include "base/metrics/histogram_functions.h"
25 #include "base/metrics/histogram_macros.h"
26 #include "base/metrics/user_metrics.h"
27 #include "base/no_destructor.h"
28 #include "base/numerics/ranges.h"
29 #include "base/numerics/safe_conversions.h"
30 #include "base/stl_util.h"
31 #include "base/strings/utf_string_conversions.h"
32 #include "base/timer/elapsed_timer.h"
33 #include "build/build_config.h"
34 #include "chrome/browser/defaults.h"
35 #include "chrome/browser/themes/theme_properties.h"
36 #include "chrome/browser/ui/layout_constants.h"
37 #include "chrome/browser/ui/tabs/tab_group_theme.h"
38 #include "chrome/browser/ui/tabs/tab_strip_model.h"
39 #include "chrome/browser/ui/tabs/tab_types.h"
40 #include "chrome/browser/ui/ui_features.h"
41 #include "chrome/browser/ui/view_ids.h"
42 #include "chrome/browser/ui/views/frame/browser_view.h"
43 #include "chrome/browser/ui/views/tabs/browser_tab_strip_controller.h"
44 #include "chrome/browser/ui/views/tabs/stacked_tab_strip_layout.h"
45 #include "chrome/browser/ui/views/tabs/tab.h"
46 #include "chrome/browser/ui/views/tabs/tab_drag_controller.h"
47 #include "chrome/browser/ui/views/tabs/tab_group_header.h"
48 #include "chrome/browser/ui/views/tabs/tab_group_highlight.h"
49 #include "chrome/browser/ui/views/tabs/tab_group_underline.h"
50 #include "chrome/browser/ui/views/tabs/tab_group_views.h"
51 #include "chrome/browser/ui/views/tabs/tab_hover_card_bubble_view.h"
52 #include "chrome/browser/ui/views/tabs/tab_slot_view.h"
53 #include "chrome/browser/ui/views/tabs/tab_strip_controller.h"
54 #include "chrome/browser/ui/views/tabs/tab_strip_layout_helper.h"
55 #include "chrome/browser/ui/views/tabs/tab_strip_layout_types.h"
56 #include "chrome/browser/ui/views/tabs/tab_strip_observer.h"
57 #include "chrome/browser/ui/views/tabs/tab_style_views.h"
58 #include "chrome/browser/ui/views/touch_uma/touch_uma.h"
59 #include "chrome/browser/ui/web_applications/app_browser_controller.h"
60 #include "chrome/grit/generated_resources.h"
61 #include "chrome/grit/theme_resources.h"
62 #include "components/tab_groups/tab_group_color.h"
63 #include "components/tab_groups/tab_group_id.h"
64 #include "components/tab_groups/tab_group_visual_data.h"
65 #include "third_party/skia/include/core/SkColorFilter.h"
66 #include "third_party/skia/include/core/SkPath.h"
67 #include "third_party/skia/include/effects/SkLayerDrawLooper.h"
68 #include "third_party/skia/include/pathops/SkPathOps.h"
69 #include "ui/base/clipboard/clipboard.h"
70 #include "ui/base/dragdrop/drag_drop_types.h"
71 #include "ui/base/l10n/l10n_util.h"
72 #include "ui/base/models/list_selection_model.h"
73 #include "ui/base/resource/resource_bundle.h"
74 #include "ui/base/theme_provider.h"
75 #include "ui/display/display.h"
76 #include "ui/display/screen.h"
77 #include "ui/gfx/animation/throb_animation.h"
78 #include "ui/gfx/animation/tween.h"
79 #include "ui/gfx/geometry/rect_conversions.h"
80 #include "ui/gfx/geometry/size.h"
81 #include "ui/gfx/image/image_skia.h"
82 #include "ui/gfx/image/image_skia_operations.h"
83 #include "ui/gfx/native_widget_types.h"
84 #include "ui/gfx/skia_util.h"
85 #include "ui/views/accessibility/view_accessibility.h"
86 #include "ui/views/controls/image_view.h"
87 #include "ui/views/masked_targeter_delegate.h"
88 #include "ui/views/mouse_watcher_view_host.h"
89 #include "ui/views/rect_based_targeting_utils.h"
90 #include "ui/views/view_model_utils.h"
91 #include "ui/views/view_observer.h"
92 #include "ui/views/view_targeter.h"
93 #include "ui/views/view_utils.h"
94 #include "ui/views/widget/root_view.h"
95 #include "ui/views/widget/widget.h"
96 #include "ui/views/window/non_client_view.h"
97 
98 #if defined(OS_WIN)
99 #include "base/win/windows_version.h"
100 #include "ui/display/win/screen_win.h"
101 #include "ui/gfx/win/hwnd_util.h"
102 #include "ui/views/win/hwnd_util.h"
103 #endif
104 
105 #if defined(USE_AURA)
106 #include "ui/aura/window.h"
107 #endif
108 
109 namespace {
110 
111 // Distance from the next/previous stacked before before we consider the tab
112 // close enough to trigger moving.
113 const int kStackedDistance = 36;
114 
115 // Given the bounds of a dragged tab, return the X coordinate to use for
116 // computing where in the strip to insert/move the tab.
GetDraggedX(const gfx::Rect & dragged_bounds)117 int GetDraggedX(const gfx::Rect& dragged_bounds) {
118   return dragged_bounds.x() + TabStyle::GetTabInternalPadding().left();
119 }
120 
121 // Max number of stacked tabs.
122 constexpr int kMaxStackedCount = 4;
123 
124 // Padding between stacked tabs.
125 constexpr int kStackedPadding = 6;
126 
127 // Size of the drop indicator.
128 int g_drop_indicator_width = 0;
129 int g_drop_indicator_height = 0;
130 
131 // Listens in on the browser event stream (as a pre target event handler) and
132 // hides an associated hover card on any keypress.
133 class TabHoverCardEventSniffer : public ui::EventHandler {
134 // On Mac, events should be added to the root view.
135 #if defined(OS_MAC)
136   using OwnerView = views::View*;
137 #else   // defined(OS_MAC)
138   using OwnerView = gfx::NativeWindow;
139 #endif  // defined(OS_MAC)
140 
141  public:
TabHoverCardEventSniffer(TabHoverCardBubbleView * hover_card,TabStrip * tab_strip)142   TabHoverCardEventSniffer(TabHoverCardBubbleView* hover_card,
143                            TabStrip* tab_strip)
144       : hover_card_(hover_card),
145         tab_strip_(tab_strip),
146 #if defined(OS_MAC)
147         owner_view_(tab_strip->GetWidget()->GetRootView()) {
148 #else   // defined(OS_MAC)
149         owner_view_(tab_strip->GetWidget()->GetNativeWindow()) {
150 #endif  // defined(OS_MAC)
151     AddPreTargetHandler();
152   }
153 
154   ~TabHoverCardEventSniffer() override {
155     RemovePreTargetHandler();
156   }
157 
158  protected:
159   void AddPreTargetHandler() {
160     if (owner_view_)
161       owner_view_->AddPreTargetHandler(this);
162   }
163 
164   void RemovePreTargetHandler() {
165     if (owner_view_)
166       owner_view_->RemovePreTargetHandler(this);
167   }
168 
169   // ui::EventTarget:
170   void OnKeyEvent(ui::KeyEvent* event) override {
171     if (!tab_strip_->IsFocusInTabs())
172       tab_strip_->UpdateHoverCard(nullptr);
173   }
174 
175   void OnMouseEvent(ui::MouseEvent* event) override {
176     if (event->IsAnyButton())
177       hover_card_->FadeOutToHide();
178   }
179 
180   void OnGestureEvent(ui::GestureEvent* event) override {
181     hover_card_->FadeOutToHide();
182   }
183 
184  private:
185   TabHoverCardBubbleView* const hover_card_;
186   TabStrip* tab_strip_;
187   const OwnerView owner_view_;
188 };
189 
190 // Provides the ability to monitor when a tab's bounds have been animated. Used
191 // to hook callbacks to adjust things like tabstrip preferred size and tab group
192 // underlines.
193 class TabSlotAnimationDelegate : public gfx::AnimationDelegate {
194  public:
195   using OnAnimationProgressedCallback =
196       base::RepeatingCallback<void(TabSlotView*)>;
197 
198   TabSlotAnimationDelegate(
199       TabStrip* tab_strip,
200       TabSlotView* slot_view,
201       OnAnimationProgressedCallback on_animation_progressed);
202   TabSlotAnimationDelegate(const TabSlotAnimationDelegate&) = delete;
203   TabSlotAnimationDelegate& operator=(const TabSlotAnimationDelegate&) = delete;
204   ~TabSlotAnimationDelegate() override;
205 
206   void AnimationProgressed(const gfx::Animation* animation) override;
207 
208  protected:
tab_strip()209   TabStrip* tab_strip() { return tab_strip_; }
slot_view()210   TabSlotView* slot_view() { return slot_view_; }
211 
212  private:
213   TabStrip* const tab_strip_;
214   TabSlotView* const slot_view_;
215   OnAnimationProgressedCallback on_animation_progressed_;
216 };
217 
TabSlotAnimationDelegate(TabStrip * tab_strip,TabSlotView * slot_view,OnAnimationProgressedCallback on_animation_progressed)218 TabSlotAnimationDelegate::TabSlotAnimationDelegate(
219     TabStrip* tab_strip,
220     TabSlotView* slot_view,
221     OnAnimationProgressedCallback on_animation_progressed)
222     : tab_strip_(tab_strip),
223       slot_view_(slot_view),
224       on_animation_progressed_(on_animation_progressed) {}
225 
226 TabSlotAnimationDelegate::~TabSlotAnimationDelegate() = default;
227 
AnimationProgressed(const gfx::Animation * animation)228 void TabSlotAnimationDelegate::AnimationProgressed(
229     const gfx::Animation* animation) {
230   on_animation_progressed_.Run(slot_view());
231 }
232 
233 // Animation delegate used when a dragged tab is released. When done sets the
234 // dragging state to false.
235 class ResetDraggingStateDelegate : public TabSlotAnimationDelegate {
236  public:
237   ResetDraggingStateDelegate(
238       TabStrip* tab_strip,
239       Tab* tab,
240       OnAnimationProgressedCallback on_animation_progressed);
241   ResetDraggingStateDelegate(const ResetDraggingStateDelegate&) = delete;
242   ResetDraggingStateDelegate& operator=(const ResetDraggingStateDelegate&) =
243       delete;
244   ~ResetDraggingStateDelegate() override;
245 
246   void AnimationEnded(const gfx::Animation* animation) override;
247   void AnimationCanceled(const gfx::Animation* animation) override;
248 };
249 
ResetDraggingStateDelegate(TabStrip * tab_strip,Tab * tab,OnAnimationProgressedCallback on_animation_progressed)250 ResetDraggingStateDelegate::ResetDraggingStateDelegate(
251     TabStrip* tab_strip,
252     Tab* tab,
253     OnAnimationProgressedCallback on_animation_progressed)
254     : TabSlotAnimationDelegate(tab_strip, tab, on_animation_progressed) {}
255 
256 ResetDraggingStateDelegate::~ResetDraggingStateDelegate() = default;
257 
AnimationEnded(const gfx::Animation * animation)258 void ResetDraggingStateDelegate::AnimationEnded(
259     const gfx::Animation* animation) {
260   static_cast<Tab*>(slot_view())->set_dragging(false);
261   AnimationProgressed(animation);
262 }
263 
AnimationCanceled(const gfx::Animation * animation)264 void ResetDraggingStateDelegate::AnimationCanceled(
265     const gfx::Animation* animation) {
266   AnimationEnded(animation);
267 }
268 
269 // If |dest| contains the point |point_in_source| the event handler from |dest|
270 // is returned. Otherwise returns null.
ConvertPointToViewAndGetEventHandler(views::View * source,views::View * dest,const gfx::Point & point_in_source)271 views::View* ConvertPointToViewAndGetEventHandler(
272     views::View* source,
273     views::View* dest,
274     const gfx::Point& point_in_source) {
275   gfx::Point dest_point(point_in_source);
276   views::View::ConvertPointToTarget(source, dest, &dest_point);
277   return dest->HitTestPoint(dest_point)
278              ? dest->GetEventHandlerForPoint(dest_point)
279              : nullptr;
280 }
281 
282 // Gets a tooltip handler for |point_in_source| from |dest|. Note that |dest|
283 // should return null if it does not contain the point.
ConvertPointToViewAndGetTooltipHandler(views::View * source,views::View * dest,const gfx::Point & point_in_source)284 views::View* ConvertPointToViewAndGetTooltipHandler(
285     views::View* source,
286     views::View* dest,
287     const gfx::Point& point_in_source) {
288   gfx::Point dest_point(point_in_source);
289   views::View::ConvertPointToTarget(source, dest, &dest_point);
290   return dest->GetTooltipHandlerForPoint(dest_point);
291 }
292 
EventSourceFromEvent(const ui::LocatedEvent & event)293 TabDragController::EventSource EventSourceFromEvent(
294     const ui::LocatedEvent& event) {
295   return event.IsGestureEvent() ? TabDragController::EVENT_SOURCE_TOUCH
296                                 : TabDragController::EVENT_SOURCE_MOUSE;
297 }
298 
GetStackableTabWidth()299 int GetStackableTabWidth() {
300   return TabStyle::GetTabOverlap() +
301          (ui::TouchUiController::Get()->touch_ui() ? 136 : 102);
302 }
303 
304 }  // namespace
305 
306 ///////////////////////////////////////////////////////////////////////////////
307 // TabStrip::RemoveTabDelegate
308 //
309 // AnimationDelegate used when removing a tab. Does the necessary cleanup when
310 // done.
311 class TabStrip::RemoveTabDelegate : public TabSlotAnimationDelegate {
312  public:
313   RemoveTabDelegate(TabStrip* tab_strip,
314                     Tab* tab,
315                     OnAnimationProgressedCallback on_animation_progressed);
316   RemoveTabDelegate(const RemoveTabDelegate&) = delete;
317   RemoveTabDelegate& operator=(const RemoveTabDelegate&) = delete;
318 
319   void AnimationEnded(const gfx::Animation* animation) override;
320   void AnimationCanceled(const gfx::Animation* animation) override;
321 };
322 
RemoveTabDelegate(TabStrip * tab_strip,Tab * tab,OnAnimationProgressedCallback on_animation_progressed)323 TabStrip::RemoveTabDelegate::RemoveTabDelegate(
324     TabStrip* tab_strip,
325     Tab* tab,
326     OnAnimationProgressedCallback on_animation_progressed)
327     : TabSlotAnimationDelegate(tab_strip, tab, on_animation_progressed) {}
328 
AnimationEnded(const gfx::Animation * animation)329 void TabStrip::RemoveTabDelegate::AnimationEnded(
330     const gfx::Animation* animation) {
331   tab_strip()->OnTabCloseAnimationCompleted(static_cast<Tab*>(slot_view()));
332 }
333 
AnimationCanceled(const gfx::Animation * animation)334 void TabStrip::RemoveTabDelegate::AnimationCanceled(
335     const gfx::Animation* animation) {
336   AnimationEnded(animation);
337 }
338 
339 ///////////////////////////////////////////////////////////////////////////////
340 // TabStrip::TabDragContextImpl
341 //
342 class TabStrip::TabDragContextImpl : public TabDragContext {
343  public:
TabDragContextImpl(TabStrip * tab_strip)344   explicit TabDragContextImpl(TabStrip* tab_strip) : tab_strip_(tab_strip) {}
345 
IsDragStarted() const346   bool IsDragStarted() const {
347     return drag_controller_ && drag_controller_->started_drag();
348   }
349 
IsMutating() const350   bool IsMutating() const {
351     return drag_controller_ && drag_controller_->is_mutating();
352   }
353 
IsDraggingWindow() const354   bool IsDraggingWindow() const {
355     return drag_controller_ && drag_controller_->is_dragging_window();
356   }
357 
IsDraggingTab(content::WebContents * contents) const358   bool IsDraggingTab(content::WebContents* contents) const {
359     return contents && drag_controller_ &&
360            drag_controller_->IsDraggingTab(contents);
361   }
362 
SetMoveBehavior(TabDragController::MoveBehavior move_behavior)363   void SetMoveBehavior(TabDragController::MoveBehavior move_behavior) {
364     if (drag_controller_)
365       drag_controller_->SetMoveBehavior(move_behavior);
366   }
367 
MaybeStartDrag(TabSlotView * source,const ui::LocatedEvent & event,const ui::ListSelectionModel & original_selection)368   void MaybeStartDrag(TabSlotView* source,
369                       const ui::LocatedEvent& event,
370                       const ui::ListSelectionModel& original_selection) {
371     std::vector<TabSlotView*> dragging_views;
372     int x = source->GetMirroredXInView(event.x());
373     int y = event.y();
374 
375     // Build the set of selected tabs to drag and calculate the offset from the
376     // source.
377     ui::ListSelectionModel selection_model;
378     if (source->GetTabSlotViewType() ==
379         TabSlotView::ViewType::kTabGroupHeader) {
380       dragging_views.push_back(source);
381 
382       const std::vector<int> grouped_tabs =
383           tab_strip_->controller()->ListTabsInGroup(source->group().value());
384       for (int index : grouped_tabs) {
385         dragging_views.push_back(GetTabAt(index));
386         // Set |selection_model| if and only if the original selection does not
387         // match the group exactly. See TabDragController::Init() for details
388         // on how |selection_model| is used.
389         if (!original_selection.IsSelected(index))
390           selection_model = original_selection;
391       }
392       if (grouped_tabs.size() != original_selection.size())
393         selection_model = original_selection;
394     } else {
395       for (int i = 0; i < GetTabCount(); ++i) {
396         Tab* other_tab = GetTabAt(i);
397         if (tab_strip_->IsTabSelected(other_tab)) {
398           dragging_views.push_back(other_tab);
399           if (other_tab == source)
400             x += GetSizeNeededForViews(dragging_views) - other_tab->width();
401         }
402       }
403       if (!original_selection.IsSelected(tab_strip_->GetModelIndexOf(source)))
404         selection_model = original_selection;
405     }
406 
407     DCHECK(!dragging_views.empty());
408     DCHECK(base::Contains(dragging_views, source));
409 
410     // Delete the existing DragController before creating a new one. We do this
411     // as creating the DragController remembers the WebContents delegates and we
412     // need to make sure the existing DragController isn't still a delegate.
413     drag_controller_.reset();
414     TabDragController::MoveBehavior move_behavior = TabDragController::REORDER;
415 
416     // Use MOVE_VISIBLE_TABS in the following conditions:
417     // . Mouse event generated from touch and the left button is down (the right
418     //   button corresponds to a long press, which we want to reorder).
419     // . Gesture tap down and control key isn't down.
420     // . Real mouse event and control is down. This is mostly for testing.
421     DCHECK(event.type() == ui::ET_MOUSE_PRESSED ||
422            event.type() == ui::ET_GESTURE_TAP_DOWN ||
423            event.type() == ui::ET_GESTURE_SCROLL_BEGIN);
424     if (tab_strip_->touch_layout_ &&
425         ((event.type() == ui::ET_MOUSE_PRESSED &&
426           (((event.flags() & ui::EF_FROM_TOUCH) &&
427             static_cast<const ui::MouseEvent&>(event).IsLeftMouseButton()) ||
428            (!(event.flags() & ui::EF_FROM_TOUCH) &&
429             static_cast<const ui::MouseEvent&>(event).IsControlDown()))) ||
430          (event.type() == ui::ET_GESTURE_TAP_DOWN && !event.IsControlDown()) ||
431          (event.type() == ui::ET_GESTURE_SCROLL_BEGIN &&
432           !event.IsControlDown()))) {
433       move_behavior = TabDragController::MOVE_VISIBLE_TABS;
434     }
435 
436     drag_controller_ = std::make_unique<TabDragController>();
437     drag_controller_->Init(this, source, dragging_views, gfx::Point(x, y),
438                            event.x(), std::move(selection_model), move_behavior,
439                            EventSourceFromEvent(event));
440   }
441 
ContinueDrag(views::View * view,const ui::LocatedEvent & event)442   void ContinueDrag(views::View* view, const ui::LocatedEvent& event) {
443     if (drag_controller_.get() &&
444         drag_controller_->event_source() == EventSourceFromEvent(event)) {
445       gfx::Point screen_location(event.location());
446       views::View::ConvertPointToScreen(view, &screen_location);
447 
448       // Note: |tab_strip_| can be destroyed during drag, also destroying
449       // |this|.
450       base::WeakPtr<TabDragContext> weak_ptr(weak_factory_.GetWeakPtr());
451       drag_controller_->Drag(screen_location);
452 
453       if (!weak_ptr)
454         return;
455     }
456 
457     // Note: |drag_controller| can be set to null during the drag above.
458     if (drag_controller_ && drag_controller_->group())
459       tab_strip_->UpdateTabGroupVisuals(*drag_controller_->group());
460   }
461 
EndDrag(EndDragReason reason)462   bool EndDrag(EndDragReason reason) {
463     if (!drag_controller_.get())
464       return false;
465     bool started_drag = drag_controller_->started_drag();
466     drag_controller_->EndDrag(reason);
467     return started_drag;
468   }
469 
470   // TabDragContext:
AsView()471   views::View* AsView() override { return tab_strip_; }
472 
AsView() const473   const views::View* AsView() const override { return tab_strip_; }
474 
GetTabAt(int i) const475   Tab* GetTabAt(int i) const override { return tab_strip_->tab_at(i); }
476 
GetIndexOf(const TabSlotView * view) const477   int GetIndexOf(const TabSlotView* view) const override {
478     return tab_strip_->GetModelIndexOf(view);
479   }
480 
GetTabCount() const481   int GetTabCount() const override { return tab_strip_->tab_count(); }
482 
IsTabPinned(const Tab * tab) const483   bool IsTabPinned(const Tab* tab) const override {
484     return tab_strip_->IsTabPinned(tab);
485   }
486 
GetPinnedTabCount() const487   int GetPinnedTabCount() const override {
488     return tab_strip_->GetPinnedTabCount();
489   }
490 
GetTabGroupHeader(const tab_groups::TabGroupId & group) const491   TabGroupHeader* GetTabGroupHeader(
492       const tab_groups::TabGroupId& group) const override {
493     return tab_strip_->group_header(group);
494   }
495 
GetTabStripModel()496   TabStripModel* GetTabStripModel() override {
497     return static_cast<BrowserTabStripController*>(
498                tab_strip_->controller_.get())
499         ->model();
500   }
501 
GetActiveTouchIndex() const502   base::Optional<int> GetActiveTouchIndex() const override {
503     if (!tab_strip_->touch_layout_)
504       return base::nullopt;
505     return tab_strip_->touch_layout_->active_index();
506   }
507 
GetDragController()508   TabDragController* GetDragController() override {
509     return drag_controller_.get();
510   }
511 
OwnDragController(TabDragController * controller)512   void OwnDragController(TabDragController* controller) override {
513     // Typically, ReleaseDragController() and OwnDragController() calls are
514     // paired via corresponding calls to TabDragController::Detach() and
515     // TabDragController::Attach(). There is one exception to that rule: when a
516     // drag might start, we create a TabDragController that is owned by the
517     // potential source tabstrip in MaybeStartDrag(). If a drag actually starts,
518     // we then call Attach() on the source tabstrip, but since the source
519     // tabstrip already owns the TabDragController, so we don't need to do
520     // anything.
521     if (drag_controller_.get() != controller)
522       drag_controller_.reset(controller);
523   }
524 
DestroyDragController()525   void DestroyDragController() override {
526     drag_controller_.reset();
527   }
528 
ReleaseDragController()529   TabDragController* ReleaseDragController() override {
530     return drag_controller_.release();
531   }
532 
IsDragSessionActive() const533   bool IsDragSessionActive() const override {
534     return drag_controller_ != nullptr;
535   }
536 
IsActiveDropTarget() const537   bool IsActiveDropTarget() const override {
538     for (int i = 0; i < GetTabCount(); ++i) {
539       const Tab* const tab = GetTabAt(i);
540       if (tab->dragging())
541         return true;
542     }
543     return false;
544   }
545 
GetTabXCoordinates() const546   std::vector<int> GetTabXCoordinates() const override {
547     std::vector<int> results;
548     for (int i = 0; i < GetTabCount(); ++i)
549       results.push_back(ideal_bounds(i).x());
550     return results;
551   }
552 
GetActiveTabWidth() const553   int GetActiveTabWidth() const override {
554     return tab_strip_->GetActiveTabWidth();
555   }
556 
GetTabDragAreaWidth() const557   int GetTabDragAreaWidth() const override {
558     // There are two cases here (with tab scrolling enabled):
559     // 1) If the tab strip is not wider than the tab strip region (and thus
560     // not scrollable), returning the available width for tabs rather than the
561     // actual width for tabs allows tabs to be dragged past the current bounds
562     // of the tabstrip, anywhere along the tab strip region.
563     // N.B. The available width for tabs in this case needs to ignore tab
564     // closing mode.
565     // 2) If the tabstrip is wider than the tab strip region (and thus is
566     // scrollable), returning the tabstrip width allows tabs to be dragged
567     // anywhere within the tabstrip, not just in the leftmost region of it.
568     return std::max(tab_strip_->GetAvailableWidthForTabStrip(),
569                     tab_strip_->width());
570   }
571 
TabDragAreaBeginX() const572   int TabDragAreaBeginX() const override {
573     return tab_strip_->GetMirroredXWithWidthInView(0, GetTabDragAreaWidth());
574   }
575 
TabDragAreaEndX() const576   int TabDragAreaEndX() const override {
577     return TabDragAreaBeginX() + GetTabDragAreaWidth();
578   }
579 
GetHorizontalDragThreshold() const580   int GetHorizontalDragThreshold() const override {
581     constexpr int kHorizontalMoveThreshold = 16;  // DIPs.
582 
583     // Stacked tabs in touch mode don't shrink.
584     if (tab_strip_->touch_layout_)
585       return kHorizontalMoveThreshold;
586 
587     double ratio = double{tab_strip_->GetInactiveTabWidth()} /
588                    TabStyle::GetStandardWidth();
589     return base::ClampRound(ratio * kHorizontalMoveThreshold);
590   }
591 
GetInsertionIndexForDraggedBounds(const gfx::Rect & dragged_bounds,bool attaching,int num_dragged_tabs,bool mouse_has_ever_moved_left,bool mouse_has_ever_moved_right,base::Optional<tab_groups::TabGroupId> group) const592   int GetInsertionIndexForDraggedBounds(
593       const gfx::Rect& dragged_bounds,
594       bool attaching,
595       int num_dragged_tabs,
596       bool mouse_has_ever_moved_left,
597       bool mouse_has_ever_moved_right,
598       base::Optional<tab_groups::TabGroupId> group) const override {
599     // If the strip has no tabs, the only position to insert at is 0.
600     if (!GetTabCount())
601       return 0;
602 
603     base::Optional<int> index;
604     base::Optional<int> touch_index = GetActiveTouchIndex();
605     if (touch_index) {
606       index = GetInsertionIndexForDraggedBoundsStacked(
607           dragged_bounds, mouse_has_ever_moved_left,
608           mouse_has_ever_moved_right);
609       if (index) {
610         // Only move the tab to the left/right if the user actually moved the
611         // mouse that way. This is necessary as tabs with stacked tabs
612         // before/after them have multiple drag positions.
613         if ((index < touch_index && !mouse_has_ever_moved_left) ||
614             (index > touch_index && !mouse_has_ever_moved_right)) {
615           index = *touch_index;
616         }
617       }
618     } else {
619       index = GetInsertionIndexFrom(dragged_bounds, 0, std::move(group));
620     }
621     if (!index) {
622       const int last_tab_right = ideal_bounds(GetTabCount() - 1).right();
623       index = (dragged_bounds.right() > last_tab_right) ? GetTabCount() : 0;
624     }
625 
626     const Tab* last_visible_tab = tab_strip_->GetLastVisibleTab();
627     int last_insertion_point =
628         last_visible_tab ? (GetIndexOf(last_visible_tab) + 1) : 0;
629     if (!attaching) {
630       // We're not in the process of attaching, so clamp the insertion point to
631       // keep it within the visible region.
632       last_insertion_point =
633           std::max(0, last_insertion_point - num_dragged_tabs);
634     }
635 
636     // Ensure the first dragged tab always stays in the visible index range.
637     return std::min(*index, last_insertion_point);
638   }
639 
ShouldDragToNextStackedTab(const gfx::Rect & dragged_bounds,int index,bool mouse_has_ever_moved_right) const640   bool ShouldDragToNextStackedTab(
641       const gfx::Rect& dragged_bounds,
642       int index,
643       bool mouse_has_ever_moved_right) const override {
644     if (index + 1 >= GetTabCount() ||
645         !tab_strip_->touch_layout_->IsStacked(index + 1) ||
646         !mouse_has_ever_moved_right)
647       return false;
648 
649     int active_x = ideal_bounds(index).x();
650     int next_x = ideal_bounds(index + 1).x();
651     int mid_x =
652         std::min(next_x - kStackedDistance, active_x + (next_x - active_x) / 4);
653     return GetDraggedX(dragged_bounds) >= mid_x;
654   }
655 
ShouldDragToPreviousStackedTab(const gfx::Rect & dragged_bounds,int index,bool mouse_has_ever_moved_left) const656   bool ShouldDragToPreviousStackedTab(
657       const gfx::Rect& dragged_bounds,
658       int index,
659       bool mouse_has_ever_moved_left) const override {
660     if (index - 1 < tab_strip_->GetPinnedTabCount() ||
661         !tab_strip_->touch_layout_->IsStacked(index - 1) ||
662         !mouse_has_ever_moved_left)
663       return false;
664 
665     int active_x = ideal_bounds(index).x();
666     int previous_x = ideal_bounds(index - 1).x();
667     int mid_x = std::max(previous_x + kStackedDistance,
668                          active_x - (active_x - previous_x) / 4);
669     return GetDraggedX(dragged_bounds) <= mid_x;
670   }
671 
DragActiveTabStacked(const std::vector<int> & initial_positions,int delta)672   void DragActiveTabStacked(const std::vector<int>& initial_positions,
673                             int delta) override {
674     DCHECK_EQ(GetTabCount(), int{initial_positions.size()});
675     SetIdealBoundsFromPositions(initial_positions);
676     tab_strip_->touch_layout_->DragActiveTab(delta);
677     tab_strip_->CompleteAnimationAndLayout();
678   }
679 
CalculateBoundsForDraggedViews(const std::vector<TabSlotView * > & views)680   std::vector<gfx::Rect> CalculateBoundsForDraggedViews(
681       const std::vector<TabSlotView*>& views) override {
682     DCHECK(!views.empty());
683 
684     std::vector<gfx::Rect> bounds;
685     const int overlap = TabStyle::GetTabOverlap();
686     int x = 0;
687     for (const TabSlotView* view : views) {
688       const int width = view->width();
689       bounds.push_back(gfx::Rect(x, 0, width, view->height()));
690       x += width - overlap;
691     }
692 
693     return bounds;
694   }
695 
SetBoundsForDrag(const std::vector<TabSlotView * > & views,const std::vector<gfx::Rect> & bounds)696   void SetBoundsForDrag(const std::vector<TabSlotView*>& views,
697                         const std::vector<gfx::Rect>& bounds) override {
698     tab_strip_->StopAnimating(false);
699     DCHECK_EQ(views.size(), bounds.size());
700     for (size_t i = 0; i < views.size(); ++i)
701       views[i]->SetBoundsRect(bounds[i]);
702     // Reset the layout size as we've effectively laid out a different size.
703     // This ensures a layout happens after the drag is done.
704     tab_strip_->last_layout_size_ = gfx::Size();
705     if (views.at(0)->group().has_value())
706       tab_strip_->UpdateTabGroupVisuals(views.at(0)->group().value());
707   }
708 
StartedDragging(const std::vector<TabSlotView * > & views)709   void StartedDragging(const std::vector<TabSlotView*>& views) override {
710     // Let the controller know that the user started dragging tabs.
711     tab_strip_->controller_->OnStartedDragging(
712         views.size() == static_cast<size_t>(tab_strip_->GetModelCount()));
713 
714     // Reset dragging state of existing tabs.
715     for (int i = 0; i < GetTabCount(); ++i)
716       GetTabAt(i)->set_dragging(false);
717 
718     for (size_t i = 0; i < views.size(); ++i) {
719       views[i]->set_dragging(true);
720       tab_strip_->bounds_animator_.StopAnimatingView(views[i]);
721     }
722 
723     // Move the dragged tabs to their ideal bounds.
724     tab_strip_->UpdateIdealBounds();
725 
726     // Sets the bounds of the dragged tab slots.
727     for (TabSlotView* view : views) {
728       if (view->GetTabSlotViewType() ==
729           TabSlotView::ViewType::kTabGroupHeader) {
730         view->SetBoundsRect(ideal_bounds(view->group().value()));
731       } else {
732         int tab_data_index = GetIndexOf(view);
733         DCHECK_NE(TabStripModel::kNoTab, tab_data_index);
734         view->SetBoundsRect(ideal_bounds(tab_data_index));
735       }
736     }
737 
738     tab_strip_->SetTabSlotVisibility();
739     tab_strip_->SchedulePaint();
740   }
741 
DraggedTabsDetached()742   void DraggedTabsDetached() override {
743     // Let the controller know that the user is not dragging this tabstrip's
744     // tabs anymore.
745     tab_strip_->controller_->OnStoppedDragging();
746   }
747 
StoppedDragging(const std::vector<TabSlotView * > & views,const std::vector<int> & initial_positions,bool move_only,bool completed)748   void StoppedDragging(const std::vector<TabSlotView*>& views,
749                        const std::vector<int>& initial_positions,
750                        bool move_only,
751                        bool completed) override {
752     // Let the controller know that the user stopped dragging tabs.
753     tab_strip_->controller_->OnStoppedDragging();
754 
755     if (move_only && tab_strip_->touch_layout_) {
756       if (completed)
757         tab_strip_->touch_layout_->SizeToFit();
758       else
759         SetIdealBoundsFromPositions(initial_positions);
760     }
761     bool is_first_view = true;
762     for (size_t i = 0; i < views.size(); ++i)
763       tab_strip_->StoppedDraggingView(views[i], &is_first_view);
764   }
765 
LayoutDraggedViewsAt(const std::vector<TabSlotView * > & views,TabSlotView * source_view,const gfx::Point & location,bool initial_drag)766   void LayoutDraggedViewsAt(const std::vector<TabSlotView*>& views,
767                             TabSlotView* source_view,
768                             const gfx::Point& location,
769                             bool initial_drag) override {
770     std::vector<gfx::Rect> bounds = CalculateBoundsForDraggedViews(views);
771     DCHECK_EQ(views.size(), bounds.size());
772 
773     int active_tab_model_index = GetIndexOf(source_view);
774     int active_tab_index = static_cast<int>(
775         std::find(views.begin(), views.end(), source_view) - views.begin());
776     for (size_t i = 0; i < views.size(); ++i) {
777       TabSlotView* view = views[i];
778       gfx::Rect new_bounds = bounds[i];
779       new_bounds.Offset(location.x(), location.y());
780       int consecutive_index =
781           active_tab_model_index - (active_tab_index - static_cast<int>(i));
782       // If this is the initial layout during a drag and the tabs aren't
783       // consecutive animate the view into position. Do the same if the tab is
784       // already animating (which means we previously caused it to animate).
785       if ((initial_drag && GetIndexOf(views[i]) != consecutive_index) ||
786           tab_strip_->bounds_animator_.IsAnimating(views[i])) {
787         tab_strip_->bounds_animator_.SetTargetBounds(views[i], new_bounds);
788       } else {
789         view->SetBoundsRect(new_bounds);
790       }
791     }
792     tab_strip_->SetTabSlotVisibility();
793     // The rightmost tab may have moved, which would change the tabstrip's
794     // preferred width.
795     tab_strip_->PreferredSizeChanged();
796   }
797 
798   // Forces the entire tabstrip to lay out.
ForceLayout()799   void ForceLayout() override {
800     tab_strip_->InvalidateLayout();
801     tab_strip_->CompleteAnimationAndLayout();
802   }
803 
804  private:
ideal_bounds(int i) const805   gfx::Rect ideal_bounds(int i) const { return tab_strip_->ideal_bounds(i); }
806 
ideal_bounds(tab_groups::TabGroupId group) const807   gfx::Rect ideal_bounds(tab_groups::TabGroupId group) const {
808     return tab_strip_->ideal_bounds(group);
809   }
810 
811   // Used by GetInsertionIndexForDraggedBounds() when the tabstrip is stacked.
GetInsertionIndexForDraggedBoundsStacked(const gfx::Rect & dragged_bounds,bool mouse_has_ever_moved_left,bool mouse_has_ever_moved_right) const812   base::Optional<int> GetInsertionIndexForDraggedBoundsStacked(
813       const gfx::Rect& dragged_bounds,
814       bool mouse_has_ever_moved_left,
815       bool mouse_has_ever_moved_right) const {
816     int active_index = *GetActiveTouchIndex();
817     // Search from the active index to the front of the tabstrip. Do this as
818     // tabs overlap each other from the active index.
819     base::Optional<int> index =
820         GetInsertionIndexFromReversed(dragged_bounds, active_index);
821     if (index != active_index)
822       return index;
823     if (!index)
824       return GetInsertionIndexFrom(dragged_bounds, active_index + 1,
825                                    base::nullopt);
826 
827     // The position to drag to corresponds to the active tab. If the
828     // next/previous tab is stacked, then shorten the distance used to determine
829     // insertion bounds. We do this as GetInsertionIndexFrom() uses the bounds
830     // of the tabs. When tabs are stacked the next/previous tab is on top of the
831     // tab.
832     if (active_index + 1 < GetTabCount() &&
833         tab_strip_->touch_layout_->IsStacked(active_index + 1)) {
834       index = GetInsertionIndexFrom(dragged_bounds, active_index + 1,
835                                     base::nullopt);
836       if (!index && ShouldDragToNextStackedTab(dragged_bounds, active_index,
837                                                mouse_has_ever_moved_right))
838         index = active_index + 1;
839       else if (index == -1)
840         index = active_index;
841     } else if (ShouldDragToPreviousStackedTab(dragged_bounds, active_index,
842                                               mouse_has_ever_moved_left)) {
843       index = active_index - 1;
844     }
845     return index;
846   }
847 
848   // Determines the index to insert tabs at. |dragged_bounds| is the bounds of
849   // the tab being dragged, |start| is the index of the tab to start looking
850   // from, and |group| is the currently dragged group, if any. The search
851   // proceeds to the end of the strip.
GetInsertionIndexFrom(const gfx::Rect & dragged_bounds,int start,base::Optional<tab_groups::TabGroupId> group) const852   base::Optional<int> GetInsertionIndexFrom(
853       const gfx::Rect& dragged_bounds,
854       int start,
855       base::Optional<tab_groups::TabGroupId> group) const {
856     const int last_tab = GetTabCount() - 1;
857     if (start < 0 || start > last_tab)
858       return base::nullopt;
859 
860     const int dragged_x = GetDraggedX(dragged_bounds);
861     if ((dragged_x < ideal_bounds(start).x() && !group.has_value()) ||
862         dragged_x > ideal_bounds(last_tab).right()) {
863       return base::nullopt;
864     }
865 
866     base::Optional<int> insertion_index;
867     for (int i = start; i <= last_tab; ++i) {
868       const gfx::Rect current_bounds = ideal_bounds(i);
869       int current_center = current_bounds.CenterPoint().x();
870 
871       base::Optional<tab_groups::TabGroupId> current_group =
872           tab_strip_->tab_at(i)->group();
873       if (current_group.has_value() &&
874           tab_strip_->controller()->IsGroupCollapsed(current_group.value())) {
875         current_center = ideal_bounds(current_group.value()).CenterPoint().x();
876       } else if (dragged_bounds.width() > current_bounds.width() &&
877                  dragged_bounds.x() < current_bounds.x()) {
878         current_center -= (dragged_bounds.width() - current_bounds.width());
879       }
880 
881       if (dragged_x < current_center) {
882         insertion_index = i;
883         break;
884       }
885     }
886 
887     if (!insertion_index.has_value())
888       return last_tab + 1;
889 
890     return GetInsertionIndexWithGroup(dragged_bounds, insertion_index.value(),
891                                       std::move(group));
892   }
893 
894   // Like GetInsertionIndexFrom(), but searches backwards from |start| to the
895   // beginning of the strip.
GetInsertionIndexFromReversed(const gfx::Rect & dragged_bounds,int start) const896   base::Optional<int> GetInsertionIndexFromReversed(
897       const gfx::Rect& dragged_bounds,
898       int start) const {
899     const int dragged_x = GetDraggedX(dragged_bounds);
900     if (start < 0 || start >= GetTabCount() ||
901         dragged_x >= ideal_bounds(start).right() ||
902         dragged_x < ideal_bounds(0).x())
903       return base::nullopt;
904 
905     for (int i = start; i >= 0; --i) {
906       if (dragged_x >= ideal_bounds(i).CenterPoint().x())
907         return i + 1;
908     }
909 
910     return 0;
911   }
912 
913   // Determines the index to insert at, accounting for the dragging group and
914   // other groups. |dragged_bounds| is the bounds of the tab being dragged,
915   // |candidate_index| is the naive insertion index found via
916   // GetInsertionIndexFrom, and |dragging_group| is the currently dragged
917   // group, if any. This is distinct from the group membership of the dragging
918   // tabs, and is only set when dragging by the group's header.
GetInsertionIndexWithGroup(const gfx::Rect & dragged_bounds,int candidate_index,base::Optional<tab_groups::TabGroupId> dragging_group) const919   int GetInsertionIndexWithGroup(
920       const gfx::Rect& dragged_bounds,
921       int candidate_index,
922       base::Optional<tab_groups::TabGroupId> dragging_group) const {
923     if (!dragging_group.has_value()) {
924       // Collapsed tabs occupy the same space and need additional checks to
925       // ensure we do not drag into a collapsed group.
926       base::Optional<tab_groups::TabGroupId> candidate_group =
927           tab_strip_->tab_at(candidate_index)->group();
928       if (candidate_group.has_value() &&
929           tab_strip_->controller()->IsGroupCollapsed(candidate_group.value()) &&
930           tab_strip_->controller()->GetActiveIndex() < candidate_index) {
931         return tab_strip_->controller()
932             ->ListTabsInGroup(candidate_group.value())
933             .back();
934       }
935       return candidate_index;
936     }
937 
938     const std::vector<int> dragging_tabs =
939         tab_strip_->controller()->ListTabsInGroup(dragging_group.value());
940     base::Optional<tab_groups::TabGroupId> other_group =
941         tab_strip_->tab_at(candidate_index)->group();
942 
943     // The other group will be the same as the dragging group if the user
944     // hasn't dragged beyond the boundaries of the current "gap". In this
945     // case, look ahead to where the dragging group would go, and sample
946     // the group that's currently there.
947     if (dragging_group == other_group) {
948       // |dragging_tabs| can only be empty if dragging in from another window,
949       // in which case |dragging_group| can't be the same as |other_group|.
950       DCHECK_GT(dragging_tabs.size(), 0u);
951       if (candidate_index <= dragging_tabs.front() ||
952           dragging_tabs.back() >= GetTabCount() - 1)
953         return dragging_tabs.front();
954 
955       other_group = tab_strip_->tab_at(dragging_tabs.back() + 1)->group();
956     }
957 
958     if (!other_group.has_value())
959       return candidate_index;
960 
961     const std::vector<int> other_tabs =
962         tab_strip_->controller()->ListTabsInGroup(other_group.value());
963 
964     if (other_tabs.size() == 0)
965       return candidate_index;
966 
967     // If the candidate index is in the middle of the other group, instead
968     // return the nearest insertion index that is not in the other group.
969     const int other_group_width =
970         ideal_bounds(other_tabs.back()).right() -
971         tab_strip_->group_header(other_group.value())->x();
972     int left_insertion_index = other_tabs.front();
973     if (dragging_tabs.size() > 0 && dragging_tabs.front() < other_tabs.front())
974       left_insertion_index = dragging_tabs.front();
975 
976     if (GetDraggedX(dragged_bounds) <
977         ideal_bounds(left_insertion_index).x() + other_group_width / 2)
978       return left_insertion_index;
979     return left_insertion_index + other_tabs.size();
980   }
981 
982   // Sets the ideal bounds x-coordinates to |positions|.
SetIdealBoundsFromPositions(const std::vector<int> & positions)983   void SetIdealBoundsFromPositions(const std::vector<int>& positions) {
984     if (static_cast<size_t>(GetTabCount()) != positions.size())
985       return;
986 
987     for (int i = 0; i < GetTabCount(); ++i) {
988       gfx::Rect bounds(ideal_bounds(i));
989       bounds.set_x(positions[i]);
990       tab_strip_->tabs_.set_ideal_bounds(i, bounds);
991     }
992   }
993 
994   TabStrip* const tab_strip_;
995 
996   // The controller for a drag initiated from a Tab. Valid for the lifetime of
997   // the drag session.
998   std::unique_ptr<TabDragController> drag_controller_;
999 
1000   base::WeakPtrFactory<TabDragContext> weak_factory_{this};
1001 };
1002 
1003 ///////////////////////////////////////////////////////////////////////////////
1004 // TabStrip, public:
1005 
TabStrip(std::unique_ptr<TabStripController> controller)1006 TabStrip::TabStrip(std::unique_ptr<TabStripController> controller)
1007     : controller_(std::move(controller)),
1008       layout_helper_(std::make_unique<TabStripLayoutHelper>(
1009           controller_.get(),
1010           base::BindRepeating(&TabStrip::tabs_view_model,
1011                               base::Unretained(this)))),
1012       drag_context_(std::make_unique<TabDragContextImpl>(this)) {
1013   Init();
1014   SetEventTargeter(std::make_unique<views::ViewTargeter>(this));
1015 }
1016 
~TabStrip()1017 TabStrip::~TabStrip() {
1018   // The animations may reference the tabs. Shut down the animation before we
1019   // delete the tabs.
1020   StopAnimating(false);
1021 
1022   // Disengage the drag controller before doing any additional cleanup. This
1023   // call can interact with child views so we can't reliably do it during member
1024   // destruction.
1025   drag_context_->DestroyDragController();
1026 
1027   // Make sure we unhook ourselves as a message loop observer so that we don't
1028   // crash in the case where the user closes the window after closing a tab
1029   // but before moving the mouse.
1030   RemoveMessageLoopObserver();
1031 
1032   hover_card_observer_.RemoveAll();
1033 
1034   // Since TabGroupViews expects be able to remove the views it creates, clear
1035   // |group_views_| before removing the remaining children below.
1036   group_views_.clear();
1037 
1038   // The child tabs may call back to us from their destructors. Delete them so
1039   // that if they call back we aren't in a weird state.
1040   RemoveAllChildViews(true);
1041 
1042   CHECK(!IsInObserverList());
1043 }
1044 
SetAvailableWidthCallback(base::RepeatingCallback<int ()> available_width_callback)1045 void TabStrip::SetAvailableWidthCallback(
1046     base::RepeatingCallback<int()> available_width_callback) {
1047   available_width_callback_ = available_width_callback;
1048 }
1049 
1050 // static
GetSizeNeededForViews(const std::vector<TabSlotView * > & views)1051 int TabStrip::GetSizeNeededForViews(const std::vector<TabSlotView*>& views) {
1052   int width = 0;
1053   for (const TabSlotView* view : views)
1054     width += view->width();
1055   if (!views.empty())
1056     width -= TabStyle::GetTabOverlap() * (views.size() - 1);
1057   return width;
1058 }
1059 
AddObserver(TabStripObserver * observer)1060 void TabStrip::AddObserver(TabStripObserver* observer) {
1061   observers_.AddObserver(observer);
1062 }
1063 
RemoveObserver(TabStripObserver * observer)1064 void TabStrip::RemoveObserver(TabStripObserver* observer) {
1065   observers_.RemoveObserver(observer);
1066 }
1067 
FrameColorsChanged()1068 void TabStrip::FrameColorsChanged() {
1069   for (int i = 0; i < tab_count(); ++i)
1070     tab_at(i)->FrameColorsChanged();
1071   UpdateContrastRatioValues();
1072   SchedulePaint();
1073 }
1074 
SetBackgroundOffset(int background_offset)1075 void TabStrip::SetBackgroundOffset(int background_offset) {
1076   if (background_offset != background_offset_) {
1077     background_offset_ = background_offset;
1078     SchedulePaint();
1079   }
1080 }
1081 
IsRectInWindowCaption(const gfx::Rect & rect)1082 bool TabStrip::IsRectInWindowCaption(const gfx::Rect& rect) {
1083   // If there is no control at this location, the hit is in the caption area.
1084   const views::View* v = GetEventHandlerForRect(rect);
1085   if (v == this)
1086     return true;
1087 
1088   // When the window has a top drag handle, a thin strip at the top of inactive
1089   // tabs and the new tab button is treated as part of the window drag handle,
1090   // to increase draggability.  This region starts 1 DIP above the top of the
1091   // separator.
1092   const int drag_handle_extension = TabStyle::GetDragHandleExtension(height());
1093 
1094   // Disable drag handle extension when tab shapes are visible.
1095   bool extend_drag_handle = !controller_->IsFrameCondensed() &&
1096                             !controller_->EverHasVisibleBackgroundTabShapes();
1097 
1098   // A hit on the tab is not in the caption unless it is in the thin strip
1099   // mentioned above.
1100   const int tab_index = tabs_.GetIndexOfView(v);
1101   if (IsValidModelIndex(tab_index)) {
1102     Tab* tab = tab_at(tab_index);
1103     gfx::Rect tab_drag_handle = tab->GetMirroredBounds();
1104     tab_drag_handle.set_height(drag_handle_extension);
1105     return extend_drag_handle && !tab->IsActive() &&
1106            tab_drag_handle.Intersects(rect);
1107   }
1108 
1109   // |v| is some other view (e.g. a close button in a tab) and therefore |rect|
1110   // is in client area.
1111   return false;
1112 }
1113 
IsPositionInWindowCaption(const gfx::Point & point)1114 bool TabStrip::IsPositionInWindowCaption(const gfx::Point& point) {
1115   return IsRectInWindowCaption(gfx::Rect(point, gfx::Size(1, 1)));
1116 }
1117 
IsTabStripCloseable() const1118 bool TabStrip::IsTabStripCloseable() const {
1119   return !drag_context_->IsDragSessionActive();
1120 }
1121 
IsTabStripEditable() const1122 bool TabStrip::IsTabStripEditable() const {
1123   return !drag_context_->IsDragSessionActive() &&
1124          !drag_context_->IsActiveDropTarget();
1125 }
1126 
IsTabCrashed(int tab_index) const1127 bool TabStrip::IsTabCrashed(int tab_index) const {
1128   return tab_at(tab_index)->data().IsCrashed();
1129 }
1130 
TabHasNetworkError(int tab_index) const1131 bool TabStrip::TabHasNetworkError(int tab_index) const {
1132   return tab_at(tab_index)->data().network_state == TabNetworkState::kError;
1133 }
1134 
GetTabAlertState(int tab_index) const1135 base::Optional<TabAlertState> TabStrip::GetTabAlertState(int tab_index) const {
1136   return Tab::GetAlertStateToShow(tab_at(tab_index)->data().alert_state);
1137 }
1138 
UpdateLoadingAnimations(const base::TimeDelta & elapsed_time)1139 void TabStrip::UpdateLoadingAnimations(const base::TimeDelta& elapsed_time) {
1140   for (int i = 0; i < tab_count(); i++)
1141     tab_at(i)->StepLoadingAnimation(elapsed_time);
1142 }
1143 
SetStackedLayout(bool stacked_layout)1144 void TabStrip::SetStackedLayout(bool stacked_layout) {
1145   if (stacked_layout == stacked_layout_)
1146     return;
1147 
1148   stacked_layout_ = stacked_layout;
1149   SetResetToShrinkOnExit(false);
1150   SwapLayoutIfNecessary();
1151 
1152   // When transitioning to stacked try to keep the active tab from moving.
1153   const int active_index = controller_->GetActiveIndex();
1154   if (touch_layout_ && active_index != -1) {
1155     touch_layout_->SetActiveTabLocation(ideal_bounds(active_index).x());
1156     AnimateToIdealBounds();
1157   }
1158 
1159   for (int i = 0; i < tab_count(); ++i)
1160     tab_at(i)->Layout();
1161 }
1162 
AddTabAt(int model_index,TabRendererData data,bool is_active)1163 void TabStrip::AddTabAt(int model_index, TabRendererData data, bool is_active) {
1164   Tab* tab = new Tab(this);
1165   tab->set_context_menu_controller(&context_menu_controller_);
1166   tab->AddObserver(this);
1167   AddChildViewAt(tab, GetViewInsertionIndex(tab, base::nullopt, model_index));
1168   const bool pinned = data.pinned;
1169   tabs_.Add(tab, model_index);
1170   selected_tabs_.IncrementFrom(model_index);
1171 
1172   // Setting data must come after all state from the model has been updated
1173   // above for the tab. Accessibility, in particular, reacts to data changed
1174   // callbacks.
1175   tab->SetData(std::move(data));
1176 
1177   if (touch_layout_) {
1178     int add_types = 0;
1179     if (pinned)
1180       add_types |= StackedTabStripLayout::kAddTypePinned;
1181     if (is_active)
1182       add_types |= StackedTabStripLayout::kAddTypeActive;
1183     touch_layout_->AddTab(model_index, add_types,
1184                           UpdateIdealBoundsForPinnedTabs(nullptr));
1185   }
1186 
1187   // Don't animate the first tab, it looks weird, and don't animate anything
1188   // if the containing window isn't visible yet.
1189   if (tab_count() > 1 && GetWidget() && GetWidget()->IsVisible()) {
1190     StartInsertTabAnimation(model_index,
1191                             pinned ? TabPinned::kPinned : TabPinned::kUnpinned);
1192   } else {
1193     layout_helper_->InsertTabAt(
1194         model_index, tab, pinned ? TabPinned::kPinned : TabPinned::kUnpinned);
1195     CompleteAnimationAndLayout();
1196   }
1197 
1198   SwapLayoutIfNecessary();
1199   UpdateAccessibleTabIndices();
1200 
1201   for (TabStripObserver& observer : observers_)
1202     observer.OnTabAdded(model_index);
1203 
1204   // Stop dragging when a new tab is added and dragging a window. Doing
1205   // otherwise results in a confusing state if the user attempts to reattach. We
1206   // could allow this and make TabDragController update itself during the add,
1207   // but this comes up infrequently enough that it's not worth the complexity.
1208   //
1209   // At the start of AddTabAt() the model and tabs are out sync. Any queries to
1210   // find a tab given a model index can go off the end of |tabs_|. As such, it
1211   // is important that we complete the drag *after* adding the tab so that the
1212   // model and tabstrip are in sync.
1213   if (!drag_context_->IsMutating() && drag_context_->IsDraggingWindow())
1214     EndDrag(END_DRAG_COMPLETE);
1215 
1216   Profile* profile = controller()->GetProfile();
1217   if (profile) {
1218     if (profile->IsGuestSession() || profile->IsEphemeralGuestProfile())
1219       base::UmaHistogramCounts100("Tab.Count.Guest", tab_count());
1220     else if (profile->IsIncognitoProfile())
1221       base::UmaHistogramCounts100("Tab.Count.Incognito", tab_count());
1222   }
1223 }
1224 
MoveTab(int from_model_index,int to_model_index,TabRendererData data)1225 void TabStrip::MoveTab(int from_model_index,
1226                        int to_model_index,
1227                        TabRendererData data) {
1228   DCHECK_GT(tabs_.view_size(), 0);
1229 
1230   Tab* moving_tab = tab_at(from_model_index);
1231   const bool pinned = data.pinned;
1232   moving_tab->SetData(std::move(data));
1233 
1234   ReorderChildView(
1235       moving_tab,
1236       GetViewInsertionIndex(moving_tab, from_model_index, to_model_index));
1237 
1238   if (touch_layout_) {
1239     tabs_.MoveViewOnly(from_model_index, to_model_index);
1240     int pinned_count = 0;
1241     const int start_x = UpdateIdealBoundsForPinnedTabs(&pinned_count);
1242     touch_layout_->MoveTab(from_model_index, to_model_index,
1243                            controller_->GetActiveIndex(), start_x,
1244                            pinned_count);
1245   } else {
1246     tabs_.Move(from_model_index, to_model_index);
1247   }
1248   selected_tabs_.Move(from_model_index, to_model_index, /*length=*/1);
1249 
1250   layout_helper_->MoveTab(moving_tab->group(), from_model_index,
1251                           to_model_index);
1252   layout_helper_->SetTabPinned(
1253       to_model_index, pinned ? TabPinned::kPinned : TabPinned::kUnpinned);
1254   StartMoveTabAnimation();
1255   SwapLayoutIfNecessary();
1256 
1257   UpdateAccessibleTabIndices();
1258 
1259   for (TabStripObserver& observer : observers_)
1260     observer.OnTabMoved(from_model_index, to_model_index);
1261 }
1262 
RemoveTabAt(content::WebContents * contents,int model_index,bool was_active)1263 void TabStrip::RemoveTabAt(content::WebContents* contents,
1264                            int model_index,
1265                            bool was_active) {
1266   StartRemoveTabAnimation(model_index, was_active);
1267 
1268   SwapLayoutIfNecessary();
1269 
1270   UpdateAccessibleTabIndices();
1271 
1272   UpdateHoverCard(nullptr);
1273 
1274   for (TabStripObserver& observer : observers_)
1275     observer.OnTabRemoved(model_index);
1276 
1277   // Stop dragging when a new tab is removed and dragging a window. Doing
1278   // otherwise results in a confusing state if the user attempts to reattach. We
1279   // could allow this and make TabDragController update itself during the
1280   // remove operation, but this comes up infrequently enough that it's not worth
1281   // the complexity.
1282   //
1283   // At the start of RemoveTabAt() the model and tabs are out sync. Any queries
1284   // to find a tab given a model index can go off the end of |tabs_|. As such,
1285   // it is important that we complete the drag *after* removing the tab so that
1286   // the model and tabstrip are in sync.
1287   if (!drag_context_->IsMutating() && drag_context_->IsDraggingTab(contents))
1288     EndDrag(END_DRAG_COMPLETE);
1289 }
1290 
SetTabData(int model_index,TabRendererData data)1291 void TabStrip::SetTabData(int model_index, TabRendererData data) {
1292   Tab* tab = tab_at(model_index);
1293   const bool pinned = data.pinned;
1294   const bool pinned_state_changed = tab->data().pinned != pinned;
1295   tab->SetData(std::move(data));
1296 
1297   if (HoverCardIsShowingForTab(tab))
1298     UpdateHoverCard(tab);
1299 
1300   if (pinned_state_changed) {
1301     if (touch_layout_) {
1302       int pinned_tab_count = 0;
1303       int start_x = UpdateIdealBoundsForPinnedTabs(&pinned_tab_count);
1304       touch_layout_->SetXAndPinnedCount(start_x, pinned_tab_count);
1305     }
1306 
1307     layout_helper_->SetTabPinned(
1308         model_index, pinned ? TabPinned::kPinned : TabPinned::kUnpinned);
1309     if (GetWidget() && GetWidget()->IsVisible())
1310       StartPinnedTabAnimation();
1311     else
1312       CompleteAnimationAndLayout();
1313   }
1314   SwapLayoutIfNecessary();
1315 }
1316 
AddTabToGroup(base::Optional<tab_groups::TabGroupId> group,int model_index)1317 void TabStrip::AddTabToGroup(base::Optional<tab_groups::TabGroupId> group,
1318                              int model_index) {
1319   tab_at(model_index)->set_group(group);
1320 
1321   // Expand the group if the tab that is getting grouped is the active tab. This
1322   // can result in the group expanding in a series of actions where the final
1323   // active tab is not in the group.
1324   if (model_index == selected_tabs_.active() && group.has_value() &&
1325       controller()->IsGroupCollapsed(group.value())) {
1326     controller()->ToggleTabGroupCollapsedState(
1327         group.value(), ToggleTabGroupCollapsedStateOrigin::kImplicitAction);
1328   }
1329 
1330   if (group.has_value())
1331     ExitTabClosingMode();
1332 }
1333 
OnGroupCreated(const tab_groups::TabGroupId & group)1334 void TabStrip::OnGroupCreated(const tab_groups::TabGroupId& group) {
1335   auto group_view = std::make_unique<TabGroupViews>(this, group);
1336   layout_helper_->InsertGroupHeader(group, group_view->header());
1337   group_views_[group] = std::move(group_view);
1338   SetStackedLayout(false);
1339 }
1340 
OnGroupEditorOpened(const tab_groups::TabGroupId & group)1341 void TabStrip::OnGroupEditorOpened(const tab_groups::TabGroupId& group) {
1342   // The context menu relies on a Browser object which is not provided in
1343   // TabStripTest.
1344   if (this->controller()->GetBrowser()) {
1345     group_views_[group]->header()->ShowContextMenuForViewImpl(
1346         this, gfx::Point(), ui::MENU_SOURCE_NONE);
1347   }
1348 }
1349 
OnGroupContentsChanged(const tab_groups::TabGroupId & group)1350 void TabStrip::OnGroupContentsChanged(const tab_groups::TabGroupId& group) {
1351   // The group header may be in the wrong place if the tab didn't actually
1352   // move in terms of model indices.
1353   OnGroupMoved(group);
1354   UpdateIdealBounds();
1355   AnimateToIdealBounds();
1356 }
1357 
OnGroupVisualsChanged(const tab_groups::TabGroupId & group)1358 void TabStrip::OnGroupVisualsChanged(const tab_groups::TabGroupId& group) {
1359   group_views_[group]->OnGroupVisualsChanged();
1360   // The group title may have changed size, so update bounds.
1361   UpdateIdealBounds();
1362   AnimateToIdealBounds();
1363 }
1364 
ToggleTabGroup(const tab_groups::TabGroupId & group,bool is_collapsing,ToggleTabGroupCollapsedStateOrigin origin)1365 void TabStrip::ToggleTabGroup(const tab_groups::TabGroupId& group,
1366                               bool is_collapsing,
1367                               ToggleTabGroupCollapsedStateOrigin origin) {
1368   if (is_collapsing && GetWidget()) {
1369     in_tab_close_ = true;
1370     if (origin == ToggleTabGroupCollapsedStateOrigin::kMouse) {
1371       AddMessageLoopObserver();
1372     } else if (origin == ToggleTabGroupCollapsedStateOrigin::kGesture) {
1373       StartResizeLayoutTabsFromTouchTimer();
1374     } else {
1375       return;
1376     }
1377 
1378     // The current group header is expanded which is slightly smaller than the
1379     // size when the header is collapsed. Calculate the size of the header once
1380     // collapsed for maintaining its position. See
1381     // TabGroupHeader::CalculateWidth() for more details.
1382     const int empty_group_title_adjustment =
1383         GetGroupTitle(group).empty() ? 2 : -2;
1384     const int title_chip_width =
1385         group_views_[group]->header()->GetTabSizeInfo().standard_width -
1386         2 * TabStyle::GetTabOverlap() - empty_group_title_adjustment;
1387     const int collapsed_header_width =
1388         title_chip_width + 2 * TabGroupUnderline::GetStrokeInset();
1389     override_available_width_for_tabs_ =
1390         ideal_bounds(GetModelCount() - 1).right() -
1391         group_views_[group]->GetBounds().width() + collapsed_header_width;
1392   } else {
1393     ExitTabClosingMode();
1394   }
1395 }
1396 
OnGroupMoved(const tab_groups::TabGroupId & group)1397 void TabStrip::OnGroupMoved(const tab_groups::TabGroupId& group) {
1398   DCHECK(group_views_[group]);
1399 
1400   layout_helper_->UpdateGroupHeaderIndex(group);
1401 
1402   TabGroupHeader* group_header = group_views_[group]->header();
1403   const int first_tab = controller_->ListTabsInGroup(group).front();
1404   const int header_index = GetIndexOf(group_header);
1405   const int first_tab_index = GetIndexOf(tab_at(first_tab));
1406 
1407   // The header should be just before the first tab. If it isn't, reorder the
1408   // header such that it is. Note that the index to reorder to is different
1409   // depending on whether the header is before or after the tab, since the
1410   // header itself occupies an index.
1411   if (header_index < first_tab_index - 1)
1412     ReorderChildView(group_header, first_tab_index - 1);
1413   if (header_index > first_tab_index - 1)
1414     ReorderChildView(group_header, first_tab_index);
1415 }
1416 
OnGroupClosed(const tab_groups::TabGroupId & group)1417 void TabStrip::OnGroupClosed(const tab_groups::TabGroupId& group) {
1418   bounds_animator_.StopAnimatingView(group_header(group));
1419   layout_helper_->RemoveGroupHeader(group);
1420 
1421   UpdateIdealBounds();
1422   AnimateToIdealBounds();
1423   group_views_.erase(group);
1424 }
1425 
ShiftGroupLeft(const tab_groups::TabGroupId & group)1426 void TabStrip::ShiftGroupLeft(const tab_groups::TabGroupId& group) {
1427   ShiftGroupRelative(group, -1);
1428 }
1429 
ShiftGroupRight(const tab_groups::TabGroupId & group)1430 void TabStrip::ShiftGroupRight(const tab_groups::TabGroupId& group) {
1431   ShiftGroupRelative(group, 1);
1432 }
1433 
ShouldTabBeVisible(const Tab * tab) const1434 bool TabStrip::ShouldTabBeVisible(const Tab* tab) const {
1435   // Detached tabs should always be invisible (as they close).
1436   if (tab->detached())
1437     return false;
1438 
1439   // When stacking tabs, all tabs should always be visible.
1440   if (stacked_layout_)
1441     return true;
1442 
1443   // If the tab is currently clipped by the trailing edge of the strip, it
1444   // shouldn't be visible.
1445   const int right_edge = tab->bounds().right();
1446   const int tabstrip_right =
1447       tab->dragging() ? drag_context_->GetTabDragAreaWidth() : width();
1448   if (right_edge > tabstrip_right)
1449     return false;
1450 
1451   // Non-clipped dragging tabs should always be visible.
1452   if (tab->dragging())
1453     return true;
1454 
1455   // Let all non-clipped closing tabs be visible.  These will probably finish
1456   // closing before the user changes the active tab, so there's little reason to
1457   // try and make the more complex logic below apply.
1458   if (tab->closing())
1459     return true;
1460 
1461   // Now we need to check whether the tab isn't currently clipped, but could
1462   // become clipped if we changed the active tab, widening either this tab or
1463   // the tabstrip portion before it.
1464 
1465   // Pinned tabs don't change size when activated, so any tab in the pinned tab
1466   // region is safe.
1467   if (tab->data().pinned)
1468     return true;
1469 
1470   // If the active tab is on or before this tab, we're safe.
1471   if (controller_->GetActiveIndex() <= GetModelIndexOf(tab))
1472     return true;
1473 
1474   // We need to check what would happen if the active tab were to move to this
1475   // tab or before.
1476   return (right_edge + GetActiveTabWidth() - GetInactiveTabWidth()) <=
1477          tabstrip_right;
1478 }
1479 
ShouldDrawStrokes() const1480 bool TabStrip::ShouldDrawStrokes() const {
1481   // If the controller says we can't draw strokes, don't.
1482   if (!controller_->CanDrawStrokes())
1483     return false;
1484 
1485   // The tabstrip normally avoids strokes and relies on the active tab
1486   // contrasting sufficiently with the frame background.  When there isn't
1487   // enough contrast, fall back to a stroke.  Always compute the contrast ratio
1488   // against the active frame color, to avoid toggling the stroke on and off as
1489   // the window activation state changes.
1490   constexpr float kMinimumContrastRatioForOutlines = 1.3f;
1491   const SkColor background_color = GetTabBackgroundColor(
1492       TabActive::kActive, BrowserFrameActiveState::kActive);
1493   const SkColor frame_color =
1494       controller_->GetFrameColor(BrowserFrameActiveState::kActive);
1495   const float contrast_ratio =
1496       color_utils::GetContrastRatio(background_color, frame_color);
1497   if (contrast_ratio < kMinimumContrastRatioForOutlines)
1498     return true;
1499 
1500   // Don't want to have to run a full feature query every time this function is
1501   // called.
1502   static const bool tab_outlines_in_low_contrast =
1503       base::FeatureList::IsEnabled(features::kTabOutlinesInLowContrastThemes);
1504   if (tab_outlines_in_low_contrast) {
1505     constexpr float kMinimumAbsoluteContrastForOutlines = 0.2f;
1506     const float background_luminance =
1507         color_utils::GetRelativeLuminance(background_color);
1508     const float frame_luminance =
1509         color_utils::GetRelativeLuminance(frame_color);
1510     const float contrast_difference =
1511         std::fabs(background_luminance - frame_luminance);
1512     if (contrast_difference < kMinimumAbsoluteContrastForOutlines)
1513       return true;
1514   }
1515 
1516   return false;
1517 }
1518 
SetSelection(const ui::ListSelectionModel & new_selection)1519 void TabStrip::SetSelection(const ui::ListSelectionModel& new_selection) {
1520   DCHECK_GE(new_selection.active(), 0)
1521       << "We should never transition to a state where no tab is active.";
1522   Tab* const new_active_tab = tab_at(new_selection.active());
1523   Tab* const old_active_tab =
1524       selected_tabs_.active() >= 0 ? tab_at(selected_tabs_.active()) : nullptr;
1525 
1526   if (new_active_tab != old_active_tab) {
1527     if (old_active_tab) {
1528       old_active_tab->ActiveStateChanged();
1529       if (old_active_tab->group().has_value())
1530         UpdateTabGroupVisuals(old_active_tab->group().value());
1531     }
1532     if (new_active_tab->group().has_value()) {
1533       const tab_groups::TabGroupId new_group = new_active_tab->group().value();
1534       // If the tab that is about to be activated is in a collapsed group,
1535       // automatically expand the group.
1536       if (controller()->IsGroupCollapsed(new_group))
1537         controller()->ToggleTabGroupCollapsedState(
1538             new_group, ToggleTabGroupCollapsedStateOrigin::kImplicitAction);
1539       UpdateTabGroupVisuals(new_group);
1540     }
1541 
1542     new_active_tab->ActiveStateChanged();
1543     layout_helper_->SetActiveTab(selected_tabs_.active(),
1544                                  new_selection.active());
1545   }
1546 
1547   if (touch_layout_) {
1548     touch_layout_->SetActiveIndex(new_selection.active());
1549     // Only start an animation if we need to. Otherwise clicking on an
1550     // unselected tab and dragging won't work because dragging is only allowed
1551     // if not animating.
1552     if (!views::ViewModelUtils::IsAtIdealBounds(tabs_))
1553       AnimateToIdealBounds();
1554     SchedulePaint();
1555   } else {
1556     if (GetActiveTabWidth() == GetInactiveTabWidth()) {
1557       // When tabs are wide enough, selecting a new tab cannot change the
1558       // ideal bounds, so only a repaint is necessary.
1559       SchedulePaint();
1560     } else if (IsAnimating()) {
1561       // The selection change will have modified the ideal bounds of the tabs
1562       // in |selected_tabs_| and |new_selection|.  We need to recompute.
1563       // Note: This is safe even if we're in the midst of mouse-based tab
1564       // closure--we won't expand the tabstrip back to the full window
1565       // width--because PrepareForCloseAt() will have set
1566       // |override_available_width_for_tabs_| already.
1567       UpdateIdealBounds();
1568       AnimateToIdealBounds();
1569     } else {
1570       // As in the animating case above, the selection change will have
1571       // affected the desired bounds of the tabs, but since we're not animating
1572       // we can just snap to the new bounds.
1573       CompleteAnimationAndLayout();
1574     }
1575   }
1576 
1577   // Use STLSetDifference to get the indices of elements newly selected
1578   // and no longer selected, since selected_indices() is always sorted.
1579   ui::ListSelectionModel::SelectedIndices no_longer_selected =
1580       base::STLSetDifference<ui::ListSelectionModel::SelectedIndices>(
1581           selected_tabs_.selected_indices(), new_selection.selected_indices());
1582   ui::ListSelectionModel::SelectedIndices newly_selected =
1583       base::STLSetDifference<ui::ListSelectionModel::SelectedIndices>(
1584           new_selection.selected_indices(), selected_tabs_.selected_indices());
1585 
1586   new_active_tab->NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true);
1587   selected_tabs_ = new_selection;
1588 
1589   UpdateHoverCard(nullptr);
1590   // The hover cards seen count is reset when the active tab is changed by any
1591   // event. Note TabStrip::SelectTab does not capture tab changes triggered by
1592   // the keyboard.
1593   if (base::FeatureList::IsEnabled(features::kTabHoverCards) && hover_card_)
1594     hover_card_->reset_hover_cards_seen_count();
1595 
1596   // Notify all tabs whose selected state changed.
1597   for (auto tab_index :
1598        base::STLSetUnion<ui::ListSelectionModel::SelectedIndices>(
1599            no_longer_selected, newly_selected)) {
1600     tab_at(tab_index)->SelectedStateChanged();
1601   }
1602 }
1603 
OnWidgetActivationChanged(views::Widget * widget,bool active)1604 void TabStrip::OnWidgetActivationChanged(views::Widget* widget, bool active) {
1605   if (active && selected_tabs_.active() >= 0) {
1606     // When the browser window is activated, fire a selection event on the
1607     // currently active tab, to help enable per-tab modes in assistive
1608     // technologies.
1609     tab_at(selected_tabs_.active())
1610         ->NotifyAccessibilityEvent(ax::mojom::Event::kSelection, true);
1611   }
1612 }
1613 
SetTabNeedsAttention(int model_index,bool attention)1614 void TabStrip::SetTabNeedsAttention(int model_index, bool attention) {
1615   tab_at(model_index)->SetTabNeedsAttention(attention);
1616 }
1617 
ideal_bounds(tab_groups::TabGroupId group) const1618 const gfx::Rect& TabStrip::ideal_bounds(tab_groups::TabGroupId group) const {
1619   return layout_helper_->group_header_ideal_bounds().at(group);
1620 }
1621 
GetModelIndexOf(const TabSlotView * view) const1622 int TabStrip::GetModelIndexOf(const TabSlotView* view) const {
1623   return tabs_.GetIndexOfView(view);
1624 }
1625 
GetModelCount() const1626 int TabStrip::GetModelCount() const {
1627   return controller_->GetCount();
1628 }
1629 
IsValidModelIndex(int model_index) const1630 bool TabStrip::IsValidModelIndex(int model_index) const {
1631   return controller_->IsValidIndex(model_index);
1632 }
1633 
GetDragContext()1634 TabDragContext* TabStrip::GetDragContext() {
1635   return drag_context_.get();
1636 }
1637 
GetPinnedTabCount() const1638 int TabStrip::GetPinnedTabCount() const {
1639   return layout_helper_->GetPinnedTabCount();
1640 }
1641 
IsAnimating() const1642 bool TabStrip::IsAnimating() const {
1643   return bounds_animator_.IsAnimating();
1644 }
1645 
StopAnimating(bool layout)1646 void TabStrip::StopAnimating(bool layout) {
1647   if (!IsAnimating())
1648     return;
1649 
1650   bounds_animator_.Cancel();
1651 
1652   if (layout)
1653     CompleteAnimationAndLayout();
1654 }
1655 
GetFocusedTabIndex() const1656 base::Optional<int> TabStrip::GetFocusedTabIndex() const {
1657   for (int i = 0; i < tabs_.view_size(); ++i) {
1658     if (tabs_.view_at(i)->HasFocus())
1659       return i;
1660   }
1661   return base::nullopt;
1662 }
1663 
GetTabViewForPromoAnchor(int index_hint)1664 views::View* TabStrip::GetTabViewForPromoAnchor(int index_hint) {
1665   return tab_at(base::ClampToRange(index_hint, 0, tab_count() - 1));
1666 }
1667 
GetDefaultFocusableChild()1668 views::View* TabStrip::GetDefaultFocusableChild() {
1669   int active = controller_->GetActiveIndex();
1670   return active != TabStripModel::kNoTab ? tab_at(active) : nullptr;
1671 }
1672 
GetSelectionModel() const1673 const ui::ListSelectionModel& TabStrip::GetSelectionModel() const {
1674   return controller_->GetSelectionModel();
1675 }
1676 
SupportsMultipleSelection()1677 bool TabStrip::SupportsMultipleSelection() {
1678   // Currently we only allow single selection in touch layout mode.
1679   return touch_layout_ == nullptr;
1680 }
1681 
ShouldHideCloseButtonForTab(Tab * tab) const1682 bool TabStrip::ShouldHideCloseButtonForTab(Tab* tab) const {
1683   if (tab->IsActive())
1684     return false;
1685   return !!touch_layout_;
1686 }
1687 
SelectTab(Tab * tab,const ui::Event & event)1688 void TabStrip::SelectTab(Tab* tab, const ui::Event& event) {
1689   int model_index = GetModelIndexOf(tab);
1690 
1691   if (IsValidModelIndex(model_index)) {
1692     if (!tab->IsActive()) {
1693       int current_selection = selected_tabs_.active();
1694       base::UmaHistogramSparse("Tabs.DesktopTabOffsetOfSwitch",
1695                                current_selection - model_index);
1696       base::UmaHistogramSparse("Tabs.DesktopTabOffsetFromLeftOfSwitch",
1697                                model_index);
1698       base::UmaHistogramSparse("Tabs.DesktopTabOffsetFromRightOfSwitch",
1699                                GetModelCount() - model_index - 1);
1700 
1701       if (tab->group().has_value()) {
1702         base::RecordAction(
1703             base::UserMetricsAction("TabGroups_SwitchGroupedTab"));
1704       }
1705     }
1706 
1707     // Report histogram metrics for the number of tab hover cards seen before
1708     // a tab is selected by mouse press.
1709     if (base::FeatureList::IsEnabled(features::kTabHoverCards) && hover_card_ &&
1710         event.type() == ui::ET_MOUSE_PRESSED && !tab->IsActive()) {
1711       hover_card_->RecordHoverCardsSeenRatioMetric();
1712     }
1713 
1714     controller_->SelectTab(model_index, event);
1715   }
1716 }
1717 
ExtendSelectionTo(Tab * tab)1718 void TabStrip::ExtendSelectionTo(Tab* tab) {
1719   int model_index = GetModelIndexOf(tab);
1720   if (IsValidModelIndex(model_index))
1721     controller_->ExtendSelectionTo(model_index);
1722 }
1723 
ToggleSelected(Tab * tab)1724 void TabStrip::ToggleSelected(Tab* tab) {
1725   int model_index = GetModelIndexOf(tab);
1726   if (IsValidModelIndex(model_index))
1727     controller_->ToggleSelected(model_index);
1728 }
1729 
AddSelectionFromAnchorTo(Tab * tab)1730 void TabStrip::AddSelectionFromAnchorTo(Tab* tab) {
1731   int model_index = GetModelIndexOf(tab);
1732   if (IsValidModelIndex(model_index))
1733     controller_->AddSelectionFromAnchorTo(model_index);
1734 }
1735 
CloseTab(Tab * tab,CloseTabSource source)1736 void TabStrip::CloseTab(Tab* tab, CloseTabSource source) {
1737   if (tab->closing()) {
1738     // If the tab is already closing, close the next tab. We do this so that the
1739     // user can rapidly close tabs by clicking the close button and not have
1740     // the animations interfere with that.
1741     std::vector<Tab*> all_tabs = layout_helper_->GetTabs();
1742     auto it = std::find(all_tabs.begin(), all_tabs.end(), tab);
1743     while (it < all_tabs.end() && (*it)->closing()) {
1744       it++;
1745     }
1746 
1747     if (it == all_tabs.end())
1748       return;
1749     tab = *it;
1750   }
1751 
1752   CloseTabInternal(GetModelIndexOf(tab), source);
1753 }
1754 
ShiftTabNext(Tab * tab)1755 void TabStrip::ShiftTabNext(Tab* tab) {
1756   ShiftTabRelative(tab, 1);
1757 }
1758 
ShiftTabPrevious(Tab * tab)1759 void TabStrip::ShiftTabPrevious(Tab* tab) {
1760   ShiftTabRelative(tab, -1);
1761 }
1762 
MoveTabFirst(Tab * tab)1763 void TabStrip::MoveTabFirst(Tab* tab) {
1764   if (tab->closing())
1765     return;
1766 
1767   const int start_index = GetModelIndexOf(tab);
1768   if (!IsValidModelIndex(start_index))
1769     return;
1770 
1771   int target_index = 0;
1772   if (!controller_->IsTabPinned(start_index)) {
1773     while (target_index < start_index && controller_->IsTabPinned(target_index))
1774       ++target_index;
1775   }
1776 
1777   if (!IsValidModelIndex(target_index))
1778     return;
1779 
1780   if (target_index != start_index)
1781     controller_->MoveTab(start_index, target_index);
1782 
1783   // The tab may unintentionally land in the first group in the tab strip, so we
1784   // remove the group to ensure consistent behavior. Even if the tab is already
1785   // at the front, it should "move" out of its current group.
1786   if (tab->group().has_value())
1787     controller_->RemoveTabFromGroup(target_index);
1788 
1789   GetViewAccessibility().AnnounceText(
1790       l10n_util::GetStringUTF16(IDS_TAB_AX_ANNOUNCE_MOVED_FIRST));
1791 }
1792 
MoveTabLast(Tab * tab)1793 void TabStrip::MoveTabLast(Tab* tab) {
1794   if (tab->closing())
1795     return;
1796 
1797   const int start_index = GetModelIndexOf(tab);
1798   if (!IsValidModelIndex(start_index))
1799     return;
1800 
1801   int target_index;
1802   if (controller_->IsTabPinned(start_index)) {
1803     int temp_index = start_index + 1;
1804     while (temp_index < tab_count() && controller_->IsTabPinned(temp_index))
1805       ++temp_index;
1806     target_index = temp_index - 1;
1807   } else {
1808     target_index = tab_count() - 1;
1809   }
1810 
1811   if (!IsValidModelIndex(target_index))
1812     return;
1813 
1814   if (target_index != start_index)
1815     controller_->MoveTab(start_index, target_index);
1816 
1817   // The tab may unintentionally land in the last group in the tab strip, so we
1818   // remove the group to ensure consistent behavior. Even if the tab is already
1819   // at the back, it should "move" out of its current group.
1820   if (tab->group().has_value())
1821     controller_->RemoveTabFromGroup(target_index);
1822 
1823   GetViewAccessibility().AnnounceText(
1824       l10n_util::GetStringUTF16(IDS_TAB_AX_ANNOUNCE_MOVED_LAST));
1825 }
1826 
ShowContextMenuForTab(Tab * tab,const gfx::Point & p,ui::MenuSourceType source_type)1827 void TabStrip::ShowContextMenuForTab(Tab* tab,
1828                                      const gfx::Point& p,
1829                                      ui::MenuSourceType source_type) {
1830   controller_->ShowContextMenuForTab(tab, p, source_type);
1831 }
1832 
IsActiveTab(const Tab * tab) const1833 bool TabStrip::IsActiveTab(const Tab* tab) const {
1834   int model_index = GetModelIndexOf(tab);
1835   return IsValidModelIndex(model_index) &&
1836          controller_->IsActiveTab(model_index);
1837 }
1838 
IsTabSelected(const Tab * tab) const1839 bool TabStrip::IsTabSelected(const Tab* tab) const {
1840   int model_index = GetModelIndexOf(tab);
1841   return IsValidModelIndex(model_index) &&
1842          controller_->IsTabSelected(model_index);
1843 }
1844 
IsTabPinned(const Tab * tab) const1845 bool TabStrip::IsTabPinned(const Tab* tab) const {
1846   if (tab->closing())
1847     return false;
1848 
1849   int model_index = GetModelIndexOf(tab);
1850   return IsValidModelIndex(model_index) &&
1851          controller_->IsTabPinned(model_index);
1852 }
1853 
IsTabFirst(const Tab * tab) const1854 bool TabStrip::IsTabFirst(const Tab* tab) const {
1855   return GetModelIndexOf(tab) == 0;
1856 }
1857 
IsFocusInTabs() const1858 bool TabStrip::IsFocusInTabs() const {
1859   return GetFocusManager() && Contains(GetFocusManager()->GetFocusedView());
1860 }
1861 
MaybeStartDrag(TabSlotView * source,const ui::LocatedEvent & event,const ui::ListSelectionModel & original_selection)1862 void TabStrip::MaybeStartDrag(
1863     TabSlotView* source,
1864     const ui::LocatedEvent& event,
1865     const ui::ListSelectionModel& original_selection) {
1866   // Don't accidentally start any drag operations during animations if the
1867   // mouse is down... during an animation tabs are being resized automatically,
1868   // so the View system can misinterpret this easily if the mouse is down that
1869   // the user is dragging.
1870   if (IsAnimating() || controller_->HasAvailableDragActions() == 0)
1871     return;
1872 
1873   // Check that the source is either a valid tab or a tab group header, which
1874   // are the only valid drag targets.
1875   if (!IsValidModelIndex(GetModelIndexOf(source))) {
1876     DCHECK_EQ(source->GetTabSlotViewType(),
1877               TabSlotView::ViewType::kTabGroupHeader);
1878   }
1879 
1880   drag_context_->MaybeStartDrag(source, event, original_selection);
1881 }
1882 
ContinueDrag(views::View * view,const ui::LocatedEvent & event)1883 void TabStrip::ContinueDrag(views::View* view, const ui::LocatedEvent& event) {
1884   drag_context_->ContinueDrag(view, event);
1885 }
1886 
EndDrag(EndDragReason reason)1887 bool TabStrip::EndDrag(EndDragReason reason) {
1888   return drag_context_->EndDrag(reason);
1889 }
1890 
GetTabAt(const gfx::Point & point)1891 Tab* TabStrip::GetTabAt(const gfx::Point& point) {
1892   views::View* view = GetEventHandlerForPoint(point);
1893   if (!view)
1894     return nullptr;  // No tab contains the point.
1895 
1896   // Walk up the view hierarchy until we find a tab, or the TabStrip.
1897   while (view && view != this && view->GetID() != VIEW_ID_TAB)
1898     view = view->parent();
1899 
1900   return view && view->GetID() == VIEW_ID_TAB ? static_cast<Tab*>(view)
1901                                               : nullptr;
1902 }
1903 
GetAdjacentTab(const Tab * tab,int offset)1904 const Tab* TabStrip::GetAdjacentTab(const Tab* tab, int offset) {
1905   int index = GetModelIndexOf(tab);
1906   if (index < 0)
1907     return nullptr;
1908   index += offset;
1909   return IsValidModelIndex(index) ? tab_at(index) : nullptr;
1910 }
1911 
OnMouseEventInTab(views::View * source,const ui::MouseEvent & event)1912 void TabStrip::OnMouseEventInTab(views::View* source,
1913                                  const ui::MouseEvent& event) {
1914   // Record time from cursor entering the tabstrip to first tap on a tab to
1915   // switch.
1916   if (mouse_entered_tabstrip_time_.has_value() &&
1917       event.type() == ui::ET_MOUSE_PRESSED && views::IsViewClass<Tab>(source)) {
1918     UMA_HISTOGRAM_MEDIUM_TIMES(
1919         "TabStrip.TimeToSwitch",
1920         base::TimeTicks::Now() - mouse_entered_tabstrip_time_.value());
1921     mouse_entered_tabstrip_time_.reset();
1922   }
1923   UpdateStackedLayoutFromMouseEvent(source, event);
1924 }
1925 
UpdateHoverCard(Tab * tab)1926 void TabStrip::UpdateHoverCard(Tab* tab) {
1927   if (!base::FeatureList::IsEnabled(features::kTabHoverCards))
1928     return;
1929   // We don't want to show a hover card while the tabstrip is animating.
1930   if (bounds_animator_.IsAnimating()) {
1931     return;
1932   }
1933 
1934   if (!hover_card_) {
1935     // There is nothing to be done if the hover card doesn't exist and we are
1936     // not trying to show it.
1937     if (!tab)
1938       return;
1939     hover_card_ = new TabHoverCardBubbleView(tab);
1940     hover_card_observer_.Add(hover_card_);
1941     if (GetWidget()) {
1942       hover_card_event_sniffer_ =
1943           std::make_unique<TabHoverCardEventSniffer>(hover_card_, this);
1944     }
1945   }
1946   if (tab)
1947     hover_card_->UpdateAndShow(tab);
1948   else
1949     hover_card_->FadeOutToHide();
1950 }
1951 
ShowDomainInHoverCards() const1952 bool TabStrip::ShowDomainInHoverCards() const {
1953   const auto* app_controller = controller_->GetBrowser()->app_controller();
1954   return !app_controller || !app_controller->is_for_system_web_app();
1955 }
1956 
HoverCardIsShowingForTab(Tab * tab)1957 bool TabStrip::HoverCardIsShowingForTab(Tab* tab) {
1958   if (!base::FeatureList::IsEnabled(features::kTabHoverCards))
1959     return false;
1960 
1961   return hover_card_ && hover_card_->GetWidget()->IsVisible() &&
1962          !hover_card_->IsFadingOut() &&
1963          hover_card_->GetDesiredAnchorView() == tab;
1964 }
1965 
GetBackgroundOffset() const1966 int TabStrip::GetBackgroundOffset() const {
1967   return background_offset_;
1968 }
1969 
GetStrokeThickness() const1970 int TabStrip::GetStrokeThickness() const {
1971   return ShouldDrawStrokes() ? 1 : 0;
1972 }
1973 
CanPaintThrobberToLayer() const1974 bool TabStrip::CanPaintThrobberToLayer() const {
1975   // Disable layer-painting of throbbers if dragging, if any tab animation is in
1976   // progress, or if stacked tabs are enabled. Also disable in fullscreen: when
1977   // "immersive" the tab strip could be sliding in or out; for other modes,
1978   // there's no tab strip.
1979   const bool dragging = drag_context_->IsDragStarted();
1980   const views::Widget* widget = GetWidget();
1981   return widget && !touch_layout_ && !dragging && !IsAnimating() &&
1982          !widget->IsFullscreen();
1983 }
1984 
HasVisibleBackgroundTabShapes() const1985 bool TabStrip::HasVisibleBackgroundTabShapes() const {
1986   return controller_->HasVisibleBackgroundTabShapes();
1987 }
1988 
ShouldPaintAsActiveFrame() const1989 bool TabStrip::ShouldPaintAsActiveFrame() const {
1990   return controller_->ShouldPaintAsActiveFrame();
1991 }
1992 
GetToolbarTopSeparatorColor() const1993 SkColor TabStrip::GetToolbarTopSeparatorColor() const {
1994   return controller_->GetToolbarTopSeparatorColor();
1995 }
1996 
GetTabSeparatorColor() const1997 SkColor TabStrip::GetTabSeparatorColor() const {
1998   return separator_color_;
1999 }
2000 
GetTabBackgroundColor(TabActive active,BrowserFrameActiveState active_state) const2001 SkColor TabStrip::GetTabBackgroundColor(
2002     TabActive active,
2003     BrowserFrameActiveState active_state) const {
2004   const ui::ThemeProvider* tp = GetThemeProvider();
2005   if (!tp)
2006     return SK_ColorBLACK;
2007 
2008   constexpr int kColorIds[2][2] = {
2009       {ThemeProperties::COLOR_TAB_BACKGROUND_INACTIVE_FRAME_INACTIVE,
2010        ThemeProperties::COLOR_TAB_BACKGROUND_INACTIVE_FRAME_ACTIVE},
2011       {ThemeProperties::COLOR_TAB_BACKGROUND_ACTIVE_FRAME_INACTIVE,
2012        ThemeProperties::COLOR_TAB_BACKGROUND_ACTIVE_FRAME_ACTIVE}};
2013 
2014   using State = BrowserFrameActiveState;
2015   const bool tab_active = active == TabActive::kActive;
2016   const bool frame_active =
2017       (active_state == State::kActive) ||
2018       ((active_state == State::kUseCurrent) && ShouldPaintAsActiveFrame());
2019   return tp->GetColor(kColorIds[tab_active][frame_active]);
2020 }
2021 
GetTabForegroundColor(TabActive active,SkColor background_color) const2022 SkColor TabStrip::GetTabForegroundColor(TabActive active,
2023                                         SkColor background_color) const {
2024   const ui::ThemeProvider* tp = GetThemeProvider();
2025   if (!tp)
2026     return SK_ColorBLACK;
2027 
2028   constexpr int kColorIds[2][2] = {
2029       {ThemeProperties::COLOR_TAB_FOREGROUND_INACTIVE_FRAME_INACTIVE,
2030        ThemeProperties::COLOR_TAB_FOREGROUND_INACTIVE_FRAME_ACTIVE},
2031       {ThemeProperties::COLOR_TAB_FOREGROUND_ACTIVE_FRAME_INACTIVE,
2032        ThemeProperties::COLOR_TAB_FOREGROUND_ACTIVE_FRAME_ACTIVE}};
2033 
2034   const bool tab_active = active == TabActive::kActive;
2035   const bool frame_active = ShouldPaintAsActiveFrame();
2036   const int color_id = kColorIds[tab_active][frame_active];
2037 
2038   SkColor color = tp->GetColor(color_id);
2039   if (tp->HasCustomColor(color_id))
2040     return color;
2041   if ((color_id ==
2042        ThemeProperties::COLOR_TAB_FOREGROUND_INACTIVE_FRAME_INACTIVE) &&
2043       tp->HasCustomColor(
2044           ThemeProperties::COLOR_TAB_FOREGROUND_INACTIVE_FRAME_ACTIVE)) {
2045     // If a custom theme sets a background tab text color for active but not
2046     // inactive windows, generate the inactive color by blending the active one
2047     // at 75% as we do in the default theme.
2048     color = tp->GetColor(
2049         ThemeProperties::COLOR_TAB_FOREGROUND_INACTIVE_FRAME_ACTIVE);
2050   }
2051 
2052   if (!frame_active)
2053     color = color_utils::AlphaBlend(color, background_color, 0.75f);
2054 
2055   // To minimize any readability cost of custom system frame colors, try to make
2056   // the text reach the same contrast ratio that it would in the default theme.
2057   const SkColor target = color_utils::GetColorWithMaxContrast(background_color);
2058   // These contrast ratios should match the actual ratios in the default theme
2059   // colors when no system colors are involved, except for the inactive tab/
2060   // inactive frame case, which has been raised from 4.48 to 4.5 to meet
2061   // accessibility guidelines.
2062   constexpr float kContrast[2][2] = {{4.5f,      // Inactive tab, inactive frame
2063                                       7.98f},    // Inactive tab, active frame
2064                                      {5.0f,      // Active tab, inactive frame
2065                                       10.46f}};  // Active tab, active frame
2066   const float contrast = kContrast[tab_active][frame_active];
2067   return color_utils::BlendForMinContrast(color, background_color, target,
2068                                           contrast)
2069       .color;
2070 }
2071 
2072 // Returns the accessible tab name for the tab.
GetAccessibleTabName(const Tab * tab) const2073 base::string16 TabStrip::GetAccessibleTabName(const Tab* tab) const {
2074   const int model_index = GetModelIndexOf(tab);
2075   return IsValidModelIndex(model_index) ? controller_->GetAccessibleTabName(tab)
2076                                         : base::string16();
2077 }
2078 
GetCustomBackgroundId(BrowserFrameActiveState active_state) const2079 base::Optional<int> TabStrip::GetCustomBackgroundId(
2080     BrowserFrameActiveState active_state) const {
2081   if (!TitlebarBackgroundIsTransparent())
2082     return controller_->GetCustomBackgroundId(active_state);
2083 
2084   constexpr int kBackgroundIdGlass = IDR_THEME_TAB_BACKGROUND_V;
2085   return GetThemeProvider()->HasCustomImage(kBackgroundIdGlass)
2086              ? base::make_optional(kBackgroundIdGlass)
2087              : base::nullopt;
2088 }
2089 
GetTabAnimationTargetBounds(const Tab * tab)2090 gfx::Rect TabStrip::GetTabAnimationTargetBounds(const Tab* tab) {
2091   return bounds_animator_.GetTargetBounds(tab);
2092 }
2093 
MouseMovedOutOfHost()2094 void TabStrip::MouseMovedOutOfHost() {
2095   ResizeLayoutTabs();
2096   if (reset_to_shrink_on_exit_) {
2097     reset_to_shrink_on_exit_ = false;
2098     SetStackedLayout(false);
2099     controller_->StackedLayoutMaybeChanged();
2100   }
2101 }
2102 
GetHoverOpacityForTab(float range_parameter) const2103 float TabStrip::GetHoverOpacityForTab(float range_parameter) const {
2104   return gfx::Tween::FloatValueBetween(range_parameter, hover_opacity_min_,
2105                                        hover_opacity_max_);
2106 }
2107 
GetHoverOpacityForRadialHighlight() const2108 float TabStrip::GetHoverOpacityForRadialHighlight() const {
2109   return radial_highlight_opacity_;
2110 }
2111 
GetGroupTitle(const tab_groups::TabGroupId & group) const2112 base::string16 TabStrip::GetGroupTitle(
2113     const tab_groups::TabGroupId& group) const {
2114   return controller_->GetGroupTitle(group);
2115 }
2116 
GetGroupColorId(const tab_groups::TabGroupId & group) const2117 tab_groups::TabGroupColorId TabStrip::GetGroupColorId(
2118     const tab_groups::TabGroupId& group) const {
2119   return controller_->GetGroupColorId(group);
2120 }
2121 
GetPaintedGroupColor(const tab_groups::TabGroupColorId & color_id) const2122 SkColor TabStrip::GetPaintedGroupColor(
2123     const tab_groups::TabGroupColorId& color_id) const {
2124   return GetThemeProvider()->GetColor(
2125       GetTabGroupTabStripColorId(color_id, ShouldPaintAsActiveFrame()));
2126 }
2127 
2128 ///////////////////////////////////////////////////////////////////////////////
2129 // TabStrip, views::AccessiblePaneView overrides:
2130 
Layout()2131 void TabStrip::Layout() {
2132   if (IsAnimating()) {
2133     // Hide tabs that have animated at least partially out of the clip region.
2134     SetTabSlotVisibility();
2135     return;
2136   }
2137 
2138   if (base::FeatureList::IsEnabled(features::kScrollableTabStrip)) {
2139     // With tab scrolling, the tabstrip is solely responsible for its own
2140     // width.
2141     // It should never be larger than its preferred width.
2142     const int max_width = CalculatePreferredSize().width();
2143     // It should never be smaller than its minimum width.
2144     const int min_width = GetMinimumSize().width();
2145     // If it can, it should fit within the tab strip region.
2146     const int available_width = available_width_callback_.Run();
2147     // It should be as wide as possible subject to the above constraints.
2148     const int width = std::min(max_width, std::max(min_width, available_width));
2149     SetBounds(0, 0, width, GetLayoutConstant(TAB_HEIGHT));
2150     SetTabSlotVisibility();
2151   }
2152 
2153   // Only do a layout if our size or the available width changed.
2154   const int available_width = GetAvailableWidthForTabStrip();
2155   if (last_layout_size_ == size() && last_available_width_ == available_width)
2156     return;
2157   if (drag_context_->IsDragSessionActive())
2158     return;
2159   CompleteAnimationAndLayout();
2160 }
2161 
PaintChildren(const views::PaintInfo & paint_info)2162 void TabStrip::PaintChildren(const views::PaintInfo& paint_info) {
2163   // This is used to log to UMA. NO EARLY RETURNS!
2164   base::ElapsedTimer paint_timer;
2165 
2166   // The view order doesn't match the paint order (layout_helper_ contains the
2167   // view ordering).
2168   bool is_dragging = false;
2169   Tab* active_tab = nullptr;
2170   std::vector<Tab*> tabs_dragging;
2171   std::vector<Tab*> selected_and_hovered_tabs;
2172 
2173   // When background tab shapes are visible, as for hovered or selected tabs,
2174   // the paint order must be handled carefully to avoid Z-order errors, so
2175   // this code defers drawing such tabs until later.
2176   const auto paint_or_add_to_tabs = [&paint_info,
2177                                      &selected_and_hovered_tabs](Tab* tab) {
2178     if (tab->tab_style()->GetZValue() > 0.0) {
2179       selected_and_hovered_tabs.push_back(tab);
2180     } else {
2181       tab->Paint(paint_info);
2182     }
2183   };
2184 
2185   std::vector<Tab*> all_tabs = layout_helper_->GetTabs();
2186 
2187   int active_tab_index = -1;
2188   for (int i = all_tabs.size() - 1; i >= 0; --i) {
2189     Tab* tab = all_tabs[i];
2190     if (tab->dragging() && !stacked_layout_) {
2191       is_dragging = true;
2192       if (tab->IsActive()) {
2193         active_tab = tab;
2194         active_tab_index = i;
2195       } else {
2196         tabs_dragging.push_back(tab);
2197       }
2198     } else if (tab->IsActive()) {
2199       active_tab = tab;
2200       active_tab_index = i;
2201     } else if (!stacked_layout_) {
2202       paint_or_add_to_tabs(tab);
2203     }
2204   }
2205 
2206   // Draw from the left and then the right if we're in touch mode.
2207   if (stacked_layout_ && active_tab_index >= 0) {
2208     for (int i = 0; i < active_tab_index; ++i) {
2209       Tab* tab = all_tabs[i];
2210       tab->Paint(paint_info);
2211     }
2212 
2213     for (int i = all_tabs.size() - 1; i > active_tab_index; --i) {
2214       Tab* tab = all_tabs[i];
2215       tab->Paint(paint_info);
2216     }
2217   }
2218 
2219   std::stable_sort(selected_and_hovered_tabs.begin(),
2220                    selected_and_hovered_tabs.end(), [](Tab* tab1, Tab* tab2) {
2221                      return tab1->tab_style()->GetZValue() <
2222                             tab2->tab_style()->GetZValue();
2223                    });
2224   for (Tab* tab : selected_and_hovered_tabs)
2225     tab->Paint(paint_info);
2226 
2227   // Keep track of the dragging group if dragging by the group header, or
2228   // the current group if just dragging tabs into a group. At most one of these
2229   // will have a value, since a drag is either a group drag or a tab drag.
2230   base::Optional<tab_groups::TabGroupId> dragging_group = base::nullopt;
2231   base::Optional<tab_groups::TabGroupId> current_group = base::nullopt;
2232 
2233   // Paint group headers and underlines.
2234   for (const auto& group_view_pair : group_views_) {
2235     if (group_view_pair.second->header()->dragging()) {
2236       // If the whole group is dragging, defer painting both the header and the
2237       // underline, since they should appear above non-dragging tabs and groups.
2238       // Instead, just track the dragging group.
2239       dragging_group = group_view_pair.first;
2240     } else {
2241       group_view_pair.second->header()->Paint(paint_info);
2242 
2243       if (tabs_dragging.size() > 0 &&
2244           tabs_dragging[0]->group() == group_view_pair.first) {
2245         // If tabs are being dragged into a group, defer painting just the
2246         // underline, which should appear above non-active dragging tabs as well
2247         // as all non-dragging tabs and groups. Instead, just track the group
2248         // that the tabs are being dragged into.
2249         current_group = group_view_pair.first;
2250       } else {
2251         group_view_pair.second->underline()->Paint(paint_info);
2252       }
2253     }
2254   }
2255 
2256   // Always paint the active tab over all the inactive tabs.
2257   if (active_tab && !is_dragging)
2258     active_tab->Paint(paint_info);
2259 
2260   // If dragging a group, paint the group highlight and header above all
2261   // non-dragging tabs and groups.
2262   if (dragging_group.has_value()) {
2263     group_views_[dragging_group.value()]->highlight()->Paint(paint_info);
2264     group_views_[dragging_group.value()]->header()->Paint(paint_info);
2265   }
2266 
2267   // Paint the dragged tabs.
2268   for (size_t i = 0; i < tabs_dragging.size(); ++i)
2269     tabs_dragging[i]->Paint(paint_info);
2270 
2271   // If dragging a group, or dragging tabs into a group, paint the group
2272   // underline above the dragging tabs. Otherwise, any non-active dragging tabs
2273   // will not get an underline.
2274   if (dragging_group.has_value())
2275     group_views_[dragging_group.value()]->underline()->Paint(paint_info);
2276   if (current_group.has_value())
2277     group_views_[current_group.value()]->underline()->Paint(paint_info);
2278 
2279   // If the active tab is being dragged, it goes last.
2280   if (active_tab && is_dragging)
2281     active_tab->Paint(paint_info);
2282 
2283   UMA_HISTOGRAM_CUSTOM_MICROSECONDS_TIMES(
2284       "TabStrip.PaintChildrenDuration", paint_timer.Elapsed(),
2285       base::TimeDelta::FromMicroseconds(1),
2286       base::TimeDelta::FromMicroseconds(10000), 50);
2287 }
2288 
GetClassName() const2289 const char* TabStrip::GetClassName() const {
2290   static const char kViewClassName[] = "TabStrip";
2291   return kViewClassName;
2292 }
2293 
GetMinimumSize() const2294 gfx::Size TabStrip::GetMinimumSize() const {
2295   // If tabs can be stacked, our minimum width is the smallest width of the
2296   // stacked tabstrip.
2297   const int minimum_width =
2298       (touch_layout_ || adjust_layout_)
2299           ? GetStackableTabWidth() + (2 * kStackedPadding * kMaxStackedCount)
2300           : layout_helper_->CalculateMinimumWidth();
2301 
2302   return gfx::Size(minimum_width, GetLayoutConstant(TAB_HEIGHT));
2303 }
2304 
CalculatePreferredSize() const2305 gfx::Size TabStrip::CalculatePreferredSize() const {
2306   int preferred_width;
2307   // The tabstrip needs to always exactly fit the bounds of the tabs so that
2308   // NTB can be laid out just to the right of the rightmost tab. When the tabs
2309   // aren't at their ideal bounds (i.e. during animation or a drag), we need to
2310   // size ourselves to exactly fit wherever the tabs *currently* are.
2311   if (IsAnimating() || drag_context_->IsDragSessionActive()) {
2312     // The visual order of the tabs can be out of sync with the logical order,
2313     // so we have to check all of them to find the visually trailing-most one.
2314     int max_x = 0;
2315     for (auto* tab : layout_helper_->GetTabs()) {
2316       max_x = std::max(max_x, tab->bounds().right());
2317     }
2318     // The tabs span from 0 to |max_x|, so |max_x| is the current width
2319     // occupied by tabs. We report the current width as our preferred width so
2320     // that the tab strip is sized to exactly fit the current position of the
2321     // tabs.
2322     preferred_width = max_x;
2323   } else {
2324     preferred_width = override_available_width_for_tabs_
2325                           ? override_available_width_for_tabs_.value()
2326                           : layout_helper_->CalculatePreferredWidth();
2327   }
2328 
2329   return gfx::Size(preferred_width, GetLayoutConstant(TAB_HEIGHT));
2330 }
2331 
GetTooltipHandlerForPoint(const gfx::Point & point)2332 views::View* TabStrip::GetTooltipHandlerForPoint(const gfx::Point& point) {
2333   if (!HitTestPoint(point))
2334     return nullptr;
2335 
2336   if (!touch_layout_) {
2337     // Return any view that isn't a Tab or this TabStrip immediately. We don't
2338     // want to interfere.
2339     views::View* v = View::GetTooltipHandlerForPoint(point);
2340     if (v && v != this && !views::IsViewClass<Tab>(v))
2341       return v;
2342 
2343     views::View* tab = FindTabHitByPoint(point);
2344     if (tab)
2345       return tab;
2346   } else {
2347     Tab* tab = FindTabForEvent(point);
2348     if (tab)
2349       return ConvertPointToViewAndGetTooltipHandler(this, tab, point);
2350   }
2351   return this;
2352 }
2353 
GetDropIndex(const ui::DropTargetEvent & event)2354 BrowserRootView::DropIndex TabStrip::GetDropIndex(
2355     const ui::DropTargetEvent& event) {
2356   // Force animations to stop, otherwise it makes the index calculation tricky.
2357   StopAnimating(true);
2358 
2359   // If the UI layout is right-to-left, we need to mirror the mouse
2360   // coordinates since we calculate the drop index based on the
2361   // original (and therefore non-mirrored) positions of the tabs.
2362   const int x = GetMirroredXInView(event.x());
2363   for (int i = 0; i < tab_count(); ++i) {
2364     Tab* tab = tab_at(i);
2365     const int tab_max_x = tab->x() + tab->width();
2366 
2367     // When hovering over the left or right quarter of a tab, the drop indicator
2368     // will point between tabs.
2369     const int hot_width = tab->width() / 4;
2370 
2371     if (x < tab_max_x) {
2372       if (x >= (tab_max_x - hot_width))
2373         return {i + 1, true};
2374       return {i, x < tab->x() + hot_width};
2375     }
2376   }
2377 
2378   // The drop isn't over a tab, add it to the end.
2379   return {tab_count(), true};
2380 }
2381 
GetViewForDrop()2382 views::View* TabStrip::GetViewForDrop() {
2383   return this;
2384 }
2385 
HandleDragUpdate(const base::Optional<BrowserRootView::DropIndex> & index)2386 void TabStrip::HandleDragUpdate(
2387     const base::Optional<BrowserRootView::DropIndex>& index) {
2388   SetDropArrow(index);
2389 }
2390 
HandleDragExited()2391 void TabStrip::HandleDragExited() {
2392   SetDropArrow({});
2393 }
2394 
2395 ///////////////////////////////////////////////////////////////////////////////
2396 // TabStrip, private:
2397 
Init()2398 void TabStrip::Init() {
2399   SetID(VIEW_ID_TAB_STRIP);
2400   // So we get enter/exit on children to switch stacked layout on and off.
2401   SetNotifyEnterExitOnChild(true);
2402 
2403   if (g_drop_indicator_width == 0) {
2404     // Direction doesn't matter, both images are the same size.
2405     gfx::ImageSkia* drop_image = GetDropArrowImage(true);
2406     g_drop_indicator_width = drop_image->width();
2407     g_drop_indicator_height = drop_image->height();
2408   }
2409 
2410   UpdateContrastRatioValues();
2411 
2412   if (!gfx::Animation::ShouldRenderRichAnimation())
2413     bounds_animator_.SetAnimationDuration(base::TimeDelta());
2414 }
2415 
GetGroupHeaders()2416 std::map<tab_groups::TabGroupId, TabGroupHeader*> TabStrip::GetGroupHeaders() {
2417   std::map<tab_groups::TabGroupId, TabGroupHeader*> group_headers;
2418   for (const auto& group_view_pair : group_views_) {
2419     group_headers.insert(std::make_pair(group_view_pair.first,
2420                                         group_view_pair.second->header()));
2421   }
2422   return group_headers;
2423 }
2424 
NewTabButtonPressed(const ui::Event & event)2425 void TabStrip::NewTabButtonPressed(const ui::Event& event) {
2426   base::RecordAction(base::UserMetricsAction("NewTab_Button"));
2427   UMA_HISTOGRAM_ENUMERATION("Tab.NewTab", TabStripModel::NEW_TAB_BUTTON,
2428                             TabStripModel::NEW_TAB_ENUM_COUNT);
2429   if (event.IsMouseEvent()) {
2430     // Prevent the hover card from popping back in immediately. This forces a
2431     // normal fade-in.
2432     if (hover_card_)
2433       hover_card_->set_last_mouse_exit_timestamp(base::TimeTicks());
2434 
2435     const ui::MouseEvent& mouse = static_cast<const ui::MouseEvent&>(event);
2436     if (mouse.IsOnlyMiddleMouseButton()) {
2437       if (ui::Clipboard::IsSupportedClipboardBuffer(
2438               ui::ClipboardBuffer::kSelection)) {
2439         ui::Clipboard* clipboard = ui::Clipboard::GetForCurrentThread();
2440         CHECK(clipboard);
2441         base::string16 clipboard_text;
2442         clipboard->ReadText(ui::ClipboardBuffer::kSelection,
2443                             /* data_dst = */ nullptr, &clipboard_text);
2444         if (!clipboard_text.empty())
2445           controller_->CreateNewTabWithLocation(clipboard_text);
2446       }
2447       return;
2448     }
2449   }
2450 
2451   controller_->CreateNewTab();
2452   if (event.type() == ui::ET_GESTURE_TAP)
2453     TouchUMA::RecordGestureAction(TouchUMA::kGestureNewTabTap);
2454 }
2455 
StartInsertTabAnimation(int model_index,TabPinned pinned)2456 void TabStrip::StartInsertTabAnimation(int model_index, TabPinned pinned) {
2457   layout_helper_->InsertTabAt(model_index, tab_at(model_index), pinned);
2458 
2459   PrepareForAnimation();
2460 
2461   ExitTabClosingMode();
2462 
2463   gfx::Rect bounds = tab_at(model_index)->bounds();
2464   bounds.set_height(GetLayoutConstant(TAB_HEIGHT));
2465 
2466   // Adjust the starting bounds of the new tab.
2467   const int tab_overlap = TabStyle::GetTabOverlap();
2468   if (model_index > 0) {
2469     // If we have a tab to our left, start at its right edge.
2470     bounds.set_x(tab_at(model_index - 1)->bounds().right() - tab_overlap);
2471   } else if (model_index + 1 < tab_count()) {
2472     // Otherwise, if we have a tab to our right, start at its left edge.
2473     bounds.set_x(tab_at(model_index + 1)->bounds().x());
2474   } else {
2475     NOTREACHED() << "First tab inserted into the tabstrip should not animate.";
2476   }
2477 
2478   // Start at the width of the overlap in order to animate at the same speed
2479   // the surrounding tabs are moving, since at this width the subsequent tab
2480   // is naturally positioned at the same X coordinate.
2481   bounds.set_width(tab_overlap);
2482   tab_at(model_index)->SetBoundsRect(bounds);
2483 
2484   // Animate in to the full width.
2485   UpdateIdealBounds();
2486   AnimateToIdealBounds();
2487 }
2488 
StartRemoveTabAnimation(int model_index,bool was_active)2489 void TabStrip::StartRemoveTabAnimation(int model_index, bool was_active) {
2490   const int model_count = GetModelCount();
2491   const int tab_overlap = TabStyle::GetTabOverlap();
2492   if (in_tab_close_ && model_count > 0 && model_index != model_count) {
2493     // The user closed a tab other than the last tab. Set
2494     // override_available_width_for_tabs_ so that as the user closes tabs with
2495     // the mouse a tab continues to fall under the mouse.
2496     int next_active_index = controller_->GetActiveIndex();
2497     DCHECK(IsValidModelIndex(next_active_index));
2498     if (model_index <= next_active_index) {
2499       // At this point, model's internal state has already been updated.
2500       // |contents| has been detached from model and the active index has been
2501       // updated. But the tab for |contents| isn't removed yet. Thus, we need to
2502       // fix up next_active_index based on it.
2503       next_active_index++;
2504     }
2505     Tab* next_active_tab = tab_at(next_active_index);
2506     Tab* tab_being_removed = tab_at(model_index);
2507 
2508     int size_delta = tab_being_removed->width();
2509     if (!tab_being_removed->data().pinned && was_active &&
2510         GetActiveTabWidth() > GetInactiveTabWidth()) {
2511       // When removing an active, non-pinned tab, an inactive tab will be made
2512       // active and thus given the active width. Thus the width being removed
2513       // from the strip is really the current width of whichever inactive tab
2514       // will be made active.
2515       size_delta = next_active_tab->width();
2516     }
2517 
2518     override_available_width_for_tabs_ =
2519         ideal_bounds(model_count).right() - size_delta + tab_overlap;
2520   }
2521 
2522   if (!touch_layout_)
2523     PrepareForAnimation();
2524 
2525   Tab* tab = tab_at(model_index);
2526   tab->SetClosing(true);
2527 
2528   int old_x = tabs_.ideal_bounds(model_index).x();
2529   RemoveTabFromViewModel(model_index);
2530 
2531   if (touch_layout_) {
2532     touch_layout_->RemoveTab(model_index,
2533                              UpdateIdealBoundsForPinnedTabs(nullptr), old_x);
2534   }
2535 
2536   layout_helper_->RemoveTabAt(model_index, tab);
2537   UpdateIdealBounds();
2538   AnimateToIdealBounds();
2539 
2540   if (in_tab_close_ && model_count > 0 &&
2541       override_available_width_for_tabs_ >
2542           ideal_bounds(model_count - 1).right()) {
2543     // Tab closing mode is no longer constraining tab widths - they're at full
2544     // size. Exit tab closing mode so that it doesn't artificially inflate the
2545     // tabstrip's bounds.
2546     ExitTabClosingMode();
2547   }
2548 
2549   // TODO(pkasting): When closing multiple tabs, we get repeated RemoveTabAt()
2550   // calls, each of which closes a new tab and thus generates different ideal
2551   // bounds.  We should update the animations of any other tabs that are
2552   // currently being closed to reflect the new ideal bounds, or else change from
2553   // removing one tab at a time to animating the removal of all tabs at once.
2554 
2555   // Compute the target bounds for animating this tab closed.  The tab's left
2556   // edge should stay joined to the right edge of the previous tab, if any.
2557   gfx::Rect tab_bounds = tab->bounds();
2558   tab_bounds.set_x((model_index > 0)
2559                        ? (ideal_bounds(model_index - 1).right() - tab_overlap)
2560                        : 0);
2561 
2562   // The tab should animate to the width of the overlap in order to close at the
2563   // same speed the surrounding tabs are moving, since at this width the
2564   // subsequent tab is naturally positioned at the same X coordinate.
2565   tab_bounds.set_width(tab_overlap);
2566 
2567   // Animate the tab closed.
2568   bounds_animator_.AnimateViewTo(
2569       tab, tab_bounds,
2570       std::make_unique<RemoveTabDelegate>(
2571           this, tab,
2572           base::BindRepeating(&TabStrip::OnTabSlotAnimationProgressed,
2573                               base::Unretained(this))));
2574 }
2575 
StartMoveTabAnimation()2576 void TabStrip::StartMoveTabAnimation() {
2577   PrepareForAnimation();
2578   UpdateIdealBounds();
2579   AnimateToIdealBounds();
2580 }
2581 
AnimateToIdealBounds()2582 void TabStrip::AnimateToIdealBounds() {
2583   UpdateHoverCard(nullptr);
2584 
2585   for (int i = 0; i < tab_count(); ++i) {
2586     // If the tab is being dragged manually, skip it.
2587     Tab* tab = tab_at(i);
2588     if (tab->dragging() && !bounds_animator_.IsAnimating(tab))
2589       continue;
2590 
2591     // Also skip tabs already being animated to the same ideal bounds.  Calling
2592     // AnimateViewTo() again restarts the animation, which changes the timing of
2593     // how the tab animates, leading to hitches.
2594     const gfx::Rect& target_bounds = ideal_bounds(i);
2595     if (bounds_animator_.GetTargetBounds(tab) == target_bounds)
2596       continue;
2597 
2598     // Set an animation delegate for the tab so it will clip appropriately.
2599     // Don't do this if dragging() is true.  In this case the tab was
2600     // previously being dragged and is now animating back to its ideal
2601     // bounds; it already has an associated ResetDraggingStateDelegate that
2602     // will reset this dragging state. Replacing this delegate would mean
2603     // this code would also need to reset the dragging state immediately,
2604     // and that could allow the new tab button to be drawn atop this tab.
2605     if (bounds_animator_.IsAnimating(tab) && tab->dragging()) {
2606       bounds_animator_.SetTargetBounds(tab, target_bounds);
2607     } else {
2608       bounds_animator_.AnimateViewTo(
2609           tab, target_bounds,
2610           std::make_unique<TabSlotAnimationDelegate>(
2611               this, tab,
2612               base::BindRepeating(&TabStrip::OnTabSlotAnimationProgressed,
2613                                   base::Unretained(this))));
2614     }
2615   }
2616 
2617   for (const auto& header_pair : group_views_) {
2618     TabGroupHeader* const header = header_pair.second->header();
2619     bounds_animator_.AnimateViewTo(
2620         header,
2621         layout_helper_->group_header_ideal_bounds().at(header_pair.first),
2622         std::make_unique<TabSlotAnimationDelegate>(
2623             this, header,
2624             base::BindRepeating(&TabStrip::OnTabSlotAnimationProgressed,
2625                                 base::Unretained(this))));
2626   }
2627 }
2628 
SnapToIdealBounds()2629 void TabStrip::SnapToIdealBounds() {
2630   for (int i = 0; i < tab_count(); ++i)
2631     tab_at(i)->SetBoundsRect(ideal_bounds(i));
2632 
2633   for (const auto& header_pair : group_views_) {
2634     header_pair.second->header()->SetBoundsRect(
2635         layout_helper_->group_header_ideal_bounds().at(header_pair.first));
2636     header_pair.second->UpdateBounds();
2637   }
2638 
2639   PreferredSizeChanged();
2640 }
2641 
ExitTabClosingMode()2642 void TabStrip::ExitTabClosingMode() {
2643   in_tab_close_ = false;
2644   override_available_width_for_tabs_.reset();
2645   layout_helper_->ExitTabClosingMode();
2646 }
2647 
ShouldHighlightCloseButtonAfterRemove()2648 bool TabStrip::ShouldHighlightCloseButtonAfterRemove() {
2649   return in_tab_close_;
2650 }
2651 
TitlebarBackgroundIsTransparent() const2652 bool TabStrip::TitlebarBackgroundIsTransparent() const {
2653 #if defined(OS_WIN)
2654   // Windows 8+ uses transparent window contents (because the titlebar area is
2655   // drawn by the system and not Chrome), but the actual titlebar is opaque.
2656   if (base::win::GetVersion() >= base::win::Version::WIN8)
2657     return false;
2658 #endif
2659   return GetWidget()->ShouldWindowContentsBeTransparent();
2660 }
2661 
CompleteAnimationAndLayout()2662 void TabStrip::CompleteAnimationAndLayout() {
2663   last_available_width_ = GetAvailableWidthForTabStrip();
2664   last_layout_size_ = size();
2665 
2666   bounds_animator_.Cancel();
2667 
2668   SwapLayoutIfNecessary();
2669   if (touch_layout_)
2670     touch_layout_->SetWidth(width());
2671 
2672   UpdateIdealBounds();
2673   SnapToIdealBounds();
2674 
2675   SetTabSlotVisibility();
2676   SchedulePaint();
2677 }
2678 
SetTabSlotVisibility()2679 void TabStrip::SetTabSlotVisibility() {
2680   bool last_tab_visible = false;
2681   base::Optional<tab_groups::TabGroupId> last_tab_group = base::nullopt;
2682   std::vector<Tab*> tabs = layout_helper_->GetTabs();
2683   for (std::vector<Tab*>::reverse_iterator tab = tabs.rbegin();
2684        tab != tabs.rend(); ++tab) {
2685     base::Optional<tab_groups::TabGroupId> current_group = (*tab)->group();
2686     if (current_group != last_tab_group && last_tab_group.has_value())
2687       group_header(last_tab_group.value())->SetVisible(last_tab_visible);
2688     last_tab_visible = ShouldTabBeVisible(*tab);
2689     last_tab_group = (*tab)->closing() ? base::nullopt : current_group;
2690 
2691     // Collapsed tabs disappear once they've reached their minimum size. This
2692     // is different than very small non-collapsed tabs, because in that case
2693     // the tab (and its favicon) must still be visible.
2694     bool is_collapsed =
2695         (current_group.has_value() &&
2696          controller()->IsGroupCollapsed(current_group.value()) &&
2697          (*tab)->bounds().width() <=
2698              (*tab)->tab_style()->GetMinimumInactiveWidth());
2699     (*tab)->SetVisible(is_collapsed ? false : last_tab_visible);
2700   }
2701 }
2702 
UpdateAccessibleTabIndices()2703 void TabStrip::UpdateAccessibleTabIndices() {
2704   const int num_tabs = tab_count();
2705   for (int i = 0; i < num_tabs; ++i)
2706     tab_at(i)->GetViewAccessibility().OverridePosInSet(i + 1, num_tabs);
2707 }
2708 
GetActiveTabWidth() const2709 int TabStrip::GetActiveTabWidth() const {
2710   return layout_helper_->active_tab_width();
2711 }
2712 
GetInactiveTabWidth() const2713 int TabStrip::GetInactiveTabWidth() const {
2714   return layout_helper_->inactive_tab_width();
2715 }
2716 
GetLastVisibleTab() const2717 const Tab* TabStrip::GetLastVisibleTab() const {
2718   for (int i = tab_count() - 1; i >= 0; --i) {
2719     const Tab* tab = tab_at(i);
2720 
2721     // The tab is marked not visible in a collapsed group, but is "visible" in
2722     // the tabstrip if the header is visible.
2723     if (tab->GetVisible() ||
2724         (tab->group().has_value() &&
2725          group_header(tab->group().value())->GetVisible())) {
2726       return tab;
2727     }
2728   }
2729   // While in normal use the tabstrip should always be wide enough to have at
2730   // least one visible tab, it can be zero-width in tests, meaning we get here.
2731   return nullptr;
2732 }
2733 
GetViewInsertionIndex(Tab * tab,base::Optional<int> from_model_index,int to_model_index) const2734 int TabStrip::GetViewInsertionIndex(Tab* tab,
2735                                     base::Optional<int> from_model_index,
2736                                     int to_model_index) const {
2737   // -1 is treated a sentinel value to indicate a tab is newly added to the
2738   // beginning of the tab strip.
2739   if (to_model_index < 0)
2740     return 0;
2741 
2742   // If to_model_index is beyond the end of the tab strip, then the tab is newly
2743   // added to the end of the tab strip. In that case we can just return one
2744   // beyond the view index of the last existing tab.
2745   if (to_model_index >= tab_count())
2746     return (tab_count() ? GetIndexOf(tab_at(tab_count() - 1)) + 1 : 0);
2747 
2748   // If there is no from_model_index, then the tab is newly added in the middle
2749   // of the tab strip. In that case we treat it as coming from the end of the
2750   // tab strip, since new views are ordered at the end by default.
2751   if (!from_model_index.has_value())
2752     from_model_index = tab_count();
2753 
2754   DCHECK_NE(to_model_index, from_model_index.value());
2755 
2756   // Since we don't have an absolute mapping from model index to view index, we
2757   // anchor on the last known view index at the given to_model_index.
2758   Tab* other_tab = tab_at(to_model_index);
2759   int other_view_index = GetIndexOf(other_tab);
2760 
2761   if (other_view_index <= 0)
2762     return 0;
2763 
2764   // When moving to the right, just use the anchor index because the tab will
2765   // replace that position in both the model and the view. This happens because
2766   // the tab itself occupies a lower index that the other tabs will shift into.
2767   if (to_model_index > from_model_index.value())
2768     return other_view_index;
2769 
2770   // When moving to the left, the tab may end up on either the left or right
2771   // side of a group header, depending on if it's in that group. This affects
2772   // its view index but not its model index, so we adjust the former only.
2773   if (other_tab->group().has_value() && other_tab->group() != tab->group())
2774     return other_view_index - 1;
2775 
2776   return other_view_index;
2777 }
2778 
CloseTabInternal(int model_index,CloseTabSource source)2779 void TabStrip::CloseTabInternal(int model_index, CloseTabSource source) {
2780   if (!IsValidModelIndex(model_index))
2781     return;
2782 
2783   // If we're not allowed to close this tab for whatever reason, we should not
2784   // proceed.
2785   if (!controller_->BeforeCloseTab(model_index, source))
2786     return;
2787 
2788   if (!in_tab_close_ && IsAnimating()) {
2789     // Cancel any current animations. We do this as remove uses the current
2790     // ideal bounds and we need to know ideal bounds is in a good state.
2791     StopAnimating(true);
2792   }
2793 
2794   if (GetWidget()) {
2795     in_tab_close_ = true;
2796     resize_layout_timer_.Stop();
2797     if (source == CLOSE_TAB_FROM_TOUCH)
2798       StartResizeLayoutTabsFromTouchTimer();
2799     else
2800       AddMessageLoopObserver();
2801   }
2802 
2803   UpdateHoverCard(nullptr);
2804   if (tab_at(model_index)->group().has_value())
2805     base::RecordAction(base::UserMetricsAction("CloseGroupedTab"));
2806   controller_->CloseTab(model_index);
2807 }
2808 
RemoveTabFromViewModel(int index)2809 void TabStrip::RemoveTabFromViewModel(int index) {
2810   Tab* closing_tab = tab_at(index);
2811   bool closing_tab_was_active = closing_tab->IsActive();
2812 
2813   UpdateHoverCard(nullptr);
2814 
2815   // We still need to keep the tab alive until the remove tab animation
2816   // completes. Defer destroying it until then.
2817   tabs_.Remove(index);
2818   selected_tabs_.DecrementFrom(index);
2819 
2820   if (closing_tab_was_active)
2821     closing_tab->ActiveStateChanged();
2822 }
2823 
OnTabCloseAnimationCompleted(Tab * tab)2824 void TabStrip::OnTabCloseAnimationCompleted(Tab* tab) {
2825   DCHECK(tab->closing());
2826 
2827   std::unique_ptr<Tab> deleter(tab);
2828   layout_helper_->OnTabDestroyed(tab);
2829 
2830   // Send the Container a message to simulate a mouse moved event at the current
2831   // mouse position. This tickles the Tab the mouse is currently over to show
2832   // the "hot" state of the close button.  Note that this is not required (and
2833   // indeed may crash!) for removes spawned by non-mouse closes and
2834   // drag-detaches.
2835   if (!GetDragContext()->IsDragSessionActive() &&
2836       ShouldHighlightCloseButtonAfterRemove()) {
2837     // The widget can apparently be null during shutdown.
2838     views::Widget* widget = GetWidget();
2839     if (widget)
2840       widget->SynthesizeMouseMoveEvent();
2841   }
2842 }
2843 
StoppedDraggingView(TabSlotView * view,bool * is_first_view)2844 void TabStrip::StoppedDraggingView(TabSlotView* view, bool* is_first_view) {
2845   if (view &&
2846       view->GetTabSlotViewType() == TabSlotView::ViewType::kTabGroupHeader) {
2847     // Ensure all tab group UI is repainted, especially the dragging highlight.
2848     view->set_dragging(false);
2849     SchedulePaint();
2850     return;
2851   }
2852 
2853   int tab_data_index = GetModelIndexOf(view);
2854   if (tab_data_index == -1) {
2855     // The tab was removed before the drag completed. Don't do anything.
2856     return;
2857   }
2858 
2859   if (*is_first_view) {
2860     *is_first_view = false;
2861     PrepareForAnimation();
2862 
2863     // Animate the view back to its correct position.
2864     UpdateIdealBounds();
2865     AnimateToIdealBounds();
2866   }
2867 
2868   // Install a delegate to reset the dragging state when done. We have to leave
2869   // dragging true for the tab otherwise it'll draw beneath the new tab button.
2870   bounds_animator_.AnimateViewTo(
2871       view, ideal_bounds(tab_data_index),
2872       std::make_unique<ResetDraggingStateDelegate>(
2873           this, static_cast<Tab*>(view),
2874           base::BindRepeating(&TabStrip::OnTabSlotAnimationProgressed,
2875                               base::Unretained(this))));
2876 }
2877 
UpdateStackedLayoutFromMouseEvent(views::View * source,const ui::MouseEvent & event)2878 void TabStrip::UpdateStackedLayoutFromMouseEvent(views::View* source,
2879                                                  const ui::MouseEvent& event) {
2880   if (!adjust_layout_)
2881     return;
2882 
2883 // The following code attempts to switch to shrink (not stacked) layout when
2884 // the mouse exits the tabstrip (or the mouse is pressed on a stacked tab) and
2885 // to stacked layout when a touch device is used. This is made problematic by
2886 // windows generating mouse move events that do not clearly indicate the move
2887 // is the result of a touch device. This assumes a real mouse is used if
2888 // |kMouseMoveCountBeforeConsiderReal| mouse move events are received within
2889 // the time window |kMouseMoveTime|.  At the time we get a mouse press we know
2890 // whether its from a touch device or not, but we don't layout then else
2891 // everything shifts. Instead we wait for the release.
2892 //
2893 // TODO(sky): revisit this when touch events are really plumbed through.
2894 #if !defined(OS_CHROMEOS)
2895   constexpr auto kMouseMoveTime = base::TimeDelta::FromMilliseconds(200);
2896   constexpr int kMouseMoveCountBeforeConsiderReal = 3;
2897 #endif
2898 
2899   switch (event.type()) {
2900     case ui::ET_MOUSE_PRESSED:
2901       mouse_move_count_ = 0;
2902       last_mouse_move_time_ = base::TimeTicks();
2903       SetResetToShrinkOnExit((event.flags() & ui::EF_FROM_TOUCH) == 0);
2904       if (reset_to_shrink_on_exit_ && touch_layout_) {
2905         gfx::Point tab_strip_point(event.location());
2906         views::View::ConvertPointToTarget(source, this, &tab_strip_point);
2907         Tab* tab = FindTabForEvent(tab_strip_point);
2908         if (tab && touch_layout_->IsStacked(GetModelIndexOf(tab))) {
2909           SetStackedLayout(false);
2910           controller_->StackedLayoutMaybeChanged();
2911         }
2912       }
2913       break;
2914 
2915     case ui::ET_MOUSE_MOVED: {
2916 #if defined(OS_CHROMEOS)
2917       // Ash does not synthesize mouse events from touch events.
2918       SetResetToShrinkOnExit(true);
2919 #else
2920       gfx::Point location(event.location());
2921       ConvertPointToTarget(source, this, &location);
2922       if (location == last_mouse_move_location_)
2923         return;  // Ignore spurious moves.
2924       last_mouse_move_location_ = location;
2925       if ((event.flags() & ui::EF_FROM_TOUCH) ||
2926           (event.flags() & ui::EF_IS_SYNTHESIZED)) {
2927         last_mouse_move_time_ = base::TimeTicks();
2928       } else if ((base::TimeTicks::Now() - last_mouse_move_time_) >=
2929                  kMouseMoveTime) {
2930         mouse_move_count_ = 1;
2931         last_mouse_move_time_ = base::TimeTicks::Now();
2932       } else if (mouse_move_count_ < kMouseMoveCountBeforeConsiderReal) {
2933         ++mouse_move_count_;
2934       } else {
2935         SetResetToShrinkOnExit(true);
2936       }
2937 #endif
2938       break;
2939     }
2940 
2941     case ui::ET_MOUSE_RELEASED: {
2942       gfx::Point location(event.location());
2943       ConvertPointToTarget(source, this, &location);
2944       last_mouse_move_location_ = location;
2945       mouse_move_count_ = 0;
2946       last_mouse_move_time_ = base::TimeTicks();
2947       if ((event.flags() & ui::EF_FROM_TOUCH) == ui::EF_FROM_TOUCH) {
2948         SetStackedLayout(true);
2949         controller_->StackedLayoutMaybeChanged();
2950       }
2951       break;
2952     }
2953 
2954     default:
2955       break;
2956   }
2957 }
2958 
UpdateContrastRatioValues()2959 void TabStrip::UpdateContrastRatioValues() {
2960   // There may be no controller in unit tests, and the call to
2961   // GetTabBackgroundColor() below requires one, so bail early if it is absent.
2962   if (!controller_)
2963     return;
2964 
2965   const SkColor inactive_bg = GetTabBackgroundColor(
2966       TabActive::kInactive, BrowserFrameActiveState::kUseCurrent);
2967   const auto get_blend = [inactive_bg](SkColor target, float contrast) {
2968     return color_utils::BlendForMinContrast(inactive_bg, inactive_bg, target,
2969                                             contrast);
2970   };
2971 
2972   const SkColor active_bg = GetTabBackgroundColor(
2973       TabActive::kActive, BrowserFrameActiveState::kUseCurrent);
2974   const auto get_hover_opacity = [active_bg, &get_blend](float contrast) {
2975     return get_blend(active_bg, contrast).alpha / 255.0f;
2976   };
2977 
2978   // The contrast ratio for the hover effect on standard-width tabs.
2979   // In the default color scheme, this corresponds to a hover opacity of 0.4.
2980   constexpr float kStandardWidthContrast = 1.11f;
2981   hover_opacity_min_ = get_hover_opacity(kStandardWidthContrast);
2982 
2983   // The contrast ratio for the hover effect on min-width tabs.
2984   // In the default color scheme, this corresponds to a hover opacity of 0.65.
2985   constexpr float kMinWidthContrast = 1.19f;
2986   hover_opacity_max_ = get_hover_opacity(kMinWidthContrast);
2987 
2988   // The contrast ratio for the radial gradient effect on hovered tabs.
2989   // In the default color scheme, this corresponds to a hover opacity of 0.45.
2990   constexpr float kRadialGradientContrast = 1.13728f;
2991   radial_highlight_opacity_ = get_hover_opacity(kRadialGradientContrast);
2992 
2993   const SkColor inactive_fg =
2994       GetTabForegroundColor(TabActive::kInactive, inactive_bg);
2995   // The contrast ratio for the separator between inactive tabs.
2996   constexpr float kTabSeparatorContrast = 2.5f;
2997   separator_color_ = get_blend(inactive_fg, kTabSeparatorContrast).color;
2998 }
2999 
ShiftTabRelative(Tab * tab,int offset)3000 void TabStrip::ShiftTabRelative(Tab* tab, int offset) {
3001   DCHECK_EQ(1, std::abs(offset));
3002   const int start_index = GetModelIndexOf(tab);
3003   int target_index = start_index + offset;
3004 
3005   if (!IsValidModelIndex(start_index))
3006     return;
3007 
3008   if (tab->closing())
3009     return;
3010 
3011   const auto old_group = tab->group();
3012   if (!IsValidModelIndex(target_index) ||
3013       controller_->IsTabPinned(start_index) !=
3014           controller_->IsTabPinned(target_index)) {
3015     // Even if we've reached the boundary of where the tab could go, it may
3016     // still be able to "move" out of its current group.
3017     if (old_group.has_value()) {
3018       AnnounceTabRemovedFromGroup(old_group.value());
3019       controller_->RemoveTabFromGroup(start_index);
3020     }
3021     return;
3022   }
3023 
3024   // If the tab is at a group boundary and the group is expanded, instead of
3025   // actually moving the tab just change its group membership.
3026   base::Optional<tab_groups::TabGroupId> target_group =
3027       tab_at(target_index)->group();
3028   if (old_group != target_group) {
3029     if (old_group.has_value()) {
3030       AnnounceTabRemovedFromGroup(old_group.value());
3031       controller_->RemoveTabFromGroup(start_index);
3032       return;
3033     } else if (target_group.has_value()) {
3034       // If the tab is at a group boundary and the group is collapsed, treat the
3035       // collapsed group as a tab and find the next available slot for the tab
3036       // to move to.
3037       if (controller_->IsGroupCollapsed(target_group.value())) {
3038         int candidate_index = target_index + offset;
3039         while (IsValidModelIndex(candidate_index) &&
3040                tab_at(candidate_index)->group() == target_group) {
3041           candidate_index += offset;
3042         }
3043         if (IsValidModelIndex(candidate_index)) {
3044           target_index = candidate_index - offset;
3045         } else {
3046           target_index = offset < 0 ? 0 : GetModelCount() - 1;
3047         }
3048       } else {
3049         // Read before adding the tab to the group so that the group description
3050         // isn't the tab we just added.
3051         AnnounceTabAddedToGroup(target_group.value());
3052         controller_->AddTabToGroup(start_index, target_group.value());
3053         return;
3054       }
3055     }
3056   }
3057 
3058   controller_->MoveTab(start_index, target_index);
3059   GetViewAccessibility().AnnounceText(l10n_util::GetStringUTF16(
3060       ((offset > 0) ^ base::i18n::IsRTL()) ? IDS_TAB_AX_ANNOUNCE_MOVED_RIGHT
3061                                            : IDS_TAB_AX_ANNOUNCE_MOVED_LEFT));
3062 }
3063 
ShiftGroupRelative(const tab_groups::TabGroupId & group,int offset)3064 void TabStrip::ShiftGroupRelative(const tab_groups::TabGroupId& group,
3065                                   int offset) {
3066   DCHECK_EQ(1, std::abs(offset));
3067   std::vector<int> tabs_in_group = controller_->ListTabsInGroup(group);
3068 
3069   const int start_index = tabs_in_group.front();
3070   int target_index = start_index + offset;
3071 
3072   if (offset > 0)
3073     target_index += tabs_in_group.size() - 1;
3074 
3075   if (!IsValidModelIndex(start_index) || !IsValidModelIndex(target_index))
3076     return;
3077 
3078   // Avoid moving into the middle of another group by accounting for its size.
3079   base::Optional<tab_groups::TabGroupId> target_group =
3080       tab_at(target_index)->group();
3081   if (target_group.has_value()) {
3082     target_index +=
3083         offset *
3084         (controller_->ListTabsInGroup(target_group.value()).size() - 1);
3085   }
3086 
3087   if (!IsValidModelIndex(target_index))
3088     return;
3089 
3090   if (controller_->IsTabPinned(start_index) !=
3091       controller_->IsTabPinned(target_index))
3092     return;
3093 
3094   controller_->MoveGroup(group, target_index);
3095 }
3096 
ResizeLayoutTabs()3097 void TabStrip::ResizeLayoutTabs() {
3098   // We've been called back after the TabStrip has been emptied out (probably
3099   // just prior to the window being destroyed). We need to do nothing here or
3100   // else GetTabAt below will crash.
3101   if (tab_count() == 0)
3102     return;
3103 
3104   // It is critically important that this is unhooked here, otherwise we will
3105   // keep spying on messages forever.
3106   RemoveMessageLoopObserver();
3107 
3108   ExitTabClosingMode();
3109   int pinned_tab_count = GetPinnedTabCount();
3110   if (pinned_tab_count == tab_count()) {
3111     // Only pinned tabs, we know the tab widths won't have changed (all
3112     // pinned tabs have the same width), so there is nothing to do.
3113     return;
3114   }
3115   // Don't try and avoid layout based on tab sizes. If tabs are small enough
3116   // then the width of the active tab may not change, but other widths may
3117   // have. This is particularly important if we've overflowed (all tabs are at
3118   // the min).
3119   StartResizeLayoutAnimation();
3120 }
3121 
ResizeLayoutTabsFromTouch()3122 void TabStrip::ResizeLayoutTabsFromTouch() {
3123   // Don't resize if the user is interacting with the tabstrip.
3124   if (!drag_context_->IsDragSessionActive())
3125     ResizeLayoutTabs();
3126   else
3127     StartResizeLayoutTabsFromTouchTimer();
3128 }
3129 
StartResizeLayoutTabsFromTouchTimer()3130 void TabStrip::StartResizeLayoutTabsFromTouchTimer() {
3131   // Amount of time we delay before resizing after a close from a touch.
3132   constexpr auto kTouchResizeLayoutTime = base::TimeDelta::FromSeconds(2);
3133 
3134   resize_layout_timer_.Stop();
3135   resize_layout_timer_.Start(FROM_HERE, kTouchResizeLayoutTime, this,
3136                              &TabStrip::ResizeLayoutTabsFromTouch);
3137 }
3138 
AddMessageLoopObserver()3139 void TabStrip::AddMessageLoopObserver() {
3140   if (!mouse_watcher_) {
3141     constexpr int kTabStripAnimationVSlop = 40;
3142     mouse_watcher_ = std::make_unique<views::MouseWatcher>(
3143         std::make_unique<views::MouseWatcherViewHost>(
3144             this, gfx::Insets(0, 0, kTabStripAnimationVSlop, 0)),
3145         this);
3146   }
3147   mouse_watcher_->Start(GetWidget()->GetNativeWindow());
3148 }
3149 
RemoveMessageLoopObserver()3150 void TabStrip::RemoveMessageLoopObserver() {
3151   mouse_watcher_ = nullptr;
3152 }
3153 
GetDropBounds(int drop_index,bool drop_before,bool * is_beneath)3154 gfx::Rect TabStrip::GetDropBounds(int drop_index,
3155                                   bool drop_before,
3156                                   bool* is_beneath) {
3157   DCHECK_NE(drop_index, -1);
3158 
3159   Tab* tab = tab_at(std::min(drop_index, tab_count() - 1));
3160   int center_x = tab->x();
3161   const int width = tab->width();
3162   const int overlap = TabStyle::GetTabOverlap();
3163   if (drop_index < tab_count())
3164     center_x += drop_before ? (overlap / 2) : (width / 2);
3165   else
3166     center_x += width - (overlap / 2);
3167 
3168   // Mirror the center point if necessary.
3169   center_x = GetMirroredXInView(center_x);
3170 
3171   // Determine the screen bounds.
3172   gfx::Point drop_loc(center_x - g_drop_indicator_width / 2,
3173                       -g_drop_indicator_height);
3174   ConvertPointToScreen(this, &drop_loc);
3175   gfx::Rect drop_bounds(drop_loc.x(), drop_loc.y(), g_drop_indicator_width,
3176                         g_drop_indicator_height);
3177 
3178   // If the rect doesn't fit on the monitor, push the arrow to the bottom.
3179   display::Screen* screen = display::Screen::GetScreen();
3180   display::Display display = screen->GetDisplayMatching(drop_bounds);
3181   *is_beneath = !display.bounds().Contains(drop_bounds);
3182   if (*is_beneath)
3183     drop_bounds.Offset(0, drop_bounds.height() + height());
3184 
3185   return drop_bounds;
3186 }
3187 
SetDropArrow(const base::Optional<BrowserRootView::DropIndex> & index)3188 void TabStrip::SetDropArrow(
3189     const base::Optional<BrowserRootView::DropIndex>& index) {
3190   if (!index) {
3191     controller_->OnDropIndexUpdate(-1, false);
3192     drop_arrow_.reset();
3193     return;
3194   }
3195 
3196   // Let the controller know of the index update.
3197   controller_->OnDropIndexUpdate(index->value, index->drop_before);
3198 
3199   if (drop_arrow_ && (index == drop_arrow_->index()))
3200     return;
3201 
3202   bool is_beneath;
3203   gfx::Rect drop_bounds =
3204       GetDropBounds(index->value, index->drop_before, &is_beneath);
3205 
3206   if (!drop_arrow_) {
3207     drop_arrow_ = std::make_unique<DropArrow>(*index, !is_beneath, GetWidget());
3208   } else {
3209     drop_arrow_->set_index(*index);
3210     drop_arrow_->SetPointDown(!is_beneath);
3211   }
3212 
3213   // Reposition the window.
3214   drop_arrow_->SetWindowBounds(drop_bounds);
3215 }
3216 
3217 // static
GetDropArrowImage(bool is_down)3218 gfx::ImageSkia* TabStrip::GetDropArrowImage(bool is_down) {
3219   return ui::ResourceBundle::GetSharedInstance().GetImageSkiaNamed(
3220       is_down ? IDR_TAB_DROP_DOWN : IDR_TAB_DROP_UP);
3221 }
3222 
3223 // TabStrip:TabContextMenuController:
3224 // ----------------------------------------------------------
3225 
TabContextMenuController(TabStrip * parent)3226 TabStrip::TabContextMenuController::TabContextMenuController(TabStrip* parent)
3227     : parent_(parent) {}
3228 
ShowContextMenuForViewImpl(views::View * source,const gfx::Point & point,ui::MenuSourceType source_type)3229 void TabStrip::TabContextMenuController::ShowContextMenuForViewImpl(
3230     views::View* source,
3231     const gfx::Point& point,
3232     ui::MenuSourceType source_type) {
3233   // We are only intended to be installed as a context-menu handler for tabs, so
3234   // this cast should be safe.
3235   DCHECK(views::IsViewClass<Tab>(source));
3236   Tab* const tab = static_cast<Tab*>(source);
3237   if (tab->closing())
3238     return;
3239   parent_->controller()->ShowContextMenuForTab(tab, point, source_type);
3240 }
3241 
3242 // TabStrip:DropArrow:
3243 // ----------------------------------------------------------
3244 
DropArrow(const BrowserRootView::DropIndex & index,bool point_down,views::Widget * context)3245 TabStrip::DropArrow::DropArrow(const BrowserRootView::DropIndex& index,
3246                                bool point_down,
3247                                views::Widget* context)
3248     : index_(index), point_down_(point_down) {
3249   arrow_window_ = new views::Widget;
3250   views::Widget::InitParams params(views::Widget::InitParams::TYPE_POPUP);
3251   params.z_order = ui::ZOrderLevel::kFloatingUIElement;
3252   params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
3253   params.accept_events = false;
3254   params.bounds = gfx::Rect(g_drop_indicator_width, g_drop_indicator_height);
3255   params.context = context->GetNativeWindow();
3256   arrow_window_->Init(std::move(params));
3257   arrow_view_ =
3258       arrow_window_->SetContentsView(std::make_unique<views::ImageView>());
3259   arrow_view_->SetImage(GetDropArrowImage(point_down_));
3260   scoped_observer_.Add(arrow_window_);
3261 
3262   arrow_window_->Show();
3263 }
3264 
~DropArrow()3265 TabStrip::DropArrow::~DropArrow() {
3266   // Close eventually deletes the window, which deletes arrow_view too.
3267   if (arrow_window_)
3268     arrow_window_->Close();
3269 }
3270 
SetPointDown(bool down)3271 void TabStrip::DropArrow::SetPointDown(bool down) {
3272   if (point_down_ == down)
3273     return;
3274 
3275   point_down_ = down;
3276   arrow_view_->SetImage(GetDropArrowImage(point_down_));
3277 }
3278 
SetWindowBounds(const gfx::Rect & bounds)3279 void TabStrip::DropArrow::SetWindowBounds(const gfx::Rect& bounds) {
3280   arrow_window_->SetBounds(bounds);
3281 }
3282 
OnWidgetDestroying(views::Widget * widget)3283 void TabStrip::DropArrow::OnWidgetDestroying(views::Widget* widget) {
3284   scoped_observer_.Remove(arrow_window_);
3285   arrow_window_ = nullptr;
3286 }
3287 
3288 ///////////////////////////////////////////////////////////////////////////////
3289 
PrepareForAnimation()3290 void TabStrip::PrepareForAnimation() {
3291   if (!drag_context_->IsDragSessionActive() &&
3292       !TabDragController::IsAttachedTo(GetDragContext())) {
3293     for (int i = 0; i < tab_count(); ++i)
3294       tab_at(i)->set_dragging(false);
3295   }
3296 }
3297 
UpdateIdealBounds()3298 void TabStrip::UpdateIdealBounds() {
3299   if (tab_count() == 0)
3300     return;  // Should only happen during creation/destruction, ignore.
3301 
3302   // Update |last_available_width_| in case there is a different amount of
3303   // available width than there was in the last layout (e.g. if the tabstrip
3304   // is currently hidden).
3305   last_available_width_ = GetAvailableWidthForTabStrip();
3306 
3307   if (!touch_layout_) {
3308     const int available_width_for_tabs = CalculateAvailableWidthForTabs();
3309     layout_helper_->UpdateIdealBounds(available_width_for_tabs);
3310   }
3311 }
3312 
UpdateIdealBoundsForPinnedTabs(int * first_non_pinned_index)3313 int TabStrip::UpdateIdealBoundsForPinnedTabs(int* first_non_pinned_index) {
3314   layout_helper_->UpdateIdealBoundsForPinnedTabs();
3315   if (first_non_pinned_index)
3316     *first_non_pinned_index = layout_helper_->first_non_pinned_tab_index();
3317   return layout_helper_->first_non_pinned_tab_x();
3318 }
3319 
CalculateAvailableWidthForTabs() const3320 int TabStrip::CalculateAvailableWidthForTabs() const {
3321   return override_available_width_for_tabs_.value_or(
3322       GetAvailableWidthForTabStrip());
3323 }
3324 
GetAvailableWidthForTabStrip() const3325 int TabStrip::GetAvailableWidthForTabStrip() const {
3326   return available_width_callback_ ? available_width_callback_.Run() : width();
3327 }
3328 
StartResizeLayoutAnimation()3329 void TabStrip::StartResizeLayoutAnimation() {
3330   PrepareForAnimation();
3331   UpdateIdealBounds();
3332   AnimateToIdealBounds();
3333 }
3334 
StartPinnedTabAnimation()3335 void TabStrip::StartPinnedTabAnimation() {
3336   ExitTabClosingMode();
3337 
3338   PrepareForAnimation();
3339 
3340   UpdateIdealBounds();
3341   AnimateToIdealBounds();
3342 }
3343 
IsPointInTab(Tab * tab,const gfx::Point & point_in_tabstrip_coords)3344 bool TabStrip::IsPointInTab(Tab* tab,
3345                             const gfx::Point& point_in_tabstrip_coords) {
3346   if (!tab->GetVisible())
3347     return false;
3348   gfx::Point point_in_tab_coords(point_in_tabstrip_coords);
3349   View::ConvertPointToTarget(this, tab, &point_in_tab_coords);
3350   return tab->HitTestPoint(point_in_tab_coords);
3351 }
3352 
FindTabForEvent(const gfx::Point & point)3353 Tab* TabStrip::FindTabForEvent(const gfx::Point& point) {
3354   DCHECK(touch_layout_);
3355   int active_tab_index = touch_layout_->active_index();
3356   Tab* tab = FindTabForEventFrom(point, active_tab_index, -1);
3357   return tab ? tab : FindTabForEventFrom(point, active_tab_index + 1, 1);
3358 }
3359 
FindTabForEventFrom(const gfx::Point & point,int start,int delta)3360 Tab* TabStrip::FindTabForEventFrom(const gfx::Point& point,
3361                                    int start,
3362                                    int delta) {
3363   // |start| equals tab_count() when there are only pinned tabs.
3364   if (start == tab_count())
3365     start += delta;
3366   for (int i = start; i >= 0 && i < tab_count(); i += delta) {
3367     if (IsPointInTab(tab_at(i), point))
3368       return tab_at(i);
3369   }
3370   return nullptr;
3371 }
3372 
FindTabHitByPoint(const gfx::Point & point)3373 Tab* TabStrip::FindTabHitByPoint(const gfx::Point& point) {
3374   // Check all tabs, even closing tabs. Mouse events need to reach closing tabs
3375   // for users to be able to rapidly middle-click close several tabs.
3376   std::vector<Tab*> all_tabs = layout_helper_->GetTabs();
3377 
3378   // The display order doesn't necessarily match the child order, so we iterate
3379   // in display order.
3380   for (size_t i = 0; i < all_tabs.size(); ++i) {
3381     // If we don't first exclude points outside the current tab, the code below
3382     // will return the wrong tab if the next tab is selected, the following tab
3383     // is active, and |point| is in the overlap region between the two.
3384     Tab* tab = all_tabs[i];
3385     if (!IsPointInTab(tab, point))
3386       continue;
3387 
3388     // Selected tabs render atop unselected ones, and active tabs render atop
3389     // everything.  Check whether the next tab renders atop this one and |point|
3390     // is in the overlap region.
3391     Tab* next_tab = i < (all_tabs.size() - 1) ? all_tabs[i + 1] : nullptr;
3392     if (next_tab &&
3393         (next_tab->IsActive() ||
3394          (next_tab->IsSelected() && !tab->IsSelected())) &&
3395         IsPointInTab(next_tab, point))
3396       return next_tab;
3397 
3398     // This is the topmost tab for this point.
3399     return tab;
3400   }
3401 
3402   return nullptr;
3403 }
3404 
SwapLayoutIfNecessary()3405 void TabStrip::SwapLayoutIfNecessary() {
3406   bool needs_touch = NeedsTouchLayout();
3407   bool using_touch = touch_layout_ != nullptr;
3408   if (needs_touch == using_touch)
3409     return;
3410 
3411   if (needs_touch) {
3412     const int overlap = TabStyle::GetTabOverlap();
3413     touch_layout_ = std::make_unique<StackedTabStripLayout>(
3414         gfx::Size(GetStackableTabWidth(), GetLayoutConstant(TAB_HEIGHT)),
3415         overlap, kStackedPadding, kMaxStackedCount, &tabs_);
3416     touch_layout_->SetWidth(width());
3417     // This has to be after SetWidth() as SetWidth() is going to reset the
3418     // bounds of the pinned tabs (since StackedTabStripLayout doesn't yet know
3419     // how many pinned tabs there are).
3420     touch_layout_->SetXAndPinnedCount(UpdateIdealBoundsForPinnedTabs(nullptr),
3421                                       GetPinnedTabCount());
3422     touch_layout_->SetActiveIndex(controller_->GetActiveIndex());
3423 
3424     base::RecordAction(
3425         base::UserMetricsAction("StackedTab_EnteredStackedLayout"));
3426   } else {
3427     touch_layout_.reset();
3428   }
3429   PrepareForAnimation();
3430   UpdateIdealBounds();
3431   AnimateToIdealBounds();
3432   SetTabSlotVisibility();
3433 }
3434 
NeedsTouchLayout() const3435 bool TabStrip::NeedsTouchLayout() const {
3436   if (!stacked_layout_)
3437     return false;
3438 
3439   // If a group is active in the tabstrip, the layout will not be swapped to
3440   // stacked mode due to incompatibility of the UI.
3441   // As an alternative, Tab Groups do interoperate with the WebUI Tab Strip,
3442   // which is enabled in situations when stacked tabs are not.
3443   if (!group_views_.empty())
3444     return false;
3445 
3446   // If tab scrolling is on, the layout will not be swapped; tab scrolling is
3447   // a replacement to stacked tabs providing similar functionality across both
3448   // touch and non-touch platforms.
3449   if (base::FeatureList::IsEnabled(features::kScrollableTabStrip))
3450     return false;
3451 
3452   const int pinned_tab_count = GetPinnedTabCount();
3453   const int normal_count = tab_count() - pinned_tab_count;
3454   if (normal_count <= 1)
3455     return false;
3456 
3457   const int tab_overlap = TabStyle::GetTabOverlap();
3458   const int normal_width =
3459       (GetStackableTabWidth() - tab_overlap) * normal_count + tab_overlap;
3460   const int pinned_width =
3461       std::max(0, pinned_tab_count * TabStyle::GetPinnedWidth() - tab_overlap);
3462   return normal_width > (width() - pinned_width);
3463 }
3464 
SetResetToShrinkOnExit(bool value)3465 void TabStrip::SetResetToShrinkOnExit(bool value) {
3466   if (!adjust_layout_)
3467     return;
3468 
3469   // We have to be using stacked layout to reset out of it.
3470   value &= stacked_layout_;
3471 
3472   if (value == reset_to_shrink_on_exit_)
3473     return;
3474 
3475   reset_to_shrink_on_exit_ = value;
3476   // Add an observer so we know when the mouse moves out of the tabstrip.
3477   if (reset_to_shrink_on_exit_)
3478     AddMessageLoopObserver();
3479   else
3480     RemoveMessageLoopObserver();
3481 }
3482 
OnTabSlotAnimationProgressed(TabSlotView * view)3483 void TabStrip::OnTabSlotAnimationProgressed(TabSlotView* view) {
3484   // The rightmost tab moving might have changed the tabstrip's preferred width.
3485   PreferredSizeChanged();
3486   if (view->group())
3487     UpdateTabGroupVisuals(view->group().value());
3488 }
3489 
UpdateTabGroupVisuals(tab_groups::TabGroupId group_id)3490 void TabStrip::UpdateTabGroupVisuals(tab_groups::TabGroupId group_id) {
3491   const auto group_views = group_views_.find(group_id);
3492   if (group_views != group_views_.end())
3493     group_views->second->UpdateBounds();
3494 }
3495 
OnMousePressed(const ui::MouseEvent & event)3496 bool TabStrip::OnMousePressed(const ui::MouseEvent& event) {
3497   UpdateStackedLayoutFromMouseEvent(this, event);
3498   // We can't return true here, else clicking in an empty area won't drag the
3499   // window.
3500   return false;
3501 }
3502 
OnMouseDragged(const ui::MouseEvent & event)3503 bool TabStrip::OnMouseDragged(const ui::MouseEvent& event) {
3504   ContinueDrag(this, event);
3505   return true;
3506 }
3507 
OnMouseReleased(const ui::MouseEvent & event)3508 void TabStrip::OnMouseReleased(const ui::MouseEvent& event) {
3509   EndDrag(END_DRAG_COMPLETE);
3510   UpdateStackedLayoutFromMouseEvent(this, event);
3511 }
3512 
OnMouseCaptureLost()3513 void TabStrip::OnMouseCaptureLost() {
3514   EndDrag(END_DRAG_CAPTURE_LOST);
3515 }
3516 
OnMouseMoved(const ui::MouseEvent & event)3517 void TabStrip::OnMouseMoved(const ui::MouseEvent& event) {
3518   UpdateStackedLayoutFromMouseEvent(this, event);
3519 }
3520 
OnMouseEntered(const ui::MouseEvent & event)3521 void TabStrip::OnMouseEntered(const ui::MouseEvent& event) {
3522   mouse_entered_tabstrip_time_ = base::TimeTicks::Now();
3523   SetResetToShrinkOnExit(true);
3524 }
3525 
OnMouseExited(const ui::MouseEvent & event)3526 void TabStrip::OnMouseExited(const ui::MouseEvent& event) {
3527   if (base::FeatureList::IsEnabled(features::kTabHoverCards) && hover_card_ &&
3528       hover_card_->IsVisible()) {
3529     hover_card_->set_last_mouse_exit_timestamp(base::TimeTicks::Now());
3530   }
3531   UpdateHoverCard(nullptr);
3532 }
3533 
AddedToWidget()3534 void TabStrip::AddedToWidget() {
3535   GetWidget()->AddObserver(this);
3536 }
3537 
RemovedFromWidget()3538 void TabStrip::RemovedFromWidget() {
3539   GetWidget()->RemoveObserver(this);
3540 }
3541 
OnGestureEvent(ui::GestureEvent * event)3542 void TabStrip::OnGestureEvent(ui::GestureEvent* event) {
3543   SetResetToShrinkOnExit(false);
3544   switch (event->type()) {
3545     case ui::ET_GESTURE_SCROLL_END:
3546     case ui::ET_SCROLL_FLING_START:
3547     case ui::ET_GESTURE_END:
3548       EndDrag(END_DRAG_COMPLETE);
3549       if (adjust_layout_) {
3550         SetStackedLayout(true);
3551         controller_->StackedLayoutMaybeChanged();
3552       }
3553       break;
3554 
3555     case ui::ET_GESTURE_LONG_PRESS:
3556       drag_context_->SetMoveBehavior(TabDragController::REORDER);
3557       break;
3558 
3559     case ui::ET_GESTURE_LONG_TAP: {
3560       EndDrag(END_DRAG_CANCEL);
3561       gfx::Point local_point = event->location();
3562       Tab* tab = touch_layout_ ? FindTabForEvent(local_point)
3563                                : FindTabHitByPoint(local_point);
3564       if (tab) {
3565         ConvertPointToScreen(this, &local_point);
3566         controller_->ShowContextMenuForTab(tab, local_point,
3567                                            ui::MENU_SOURCE_TOUCH);
3568       }
3569       break;
3570     }
3571 
3572     case ui::ET_GESTURE_SCROLL_UPDATE:
3573       ContinueDrag(this, *event);
3574       break;
3575 
3576     case ui::ET_GESTURE_TAP_DOWN:
3577       EndDrag(END_DRAG_CANCEL);
3578       break;
3579 
3580     case ui::ET_GESTURE_TAP: {
3581       const int active_index = controller_->GetActiveIndex();
3582       DCHECK_NE(-1, active_index);
3583       Tab* active_tab = tab_at(active_index);
3584       TouchUMA::GestureActionType action = TouchUMA::kGestureTabNoSwitchTap;
3585       if (active_tab->tab_activated_with_last_tap_down())
3586         action = TouchUMA::kGestureTabSwitchTap;
3587       TouchUMA::RecordGestureAction(action);
3588       break;
3589     }
3590 
3591     default:
3592       break;
3593   }
3594   event->SetHandled();
3595 }
3596 
TargetForRect(views::View * root,const gfx::Rect & rect)3597 views::View* TabStrip::TargetForRect(views::View* root, const gfx::Rect& rect) {
3598   CHECK_EQ(root, this);
3599 
3600   if (!views::UsePointBasedTargeting(rect))
3601     return views::ViewTargeterDelegate::TargetForRect(root, rect);
3602   const gfx::Point point(rect.CenterPoint());
3603 
3604   if (!touch_layout_) {
3605     // Return any view that isn't a Tab or this TabStrip immediately. We don't
3606     // want to interfere.
3607     views::View* v = views::ViewTargeterDelegate::TargetForRect(root, rect);
3608     if (v && v != this && !views::IsViewClass<Tab>(v))
3609       return v;
3610 
3611     views::View* tab = FindTabHitByPoint(point);
3612     if (tab)
3613       return tab;
3614   } else {
3615     Tab* tab = FindTabForEvent(point);
3616     if (tab)
3617       return ConvertPointToViewAndGetEventHandler(this, tab, point);
3618   }
3619   return this;
3620 }
3621 
OnViewIsDeleting(views::View * observed_view)3622 void TabStrip::OnViewIsDeleting(views::View* observed_view) {
3623   if (observed_view == hover_card_) {
3624     hover_card_observer_.Remove(hover_card_);
3625     hover_card_event_sniffer_.reset();
3626     hover_card_ = nullptr;
3627   }
3628 }
3629 
OnViewFocused(views::View * observed_view)3630 void TabStrip::OnViewFocused(views::View* observed_view) {
3631   int index = tabs_.GetIndexOfView(observed_view);
3632   if (index != -1)
3633     controller_->OnKeyboardFocusedTabChanged(index);
3634 }
3635 
OnViewBlurred(views::View * observed_view)3636 void TabStrip::OnViewBlurred(views::View* observed_view) {
3637   controller_->OnKeyboardFocusedTabChanged(base::nullopt);
3638 }
3639 
OnTouchUiChanged()3640 void TabStrip::OnTouchUiChanged() {
3641   StopAnimating(true);
3642   PreferredSizeChanged();
3643 }
3644 
AnnounceTabAddedToGroup(tab_groups::TabGroupId group_id)3645 void TabStrip::AnnounceTabAddedToGroup(tab_groups::TabGroupId group_id) {
3646   const base::string16 group_title = controller()->GetGroupTitle(group_id);
3647   const base::string16 contents_string =
3648       controller()->GetGroupContentString(group_id);
3649   GetViewAccessibility().AnnounceText(
3650       group_title.empty()
3651           ? l10n_util::GetStringFUTF16(
3652                 IDS_TAB_AX_ANNOUNCE_TAB_ADDED_TO_UNNAMED_GROUP, contents_string)
3653           : l10n_util::GetStringFUTF16(
3654                 IDS_TAB_AX_ANNOUNCE_TAB_ADDED_TO_NAMED_GROUP, group_title,
3655                 contents_string));
3656 }
3657 
AnnounceTabRemovedFromGroup(tab_groups::TabGroupId group_id)3658 void TabStrip::AnnounceTabRemovedFromGroup(tab_groups::TabGroupId group_id) {
3659   const base::string16 group_title = controller()->GetGroupTitle(group_id);
3660   const base::string16 contents_string =
3661       controller()->GetGroupContentString(group_id);
3662   GetViewAccessibility().AnnounceText(
3663       group_title.empty()
3664           ? l10n_util::GetStringFUTF16(
3665                 IDS_TAB_AX_ANNOUNCE_TAB_REMOVED_FROM_UNNAMED_GROUP,
3666                 contents_string)
3667           : l10n_util::GetStringFUTF16(
3668                 IDS_TAB_AX_ANNOUNCE_TAB_REMOVED_FROM_NAMED_GROUP, group_title,
3669                 contents_string));
3670 }
3671