chromium/ash/system/unified/quick_settings_slider.cc

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ash/system/unified/quick_settings_slider.h"

#include "ash/strings/grit/ash_strings.h"
#include "ash/style/ash_color_provider.h"
#include "ash/style/color_util.h"
#include "base/notreached.h"
#include "cc/paint/paint_flags.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.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/rect.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/slider.h"

namespace ash {

using Style = QuickSettingsSlider::Style;

namespace {

// The radius used to draw the rounded empty slider ends.
constexpr float kEmptySliderRoundedRadius = 2.f;
constexpr float kEmptySliderWidth = 2 * kEmptySliderRoundedRadius;

// The radius used to draw the rounded full slider ends.
constexpr float kFullSliderRoundedRadius = 16.f;
constexpr float kFullSliderWidth = 2 * kFullSliderRoundedRadius;

// The radius used to draw the rounded corner for active/inactive slider on the
// audio subpage.
constexpr float kActiveRadioSliderRoundedRadius = 18.f;
constexpr float kInactiveRadioSliderRoundedRadius = 8.f;
constexpr float kRadioSliderWidth = 2 * kActiveRadioSliderRoundedRadius;

// The thickness of the focus ring border.
constexpr int kLineThickness = 2;
// The gap between the focus ring and the slider.
constexpr int kFocusOffset = 2;

// The offset for the slider top padding.
constexpr int kTopPaddingOffset = 4;

float GetSliderRoundedCornerRadius(Style slider_style) {
  switch (slider_style) {
    case Style::kDefault:
    case Style::kDefaultMuted:
      return kFullSliderRoundedRadius;
    case Style::kRadioActive:
    case Style::kRadioActiveMuted:
      return kActiveRadioSliderRoundedRadius;
    case Style::kRadioInactive:
      return kInactiveRadioSliderRoundedRadius;
    default:
      NOTREACHED();
  }
}

float GetSliderWidth(Style slider_style) {
  switch (slider_style) {
    case Style::kDefault:
    case Style::kDefaultMuted:
      return kFullSliderWidth;
    case Style::kRadioActive:
    case Style::kRadioActiveMuted:
    case Style::kRadioInactive:
      return kRadioSliderWidth;
    default:
      NOTREACHED();
  }
}

}  // namespace

QuickSettingsSlider::QuickSettingsSlider(views::SliderListener* listener,
                                         Style slider_style)
    : views::Slider(listener), slider_style_(slider_style) {
  SetValueIndicatorRadius(kFullSliderRoundedRadius);
  SetFocusBehavior(FocusBehavior::ALWAYS);

  GetViewAccessibility().AddAction(ax::mojom::Action::kIncrement);
  GetViewAccessibility().AddAction(ax::mojom::Action::kDecrement);
}

QuickSettingsSlider::~QuickSettingsSlider() = default;

void QuickSettingsSlider::SetSliderStyle(Style style) {
  if (slider_style_ == style)
    return;

  slider_style_ = style;

  if (slider_style_ == Style::kRadioInactive)
    SetFocusBehavior(FocusBehavior::NEVER);

  SchedulePaint();
}

gfx::Rect QuickSettingsSlider::GetInactiveRadioSliderRect() {
  const gfx::Rect content = GetContentsBounds();
  return gfx::Rect(content.x() - kFocusOffset,
                   content.height() / 2 - kRadioSliderWidth / 2 - kFocusOffset +
                       kTopPaddingOffset,
                   content.width() + 2 * kFocusOffset,
                   kRadioSliderWidth + 2 * kFocusOffset);
}

int QuickSettingsSlider::GetInactiveRadioSliderRoundedCornerRadius() {
  return kInactiveRadioSliderRoundedRadius + kFocusOffset;
}

void QuickSettingsSlider::GetAccessibleNodeData(ui::AXNodeData* node_data) {
  View::GetAccessibleNodeData(node_data);
  node_data->role = ax::mojom::Role::kSlider;
  std::u16string volume_level = base::UTF8ToUTF16(
      base::StringPrintf("%d%%", static_cast<int>(GetValue() * 100 + 0.5)));

  if (is_toggleable_volume_slider_) {
    std::u16string message = l10n_util::GetStringFUTF16(
        slider_style_ == Style::kDefaultMuted
            ? IDS_ASH_STATUS_TRAY_VOLUME_SLIDER_MUTED_ACCESSIBILITY_ANNOUNCEMENT
            : IDS_ASH_STATUS_TRAY_VOLUME_SLIDER_ACCESSIBILITY_ANNOUNCEMENT,
        volume_level);

    node_data->SetValue(message);
  } else {
    node_data->SetValue(volume_level);
  }
}

SkColor QuickSettingsSlider::GetThumbColor() const {
  switch (slider_style_) {
    case Style::kDefault:
    case Style::kRadioActive:
      return GetColorProvider()->GetColor(static_cast<ui::ColorId>(
          cros_tokens::kCrosSysSystemPrimaryContainer));
    case Style::kDefaultMuted:
      return GetColorProvider()->GetColor(
          static_cast<ui::ColorId>(cros_tokens::kCrosSysDisabledOpaque));
    case Style::kRadioActiveMuted:
    case Style::kRadioInactive:
      return GetColorProvider()->GetColor(
          static_cast<ui::ColorId>(cros_tokens::kCrosSysDisabledContainer));
    default:
      NOTREACHED();
  }
}

SkColor QuickSettingsSlider::GetTroughColor() const {
  switch (slider_style_) {
    case Style::kDefault:
      return GetColorProvider()->GetColor(
          static_cast<ui::ColorId>(cros_tokens::kCrosSysSystemOnBase));
    case Style::kRadioActive:
      return GetColorProvider()->GetColor(
          static_cast<ui::ColorId>(cros_tokens::kCrosSysHighlightShape));
    case Style::kDefaultMuted:
    case Style::kRadioActiveMuted:
    case Style::kRadioInactive:
      return GetColorProvider()->GetColor(
          static_cast<ui::ColorId>(cros_tokens::kCrosSysDisabledContainer));
    default:
      NOTREACHED();
  }
}

void QuickSettingsSlider::OnPaint(gfx::Canvas* canvas) {
  const gfx::Rect content = GetContentsBounds();
  const float slider_width = GetSliderWidth(slider_style_);
  const float slider_radius = GetSliderRoundedCornerRadius(slider_style_);
  const int width = content.width() - slider_width;
  const int full_width = GetAnimatingValue() * width + slider_width;
  const int x = content.x();
  const int y = content.height() / 2 - slider_width / 2 + kTopPaddingOffset;

  gfx::Rect empty_slider_rect;
  float empty_slider_radius;
  switch (slider_style_) {
    case Style::kDefault:
    case Style::kDefaultMuted: {
      const int empty_width =
          width + kFullSliderRoundedRadius - full_width + kEmptySliderWidth;
      const int x_empty = x + full_width - kEmptySliderRoundedRadius;
      const int y_empty =
          content.height() / 2 - kEmptySliderWidth / 2 + kTopPaddingOffset;

      empty_slider_rect =
          gfx::Rect(x_empty, y_empty, empty_width, kEmptySliderWidth);
      empty_slider_radius = kEmptySliderRoundedRadius;
      break;
    }
    case Style::kRadioActive:
    case Style::kRadioActiveMuted:
    case Style::kRadioInactive: {
      empty_slider_rect = gfx::Rect(x, y, content.width(), kRadioSliderWidth);
      empty_slider_radius = slider_radius;
      break;
    }
    default:
      NOTREACHED();
  }

  cc::PaintFlags slider_flags;
  slider_flags.setAntiAlias(true);

  slider_flags.setColor(GetTroughColor());
  canvas->DrawRoundRect(empty_slider_rect, empty_slider_radius, slider_flags);

  slider_flags.setColor(GetThumbColor());
  canvas->DrawRoundRect(gfx::Rect(x, y, full_width, slider_width),
                        slider_radius, slider_flags);

  // Paints the focusing ring for the slider. It should be painted last to be
  // on the top.
  if (HasFocus()) {
    cc::PaintFlags highlight_border;
    highlight_border.setColor(GetColorProvider()->GetColor(
        static_cast<ui::ColorId>(cros_tokens::kCrosSysPrimary)));
    highlight_border.setAntiAlias(true);
    highlight_border.setStyle(cc::PaintFlags::kStroke_Style);
    highlight_border.setStrokeWidth(kLineThickness);
    canvas->DrawRoundRect(gfx::Rect(x - kFocusOffset, y - kFocusOffset,
                                    full_width + 2 * kFocusOffset,
                                    slider_width + 2 * kFocusOffset),
                          slider_radius + kFocusOffset, highlight_border);
  }
}

void QuickSettingsSlider::OnThemeChanged() {
  views::View::OnThemeChanged();

  // Signals that this view needs to be repainted since `GetColorProvider()` is
  // called in `OnPaint()` and the views system won't know about it.
  SchedulePaint();
}

ReadOnlySlider::ReadOnlySlider(Style slider_style)
    : QuickSettingsSlider(/*listener=*/nullptr, slider_style) {}

ReadOnlySlider::~ReadOnlySlider() = default;

bool ReadOnlySlider::CanAcceptEvent(const ui::Event& event) {
  return false;
}

BEGIN_METADATA(QuickSettingsSlider)
END_METADATA

BEGIN_METADATA(ReadOnlySlider)
END_METADATA

}  // namespace ash