910e62b5创建于 1月15日历史提交
// 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/bubble/bubble_border.h"

#include <algorithm>
#include <map>
#include <tuple>
#include <utility>

#include "base/check.h"
#include "base/check_op.h"
#include "base/no_destructor.h"
#include "cc/paint/paint_flags.h"
#include "third_party/skia/include/core/SkBlendMode.h"
#include "third_party/skia/include/core/SkClipOp.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkPath.h"
#include "third_party/skia/include/core/SkPoint.h"
#include "third_party/skia/include/core/SkRRect.h"
#include "third_party/skia/include/core/SkRect.h"
#include "third_party/skia/include/core/SkScalar.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.h"
#include "ui/color/color_variant.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/geometry/rrect_f.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/gfx/geometry/vector2d.h"
#include "ui/gfx/scoped_canvas.h"
#include "ui/gfx/shadow_value.h"
#include "ui/gfx/skia_paint_util.h"
#include "ui/views/bubble/bubble_border_arrow_utils.h"
#include "ui/views/metadata/type_conversion.h"
#include "ui/views/view.h"
#include "ui/views/widget/widget.h"
#include "ui/wm/core/shadow_controller.h"

namespace views {

namespace {

// GetShadowValues and GetBorderAndShadowFlags cache their results. The shadow
// values depend on the shadow elevation, color and shadow type, so we create a
// tuple to key the cache.
using ShadowCacheKey = std::tuple<int, SkColor, BubbleBorder::Shadow>;

// The shadow type for default shadow colors.
constexpr int kDefaultShadowType = -1;

ui::Shadow::ElevationToColorsMap ShadowElevationToColorsMap(
    BubbleBorder::Shadow shadow,
    const ui::ColorProvider* color_provider) {
  ui::Shadow::ElevationToColorsMap colors_map;
  if (color_provider) {
    switch (shadow) {
      case BubbleBorder::Shadow::STANDARD_SHADOW:
        colors_map[3] = std::make_pair(
            color_provider->GetColor(
                ui::kColorShadowValueKeyShadowElevationThree),
            color_provider->GetColor(
                ui::kColorShadowValueAmbientShadowElevationThree));
        colors_map[16] = std::make_pair(
            color_provider->GetColor(
                ui::kColorShadowValueKeyShadowElevationSixteen),
            color_provider->GetColor(
                ui::kColorShadowValueAmbientShadowElevationSixteen));
        break;
#if BUILDFLAG(IS_CHROMEOS)
      case BubbleBorder::Shadow::CHROMEOS_SYSTEM_UI_SHADOW:
        colors_map =
            wm::ShadowController::GenerateShadowColorsMap(color_provider);
        break;
#endif
      default:
        NOTREACHED() << "Invalid bubble border shadow type.";
    }
  }

  const SkColor default_color =
      color_provider ? color_provider->GetColor(ui::kColorShadowBase)
                     : gfx::kPlaceholderColor;
  colors_map[kDefaultShadowType] = std::make_pair(default_color, default_color);
  return colors_map;
}

enum class BubbleArrowPart { kFill, kBorder };

SkPath GetVisibleArrowPath(BubbleBorder::Arrow arrow,
                           const gfx::Rect& bounds,
                           BubbleArrowPart part) {
  constexpr size_t kNumPoints = 4;
  gfx::RectF bounds_f(bounds);
  SkPoint points[kNumPoints];
  switch (GetBubbleArrowSide(arrow)) {
    case BubbleArrowSide::kRight:
      points[0] = {bounds_f.x(), bounds_f.y()};
      points[1] = {bounds_f.right(),
                   bounds_f.y() + BubbleBorder::kVisibleArrowRadius - 1};
      points[2] = {bounds_f.right(),
                   bounds_f.y() + BubbleBorder::kVisibleArrowRadius};
      points[3] = {bounds_f.x(), bounds_f.bottom() - 1};
      break;
    case BubbleArrowSide::kLeft:
      points[0] = {bounds_f.right(), bounds_f.bottom() - 1};
      points[1] = {bounds_f.x(),
                   bounds_f.y() + BubbleBorder::kVisibleArrowRadius};
      points[2] = {bounds_f.x(),
                   bounds_f.y() + BubbleBorder::kVisibleArrowRadius - 1};
      points[3] = {bounds_f.right(), bounds_f.y()};
      break;
    case BubbleArrowSide::kTop:
      points[0] = {bounds_f.x(), bounds_f.bottom()};
      points[1] = {bounds_f.x() + BubbleBorder::kVisibleArrowRadius - 1,
                   bounds_f.y()};
      points[2] = {bounds_f.x() + BubbleBorder::kVisibleArrowRadius,
                   bounds_f.y()};
      points[3] = {bounds_f.right() - 1, bounds_f.bottom()};
      break;
    case BubbleArrowSide::kBottom:
      points[0] = {bounds_f.right() - 1, bounds_f.y()};
      points[1] = {bounds_f.x() + BubbleBorder::kVisibleArrowRadius,
                   bounds_f.bottom()};
      points[2] = {bounds_f.x() + BubbleBorder::kVisibleArrowRadius - 1,
                   bounds_f.bottom()};
      points[3] = {bounds_f.x(), bounds_f.y()};
      break;
  }

  return SkPath::Polygon(points, part == BubbleArrowPart::kFill);
}

const gfx::ShadowValues& GetShadowValues(
    const ui::ColorProvider* color_provider,
    const std::optional<int>& elevation,
    BubbleBorder::Shadow shadow_type) {
  // If the color provider does not exist the shadow values are being created in
  // order to calculate Insets. In that case the color plays no role so always
  // set those colors to gfx::kPlaceholderColor.

  SkColor color = color_provider
                      ? color_provider->GetColor(ui::kColorShadowBase)
                      : gfx::kPlaceholderColor;

  // The shadows are always the same for any elevation and color combination, so
  // construct them once and cache.
  static base::NoDestructor<std::map<ShadowCacheKey, gfx::ShadowValues>>
      shadow_map;
  ShadowCacheKey key(elevation.value_or(-1), color, shadow_type);

  if (shadow_map->find(key) != shadow_map->end()) {
    return shadow_map->find(key)->second;
  }

  gfx::ShadowValues shadows;
  if (elevation.has_value()) {
    DCHECK_GE(elevation.value(), 0);
    auto shadow_colors_map =
        ShadowElevationToColorsMap(shadow_type, color_provider);
    const auto iter = shadow_colors_map.find(elevation.value());
    const auto key_ambient_colors = (iter != shadow_colors_map.end())
                                        ? iter->second
                                        : shadow_colors_map[kDefaultShadowType];
    switch (shadow_type) {
      case BubbleBorder::Shadow::STANDARD_SHADOW:
        shadows = gfx::ShadowValue::MakeShadowValues(elevation.value(),
                                                     key_ambient_colors.first,
                                                     key_ambient_colors.second);
        break;
#if BUILDFLAG(IS_CHROMEOS)
      case BubbleBorder::CHROMEOS_SYSTEM_UI_SHADOW:
        if (key_ambient_colors.first == key_ambient_colors.second) {
          shadows = gfx::ShadowValue::MakeChromeOSSystemUIShadowValues(
              elevation.value(), key_ambient_colors.first);
        } else {
          shadows = gfx::ShadowValue::MakeChromeOSSystemUIShadowValues(
              elevation.value(), key_ambient_colors.first,
              key_ambient_colors.second);
        }
        break;
#endif
      default:
        NOTREACHED() << "Invalid bubble border shadow type";
    }
  } else {
    constexpr gfx::Vector2d kOffset(0, 2);
    constexpr int kSmallShadowBlur = 4;
    const SkColor small_shadow_color =
        color_provider
            ? color_provider->GetColor(ui::kColorBubbleBorderShadowSmall)
            : gfx::kPlaceholderColor;
    const SkColor large_shadow_color =
        color_provider
            ? color_provider->GetColor(ui::kColorBubbleBorderShadowLarge)
            : gfx::kPlaceholderColor;
    // gfx::ShadowValue counts blur pixels both inside and outside the shape,
    // whereas these blur values only describe the outside portion, hence they
    // must be doubled.
    shadows = gfx::ShadowValues({
        {kOffset, 2 * kSmallShadowBlur, small_shadow_color},
        {kOffset, 2 * BubbleBorder::kShadowBlur, large_shadow_color},
    });
  }

  shadow_map->insert({key, shadows});
  return shadow_map->find(key)->second;
}

bool ShouldDrawStrokeForArgs(const std::optional<bool>& draw_border_stroke,
                             const std::optional<int>& elevation,
                             BubbleBorder::Shadow shadow_type) {
  return draw_border_stroke.value_or(!elevation.has_value() &&
                                     shadow_type != BubbleBorder::NO_SHADOW);
}

const cc::PaintFlags& GetBorderAndShadowFlags(
    const ui::ColorProvider* color_provider,
    const std::optional<int>& elevation,
    BubbleBorder::Shadow shadow_type) {
  // The flags are always the same for any elevation and color combination, so
  // construct them once and cache.
  static base::NoDestructor<std::map<ShadowCacheKey, cc::PaintFlags>> flag_map;
  ShadowCacheKey key(elevation.value_or(-1),
                     color_provider->GetColor(ui::kColorShadowBase),
                     shadow_type);

  if (flag_map->find(key) != flag_map->end()) {
    return flag_map->find(key)->second;
  }

  cc::PaintFlags flags;
  flags.setColor(color_provider->GetColor(ui::kColorBubbleBorder));
  flags.setAntiAlias(true);
  flags.setLooper(gfx::CreateShadowDrawLooper(
      GetShadowValues(color_provider, elevation, shadow_type)));
  flag_map->insert({key, flags});
  return flag_map->find(key)->second;
}

template <typename T>
void DrawBorderAndShadowImpl(
    T rect,
    void (cc::PaintCanvas::*draw)(const T&, const cc::PaintFlags&),
    gfx::Canvas* canvas,
    const ui::ColorProvider* color_provider,
    bool draw_stroke = true,
    const std::optional<int>& elevation = std::nullopt,
    BubbleBorder::Shadow shadow_type = BubbleBorder::STANDARD_SHADOW) {
  if (draw_stroke) {
    // Provide a 1 px border outside the bounds.
    constexpr int kBorderStrokeThicknessPx = 1;
    const SkScalar one_pixel =
        SkFloatToScalar(kBorderStrokeThicknessPx / canvas->image_scale());
    rect.outset(one_pixel, one_pixel);
  }

  (canvas->sk_canvas()->*draw)(
      rect, GetBorderAndShadowFlags(color_provider, elevation, shadow_type));
}

// Shadow is set to NO_SHADOW if compositing is not supported. This checks for
// NO_SHADOW in cases where it would have to be explicitly set.
bool IsExplicitNoShadow(BubbleBorder::Shadow shadow) {
  if (shadow == BubbleBorder::NO_SHADOW) {
    return Widget::IsWindowCompositingSupported();
  }
  return false;
}

}  // namespace

BubbleBorder::BubbleBorder(Arrow arrow, Shadow shadow)
    : arrow_(arrow), shadow_(shadow) {
  DCHECK_LT(shadow_, SHADOW_COUNT);
  SetColor(ui::kColorDialogBackground);
}

BubbleBorder::~BubbleBorder() = default;

// static
gfx::Insets BubbleBorder::GetBorderAndShadowInsets(
    const std::optional<int>& elevation,
    const std::optional<bool>& draw_border_stroke,
    BubbleBorder::Shadow shadow_type) {
  return gfx::Insets(
             ShouldDrawStrokeForArgs(draw_border_stroke, elevation, shadow_type)
                 ? kBorderThicknessDip
                 : 0) -
         gfx::ShadowValue::GetMargin(
             GetShadowValues(nullptr, elevation, shadow_type));
}

gfx::Rect BubbleBorder::GetBounds(const gfx::Rect& anchor_rect,
                                  const gfx::Size& contents_size) const {
  const gfx::Size size(GetSizeForContentsSize(contents_size));
  // In floating mode, the bounds of the bubble border and the |anchor_rect|
  // have coinciding central points.
  if (arrow_ == FLOAT) {
    gfx::Rect rect(anchor_rect.CenterPoint(), size);
    rect.Offset(gfx::Vector2d(-size.width() / 2, -size.height() / 2));
    return rect;
  }

  // If no arrow is used, in the vertical direction, the bubble is placed below
  // the |anchor_rect| while they have coinciding horizontal centers.
  if (arrow_ == NONE) {
    gfx::Rect rect(anchor_rect.bottom_center(), size);
    rect.Offset(gfx::Vector2d(-size.width() / 2, 0));
    return rect;
  }

  // In all other cases, the used arrow determines the placement of the bubble
  // with respect to the |anchor_rect|.
  gfx::Rect contents_bounds(contents_size);
  // Always apply the border part of the inset before calculating coordinates,
  // that ensures the bubble's border is aligned with the anchor's border.
  // For the purposes of positioning, the border is rounded up to a dip, which
  // may cause misalignment in scale factors greater than 1.
  // TODO(estade): when it becomes possible to provide px bounds instead of
  // dip bounds, fix this.
  const gfx::Insets border_insets(ShouldDrawStroke() ? kBorderThicknessDip : 0);
  const gfx::Insets insets = GetInsets();
  const gfx::Insets shadow_insets = insets - border_insets;
  // TODO(dfried): Collapse border into visible arrow where applicable.
  contents_bounds.Inset(-border_insets);
  DCHECK(!avoid_shadow_overlap_ || !visible_arrow_);

  // If |avoid_shadow_overlap_| is true, the shadow part of the inset is also
  // applied now, to ensure that the shadow itself doesn't overlap the anchor.
  if (avoid_shadow_overlap_) {
    contents_bounds.Inset(-shadow_insets);
  }

  // Adjust the contents to align with the arrow. The `anchor_point` is the
  // point on `anchor_rect` to offset from; it is also used as part of the
  // visible arrow calculation if present.
  gfx::Point anchor_point =
      GetArrowAnchorPointFromAnchorRect(arrow_, anchor_rect);

  contents_bounds += GetContentBoundsOffsetToArrowAnchorPoint(
      contents_bounds, arrow_, anchor_point);

  // With NO_SHADOW, there should be further insets, but the same logic is
  // used to position the bubble origin according to |anchor_rect|.
  DCHECK(!IsExplicitNoShadow(shadow_) || insets_.has_value() ||
         shadow_insets.IsEmpty() || visible_arrow_);
  if (!avoid_shadow_overlap_) {
    contents_bounds.Inset(-shadow_insets);
  }

  // |arrow_offset_| is used to adjust bubbles that would normally be
  // partially offscreen.
  if (is_arrow_on_horizontal(arrow_)) {
    contents_bounds += gfx::Vector2d(-arrow_offset_, 0);
  } else {
    contents_bounds += gfx::Vector2d(0, -arrow_offset_);
  }

  // If no visible arrow is shown, return the content bounds.
  if (!visible_arrow_) {
    return contents_bounds;
  }

  // Finally, get the needed movement vector of |contents_bounds| to create the
  // space needed to place the visible arrow. adjustments because we don't want
  // the positioning to be altered. Offset by the size of the arrow's inset on
  // each side (only one side will be nonzero) to create space for the visible
  // arrow.
  contents_bounds +=
      GetContentsBoundsOffsetToPlaceVisibleArrow(arrow_, /*include_gap=*/true);

  // We have an anchor point which is appropriate for the arrow type, but
  // when anchoring to a small view it looks better to track from the middle
  // of the view rather than a corner. We may still adjust this point if
  // it's too close to the edge of the bubble (in this case by adjusting the
  // bubble by a few pixels rather than the anchor point).
  const gfx::Point anchor_center = anchor_rect.CenterPoint();
  const gfx::Point contents_center = contents_bounds.CenterPoint();
  if (IsVerticalArrow(arrow_)) {
    const int right_bound =
        contents_bounds.right() -
        (kVisibleArrowBuffer + kVisibleArrowRadius + shadow_insets.right());
    const int left_bound = contents_bounds.x() + kVisibleArrowBuffer +
                           kVisibleArrowRadius + shadow_insets.left();
    if (anchor_point.x() > anchor_center.x() &&
        anchor_center.x() > contents_center.x()) {
      anchor_point.set_x(anchor_center.x());
    } else if (anchor_point.x() > right_bound) {
      anchor_point.set_x(std::max(anchor_rect.x(), right_bound));
    } else if (anchor_point.x() < anchor_center.x() &&
               anchor_center.x() < contents_center.x()) {
      anchor_point.set_x(anchor_center.x());
    } else if (anchor_point.x() < left_bound) {
      anchor_point.set_x(std::min(anchor_rect.right(), left_bound));
    }
    if (anchor_point.x() < left_bound) {
      contents_bounds -= gfx::Vector2d(left_bound - anchor_point.x(), 0);
    } else if (anchor_point.x() > right_bound) {
      contents_bounds += gfx::Vector2d(anchor_point.x() - right_bound, 0);
    }
  } else {
    const int bottom_bound =
        contents_bounds.bottom() -
        (kVisibleArrowBuffer + kVisibleArrowRadius + shadow_insets.bottom());
    const int top_bound = contents_bounds.y() + kVisibleArrowBuffer +
                          kVisibleArrowRadius + shadow_insets.top();
    if (anchor_point.y() > anchor_center.y() &&
        anchor_center.y() > contents_center.y()) {
      anchor_point.set_y(anchor_center.y());
    } else if (anchor_point.y() > bottom_bound) {
      anchor_point.set_y(std::max(anchor_rect.y(), bottom_bound));
    } else if (anchor_point.y() < anchor_center.y() &&
               anchor_center.y() < contents_center.y()) {
      anchor_point.set_y(anchor_center.y());
    } else if (anchor_point.y() < top_bound) {
      anchor_point.set_y(std::min(anchor_rect.bottom(), top_bound));
    }
    if (anchor_point.y() < top_bound) {
      contents_bounds -= gfx::Vector2d(0, top_bound - anchor_point.y());
    } else if (anchor_point.y() > bottom_bound) {
      contents_bounds += gfx::Vector2d(0, anchor_point.y() - bottom_bound);
    }
  }

  CalculateVisibleArrowRect(contents_bounds, anchor_point);

  return contents_bounds;
}

// static
gfx::Vector2d BubbleBorder::GetContentsBoundsOffsetToPlaceVisibleArrow(
    BubbleBorder::Arrow arrow,
    bool include_gap) {
  DCHECK(has_arrow(arrow));

  const gfx::Insets visible_arrow_insets =
      GetVisibleArrowInsets(arrow, include_gap);
  return gfx::Vector2d(
      visible_arrow_insets.left() - visible_arrow_insets.right(),
      visible_arrow_insets.top() - visible_arrow_insets.bottom());
}

void BubbleBorder::Paint(const views::View& view, gfx::Canvas* canvas) {
  if (shadow_ == NO_SHADOW) {
    PaintNoShadow(view, canvas);
    return;
  }

  gfx::ScopedCanvas scoped(canvas);
  SkRRect r_rect = GetClientRect(view);
  canvas->sk_canvas()->clipRRect(r_rect, SkClipOp::kDifference,
                                 true /*doAntiAlias*/);
  DrawBorderAndShadowImpl(r_rect, &cc::PaintCanvas::drawRRect, canvas,
                          view.GetColorProvider(), ShouldDrawStroke(),
                          md_shadow_elevation_, shadow_);

  if (visible_arrow_) {
    PaintVisibleArrow(view, canvas);
  }
}

// static
void BubbleBorder::DrawBorderAndShadow(
    SkRect rect,
    gfx::Canvas* canvas,
    const ui::ColorProvider* color_provider) {
  DrawBorderAndShadowImpl(rect, &cc::PaintCanvas::drawRect, canvas,
                          color_provider);
}

gfx::Insets BubbleBorder::GetInsets() const {
  // Visible arrow is not compatible with OS-drawn shadow or with manual insets.
  DCHECK((!insets_ && !IsExplicitNoShadow(shadow_)) || !visible_arrow_);
  if (insets_.has_value()) {
    return insets_.value();
  }
  gfx::Insets insets;

  switch (shadow_) {
    case STANDARD_SHADOW:
#if BUILDFLAG(IS_CHROMEOS)
    case CHROMEOS_SYSTEM_UI_SHADOW:
#endif
      insets = GetBorderAndShadowInsets(md_shadow_elevation_,
                                        draw_border_stroke_, shadow_);
      break;
    default:
      break;
  }

  if (visible_arrow_) {
    const gfx::Insets arrow_insets = GetVisibleArrowInsets(arrow_, false);
    insets = gfx::Insets::TLBR(std::max(insets.top(), arrow_insets.top()),
                               std::max(insets.left(), arrow_insets.left()),
                               std::max(insets.bottom(), arrow_insets.bottom()),
                               std::max(insets.right(), arrow_insets.right()));
  }
  return insets;
}

gfx::Size BubbleBorder::GetMinimumSize() const {
  return GetSizeForContentsSize(gfx::Size());
}

void BubbleBorder::OnViewThemeChanged(View* view) {
  view->SchedulePaint();
}

gfx::Size BubbleBorder::GetSizeForContentsSize(
    const gfx::Size& contents_size) const {
  // Enlarge the contents size by the thickness of the border images.
  gfx::Size size(contents_size);
  const gfx::Insets insets = GetInsets();
  size.Enlarge(insets.width(), insets.height());
  return size;
}

bool BubbleBorder::AddArrowToBubbleCornerAndPointTowardsAnchor(
    const gfx::Rect& anchor_rect,
    gfx::Rect& popup_bounds,
    int popup_min_y) {
  // This function should only be called for a visible arrow.
  DCHECK(arrow_ != Arrow::NONE && arrow_ != Arrow::FLOAT);
  CHECK_GE(popup_bounds.y(), popup_min_y);

  // The total size of the arrow in its normal direction.
  const int kVisibleArrowDiamater = 2 * kVisibleArrowRadius;

  // To store the resulting x and y position of the arrow.
  int x_position, y_position;

  // The definition of an vertical arrow is inconsistent.
  if (IsVerticalArrow(arrow_)) {
    // The optimal x position depends on the |arrow_|.
    // For a center-aligned arrow, the optimal position of the arrow points
    // towards the center of the element. If the arrow is right-aligned, it
    // points towards the right edge of the element, and to the left otherwise.
    int x_optimal_position =
        (int{arrow_} & ArrowMask::CENTER)
            ? anchor_rect.CenterPoint().x() - kVisibleArrowRadius
            : ((int{arrow_} & ArrowMask::RIGHT)
                   ? anchor_rect.right() - kVisibleArrowDiamater
                   : anchor_rect.x());

    // The most left position for the arrow is the left edge of the bubble
    // plus the minimum spacing of the arrow from the edge.
    int leftmost_position_on_bubble = popup_bounds.x() + kVisibleArrowBuffer;

    // Analogous, the most right position is the right side minus the diameter
    // of the arrow and the spacing of the arrow from the edge.
    int rightmost_position_on_bubble =
        popup_bounds.right() - kVisibleArrowDiamater - kVisibleArrowBuffer;

    // If the right-most position is smaller than the left-most position, the
    // bubble's width is not sufficient to add an arrow.
    if (leftmost_position_on_bubble > rightmost_position_on_bubble) {
      // Make the arrow invisible because there is not enough space to show it.
      set_visible_arrow(false);
      return false;
    }

    // Make sure the x position is limited to the range defined by the bubble.
    x_position = std::clamp(x_optimal_position, leftmost_position_on_bubble,
                            rightmost_position_on_bubble);

    // Calculate the y position of the arrow to be either on top of below the
    // bubble.
    y_position = (int{arrow_} & ArrowMask::BOTTOM)
                     ? popup_bounds.bottom()
                     : popup_bounds.y() - kVisibleArrowLength;
  } else {
    // Adjust y position of the popup to keep the arrow pointing exactly in
    // the middle of the anchor element, still respecting
    // the |kVisibleArrowBuffer| restrictions.
    int popup_y_upper_bound = anchor_rect.CenterPoint().y() -
                              (kVisibleArrowRadius + kVisibleArrowBuffer);
    int popup_y_lower_bound = anchor_rect.CenterPoint().y() +
                              (kVisibleArrowRadius + kVisibleArrowBuffer) -
                              popup_bounds.height();

    // The popup height is not enough to accommodate the arrow.
    if (popup_y_upper_bound < popup_y_lower_bound) {
      set_visible_arrow(false);
      return false;
    }

    int popup_y_adjusted =
        std::clamp(popup_bounds.y(), popup_y_lower_bound, popup_y_upper_bound);
    popup_bounds.set_y(popup_y_adjusted);

    // For an horizontal arrow, the x position is either the left or the right
    // edge of the bubble, taking the length of the arrow into account.
    x_position = (int{arrow_} & ArrowMask::RIGHT)
                     ? popup_bounds.right()
                     : popup_bounds.x() - kVisibleArrowLength;

    // Calculate the top- and bottom-most position for the bubble.
    int topmost_position_on_bubble = popup_bounds.y() + kVisibleArrowBuffer;

    int bottommost_position_on_bubble =
        popup_bounds.bottom() - kVisibleArrowDiamater - kVisibleArrowBuffer;

    // If the top-most position is below the bottom-most position, the bubble
    // has not enough height to place an arrow.
    if (topmost_position_on_bubble > bottommost_position_on_bubble) {
      // Make the arrow invisible because there is not enough space to show it.
      set_visible_arrow(false);
      return false;
    }

    // Align the arrow with the horizontal center of the element.
    // Here, there is no differentiation between the different positions of a
    // left or right aligned arrow.
    y_position =
        std::clamp(anchor_rect.CenterPoint().y() - kVisibleArrowRadius,
                   topmost_position_on_bubble, bottommost_position_on_bubble);
  }

  visible_arrow_rect_.set_size(GetVisibleArrowSize(arrow_));
  visible_arrow_rect_.set_origin({x_position, y_position});

  // The arrow is positioned around the popup, but the popup is still in its
  // original position and the arrow may overlap the anchor element. To make
  // the whole tandem visually pointing to the anchor it must be shifted
  // in the opposite direction.
  gfx::Vector2d popup_offset =
      GetContentsBoundsOffsetToPlaceVisibleArrow(arrow_, false);
  popup_bounds.set_origin(popup_bounds.origin() + popup_offset);
  visible_arrow_rect_.set_origin(visible_arrow_rect_.origin() + popup_offset);

  // Adjust positions if the shifted popup violates the min y restrictions.
  int min_y_overlay = popup_min_y - popup_bounds.y();
  if (min_y_overlay > 0) {
    // gfx::Vector2d min_y_offset{0, min_y_overlay};
    popup_bounds.Offset(0, min_y_overlay);
    visible_arrow_rect_.Offset(0, min_y_overlay);
  }

  set_visible_arrow(true);
  return true;
}

void BubbleBorder::CalculateVisibleArrowRect(
    const gfx::Rect& contents_bounds,
    const gfx::Point& anchor_point) const {
  const gfx::Insets insets = GetInsets();

  gfx::Point new_origin;
  switch (GetBubbleArrowSide(arrow_)) {
    case BubbleArrowSide::kTop:
      new_origin =
          gfx::Point(anchor_point.x() - kVisibleArrowRadius + 1,
                     contents_bounds.y() + insets.top() - kVisibleArrowLength);
      break;

    case BubbleArrowSide::kBottom:
      new_origin = gfx::Point(anchor_point.x() - kVisibleArrowRadius + 1,
                              contents_bounds.bottom() - insets.bottom());
      break;

    case BubbleArrowSide::kRight:
      new_origin = gfx::Point(contents_bounds.right() - insets.right(),
                              anchor_point.y() - kVisibleArrowRadius + 1);
      break;

    case BubbleArrowSide::kLeft:
      new_origin =
          gfx::Point(contents_bounds.x() + insets.left() - kVisibleArrowLength,
                     anchor_point.y() - kVisibleArrowRadius + 1);
      break;
  }
  visible_arrow_rect_.set_origin(new_origin);
  visible_arrow_rect_.set_size(GetVisibleArrowSize(arrow_));
}

SkRRect BubbleBorder::GetClientRect(const View& view) const {
  gfx::RectF bounds(view.GetLocalBounds());
  bounds.Inset(gfx::InsetsF(GetInsets()));
  return SkRRect(gfx::RRectF(bounds, rounded_corners_));
}

bool BubbleBorder::ShouldDrawStroke() const {
  return ShouldDrawStrokeForArgs(draw_border_stroke_, md_shadow_elevation_,
                                 shadow_);
}

void BubbleBorder::PaintNoShadow(const View& view, gfx::Canvas* canvas) {
  gfx::ScopedCanvas scoped(canvas);
  canvas->sk_canvas()->clipRRect(GetClientRect(view), SkClipOp::kDifference,
                                 true /*doAntiAlias*/);
  canvas->sk_canvas()->drawColor(SkColors::kTransparent, SkBlendMode::kSrc);
}

void BubbleBorder::PaintVisibleArrow(const View& view, gfx::Canvas* canvas) {
  gfx::Point arrow_origin = visible_arrow_rect_.origin();
  View::ConvertPointFromScreen(&view, &arrow_origin);
  const gfx::Rect arrow_bounds(arrow_origin, visible_arrow_rect_.size());

  // Clip the canvas to a box that's big enough to hold the shadow in every
  // dimension but won't overlap the bubble itself.
  gfx::ScopedCanvas scoped(canvas);
  gfx::Rect clip_rect = arrow_bounds;
  const BubbleArrowSide side = GetBubbleArrowSide(arrow_);
  clip_rect.Inset(gfx::Insets::TLBR(side == BubbleArrowSide::kBottom ? 0 : -2,
                                    side == BubbleArrowSide::kRight ? 0 : -2,
                                    side == BubbleArrowSide::kTop ? 0 : -2,
                                    side == BubbleArrowSide::kLeft ? 0 : -2));
  canvas->ClipRect(clip_rect);

  // Unlike the flags for drawing the border, these are not cached because
  // arrows are currently rare. Should this change over time, we might want to
  // cache these flags, too.
  cc::PaintFlags flags;
  flags.setStrokeCap(cc::PaintFlags::kRound_Cap);

  if (ShouldDrawStroke()) {
    flags.setColor(view.GetColorProvider()->GetColor(ui::kColorBubbleBorder));
    flags.setStyle(cc::PaintFlags::kStroke_Style);
    flags.setStrokeWidth(1.2);
    flags.setAntiAlias(true);
    flags.setLooper(gfx::CreateShadowDrawLooper(GetShadowValues(
        view.GetColorProvider(), md_shadow_elevation_, shadow_)));
    canvas->DrawPath(
        GetVisibleArrowPath(arrow_, arrow_bounds, BubbleArrowPart::kBorder),
        flags);
  }

  flags.setColor(color().ResolveToSkColor(view.GetColorProvider()));
  flags.setStyle(cc::PaintFlags::kFill_Style);
  flags.setStrokeWidth(1.0);
  flags.setAntiAlias(true);
  flags.setLooper(gfx::CreateShadowDrawLooper(
      GetShadowValues(view.GetColorProvider(), md_shadow_elevation_, shadow_)));
  canvas->DrawPath(
      GetVisibleArrowPath(arrow_, arrow_bounds, BubbleArrowPart::kFill), flags);
}

void BubbleBackground::Paint(gfx::Canvas* canvas, views::View* view) const {
  // Fill the contents with a round-rect region to match the border images.
  cc::PaintFlags flags;
  flags.setAntiAlias(true);
  flags.setStyle(cc::PaintFlags::kFill_Style);
  flags.setColor(border_->color().ResolveToSkColor(view->GetColorProvider()));
  gfx::RectF bounds(view->GetLocalBounds());
  bounds.Inset(gfx::InsetsF(border_->GetInsets()));

  canvas->sk_canvas()->drawRRect(
      SkRRect(gfx::RRectF(bounds, border_->rounded_corners())), flags);
}

}  // namespace views

