| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/views/controls/slider.h" |
| |
| #include <algorithm> |
| #include <iterator> |
| #include <memory> |
| #include <utility> |
| |
| #include "base/check_op.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/current_thread.h" |
| #include "build/build_config.h" |
| #include "cc/paint/paint_flags.h" |
| #include "third_party/skia/include/core/SkCanvas.h" |
| #include "third_party/skia/include/core/SkColor.h" |
| #include "third_party/skia/include/core/SkPaint.h" |
| #include "ui/accessibility/ax_action_data.h" |
| #include "ui/accessibility/ax_enums.mojom.h" |
| #include "ui/accessibility/ax_node_data.h" |
| #include "ui/base/resource/resource_bundle.h" |
| #include "ui/events/event.h" |
| #include "ui/gfx/canvas.h" |
| #include "ui/gfx/geometry/point.h" |
| #include "ui/gfx/geometry/rect.h" |
| #include "ui/native_theme/native_theme.h" |
| #include "ui/views/metadata/metadata_impl_macros.h" |
| #include "ui/views/widget/widget.h" |
| |
| namespace views { |
| |
| namespace { |
| |
| // The thickness of the slider. |
| constexpr int kLineThickness = 2; |
| |
| // The radius used to draw rounded slider ends. |
| constexpr float kSliderRoundedRadius = 2.f; |
| |
| // The padding used to hide the slider underneath the thumb. |
| constexpr int kSliderPadding = 2; |
| |
| // The radius of the thumb and the highlighted thumb of the slider, |
| // respectively. |
| constexpr float kThumbRadius = 4.f; |
| constexpr float kThumbWidth = 2 * kThumbRadius; |
| constexpr float kThumbHighlightRadius = 12.f; |
| |
| float GetNearestAllowedValue(const base::flat_set<float>& allowed_values, |
| float suggested_value) { |
| if (allowed_values.empty()) |
| return suggested_value; |
| |
| const base::flat_set<float>::const_iterator greater = |
| allowed_values.upper_bound(suggested_value); |
| if (greater == allowed_values.end()) |
| return *allowed_values.rbegin(); |
| |
| if (greater == allowed_values.begin()) |
| return *allowed_values.cbegin(); |
| |
| // Select a value nearest to the |suggested_value|. |
| if ((*greater - suggested_value) > (suggested_value - *std::prev(greater))) |
| return *std::prev(greater); |
| |
| return *greater; |
| } |
| |
| } // namespace |
| |
| Slider::Slider(SliderListener* listener) : listener_(listener) { |
| highlight_animation_.SetSlideDuration(base::TimeDelta::FromMilliseconds(150)); |
| SetFlipCanvasOnPaintForRTLUI(true); |
| #if defined(OS_APPLE) |
| SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY); |
| #else |
| SetFocusBehavior(FocusBehavior::ALWAYS); |
| #endif |
| |
| SchedulePaint(); |
| } |
| |
| Slider::~Slider() = default; |
| |
| float Slider::GetValue() const { |
| return value_; |
| } |
| |
| void Slider::SetValue(float value) { |
| SetValueInternal(value, SliderChangeReason::kByApi); |
| } |
| |
| bool Slider::GetEnableAccessibilityEvents() const { |
| return accessibility_events_enabled_; |
| } |
| |
| void Slider::SetEnableAccessibilityEvents(bool enabled) { |
| if (accessibility_events_enabled_ == enabled) |
| return; |
| accessibility_events_enabled_ = enabled; |
| OnPropertyChanged(&accessibility_events_enabled_, kPropertyEffectsNone); |
| } |
| |
| void Slider::SetRenderingStyle(RenderingStyle style) { |
| style_ = style; |
| SchedulePaint(); |
| } |
| |
| void Slider::SetAllowedValues(const base::flat_set<float>* allowed_values) { |
| if (!allowed_values) { |
| allowed_values_.clear(); |
| return; |
| } |
| #if DCHECK_IS_ON() |
| // Disallow empty sliders. |
| DCHECK(allowed_values->size()); |
| for (const float v : *allowed_values) { |
| // sanity check. |
| DCHECK_GE(v, 0.0f); |
| DCHECK_LE(v, 1.0f); |
| } |
| #endif |
| allowed_values_ = *allowed_values; |
| |
| const auto position = allowed_values_.lower_bound(value_); |
| const float new_value = (position == allowed_values_.end()) |
| ? *allowed_values_.cbegin() |
| : *position; |
| if (new_value != value_) |
| SetValue(new_value); |
| } |
| |
| float Slider::GetAnimatingValue() const { |
| return move_animation_ && move_animation_->is_animating() |
| ? move_animation_->CurrentValueBetween(initial_animating_value_, |
| value_) |
| : value_; |
| } |
| |
| void Slider::SetHighlighted(bool is_highlighted) { |
| if (is_highlighted) |
| highlight_animation_.Show(); |
| else |
| highlight_animation_.Hide(); |
| } |
| |
| void Slider::AnimationProgressed(const gfx::Animation* animation) { |
| if (animation == &highlight_animation_) { |
| thumb_highlight_radius_ = |
| animation->CurrentValueBetween(kThumbRadius, kThumbHighlightRadius); |
| } |
| |
| SchedulePaint(); |
| } |
| |
| void Slider::AnimationEnded(const gfx::Animation* animation) { |
| if (animation == move_animation_.get()) { |
| move_animation_.reset(); |
| return; |
| } |
| DCHECK_EQ(animation, &highlight_animation_); |
| } |
| |
| void Slider::SetValueInternal(float value, SliderChangeReason reason) { |
| bool old_value_valid = value_is_valid_; |
| |
| value_is_valid_ = true; |
| if (value < 0.0) |
| value = 0.0; |
| else if (value > 1.0) |
| value = 1.0; |
| value = GetNearestAllowedValue(allowed_values_, value); |
| if (value_ == value) |
| return; |
| float old_value = value_; |
| value_ = value; |
| if (listener_) |
| listener_->SliderValueChanged(this, value_, old_value, reason); |
| |
| if (old_value_valid && base::CurrentThread::Get()) { |
| // Do not animate when setting the value of the slider for the first time. |
| // There is no message-loop when running tests. So we cannot animate then. |
| if (!move_animation_) { |
| initial_animating_value_ = old_value; |
| move_animation_ = std::make_unique<gfx::SlideAnimation>(this); |
| move_animation_->SetSlideDuration(base::TimeDelta::FromMilliseconds(150)); |
| move_animation_->Show(); |
| } |
| OnPropertyChanged(&value_, kPropertyEffectsNone); |
| } else { |
| OnPropertyChanged(&value_, kPropertyEffectsPaint); |
| } |
| |
| if (accessibility_events_enabled_) { |
| if (GetWidget() && GetWidget()->IsVisible()) { |
| DCHECK(!pending_accessibility_value_change_); |
| NotifyAccessibilityEvent(ax::mojom::Event::kValueChanged, true); |
| } else { |
| pending_accessibility_value_change_ = true; |
| } |
| } |
| } |
| |
| void Slider::PrepareForMove(const int new_x) { |
| // Try to remember the position of the mouse cursor on the button. |
| gfx::Insets inset = GetInsets(); |
| gfx::Rect content = GetContentsBounds(); |
| float value = GetAnimatingValue(); |
| |
| const int thumb_x = value * (content.width() - kThumbWidth); |
| const int candidate_x = GetMirroredXInView(new_x - inset.left()) - thumb_x; |
| if (candidate_x >= 0 && candidate_x < kThumbWidth) |
| initial_button_offset_ = candidate_x; |
| else |
| initial_button_offset_ = kThumbRadius; |
| } |
| |
| void Slider::MoveButtonTo(const gfx::Point& point) { |
| const gfx::Insets inset = GetInsets(); |
| // Calculate the value. |
| int amount = base::i18n::IsRTL() |
| ? width() - inset.left() - point.x() - initial_button_offset_ |
| : point.x() - inset.left() - initial_button_offset_; |
| SetValueInternal( |
| static_cast<float>(amount) / (width() - inset.width() - kThumbWidth), |
| SliderChangeReason::kByUser); |
| } |
| |
| void Slider::OnSliderDragStarted() { |
| SetHighlighted(true); |
| if (listener_) |
| listener_->SliderDragStarted(this); |
| } |
| |
| void Slider::OnSliderDragEnded() { |
| SetHighlighted(false); |
| if (listener_) |
| listener_->SliderDragEnded(this); |
| } |
| |
| gfx::Size Slider::CalculatePreferredSize() const { |
| constexpr int kSizeMajor = 200; |
| constexpr int kSizeMinor = 40; |
| |
| return gfx::Size(std::max(width(), kSizeMajor), kSizeMinor); |
| } |
| |
| bool Slider::OnMousePressed(const ui::MouseEvent& event) { |
| if (!event.IsOnlyLeftMouseButton()) |
| return false; |
| OnSliderDragStarted(); |
| PrepareForMove(event.location().x()); |
| MoveButtonTo(event.location()); |
| return true; |
| } |
| |
| bool Slider::OnMouseDragged(const ui::MouseEvent& event) { |
| MoveButtonTo(event.location()); |
| return true; |
| } |
| |
| void Slider::OnMouseReleased(const ui::MouseEvent& event) { |
| OnSliderDragEnded(); |
| } |
| |
| bool Slider::OnKeyPressed(const ui::KeyEvent& event) { |
| int direction = 1; |
| switch (event.key_code()) { |
| case ui::VKEY_LEFT: |
| direction = base::i18n::IsRTL() ? 1 : -1; |
| break; |
| case ui::VKEY_RIGHT: |
| direction = base::i18n::IsRTL() ? -1 : 1; |
| break; |
| case ui::VKEY_UP: |
| direction = 1; |
| break; |
| case ui::VKEY_DOWN: |
| direction = -1; |
| break; |
| |
| default: |
| return false; |
| } |
| if (allowed_values_.empty()) { |
| SetValueInternal(value_ + direction * keyboard_increment_, |
| SliderChangeReason::kByUser); |
| } else { |
| // discrete slider. |
| if (direction > 0) { |
| const base::flat_set<float>::const_iterator greater = |
| allowed_values_.upper_bound(value_); |
| SetValueInternal(greater == allowed_values_.cend() |
| ? *allowed_values_.crend() |
| : *greater, |
| SliderChangeReason::kByUser); |
| } else { |
| const base::flat_set<float>::const_iterator lesser = |
| allowed_values_.lower_bound(value_); |
| // Current value must be in the list of allowed values. |
| DCHECK(lesser != allowed_values_.cend()); |
| SetValueInternal(lesser == allowed_values_.cbegin() |
| ? *allowed_values_.cbegin() |
| : *std::prev(lesser), |
| SliderChangeReason::kByUser); |
| } |
| } |
| return true; |
| } |
| |
| void Slider::GetAccessibleNodeData(ui::AXNodeData* node_data) { |
| node_data->role = ax::mojom::Role::kSlider; |
| node_data->SetValue(base::UTF8ToUTF16( |
| base::StringPrintf("%d%%", static_cast<int>(value_ * 100 + 0.5)))); |
| node_data->AddAction(ax::mojom::Action::kIncrement); |
| node_data->AddAction(ax::mojom::Action::kDecrement); |
| } |
| |
| bool Slider::HandleAccessibleAction(const ui::AXActionData& action_data) { |
| if (action_data.action == ax::mojom::Action::kIncrement) { |
| SetValueInternal(value_ + keyboard_increment_, SliderChangeReason::kByUser); |
| return true; |
| } else if (action_data.action == ax::mojom::Action::kDecrement) { |
| SetValueInternal(value_ - keyboard_increment_, SliderChangeReason::kByUser); |
| return true; |
| } else { |
| return views::View::HandleAccessibleAction(action_data); |
| } |
| } |
| |
| void Slider::OnPaint(gfx::Canvas* canvas) { |
| // Paint the slider. |
| const gfx::Rect content = GetContentsBounds(); |
| const int width = content.width() - kThumbRadius * 2; |
| const int full = GetAnimatingValue() * width; |
| const int empty = width - full; |
| const int y = content.height() / 2 - kLineThickness / 2; |
| const int x = content.x() + full + kThumbRadius; |
| |
| cc::PaintFlags slider_flags; |
| slider_flags.setAntiAlias(true); |
| slider_flags.setColor(GetThumbColor()); |
| canvas->DrawRoundRect( |
| gfx::Rect(content.x(), y, full - GetSliderExtraPadding(), kLineThickness), |
| kSliderRoundedRadius, slider_flags); |
| slider_flags.setColor(GetTroughColor()); |
| canvas->DrawRoundRect( |
| gfx::Rect(x + kThumbRadius + GetSliderExtraPadding(), y, |
| empty - GetSliderExtraPadding(), kLineThickness), |
| kSliderRoundedRadius, slider_flags); |
| |
| gfx::Point thumb_center(x, content.height() / 2); |
| |
| // Paint the thumb highlight if it exists. |
| const int thumb_highlight_radius = |
| HasFocus() ? kThumbHighlightRadius : thumb_highlight_radius_; |
| if (thumb_highlight_radius > kThumbRadius) { |
| cc::PaintFlags highlight_background; |
| highlight_background.setColor(GetTroughColor()); |
| highlight_background.setAntiAlias(true); |
| canvas->DrawCircle(thumb_center, thumb_highlight_radius, |
| highlight_background); |
| |
| cc::PaintFlags highlight_border; |
| highlight_border.setColor(GetThumbColor()); |
| highlight_border.setAntiAlias(true); |
| highlight_border.setStyle(cc::PaintFlags::kStroke_Style); |
| highlight_border.setStrokeWidth(kLineThickness); |
| canvas->DrawCircle(thumb_center, thumb_highlight_radius, highlight_border); |
| } |
| |
| // Paint the thumb of the slider. |
| cc::PaintFlags flags; |
| flags.setColor(GetThumbColor()); |
| flags.setAntiAlias(true); |
| |
| canvas->DrawCircle(thumb_center, kThumbRadius, flags); |
| } |
| |
| void Slider::OnFocus() { |
| View::OnFocus(); |
| SchedulePaint(); |
| } |
| |
| void Slider::OnBlur() { |
| View::OnBlur(); |
| SchedulePaint(); |
| } |
| |
| void Slider::VisibilityChanged(View* starting_from, bool is_visible) { |
| if (is_visible) |
| NotifyPendingAccessibilityValueChanged(); |
| } |
| |
| void Slider::AddedToWidget() { |
| if (GetWidget()->IsVisible()) |
| NotifyPendingAccessibilityValueChanged(); |
| } |
| |
| void Slider::NotifyPendingAccessibilityValueChanged() { |
| if (!pending_accessibility_value_change_) |
| return; |
| |
| NotifyAccessibilityEvent(ax::mojom::Event::kValueChanged, true); |
| pending_accessibility_value_change_ = false; |
| } |
| |
| void Slider::OnGestureEvent(ui::GestureEvent* event) { |
| switch (event->type()) { |
| // In a multi point gesture only the touch point will generate |
| // an ET_GESTURE_TAP_DOWN event. |
| case ui::ET_GESTURE_TAP_DOWN: |
| OnSliderDragStarted(); |
| PrepareForMove(event->location().x()); |
| FALLTHROUGH; |
| case ui::ET_GESTURE_SCROLL_BEGIN: |
| case ui::ET_GESTURE_SCROLL_UPDATE: |
| MoveButtonTo(event->location()); |
| event->SetHandled(); |
| break; |
| case ui::ET_GESTURE_END: |
| MoveButtonTo(event->location()); |
| event->SetHandled(); |
| if (event->details().touch_points() <= 1) |
| OnSliderDragEnded(); |
| break; |
| default: |
| break; |
| } |
| } |
| |
| SkColor Slider::GetThumbColor() const { |
| switch (style_) { |
| case RenderingStyle::kDefaultStyle: |
| return GetNativeTheme()->GetSystemColor( |
| ui::NativeTheme::kColorId_SliderThumbDefault); |
| case RenderingStyle::kMinimalStyle: |
| return GetNativeTheme()->GetSystemColor( |
| ui::NativeTheme::kColorId_SliderThumbMinimal); |
| } |
| } |
| |
| SkColor Slider::GetTroughColor() const { |
| switch (style_) { |
| case RenderingStyle::kDefaultStyle: |
| return GetNativeTheme()->GetSystemColor( |
| ui::NativeTheme::kColorId_SliderTroughDefault); |
| case RenderingStyle::kMinimalStyle: |
| return GetNativeTheme()->GetSystemColor( |
| ui::NativeTheme::kColorId_SliderTroughMinimal); |
| } |
| } |
| |
| int Slider::GetSliderExtraPadding() const { |
| // Padding is negative when slider style is default so that there is no |
| // separation between slider and thumb. |
| switch (style_) { |
| case RenderingStyle::kDefaultStyle: |
| return -kSliderPadding; |
| case RenderingStyle::kMinimalStyle: |
| return kSliderPadding; |
| } |
| } |
| |
| BEGIN_METADATA(Slider, View) |
| ADD_PROPERTY_METADATA(float, Value) |
| ADD_PROPERTY_METADATA(bool, EnableAccessibilityEvents) |
| END_METADATA |
| |
| } // namespace views |