// 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