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

#include "ui/views/controls/combobox/combobox.h"

#include <algorithm>
#include <memory>
#include <utility>
#include <vector>

#include "base/check_op.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "build/build_config.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/ime/input_method.h"
#include "ui/base/menu_source_utils.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/base/mojom/menu_source_type.mojom.h"
#include "ui/base/ui_base_features.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/events/event.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/scoped_canvas.h"
#include "ui/gfx/text_utils.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/flood_fill_ink_drop_ripple.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/animation/ink_drop_impl.h"
#include "ui/views/background.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/button/button_controller.h"
#include "ui/views/controls/combobox/combobox_menu_model.h"
#include "ui/views/controls/combobox/combobox_util.h"
#include "ui/views/controls/combobox/empty_combobox_model.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/focusable_border.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/menu/menu_config.h"
#include "ui/views/controls/menu/menu_runner.h"
#include "ui/views/controls/prefix_selector.h"
#include "ui/views/layout/layout_provider.h"
#include "ui/views/mouse_constants.h"
#include "ui/views/property_effects.h"
#include "ui/views/style/platform_style.h"
#include "ui/views/style/typography.h"
#include "ui/views/style/typography_provider.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"

namespace views {

namespace {

constexpr int kBorderThickness = 1;

float GetCornerRadius() {
  return LayoutProvider::Get()->GetCornerRadiusMetric(
      ShapeContextTokens::kComboboxRadius);
}

SkColor GetTextColorForEnableState(const Combobox& combobox, bool enabled) {
  const int style = enabled ? style::STYLE_PRIMARY : style::STYLE_DISABLED;
  return combobox.GetColorProvider()->GetColor(
      TypographyProvider::Get().GetColorId(style::CONTEXT_TEXTFIELD, style));
}

// The transparent button which holds a button state but is not rendered.
class TransparentButton : public Button {
  METADATA_HEADER(TransparentButton, Button)

 public:
  explicit TransparentButton(PressedCallback callback)
      : Button(std::move(callback)) {
    SetFocusBehavior(FocusBehavior::NEVER);
    button_controller()->set_notify_action(
        ButtonController::NotifyAction::kOnPress);

    views::InstallRoundRectHighlightPathGenerator(this, gfx::Insets(),
                                                  GetCornerRadius());
    ConfigureComboboxButtonInkDrop(this);
  }
  TransparentButton(const TransparentButton&) = delete;
  TransparentButton& operator&=(const TransparentButton&) = delete;
  ~TransparentButton() override = default;

  bool OnMousePressed(const ui::MouseEvent& mouse_event) override {
#if !BUILDFLAG(IS_MAC)
    // On Mac, comboboxes do not take focus on mouse click, but on other
    // platforms they do.
    parent()->RequestFocus();
#endif
    return Button::OnMousePressed(mouse_event);
  }

  double GetAnimationValue() const {
    return hover_animation().GetCurrentValue();
  }

