CrOS Shelf: Change icon centering strategy

The main change here is that icons are first centered on the whole
screen, then, when space becomes tighter, they are centered over the
available area on the shelf.

A few changes to make this work properly:

* Fix an issue in |GetDimensionOfCenteredShelfItems| which was taking
  into account all items, even the ones within overflow, making the
  calculations incorrect.
* Another issue was that the code calculating ideal bounds would
  assume control buttons (app list, back, overflow) were the same size
  as app buttons (56) while they're actually a little smaller (40).
  Giving control buttons their "real" size in the layout code allows
  for the removal of some overrides at each button class level.
* Split out a small chunk from |CalculateIdealBounds| which is getting
  too long. More of that to come in future changes.
* Make the special spacing between the app list button and the first
  app icon also there on the other side for more symmetry.

Also add some extensive test coverage.

Bug: 891080, 933291
Change-Id: If4bb69aa6e182cd29c02d467039b5d839d09f493
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/1512299
Commit-Queue: Manu Cornet <manucornet@chromium.org>
Reviewed-by: Xiyuan Xia <xiyuan@chromium.org>
Cr-Commit-Position: refs/heads/master@{#642245}
diff --git a/ash/shelf/app_list_button.cc b/ash/shelf/app_list_button.cc
index 3d37e025..28af351 100644
--- a/ash/shelf/app_list_button.cc
+++ b/ash/shelf/app_list_button.cc
@@ -188,19 +188,6 @@
   return kViewClassName;
 }
 
-std::unique_ptr<views::InkDropRipple> AppListButton::CreateInkDropRipple()
-    const {
-  const int app_list_button_radius = ShelfConstants::control_border_radius();
-  gfx::Point center = GetCenterPoint();
-  gfx::Rect bounds(center.x() - app_list_button_radius,
-                   center.y() - app_list_button_radius,
-                   2 * app_list_button_radius, 2 * app_list_button_radius);
-  return std::make_unique<views::FloodFillInkDropRipple>(
-      size(), GetLocalBounds().InsetsFrom(bounds),
-      GetInkDropCenterBasedOnLastEvent(), GetInkDropBaseColor(),
-      ink_drop_visible_opacity());
-}
-
 void AppListButton::PaintButtonContents(gfx::Canvas* canvas) {
   gfx::PointF circle_center(GetCenterPoint());
 
diff --git a/ash/shelf/app_list_button.h b/ash/shelf/app_list_button.h
index c92b86c..c698120 100644
--- a/ash/shelf/app_list_button.h
+++ b/ash/shelf/app_list_button.h
@@ -50,7 +50,6 @@
 
  protected:
   // views::Button:
-  std::unique_ptr<views::InkDropRipple> CreateInkDropRipple() const override;
   void PaintButtonContents(gfx::Canvas* canvas) override;
 
  private:
diff --git a/ash/shelf/app_list_button_unittest.cc b/ash/shelf/app_list_button_unittest.cc
index fa0bbb0..f94e57c 100644
--- a/ash/shelf/app_list_button_unittest.cc
+++ b/ash/shelf/app_list_button_unittest.cc
@@ -16,6 +16,7 @@
 #include "ash/root_window_controller.h"
 #include "ash/session/session_controller.h"
 #include "ash/shelf/shelf.h"
+#include "ash/shelf/shelf_constants.h"
 #include "ash/shelf/shelf_view.h"
 #include "ash/shelf/shelf_view_test_api.h"
 #include "ash/shell.h"
@@ -153,7 +154,7 @@
 
   Shell::Get()->tablet_mode_controller()->EnableTabletModeWindowManager(false);
   test_api.RunMessageLoopUntilAnimationsDone();
-  EXPECT_EQ(0, app_list_button()->bounds().x());
+  EXPECT_EQ(ShelfConstants::button_spacing(), app_list_button()->bounds().x());
 }
 
 class VoiceInteractionAppListButtonTest : public AppListButtonTest {
diff --git a/ash/shelf/overflow_bubble_view.cc b/ash/shelf/overflow_bubble_view.cc
index d7c242f..487773d 100644
--- a/ash/shelf/overflow_bubble_view.cc
+++ b/ash/shelf/overflow_bubble_view.cc
@@ -179,6 +179,8 @@
 gfx::Rect OverflowBubbleView::GetBubbleBounds() {
   const gfx::Size content_size = GetPreferredSize();
   const gfx::Rect anchor_rect = GetAnchorRect();
+  const int distance_to_overflow_button =
+      kDistanceToMainShelf + (kShelfSize - kShelfControlSize) / 2;
   gfx::Rect monitor_rect =
       display::Screen::GetScreen()
           ->GetDisplayNearestPoint(anchor_rect.CenterPoint())
@@ -191,7 +193,7 @@
         base::i18n::IsRTL()
             ? anchor_rect.x() - kEndPadding
             : anchor_rect.right() - content_size.width() - kEndPadding,
-        anchor_rect.y() - kDistanceToMainShelf - content_size.height(),
+        anchor_rect.y() - distance_to_overflow_button - content_size.height(),
         content_size.width() + 2 * kEndPadding, content_size.height());
     if (bounds.x() < monitor_rect.x())
       bounds.Offset(monitor_rect.x() - bounds.x(), 0);
@@ -203,9 +205,10 @@
       0, anchor_rect.bottom() - content_size.height() - kEndPadding,
       content_size.width(), content_size.height() + 2 * kEndPadding);
   if (shelf_->alignment() == SHELF_ALIGNMENT_LEFT)
