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