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