1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 #include "ui/views/bubble/bubble_border.h"
6 
7 #include <algorithm>
8 #include <map>
9 #include <tuple>
10 #include <utility>
11 #include <vector>
12 
13 #include "base/check_op.h"
14 #include "base/no_destructor.h"
15 #include "base/notreached.h"
16 #include "cc/paint/paint_flags.h"
17 #include "third_party/skia/include/core/SkDrawLooper.h"
18 #include "third_party/skia/include/core/SkPath.h"
19 #include "ui/base/resource/resource_bundle.h"
20 #include "ui/gfx/color_palette.h"
21 #include "ui/gfx/geometry/rect.h"
22 #include "ui/gfx/scoped_canvas.h"
23 #include "ui/gfx/shadow_value.h"
24 #include "ui/gfx/skia_paint_util.h"
25 #include "ui/resources/grit/ui_resources.h"
26 #include "ui/views/layout/layout_provider.h"
27 #include "ui/views/painter.h"
28 #include "ui/views/resources/grit/views_resources.h"
29 #include "ui/views/view.h"
30 
31 namespace views {
32 
33 namespace {
34 
35 // GetShadowValues and GetBorderAndShadowFlags cache their results. The shadow
36 // values depend on both the shadow elevation and color, so we create a tuple to
37 // key the cache.
38 using ShadowCacheKey = std::tuple<int, SkColor>;
39 
40 // Utility functions for getting alignment points on the edge of a rectangle.
CenterTop(const gfx::Rect & rect)41 gfx::Point CenterTop(const gfx::Rect& rect) {
42   return gfx::Point(rect.CenterPoint().x(), rect.y());
43 }
44 
CenterBottom(const gfx::Rect & rect)45 gfx::Point CenterBottom(const gfx::Rect& rect) {
46   return gfx::Point(rect.CenterPoint().x(), rect.bottom());
47 }
48 
LeftCenter(const gfx::Rect & rect)49 gfx::Point LeftCenter(const gfx::Rect& rect) {
50   return gfx::Point(rect.x(), rect.CenterPoint().y());
51 }
52 
RightCenter(const gfx::Rect & rect)53 gfx::Point RightCenter(const gfx::Rect& rect) {
54   return gfx::Point(rect.right(), rect.CenterPoint().y());
55 }
56 
57 }  // namespace
58 
BubbleBorder(Arrow arrow,Shadow shadow,SkColor color)59 BubbleBorder::BubbleBorder(Arrow arrow, Shadow shadow, SkColor color)
60     : arrow_(arrow),
61       arrow_offset_(0),
62       shadow_(shadow),
63       background_color_(color),
64       use_theme_background_color_(false) {
65   DCHECK(shadow_ < SHADOW_COUNT);
66 }
67 
68 BubbleBorder::~BubbleBorder() = default;
69 
70 // static
GetBorderAndShadowInsets(base::Optional<int> elevation)71 gfx::Insets BubbleBorder::GetBorderAndShadowInsets(
72     base::Optional<int> elevation) {
73   // Borders with custom shadow elevations do not draw the 1px border.
74   if (elevation.has_value())
75     return -gfx::ShadowValue::GetMargin(GetShadowValues(elevation));
76 
77   constexpr gfx::Insets blur(kShadowBlur + kBorderThicknessDip);
78   constexpr gfx::Insets offset(-kShadowVerticalOffset, 0, kShadowVerticalOffset,
79                                0);
80   return blur + offset;
81 }
82 
SetCornerRadius(int corner_radius)83 void BubbleBorder::SetCornerRadius(int corner_radius) {
84   corner_radius_ = corner_radius;
85 }
86 
GetBounds(const gfx::Rect & anchor_rect,const gfx::Size & contents_size) const87 gfx::Rect BubbleBorder::GetBounds(const gfx::Rect& anchor_rect,
88                                   const gfx::Size& contents_size) const {
89   // In MD, there are no arrows, so positioning logic is significantly simpler.
90   if (has_arrow(arrow_)) {
91     gfx::Rect contents_bounds(contents_size);
92     // Always apply the border part of the inset before calculating coordinates,
93     // that ensures the bubble's border is aligned with the anchor's border.
94     // For the purposes of positioning, the border is rounded up to a dip, which
95     // may cause misalignment in scale factors greater than 1.
96     // TODO(estade): when it becomes possible to provide px bounds instead of
97     // dip bounds, fix this.
98     // Borders with custom shadow elevations do not draw the 1px border.
99     const gfx::Insets border_insets =
100         shadow_ == NO_ASSETS || md_shadow_elevation_.has_value()
101             ? gfx::Insets()
102             : gfx::Insets(kBorderThicknessDip);
103     const gfx::Insets shadow_insets = GetInsets() - border_insets;
104     contents_bounds.Inset(-border_insets);
105     // If |avoid_shadow_overlap_| is true, the shadow part of the inset is also
106     // applied now, to ensure that the shadow itself doesn't overlap the anchor.
107     if (avoid_shadow_overlap_)
108       contents_bounds.Inset(-shadow_insets);
109     switch (arrow_) {
110       case TOP_LEFT:
111         contents_bounds += anchor_rect.bottom_left() - contents_bounds.origin();
112         break;
113       case TOP_RIGHT:
114         contents_bounds +=
115             anchor_rect.bottom_right() - contents_bounds.top_right();
116         break;
117       case BOTTOM_LEFT:
118         contents_bounds += anchor_rect.origin() - contents_bounds.bottom_left();
119         break;
120       case BOTTOM_RIGHT:
121         contents_bounds +=
122             anchor_rect.top_right() - contents_bounds.bottom_right();
123         break;
124       case LEFT_TOP:
125         contents_bounds += anchor_rect.top_right() - contents_bounds.origin();
126         break;
127       case RIGHT_TOP:
128         contents_bounds += anchor_rect.origin() - contents_bounds.top_right();
129         break;
130       case LEFT_BOTTOM:
131         contents_bounds +=
132             anchor_rect.bottom_right() - contents_bounds.bottom_left();
133         break;
134       case RIGHT_BOTTOM:
135         contents_bounds +=
136             anchor_rect.bottom_left() - contents_bounds.bottom_right();
137         break;
138       case TOP_CENTER:
139         contents_bounds +=
140             CenterBottom(anchor_rect) - CenterTop(contents_bounds);
141         break;
142       case BOTTOM_CENTER:
143         contents_bounds +=
144             CenterTop(anchor_rect) - CenterBottom(contents_bounds);
145         break;
146       case LEFT_CENTER:
147         contents_bounds +=
148             RightCenter(anchor_rect) - LeftCenter(contents_bounds);
149         break;
150       case RIGHT_CENTER:
151         contents_bounds +=
152             LeftCenter(anchor_rect) - RightCenter(contents_bounds);
153         break;
154       default:
155         NOTREACHED();
156     }
157     // With NO_ASSETS, there should be further insets, but the same logic is
158     // used to position the bubble origin according to |anchor_rect|.
159     DCHECK((shadow_ != NO_ASSETS && shadow_ != NO_SHADOW) ||
160            insets_.has_value() || shadow_insets.IsEmpty());
161     if (!avoid_shadow_overlap_)
162       contents_bounds.Inset(-shadow_insets);
163     // |arrow_offset_| is used to adjust bubbles that would normally be
164     // partially offscreen.
165     if (is_arrow_on_horizontal(arrow_))
166       contents_bounds += gfx::Vector2d(-arrow_offset_, 0);
167     else
168       contents_bounds += gfx::Vector2d(0, -arrow_offset_);
169     return contents_bounds;
170   }
171 
172   int x = anchor_rect.x();
173   int y = anchor_rect.y();
174   int w = anchor_rect.width();
175   int h = anchor_rect.height();
176   const gfx::Size size(GetSizeForContentsSize(contents_size));
177   const int stroke_width = shadow_ == NO_ASSETS ? 0 : kStroke;
178 
179   // Calculate the bubble coordinates based on the border and arrow settings.
180   if (is_arrow_on_horizontal(arrow_)) {
181     if (is_arrow_on_left(arrow_)) {
182       x += stroke_width;
183     } else if (is_arrow_at_center(arrow_)) {
184       x += w / 2;
185     } else {
186       x += w - size.width() - stroke_width;
187     }
188     y += is_arrow_on_top(arrow_) ? h : -size.height();
189   } else if (has_arrow(arrow_)) {
190     x += is_arrow_on_left(arrow_) ? w : -size.width();
191     if (is_arrow_on_top(arrow_)) {
192       y += stroke_width;
193     } else if (is_arrow_at_center(arrow_)) {
194       y += h / 2;
195     } else {
196       y += h - size.height() - stroke_width;
197     }
198   } else {
199     x += (w - size.width()) / 2;
200     y += (arrow_ == NONE) ? h : (h - size.height()) / 2;
201   }
202 
203   return gfx::Rect(x, y, size.width(), size.height());
204 }
205 
Paint(const views::View & view,gfx::Canvas * canvas)206 void BubbleBorder::Paint(const views::View& view, gfx::Canvas* canvas) {
207   if (shadow_ == NO_ASSETS)
208     return PaintNoAssets(view, canvas);
209 
210   if (shadow_ == NO_SHADOW)
211     return PaintNoShadow(view, canvas);
212 
213   gfx::ScopedCanvas scoped(canvas);
214 
215   SkRRect r_rect = GetClientRect(view);
216   canvas->sk_canvas()->clipRRect(r_rect, SkClipOp::kDifference,
217                                  true /*doAntiAlias*/);
218 
219   DrawBorderAndShadow(std::move(r_rect), &cc::PaintCanvas::drawRRect, canvas,
220                       md_shadow_elevation_, md_shadow_color_);
221 }
222 
GetInsets() const223 gfx::Insets BubbleBorder::GetInsets() const {
224   if (insets_.has_value())
225     return insets_.value();
226   if (shadow_ == NO_ASSETS)
227     return gfx::Insets();
228   if (shadow_ == NO_SHADOW)
229     return gfx::Insets(kBorderThicknessDip);
230   return GetBorderAndShadowInsets(md_shadow_elevation_);
231 }
232 
GetMinimumSize() const233 gfx::Size BubbleBorder::GetMinimumSize() const {
234   return GetSizeForContentsSize(gfx::Size());
235 }
236 
237 // static
GetShadowValues(base::Optional<int> elevation,SkColor color)238 const gfx::ShadowValues& BubbleBorder::GetShadowValues(
239     base::Optional<int> elevation,
240     SkColor color) {
241   // The shadows are always the same for any elevation and color combination, so
242   // construct them once and cache.
243   static base::NoDestructor<std::map<ShadowCacheKey, gfx::ShadowValues>>
244       shadow_map;
245   ShadowCacheKey key(elevation.value_or(-1), color);
246 
247   if (shadow_map->find(key) != shadow_map->end())
248     return shadow_map->find(key)->second;
249 
250   gfx::ShadowValues shadows;
251   if (elevation.has_value()) {
252     DCHECK_GE(elevation.value(), 0);
253     shadows = LayoutProvider::Get()->MakeShadowValues(elevation.value(), color);
254   } else {
255     constexpr int kSmallShadowVerticalOffset = 2;
256     constexpr int kSmallShadowBlur = 4;
257     SkColor kSmallShadowColor = SkColorSetA(color, 0x33);
258     SkColor kLargeShadowColor = SkColorSetA(color, 0x1A);
259     // gfx::ShadowValue counts blur pixels both inside and outside the shape,
260     // whereas these blur values only describe the outside portion, hence they
261     // must be doubled.
262     shadows = gfx::ShadowValues({
263         {gfx::Vector2d(0, kSmallShadowVerticalOffset), 2 * kSmallShadowBlur,
264          kSmallShadowColor},
265         {gfx::Vector2d(0, kShadowVerticalOffset), 2 * kShadowBlur,
266          kLargeShadowColor},
267     });
268   }
269 
270   shadow_map->insert({key, shadows});
271   return shadow_map->find(key)->second;
272 }
273 
274 // static
GetBorderAndShadowFlags(base::Optional<int> elevation,SkColor color)275 const cc::PaintFlags& BubbleBorder::GetBorderAndShadowFlags(
276     base::Optional<int> elevation,
277     SkColor color) {
278   // The flags are always the same for any elevation and color combination, so
279   // construct them once and cache.
280   static base::NoDestructor<std::map<ShadowCacheKey, cc::PaintFlags>> flag_map;
281   ShadowCacheKey key(elevation.value_or(-1), color);
282 
283   if (flag_map->find(key) != flag_map->end())
284     return flag_map->find(key)->second;
285 
286   cc::PaintFlags flags;
287   constexpr SkColor kBlurredBorderColor = SkColorSetA(SK_ColorBLACK, 0x26);
288   flags.setColor(kBlurredBorderColor);
289   flags.setAntiAlias(true);
290   flags.setLooper(
291       gfx::CreateShadowDrawLooper(GetShadowValues(elevation, color)));
292 
293   flag_map->insert({key, flags});
294   return flag_map->find(key)->second;
295 }
296 
GetSizeForContentsSize(const gfx::Size & contents_size) const297 gfx::Size BubbleBorder::GetSizeForContentsSize(
298     const gfx::Size& contents_size) const {
299   // Enlarge the contents size by the thickness of the border images.
300   gfx::Size size(contents_size);
301   const gfx::Insets insets = GetInsets();
302   size.Enlarge(insets.width(), insets.height());
303   return size;
304 }
305 
GetClientRect(const View & view) const306 SkRRect BubbleBorder::GetClientRect(const View& view) const {
307   gfx::RectF bounds(view.GetLocalBounds());
308   bounds.Inset(GetInsets());
309   return SkRRect::MakeRectXY(gfx::RectFToSkRect(bounds), corner_radius(),
310                              corner_radius());
311 }
312 
PaintNoAssets(const View & view,gfx::Canvas * canvas)313 void BubbleBorder::PaintNoAssets(const View& view, gfx::Canvas* canvas) {
314   gfx::ScopedCanvas scoped(canvas);
315   canvas->sk_canvas()->clipRRect(GetClientRect(view), SkClipOp::kDifference,
316                                  true /*doAntiAlias*/);
317   canvas->sk_canvas()->drawColor(SK_ColorTRANSPARENT, SkBlendMode::kSrc);
318 }
319 
PaintNoShadow(const View & view,gfx::Canvas * canvas)320 void BubbleBorder::PaintNoShadow(const View& view, gfx::Canvas* canvas) {
321   gfx::RectF bounds(view.GetLocalBounds());
322   bounds.Inset(gfx::InsetsF(kBorderThicknessDip / 2.0f));
323   cc::PaintFlags flags;
324   flags.setAntiAlias(true);
325   flags.setStyle(cc::PaintFlags::kStroke_Style);
326   flags.setStrokeWidth(kBorderThicknessDip);
327   SkColor kBorderColor = view.GetNativeTheme()->GetSystemColor(
328       ui::NativeTheme::kColorId_BubbleBorder);
329   flags.setColor(kBorderColor);
330   canvas->DrawRoundRect(bounds, corner_radius(), flags);
331 }
332 
Paint(gfx::Canvas * canvas,views::View * view) const333 void BubbleBackground::Paint(gfx::Canvas* canvas, views::View* view) const {
334   if (border_->shadow() == BubbleBorder::NO_SHADOW_OPAQUE_BORDER)
335     canvas->DrawColor(border_->background_color());
336 
337   // Fill the contents with a round-rect region to match the border images.
338   cc::PaintFlags flags;
339   flags.setAntiAlias(true);
340   flags.setStyle(cc::PaintFlags::kFill_Style);
341   flags.setColor(border_->background_color());
342   gfx::RectF bounds(view->GetLocalBounds());
343   bounds.Inset(gfx::InsetsF(border_->GetInsets()));
344 
345   canvas->DrawRoundRect(bounds, border_->corner_radius(), flags);
346 }
347 
348 }  // namespace views
349