-    bounds.set_x(anchor_rect.right() + kDistanceToMainShelf);
+    bounds.set_x(anchor_rect.right() + distance_to_overflow_button);
   else
-    bounds.set_x(anchor_rect.x() - kDistanceToMainShelf - content_size.width());
+    bounds.set_x(anchor_rect.x() - distance_to_overflow_button -
+                 content_size.width());
   if (bounds.y() < monitor_rect.y())
     bounds.Offset(0, monitor_rect.y() - bounds.y());
   if (bounds.bottom() > monitor_rect.bottom())
diff --git a/ash/shelf/shelf_control_button.cc b/ash/shelf/shelf_control_button.cc
index 0143fb8..e5a867ff 100644
--- a/ash/shelf/shelf_control_button.cc
+++ b/ash/shelf/shelf_control_button.cc
@@ -22,21 +22,22 @@
 ShelfControlButton::ShelfControlButton(ShelfView* shelf_view)
     : ShelfButton(shelf_view), shelf_(shelf_view->shelf()) {
   set_has_ink_drop_action_on_click(true);
-  SetSize(gfx::Size(kShelfControlSize, kShelfControlSize));
 }
 
 ShelfControlButton::~ShelfControlButton() = default;
 
 gfx::Point ShelfControlButton::GetCenterPoint() const {
-  return gfx::Point(width() / 2.f, width() / 2.f);
+  return gfx::Point(width() / 2.f, height() / 2.f);
 }
 
 std::unique_ptr<views::InkDropRipple> ShelfControlButton::CreateInkDropRipple()
     const {
+  const int button_radius = ShelfConstants::control_border_radius();
+  gfx::Point center = GetCenterPoint();
+  gfx::Rect bounds(center.x() - button_radius, center.y() - button_radius,
+                   2 * button_radius, 2 * button_radius);
   return std::make_unique<views::FloodFillInkDropRipple>(
-      size(),
-      gfx::Insets(ShelfConstants::button_size() / 2 -
-                  ShelfConstants::control_border_radius()),
+      size(), GetLocalBounds().InsetsFrom(bounds),
       GetInkDropCenterBasedOnLastEvent(), GetInkDropBaseColor(),
       ink_drop_visible_opacity());
 }
@@ -51,21 +52,8 @@
   return "ash/ShelfControlButton";
 }
 
