chromium/ash/system/video_conference/bubble/toggle_effects_view.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/video_conference/bubble/toggle_effects_view.h"

#include <algorithm>
#include <memory>
#include <utility>

#include "ash/bubble/bubble_utils.h"
#include "ash/constants/ash_features.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/strings/grit/ash_strings.h"
#include "ash/style/icon_button.h"
#include "ash/style/typography.h"
#include "ash/system/tray/tray_constants.h"
#include "ash/system/unified/feature_tile.h"
#include "ash/system/video_conference/bubble/bubble_view_ids.h"
#include "ash/system/video_conference/bubble/vc_tile_ui_controller.h"
#include "ash/system/video_conference/effects/video_conference_tray_effects_manager_types.h"
#include "ash/system/video_conference/video_conference_tray_controller.h"
#include "ash/system/video_conference/video_conference_utils.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/utf_string_conversions.h"
#include "chromeos/utils/haptics_util.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/events/devices/haptic_touchpad_effects.h"
#include "ui/events/event.h"
#include "ui/gfx/font.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/size.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/flex_layout_types.h"
#include "ui/views/view_class_properties.h"

namespace ash::video_conference {

namespace {

constexpr int kButtonCornerRadius = 16;
constexpr int kIconSize = 20;
constexpr int kButtonHeight = 64;
constexpr int kButtonContainerSpacing = 8;
constexpr int kButtonVerticalPadding = 4;
constexpr int kButtonHorizontalPadding = 8;
constexpr int kButtonHorizontalPaddingWithMultilineLabel = 16;
constexpr int kButtonSpacing = 8;
constexpr int kButtonSpacingWithMultilineLabel = 4;
constexpr int kMaxLinesForLabel = 2;

constexpr char kGoogleSansFont[] = "Google Sans";

// A customized label for the toggle effects button. When the label has more
// than 1 line, it will automatically adjust the padding and the spacing of the
// button.
class ToggleEffectsButtonLabel : public views::Label {
  METADATA_HEADER(ToggleEffectsButtonLabel, views::Label)

 public:
  ToggleEffectsButtonLabel(ToggleEffectsButton* button,
                           const std::u16string& label_text,
                           int num_button_per_row)
      : button_(button), num_button_per_row_(num_button_per_row) {
    // Need to set up `label_max_width_` so that the first round of layout is
    // set up correctly for multiline label (crbug.com/1349528). For this first
    // layout, we will assume that the text is multi line to fix the mentioned
    // bug. A one-line text will be adjusted correctly in subsequent layout(s).
    SetLabelMaxWidth(/*is_multi_line=*/true);

    SetID(video_conference::BubbleViewID::kToggleEffectLabel);
    SetAutoColorReadabilityEnabled(false);
    SetEnabledColorId(cros_tokens::kCrosSysOnPrimaryContainer);
    SetMultiLine(true);
    SetMaxLines(kMaxLinesForLabel);
    SetProperty(
        views::kFlexBehaviorKey,
        views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToZero,
                                 views::MaximumFlexSizeRule::kPreferred));

    // TODO(b/290374705): Use token style when it is available.
    SetFontList(gfx::FontList({kGoogleSansFont}, gfx::Font::NORMAL, 12,
                              gfx::Font::Weight::MEDIUM));
    SetLineHeight(16);

    SetText(label_text);
  }

  ToggleEffectsButtonLabel(const ToggleEffectsButtonLabel&) = delete;
  ToggleEffectsButtonLabel& operator=(const ToggleEffectsButtonLabel&) = delete;

  ~ToggleEffectsButtonLabel() override = default;

