chromium/ash/system/audio/labeled_slider_view.cc

// Copyright 2024 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/audio/labeled_slider_view.h"

#include <memory>
#include <optional>
#include <string>
#include <utility>

#include "ash/strings/grit/ash_strings.h"
#include "ash/style/typography.h"
#include "ash/system/audio/audio_detailed_view_utils.h"
#include "ash/system/tray/hover_highlight_view.h"
#include "ash/system/tray/tray_detailed_view.h"
#include "ash/system/tray/tray_popup_utils.h"
#include "ash/system/unified/unified_slider_view.h"
#include "chromeos/ash/components/audio/audio_device.h"
#include "chromeos/ash/components/audio/cras_audio_handler.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/compositor/layer.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/gfx/geometry/rrect_f.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/label.h"
#include "ui/views/view_utils.h"

namespace ash {

namespace {

constexpr auto kDevicesNameViewPreferredSize = gfx::Size(0, 44);
constexpr auto kDevicesTriViewInsets = gfx::Insets::TLBR(0, 24, 0, 32);
constexpr auto kDevicesTriViewBorder = gfx::Insets::VH(0, 4);
constexpr auto kWideDevicesSliderBorder = gfx::Insets::VH(4, 8);
constexpr auto kWideDevicesTriViewInsets = gfx::Insets::TLBR(0, 8, 0, 16);

// Returns the corresponding device name based on the passed in `device`.
std::u16string GetAudioDeviceName(const AudioDevice& device) {
  switch (device.type) {
    case AudioDeviceType::kBluetooth:
    case AudioDeviceType::kBluetoothNbMic:
      return l10n_util::GetStringFUTF16(
          IDS_ASH_STATUS_TRAY_AUDIO_BLUETOOTH_DEVICE,
          base::UTF8ToUTF16(device.display_name));
    case AudioDeviceType::kFrontMic:
      return l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_AUDIO_FRONT_MIC);
    case AudioDeviceType::kHeadphone:
      return l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_AUDIO_HEADPHONE);
    case AudioDeviceType::kHdmi:
      return l10n_util::GetStringFUTF16(IDS_ASH_STATUS_TRAY_AUDIO_HDMI_DEVICE,
                                        base::UTF8ToUTF16(device.display_name));
    case AudioDeviceType::kInternalMic:
      return l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_AUDIO_INTERNAL_MIC);
    case AudioDeviceType::kInternalSpeaker:
      return l10n_util::GetStringUTF16(
          IDS_ASH_STATUS_TRAY_AUDIO_INTERNAL_SPEAKER);
    case AudioDeviceType::kMic:
      return l10n_util::GetStringUTF16(
          IDS_ASH_STATUS_TRAY_AUDIO_MIC_JACK_DEVICE);
    case AudioDeviceType::kRearMic:
      return l10n_util::GetStringUTF16(IDS_ASH_STATUS_TRAY_AUDIO_REAR_MIC);
    case AudioDeviceType::kUsb:
      return l10n_util::GetStringFUTF16(IDS_ASH_STATUS_TRAY_AUDIO_USB_DEVICE,
                                        base::UTF8ToUTF16(device.display_name));
    case AudioDeviceType::kAlsaLoopback:
    case AudioDeviceType::kHotword:
    case AudioDeviceType::kKeyboardMic:
    case AudioDeviceType::kLineout:
    case AudioDeviceType::kPostDspLoopback:
    case AudioDeviceType::kPostMixLoopback:
    case AudioDeviceType::kOther:
      return base::UTF8ToUTF16(device.display_name);
  }
}

// Customizes the highlight path so that the device container view's focus ring
// follows the slider's size instead of its own size.
// NOTE: This class requires a valid slider during its life cycle.
class DeviceNameContainerHighlightPathGenerator
    : public views::HighlightPathGenerator {
 public:
  DeviceNameContainerHighlightPathGenerator(QuickSettingsSlider* slider,
                                            bool is_wide_slider)
      : slider_(slider), is_wide_slider_(is_wide_slider) {
    CHECK(slider);
  }
  DeviceNameContainerHighlightPathGenerator(
      const DeviceNameContainerHighlightPathGenerator&) = delete;
  DeviceNameContainerHighlightPathGenerator& operator=(
      const DeviceNameContainerHighlightPathGenerator&) = delete;
  ~DeviceNameContainerHighlightPathGenerator() override = default;

 private:
  // HighlightPathGenerator:
  std::optional<gfx::RRectF> GetRoundRect(const gfx::RectF& rect) override {
    const gfx::Rect slider_bounds = slider_->GetInactiveRadioSliderRect();
    const gfx::RectF bounds(
        slider_bounds.x() +
            (is_wide_slider_ ? 0 : kRadioSliderViewPadding.left()),
        slider_bounds.y(), slider_bounds.width(), slider_bounds.height());
    const gfx::RoundedCornersF rounded(
        slider_->GetInactiveRadioSliderRoundedCornerRadius());
    return gfx::RRectF(bounds, rounded);
  }

  const raw_ptr<QuickSettingsSlider> slider_;
  const bool is_wide_slider_;
};

}  // namespace

