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