#include "ash/clipboard/views/clipboard_history_bitmap_item_view.h"
#include "ash/clipboard/clipboard_history_item.h"
#include "ash/clipboard/views/clipboard_history_view_constants.h"
#include "ash/style/ash_color_id.h"
#include "base/callback_list.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/time/time.h"
#include "third_party/skia/include/core/SkPath.h"
#include "third_party/skia/include/core/SkPathBuilder.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/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/layer_animation_observer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/view_class_properties.h"
namespace ash {
namespace {
constexpr base::TimeDelta kFadeOutDurationMs = base::Milliseconds(60);
constexpr base::TimeDelta kFadeInDurationMs = base::Milliseconds(200);
class FadeImageView : public views::ImageView,
public ui::ImplicitAnimationObserver {
METADATA_HEADER(FadeImageView, views::ImageView)
public:
FadeImageView(
base::RepeatingCallback<const ClipboardHistoryItem*()> item_resolver,
base::RepeatingClosure update_callback)
: item_resolver_(item_resolver), update_callback_(update_callback) {
CHECK(item_resolver_);
CHECK(update_callback_);
const auto* item = item_resolver_.Run();
CHECK(item);
display_image_updated_subscription_ =
item->AddDisplayImageUpdatedCallback(base::BindRepeating(
&FadeImageView::OnDisplayImageUpdated, base::Unretained(this)));
SetImageFromModel();
}
FadeImageView(const FadeImageView& rhs) = delete;
FadeImageView& operator=(const FadeImageView& rhs) = delete;
~FadeImageView() override { StopObservingImplicitAnimations(); }
void OnDisplayImageUpdated() {
CHECK_EQ(FadeAnimationState::kNoFadeAnimation, animation_state_);
SetPaintToLayer();
layer()->SetFillsBoundsOpaquely(false);
animation_state_ = FadeAnimationState::kFadeOut;
ui::ScopedLayerAnimationSettings settings(layer()->GetAnimator());
settings.SetTransitionDuration(kFadeOutDurationMs);
settings.AddObserver(this);
layer()->SetOpacity(0.0f);
}
void OnImplicitAnimationsCompleted() override {
switch (animation_state_) {
case FadeAnimationState::kNoFadeAnimation:
NOTREACHED();
case FadeAnimationState::kFadeOut:
CHECK_EQ(layer()->opacity(), 0.0f);
animation_state_ = FadeAnimationState::kFadeIn;
SetImageFromModel();
{
ui::ScopedLayerAnimationSettings settings(layer()->GetAnimator());
settings.AddObserver(this);
settings.SetTransitionDuration(kFadeInDurationMs);
layer()->SetOpacity(1.0f);
}
return;
case FadeAnimationState::kFadeIn:
DestroyLayer();
animation_state_ = FadeAnimationState::kNoFadeAnimation;
return;
}
}
void SetImageFromModel() {
if (const auto* item = item_resolver_.Run()) {
CHECK(item->display_image().has_value());
SetImage(item->display_image().value());
}
if (animation_state_ == FadeAnimationState::kFadeIn)
update_callback_.Run();
}
enum class FadeAnimationState {
kNoFadeAnimation,
kFadeOut,
kFadeIn,
};
FadeAnimationState animation_state_ = FadeAnimationState::kNoFadeAnimation;
base::RepeatingCallback<const ClipboardHistoryItem*()> item_resolver_;
base::RepeatingClosure update_callback_;
base::CallbackListSubscription display_image_updated_subscription_;
};
BEGIN_METADATA(FadeImageView)
END_METADATA
}
class ClipboardHistoryBitmapItemView::BitmapContentsView
: public ClipboardHistoryBitmapItemView::ContentsView {
METADATA_HEADER(BitmapContentsView, ContentsView)
public:
explicit BitmapContentsView(ClipboardHistoryBitmapItemView* container)
: container_(container) {
views::Builder<views::View>(this)
.SetLayoutManager(std::make_unique<views::FillLayout>())
.AddChild(views::Builder<views::ImageView>(BuildImageView())
.CopyAddressTo(&image_view_))
.BuildChildren();
SetBackground(views::CreateRoundedRectBackground(
cros_tokens::kCrosSysSeparator,
ClipboardHistoryViews::kImageBackgroundCornerRadius));
}
BitmapContentsView(const BitmapContentsView& rhs) = delete;
BitmapContentsView& operator=(const BitmapContentsView& rhs) = delete;
~BitmapContentsView() override = default;
private:
SkPath GetClipPath() override {
const SkRect contents_bounds = gfx::RectToSkRect(GetContentsBounds());
if (!is_delete_button_visible()) {
const SkScalar radius =
SkIntToScalar(ClipboardHistoryViews::kImageBackgroundCornerRadius);
return SkPath::RRect(contents_bounds, radius, radius);
}
const auto width = contents_bounds.width();
const auto height = contents_bounds.height();
const auto radius = ClipboardHistoryViews::kImageBackgroundCornerRadius;
const auto top_left = SkPoint::Make(0.f, 0.f);
const auto bottom_left = SkPoint::Make(0.f, height);
const auto bottom_right = SkPoint::Make(width, height);
const auto horizontal_offset = SkPoint::Make(radius, 0.f);
const auto vertical_offset = SkPoint::Make(0.f, radius);
return SkPathBuilder()
.moveTo(radius, 0.f)
.arcTo(top_left, top_left + vertical_offset, radius)
.arcTo(bottom_left, bottom_left + horizontal_offset, radius)
.arcTo(bottom_right, bottom_right - vertical_offset, radius)
.lineTo(width, ClipboardHistoryViews::kCornerCutoutHeight)
.rCubicTo(0.f, -8.f, -6.7f, -10.f, -10.f, -10.f)
.rLineTo(-4.f, 0.f)
.rCubicTo(-7.7f, 0.f, -14.f, -6.3f, -14.f, -14.f)
.rLineTo(0.f, -4.f)
.rCubicTo(0.f, -3.3f, -2.f, -10.f, -10.f, -10.f)
.lineTo(radius, 0.f)
.close()
.detach();
}
gfx::Size CalculatePreferredSize(
const views::SizeBounds& available_size) const override {
const int preferred_width =
ClipboardHistoryBitmapItemView::ContentsView::CalculatePreferredSize(
available_size)
.width();
return gfx::Size(preferred_width,
ClipboardHistoryViews::kImageViewPreferredHeight);
}
void OnBoundsChanged(const gfx::Rect& previous_bounds) override {
SetClipPath(GetClipPath());
UpdateImageViewSize();
}
std::unique_ptr<views::ImageView> BuildImageView() {
const auto* clipboard_history_item = container_->GetClipboardHistoryItem();
CHECK(clipboard_history_item);
return std::make_unique<FadeImageView>(
base::BindRepeating(
&ClipboardHistoryBitmapItemView::GetClipboardHistoryItem,
base::Unretained(container_)),
base::BindRepeating(&BitmapContentsView::UpdateImageViewSize,
base::Unretained(this)));
}
void UpdateImageViewSize() {
if (image_view_->GetImageModel() ==
clipboard_history_util::GetHtmlPreviewPlaceholder()) {
image_view_->SetImageSize(
gfx::Size(ClipboardHistoryViews::kBitmapItemPlaceholderIconSize,
ClipboardHistoryViews::kBitmapItemPlaceholderIconSize));
return;
}
const gfx::Size image_size = image_view_->GetImage().size();
gfx::Rect contents_bounds = GetContentsBounds();
const float width_ratio =
image_size.width() / float(contents_bounds.width());
const float height_ratio =
image_size.height() / float(contents_bounds.height());
float scaling_up_ratio = 0.f;
switch (container_->data_format_) {
case ui::ClipboardInternalFormat::kPng: {
scaling_up_ratio = std::fmin(width_ratio, height_ratio);
break;
}
case ui::ClipboardInternalFormat::kHtml: {
scaling_up_ratio = std::fmax(width_ratio, height_ratio);
break;
}
default:
NOTREACHED();
}
CHECK_GT(scaling_up_ratio, 0.f);
image_view_->SetImageSize(
gfx::Size(image_size.width() / scaling_up_ratio,
image_size.height() / scaling_up_ratio));
}
const raw_ptr<ClipboardHistoryBitmapItemView> container_;
raw_ptr<views::ImageView> image_view_ = nullptr;
};
BEGIN_METADATA(ClipboardHistoryBitmapItemView, BitmapContentsView)
END_METADATA
ClipboardHistoryBitmapItemView::ClipboardHistoryBitmapItemView(
const base::UnguessableToken& item_id,
const ClipboardHistory* clipboard_history,
views::MenuItemView* container)
: ClipboardHistoryItemView(item_id, clipboard_history, container),
data_format_(GetClipboardHistoryItem()->main_format()) {
SetID(clipboard_history_util::kBitmapItemView);
switch (data_format_) {
case ui::ClipboardInternalFormat::kHtml:
GetViewAccessibility().SetName(
l10n_util::GetStringUTF16(IDS_CLIPBOARD_HISTORY_MENU_HTML_IMAGE));
break;
case ui::ClipboardInternalFormat::kPng:
GetViewAccessibility().SetName(
l10n_util::GetStringUTF16(IDS_CLIPBOARD_HISTORY_MENU_PNG_IMAGE));
break;
default:
NOTREACHED();
}
}
ClipboardHistoryBitmapItemView::~ClipboardHistoryBitmapItemView() = default;
std::unique_ptr<ClipboardHistoryBitmapItemView::ContentsView>
ClipboardHistoryBitmapItemView::CreateContentsView() {
return std::make_unique<BitmapContentsView>(this);
}
BEGIN_METADATA(ClipboardHistoryBitmapItemView)
END_METADATA
}