-gfx::Rect ShelfControlButton::CalculateButtonBounds() const {
-  ShelfAlignment alignment = shelf_->alignment();
-  gfx::Rect content_bounds = GetContentsBounds();
-  // Align the button to the top of a bottom-aligned shelf, to the right edge
-  // a left-aligned shelf, and to the left edge of a right-aligned shelf.
-  const int inset = (ShelfConstants::shelf_size() - kShelfControlSize) / 2;
-  const int x = alignment == SHELF_ALIGNMENT_LEFT
-                    ? content_bounds.right() - inset - kShelfControlSize
-                    : content_bounds.x() + inset;
-  return gfx::Rect(x, content_bounds.y() + inset, kShelfControlSize,
-                   kShelfControlSize);
-}
-
 void ShelfControlButton::PaintButtonContents(gfx::Canvas* canvas) {
-  PaintBackground(canvas, CalculateButtonBounds());
+  PaintBackground(canvas, GetContentsBounds());
 }
 
 void ShelfControlButton::PaintBackground(gfx::Canvas* canvas,
diff --git a/ash/shelf/shelf_control_button.h b/ash/shelf/shelf_control_button.h
index ed1055c..1b10a4e 100644
--- a/ash/shelf/shelf_control_button.h
+++ b/ash/shelf/shelf_control_button.h
@@ -37,9 +37,6 @@
   void PaintButtonContents(gfx::Canvas* canvas) override;
 
  private:
-  // Calculates the bounds of the control button based on the shelf alignment.
-  gfx::Rect CalculateButtonBounds() const;
-
   Shelf* shelf_;
 
   DISALLOW_COPY_AND_ASSIGN(ShelfControlButton);
diff --git a/ash/shelf/shelf_view.cc b/ash/shelf/shelf_view.cc
index c8b1dd6..87be8b8 100644
--- a/ash/shelf/shelf_view.cc
+++ b/ash/shelf/shelf_view.cc
@@ -89,8 +89,8 @@
 constexpr int kSeparatorSize = 20;
 constexpr int kSeparatorThickness = 1;
 
-// The margin between the app list button and the first shelf item.
-constexpr int kAppListButtonMargin = 32;
+// The margin on either side of the group of app icons.
+constexpr int kAppIconGroupMargin = 16;
 
 // White with ~20% opacity.
 constexpr SkColor kSeparatorColor = SkColorSetARGB(0x32, 0xFF, 0xFF, 0xFF);
@@ -518,8 +518,7 @@
 }
 
 gfx::Size ShelfView::CalculatePreferredSize() const {
-  gfx::Rect overflow_bounds;
-  CalculateIdealBounds(&overflow_bounds);
+  CalculateIdealBounds();
 
   int last_button_index = last_visible_index_;
   if (!is_overflow_mode() && overflow_button_ && overflow_button_->visible())
@@ -1060,12 +1059,11 @@
     return;
   }
 
-  gfx::Rect overflow_bounds;
-  CalculateIdealBounds(&overflow_bounds);
+  CalculateIdealBounds();
   views::ViewModelUtils::SetViewBoundsToIdealBounds(*view_model_);
-  overflow_button_->SetBoundsRect(overflow_bounds);
   UpdateBackButton();
   LayoutAppListAndBackButtonHighlight();
+  LayoutOverflowButton();
   UpdateVisibleShelfItemBoundsUnion();
 }
 
@@ -1083,17 +1081,24 @@
   return -1;
 }
 
-int ShelfView::GetDimensionOfCenteredShelfItems() const {
+int ShelfView::GetDimensionOfAppIcons(int max_size) const {
   int size = 0;
-  int added_items = 0;
-  for (ShelfItem item : model_->items()) {
-    if (item.type == TYPE_PINNED_APP || item.type == TYPE_APP ||
-        item.type == TYPE_BROWSER_SHORTCUT) {
-      size += ShelfConstants::button_size();
-      added_items++;
+  for (int i = kAppListButtonIndex + 1; i < view_model_->view_size(); ++i) {
+    int new_size = size;
+    new_size += ShelfConstants::button_size();
+    if (i > kAppListButtonIndex + 1) {
+      // TODO(manucornet): If one of the displayed items is the overflow
+      // button, we are overestimating the dimension a tiny bit because
+      // control buttons are smaller than app buttons. But taking this into
+      // account is a little tricky because at this stage we don't know whether
+      // we are overflowing. Fix this and add tests to check for perfect
+      // centering when the overflow button is shown.
+      new_size += ShelfConstants::button_spacing();
     }
+    if (new_size > max_size)
+      return size;
+    size = new_size;
   }
-  size += (added_items - 1) * ShelfConstants::button_spacing();
   return size;
 }
 
@@ -1138,13 +1143,17 @@
                                back_and_app_list_background_size));
 }
 
