1 // Copyright 2016 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/controls/focus_ring.h"
6 
7 #include <memory>
8 #include <utility>
9 
10 #include "base/memory/ptr_util.h"
11 #include "ui/gfx/canvas.h"
12 #include "ui/views/controls/focusable_border.h"
13 #include "ui/views/controls/highlight_path_generator.h"
14 #include "ui/views/style/platform_style.h"
15 #include "ui/views/view_class_properties.h"
16 
17 namespace views {
18 
19 namespace {
20 
IsPathUsable(const SkPath & path)21 bool IsPathUsable(const SkPath& path) {
22   return !path.isEmpty() && (path.isRect(nullptr) || path.isOval(nullptr) ||
23                              path.isRRect(nullptr));
24 }
25 
ColorIdForValidity(bool valid)26 ui::NativeTheme::ColorId ColorIdForValidity(bool valid) {
27   return valid ? ui::NativeTheme::kColorId_FocusedBorderColor
28                : ui::NativeTheme::kColorId_AlertSeverityHigh;
29 }
30 
GetCornerRadius()31 double GetCornerRadius() {
32   double thickness = PlatformStyle::kFocusHaloThickness / 2.f;
33   return FocusableBorder::kCornerRadiusDp + thickness;
34 }
35 
GetHighlightPathInternal(const View * view)36 SkPath GetHighlightPathInternal(const View* view) {
37   HighlightPathGenerator* path_generator =
38       view->GetProperty(kHighlightPathGeneratorKey);
39 
40   if (path_generator) {
41     SkPath highlight_path = path_generator->GetHighlightPath(view);
42     // The generated path might be empty or otherwise unusable. If that's the
43     // case we should fall back on the default path.
44     if (IsPathUsable(highlight_path))
45       return highlight_path;
46   }
47 
48   const double corner_radius = GetCornerRadius();
49   return SkPath().addRRect(SkRRect::MakeRectXY(
50       RectToSkRect(view->GetLocalBounds()), corner_radius, corner_radius));
51 }
52 
53 }  // namespace
54 
55 // static
Install(View * parent)56 std::unique_ptr<FocusRing> FocusRing::Install(View* parent) {
57   auto ring = base::WrapUnique<FocusRing>(new FocusRing());
58   ring->set_owned_by_client();
59   parent->AddChildView(ring.get());
60   ring->InvalidateLayout();
61   ring->SchedulePaint();
62   return ring;
63 }
64 
SetPathGenerator(std::unique_ptr<HighlightPathGenerator> generator)65 void FocusRing::SetPathGenerator(
66     std::unique_ptr<HighlightPathGenerator> generator) {
67   path_generator_ = std::move(generator);
68   SchedulePaint();
69 }
70 
SetInvalid(bool invalid)71 void FocusRing::SetInvalid(bool invalid) {
72   invalid_ = invalid;
73   SchedulePaint();
74 }
75 
SetHasFocusPredicate(const ViewPredicate & predicate)76 void FocusRing::SetHasFocusPredicate(const ViewPredicate& predicate) {
77   has_focus_predicate_ = predicate;
78   RefreshLayer();
79 }
80 
SetColor(base::Optional<SkColor> color)81 void FocusRing::SetColor(base::Optional<SkColor> color) {
82   color_ = color;
83   SchedulePaint();
84 }
85 
Layout()86 void FocusRing::Layout() {
87   // The focus ring handles its own sizing, which is simply to fill the parent
88   // and extend a little beyond its borders.
89   gfx::Rect focus_bounds = parent()->GetLocalBounds();
90   focus_bounds.Inset(gfx::Insets(PlatformStyle::kFocusHaloInset));
91   SetBoundsRect(focus_bounds);
92 
93   // Need to match canvas direction with the parent. This is required to ensure
94   // asymmetric focus ring shapes match their respective buttons in RTL mode.
95   EnableCanvasFlippingForRTLUI(parent()->flip_canvas_on_paint_for_rtl_ui());
96 }
97 
ViewHierarchyChanged(const ViewHierarchyChangedDetails & details)98 void FocusRing::ViewHierarchyChanged(
99     const ViewHierarchyChangedDetails& details) {
100   if (details.child != this)
101     return;
102 
103   if (details.is_add) {
104     // Need to start observing the parent.
105     details.parent->AddObserver(this);
106   } else {
107     // This view is being removed from its parent. It needs to remove itself
108     // from its parent's observer list. Otherwise, since its |parent_| will
109     // become a nullptr, it won't be able to do so in its destructor.
110     details.parent->RemoveObserver(this);
111   }
112   RefreshLayer();
113 }
114 
OnPaint(gfx::Canvas * canvas)115 void FocusRing::OnPaint(gfx::Canvas* canvas) {
116   // TODO(pbos): Reevaluate if this can turn into a DCHECK, e.g. we should
117   // never paint if there's no parent focus.
118   if (has_focus_predicate_) {
119     if (!(*has_focus_predicate_)(parent()))
120       return;
121   } else if (!parent()->HasFocus()) {
122     return;
123   }
124 
125   cc::PaintFlags paint;
126   paint.setAntiAlias(true);
127   paint.setColor(color_.value_or(
128       GetNativeTheme()->GetSystemColor(ColorIdForValidity(!invalid_))));
129   paint.setStyle(cc::PaintFlags::kStroke_Style);
130   paint.setStrokeWidth(PlatformStyle::kFocusHaloThickness);
131 
132   SkPath path;
133   if (path_generator_)
134     path = path_generator_->GetHighlightPath(parent());
135 
136   // If there's no path generator or the generated path is unusable, fall back
137   // to the default.
138   if (!IsPathUsable(path))
139     path = GetHighlightPathInternal(parent());
140 
141   DCHECK(IsPathUsable(path));
142   DCHECK_EQ(flip_canvas_on_paint_for_rtl_ui(),
143             parent()->flip_canvas_on_paint_for_rtl_ui());
144   SkRect bounds;
145   SkRRect rbounds;
146   if (path.isRect(&bounds)) {
147     canvas->sk_canvas()->drawRRect(RingRectFromPathRect(bounds), paint);
148   } else if (path.isOval(&bounds)) {
149     gfx::RectF rect = gfx::SkRectToRectF(bounds);
150     View::ConvertRectToTarget(parent(), this, &rect);
151     canvas->sk_canvas()->drawRRect(SkRRect::MakeOval(gfx::RectFToSkRect(rect)),
152                                    paint);
153   } else if (path.isRRect(&rbounds)) {
154     canvas->sk_canvas()->drawRRect(RingRectFromPathRect(rbounds), paint);
155   }
156 }
157 
OnViewFocused(View * view)158 void FocusRing::OnViewFocused(View* view) {
159   RefreshLayer();
160 }
161 
OnViewBlurred(View * view)162 void FocusRing::OnViewBlurred(View* view) {
163   RefreshLayer();
164 }
165 
FocusRing()166 FocusRing::FocusRing() {
167   // Don't allow the view to process events.
168   set_can_process_events_within_subtree(false);
169 }
170 
~FocusRing()171 FocusRing::~FocusRing() {
172   if (parent())
173     parent()->RemoveObserver(this);
174 }
175 
RefreshLayer()176 void FocusRing::RefreshLayer() {
177   // TODO(pbos): This always keeps the layer alive if |has_focus_predicate_| is
178   // set. This is done because we're not notified when the predicate might
179   // return a different result and there are call sites that call SchedulePaint
180   // on FocusRings and expect that to be sufficient.
181   // The cleanup would be to always call has_focus_predicate_ here and make sure
182   // that RefreshLayer gets called somehow whenever |has_focused_predicate_|
183   // returns a new value.
184   const bool should_paint =
185       has_focus_predicate_.has_value() || (parent() && parent()->HasFocus());
186   SetVisible(should_paint);
187   if (should_paint) {
188     // A layer is necessary to paint beyond the parent's bounds.
189     SetPaintToLayer();
190     layer()->SetFillsBoundsOpaquely(false);
191   } else {
192     DestroyLayer();
193   }
194 }
195 
RingRectFromPathRect(const SkRect & rect) const196 SkRRect FocusRing::RingRectFromPathRect(const SkRect& rect) const {
197   const double corner_radius = GetCornerRadius();
198   return RingRectFromPathRect(
199       SkRRect::MakeRectXY(rect, corner_radius, corner_radius));
200 }
201 
RingRectFromPathRect(const SkRRect & rrect) const202 SkRRect FocusRing::RingRectFromPathRect(const SkRRect& rrect) const {
203   double thickness = PlatformStyle::kFocusHaloThickness / 2.f;
204   gfx::RectF r = gfx::SkRectToRectF(rrect.rect());
205   View::ConvertRectToTarget(parent(), this, &r);
206 
207   SkRRect skr =
208       rrect.makeOffset(r.x() - rrect.rect().x(), r.y() - rrect.rect().y());
209 
210   // The focus indicator should hug the normal border, when present (as in the
211   // case of text buttons). Since it's drawn outside the parent view, increase
212   // the rounding slightly by adding half the ring thickness.
213   skr.inset(PlatformStyle::kFocusHaloInset, PlatformStyle::kFocusHaloInset);
214   skr.inset(thickness, thickness);
215 
216   return skr;
217 }
218 
GetHighlightPath(const View * view)219 SkPath GetHighlightPath(const View* view) {
220   SkPath path = GetHighlightPathInternal(view);
221   if (view->flip_canvas_on_paint_for_rtl_ui() && base::i18n::IsRTL()) {
222     gfx::Point center = view->GetLocalBounds().CenterPoint();
223     SkMatrix flip;
224     flip.setScale(-1, 1, center.x(), center.y());
225     path.transform(flip);
226   }
227   return path;
228 }
229 
230 BEGIN_METADATA(FocusRing)
231 METADATA_PARENT_CLASS(View)
232 END_METADATA()
233 
234 }  // namespace views
235