chromium/ash/shelf/shelf_navigation_widget.cc

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/shelf/shelf_navigation_widget.h"

#include "ash/focus_cycler.h"
#include "ash/public/cpp/metrics_util.h"
#include "ash/public/cpp/shelf_config.h"
#include "ash/shelf/back_button.h"
#include "ash/shelf/home_button.h"
#include "ash/shelf/shelf.h"
#include "ash/shelf/shelf_focus_cycler.h"
#include "ash/shelf/shelf_layout_manager.h"
#include "ash/shelf/shelf_layout_manager_observer.h"
#include "ash/shelf/shelf_view.h"
#include "ash/shelf/shelf_widget.h"
#include "ash/shell.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/system/status_area_widget.h"
#include "base/functional/bind.h"
#include "base/i18n/rtl.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_macros.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/compositor/animation_throughput_reporter.h"
#include "ui/compositor/compositor.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animation_observer.h"
#include "ui/compositor/layer_delegate.h"
#include "ui/compositor/paint_recorder.h"
#include "ui/compositor/scoped_animation_duration_scale_mode.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/compositor/throughput_tracker.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/gfx/geometry/transform_util.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/bounds_animator.h"
#include "ui/views/view.h"
#include "ui/wm/core/coordinate_conversion.h"

namespace ash {
namespace {

// The duration of the back button opacity animation.
constexpr base::TimeDelta kButtonOpacityAnimationDuration =
    base::Milliseconds(50);

// Returns the bounds for the first button shown in this view (the back
// button in tablet mode, the home button otherwise).
// `preferred_size` is the button's preferred size.
gfx::Rect GetFirstButtonBounds(bool is_shelf_horizontal,
                               const gfx::Size& preferred_size) {
  // ShelfNavigationWidget is larger than the buttons in order to enable child
  // views to capture events nearby.
  return gfx::Rect(
      ShelfConfig::Get()->control_button_edge_spacing(is_shelf_horizontal),
      ShelfConfig::Get()->control_button_edge_spacing(!is_shelf_horizontal),
      preferred_size.width(), preferred_size.height());
}

// Returns the bounds for the second button shown in this view (which is
// always the home button and only in tablet mode, which implies a horizontal
// shelf).
gfx::Rect GetSecondButtonBounds() {
  // Second button only shows for horizontal shelf.
  return gfx::Rect(ShelfConfig::Get()->control_button_edge_spacing(
                       true /* is_primary_axis_edge */) +
                       ShelfConfig::Get()->control_size() +
                       ShelfConfig::Get()->button_spacing(),
                   ShelfConfig::Get()->control_button_edge_spacing(
                       false /* is_primary_axis_edge */),
                   ShelfConfig::Get()->control_size(),
                   ShelfConfig::Get()->control_size());
}

bool IsBackButtonShown(bool horizontal_alignment) {
  // Back button should only be shown in horizontal shelf.
  // TODO(https://crbug.com/1102648): Horizontal shelf should be implied by
  // tablet mode, but this may not be the case during tablet mode transition as
  // shelf layout may get updated before the correct shelf alignment is set.
  // Remove this when the linked bug is resolved.
  if (!horizontal_alignment)
    return false;

  // TODO(https://crbug.com/1058205): Test this behavior.
  if (ShelfConfig::Get()->is_virtual_keyboard_shown())
    return true;

  if (!ShelfConfig::Get()->shelf_controls_shown())
    return false;

  return Shell::Get()->IsInTabletMode() && ShelfConfig::Get()->is_in_app();
}

bool IsHomeButtonShown() {
  return ShelfConfig::Get()->shelf_controls_shown();
}

// An implicit animation observer that hides a view once the view's opacity
// animation finishes.
// It deletes itself when the animation is done.
class AnimationObserverToHideView : public ui::ImplicitAnimationObserver {
 public:
  explicit AnimationObserverToHideView(views::View* view) : view_(view) {}
  ~AnimationObserverToHideView() override = default;

  // ui::ImplicitAnimationObserver:
  void OnImplicitAnimationsCompleted() override {
    if (view_->layer()->GetTargetOpacity() == 0.0f)
      view_->SetVisible(false);
    delete this;
  }