  void SetText(const std::u16string& new_text) override {
    views::Label::SetText(new_text);

    // Need to size to the new preferred size to know the number of lines
    // required to display the text. If we display the text in 2 lines, we need
    // to adjust the button horizontal padding and spacing between the icon and
    // the label.
    SizeToPreferredSize();
    bool is_multi_line = GetRequiredLines() > 1;

    SetLabelMaxWidth(is_multi_line);
    SetMaximumWidth(label_max_width_);

    button_->layout()->SetInteriorMargin(gfx::Insets::VH(
        kButtonVerticalPadding, is_multi_line
                                    ? kButtonHorizontalPaddingWithMultilineLabel
                                    : kButtonHorizontalPadding));

    button_->icon()->SetBorder(views::CreateEmptyBorder(gfx::Insets::TLBR(
        0, 0, is_multi_line ? kButtonSpacingWithMultilineLabel : kButtonSpacing,
        0)));
  }

  gfx::Size CalculatePreferredSize(
      const views::SizeBounds &available_size) const override {
    // TODO(crbug.com/40233803): The size constraint is not passed down from
    // the views tree in the first round of layout, so multiline label might
    // be broken here. We need to explicitly set the size to fix this.
    return gfx::Size(label_max_width_,
                     views::Label::CalculatePreferredSize(
                         views::SizeBounds(label_max_width_, {}))
                         .height());
  }

 private:
  // Set `label_max_width_` based on whether the label has more than 1 line.
  void SetLabelMaxWidth(bool is_multi_line) {
    int button_width =
        (kTrayMenuWidth - kVideoConferenceBubbleHorizontalPadding * 2 -
         kButtonContainerSpacing -
         kButtonContainerSpacing * (num_button_per_row_ - 1)) /
        num_button_per_row_;

    auto horizontal_padding = is_multi_line
                                  ? kButtonHorizontalPaddingWithMultilineLabel
                                  : kButtonHorizontalPadding;

    label_max_width_ = button_width - horizontal_padding * 2;
  }

  raw_ptr<ToggleEffectsButton> button_ = nullptr;

  // Keeps track of the number of buttons that is in the row that this label
  // resides in. Used to calculate the max width of this label.
  const int num_button_per_row_;

  int label_max_width_ = 0;
};

BEGIN_METADATA(ToggleEffectsButtonLabel);
END_METADATA

}  // namespace

ToggleEffectsButton::ToggleEffectsButton(
    views::Button::PressedCallback callback,
    const gfx::VectorIcon* vector_icon,
    bool toggle_state,
    const std::u16string& label_text,
    const int accessible_name_id,
    std::optional<int> container_id,
    const VcEffectId effect_id,
    int num_button_per_row)
    : callback_(std::move(callback)),
      toggled_(toggle_state),
      effect_id_(effect_id),
      vector_icon_(vector_icon),
      accessible_name_id_(accessible_name_id) {
  SetCallback(base::BindRepeating(&ToggleEffectsButton::OnButtonClicked,
                                  weak_ptr_factory_.GetWeakPtr()));
  SetID(video_conference::BubbleViewID::kToggleEffectsButton);

  layout_ = SetLayoutManager(std::make_unique<views::FlexLayout>());
  layout_->SetOrientation(views::LayoutOrientation::kVertical)
      .SetMainAxisAlignment(views::LayoutAlignment::kCenter)
      .SetCrossAxisAlignment(views::LayoutAlignment::kCenter)
      .SetInteriorMargin(
          gfx::Insets::TLBR(kButtonVerticalPadding, kButtonHorizontalPadding,
                            kButtonVerticalPadding, kButtonHorizontalPadding));

  // If VcDlcUi is enabled then the button's preferred size and flex properties
  // are controlled externally to the button rather than by the button itself.
  if (!features::IsVcDlcUiEnabled()) {
    // This makes the view the expand or contract to occupy any available space.
    SetProperty(
        views::kFlexBehaviorKey,
        views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToMinimum,
                                 views::MaximumFlexSizeRule::kUnbounded));

    SetPreferredSize(gfx::Size(GetPreferredSize().width(), kButtonHeight));
  }

  views::InstallRoundRectHighlightPathGenerator(this, gfx::Insets(),
                                                kButtonCornerRadius);

  auto* focus_ring = views::FocusRing::Get(this);
  focus_ring->SetColorId(cros_tokens::kCrosSysFocusRing);
  // The focus ring appears slightly outside the tile bounds.
  focus_ring->SetHaloInset(-3);
  // Since the focus ring doesn't set a LayoutManager it won't get drawn
  // unless excluded by the tile's LayoutManager.
  focus_ring->SetProperty(views::kViewIgnoredByLayoutKey, true);

  auto icon = std::make_unique<views::ImageView>();
  icon->SetID(video_conference::BubbleViewID::kToggleEffectIcon);
  // `icon_` image set in `UpdateColorsAndBackground()`.
  icon_ = AddChildView(std::move(icon));

  auto label = std::make_unique<ToggleEffectsButtonLabel>(
      /*button=*/this, label_text, num_button_per_row);
  label_ = AddChildView(std::move(label));

  SetTooltipText(l10n_util::GetStringFUTF16(
      VIDEO_CONFERENCE_TOGGLE_BUTTON_TOOLTIP,
      l10n_util::GetStringUTF16(accessible_name_id_),
      l10n_util::GetStringUTF16(
          toggled_ ? VIDEO_CONFERENCE_TOGGLE_BUTTON_STATE_ON
                   : VIDEO_CONFERENCE_TOGGLE_BUTTON_STATE_OFF)));
  GetViewAccessibility().SetRole(ax::mojom::Role::kToggleButton);
  SetFocusBehavior(FocusBehavior::ALWAYS);

  UpdateColorsAndBackground();

  // Assign the ID, if present, to the outermost container view. Only used in
  // tests.
  if (container_id.has_value()) {
    SetID(container_id.value());
  }
}

