blob: 5536a302993856f3a48e5a284649fb270c58b582 [file] [log] [blame]
// Copyright 2012 The Chromium Authors
// 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/i18n/rtl.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/metadata/metadata_impl_macros.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/color/color_id.h"
#include "ui/color/color_provider.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/views/accessibility/view_accessibility.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 highlighted thumb of the slider
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::Milliseconds(150));
SetFlipCanvasOnPaintForRTLUI(true);
#if BUILDFLAG(IS_MAC)
SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
#else
SetFocusBehavior(FocusBehavior::ALWAYS);
#endif
SchedulePaint();
GetViewAccessibility().SetRole(ax::mojom::Role::kSlider);
GetViewAccessibility().AddAction(ax::mojom::Action::kIncrement);
GetViewAccessibility().AddAction(ax::mojom::Action::kDecrement);
}
Slider::~Slider() = default;
float Slider::GetValue() const {
return value_;
}
void Slider::SetValue(float value) {
SetValueInternal(value, SliderChangeReason::kByApi);
}
float Slider::GetValueIndicatorRadius() const {
return value_indicator_radius_;
}
void Slider::SetValueIndicatorRadius(float radius) {
value_indicator_radius_ = radius;
}
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;
UpdateAccessibleValue();
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::Milliseconds(150));
move_animation_->Show();
}
OnPropertyChanged(&value_, kPropertyEffectsNone);
} else {
OnPropertyChanged(&value_, kPropertyEffectsPaint);
}
if (accessibility_events_enabled_) {
if (GetWidget() && GetWidget()->IsVisible() && GetVisible()) {
DCHECK(!pending_accessibility_value_change_);
NotifyAccessibilityEventDeprecated(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() - 2 * value_indicator_radius_);
const int candidate_x = GetMirroredXInView(new_x - inset.left()) - thumb_x;
if (candidate_x >= value_indicator_radius_ - kThumbRadius &&
candidate_x < value_indicator_radius_ + kThumbRadius) {
initial_button_offset_ = candidate_x;
} else {
initial_button_offset_ = value_indicator_radius_;
}
}
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() - 2 * value_indicator_radius_),
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 SizeBounds& available_size) const {
constexpr int kSizeMajor = 200;
constexpr int kSizeMinor = 40;
return gfx::Size(std::max(available_size.width().value_or(0), 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;
}
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 && GetWidget() && GetWidget()->IsVisible() && GetVisible()) {
ApplyPendingAccessibleValueUpdate();
}
}
void Slider::AddedToWidget() {
if (GetWidget()->IsVisible() && GetVisible()) {
ApplyPendingAccessibleValueUpdate();
}
}
void Slider::ApplyPendingAccessibleValueUpdate() {
if (!pending_accessibility_value_change_)
return;
NotifyAccessibilityEventDeprecated(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 EventType::kGestureTapDown event.
case ui::EventType::kGestureTapDown:
OnSliderDragStarted();
PrepareForMove(event->location().x());
[[fallthrough]];
case ui::EventType::kGestureScrollBegin:
case ui::EventType::kGestureScrollUpdate:
MoveButtonTo(event->location());
event->SetHandled();
break;
case ui::EventType::kGestureEnd:
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 GetColorProvider()->GetColor(ui::kColorSliderThumb);
case RenderingStyle::kMinimalStyle:
return GetColorProvider()->GetColor(ui::kColorSliderThumbMinimal);
}
}
SkColor Slider::GetTroughColor() const {
switch (style_) {
case RenderingStyle::kDefaultStyle:
return GetColorProvider()->GetColor(ui::kColorSliderTrack);
case RenderingStyle::kMinimalStyle:
return GetColorProvider()->GetColor(ui::kColorSliderTrackMinimal);
}
}
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;
}
}
void Slider::UpdateAccessibleValue() {
views::ScopedAccessibilityEventBlocker scoped_event_blocker(
GetViewAccessibility());
GetViewAccessibility().SetValue(base::UTF8ToUTF16(
base::StringPrintf("%d%%", static_cast<int>(value_ * 100 + 0.5))));
}
BEGIN_METADATA(Slider)
ADD_PROPERTY_METADATA(float, Value)
ADD_PROPERTY_METADATA(bool, EnableAccessibilityEvents)
ADD_PROPERTY_METADATA(float, ValueIndicatorRadius)
END_METADATA
} // namespace views