DEFINE_ENUM_CONVERTERS(
    views::BubbleBorder::Arrow,
    {views::BubbleBorder::Arrow::TOP_LEFT, u"TOP_LEFT"},
    {views::BubbleBorder::Arrow::TOP_RIGHT, u"TOP_RIGHT"},
    {views::BubbleBorder::Arrow::BOTTOM_LEFT, u"BOTTOM_LEFT"},
    {views::BubbleBorder::Arrow::BOTTOM_RIGHT, u"BOTTOM_RIGHT"},
    {views::BubbleBorder::Arrow::LEFT_TOP, u"LEFT_TOP"},
    {views::BubbleBorder::Arrow::RIGHT_TOP, u"RIGHT_TOP"},
    {views::BubbleBorder::Arrow::LEFT_BOTTOM, u"LEFT_BOTTOM"},
    {views::BubbleBorder::Arrow::RIGHT_BOTTOM, u"RIGHT_BOTTOM"},
    {views::BubbleBorder::Arrow::TOP_CENTER, u"TOP_CENTER"},
    {views::BubbleBorder::Arrow::BOTTOM_CENTER, u"BOTTOM_CENTER"},
    {views::BubbleBorder::Arrow::LEFT_CENTER, u"LEFT_CENTER"},
    {views::BubbleBorder::Arrow::RIGHT_CENTER, u"RIGHT_CENTER"},
    {views::BubbleBorder::Arrow::NONE, u"NONE"},
    {views::BubbleBorder::Arrow::FLOAT, u"FLOAT"})