#include "ui/views/controls/menu/menu_scroll_view_container.h"
#include <algorithm>
#include <memory>
#include <utility>
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "build/build_config.h"
#include "build/chromeos_buildflags.h"
#include "cc/paint/paint_flags.h"
#include "chromeos/constants/chromeos_features.h"
#include "third_party/skia/include/core/SkPath.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/dragdrop/mojom/drag_drop_types.mojom.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_tree_owner.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/bubble/bubble_border.h"
#include "ui/views/controls/menu/menu_config.h"
#include "ui/views/controls/menu/menu_controller.h"
#include "ui/views/controls/menu/menu_item_view.h"
#include "ui/views/controls/menu/submenu_view.h"
#include "ui/views/highlight_border.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/round_rect_painter.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"
namespace views {
namespace {
static constexpr int kBorderPaddingDueToRoundedCorners = 1;
static constexpr float kBackgroundBlurSigma = 30.f;
static constexpr float kBackgroundBlurQuality = 0.33f;
class MenuScrollButton : public View {
public:
METADATA_HEADER(MenuScrollButton);
MenuScrollButton(SubmenuView* host, bool is_up)
: host_(host),
is_up_(is_up),
pref_height_(MenuItemView::pref_menu_height()) {}
MenuScrollButton(const MenuScrollButton&) = delete;
MenuScrollButton& operator=(const MenuScrollButton&) = delete;
gfx::Size CalculatePreferredSize() const override {
return gfx::Size(MenuConfig::instance().scroll_arrow_height * 2 - 1,
pref_height_);
}
void OnThemeChanged() override {
View::OnThemeChanged();
arrow_color_ = GetColorProvider()->GetColor(ui::kColorMenuItemForeground);
}
bool CanDrop(const OSExchangeData& data) override {
DCHECK(host_->GetMenuItem()->GetMenuController());
return true;
}
void OnDragEntered(const ui::DropTargetEvent& event) override {
DCHECK(host_->GetMenuItem()->GetMenuController());
host_->GetMenuItem()->GetMenuController()->OnDragEnteredScrollButton(
host_, is_up_);
}
int OnDragUpdated(const ui::DropTargetEvent& event) override {
return ui::DragDropTypes::DRAG_NONE;
}
void OnDragExited() override {
DCHECK(host_->GetMenuItem()->GetMenuController());
host_->GetMenuItem()->GetMenuController()->OnDragExitedScrollButton(host_);
}
DropCallback GetDropCallback(const ui::DropTargetEvent& event) override {
return base::DoNothing();
}
void OnPaint(gfx::Canvas* canvas) override {
const MenuConfig& config = MenuConfig::instance();
gfx::Rect item_bounds(0, 0, width(), height());
ui::NativeTheme::ExtraParams extra;
GetNativeTheme()->Paint(canvas->sk_canvas(), GetColorProvider(),
ui::NativeTheme::kMenuItemBackground,
ui::NativeTheme::kNormal, item_bounds, extra);
int x = width() / 2;
int y = (height() - config.scroll_arrow_height) / 2;
int x_left = x - config.scroll_arrow_height;
int x_right = x + config.scroll_arrow_height;
int y_bottom;
if (!is_up_) {
y_bottom = y;
y = y_bottom + config.scroll_arrow_height;
} else {
y_bottom = y + config.scroll_arrow_height;
}
SkPath path;
path.setFillType(SkPathFillType::kWinding);
path.moveTo(SkIntToScalar(x), SkIntToScalar(y));
path.lineTo(SkIntToScalar(x_left), SkIntToScalar(y_bottom));
path.lineTo(SkIntToScalar(x_right), SkIntToScalar(y_bottom));
path.lineTo(SkIntToScalar(x), SkIntToScalar(y));
cc::PaintFlags flags;
flags.setStyle(cc::PaintFlags::kFill_Style);
flags.setAntiAlias(true);
flags.setColor(arrow_color_);
canvas->DrawPath(path, flags);
}
private:
raw_ptr<SubmenuView> host_;
bool is_up_;
int pref_height_;
SkColor arrow_color_;
};
BEGIN_METADATA(MenuScrollButton, View)
END_METADATA
}
class MenuScrollViewContainer::MenuScrollView : public View {
public:
METADATA_HEADER(MenuScrollView);
MenuScrollView(View* child, MenuScrollViewContainer* owner) : owner_(owner) {
AddChildView(child);
}
MenuScrollView(const MenuScrollView&) = delete;
MenuScrollView& operator=(const MenuScrollView&) = delete;
void ScrollRectToVisible(const gfx::Rect& rect) override {
if (GetLocalBounds().Contains(rect))
return;
int dy = 0;
if (rect.bottom() > GetLocalBounds().bottom())
dy = rect.bottom() - GetLocalBounds().bottom();
else
dy = rect.y();
View* child = GetContents();
int old_y = child->y();
int y = -std::max(
0, std::min(child->GetPreferredSize().height() - this->height(),
dy - child->y()));
child->SetY(y);
const int min_y = 0;
const int max_y = -(child->GetPreferredSize().height() - this->height());
if (old_y == min_y && old_y != y)
owner_->DidScrollAwayFromTop();
if (old_y == max_y && old_y != y)
owner_->DidScrollAwayFromBottom();
if (y == min_y)
owner_->DidScrollToTop();
if (y == max_y)
owner_->DidScrollToBottom();
}
View* GetContents() { return children().front(); }
const View* GetContents() const { return children().front(); }
private:
raw_ptr<MenuScrollViewContainer> owner_;
};
BEGIN_METADATA(MenuScrollViewContainer, MenuScrollView, View)
END_METADATA
MenuScrollViewContainer::MenuScrollViewContainer(SubmenuView* content_view)
: content_view_(content_view),
use_ash_system_ui_layout_(content_view->GetMenuItem()
->GetMenuController()
->use_ash_system_ui_layout()) {
background_view_ = AddChildView(std::make_unique<View>());
if (use_ash_system_ui_layout_) {
background_view_->SetPaintToLayer();
auto* background_layer = background_view_->layer();
background_layer->SetFillsBoundsOpaquely(false);
background_layer->SetBackgroundBlur(kBackgroundBlurSigma);
background_layer->SetBackdropFilterQuality(kBackgroundBlurQuality);
}
auto* layout =
background_view_->SetLayoutManager(std::make_unique<views::FlexLayout>());
layout->SetOrientation(views::LayoutOrientation::kVertical);
scroll_up_button_ = background_view_->AddChildView(
std::make_unique<MenuScrollButton>(content_view, true));
scroll_view_ = background_view_->AddChildView(
std::make_unique<MenuScrollView>(content_view, this));
scroll_view_->SetProperty(
views::kFlexBehaviorKey,
views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToMinimum,
views::MaximumFlexSizeRule::kUnbounded));
scroll_down_button_ = background_view_->AddChildView(
std::make_unique<MenuScrollButton>(content_view, false));
SkColor override_color;
MenuDelegate* delegate = content_view_->GetMenuItem()->GetDelegate();
if (delegate && delegate->GetBackgroundColor(-1, false, &override_color))
SetBackground(views::CreateSolidBackground(override_color));
arrow_ = BubbleBorderTypeFromAnchor(
content_view_->GetMenuItem()->GetMenuController()->GetAnchorPosition());
CreateBorder();
}
bool MenuScrollViewContainer::HasBubbleBorder() const {
return arrow_ != BubbleBorder::NONE;
}
MenuItemView* MenuScrollViewContainer::GetFootnote() const {
MenuItemView* const footnote = content_view_->GetLastItem();
return (footnote && footnote->GetType() == MenuItemView::Type::kHighlighted)
? footnote
: nullptr;
}
gfx::RoundedCornersF MenuScrollViewContainer::GetRoundedCorners() const {
auto* menu_controller = content_view_->GetMenuItem()->GetMenuController();
if (!menu_controller)
return gfx::RoundedCornersF(corner_radius_);
absl::optional<gfx::RoundedCornersF> rounded_corners =
menu_controller->rounded_corners();
if (rounded_corners.has_value())
return rounded_corners.value();
return gfx::RoundedCornersF(corner_radius_);
}
gfx::Size MenuScrollViewContainer::CalculatePreferredSize() const {
gfx::Size prefsize = scroll_view_->GetContents()->GetPreferredSize();
gfx::Insets insets = GetInsets();
prefsize.Enlarge(insets.width(), insets.height());
const MenuConfig& config = MenuConfig::instance();
if (GetFootnote() && config.use_outer_border && !HasBubbleBorder() &&
!config.use_bubble_border) {
prefsize.Enlarge(0, 1);
}
return prefsize;
}
void MenuScrollViewContainer::OnThemeChanged() {
View::OnThemeChanged();
CreateBorder();
}
void MenuScrollViewContainer::OnPaintBackground(gfx::Canvas* canvas) {
if (background()) {
View::OnPaintBackground(canvas);
return;
}
if (use_ash_system_ui_layout_ && background_view_->background())
return;
gfx::Rect bounds(0, 0, width(), height());
ui::NativeTheme::ExtraParams extra;
const MenuConfig& menu_config = MenuConfig::instance();
extra.menu_background.corner_radius = menu_config.CornerRadiusForMenu(
content_view_->GetMenuItem()->GetMenuController());
if (border_color_id_.has_value()) {
ui::ColorProvider* color_provider = GetColorProvider();
cc::PaintFlags flags;
flags.setAntiAlias(true);
flags.setStyle(cc::PaintFlags::kFill_Style);
flags.setColor(color_provider->GetColor(border_color_id_.value()));
canvas->DrawRoundRect(GetLocalBounds(), extra.menu_background.corner_radius,
flags);
return;
}
GetNativeTheme()->Paint(canvas->sk_canvas(), GetColorProvider(),
ui::NativeTheme::kMenuPopupBackground,
ui::NativeTheme::kNormal, bounds, extra);
}
void MenuScrollViewContainer::GetAccessibleNodeData(ui::AXNodeData* node_data) {
content_view_->GetAccessibleNodeData(node_data);
#if BUILDFLAG(IS_MAC)
node_data->role = ax::mojom::Role::kNone;
#else
node_data->role = ax::mojom::Role::kMenuBar;
#endif
}
void MenuScrollViewContainer::OnBoundsChanged(
const gfx::Rect& previous_bounds) {
scroll_up_button_->SetVisible(false);
scroll_down_button_->SetVisible(
scroll_view_->GetContents()->GetPreferredSize().height() > height());
const bool any_scroll_button_visible =
scroll_up_button_->GetVisible() || scroll_down_button_->GetVisible();
MenuItemView* const footnote = GetFootnote();
if (footnote)
footnote->SetCornerRadius(any_scroll_button_visible ? 0 : corner_radius_);
InvalidateLayout();
background_view_->SetBoundsRect(GetContentsBounds());
}
void MenuScrollViewContainer::DidScrollToTop() {
scroll_up_button_->SetVisible(false);
}
void MenuScrollViewContainer::DidScrollToBottom() {
scroll_down_button_->SetVisible(false);
}
void MenuScrollViewContainer::DidScrollAwayFromTop() {
scroll_up_button_->SetVisible(true);
}
void MenuScrollViewContainer::DidScrollAwayFromBottom() {
scroll_down_button_->SetVisible(true);
}
void MenuScrollViewContainer::CreateBorder() {
if (HasBubbleBorder())
CreateBubbleBorder();
else
CreateDefaultBorder();
}
void MenuScrollViewContainer::CreateDefaultBorder() {
DCHECK_EQ(arrow_, BubbleBorder::NONE);
MenuController* menu_controller =
content_view_->GetMenuItem()->GetMenuController();
const MenuConfig& menu_config = MenuConfig::instance();
corner_radius_ = menu_config.CornerRadiusForMenu(
content_view_->GetMenuItem()->GetMenuController());
int padding = menu_config.use_outer_border && corner_radius_ > 0
? kBorderPaddingDueToRoundedCorners
: 0;
const int vertical_inset =
(corner_radius_ ? corner_radius_
: menu_config.menu_vertical_border_size) +
padding;
const int horizontal_inset =
menu_config.menu_horizontal_border_size + padding;
int bottom_inset = GetFootnote() ? 0 : vertical_inset;
if (menu_config.use_outer_border) {
if (menu_config.use_bubble_border && (corner_radius_ > 0) &&
!menu_controller->IsCombobox()) {
CreateBubbleBorder();
} else {
gfx::Insets insets = gfx::Insets::TLBR(vertical_inset, horizontal_inset,
bottom_inset, horizontal_inset);
if (border_color_id_.has_value()) {
SetBorder(views::CreateThemedSolidSidedBorder(
insets, border_color_id_.value()));
return;
}
SetBackground(CreateThemedRoundedRectBackground(ui::kColorMenuBackground,
corner_radius_));
SkColor color = GetWidget()
? GetColorProvider()->GetColor(ui::kColorMenuBorder)
: gfx::kPlaceholderColor;
SetBorder(views::CreateBorderPainter(
std::make_unique<views::RoundRectPainter>(color, corner_radius_),
insets));
}
} else {
SetBorder(CreateEmptyBorder(gfx::Insets::TLBR(
vertical_inset, horizontal_inset, bottom_inset, horizontal_inset)));
}
}
void MenuScrollViewContainer::CreateBubbleBorder() {
const MenuConfig& menu_config = MenuConfig::instance();
auto* menu_controller = content_view_->GetMenuItem()->GetMenuController();
const int border_radius = menu_config.CornerRadiusForMenu(menu_controller);
ui::ColorId id = ui::kColorMenuBackground;
BubbleBorder::Shadow shadow_type = BubbleBorder::STANDARD_SHADOW;
#if BUILDFLAG(IS_CHROMEOS_ASH)
id = ui::kColorAshSystemUIMenuBackground;
if (use_ash_system_ui_layout_)
shadow_type = BubbleBorder::CHROMEOS_SYSTEM_UI_SHADOW;
#endif
if (border_color_id_.has_value()) {
id = border_color_id_.value();
}
auto bubble_border = std::make_unique<BubbleBorder>(arrow_, shadow_type, id);
bool has_customized_corner = use_ash_system_ui_layout_ && menu_controller &&
menu_controller->rounded_corners().has_value();
if (use_ash_system_ui_layout_ || border_radius > 0 || has_customized_corner) {
if (has_customized_corner) {
bubble_border->SetRoundedCorners(
GetRoundedCorners().upper_left(), GetRoundedCorners().upper_right(),
GetRoundedCorners().lower_right(), GetRoundedCorners().lower_left());
} else {
bubble_border->SetCornerRadius(border_radius);
}
const bool is_top_menu = !content_view_->GetMenuItem()->GetParentMenuItem();
bubble_border->set_md_shadow_elevation(
is_top_menu ? menu_config.touchable_menu_shadow_elevation
: menu_config.touchable_submenu_shadow_elevation);
auto insets =
gfx::Insets::VH(menu_config.vertical_touchable_menu_item_padding, 0);
if (GetFootnote())
insets.set_bottom(0);
scroll_view_->GetContents()->SetBorder(CreateEmptyBorder(insets));
}
corner_radius_ = bubble_border->corner_radius();
if (use_ash_system_ui_layout_) {
background_view_->SetBackground(
CreateThemedRoundedRectBackground(id, corner_radius_));
background_view_->layer()->SetRoundedCornerRadius(GetRoundedCorners());
#if BUILDFLAG(IS_CHROMEOS_ASH)
background_view_->SetBorder(std::make_unique<HighlightBorder>(
GetRoundedCorners(),
chromeos::features::IsJellyrollEnabled()
? HighlightBorder::Type::kHighlightBorderOnShadow
: HighlightBorder::Type::kHighlightBorder1));
#endif
} else {
SetBackground(std::make_unique<BubbleBackground>(bubble_border.get()));
}
SetBorder(std::move(bubble_border));
}
BubbleBorder::Arrow MenuScrollViewContainer::BubbleBorderTypeFromAnchor(
MenuAnchorPosition anchor) {
switch (anchor) {
case MenuAnchorPosition::kTopLeft:
case MenuAnchorPosition::kTopRight:
case MenuAnchorPosition::kBottomCenter:
return BubbleBorder::NONE;
case MenuAnchorPosition::kBubbleTopLeft:
case MenuAnchorPosition::kBubbleTopRight:
case MenuAnchorPosition::kBubbleLeft:
case MenuAnchorPosition::kBubbleRight:
case MenuAnchorPosition::kBubbleBottomLeft:
case MenuAnchorPosition::kBubbleBottomRight:
return BubbleBorder::FLOAT;
}
}
BEGIN_METADATA(MenuScrollViewContainer, View)
END_METADATA
}