ToggleEffectsButton::~ToggleEffectsButton() = default;

void ToggleEffectsButton::OnButtonClicked(const ui::Event& event) {
  callback_.Run(event);

  // Sets the toggled state.
  toggled_ = !toggled_;

  base::UmaHistogramBoolean(
      video_conference_utils::GetEffectHistogramNameForClick(effect_id_),
      toggled_);

  chromeos::haptics_util::PlayHapticToggleEffect(
      !toggled_, ui::HapticTouchpadEffectStrength::kMedium);

  UpdateColorsAndBackground();
  SetTooltipText(l10n_util::GetStringFUTF16(
      VIDEO_CONFERENCE_TOGGLE_BUTTON_TOOLTIP,
      l10n_util::GetStringUTF16(accessible_name_id_),
      l10n_util::GetStringUTF16(
          toggled_ ? VIDEO_CONFERENCE_TOGGLE_BUTTON_STATE_ON
                   : VIDEO_CONFERENCE_TOGGLE_BUTTON_STATE_OFF)));
}

void ToggleEffectsButton::UpdateColorsAndBackground() {
  ui::ColorId background_color_id =
      toggled_ ? cros_tokens::kCrosSysSystemPrimaryContainer
               : cros_tokens::kCrosSysSystemOnBase;
  SetBackground(views::CreateThemedRoundedRectBackground(background_color_id,
                                                         kButtonCornerRadius));

  ui::ColorId foreground_color_id =
      toggled_ ? cros_tokens::kCrosSysSystemOnPrimaryContainer
               : cros_tokens::kCrosSysOnSurface;
  icon_->SetImage(ui::ImageModel::FromVectorIcon(
      *vector_icon_, foreground_color_id, kIconSize));
  label_->SetEnabledColorId(foreground_color_id);
}

BEGIN_METADATA(ToggleEffectsButton);
END_METADATA

