chromium/ash/capture_mode/capture_button_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/capture_mode/capture_button_view.h"

#include <memory>
#include <string>

#include "ash/capture_mode/capture_mode_constants.h"
#include "ash/capture_mode/capture_mode_controller.h"
#include "ash/capture_mode/capture_mode_session_focus_cycler.h"
#include "ash/capture_mode/capture_mode_types.h"
#include "ash/capture_mode/capture_mode_util.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/ash_color_id.h"
#include "ash/style/style_util.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/raw_ref.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/rounded_corners_f.h"
#include "ui/gfx/vector_icon_types.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/button/label_button.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/separator.h"
#include "ui/views/layout/box_layout.h"

namespace ash {

namespace {

// The `capture_button_` can be fully rounded or half rounded depending on
// whether the `drop_down_button_` is visible or not.
constexpr gfx::RoundedCornersF kCaptureButtonFullyRoundedCorners(18);
constexpr gfx::RoundedCornersF kCaptureButtonHalfRoundedCorners(18, 0, 0, 18);
// However, the `drop_down_button_` is always half rounded.
constexpr gfx::RoundedCornersF kDropDownButtonRoundedCorners(0, 18, 18, 0);

// Regular focus rings are drawn outside the view's bounds such that there is a
// gap between the view and its focus ring. However, the
// `capture_label_widget_` tightly contains its contents view, meaning that the
// size of the widget is the same as the size of the `capture_label_view_`. If
// we outset the focus rings of `capture_button_` and `drop_down_button_`, they
// will be masked by the widget's bounds, and won't show. Hence, we inset by
// half the focus ring default thickness to ensure the focus ring is drawn
// inside the widget bounds.
constexpr gfx::Insets kFocusRingPathInsets(
    views::FocusRing::kDefaultHaloThickness / 2);

// Defines the state of the capture button, which is the ID of the string used
// as its label, and its icon. These are selected based on the current state of
// capture mode, whether capture images or videos, and which video format is
// selected.
struct CaptureButtonState {
  const int label_id;
  const raw_ref<const gfx::VectorIcon> vector_icon;
};

// Based on the current state of capture mode, returns the state with which the
// capture button should be updated.
CaptureButtonState GetCaptureButtonState() {
  const auto* const controller = CaptureModeController::Get();
  if (controller->type() == CaptureModeType::kImage) {
    return CaptureButtonState{IDS_ASH_SCREEN_CAPTURE_LABEL_IMAGE_CAPTURE,
                              ToRawRef(kCaptureModeImageIcon)};
  }

  if (controller->recording_type() == RecordingType::kWebM) {
    return CaptureButtonState{IDS_ASH_SCREEN_CAPTURE_LABEL_VIDEO_RECORD,
                              ToRawRef(kCaptureModeVideoIcon)};
  }

  DCHECK(features::IsGifRecordingEnabled());
  DCHECK_EQ(controller->recording_type(), RecordingType::kGif);

  return CaptureButtonState{IDS_ASH_SCREEN_CAPTURE_LABEL_GIF_RECORD,
                            ToRawRef(kCaptureGifIcon)};
}

}  // namespace

CaptureButtonView::CaptureButtonView(
    views::Button::PressedCallback on_capture_button_pressed,
    views::Button::PressedCallback on_drop_down_pressed,
    CaptureModeBehavior* active_behavior)
    : capture_button_(AddChildView(std::make_unique<views::LabelButton>(
          std::move(on_capture_button_pressed),
          std::u16string()))) {
  auto* box_layout = SetLayoutManager(std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kHorizontal, gfx::Insets(),
      /*between_child_spacing=*/0));
  box_layout->set_cross_axis_alignment(
      views::BoxLayout::CrossAxisAlignment::kStretch);

  capture_button_->SetHorizontalAlignment(gfx::ALIGN_CENTER);
  capture_button_->SetBorder(views::CreateEmptyBorder(gfx::Insets::VH(0, 12)));
  SetupButton(capture_button_);

  // Only show the drop down button if there are more than one recording types
  // that are currently supported in the current mode (i.e. we don't bother to
  // show a drop down for a single item).
  if (active_behavior->GetSupportedRecordingTypes().size() > 1u) {
    separator_ = AddChildView(std::make_unique<views::Separator>());
    separator_->SetColorId(ui::kColorAshSystemUIMenuSeparator);
    drop_down_button_ = AddChildView(
        std::make_unique<views::ImageButton>(std::move(on_drop_down_pressed)));
    SetupButton(drop_down_button_);
    drop_down_button_->SetBorder(
        views::CreateEmptyBorder(gfx::Insets::TLBR(0, 6, 0, 8)));
    drop_down_button_->SetImageHorizontalAlignment(
        views::ImageButton::ALIGN_CENTER);
    drop_down_button_->SetImageVerticalAlignment(
        views::ImageButton::ALIGN_MIDDLE);
    drop_down_button_->SetMinimumImageSize(capture_mode::kSettingsIconSize);
    drop_down_button_->SetTooltipText(l10n_util::GetStringUTF16(
        IDS_ASH_SCREEN_CAPTURE_RECORDING_TYPE_BUTTON_TOOLTIP));
  }
}

