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/controls/slider.h"
6
7 #include <algorithm>
8 #include <iterator>
9 #include <memory>
10 #include <utility>
11
12 #include "base/check_op.h"
13 #include "base/strings/stringprintf.h"
14 #include "base/strings/utf_string_conversions.h"
15 #include "base/task/current_thread.h"
16 #include "build/build_config.h"
17 #include "cc/paint/paint_flags.h"
18 #include "third_party/skia/include/core/SkCanvas.h"
19 #include "third_party/skia/include/core/SkColor.h"
20 #include "third_party/skia/include/core/SkPaint.h"
21 #include "ui/accessibility/ax_action_data.h"
22 #include "ui/accessibility/ax_enums.mojom.h"
23 #include "ui/accessibility/ax_node_data.h"
24 #include "ui/base/resource/resource_bundle.h"
25 #include "ui/events/event.h"
26 #include "ui/gfx/canvas.h"
27 #include "ui/gfx/geometry/point.h"
28 #include "ui/gfx/geometry/rect.h"
29 #include "ui/native_theme/native_theme.h"
30 #include "ui/views/metadata/metadata_impl_macros.h"
31 #include "ui/views/widget/widget.h"
32
33 namespace views {
34
35 namespace {
36
37 // The thickness of the slider.
38 constexpr int kLineThickness = 2;
39
40 // The radius used to draw rounded slider ends.
41 constexpr float kSliderRoundedRadius = 2.f;
42
43 // The padding used to hide the slider underneath the thumb.
44 constexpr int kSliderPadding = 2;
45
46 // The radius of the thumb and the highlighted thumb of the slider,
47 // respectively.
48 constexpr float kThumbRadius = 4.f;
49 constexpr float kThumbWidth = 2 * kThumbRadius;
50 constexpr float kThumbHighlightRadius = 12.f;
51
GetNearestAllowedValue(const base::flat_set<float> & allowed_values,float suggested_value)52 float GetNearestAllowedValue(const base::flat_set<float>& allowed_values,
53 float suggested_value) {
54 if (allowed_values.empty())
55 return suggested_value;
56
57 const base::flat_set<float>::const_iterator greater =
58 allowed_values.upper_bound(suggested_value);
59 if (greater == allowed_values.end())
60 return *allowed_values.rbegin();
61
62 if (greater == allowed_values.begin())
63 return *allowed_values.cbegin();
64
65 // Select a value nearest to the |suggested_value|.
66 if ((*greater - suggested_value) > (suggested_value - *std::prev(greater)))
67 return *std::prev(greater);
68
69 return *greater;
70 }
71
72 } // namespace
73
Slider(SliderListener * listener)74 Slider::Slider(SliderListener* listener) : listener_(listener) {
75 highlight_animation_.SetSlideDuration(base::TimeDelta::FromMilliseconds(150));
76 SetFlipCanvasOnPaintForRTLUI(true);
77 #if defined(OS_APPLE)
78 SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
79 #else
80 SetFocusBehavior(FocusBehavior::ALWAYS);
81 #endif
82
83 SchedulePaint();
84 }
85
86 Slider::~Slider() = default;
87
GetValue() const88 float Slider::GetValue() const {
89 return value_;
90 }
91
SetValue(float value)92 void Slider::SetValue(float value) {
93 SetValueInternal(value, SliderChangeReason::kByApi);
94 }
95
GetEnableAccessibilityEvents() const96 bool Slider::GetEnableAccessibilityEvents() const {
97 return accessibility_events_enabled_;
98 }
99
SetEnableAccessibilityEvents(bool enabled)100 void Slider::SetEnableAccessibilityEvents(bool enabled) {
101 if (accessibility_events_enabled_ == enabled)
102 return;
103 accessibility_events_enabled_ = enabled;
104 OnPropertyChanged(&accessibility_events_enabled_, kPropertyEffectsNone);
105 }
106
SetRenderingStyle(RenderingStyle style)107 void Slider::SetRenderingStyle(RenderingStyle style) {
108 style_ = style;
109 SchedulePaint();
110 }
111
SetAllowedValues(const base::flat_set<float> * allowed_values)112 void Slider::SetAllowedValues(const base::flat_set<float>* allowed_values) {
113 if (!allowed_values) {
114 allowed_values_.clear();
115 return;
116 }
117 #if DCHECK_IS_ON()
118 // Disallow empty sliders.
119 DCHECK(allowed_values->size());
120 for (const float v : *allowed_values) {
121 // sanity check.
122 DCHECK_GE(v, 0.0f);
123 DCHECK_LE(v, 1.0f);
124 }
125 #endif
126 allowed_values_ = *allowed_values;
127
128 const auto position = allowed_values_.lower_bound(value_);
129 const float new_value = (position == allowed_values_.end())
130 ? *allowed_values_.cbegin()
131 : *position;
132 if (new_value != value_)
133 SetValue(new_value);
134 }
135
GetAnimatingValue() const136 float Slider::GetAnimatingValue() const {
137 return move_animation_ && move_animation_->is_animating()
138 ? move_animation_->CurrentValueBetween(initial_animating_value_,
139 value_)
140 : value_;
141 }
142
SetHighlighted(bool is_highlighted)143 void Slider::SetHighlighted(bool is_highlighted) {
144 if (is_highlighted)
145 highlight_animation_.Show();
146 else
147 highlight_animation_.Hide();
148 }
149
AnimationProgressed(const gfx::Animation * animation)150 void Slider::AnimationProgressed(const gfx::Animation* animation) {
151 if (animation == &highlight_animation_) {
152 thumb_highlight_radius_ =
153 animation->CurrentValueBetween(kThumbRadius, kThumbHighlightRadius);
154 }
155
156 SchedulePaint();
157 }
158
AnimationEnded(const gfx::Animation * animation)159 void Slider::AnimationEnded(const gfx::Animation* animation) {
160 if (animation == move_animation_.get()) {
161 move_animation_.reset();
162 return;
163 }
164 DCHECK_EQ(animation, &highlight_animation_);
165 }
166
SetValueInternal(float value,SliderChangeReason reason)167 void Slider::SetValueInternal(float value, SliderChangeReason reason) {
168 bool old_value_valid = value_is_valid_;
169
170 value_is_valid_ = true;
171 if (value < 0.0)
172 value = 0.0;
173 else if (value > 1.0)
174 value = 1.0;
175 value = GetNearestAllowedValue(allowed_values_, value);
176 if (value_ == value)
177 return;
178 float old_value = value_;
179 value_ = value;
180 if (listener_)
181 listener_->SliderValueChanged(this, value_, old_value, reason);
182
183 if (old_value_valid && base::CurrentThread::Get()) {
184 // Do not animate when setting the value of the slider for the first time.
185 // There is no message-loop when running tests. So we cannot animate then.
186 if (!move_animation_) {
187 initial_animating_value_ = old_value;
188 move_animation_ = std::make_unique<gfx::SlideAnimation>(this);
189 move_animation_->SetSlideDuration(base::TimeDelta::FromMilliseconds(150));
190 move_animation_->Show();
191 }
192 OnPropertyChanged(&value_, kPropertyEffectsNone);
193 } else {
194 OnPropertyChanged(&value_, kPropertyEffectsPaint);
195 }
196
197 if (accessibility_events_enabled_) {
198 if (GetWidget() && GetWidget()->IsVisible()) {
199 DCHECK(!pending_accessibility_value_change_);
200 NotifyAccessibilityEvent(ax::mojom::Event::kValueChanged, true);
201 } else {
202 pending_accessibility_value_change_ = true;
203 }
204 }
205 }
206
PrepareForMove(const int new_x)207 void Slider::PrepareForMove(const int new_x) {
208 // Try to remember the position of the mouse cursor on the button.
209 gfx::Insets inset = GetInsets();
210 gfx::Rect content = GetContentsBounds();
211 float value = GetAnimatingValue();
212
213 const int thumb_x = value * (content.width() - kThumbWidth);
214 const int candidate_x = GetMirroredXInView(new_x - inset.left()) - thumb_x;
215 if (candidate_x >= 0 && candidate_x < kThumbWidth)
216 initial_button_offset_ = candidate_x;
217 else
218 initial_button_offset_ = kThumbRadius;
219 }
220
MoveButtonTo(const gfx::Point & point)221 void Slider::MoveButtonTo(const gfx::Point& point) {
222 const gfx::Insets inset = GetInsets();
223 // Calculate the value.
224 int amount = base::i18n::IsRTL()
225 ? width() - inset.left() - point.x() - initial_button_offset_
226 : point.x() - inset.left() - initial_button_offset_;
227 SetValueInternal(
228 static_cast<float>(amount) / (width() - inset.width() - kThumbWidth),
229 SliderChangeReason::kByUser);
230 }
231
OnSliderDragStarted()232 void Slider::OnSliderDragStarted() {
233 SetHighlighted(true);
234 if (listener_)
235 listener_->SliderDragStarted(this);
236 }
237
OnSliderDragEnded()238 void Slider::OnSliderDragEnded() {
239 SetHighlighted(false);
240 if (listener_)
241 listener_->SliderDragEnded(this);
242 }
243
CalculatePreferredSize() const244 gfx::Size Slider::CalculatePreferredSize() const {
245 constexpr int kSizeMajor = 200;
246 constexpr int kSizeMinor = 40;
247
248 return gfx::Size(std::max(width(), kSizeMajor), kSizeMinor);
249 }
250
OnMousePressed(const ui::MouseEvent & event)251 bool Slider::OnMousePressed(const ui::MouseEvent& event) {
252 if (!event.IsOnlyLeftMouseButton())
253 return false;
254 OnSliderDragStarted();
255 PrepareForMove(event.location().x());
256 MoveButtonTo(event.location());
257 return true;
258 }
259
OnMouseDragged(const ui::MouseEvent & event)260 bool Slider::OnMouseDragged(const ui::MouseEvent& event) {
261 MoveButtonTo(event.location());
262 return true;
263 }
264
OnMouseReleased(const ui::MouseEvent & event)265 void Slider::OnMouseReleased(const ui::MouseEvent& event) {
266 OnSliderDragEnded();
267 }
268
OnKeyPressed(const ui::KeyEvent & event)269 bool Slider::OnKeyPressed(const ui::KeyEvent& event) {
270 int direction = 1;
271 switch (event.key_code()) {
272 case ui::VKEY_LEFT:
273 direction = base::i18n::IsRTL() ? 1 : -1;
274 break;
275 case ui::VKEY_RIGHT:
276 direction = base::i18n::IsRTL() ? -1 : 1;
277 break;
278 case ui::VKEY_UP:
279 direction = 1;
280 break;
281 case ui::VKEY_DOWN:
282 direction = -1;
283 break;
284
285 default:
286 return false;
287 }
288 if (allowed_values_.empty()) {
289 SetValueInternal(value_ + direction * keyboard_increment_,
290 SliderChangeReason::kByUser);
291 } else {
292 // discrete slider.
293 if (direction > 0) {
294 const base::flat_set<float>::const_iterator greater =
295 allowed_values_.upper_bound(value_);
296 SetValueInternal(greater == allowed_values_.cend()
297 ? *allowed_values_.crend()
298 : *greater,
299 SliderChangeReason::kByUser);
300 } else {
301 const base::flat_set<float>::const_iterator lesser =
302 allowed_values_.lower_bound(value_);
303 // Current value must be in the list of allowed values.
304 DCHECK(lesser != allowed_values_.cend());
305 SetValueInternal(lesser == allowed_values_.cbegin()
306 ? *allowed_values_.cbegin()
307 : *std::prev(lesser),
308 SliderChangeReason::kByUser);
309 }
310 }
311 return true;
312 }
313
GetAccessibleNodeData(ui::AXNodeData * node_data)314 void Slider::GetAccessibleNodeData(ui::AXNodeData* node_data) {
315 node_data->role = ax::mojom::Role::kSlider;
316 node_data->SetValue(base::UTF8ToUTF16(
317 base::StringPrintf("%d%%", static_cast<int>(value_ * 100 + 0.5))));
318 node_data->AddAction(ax::mojom::Action::kIncrement);
319 node_data->AddAction(ax::mojom::Action::kDecrement);
320 }
321
HandleAccessibleAction(const ui::AXActionData & action_data)322 bool Slider::HandleAccessibleAction(const ui::AXActionData& action_data) {
323 if (action_data.action == ax::mojom::Action::kIncrement) {
324 SetValueInternal(value_ + keyboard_increment_, SliderChangeReason::kByUser);
325 return true;
326 } else if (action_data.action == ax::mojom::Action::kDecrement) {
327 SetValueInternal(value_ - keyboard_increment_, SliderChangeReason::kByUser);
328 return true;
329 } else {
330 return views::View::HandleAccessibleAction(action_data);
331 }
332 }
333
OnPaint(gfx::Canvas * canvas)334 void Slider::OnPaint(gfx::Canvas* canvas) {
335 // Paint the slider.
336 const gfx::Rect content = GetContentsBounds();
337 const int width = content.width() - kThumbRadius * 2;
338 const int full = GetAnimatingValue() * width;
339 const int empty = width - full;
340 const int y = content.height() / 2 - kLineThickness / 2;
341 const int x = content.x() + full + kThumbRadius;
342
343 cc::PaintFlags slider_flags;
344 slider_flags.setAntiAlias(true);
345 slider_flags.setColor(GetThumbColor());
346 canvas->DrawRoundRect(
347 gfx::Rect(content.x(), y, full - GetSliderExtraPadding(), kLineThickness),
348 kSliderRoundedRadius, slider_flags);
349 slider_flags.setColor(GetTroughColor());
350 canvas->DrawRoundRect(
351 gfx::Rect(x + kThumbRadius + GetSliderExtraPadding(), y,
352 empty - GetSliderExtraPadding(), kLineThickness),
353 kSliderRoundedRadius, slider_flags);
354
355 gfx::Point thumb_center(x, content.height() / 2);
356
357 // Paint the thumb highlight if it exists.
358 const int thumb_highlight_radius =
359 HasFocus() ? kThumbHighlightRadius : thumb_highlight_radius_;
360 if (thumb_highlight_radius > kThumbRadius) {
361 cc::PaintFlags highlight;
362 highlight.setColor(GetTroughColor());
363 highlight.setAntiAlias(true);
364 canvas->DrawCircle(thumb_center, thumb_highlight_radius, highlight);
365 }
366
367 // Paint the thumb of the slider.
368 cc::PaintFlags flags;
369 flags.setColor(GetThumbColor());
370 flags.setAntiAlias(true);
371
372 canvas->DrawCircle(thumb_center, kThumbRadius, flags);
373 }
374
OnFocus()375 void Slider::OnFocus() {
376 View::OnFocus();
377 SchedulePaint();
378 }
379
OnBlur()380 void Slider::OnBlur() {
381 View::OnBlur();
382 SchedulePaint();
383 }
384
VisibilityChanged(View * starting_from,bool is_visible)385 void Slider::VisibilityChanged(View* starting_from, bool is_visible) {
386 if (is_visible)
387 NotifyPendingAccessibilityValueChanged();
388 }
389
AddedToWidget()390 void Slider::AddedToWidget() {
391 if (GetWidget()->IsVisible())
392 NotifyPendingAccessibilityValueChanged();
393 }
394
NotifyPendingAccessibilityValueChanged()395 void Slider::NotifyPendingAccessibilityValueChanged() {
396 if (!pending_accessibility_value_change_)
397 return;
398
399 NotifyAccessibilityEvent(ax::mojom::Event::kValueChanged, true);
400 pending_accessibility_value_change_ = false;
401 }
402
OnGestureEvent(ui::GestureEvent * event)403 void Slider::OnGestureEvent(ui::GestureEvent* event) {
404 switch (event->type()) {
405 // In a multi point gesture only the touch point will generate
406 // an ET_GESTURE_TAP_DOWN event.
407 case ui::ET_GESTURE_TAP_DOWN:
408 OnSliderDragStarted();
409 PrepareForMove(event->location().x());
410 FALLTHROUGH;
411 case ui::ET_GESTURE_SCROLL_BEGIN:
412 case ui::ET_GESTURE_SCROLL_UPDATE:
413 MoveButtonTo(event->location());
414 event->SetHandled();
415 break;
416 case ui::ET_GESTURE_END:
417 MoveButtonTo(event->location());
418 event->SetHandled();
419 if (event->details().touch_points() <= 1)
420 OnSliderDragEnded();
421 break;
422 default:
423 break;
424 }
425 }
426
GetThumbColor() const427 SkColor Slider::GetThumbColor() const {
428 switch (style_) {
429 case RenderingStyle::kDefaultStyle:
430 return GetNativeTheme()->GetSystemColor(
431 ui::NativeTheme::kColorId_SliderThumbDefault);
432 case RenderingStyle::kMinimalStyle:
433 return GetNativeTheme()->GetSystemColor(
434 ui::NativeTheme::kColorId_SliderThumbMinimal);
435 }
436 }
437
GetTroughColor() const438 SkColor Slider::GetTroughColor() const {
439 switch (style_) {
440 case RenderingStyle::kDefaultStyle:
441 return GetNativeTheme()->GetSystemColor(
442 ui::NativeTheme::kColorId_SliderTroughDefault);
443 case RenderingStyle::kMinimalStyle:
444 return GetNativeTheme()->GetSystemColor(
445 ui::NativeTheme::kColorId_SliderTroughMinimal);
446 }
447 }
448
GetSliderExtraPadding() const449 int Slider::GetSliderExtraPadding() const {
450 // Padding is negative when slider style is default so that there is no
451 // separation between slider and thumb.
452 switch (style_) {
453 case RenderingStyle::kDefaultStyle:
454 return -kSliderPadding;
455 case RenderingStyle::kMinimalStyle:
456 return kSliderPadding;
457 }
458 }
459
460 BEGIN_METADATA(Slider, View)
461 ADD_PROPERTY_METADATA(float, Value)
462 ADD_PROPERTY_METADATA(bool, EnableAccessibilityEvents)
463 END_METADATA
464
465 } // namespace views
466