-void ShelfView::CalculateIdealBounds(gfx::Rect* overflow_bounds) const {
+void ShelfView::CalculateIdealBounds() const {
   DCHECK(model_->item_count() == view_model_->view_size());
 
   const int button_spacing = ShelfConstants::button_spacing();
-  const int button_size = ShelfConstants::button_size();
-
   const int available_size = shelf_->PrimaryAxisValue(width(), height());
+  // Size occupied by the app list button and back button plus all appropriate
+  // margins is not available for actual app icons.
+  const int available_size_for_app_icons =
+      available_size - kShelfButtonSpacing -
+      (IsTabletModeEnabled() ? 2 : 1) * kShelfControlSize -
+      2 * kAppIconGroupMargin;
   const int separator_index = GetSeparatorIndex();
   const bool virtual_keyboard_visible =
       Shell::Get()->system_tray_model()->virtual_keyboard()->visible();
@@ -1155,13 +1164,17 @@
                          !virtual_keyboard_visible);
   int app_list_button_position;
 
-  int x = 0;
-  int y = 0;
-
-  int w = shelf_->PrimaryAxisValue(button_size, width());
-  int h = shelf_->PrimaryAxisValue(height(), button_size);
+  int x = shelf_->PrimaryAxisValue(button_spacing, 0);
+  int y = shelf_->PrimaryAxisValue(0, button_spacing);
 
   for (int i = 0; i < view_model_->view_size(); ++i) {
+    // "Primary" as in "same direction as the shelf's direction". The
+    // "secondary" (orthogonal) size is always the full shelf to maximize click
+    // targets even for control buttons.
+    const int size_primary =
+        (i <= kAppListButtonIndex) ? kShelfControlSize : kShelfButtonSize;
+    const int size_secondary = kShelfButtonSize;
+
     if (i < first_visible_index_) {
       // This happens for the overflow view.
       view_model_->set_ideal_bounds(i, gfx::Rect(x, y, 0, 0));
@@ -1169,19 +1182,41 @@
     }
     if (i == kAppListButtonIndex + 1) {
       // Start centering after we've laid out the app list button.
-      // Center the shelf items on the whole shelf, including the status
-      // area widget.
-      int centered_shelf_items_size = GetDimensionOfCenteredShelfItems();
+      // Now there are two possibilities. Either all the apps fit when centered
+      // on the whole screen width, in which case we do that. Or, when space
+      // becomes a little tight (which happens especially when the status area
+      // is wider because of extra panels), we center apps according to the
+      // available space on the shelf (subtracting what's already allocated to
+      // the app list button).
+
+      int app_icons_size = GetDimensionOfAppIcons(available_size_for_app_icons);
       StatusAreaWidget* status_widget = shelf_widget_->status_area_widget();
-      int status_widget_size =
+      const int status_widget_size =
           status_widget ? shelf_->PrimaryAxisValue(
                               status_widget->GetWindowBoundsInScreen().width(),
                               status_widget->GetWindowBoundsInScreen().height())
                         : 0;
-      int padding_for_centering =
-          (available_size + status_widget_size - centered_shelf_items_size) / 2;
+      const int screen_size = available_size + status_widget_size;
+
+      int padding_for_centering = 0;
+      // An easy way to check whether the apps fit at the exact center of the
+      // screen is to imagine that we have another status widget on the
+      // other side (the status widget is always bigger than the app list
+      // button plus the back button if applicable) and see if the apps
+      // can fit in the middle.
+      if (app_icons_size + 2 * status_widget_size + 2 * kAppIconGroupMargin <
+          screen_size) {
+        padding_for_centering = (screen_size - app_icons_size) / 2;
+      } else {
+        padding_for_centering =
+            kShelfButtonSpacing +
+            (IsTabletModeEnabled() ? 2 : 1) * kShelfControlSize +
+            kAppIconGroupMargin +
+            (available_size_for_app_icons - app_icons_size) / 2;
+      }
+
       if (padding_for_centering >
-          app_list_button_position + kAppListButtonMargin) {
+          app_list_button_position + kAppIconGroupMargin) {
         // Only shift buttons to the right, never let them interfere with the
         // left-aligned system buttons.
         x = shelf_->PrimaryAxisValue(padding_for_centering, 0);
@@ -1189,30 +1224,26 @@
       }
     }
 
-    view_model_->set_ideal_bounds(i, gfx::Rect(x, y, w, h));
+    view_model_->set_ideal_bounds(
+        i,
+        gfx::Rect(x, y, shelf_->PrimaryAxisValue(size_primary, size_secondary),
+                  shelf_->PrimaryAxisValue(size_secondary, size_primary)));
+
     // If not in tablet mode do not increase |x| or |y|. Instead just let the
     // next item (app list button) cover the back button, which will have
     // opacity 0 anyways.
     if (i == kBackButtonIndex && !IsTabletModeEnabled())
       continue;
 
-    // There is no spacing between the first two elements. Do not worry about y
-    // since the back button only appears in tablet mode, which forces the shelf
-    // to be bottom aligned.
-    x = shelf_->PrimaryAxisValue(x + w + (i == 0 ? 0 : button_spacing), x);
-    y = shelf_->PrimaryAxisValue(y, y + h + button_spacing);
-
-    // In the new UI, padding between the back & app list buttons is smaller
-    // than between all other shelf items.
-    if (i == kBackButtonIndex)
-      x -= button_spacing;
+    x = shelf_->PrimaryAxisValue(x + size_primary + button_spacing, x);
+    y = shelf_->PrimaryAxisValue(y, y + size_primary + button_spacing);
 
     if (i == kAppListButtonIndex) {
       app_list_button_position = shelf_->PrimaryAxisValue(x, y);
       // A larger minimum padding after the app list button is required:
       // increment with the necessary extra amount.
-      x += shelf_->PrimaryAxisValue(kAppListButtonMargin - button_spacing, 0);
-      y += shelf_->PrimaryAxisValue(0, kAppListButtonMargin - button_spacing);
+      x += shelf_->PrimaryAxisValue(kAppIconGroupMargin - button_spacing, 0);
+      y += shelf_->PrimaryAxisValue(0, kAppIconGroupMargin - button_spacing);
     }
 
     if (i == separator_index) {
@@ -1237,11 +1268,8 @@
     const_cast<ShelfView*>(this)->UpdateAllButtonsVisibilityInOverflowMode();
     return;
   }
-
-  overflow_bounds->set_size(gfx::Size(shelf_->PrimaryAxisValue(w, width()),
-                                      shelf_->PrimaryAxisValue(height(), h)));
   last_visible_index_ =
-      IndexOfLastItemThatFitsSize(available_size - button_spacing);
+      IndexOfLastItemThatFitsSize(available_size - kAppIconGroupMargin);
   bool show_overflow = last_visible_index_ < model_->item_count() - 1;
 
   // In the main shelf, the first visible index is either the back button (in
@@ -1263,8 +1291,8 @@
     // FinalizeRipOffDrag().
     if (dragged_off_shelf_ && view_model_->view_at(i) == drag_view_)
       continue;
-    // If virtual keyboard is visible, only back button and app list button are
-    // shown.
+    // If the virtual keyboard is visible, only the back button and the app
+    // list button are shown.
     const bool is_visible_item = !virtual_keyboard_visible ||
                                  i == kBackButtonIndex ||
                                  i == kAppListButtonIndex;
@@ -1274,28 +1302,6 @@
 
   overflow_button_->SetVisible(show_overflow);
   if (show_overflow) {
-    DCHECK_NE(0, view_model_->view_size());
-    if (last_visible_index_ == -1) {
-      x = 0;
-      y = 0;
-    } else {
-      x = shelf_->PrimaryAxisValue(
-          view_model_->ideal_bounds(last_visible_index_).right(),
-          view_model_->ideal_bounds(last_visible_index_).x());
-      y = shelf_->PrimaryAxisValue(
-          view_model_->ideal_bounds(last_visible_index_).y(),
-          view_model_->ideal_bounds(last_visible_index_).bottom());
-    }
-
-    if (last_visible_index_ >= 0) {
-      // Add more space between last visible item and overflow button.
-      // Without this, two buttons look too close compared with other items.
-      x = shelf_->PrimaryAxisValue(x + button_spacing, x);
-      y = shelf_->PrimaryAxisValue(y, y + button_spacing);
-    }
-
-    overflow_bounds->set_x(x);
-    overflow_bounds->set_y(y);
     if (overflow_bubble_.get() && overflow_bubble_->IsShowing())
       UpdateOverflowRange(overflow_bubble_->bubble_view()->shelf_view());
   } else {
@@ -1304,6 +1310,31 @@
   }
 }
 
+void ShelfView::LayoutOverflowButton() const {
+  DCHECK_NE(0, view_model_->view_size());
+  int x = 0;
+  int y = 0;
+  if (last_visible_index_ != -1) {
+    const int offset = (kShelfButtonSize - kShelfControlSize) / 2;
+    x = shelf_->PrimaryAxisValue(
+        offset + view_model_->ideal_bounds(last_visible_index_).right(),
+        offset + view_model_->ideal_bounds(last_visible_index_).x());
+    y = shelf_->PrimaryAxisValue(
+        offset + view_model_->ideal_bounds(last_visible_index_).y(),
+        offset + view_model_->ideal_bounds(last_visible_index_).bottom());
+  }
+
+  if (last_visible_index_ >= 0) {
+    // Add more space between last visible item and overflow button.
+    // Without this, two buttons look too close compared with other items.
+    x = shelf_->PrimaryAxisValue(x + ShelfConstants::button_spacing(), x);
+    y = shelf_->PrimaryAxisValue(y, y + ShelfConstants::button_spacing());
+  }
+
+  overflow_button_->SetBoundsRect(
+      gfx::Rect(x, y, kShelfControlSize, kShelfControlSize));
+}
+
 int ShelfView::IndexOfLastItemThatFitsSize(int max_value) const {
   int index = model_->item_count() - 1;
   while (index >= 0 &&
@@ -1316,8 +1347,7 @@
 }
 
 void ShelfView::AnimateToIdealBounds() {
-  gfx::Rect overflow_bounds;
-  CalculateIdealBounds(&overflow_bounds);
+  CalculateIdealBounds();
   for (int i = 0; i < view_model_->view_size(); ++i) {
     View* view = view_model_->view_at(i);
     bounds_animator_->AnimateViewTo(view, view_model_->ideal_bounds(i));
@@ -1326,8 +1356,8 @@
     if (i && view->border())
       view->SetBorder(views::NullBorder());
   }
-  overflow_button_->SetBoundsRect(overflow_bounds);
   LayoutAppListAndBackButtonHighlight();
+  LayoutOverflowButton();
   UpdateVisibleShelfItemBoundsUnion();
 }
 
@@ -2020,8 +2050,7 @@
   // button before this animation completes it doesn't appear at some random
   // spot (because it was in the middle of animating from 0,0 0x0 to its
   // target).
-  gfx::Rect overflow_bounds;
-  CalculateIdealBounds(&overflow_bounds);
+  CalculateIdealBounds();
   view->SetBoundsRect(view_model_->ideal_bounds(model_index));
 
   // The first animation moves all the views to their target position. |view|
diff --git a/ash/shelf/shelf_view.h b/ash/shelf/shelf_view.h
index 8af59e6..1695af0 100644
--- a/ash/shelf/shelf_view.h
+++ b/ash/shelf/shelf_view.h
@@ -247,9 +247,10 @@
   // Returns whether |item| should belong in the pinned section of the shelf.
   bool IsItemPinned(const ShelfItem& item) const;
 
-  // Enumerates the shelf items that are centered in the new UI and returns
-  // the total size they occupy.
-  int GetDimensionOfCenteredShelfItems() const;
+  // Enumerates the shelf apps and returns the total size they occupy,
+  // accounting for all apps or, if the total size is greater than |max_size|,
+  // the size of however many app can fit without exceeding |max_size|.
+  int GetDimensionOfAppIcons(int max_size) const;
 
   // Returns the index of the item after which the separator should be shown,
   // or -1 if no separator is required.
@@ -343,7 +344,9 @@
 
   // Calculates the ideal bounds. The bounds of each button corresponding to an
   // item in the model is set in |view_model_|.
-  void CalculateIdealBounds(gfx::Rect* overflow_bounds) const;
+  void CalculateIdealBounds() const;
+
+  void LayoutOverflowButton() const;
 
   // Returns the index of the last view whose max primary axis coordinate is
   // less than |max_value|. Returns -1 if nothing fits, or there are no views.
diff --git a/ash/shelf/shelf_view_unittest.cc b/ash/shelf/shelf_view_unittest.cc
index 656b3dd..205ee78 100644
--- a/ash/shelf/shelf_view_unittest.cc
+++ b/ash/shelf/shelf_view_unittest.cc
@@ -12,6 +12,7 @@
 #include "ash/app_list/test/app_list_test_helper.h"
 #include "ash/app_list/views/app_list_view.h"
 #include "ash/focus_cycler.h"
+#include "ash/ime/ime_controller.h"
 #include "ash/public/cpp/shelf_item_delegate.h"
 #include "ash/public/cpp/shelf_model.h"
 #include "ash/public/cpp/shelf_prefs.h"
@@ -2281,6 +2282,122 @@
   EXPECT_TRUE(test_api_->CloseMenu());
 }
 
+void ExpectWithinOnePixel(int a, int b) {
+  EXPECT_TRUE(abs(a - b) <= 1) << "Values " << a << " and " << b
+                               << " should have a difference no greater than 1";
+}
+
+TEST_F(ShelfViewTest, IconCenteringTest) {
+  const display::Display display =
+      display::Screen::GetScreen()->GetPrimaryDisplay();
+  const int screen_width = display.bounds().width();
+  const int screen_center = screen_width / 2;
+
+  // Show the IME panel, to introduce for asymettry with a larger status area.
+  Shell::Get()->ime_controller()->ShowImeMenuOnShelf(true);
+
+  // At the start, we have exactly one app icon for the browser. That should
+  // be centered on the screen.
+  const ShelfAppButton* button1 = GetButtonByID(model_->items()[2].id);
+  ExpectWithinOnePixel(screen_center,
+                       button1->GetBoundsInScreen().CenterPoint().x());
+  // Also check that the distance between the icon edge and the screen edge is
+  // the same on both sides.
+  ExpectWithinOnePixel(button1->GetBoundsInScreen().x(),
+                       screen_width - button1->GetBoundsInScreen().right());
+
+  const int apps_that_can_fit_at_center_of_screen = 8;
+  std::vector<ShelfAppButton*> app_buttons;
+  // Start with just the browser app button.
+  app_buttons.push_back(GetButtonByID(model_->items()[2].id));
+  int n_buttons = 1;
+
+  // Now repeat the same process by adding apps until they can't fit at the
+  // center of the screen.
+  for (int i = 1; i < apps_that_can_fit_at_center_of_screen; ++i) {
+    // Add a new app and add its button to our list.
+    app_buttons.push_back(GetButtonByID(AddApp()));
+    n_buttons = app_buttons.size();
+    if (n_buttons % 2 == 1) {
+      // Odd number of apps. Check that the middle app is exactly at the center
+      // of the screen.
+      ExpectWithinOnePixel(
+          screen_center,
+          app_buttons[n_buttons / 2]->GetBoundsInScreen().CenterPoint().x());
+    }
+    // Also check that the first icon is at the same distance from the left
+    // screen edge as the last icon is from the right screen edge.
+    ExpectWithinOnePixel(
+        app_buttons[0]->GetBoundsInScreen().x(),
+        screen_width - app_buttons[n_buttons - 1]->GetBoundsInScreen().right());
+  }
+
+  // Add one more app. Now the block of apps should be at the center of the
+  // shelf part of the shelf widget (not including the status area) as opposed
+  // to at the center of the whole screen. But we're not overflowing yet.
+  app_buttons.push_back(GetButtonByID(AddApp()));
+  n_buttons = app_buttons.size();
+  EXPECT_FALSE(shelf_view_->GetOverflowButton()->visible());
+  // Icons at either end should also be at the same distance from the app list
+  // button on the left, and the status area on the right.
+  gfx::NativeWindow window = shelf_view_->shelf_widget()->GetNativeWindow();
+  views::View* status_area_view = RootWindowController::ForWindow(window)
+                                      ->GetStatusAreaWidget()
+                                      ->GetContentsView();
+  const int status_area_left = status_area_view->GetBoundsInScreen().x();
+  const int app_list_button_right =
+      shelf_view_->GetAppListButton()->GetBoundsInScreen().right();
+  ExpectWithinOnePixel(
+      app_buttons[0]->GetBoundsInScreen().x() - app_list_button_right,
+      status_area_left -
+          app_buttons[n_buttons - 1]->GetBoundsInScreen().right());
+
+  // Add another app. The overflow button should now appear.
+  app_buttons.push_back(GetButtonByID(AddApp()));
+  n_buttons = app_buttons.size();
+  EXPECT_TRUE(shelf_view_->GetOverflowButton()->visible());
+}
+
+TEST_F(ShelfViewTest, FirstAndLastVisibleIndex) {
+  // At the start, the only things visible on the shelf are the app list button
+  // (index 1) and the browser app button (index 2).
+  EXPECT_EQ(1, shelf_view_->first_visible_index());
+  EXPECT_EQ(2, shelf_view_->last_visible_index());
+  // By enabling tablet mode, the back button (index 0) should become visible.
+  Shell::Get()->tablet_mode_controller()->EnableTabletModeWindowManager(true);
+  EXPECT_EQ(0, shelf_view_->first_visible_index());
+  EXPECT_EQ(2, shelf_view_->last_visible_index());
+  // And things should return back to the previous state once tablet mode is off
+  // again.
+  Shell::Get()->tablet_mode_controller()->EnableTabletModeWindowManager(false);
+  EXPECT_EQ(1, shelf_view_->first_visible_index());
+  EXPECT_EQ(2, shelf_view_->last_visible_index());
+  // Now let's add some apps until the overflow button shows up, each time
+  // checking the first and last visible indices are what we expect.
+  int last_visible_index = 2;
+  int last_visible_index_before_overflow;
+  ShelfID last_added_item_id;
+  while (true) {
+    last_added_item_id = AddApp();
+    if (shelf_view_->GetOverflowButton()->visible()) {
+      last_visible_index_before_overflow = last_visible_index;
+      break;
+    }
+    last_visible_index++;
+    EXPECT_EQ(1, shelf_view_->first_visible_index());
+    EXPECT_EQ(last_visible_index, shelf_view_->last_visible_index());
+  }
+  // Now remove the last item we just added. That should get rid of the
+  // overflow button, and get back to the previous state.
+  RemoveByID(last_added_item_id);
+  EXPECT_EQ(1, shelf_view_->first_visible_index());
+  EXPECT_EQ(last_visible_index_before_overflow,
+            shelf_view_->last_visible_index());
+  // Adding another app should let the overflow button appear again.
+  AddApp();
+  EXPECT_TRUE(shelf_view_->GetOverflowButton()->visible());
+}
+
 // Test class that tests both context and application menus.
 class ShelfViewMenuTest : public ShelfViewTest,
                           public testing::WithParamInterface<bool> {