 private:
  const raw_ptr<views::View, LeakedDanglingUntriaged> view_;
};

// Tracks the animation smoothness of a view's bounds animation using
// ui::ThroughputTracker.
class BoundsAnimationReporter : public gfx::AnimationDelegate {
 public:
  BoundsAnimationReporter(views::View* view,
                          metrics_util::ReportCallback report_callback)
      : tracker_(
            view->GetWidget()->GetCompositor()->RequestNewThroughputTracker()) {
    tracker_.Start(std::move(report_callback));
  }
  BoundsAnimationReporter(const BoundsAnimationReporter& other) = delete;
  BoundsAnimationReporter& operator=(const BoundsAnimationReporter& other) =
      delete;
  ~BoundsAnimationReporter() override = default;

  // gfx::AnimationDelegate:
  void AnimationEnded(const gfx::Animation* animation) override {
    tracker_.Stop();
  }
  void AnimationCanceled(const gfx::Animation* animation) override {
    tracker_.Cancel();
  }

 private:
  ui::ThroughputTracker tracker_;
};

}  // namespace

// An animation metrics reporter for the shelf navigation buttons.
class ASH_EXPORT NavigationButtonAnimationMetricsReporter {
 public:
  // The different kinds of navigation buttons.
  enum class NavigationButtonType {
    // The Navigation Widget's back button.
    kBackButton,
    // The Navigation Widget's home button.
    kHomeButton
  };
  explicit NavigationButtonAnimationMetricsReporter(
      NavigationButtonType navigation_button_type)
      : navigation_button_type_(navigation_button_type) {}

  ~NavigationButtonAnimationMetricsReporter() = default;

  NavigationButtonAnimationMetricsReporter(
      const NavigationButtonAnimationMetricsReporter&) = delete;
  NavigationButtonAnimationMetricsReporter& operator=(
      const NavigationButtonAnimationMetricsReporter&) = delete;

  void ReportSmoothness(HotseatState target_hotseat_state, int smoothness) {
    switch (target_hotseat_state) {
      case HotseatState::kShownClamshell:
      case HotseatState::kShownHomeLauncher:
        switch (navigation_button_type_) {
          case NavigationButtonType::kBackButton:
            UMA_HISTOGRAM_PERCENTAGE(
                "Ash.NavigationWidget.BackButton.AnimationSmoothness."
                "TransitionToShownHotseat",
                smoothness);
            break;
          case NavigationButtonType::kHomeButton:
            UMA_HISTOGRAM_PERCENTAGE(
                "Ash.NavigationWidget.HomeButton.AnimationSmoothness."
                "TransitionToShownHotseat",
                smoothness);
            break;
          default:
            NOTREACHED();
        }
        break;
      case HotseatState::kExtended:
        switch (navigation_button_type_) {
          case NavigationButtonType::kBackButton:
            UMA_HISTOGRAM_PERCENTAGE(
                "Ash.NavigationWidget.BackButton.AnimationSmoothness."
                "TransitionToExtendedHotseat",
                smoothness);
            break;
          case NavigationButtonType::kHomeButton:
            UMA_HISTOGRAM_PERCENTAGE(
                "Ash.NavigationWidget.HomeButton.AnimationSmoothness."
                "TransitionToExtendedHotseat",
                smoothness);
            break;
          default:
            NOTREACHED();
        }
        break;
      case HotseatState::kHidden:
        switch (navigation_button_type_) {
          case NavigationButtonType::kBackButton:
            UMA_HISTOGRAM_PERCENTAGE(
                "Ash.NavigationWidget.BackButton.AnimationSmoothness."
                "TransitionToHiddenHotseat",
                smoothness);
            break;
          case NavigationButtonType::kHomeButton:
            UMA_HISTOGRAM_PERCENTAGE(
                "Ash.NavigationWidget.HomeButton.AnimationSmoothness."
                "TransitionToHiddenHotseat",
                smoothness);
            break;
          default:
            NOTREACHED();
        }
        break;
      default:
        NOTREACHED();
    }
  }

  metrics_util::ReportCallback GetReportCallback(
      HotseatState target_hotseat_state) {
    DCHECK_NE(target_hotseat_state, HotseatState::kNone);
    return metrics_util::ForSmoothnessV3(base::BindRepeating(
        &NavigationButtonAnimationMetricsReporter::ReportSmoothness,
        weak_ptr_factory_.GetWeakPtr(), target_hotseat_state));
  }