void CaptureButtonView::UpdateViewVisuals() {
  // This view should be visible only if we're capturing a non-empty region.
  DCHECK(GetVisible());

  // The path of the capture button's focus ring may need to change if we switch
  // from a single button to a dual button. We'll use a change in the visibility
  // of the separator as an indicator for the need to re-install a new highlight
  // path generator on the capture button.
  bool should_invalidate_focus_ring = false;

  // The recording type selection views are visible only when the capture type
  // is video recording.
  const bool is_capturing_image =
      CaptureModeController::Get()->type() == CaptureModeType::kImage;
  if (drop_down_button_) {
    DCHECK(separator_);
    const bool new_visibility = !is_capturing_image;
    should_invalidate_focus_ring = new_visibility != separator_->GetVisible();
    separator_->SetVisible(new_visibility);
    drop_down_button_->SetVisible(new_visibility);
  }

  const auto button_state = GetCaptureButtonState();
  capture_button_->SetText(l10n_util::GetStringUTF16(button_state.label_id));

  const SkColor icon_color =
      GetColorProvider()->GetColor(kColorAshIconColorPrimary);
  capture_button_->SetImageModel(
      views::Button::STATE_NORMAL,
      ui::ImageModel::FromVectorIcon(*button_state.vector_icon, icon_color));

  if (should_invalidate_focus_ring) {
    // Note that we don't need to invalidate the focus ring of the
    // `drop_down_button_` as it never changes, and always remains half rounded.
    CaptureModeSessionFocusCycler::HighlightHelper::Get(capture_button_)
        ->InvalidateFocusRingPath();

    // The ink drop highlight needs to be updated as well, since the rounded
    // corners have changed.
    views::HighlightPathGenerator::Install(
        capture_button_,
        CreateFocusRingPath(capture_button_, /*use_zero_insets=*/true));
  }
}

std::vector<CaptureModeSessionFocusCycler::HighlightableView*>
CaptureButtonView::GetHighlightableItems() const {
  std::vector<CaptureModeSessionFocusCycler::HighlightableView*> result{
      CaptureModeSessionFocusCycler::HighlightHelper::Get(capture_button_)};
  if (drop_down_button_ && drop_down_button_->GetVisible()) {
    result.push_back(
        CaptureModeSessionFocusCycler::HighlightHelper::Get(drop_down_button_));
  }
  return result;
}

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

  auto* color_provider = GetColorProvider();
  capture_button_->SetEnabledTextColors(
      color_provider->GetColor(kColorAshTextColorPrimary));

  if (drop_down_button_) {
    drop_down_button_->SetImageModel(
        views::Button::STATE_NORMAL,
        ui::ImageModel::FromVectorIcon(
            kDropDownArrowIcon,
            color_provider->GetColor(kColorAshIconColorPrimary)));
  }
}

void CaptureButtonView::SetupButton(views::Button* button) {
  button->SetFocusBehavior(views::View::FocusBehavior::ACCESSIBLE_ONLY);
  views::InkDrop::Get(button)->SetMode(views::InkDropHost::InkDropMode::ON);
  button->SetHasInkDropActionOnClick(true);
  StyleUtil::ConfigureInkDropAttributes(
      button, StyleUtil::kBaseColor | StyleUtil::kInkDropOpacity);
  button->SetNotifyEnterExitOnChild(true);

  // This installs a path generator that will be used for the ink drop
  // highlight. It should not have any insets as the highlight should span the
  // entire bounds of the view.
  views::HighlightPathGenerator::Install(
      button, CreateFocusRingPath(button, /*use_zero_insets=*/true));

  // This will be used to install a path generator for the focus ring, which
  // should be insetted a little so that the focus ring can paint within the
  // bounds the view.
  CaptureModeSessionFocusCycler::HighlightHelper::Install(
      button, base::BindRepeating(&CaptureButtonView::CreateFocusRingPath,
                                  base::Unretained(this), button,
                                  /*use_zero_insets=*/false));

  StyleUtil::SetUpInkDropForButton(button);
}

std::unique_ptr<views::HighlightPathGenerator>
CaptureButtonView::CreateFocusRingPath(views::View* view,
                                       bool use_zero_insets) {
  const auto insets = use_zero_insets ? gfx::Insets() : kFocusRingPathInsets;
  if (view == capture_button_) {
    const bool should_ring_be_half_rounded =
        drop_down_button_ && drop_down_button_->GetVisible();
    return std::make_unique<views::RoundRectHighlightPathGenerator>(
        insets, should_ring_be_half_rounded
                    ? kCaptureButtonHalfRoundedCorners
                    : kCaptureButtonFullyRoundedCorners);
  }

  DCHECK_EQ(view, drop_down_button_);
  return std::make_unique<views::RoundRectHighlightPathGenerator>(
      insets, kDropDownButtonRoundedCorners);
}

BEGIN_METADATA(CaptureButtonView)
END_METADATA

}  // namespace ash