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