LabeledSliderView::LabeledSliderView(TrayDetailedView* detailed_view,
                                     std::unique_ptr<views::View> slider_view,
                                     const AudioDevice& device,
                                     bool is_wide_slider)
    : is_wide_slider_(is_wide_slider) {
  SetUseDefaultFillLayout(true);

  // TODO (b/319941708): Remove this work-around after this bug is fixed.
  // Adding a layer avoids calling `OrphanLayers()` when removing child views.
  // Note that we set the layer of `unified_slider_view_` beneath the layer of
  // `device_name_view_`. Meanwhile, `device_name_view_` is removed before
  // `unified_slider_view_` due to the view order. As a result, the layer of
  // `unified_slider_view_` will be removed from the parent twice, causing a
  // CHECK error.
  SetPaintToLayer();

  // Creates and formats the slider view.
  unified_slider_view_ = views::AsViewClass<UnifiedSliderView>(
      AddChildView(std::move(slider_view)));
  auto* slider =
      views::AsViewClass<QuickSettingsSlider>(unified_slider_view_->slider());
  if (is_wide_slider_) {
    slider->SetBorder(views::CreateEmptyBorder(kWideDevicesSliderBorder));
  }

  // Creates and formats the device name view.
  device_name_view_ = detailed_view->AddScrollListCheckableItem(
      this, gfx::kNoneIcon, GetAudioDeviceName(device), device.active);
  ConfigureDeviceNameView(device);

  // Puts `unified_slider_view_` beneath `device_name_view_`.
  device_name_view_->AddLayerToRegion(unified_slider_view_->layer(),
                                      views::LayerRegion::kBelow);

  ConfigureFocusBehavior(device.active, slider);
}

LabeledSliderView::~LabeledSliderView() {
  // Remove the focus ring before the slider view that highlight path generator
  // of `device_name_view_` depends on is deleted.
  views::FocusRing::Remove(device_name_view_);
}

void LabeledSliderView::ConfigureDeviceNameView(const AudioDevice& device) {
  device_name_view_->SetPaintToLayer();

  // Set this flag to false to make the assigned color id effective.
  // Otherwise it will use `color_utils::BlendForMinContrast()` to improve
  // label readability over the background.
  device_name_view_->text_label()->SetAutoColorReadabilityEnabled(
      /*enabled=*/false);

  device_name_view_->SetPreferredSize(kDevicesNameViewPreferredSize);
  device_name_view_->tri_view()->SetInsets(
      is_wide_slider_ ? kWideDevicesTriViewInsets : kDevicesTriViewInsets);
  device_name_view_->tri_view()->SetContainerBorder(
      TriView::Container::CENTER,
      views::CreateEmptyBorder(kDevicesTriViewBorder));
  const bool is_muted =
      device.is_input
          ? CrasAudioHandler::Get()->IsInputMutedForDevice(device.id)
          : CrasAudioHandler::Get()->IsOutputMutedForDevice(device.id);
  UpdateDeviceContainerColor(device_name_view_, is_muted, device.active);
  TypographyProvider::Get()->StyleLabel(TypographyToken::kCrosButton2,
                                        *device_name_view_->text_label());
}

void LabeledSliderView::ConfigureFocusBehavior(const bool is_active,
                                               QuickSettingsSlider* slider) {
  // If this device is the active one, disables event handling on
  // `device_name_view_` so that `slider` can handle the events.
  if (is_active) {
    device_name_view_->SetFocusBehavior(
        HoverHighlightView::FocusBehavior::NEVER);
    device_name_view_->SetCanProcessEventsWithinSubtree(false);
  } else {
    // Installs the customized focus ring path generator for
    // `device_name_view_`.
    device_name_view_->SetInstallFocusRingOnFocus(true);
    views::FocusRing::Get(device_name_view_)
        ->SetPathGenerator(
            std::make_unique<DeviceNameContainerHighlightPathGenerator>(
                slider, is_wide_slider_));
    device_name_view_->SetFocusPainter(nullptr);
    views::FocusRing::Get(device_name_view_)
        ->SetColorId(cros_tokens::kCrosSysPrimary);
  }
}

BEGIN_METADATA(LabeledSliderView)
END_METADATA

}  // namespace ash