1 // Copyright 2019 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/toolbar/webui_tab_counter_button.h"
6 
7 #include <memory>
8 
9 #include "base/bind.h"
10 #include "base/i18n/message_formatter.h"
11 #include "base/i18n/number_formatting.h"
12 #include "base/strings/string16.h"
13 #include "base/strings/utf_string_conversions.h"
14 #include "chrome/app/vector_icons/vector_icons.h"
15 #include "chrome/browser/themes/theme_properties.h"
16 #include "chrome/browser/ui/browser.h"
17 #include "chrome/browser/ui/layout_constants.h"
18 #include "chrome/browser/ui/tabs/tab_strip_model.h"
19 #include "chrome/browser/ui/tabs/tab_strip_model_delegate.h"
20 #include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
21 #include "chrome/browser/ui/views/chrome_layout_provider.h"
22 #include "chrome/browser/ui/views/chrome_typography.h"
23 #include "chrome/browser/ui/views/chrome_view_class_properties.h"
24 #include "chrome/browser/ui/views/flying_indicator.h"
25 #include "chrome/browser/ui/views/frame/browser_view.h"
26 #include "chrome/browser/ui/views/toolbar/toolbar_ink_drop_util.h"
27 #include "chrome/browser/ui/views/user_education/feature_promo_colors.h"
28 #include "chrome/grit/generated_resources.h"
29 #include "components/vector_icons/vector_icons.h"
30 #include "ui/aura/window.h"
31 #include "ui/base/l10n/l10n_util.h"
32 #include "ui/base/models/image_model.h"
33 #include "ui/base/models/menu_separator_types.h"
34 #include "ui/base/models/simple_menu_model.h"
35 #include "ui/base/theme_provider.h"
36 #include "ui/gfx/animation/multi_animation.h"
37 #include "ui/gfx/animation/slide_animation.h"
38 #include "ui/gfx/animation/tween.h"
39 #include "ui/gfx/color_palette.h"
40 #include "ui/gfx/favicon_size.h"
41 #include "ui/gfx/geometry/point.h"
42 #include "ui/gfx/geometry/vector2d.h"
43 #include "ui/gfx/paint_vector_icon.h"
44 #include "ui/views/animation/ink_drop.h"
45 #include "ui/views/animation/ink_drop_highlight.h"
46 #include "ui/views/bubble/bubble_dialog_delegate_view.h"
47 #include "ui/views/bubble/bubble_frame_view.h"
48 #include "ui/views/context_menu_controller.h"
49 #include "ui/views/controls/image_view.h"
50 #include "ui/views/controls/label.h"
51 #include "ui/views/controls/menu/menu_model_adapter.h"
52 #include "ui/views/controls/menu/menu_runner.h"
53 #include "ui/views/controls/throbber.h"
54 #include "ui/views/layout/fill_layout.h"
55 #include "ui/views/layout/flex_layout.h"
56 #include "ui/views/layout/layout_provider.h"
57 #include "ui/views/style/typography.h"
58 #include "ui/views/view_class_properties.h"
59 #include "ui/views/widget/native_widget.h"
60 #include "ui/views/widget/widget.h"
61 
62 namespace {
63 
64 // The distance to move a label so it appears "offscreen" - that is, the text
65 // will be clipped by the border and not visible.
66 constexpr int kOffscreenLabelDistance = 16;
67 
68 constexpr base::TimeDelta kFirstPartDuration =
69     base::TimeDelta::FromMilliseconds(100);
70 
71 // Returns whether |change| to |tab_strip_mode| should start the tab counter
72 // throbber animation.
ShouldChangeStartThrobber(TabStripModel * tab_strip_model,const TabStripModelChange & change)73 bool ShouldChangeStartThrobber(TabStripModel* tab_strip_model,
74                                const TabStripModelChange& change) {
75   if (change.type() != TabStripModelChange::kInserted)
76     return false;
77   const auto& contents = change.GetInsert()->contents;
78   return contents.size() > 1 ||
79          tab_strip_model->GetActiveWebContents() != contents[0].contents;
80 }
81 
GetTabCounterLabelText(int num_tabs)82 base::string16 GetTabCounterLabelText(int num_tabs) {
83   // In the triple-digit case, fall back to ':D' to match Android.
84   if (num_tabs >= 100)
85     return base::string16(base::ASCIIToUTF16(":D"));
86   return base::FormatNumber(num_tabs);
87 }
88 
89 //------------------------------------------------------------------------
90 // NumberLabel
91 
92 // Label to display a number of tabs. Because there is limited space within the
93 // tab counter border, the font shrinks when the count is 10 or higher.
94 class NumberLabel : public views::Label {
95  public:
NumberLabel()96   NumberLabel() : Label(base::string16(), CONTEXT_TAB_COUNTER) {
97     single_digit_font_ = font_list();
98     double_digit_font_ = views::style::GetFont(CONTEXT_TAB_COUNTER,
99                                                views::style::STYLE_SECONDARY);
100   }
101 
102   ~NumberLabel() override = default;
103 
SetText(const base::string16 & text)104   void SetText(const base::string16& text) override {
105     SetFontList(text.length() > 1 ? double_digit_font_ : single_digit_font_);
106     Label::SetText(text);
107   }
108 
109  private:
110   gfx::FontList single_digit_font_;
111   gfx::FontList double_digit_font_;
112 };
113 
114 ///////////////////////////////////////////////////////////////////////////////
115 // InteractionTracker
116 
117 // Listens in on the widget event stream (as a pre target event handler) and
118 // records user interactions (mouse clicks, taps, etc.) Used so that we know
119 // where a link that was opened in a background tab was opened from so that we
120 // can play a "flying link" animation.
121 class InteractionTracker : public ui::EventHandler,
122                            public views::WidgetObserver {
123  public:
InteractionTracker(views::Widget * widget)124   explicit InteractionTracker(views::Widget* widget)
125       : native_window_(widget->GetNativeWindow()) {
126     if (native_window_)
127       native_window_->AddPreTargetHandler(this);
128     scoped_widget_observer_.Add(widget);
129   }
130 
131   InteractionTracker(const InteractionTracker& other) = delete;
132   void operator=(const InteractionTracker& other) = delete;
133 
~InteractionTracker()134   ~InteractionTracker() override {
135     if (native_window_)
136       native_window_->RemovePreTargetHandler(this);
137   }
138 
last_interaction_location() const139   const base::Optional<gfx::Point>& last_interaction_location() const {
140     return last_interaction_location_;
141   }
142 
143  private:
144   // ui::EventHandler:
OnEvent(ui::Event * event)145   void OnEvent(ui::Event* event) override {
146     if (event->type() == ui::ET_MOUSE_PRESSED ||
147         event->type() == ui::ET_MOUSE_RELEASED ||
148         event->type() == ui::ET_TOUCH_PRESSED) {
149       const ui::LocatedEvent* const located = event->AsLocatedEvent();
150       last_interaction_location_ =
151           located->target()->GetScreenLocation(*located);
152     }
153   }
154 
155   // views::WidgetObserver:
OnWidgetBoundsChanged(views::Widget * widget,const gfx::Rect & new_bounds)156   void OnWidgetBoundsChanged(views::Widget* widget,
157                              const gfx::Rect& new_bounds) override {
158     last_interaction_location_.reset();
159   }
OnWidgetDestroying(views::Widget * widget)160   void OnWidgetDestroying(views::Widget* widget) override {
161     // Clean up all of our observers and event handlers before the native window
162     // disappears.
163     scoped_widget_observer_.Remove(widget);
164     if (widget->GetNativeWindow()) {
165       widget->GetNativeWindow()->RemovePreTargetHandler(this);
166       native_window_ = nullptr;
167     }
168   }
169 
170   base::Optional<gfx::Point> last_interaction_location_;
171   gfx::NativeWindow native_window_;
172   ScopedObserver<views::Widget, views::WidgetObserver> scoped_widget_observer_{
173       this};
174 };
175 
176 //------------------------------------------------------------------------
177 // TabCounterAnimator
178 
179 // Animates the label and border. |border_view_| does a little bounce. At the
180 // peak of |border_view_|'s bounce, the |disappearing_label_| begins to scroll
181 // away in the same direction and is replaced with |appearing_label_|, which
182 // shows the new number of tabs. This animation is played upside-down when a tab
183 // is added vs. removed.
184 class TabCounterAnimator : public gfx::AnimationDelegate {
185  public:
186   TabCounterAnimator(views::Label* appearing_label,
187                      views::Label* disappearing_label,
188                      views::View* border_view,
189                      views::Throbber* throbber);
190   TabCounterAnimator(const TabCounterAnimator&) = delete;
191   void operator=(const TabCounterAnimator&) = delete;
192   ~TabCounterAnimator() override = default;
193 
194   void Animate(int new_num_tabs, bool should_start_throbber);
195   void StartFlyingLinkFrom(const gfx::Point& screen_position);
196   void LayoutIfAnimating();
197 
198  private:
199   // Describes the current counter animation (if any). The animation is played
200   // one way to show a decrease, and upside down from that to show an increase.
201   enum class TabCounterAnimationType { kNone, kIncreasing, kDecreasing };
202 
203   // AnimationDelegate:
204   void AnimationProgressed(const gfx::Animation* animation) override;
205   void AnimationEnded(const gfx::Animation* animation) override;
206 
207   void MaybeStartPendingAnimation();
208   void StartAnimation();
209 
210   int GetBorderTargetYDelta() const;
211   int GetBorderOvershootYDelta() const;
212   int GetAppearingLabelStartPosition() const;
213   int GetDisappearingLabelTargetPosition() const;
214   int GetBorderStartingY() const;
215 
216   base::Optional<int> last_num_tabs_;
217   base::Optional<int> pending_num_tabs_ = 0;
218   bool pending_throbber_ = false;
219   TabCounterAnimationType current_animation_ = TabCounterAnimationType::kNone;
220 
221   // The label that will be animated into view, showing the new value.
222   views::Label* const appearing_label_;
223   // The label that will be animated out of view, showing the old value.
224   views::Label* const disappearing_label_;
225   gfx::MultiAnimation label_animation_;
226 
227   views::View* const border_view_;
228   gfx::MultiAnimation border_animation_;
229 
230   views::Throbber* const throbber_;
231   base::OneShotTimer throbber_timer_;
232 
233   std::unique_ptr<FlyingIndicator> flying_link_;
234 };
235 
TabCounterAnimator(views::Label * appearing_label,views::Label * disappearing_label,views::View * border_view,views::Throbber * throbber)236 TabCounterAnimator::TabCounterAnimator(views::Label* appearing_label,
237                                        views::Label* disappearing_label,
238                                        views::View* border_view,
239                                        views::Throbber* throbber)
240     : appearing_label_(appearing_label),
241       disappearing_label_(disappearing_label),
242       label_animation_(
243           std::vector<gfx::MultiAnimation::Part>{
244               // Stay in place.
245               gfx::MultiAnimation::Part(kFirstPartDuration,
246                                         gfx::Tween::Type::ZERO),
247               // Swap out to the new label.
248               gfx::MultiAnimation::Part(base::TimeDelta::FromMilliseconds(200),
249                                         gfx::Tween::Type::EASE_IN_OUT)},
250           gfx::MultiAnimation::kDefaultTimerInterval),
251       border_view_(border_view),
252       border_animation_(
253           std::vector<gfx::MultiAnimation::Part>{
254               gfx::MultiAnimation::Part(kFirstPartDuration,
255                                         gfx::Tween::Type::EASE_OUT),
256               gfx::MultiAnimation::Part(base::TimeDelta::FromMilliseconds(150),
257                                         gfx::Tween::Type::EASE_IN_OUT),
258               gfx::MultiAnimation::Part(base::TimeDelta::FromMilliseconds(50),
259                                         gfx::Tween::Type::EASE_IN_OUT)},
260           gfx::MultiAnimation::kDefaultTimerInterval),
261       throbber_(throbber) {
262   label_animation_.set_delegate(this);
263   label_animation_.set_continuous(false);
264 
265   border_animation_.set_delegate(this);
266   border_animation_.set_continuous(false);
267 }
268 
Animate(int new_num_tabs,bool should_start_throbber)269 void TabCounterAnimator::Animate(int new_num_tabs, bool should_start_throbber) {
270   pending_num_tabs_ = new_num_tabs;
271   pending_throbber_ |= should_start_throbber;
272   MaybeStartPendingAnimation();
273 }
274 
MaybeStartPendingAnimation()275 void TabCounterAnimator::MaybeStartPendingAnimation() {
276   if (flying_link_ && flying_link_->is_flying())
277     return;
278 
279   if (pending_throbber_) {
280     // If the throbber is already showing, just reset the timer so that the
281     // animation continues smoothly for tabs created in quick succession.
282     if (throbber_timer_.IsRunning()) {
283       throbber_timer_.Reset();
284     } else {
285       throbber_->Start();
286 
287       // Automatically stop the throbber after 1 second. Currently we do not
288       // check the real loading state of the new tab(s), as that adds
289       // unnecessary complexity. The purpose of the throbber is just to
290       // indicate to the user that some activity has happened in the
291       // background, which may not otherwise have been obvious because the tab
292       // strip is hidden in this mode.
293       throbber_timer_.Start(FROM_HERE, base::TimeDelta::FromMilliseconds(1000),
294                             throbber_, &views::Throbber::Stop);
295     }
296     pending_throbber_ = false;
297   }
298 
299   if (pending_num_tabs_.has_value()) {
300     if (last_num_tabs_.has_value() &&
301         last_num_tabs_.value() != pending_num_tabs_.value()) {
302       current_animation_ = pending_num_tabs_.value() > last_num_tabs_.value()
303                                ? TabCounterAnimationType::kIncreasing
304                                : TabCounterAnimationType::kDecreasing;
305       disappearing_label_->SetText(appearing_label_->GetText());
306       appearing_label_->SetText(
307           GetTabCounterLabelText(pending_num_tabs_.value()));
308       border_animation_.Stop();
309       border_animation_.Start();
310       label_animation_.Stop();
311       label_animation_.Start();
312       appearing_label_->InvalidateLayout();
313       LayoutIfAnimating();
314     } else if (!last_num_tabs_.has_value()) {
315       appearing_label_->SetText(
316           GetTabCounterLabelText(pending_num_tabs_.value()));
317     }
318     last_num_tabs_ = pending_num_tabs_;
319   }
320 }
321 
StartFlyingLinkFrom(const gfx::Point & screen_position)322 void TabCounterAnimator::StartFlyingLinkFrom(
323     const gfx::Point& screen_position) {
324   flying_link_ = FlyingIndicator::StartFlyingIndicator(
325       kWebIcon, screen_position, throbber_,
326       base::BindOnce(&TabCounterAnimator::MaybeStartPendingAnimation,
327                      base::Unretained(this)));
328 }
329 
LayoutIfAnimating()330 void TabCounterAnimator::LayoutIfAnimating() {
331   if (!border_animation_.is_animating() && !label_animation_.is_animating())
332     return;
333 
334   // |border_view_| does a hop or a dip based on animation type.
335   int border_y_delta = 0;
336   switch (border_animation_.current_part_index()) {
337     case 0:
338       // Move away.
339       border_y_delta = gfx::Tween::IntValueBetween(
340           border_animation_.GetCurrentValue(), 0, GetBorderTargetYDelta());
341       break;
342     case 1:
343       // Return, slightly overshooting the start position.
344       border_y_delta = gfx::Tween::IntValueBetween(
345           border_animation_.GetCurrentValue(), GetBorderTargetYDelta(),
346           GetBorderOvershootYDelta());
347       break;
348     case 2:
349       // Return back to the start position.
350       border_y_delta = gfx::Tween::IntValueBetween(
351           border_animation_.GetCurrentValue(), GetBorderOvershootYDelta(), 0);
352       break;
353     default:
354       NOTREACHED();
355   }
356   border_view_->SetY(GetBorderStartingY() + border_y_delta);
357 
358   // |appearing_label_| scrolls into view - from above if the counter is
359   // increasing, below if it is decreasing.
360   const int appearing_label_position = gfx::Tween::IntValueBetween(
361       label_animation_.GetCurrentValue(), GetAppearingLabelStartPosition(), 0);
362   appearing_label_->SetY(appearing_label_position - border_y_delta);
363 
364   // |disappearing_label_| scrolls out of view - out the bottom if
365   // |appearing_label_| is decreasing, and from below if increasing.
366   const int disappearing_label_position =
367       gfx::Tween::IntValueBetween(label_animation_.GetCurrentValue(), 0,
368                                   GetDisappearingLabelTargetPosition());
369   disappearing_label_->SetY(disappearing_label_position - border_y_delta);
370 }
371 
AnimationProgressed(const gfx::Animation * animation)372 void TabCounterAnimator::AnimationProgressed(const gfx::Animation* animation) {
373   LayoutIfAnimating();
374 }
375 
AnimationEnded(const gfx::Animation * animation)376 void TabCounterAnimator::AnimationEnded(const gfx::Animation* animation) {
377   AnimationProgressed(animation);
378 }
379 
GetBorderTargetYDelta() const380 int TabCounterAnimator::GetBorderTargetYDelta() const {
381   constexpr int kBorderBounceDistance = 4;
382   switch (current_animation_) {
383     case TabCounterAnimationType::kIncreasing:
384       return kBorderBounceDistance;
385     case TabCounterAnimationType::kDecreasing:
386       return -kBorderBounceDistance;
387     default:
388       NOTREACHED();
389       return 0;
390   }
391 }
392 
GetBorderOvershootYDelta() const393 int TabCounterAnimator::GetBorderOvershootYDelta() const {
394   constexpr int kBorderBounceOvershoot = 2;
395   switch (current_animation_) {
396     case TabCounterAnimationType::kIncreasing:
397       return -kBorderBounceOvershoot;
398     case TabCounterAnimationType::kDecreasing:
399       return kBorderBounceOvershoot;
400     default:
401       NOTREACHED();
402       return 0;
403   }
404 }
405 
GetAppearingLabelStartPosition() const406 int TabCounterAnimator::GetAppearingLabelStartPosition() const {
407   switch (current_animation_) {
408     case TabCounterAnimationType::kIncreasing:
409       return -kOffscreenLabelDistance;
410     case TabCounterAnimationType::kDecreasing:
411       return kOffscreenLabelDistance;
412     default:
413       NOTREACHED();
414       return 0;
415   }
416 }
417 
GetDisappearingLabelTargetPosition() const418 int TabCounterAnimator::GetDisappearingLabelTargetPosition() const {
419   // We want to exit out the opposite side that |appearing_label_| entered
420   // from.
421   return -GetAppearingLabelStartPosition();
422 }
423 
GetBorderStartingY() const424 int TabCounterAnimator::GetBorderStartingY() const {
425   // When at rest, |border_view_| should be vertically centered within its
426   // container.
427   views::View* border_container = border_view_->parent();
428   int border_available_space = border_container->GetLocalBounds().height();
429   return (border_available_space - border_view_->GetLocalBounds().height()) / 2;
430 }
431 
432 //------------------------------------------------------------------------
433 // WebUITabCounterButton
434 
435 class WebUITabCounterButton : public views::Button,
436                               public TabStripModelObserver,
437                               public views::ContextMenuController,
438                               public ui::SimpleMenuModel::Delegate {
439  public:
440   static constexpr int WEBUI_TAB_COUNTER_CXMENU_CLOSE_TAB = 13;
441   static constexpr int WEBUI_TAB_COUNTER_CXMENU_NEW_TAB = 14;
442 
443   WebUITabCounterButton(PressedCallback pressed_callback,
444                         BrowserView* browser_view);
445   ~WebUITabCounterButton() override;
446 
447   void UpdateTooltip(int tab_count);
448   void UpdateColors();
449   void Init();
450 
451  private:
452   // views::Button:
453   void AddedToWidget() override;
454   void AfterPropertyChange(const void* key, int64_t old_value) override;
455   void AddLayerBeneathView(ui::Layer* new_layer) override;
456   void RemoveLayerBeneathView(ui::Layer* old_layer) override;
457   void OnThemeChanged() override;
458   void Layout() override;
459 
460   // TabStripModelObserver:
461   void OnTabStripModelChanged(
462       TabStripModel* tab_strip_model,
463       const TabStripModelChange& change,
464       const TabStripSelectionChange& selection) override;
465 
466   // views::ContextMenuController:
467   void ShowContextMenuForViewImpl(views::View* source,
468                                   const gfx::Point& point,
469                                   ui::MenuSourceType source_type) override;
470 
471   // ui::SimpleMenuModel::Delegate:
472   void ExecuteCommand(int command_id, int event_flags) override;
473 
474   void MaybeStartFlyingLink(WindowOpenDisposition disposition);
475 
476   views::InkDropContainerView* ink_drop_container_;
477   views::Label* appearing_label_;
478   views::Label* disappearing_label_;
479   views::View* border_view_;
480   std::unique_ptr<TabCounterAnimator> animator_;
481   views::Throbber* throbber_;
482 
483   std::unique_ptr<ui::SimpleMenuModel> menu_model_;
484   std::unique_ptr<views::MenuRunner> menu_runner_;
485   std::unique_ptr<InteractionTracker> interaction_tracker_;
486 
487   TabStripModel* const tab_strip_model_;
488   BrowserView* const browser_view_;
489   BrowserView::OnLinkOpeningFromGestureSubscription
490       link_opened_from_gesture_subscription_;
491 };
492 
WebUITabCounterButton(PressedCallback pressed_callback,BrowserView * browser_view)493 WebUITabCounterButton::WebUITabCounterButton(PressedCallback pressed_callback,
494                                              BrowserView* browser_view)
495     : Button(std::move(pressed_callback)),
496       tab_strip_model_(browser_view->browser()->tab_strip_model()),
497       browser_view_(browser_view) {
498   // Not focusable by default, only for accessibility.
499   SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
500 }
501 
502 WebUITabCounterButton::~WebUITabCounterButton() = default;
503 
UpdateTooltip(int num_tabs)504 void WebUITabCounterButton::UpdateTooltip(int num_tabs) {
505   SetTooltipText(base::i18n::MessageFormatter::FormatWithNumberedArgs(
506       l10n_util::GetStringUTF16(IDS_TOOLTIP_WEBUI_TAB_STRIP_TAB_COUNTER),
507       num_tabs));
508 }
509 
UpdateColors()510 void WebUITabCounterButton::UpdateColors() {
511   const ui::ThemeProvider* theme_provider = GetThemeProvider();
512   const SkColor toolbar_color =
513       theme_provider ? theme_provider->GetColor(ThemeProperties::COLOR_TOOLBAR)
514                      : gfx::kPlaceholderColor;
515   appearing_label_->SetBackgroundColor(toolbar_color);
516   disappearing_label_->SetBackgroundColor(toolbar_color);
517 
518   const SkColor normal_text_color =
519       theme_provider
520           ? theme_provider->GetColor(ThemeProperties::COLOR_TOOLBAR_BUTTON_ICON)
521           : gfx::kPlaceholderColor;
522   const SkColor current_text_color =
523       GetProperty(kHasInProductHelpPromoKey)
524           ? GetFeaturePromoHighlightColorForToolbar(theme_provider)
525           : normal_text_color;
526 
527   appearing_label_->SetEnabledColor(current_text_color);
528   disappearing_label_->SetEnabledColor(current_text_color);
529   border_view_->SetBorder(views::CreateRoundedRectBorder(
530       2,
531       views::LayoutProvider::Get()->GetCornerRadiusMetric(
532           views::EMPHASIS_MEDIUM),
533       current_text_color));
534 }
535 
Init()536 void WebUITabCounterButton::Init() {
537   SetProperty(
538       views::kFlexBehaviorKey,
539       views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToMinimum,
540                                views::MaximumFlexSizeRule::kPreferred)
541           .WithOrder(1));
542 
543   const int button_height = GetLayoutConstant(TOOLBAR_BUTTON_HEIGHT);
544   SetPreferredSize(gfx::Size(button_height, button_height));
545 
546   ink_drop_container_ =
547       AddChildView(std::make_unique<views::InkDropContainerView>());
548   ink_drop_container_->SetBoundsRect(GetLocalBounds());
549 
550   throbber_ = AddChildView(std::make_unique<views::Throbber>());
551 
552   border_view_ = AddChildView(std::make_unique<views::View>());
553 
554   appearing_label_ =
555       border_view_->AddChildView(std::make_unique<NumberLabel>());
556   disappearing_label_ =
557       border_view_->AddChildView(std::make_unique<NumberLabel>());
558 
559   animator_ = std::make_unique<TabCounterAnimator>(
560       appearing_label_, disappearing_label_, border_view_, throbber_);
561 
562   set_context_menu_controller(this);
563   menu_model_ = std::make_unique<ui::SimpleMenuModel>(this);
564   menu_model_->AddItemWithIcon(
565       WEBUI_TAB_COUNTER_CXMENU_CLOSE_TAB,
566       l10n_util::GetStringUTF16(
567           IDS_WEBUI_TAB_STRIP_TAB_COUNTER_CXMENU_CLOSE_TAB),
568       ui::ImageModel::FromImageSkia(gfx::CreateVectorIcon(
569           vector_icons::kCloseIcon, gfx::kFaviconSize, SK_ColorGRAY)));
570   menu_model_->AddSeparator(ui::MenuSeparatorType::NORMAL_SEPARATOR);
571   menu_model_->AddItemWithIcon(
572       WEBUI_TAB_COUNTER_CXMENU_NEW_TAB,
573       l10n_util::GetStringUTF16(IDS_WEBUI_TAB_STRIP_TAB_COUNTER_CXMENU_NEW_TAB),
574       ui::ImageModel::FromImageSkia(
575           gfx::CreateVectorIcon(kAddIcon, gfx::kFaviconSize, SK_ColorGRAY)));
576   menu_runner_ = std::make_unique<views::MenuRunner>(
577       menu_model_.get(), views::MenuRunner::HAS_MNEMONICS |
578                              views::MenuRunner::CONTEXT_MENU |
579                              views::MenuRunner::FIXED_ANCHOR);
580 
581   tab_strip_model_->AddObserver(this);
582   const int tab_count = tab_strip_model_->count();
583   UpdateTooltip(tab_count);
584   appearing_label_->SetText(GetTabCounterLabelText(tab_count));
585 }
586 
AddedToWidget()587 void WebUITabCounterButton::AddedToWidget() {
588   interaction_tracker_ = std::make_unique<InteractionTracker>(GetWidget());
589   link_opened_from_gesture_subscription_ =
590       browser_view_->AddOnLinkOpeningFromGestureCallback(
591           base::BindRepeating(&WebUITabCounterButton::MaybeStartFlyingLink,
592                               base::Unretained(this)));
593 }
594 
AfterPropertyChange(const void * key,int64_t old_value)595 void WebUITabCounterButton::AfterPropertyChange(const void* key,
596                                                 int64_t old_value) {
597   if (key != kHasInProductHelpPromoKey)
598     return;
599   UpdateColors();
600 }
601 
AddLayerBeneathView(ui::Layer * new_layer)602 void WebUITabCounterButton::AddLayerBeneathView(ui::Layer* new_layer) {
603   ink_drop_container_->AddLayerBeneathView(new_layer);
604 }
605 
RemoveLayerBeneathView(ui::Layer * old_layer)606 void WebUITabCounterButton::RemoveLayerBeneathView(ui::Layer* old_layer) {
607   ink_drop_container_->RemoveLayerBeneathView(old_layer);
608 }
609 
OnThemeChanged()610 void WebUITabCounterButton::OnThemeChanged() {
611   views::Button::OnThemeChanged();
612   UpdateColors();
613   ConfigureInkDropForToolbar(this);
614 }
615 
Layout()616 void WebUITabCounterButton::Layout() {
617   const gfx::Rect view_bounds = GetLocalBounds();
618 
619   // Position views from the outside in (beacuse it's easier).
620   // Start with the throbber.
621   const int throbber_height = GetLayoutConstant(LOCATION_BAR_HEIGHT);
622   gfx::Rect throbber_rect = view_bounds;
623   throbber_rect.ClampToCenteredSize(
624       gfx::Size(throbber_height, throbber_height));
625   throbber_->SetBoundsRect(throbber_rect);
626 
627   // Next is the rounded rect border around the counter.
628   constexpr gfx::Size kDesiredBorderSize(22, 22);
629   gfx::Rect border_bounds = view_bounds;
630   border_bounds.ClampToCenteredSize(kDesiredBorderSize);
631   border_view_->SetBoundsRect(border_bounds);
632 
633   // Finally is the numbers themselves, which nest inside the label view.
634   appearing_label_->SetBoundsRect(gfx::Rect(kDesiredBorderSize));
635   disappearing_label_->SetBoundsRect(
636       gfx::Rect(gfx::Point(0, -kOffscreenLabelDistance), kDesiredBorderSize));
637 
638   // Adjust label positions for animation.
639   animator_->LayoutIfAnimating();
640 }
641 
MaybeStartFlyingLink(WindowOpenDisposition disposition)642 void WebUITabCounterButton::MaybeStartFlyingLink(
643     WindowOpenDisposition disposition) {
644   if (disposition == WindowOpenDisposition::NEW_BACKGROUND_TAB &&
645       interaction_tracker_ &&
646       interaction_tracker_->last_interaction_location().has_value())
647     animator_->StartFlyingLinkFrom(
648         interaction_tracker_->last_interaction_location().value());
649 }
650 
OnTabStripModelChanged(TabStripModel * tab_strip_model,const TabStripModelChange & change,const TabStripSelectionChange & selection)651 void WebUITabCounterButton::OnTabStripModelChanged(
652     TabStripModel* tab_strip_model,
653     const TabStripModelChange& change,
654     const TabStripSelectionChange& selection) {
655   const int num_tabs = tab_strip_model->count();
656   UpdateTooltip(num_tabs);
657   animator_->Animate(num_tabs,
658                      ShouldChangeStartThrobber(tab_strip_model, change));
659 }
660 
ShowContextMenuForViewImpl(views::View * source,const gfx::Point & point,ui::MenuSourceType source_type)661 void WebUITabCounterButton::ShowContextMenuForViewImpl(
662     views::View* source,
663     const gfx::Point& point,
664     ui::MenuSourceType source_type) {
665   menu_runner_->RunMenuAt(GetWidget(), nullptr,
666                           border_view_->GetBoundsInScreen(),
667                           views::MenuAnchorPosition::kTopRight, source_type);
668 }
669 
ExecuteCommand(int command_id,int event_flags)670 void WebUITabCounterButton::ExecuteCommand(int command_id, int event_flags) {
671   switch (command_id) {
672     case WEBUI_TAB_COUNTER_CXMENU_CLOSE_TAB: {
673       tab_strip_model_->CloseWebContentsAt(
674           tab_strip_model_->active_index(),
675           TabStripModel::CLOSE_USER_GESTURE |
676               TabStripModel::CLOSE_CREATE_HISTORICAL_TAB);
677       break;
678     }
679     case WEBUI_TAB_COUNTER_CXMENU_NEW_TAB:
680       tab_strip_model_->delegate()->AddTabAt(GURL(), -1, true);
681       break;
682     default:
683       NOTREACHED();
684   }
685 }
686 
687 }  // namespace
688 
CreateWebUITabCounterButton(views::Button::PressedCallback pressed_callback,BrowserView * browser_view)689 std::unique_ptr<views::View> CreateWebUITabCounterButton(
690     views::Button::PressedCallback pressed_callback,
691     BrowserView* browser_view) {
692   auto tab_counter = std::make_unique<WebUITabCounterButton>(
693       std::move(pressed_callback), browser_view);
694 
695   tab_counter->Init();
696 
697   return tab_counter;
698 }
699