 private:
  // The type of navigation button that is animated.
  const NavigationButtonType navigation_button_type_;

  base::WeakPtrFactory<NavigationButtonAnimationMetricsReporter>
      weak_ptr_factory_{this};
};

class ShelfNavigationWidget::Delegate : public views::AccessiblePaneView,
                                        public views::WidgetDelegate {
 public:
  Delegate(Shelf* shelf, ShelfView* shelf_view);

  Delegate(const Delegate&) = delete;
  Delegate& operator=(const Delegate&) = delete;

  ~Delegate() override;

  // views::View:
  FocusTraversable* GetPaneFocusTraversable() override;
  void GetAccessibleNodeData(ui::AXNodeData* node_data) override;

  // views::AccessiblePaneView:
  View* GetDefaultFocusableChild() override;

  // views::WidgetDelegate:
  bool CanActivate() const override;
  views::Widget* GetWidget() override { return View::GetWidget(); }
  const views::Widget* GetWidget() const override { return View::GetWidget(); }

  BackButton* back_button() const { return back_button_; }
  HomeButton* home_button() const { return home_button_; }

  void set_default_last_focusable_child(bool default_last_focusable_child) {
    default_last_focusable_child_ = default_last_focusable_child;
  }

 private:
  void RefreshAccessibilityWidgetNextPreviousFocus(ShelfWidget* shelf);

  raw_ptr<BackButton> back_button_ = nullptr;
  raw_ptr<HomeButton> home_button_ = nullptr;
  // When true, the default focus of the navigation widget is the last
  // focusable child.
  bool default_last_focusable_child_ = false;

  raw_ptr<Shelf> shelf_ = nullptr;
};

ShelfNavigationWidget::Delegate::Delegate(Shelf* shelf, ShelfView* shelf_view)
    : shelf_(shelf) {
  SetOwnedByWidget(true);

  set_allow_deactivate_on_esc(true);

  const int control_size = ShelfConfig::Get()->control_size();
  std::unique_ptr<BackButton> back_button_ptr =
      std::make_unique<BackButton>(shelf);
  back_button_ = AddChildView(std::move(back_button_ptr));
  back_button_->SetSize(gfx::Size(control_size, control_size));

  std::unique_ptr<HomeButton> home_button_ptr =
      std::make_unique<HomeButton>(shelf);
  home_button_ = AddChildView(std::move(home_button_ptr));
  home_button_->set_context_menu_controller(shelf_view);
  home_button_->SetSize(gfx::Size(control_size, control_size));

  // Ensure widgets are represented in accessibility.
  if (shelf->hotseat_widget()) {
    shelf->hotseat_widget()->GetRootView()->NotifyAccessibilityEvent(
        ax::mojom::Event::kChildrenChanged, true);
  }

  if (shelf->GetStatusAreaWidget()) {
    shelf->GetStatusAreaWidget()->GetRootView()->NotifyAccessibilityEvent(
        ax::mojom::Event::kChildrenChanged, true);
  }

  GetViewAccessibility().SetRole(ax::mojom::Role::kToolbar);
  GetViewAccessibility().SetName(
      l10n_util::GetStringUTF8(IDS_ASH_SHELF_ACCESSIBLE_NAME));
  RefreshAccessibilityWidgetNextPreviousFocus(shelf->shelf_widget());
}

ShelfNavigationWidget::Delegate::~Delegate() = default;

bool ShelfNavigationWidget::Delegate::CanActivate() const {
  // We don't want mouse clicks to activate us, but we need to allow
  // activation when the user is using the keyboard (FocusCycler).
  return Shell::Get()->focus_cycler()->widget_activating() == GetWidget();
}

views::FocusTraversable*
ShelfNavigationWidget::Delegate::GetPaneFocusTraversable() {
  return this;
}

void ShelfNavigationWidget::Delegate::GetAccessibleNodeData(
    ui::AXNodeData* node_data) {
  RefreshAccessibilityWidgetNextPreviousFocus(
      Shelf::ForWindow(GetWidget()->GetNativeWindow())->shelf_widget());
}

views::View* ShelfNavigationWidget::Delegate::GetDefaultFocusableChild() {
  return default_last_focusable_child_ ? GetLastFocusableChild()
                                       : GetFirstFocusableChild();
}

void ShelfNavigationWidget::Delegate::
    RefreshAccessibilityWidgetNextPreviousFocus(ShelfWidget* shelf) {
  GetViewAccessibility().SetNextFocus(shelf->hotseat_widget());
  GetViewAccessibility().SetPreviousFocus(shelf->status_area_widget());
}

ShelfNavigationWidget::TestApi::TestApi(ShelfNavigationWidget* widget)
    : navigation_widget_(widget) {}

ShelfNavigationWidget::TestApi::~TestApi() = default;

bool ShelfNavigationWidget::TestApi::IsHomeButtonVisible() const {
  const HomeButton* button = navigation_widget_->delegate_->home_button();
  const float opacity = button->layer()->GetTargetOpacity();
  DCHECK(opacity == 0.0f || opacity == 1.0f)
      << "Unexpected opacity " << opacity;
  return opacity > 0.0f && button->GetVisible();
}

bool ShelfNavigationWidget::TestApi::IsBackButtonVisible() const {
  const BackButton* button = navigation_widget_->delegate_->back_button();
  const float opacity = button->layer()->GetTargetOpacity();
  DCHECK(opacity == 0.0f || opacity == 1.0f)
      << "Unexpected opacity " << opacity;
  return opacity > 0.0f && button->GetVisible();
}

views::BoundsAnimator* ShelfNavigationWidget::TestApi::GetBoundsAnimator() {
  return navigation_widget_->bounds_animator_.get();
}

views::View* ShelfNavigationWidget::TestApi::GetWidgetDelegateView() {
  return static_cast<Delegate*>(navigation_widget_->widget_delegate());
}

ShelfNavigationWidget::ShelfNavigationWidget(Shelf* shelf,
                                             ShelfView* shelf_view)
    : shelf_(shelf),
      delegate_(new ShelfNavigationWidget::Delegate(shelf, shelf_view)),
      bounds_animator_(
          std::make_unique<views::BoundsAnimator>(delegate_,
                                                  /*use_transforms=*/true)),
      back_button_metrics_reporter_(
          std::make_unique<NavigationButtonAnimationMetricsReporter>(
              NavigationButtonAnimationMetricsReporter::NavigationButtonType::
                  kBackButton)),
      home_button_metrics_reporter_(
          std::make_unique<NavigationButtonAnimationMetricsReporter>(
              NavigationButtonAnimationMetricsReporter::NavigationButtonType::
                  kHomeButton)) {
  DCHECK(shelf_);
}

ShelfNavigationWidget::~ShelfNavigationWidget() {
  // Cancel animations now so the BoundsAnimator doesn't outlive the metrics
  // reporter associated to it.
  bounds_animator_->Cancel();
}

void ShelfNavigationWidget::Initialize(aura::Window* container) {
  DCHECK(container);
  views::Widget::InitParams params(
      views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET,
      views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
  params.name = "ShelfNavigationWidget";
  params.delegate = delegate_.get();
  params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
  params.parent = container;
  Init(std::move(params));
  set_focus_on_creation(false);
  delegate_->SetEnableArrowKeyTraversal(true);
  SetContentsView(delegate_);
  SetSize(CalculateIdealSize(/*only_visible_area=*/false));
  UpdateLayout(/*animate=*/false);
}

void ShelfNavigationWidget::OnMouseEvent(ui::MouseEvent* event) {
  if (event->IsMouseWheelEvent()) {
    ui::MouseWheelEvent* mouse_wheel_event = event->AsMouseWheelEvent();
    shelf_->ProcessMouseWheelEvent(mouse_wheel_event);
    return;
  }

  views::Widget::OnMouseEvent(event);
}

void ShelfNavigationWidget::OnScrollEvent(ui::ScrollEvent* event) {
  shelf_->ProcessScrollEvent(event);
  if (!event->handled())
    views::Widget::OnScrollEvent(event);
}

bool ShelfNavigationWidget::OnNativeWidgetActivationChanged(bool active) {
  if (!Widget::OnNativeWidgetActivationChanged(active))
    return false;
  if (active)
    delegate_->SetPaneFocusAndFocusDefault();
  return true;
}

void ShelfNavigationWidget::OnGestureEvent(ui::GestureEvent* event) {
  // Shelf::ProcessGestureEvent expects an event whose location is in screen
  // coordinates - create a copy of the event with the location in screen
  // coordinate system.
  ui::GestureEvent copy_event(*event);
  gfx::Point location_in_screen(copy_event.location());
  wm::ConvertPointToScreen(GetNativeWindow(), &location_in_screen);
  copy_event.set_location(location_in_screen);

  if (shelf_->ProcessGestureEvent(copy_event)) {
    event->StopPropagation();
    return;
  }
  views::Widget::OnGestureEvent(event);
}

BackButton* ShelfNavigationWidget::GetBackButton() const {
  return IsBackButtonShown(shelf_->IsHorizontalAlignment())
             ? delegate_->back_button()
             : nullptr;
}

HomeButton* ShelfNavigationWidget::GetHomeButton() const {
  return IsHomeButtonShown() ? delegate_->home_button() : nullptr;
}

void ShelfNavigationWidget::SetDefaultLastFocusableChild(
    bool default_last_focusable_child) {
  delegate_->set_default_last_focusable_child(default_last_focusable_child);
}

void ShelfNavigationWidget::CalculateTargetBounds() {
  const gfx::Point shelf_origin =
      shelf_->shelf_widget()->GetTargetBounds().origin();

  gfx::Point nav_origin = gfx::Point(shelf_origin.x(), shelf_origin.y());
  gfx::Size nav_size = CalculateIdealSize(/*only_visible_area=*/false);

  if (shelf_->IsHorizontalAlignment() && base::i18n::IsRTL()) {
    nav_origin.set_x(shelf_origin.x() +
                     shelf_->shelf_widget()->GetTargetBounds().size().width() -
                     nav_size.width());
  }
  target_bounds_ = gfx::Rect(nav_origin, nav_size);
  clip_rect_after_rtl_ = CalculateClipRectAfterRTL();
}

gfx::Rect ShelfNavigationWidget::GetTargetBounds() const {
  return target_bounds_;
}

void ShelfNavigationWidget::UpdateLayout(bool animate) {
  const bool back_button_shown =
      IsBackButtonShown(shelf_->IsHorizontalAlignment());
  const bool home_button_shown = IsHomeButtonShown();

  const ShelfLayoutManager* layout_manager = shelf_->shelf_layout_manager();
  // Having a window which is visible but does not have an opacity is an
  // illegal state. Also, never show this widget outside of an active session.
  if (layout_manager->GetOpacity() &&
      layout_manager->is_active_session_state()) {
    ShowInactive();
  } else {
    Hide();
  }

  // If the widget is currently active, and all the buttons will be hidden,
  // focus out to the status area (the widget's focus manager does not properly
  // handle the case where the widget does not have another view to focus - it
  // would clear the focus, and hit a DCHECK trying to cycle focus within the
  // widget).
  if (IsActive() && !back_button_shown && !home_button_shown) {
    Shelf::ForWindow(GetNativeWindow())
        ->shelf_focus_cycler()
        ->FocusOut(true /*reverse*/, SourceView::kShelfNavigationView);
  }

  // Use the same duration for all parts of the upcoming animation.
  const base::TimeDelta animation_duration =
      animate ? ShelfConfig::Get()->shelf_animation_duration()
              : base::Milliseconds(0);

  const HotseatState target_hotseat_state =
      layout_manager->CalculateHotseatState(layout_manager->visibility_state(),
                                            layout_manager->auto_hide_state());

  const bool update_opacity = !animate || GetLayer()->GetTargetOpacity() !=
                                              layout_manager->GetOpacity();
  const bool update_bounds =
      !animate || GetLayer()->GetTargetBounds() != target_bounds_;

  if (update_opacity || update_bounds) {
    ui::ScopedLayerAnimationSettings nav_animation_setter(
        GetNativeView()->layer()->GetAnimator());
    nav_animation_setter.SetTransitionDuration(animation_duration);
    nav_animation_setter.SetTweenType(gfx::Tween::EASE_OUT);
    nav_animation_setter.SetPreemptionStrategy(
        ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);

    std::optional<ui::AnimationThroughputReporter> reporter;
    if (animate) {
      reporter.emplace(nav_animation_setter.GetAnimator(),
                       shelf_->GetNavigationWidgetAnimationReportCallback(
                           target_hotseat_state));
    }
    if (update_opacity)
      GetLayer()->SetOpacity(layout_manager->GetOpacity());
    if (update_bounds) {
      SetBounds(target_bounds_);
    }
  }

  if (update_bounds) {
    GetLayer()->SetClipRect(clip_rect_after_rtl_);
  }

  views::View* const back_button = delegate_->back_button();
  UpdateButtonVisibility(back_button, back_button_shown, animate,
                         back_button_metrics_reporter_.get(),
                         target_hotseat_state);

  views::View* const home_button = delegate_->home_button();
  UpdateButtonVisibility(home_button, home_button_shown, animate,
                         home_button_metrics_reporter_.get(),
                         target_hotseat_state);

  if (back_button_shown) {
    // TODO(https://crbug.com/1058205): Test this behavior.
    gfx::Transform rotation;
    // If the IME virtual keyboard is visible, rotate the back button downwards,
    // this indicates it can be used to close the keyboard.
    if (ShelfConfig::Get()->is_virtual_keyboard_shown())
      rotation.Rotate(270.0);

    delegate_->back_button()->layer()->SetTransform(TransformAboutPivot(
        delegate_->back_button()->GetCenterPoint(), rotation));
  }

  gfx::Rect home_button_bounds =
      back_button_shown ? GetSecondButtonBounds()
                        : GetFirstButtonBounds(shelf_->IsHorizontalAlignment(),
                                               home_button->GetPreferredSize());

  if (animate) {
    if (bounds_animator_->GetTargetBounds(home_button) != home_button_bounds) {
      bounds_animator_->SetAnimationDuration(
          ui::ScopedAnimationDurationScaleMode::duration_multiplier() *
          animation_duration);
      bounds_animator_->AnimateViewTo(
          home_button, home_button_bounds,
          std::make_unique<BoundsAnimationReporter>(
              home_button, home_button_metrics_reporter_->GetReportCallback(
                               target_hotseat_state)));
    }
  } else {
    bounds_animator_->Cancel();
    home_button->SetBoundsRect(home_button_bounds);
  }

  back_button->SetBoundsRect(GetFirstButtonBounds(
      shelf_->IsHorizontalAlignment(), back_button->GetPreferredSize()));
}

void ShelfNavigationWidget::UpdateTargetBoundsForGesture(int shelf_position) {
  if (shelf_->IsHorizontalAlignment())
    target_bounds_.set_y(shelf_position);
  else
    target_bounds_.set_x(shelf_position);
}

gfx::Rect ShelfNavigationWidget::GetVisibleBounds() const {
  return gfx::Rect(target_bounds_.origin(), clip_rect_after_rtl_.size());
}

void ShelfNavigationWidget::PrepareForGettingFocus(bool last_element) {
  SetDefaultLastFocusableChild(last_element);

  // The native view of the navigation widget is not activatable when its target
  // visibility is false. So show the widget before setting focus.

  // Layer opacity should be set first. Because it is not allowed that a window
  // is visible but the layers alpha is fully transparent.
  ui::Layer* layer = GetLayer();
  if (layer->GetTargetOpacity() != 1.f)
    GetLayer()->SetOpacity(1.f);
  if (!IsVisible())
    ShowInactive();
}

void ShelfNavigationWidget::HandleLocaleChange() {
  delegate_->home_button()->HandleLocaleChange();
  delegate_->back_button()->HandleLocaleChange();
}

void ShelfNavigationWidget::UpdateButtonVisibility(
    views::View* button,
    bool visible,
    bool animate,
    NavigationButtonAnimationMetricsReporter* metrics_reporter,
    HotseatState target_hotseat_state) {
  if (animate && button->layer()->GetTargetOpacity() == visible)
    return;

  // Update visibility immediately only if making the button visible. When
  // hiding the button, the visibility will be updated when the animations
  // complete (by AnimationObserverToHideView).
  if (visible)
    button->SetVisible(true);
  button->SetFocusBehavior(visible ? views::View::FocusBehavior::ALWAYS
                                   : views::View::FocusBehavior::NEVER);

  ui::ScopedLayerAnimationSettings opacity_settings(
      button->layer()->GetAnimator());
  opacity_settings.SetTransitionDuration(
      animate ? kButtonOpacityAnimationDuration : base::TimeDelta());
  opacity_settings.SetPreemptionStrategy(
      ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);

  std::optional<ui::AnimationThroughputReporter> reporter;
  if (animate) {
    reporter.emplace(opacity_settings.GetAnimator(),
                     metrics_reporter->GetReportCallback(target_hotseat_state));
  }

  if (!visible)
    opacity_settings.AddObserver(new AnimationObserverToHideView(button));

  button->layer()->SetOpacity(visible ? 1.0f : 0.0f);
}

gfx::Rect ShelfNavigationWidget::CalculateClipRectAfterRTL() const {
  gfx::Rect clip_bounds;
  if (Shell::Get()->IsInTabletMode()) {
    clip_bounds = gfx::Rect(CalculateIdealSize(/*only_visible_area=*/true));
  } else {
    clip_bounds = gfx::Rect(target_bounds_.size());
  }

  // Bounds will be used to set a layer clip rect, and thus need to be modified
  // for RTL - avoid using `GetMirroredRect()` method, as it would use the
  // current widget/root view bounds instead of target bounds.
  if (base::i18n::IsRTL()) {
    clip_bounds.set_x(target_bounds_.width() - clip_bounds.right());
  }
  return clip_bounds;
}

gfx::Size ShelfNavigationWidget::CalculateIdealSize(
    bool only_visible_area) const {
  const bool home_button_shown = IsHomeButtonShown();
  const bool back_button_shown =
      IsBackButtonShown(shelf_->IsHorizontalAlignment());
  if (!home_button_shown && !back_button_shown)
    return gfx::Size();

  int controls_space = 0;
  const int control_size = ShelfConfig::Get()->control_size();

  if (Shell::Get()->IsInTabletMode() && !only_visible_area) {
    // There are home button and back button. So the maximum is 2.
    controls_space = control_size * 2 + ShelfConfig::Get()->button_spacing();
  } else {
    // Use CalculatePreferredSize here to take the launcher nudge label or quick
    // app button into consideration.
    controls_space +=
        home_button_shown
            ? (shelf_->IsHorizontalAlignment()
                   ? GetHomeButton()->CalculatePreferredSize({}).width()
                   : GetHomeButton()->CalculatePreferredSize({}).height())
            : 0;
    controls_space += back_button_shown ? control_size : 0;
    controls_space +=
        (CalculateButtonCount() - 1) * ShelfConfig::Get()->button_spacing();
  }

  const int major_axis_spacing =
      2 * ShelfConfig::Get()->control_button_edge_spacing(
              shelf_->IsHorizontalAlignment());

  const int major_axis_length = controls_space + major_axis_spacing;

  // Calculate |minor_axis_spacing|.
  int minor_axis_spacing;
  if (only_visible_area) {
    minor_axis_spacing = 2 * ShelfConfig::Get()->control_button_edge_spacing(
                                 !shelf_->IsHorizontalAlignment());
  } else {
    // When calculating the minor axis length of the navigation widget, use
    // the larger edge spacing between the home launcher state and the in-app
    // state. It ensures that the widget' size is constant during the transition
    // between these two states.

    DCHECK_GT(ShelfConfig::Get()->system_shelf_size(),
              ShelfConfig::Get()->in_app_shelf_size());
    minor_axis_spacing = ShelfConfig::Get()->system_shelf_size() - control_size;
  }

  const int minor_axis_length = control_size + minor_axis_spacing;

  return shelf_->IsHorizontalAlignment()
             ? gfx::Size(major_axis_length, minor_axis_length)
             : gfx::Size(minor_axis_length, major_axis_length);
}

int ShelfNavigationWidget::CalculateButtonCount() const {
  return (IsBackButtonShown(shelf_->IsHorizontalAlignment()) ? 1 : 0) +
         (IsHomeButtonShown() ? 1 : 0);
}

}  // namespace ash