  void UpdateInkDrop(bool show_on_press_and_hover) {
    if (show_on_press_and_hover) {
      // We must use UseInkDropForFloodFillRipple here because
      // UseInkDropForSquareRipple hides the InkDrop when the ripple effect is
      // active instead of layering underneath it causing flashing.
      InkDrop::UseInkDropForFloodFillRipple(InkDrop::Get(this),
                                            /*highlight_on_hover=*/true);
    } else {
      InkDrop::UseInkDropForSquareRipple(InkDrop::Get(this),
                                         /*highlight_on_hover=*/false);
    }
  }
};

BEGIN_METADATA(TransparentButton)
END_METADATA

}  // namespace

////////////////////////////////////////////////////////////////////////////////
// Combobox, public:

Combobox::Combobox()
    : Combobox(std::make_unique<internal::EmptyComboboxModel>()) {}

Combobox::Combobox(std::unique_ptr<ui::ComboboxModel> model)
    : Combobox(model.get()) {
  owned_model_ = std::move(model);
}

Combobox::Combobox(ui::ComboboxModel* model) {
  SetModel(model);
#if BUILDFLAG(IS_MAC)
  SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
#else
  SetFocusBehavior(FocusBehavior::ALWAYS);
#endif

  SetBackgroundColorId(ui::kColorComboboxBackground);

  UpdateBorder();

  FocusRing::Install(this);
  views::FocusRing::Get(this)->SetOutsetFocusRingDisabled(true);

  arrow_button_ =
      AddChildView(std::make_unique<TransparentButton>(base::BindRepeating(
          &Combobox::ArrowButtonPressed, base::Unretained(this))));

  // TODO(crbug.com/40250124): This setter should be removed and the behavior
  // made default when ChromeRefresh2023 is finalized.
  SetEventHighlighting(true);
  enabled_changed_subscription_ =
      AddEnabledInViewsSubtreeChangedCallback(base::BindRepeating(
          [](Combobox* combobox) {
            combobox->SetBackgroundColorId(
                combobox->GetEnabledInViewsSubtree()
                    ? ui::kColorComboboxBackground
                    : ui::kColorComboboxBackgroundDisabled);
            combobox->UpdateBorder();
            combobox->UpdateAccessibleDefaultActionVerb();
          },
          base::Unretained(this)));

  // A layer is applied to make sure that canvas bounds are snapped to pixel
  // boundaries (for the sake of drawing the arrow).
  SetPaintToLayer();
  layer()->SetFillsBoundsOpaquely(false);

  views::InstallRoundRectHighlightPathGenerator(this, gfx::Insets(),
                                                GetCornerRadius());

  GetViewAccessibility().SetRole(ax::mojom::Role::kComboBoxSelect);

  UpdateAccessibleValue();
  UpdateExpandedCollapsedAccessibleState();
  UpdateAccessibleDefaultActionVerb();
}

Combobox::~Combobox() {
  if (GetInputMethod() && selector_.get()) {
    // Combobox should have been blurred before destroy.
    DCHECK(selector_.get() != GetInputMethod()->GetTextInputClient());
  }
}

const gfx::FontList& Combobox::GetFontList() const {
  return TypographyProvider::Get().GetFont(kContext, kStyle);
}

void Combobox::SetSelectedIndex(std::optional<size_t> index) {
  if (selected_index_ == index) {
    return;
  }
  // TODO(pbos): Add (D)CHECKs to validate the selected index.
  selected_index_ = index;

  if (selected_index_.has_value()) {
    GetViewAccessibility().SetPosInSet(
        base::checked_cast<int>(selected_index_.value()));
  }

  if (size_to_largest_label_) {
    OnPropertyChanged(&selected_index_, PropertyEffects::kPaint);
  } else {
    content_size_ = GetContentSize();
    OnPropertyChanged(&selected_index_, PropertyEffects::kPreferredSizeChanged);
  }

  UpdateAccessibleValue();
}

base::CallbackListSubscription Combobox::AddSelectedIndexChangedCallback(
    views::PropertyChangedCallback callback) {
  return AddPropertyChangedCallback(&selected_index_, std::move(callback));
}

bool Combobox::SelectValue(const std::u16string& value) {
  for (size_t i = 0; i < GetModel()->GetItemCount(); ++i) {
    if (value == GetModel()->GetItemAt(i)) {
      SetSelectedIndex(i);
      return true;
    }
  }
  return false;
}

void Combobox::SetOwnedModel(std::unique_ptr<ui::ComboboxModel> model) {
  // The swap keeps the outgoing model alive for SetModel().
  owned_model_.swap(model);
  SetModel(owned_model_.get());
}

void Combobox::SetModel(ui::ComboboxModel* model) {
  if (!model) {
    SetOwnedModel(std::make_unique<internal::EmptyComboboxModel>());
    return;
  }

  if (model_) {
    DCHECK(observation_.IsObservingSource(model_.get()));
    observation_.Reset();
  }

  model_ = model;

  if (model_) {
    model_ = model;
    menu_model_ = std::make_unique<ComboboxMenuModel>(this, model_);
    observation_.Observe(model_.get());
    SetSelectedIndex(model_->GetDefaultIndex());
    OnComboboxModelChanged(model_);
  }
}

std::u16string Combobox::GetTooltipTextAndAccessibleName() const {
  return arrow_button_->GetTooltipText();
}

void Combobox::SetTooltipTextAndAccessibleName(
    const std::u16string& tooltip_text) {
  arrow_button_->SetTooltipText(tooltip_text);
  if (GetViewAccessibility().GetCachedName().empty()) {
    GetViewAccessibility().SetName(tooltip_text);
  }
}

void Combobox::SetInvalid(bool invalid) {
  if (invalid == invalid_) {
    return;
  }

  invalid_ = invalid;

  if (views::FocusRing::Get(this)) {
    views::FocusRing::Get(this)->SetInvalid(invalid);
  }

  UpdateBorder();
  OnPropertyChanged(&invalid_, PropertyEffects::kPaint);
}

void Combobox::SetBorderColorId(ui::ColorId color_id) {
  border_color_id_ = color_id;
  UpdateBorder();
}

void Combobox::SetBackgroundColorId(ui::ColorId color_id) {
  SetBackground(CreateRoundedRectBackground(color_id, GetCornerRadius()));
}

void Combobox::SetForegroundColorId(ui::ColorId color_id) {
  foreground_color_id_ = color_id;
  SchedulePaint();
}

void Combobox::SetForegroundIconColorId(ui::ColorId color_id) {
  foreground_icon_color_id_ = color_id;
  SchedulePaint();
}

void Combobox::SetForegroundTextStyle(style::TextStyle text_style) {
  foreground_text_style_ = text_style;
  SchedulePaint();
}

void Combobox::SetEventHighlighting(bool should_highlight) {
  should_highlight_ = should_highlight;
  AsViewClass<TransparentButton>(arrow_button_)
      ->UpdateInkDrop(should_highlight);
}

void Combobox::SetSizeToLargestLabel(bool size_to_largest_label) {
  if (size_to_largest_label_ == size_to_largest_label) {
    return;
  }

  size_to_largest_label_ = size_to_largest_label;
  content_size_ = GetContentSize();
  OnPropertyChanged(&size_to_largest_label_,
                    PropertyEffects::kPreferredSizeChanged);
}

bool Combobox::IsMenuRunning() const {
  return menu_runner_ && menu_runner_->IsRunning();
}

void Combobox::OnThemeChanged() {
  View::OnThemeChanged();
  OnContentSizeMaybeChanged();
}

size_t Combobox::GetRowCount() {
  return GetModel()->GetItemCount();
}

std::optional<size_t> Combobox::GetSelectedRow() {
  return selected_index_;
}

void Combobox::SetSelectedRow(std::optional<size_t> row) {
  std::optional<size_t> prev_index = selected_index_;
  SetSelectedIndex(row);
  if (selected_index_ != prev_index) {
    OnPerformAction();
  }
}

std::u16string Combobox::GetTextForRow(size_t row) {
  return GetModel()->IsItemSeparatorAt(row) ? std::u16string()
                                            : GetModel()->GetItemAt(row);
}

base::CallbackListSubscription Combobox::AddMenuWillShowCallback(
    MenuWillShowCallback callback) {
  return on_menu_will_show_.Add(std::move(callback));
}

////////////////////////////////////////////////////////////////////////////////
// Combobox, View overrides:

gfx::Size Combobox::CalculatePreferredSize(
    const SizeBounds& /*available_size*/) const {
  // Limit how small a combobox can be.
  constexpr int kMinComboboxWidth = 25;

  // The preferred size will drive the local bounds which in turn is used to set
  // the minimum width for the dropdown list.
  int width = std::max(kMinComboboxWidth, content_size_.width()) +
              LayoutProvider::Get()->GetDistanceMetric(
                  DISTANCE_TEXTFIELD_HORIZONTAL_TEXT_PADDING) *
                  2 +
              GetInsets().width();

  // If an arrow is being shown, add extra width to include that arrow.
  if (should_show_arrow_) {
    width += GetComboboxArrowContainerWidthAndMargins();
  }

  return gfx::Size(width, LayoutProvider::GetControlHeightForFont(
                              kContext, kStyle, GetForegroundFontList()));
}

void Combobox::OnBoundsChanged(const gfx::Rect& previous_bounds) {
  arrow_button_->SetBounds(0, 0, width(), height());
}

bool Combobox::SkipDefaultKeyEventProcessing(const ui::KeyEvent& e) {
  // Escape should close the drop down list when it is active, not host UI.
  if (e.key_code() != ui::VKEY_ESCAPE || e.IsShiftDown() || e.IsControlDown() ||
      e.IsAltDown() || e.IsAltGrDown()) {
    return false;
  }
  return !!menu_runner_;
}

bool Combobox::OnKeyPressed(const ui::KeyEvent& e) {
  // TODO(oshima): handle IME.
  CHECK_EQ(e.type(), ui::EventType::kKeyPressed);

  if (!selected_index_.has_value()) {
    CHECK_EQ(model_->GetItemCount(), 0u);
    return false;
  }
  CHECK_LT(selected_index_.value(), GetModel()->GetItemCount());

#if BUILDFLAG(IS_MAC)
  if (e.key_code() != ui::VKEY_DOWN && e.key_code() != ui::VKEY_UP &&
      e.key_code() != ui::VKEY_SPACE && e.key_code() != ui::VKEY_HOME &&
      e.key_code() != ui::VKEY_END) {
    return false;
  }
  ShowDropDownMenu(ui::mojom::MenuSourceType::kKeyboard);
  return true;
#else
  const auto index_at_or_after = [](ui::ComboboxModel* model,
                                    size_t index) -> std::optional<size_t> {
    for (; index < model->GetItemCount(); ++index) {
      if (!model->IsItemSeparatorAt(index) && model->IsItemEnabledAt(index)) {
        return index;
      }
    }
    return std::nullopt;
  };
  const auto index_before = [](ui::ComboboxModel* model,
                               size_t index) -> std::optional<size_t> {
    for (; index > 0; --index) {
      const auto prev = index - 1;
      if (!model->IsItemSeparatorAt(prev) && model->IsItemEnabledAt(prev)) {
        return prev;
      }
    }
    return std::nullopt;
  };

  std::optional<size_t> new_index;
  switch (e.key_code()) {
    // Show the menu on F4 without modifiers.
    case ui::VKEY_F4:
      if (e.IsAltDown() || e.IsAltGrDown() || e.IsControlDown()) {
        return false;
      }
      ShowDropDownMenu(ui::mojom::MenuSourceType::kKeyboard);
      return true;

    // Move to the next item if any, or show the menu on Alt+Down like Windows.
    case ui::VKEY_DOWN:
      if (e.IsAltDown()) {
        ShowDropDownMenu(ui::mojom::MenuSourceType::kKeyboard);
        return true;
      }
      new_index = index_at_or_after(GetModel(), selected_index_.value() + 1);
      break;

    // Move to the end of the list.
    case ui::VKEY_END:
    case ui::VKEY_NEXT:  // Page down.
      new_index = index_before(GetModel(), GetModel()->GetItemCount());
      break;

    // Move to the beginning of the list.
    case ui::VKEY_HOME:
    case ui::VKEY_PRIOR:  // Page up.
      new_index = index_at_or_after(GetModel(), 0);
      break;

    // Move to the previous item if any.
    case ui::VKEY_UP:
      new_index = index_before(GetModel(), selected_index_.value());
      break;

    case ui::VKEY_RETURN:
    case ui::VKEY_SPACE:
      ShowDropDownMenu(ui::mojom::MenuSourceType::kKeyboard);
      return true;

    default:
      return false;
  }

  if (new_index.has_value()) {
    SetSelectedIndex(new_index);
    OnPerformAction();
  }
  return true;
#endif  // BUILDFLAG(IS_MAC)
}

void Combobox::OnPaint(gfx::Canvas* canvas) {
  OnPaintBackground(canvas);
  PaintIconAndText(canvas);
  OnPaintBorder(canvas);
}

void Combobox::OnFocus() {
  if (GetInputMethod()) {
    GetInputMethod()->SetFocusedTextInputClient(GetPrefixSelector());
  }

  View::OnFocus();
  // Border renders differently when focused.
  SchedulePaint();
}

void Combobox::OnBlur() {
  if (GetInputMethod()) {
    GetInputMethod()->DetachTextInputClient(GetPrefixSelector());
  }

  if (selector_) {
    selector_->OnViewBlur();
  }
  // Border renders differently when focused.
  SchedulePaint();
}

bool Combobox::HandleAccessibleAction(const ui::AXActionData& action_data) {
  // The action handling in View would generate a mouse event and send it to
  // |this|. However, mouse events for Combobox are handled by |arrow_button_|,
  // which is hidden from the a11y tree (so can't expose actions). Rather than
  // forwarding ax::mojom::Action::kDoDefault to View and then forwarding the
  // mouse event it generates to |arrow_button_| to have it forward back to the
  // callback on |this|, just handle the action explicitly here and bypass View.
  if (GetEnabled() && action_data.action == ax::mojom::Action::kDoDefault) {
    ShowDropDownMenu(ui::mojom::MenuSourceType::kKeyboard);
    return true;
  }
  return View::HandleAccessibleAction(action_data);
}

void Combobox::OnComboboxModelChanged(ui::ComboboxModel* model) {
  DCHECK_EQ(model_, model);

  if (IsMenuRunning()) {
    menu_runner_.reset();
    UpdateExpandedCollapsedAccessibleState();
  }

  // If the selection is no longer valid (or the model is empty), restore the
  // default index.
  if (!selected_index_.has_value() ||
      selected_index_ >= model_->GetItemCount() ||
      model_->GetItemCount() == 0 ||
      model_->IsItemSeparatorAt(selected_index_.value())) {
    SetSelectedIndex(model_->GetDefaultIndex());
  }

  OnContentSizeMaybeChanged();
  SchedulePaint();

  GetViewAccessibility().SetSetSize(
      base::checked_cast<int>(model_->GetItemCount()));
}

void Combobox::OnComboboxModelDestroying(ui::ComboboxModel* model) {
  SetModel(nullptr);
}

const base::RepeatingClosure& Combobox::GetCallback() const {
  return callback_;
}

const std::unique_ptr<ui::ComboboxModel>& Combobox::GetOwnedModel() const {
  return owned_model_;
}

void Combobox::UpdateBorder() {
  if (!GetEnabledInViewsSubtree()) {
    SetBorder(nullptr);
    return;
  }
  SetBorder(CreateRoundedRectBorder(
      kBorderThickness, GetCornerRadius(),
      invalid_
          ? ui::kColorAlertHighSeverity
          : border_color_id_.value_or(ui::kColorComboboxContainerOutline)));
}

void Combobox::AdjustBoundsForRTLUI(gfx::Rect* rect) const {
  rect->set_x(GetMirroredXForRect(*rect));
}

void Combobox::PaintIconAndText(gfx::Canvas* canvas) {
  if (!selected_index_.has_value()) {
    return;
  }
  CHECK_LT(selected_index_.value(), GetModel()->GetItemCount());

  gfx::Insets insets = GetInsets();
  insets += gfx::Insets::VH(0, LayoutProvider::Get()->GetDistanceMetric(
                                   DISTANCE_TEXTFIELD_HORIZONTAL_TEXT_PADDING));

  gfx::ScopedCanvas scoped_canvas(canvas);
  canvas->ClipRect(GetContentsBounds());

  int x = insets.left();
  int y = insets.top();
  int contents_height = height() - insets.height();

  // Draw the icon.
  ui::ImageModel icon = GetModel()->GetIconAt(selected_index_.value());
  if (!icon.IsEmpty()) {
    // Update the icon color if provided and if the icon color can be changed.
    if (foreground_icon_color_id_ && icon.IsVectorIcon()) {
      icon = ui::ImageModel::FromVectorIcon(*icon.GetVectorIcon().vector_icon(),
                                            foreground_icon_color_id_.value(),
                                            icon.GetVectorIcon().icon_size());
    }
    gfx::ImageSkia icon_skia = icon.Rasterize(GetColorProvider());
    int icon_y = y + (contents_height - icon_skia.height()) / 2;
    gfx::Rect icon_bounds(x, icon_y, icon_skia.width(), icon_skia.height());
    AdjustBoundsForRTLUI(&icon_bounds);
    canvas->DrawImageInt(icon_skia, icon_bounds.x(), icon_bounds.y());
    x += icon_skia.width();
  }

  // Draw the text.
  SkColor text_color =
      foreground_color_id_
          ? GetColorProvider()->GetColor(*foreground_color_id_)
          : GetTextColorForEnableState(*this, GetEnabledInViewsSubtree());
  std::u16string text = GetModel()->GetItemAt(*selected_index_);
  const gfx::FontList& font_list = GetForegroundFontList();

  // If the text is not empty, add padding between it and the icon. If there
  // was an empty icon, this padding is not necessary.
  if (!text.empty() && !icon.IsEmpty()) {
    x += LayoutProvider::Get()->GetDistanceMetric(
        DISTANCE_RELATED_LABEL_HORIZONTAL);
  }

  // The total width of the text is the minimum of either the string width,
  // or the available space, accounting for optional arrow.
  int text_width = gfx::GetStringWidth(text, font_list);
  int available_width = width() - x - insets.right();
  if (should_show_arrow_) {
    available_width -= GetComboboxArrowContainerWidthAndMargins();
  }
  text_width = std::min(text_width, available_width);

  gfx::Rect text_bounds(x, y, text_width, contents_height);
  AdjustBoundsForRTLUI(&text_bounds);
  canvas->DrawStringRect(text, font_list, text_color, text_bounds);

  // Draw the arrow.
  // TODO(crbug.com/40247801): Replace placeholder spacing and color values for
  // ChromeRefresh2023.
  if (should_show_arrow_) {
    gfx::Rect arrow_bounds(width() - GetComboboxArrowContainerWidthAndMargins(),
                           0, GetComboboxArrowContainerWidth(), height());
    arrow_bounds.ClampToCenteredSize(ComboboxArrowSize());
    AdjustBoundsForRTLUI(&arrow_bounds);

    PaintComboboxArrow(text_color, arrow_bounds, canvas);
  }
}

void Combobox::ArrowButtonPressed(const ui::Event& event) {
  if (!GetEnabled()) {
    return;
  }

  // TODO(hajimehoshi): Fix the problem that the arrow button blinks when
  // cliking this while the dropdown menu is opened.
  if ((base::TimeTicks::Now() - closed_time_) >
      kMinimumTimeBetweenButtonClicks) {
    ShowDropDownMenu(ui::GetMenuSourceTypeForEvent(event));
  }
}

void Combobox::ShowDropDownMenu(ui::mojom::MenuSourceType source_type) {
  on_menu_will_show_.Notify();

  constexpr int kMenuBorderWidthTop = 1;
  // Menu's requested position's width should be the same as local bounds so the
  // border of the menu lines up with the border of the combobox. The y
  // coordinate however should be shifted to the bottom with the border with not
  // to overlap with the combobox border.
  gfx::Rect lb = GetLocalBounds();
  gfx::Point menu_position(lb.origin());
  menu_position.set_y(menu_position.y() + kMenuBorderWidthTop);

  View::ConvertPointToScreen(this, &menu_position);

  gfx::Rect bounds(menu_position, lb.size());
  // If check marks exist in the combobox, adjust with bounds width to account
  // for them.
  if (!size_to_largest_label_) {
    bounds.set_width(MaybeAdjustWidthForCheckmarks(bounds.width()));
  }

  Button::ButtonState original_state = arrow_button_->GetState();
  arrow_button_->SetState(Button::STATE_PRESSED);

  // Allow |menu_runner_| to be set by the testing API, but if this method is
  // ever invoked recursively, ensure the old menu is closed.
  if (!menu_runner_ || IsMenuRunning()) {
    menu_runner_ = std::make_unique<MenuRunner>(
        menu_model_.get(), MenuRunner::COMBOBOX,
        base::BindRepeating(&Combobox::OnMenuClosed, base::Unretained(this),
                            original_state));
  }
  if (should_highlight_) {
    InkDrop::Get(arrow_button_)
        ->AnimateToState(InkDropState::ACTIVATED, nullptr);
  }
  menu_runner_->RunMenuAt(GetWidget(), nullptr, bounds,
                          MenuAnchorPosition::kTopLeft, source_type);
  UpdateExpandedCollapsedAccessibleState();
}

void Combobox::OnMenuClosed(Button::ButtonState original_button_state) {
  if (should_highlight_) {
    InkDrop::Get(arrow_button_)
        ->AnimateToState(InkDropState::DEACTIVATED, nullptr);
    InkDrop::Get(arrow_button_)->GetInkDrop()->SetHovered(IsMouseHovered());
  }
  menu_runner_.reset();
  arrow_button_->SetState(original_button_state);
  closed_time_ = base::TimeTicks::Now();
  UpdateExpandedCollapsedAccessibleState();
}

void Combobox::MenuSelectionAt(size_t index) {
  if (!menu_selection_at_callback_ || !menu_selection_at_callback_.Run(index)) {
    SetSelectedIndex(index);
    OnPerformAction();
  }
}

void Combobox::OnPerformAction() {
  SchedulePaint();

  if (callback_) {
    callback_.Run();
  }

  // Note |this| may be deleted by |callback_|.
}

gfx::Size Combobox::GetContentSize() const {
  const gfx::FontList& font_list = GetForegroundFontList();
  int height = font_list.GetHeight();
  int width = 0;
  for (size_t i = 0; i < GetModel()->GetItemCount(); ++i) {
    if (model_->IsItemSeparatorAt(i)) {
      continue;
    }

    if (size_to_largest_label_ || i == selected_index_) {
      int item_width = 0;
      ui::ImageModel icon = GetModel()->GetIconAt(i);
      std::u16string text = GetModel()->GetItemAt(i);
      if (!icon.IsEmpty()) {
        gfx::ImageSkia icon_skia;
        if (GetWidget()) {
          icon_skia = icon.Rasterize(GetColorProvider());
        }
        item_width += icon_skia.width();
        height = std::max(height, icon_skia.height());

        // If both the text and icon are not empty, include padding between.
        // We do not include this padding if there is no icon present.
        if (!text.empty()) {
          item_width += LayoutProvider::Get()->GetDistanceMetric(
              DISTANCE_RELATED_LABEL_HORIZONTAL);
        }
      }

      // If text is not empty, the content size needs to include the text width
      if (!text.empty()) {
        item_width += gfx::GetStringWidth(GetModel()->GetItemAt(i), font_list);
      }

      if (size_to_largest_label_) {
        item_width = MaybeAdjustWidthForCheckmarks(item_width);
      }
      width = std::max(width, item_width);
    }
  }
  return gfx::Size(width, height);
}

int Combobox::MaybeAdjustWidthForCheckmarks(int original_width) const {
  return MenuConfig::instance().check_selected_combobox_item
             ? original_width + kMenuCheckSize +
                   LayoutProvider::Get()->GetDistanceMetric(
                       DISTANCE_RELATED_BUTTON_HORIZONTAL)
             : original_width;
}

void Combobox::OnContentSizeMaybeChanged() {
  content_size_ = GetContentSize();
  PreferredSizeChanged();
}

PrefixSelector* Combobox::GetPrefixSelector() {
  if (!selector_) {
    selector_ = std::make_unique<PrefixSelector>(this, this);
  }
  return selector_.get();
}

const gfx::FontList& Combobox::GetForegroundFontList() const {
  if (foreground_text_style_) {
    return TypographyProvider::Get().GetFont(kContext, *foreground_text_style_);
  }
  return GetFontList();
}

void Combobox::UpdateExpandedCollapsedAccessibleState() const {
  if (menu_runner_) {
    GetViewAccessibility().SetIsExpanded();
  } else {
    GetViewAccessibility().SetIsCollapsed();
  }
}

void Combobox::UpdateAccessibleValue() const {
  if (model_->GetItemCount() > 0 && selected_index_.has_value()) {
    GetViewAccessibility().SetValue(model_->GetItemAt(selected_index_.value()));
  } else {
    GetViewAccessibility().RemoveValue();
  }
}

void Combobox::UpdateAccessibleDefaultActionVerb() {
  if (GetEnabledInViewsSubtree()) {
    GetViewAccessibility().SetDefaultActionVerb(
        ax::mojom::DefaultActionVerb::kOpen);
  } else {
    GetViewAccessibility().RemoveDefaultActionVerb();
  }
}

BEGIN_METADATA(Combobox)
ADD_PROPERTY_METADATA(base::RepeatingClosure, Callback)
ADD_PROPERTY_METADATA(std::unique_ptr<ui::ComboboxModel>, OwnedModel)
ADD_PROPERTY_METADATA(ui::ComboboxModel*, Model)
ADD_PROPERTY_METADATA(std::optional<size_t>, SelectedIndex)
ADD_PROPERTY_METADATA(bool, Invalid)
ADD_PROPERTY_METADATA(bool, SizeToLargestLabel)
ADD_PROPERTY_METADATA(std::u16string, TooltipTextAndAccessibleName)
END_METADATA

}  // namespace views