ToggleEffectsView::ToggleEffectsView(
    VideoConferenceTrayController* controller) {
  SetID(BubbleViewID::kToggleEffectsView);

  // Layout for the entire toggle effects section.
  SetLayoutManager(std::make_unique<views::FlexLayout>())
      ->SetOrientation(views::LayoutOrientation::kVertical)
      .SetMainAxisAlignment(views::LayoutAlignment::kCenter)
      .SetCrossAxisAlignment(views::LayoutAlignment::kStretch)
      .SetDefault(views::kMarginsKey,
                  gfx::Insets::TLBR(0, 0, kButtonContainerSpacing, 0))
      .SetIgnoreDefaultMainAxisMargins(true);

  // The effects manager provides the toggle effects in rows.
  auto& effects_manager = controller->GetEffectsManager();
  const VideoConferenceTrayEffectsManager::EffectDataTable tile_rows =
      effects_manager.GetToggleEffectButtonTable();
  for (auto& row : tile_rows) {
    // Each row is its own view, with its own layout.
    std::unique_ptr<views::View> row_view = std::make_unique<views::View>();
    row_view->SetLayoutManager(std::make_unique<views::FlexLayout>())
        ->SetOrientation(views::LayoutOrientation::kHorizontal)
        .SetMainAxisAlignment(views::LayoutAlignment::kCenter)
        .SetCrossAxisAlignment(views::LayoutAlignment::kStretch)
        .SetDefault(views::kMarginsKey,
                    gfx::Insets::TLBR(0, kButtonContainerSpacing / 2, 0,
                                      kButtonContainerSpacing / 2))
        .SetIgnoreDefaultMainAxisMargins(true);

    // TODO(crbug.com/40232718): See View::SetLayoutManagerUseConstrainedSpace.
    row_view->SetLayoutManagerUseConstrainedSpace(false);

    // Add a button for each item in the row.
    for (auto* tile : row) {
      DCHECK_EQ(tile->type(), VcEffectType::kToggle);
      DCHECK_EQ(tile->GetNumStates(), 1);

      // If `current_state` has no value, it means the state of the effect
      // (represented by `tile`) cannot be obtained. This can happen if the
      // `VcEffectsDelegate` hosting the effect has encountered an error or is
      // in some bad state. In this case its controls are not presented.
      std::optional<int> current_state = tile->get_state_callback().Run();
      if (!current_state.has_value()) {
        continue;
      }

      // `current_state` can only be a `bool` for a toggle effect.
      bool toggle_state = current_state.value() != 0;
      const VcEffectState* state = tile->GetState(/*index=*/0);

      // The button should either be a `FeatureTile` or a `ToggleEffectsButton`,
      // depending on the following logic.
      std::unique_ptr<views::View> button;
      if (ash::features::IsVcDlcUiEnabled()) {
        // If VcDlcUi is enabled then first try to see if the button should be a
        // `FeatureTile` by determining if there is a tile controller for the
        // VC effect.
        auto* tile_controller =
            effects_manager.GetUiControllerForEffectId(tile->id());
        if (tile_controller) {
          button = tile_controller->CreateTile();
        }
      }
      if (!button) {
        // If there was no tile controller or if VcDlcUi is not enabled, then
        // the button should be a `ToggleEffectsButton`.
        button = std::make_unique<ToggleEffectsButton>(
            state->button_callback(), state->icon(), toggle_state,
            state->label_text(), state->accessible_name_id(),
            tile->container_id(), tile->id(),
            /*num_button_per_row=*/row.size());
      }

      // If VcDlcUi is enabled then the button's preferred size and flex
      // properties are controlled externally to the button rather than by the
      // button itself.
      if (ash::features::IsVcDlcUiEnabled()) {
        // Set the preferred width to 0 so that the container (`row_view`) can
        // equally distribute its full width among all its child buttons.
        button->SetPreferredSize(gfx::Size(0, kButtonHeight));

        // Allow the button to expand or contract to whatever size is available
        // for it.
        button->SetProperty(views::kFlexBehaviorKey,
                            views::FlexSpecification(
                                views::MinimumFlexSizeRule::kScaleToMinimum,
                                views::MaximumFlexSizeRule::kUnbounded));
      }

      row_view->AddChildView(std::move(button));
    }

    // Add the row as a child, now that it's fully populated,
    AddChildView(std::move(row_view));
  }
}

BEGIN_METADATA(ToggleEffectsView);
END_METADATA

}  // namespace ash::video_conference