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