chromium/ash/app_list/views/search_result_image_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/app_list/views/search_result_image_view.h"

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

#include "ash/app_list/model/search/search_result.h"
#include "ash/app_list/views/pulsing_block_view.h"
#include "ash/app_list/views/search_result_image_list_view.h"
#include "ash/app_list/views/search_result_image_view_delegate.h"
#include "ash/style/ash_color_id.h"
#include "cc/paint/display_item_list.h"
#include "chromeos/constants/chromeos_features.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/compositor.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"

namespace ash {

namespace {

// Sizing and spacing values for `result_image_`.
constexpr int kRoundedCornerRadius = 16;

// Focus ring constants
constexpr int kSpaceBetweenFocusRingAndImage = 2;
constexpr int kFocusRingCornerRadius =
    kRoundedCornerRadius + kSpaceBetweenFocusRingAndImage;
constexpr gfx::Insets kFocusRingInsets =
    gfx::Insets(-kSpaceBetweenFocusRingAndImage -
                views::FocusRing::kDefaultHaloThickness / 2);

// Dragged image constants
constexpr double kDraggedImageOpacity = 0.6;

class ImagePreviewView : public views::ImageButton {
  METADATA_HEADER(ImagePreviewView, views::ImageButton)

 public:
  ImagePreviewView() {
    SetInstallFocusRingOnFocus(false);
    SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
  }
  ImagePreviewView(const ImagePreviewView&) = delete;
  ImagePreviewView& operator=(const ImagePreviewView&) = delete;
  ~ImagePreviewView() override = default;

 private:
  // views::ImageView:
  void PaintButtonContents(gfx::Canvas* canvas) override {
    if (GetImage(views::Button::STATE_NORMAL).isNull()) {
      return;
    }

    SkPath mask;
    mask.addRoundRect(gfx::RectToSkRect(GetContentsBounds()),
                      kRoundedCornerRadius, kRoundedCornerRadius);
    canvas->ClipPath(mask, true);
    views::ImageButton::PaintButtonContents(canvas);
    views::FocusRing::Get(parent())->SchedulePaint();
  }
};

BEGIN_METADATA(ImagePreviewView)
END_METADATA

}  // namespace

SearchResultImageView::SearchResultImageView(
    int index,
    SearchResultImageListView* list_view,
    SearchResultImageViewDelegate* image_view_delegate)
    : index_(index), list_view_(list_view) {
  SetLayoutManager(std::make_unique<views::FillLayout>());
  result_image_ = AddChildView(std::make_unique<ImagePreviewView>());
  result_image_->SetCanProcessEventsWithinSubtree(false);
  result_image_->GetViewAccessibility().SetIsIgnored(true);

  views::FocusRing::Install(this);
  views::FocusRing* const focus_ring = views::FocusRing::Get(this);
  focus_ring->SetOutsetFocusRingDisabled(true);
  focus_ring->SetColorId(cros_tokens::kCrosSysFocusRing);
  focus_ring->SetHasFocusPredicate(base::BindRepeating([](const View* view) {
    const auto* v = views::AsViewClass<SearchResultBaseView>(view);
    CHECK(v);
    return v->selected();
  }));
  views::InstallRoundRectHighlightPathGenerator(this, kFocusRingInsets,
                                                kFocusRingCornerRadius);

  SetCallback(base::BindRepeating(&SearchResultImageView::OnImageViewPressed,
                                  base::Unretained(this)));

  set_drag_controller(image_view_delegate);
}

void SearchResultImageView::OnImageViewPressed(const ui::Event& event) {
  list_view_->SearchResultActivated(this, event.flags(),
                                    true /* by_button_press */);
}

gfx::Size SearchResultImageView::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  // Keep the ratio of the width and height be 3:2.
  return gfx::Size(preferred_width_, 2 * preferred_width_ / 3);
}

void SearchResultImageView::OnResultChanged() {
  OnMetadataChanged();
  SchedulePaint();
}

void SearchResultImageView::ConfigureLayoutForAvailableWidth(int width) {
  if (preferred_width_ == width) {
    return;
  }
  preferred_width_ = width;
  PreferredSizeChanged();
}

void SearchResultImageView::CreatePulsingBlockView() {
  pulsing_block_view_ = AddChildView(std::make_unique<PulsingBlockView>(
      size(), base::Milliseconds(index_ * 200), kRoundedCornerRadius));
  pulsing_block_view_->SetCanProcessEventsWithinSubtree(false);
  pulsing_block_view_->GetViewAccessibility().SetIsIgnored(true);
}

gfx::ImageSkia SearchResultImageView::CreateDragImage() {
  const ui::Compositor* compositor = GetWidget()->GetCompositor();
  const float scale = compositor->device_scale_factor();
  const gfx::Rect paint_bounds(gfx::ScaleToCeiledSize(size(), scale));
  const bool is_pixel_canvas = compositor->is_pixel_canvas();
  SkBitmap bitmap;
  bitmap.allocN32Pixels(paint_bounds.width(), paint_bounds.height());
  bitmap.eraseColor(SK_ColorTRANSPARENT);

  SkCanvas canvas(bitmap);
  auto list = base::MakeRefCounted<cc::DisplayItemList>();
  ui::PaintContext context(list.get(), scale, paint_bounds, is_pixel_canvas);

  result_image_->Paint(
      views::PaintInfo::CreateRootPaintInfo(context, paint_bounds.size()));
  list->Finalize();
  list->Raster(&canvas, nullptr);

  gfx::ImageSkia dragged_image =
      gfx::ImageSkia::CreateFromBitmap(bitmap, scale);
  return gfx::ImageSkiaOperations::CreateTransparentImage(dragged_image,
                                                          kDraggedImageOpacity);
}

void SearchResultImageView::OnMetadataChanged() {
  UpdateAccessibleName();
  // By default, the description will be set to the tooltip text, but the title
  // is already announced in the accessible name.
  GetViewAccessibility().SetDescription(
      u"", ax::mojom::DescriptionFrom::kAttributeExplicitlyEmpty);

  if (!result()) {
    result_image_->SetVisible(false);
    return;
  }

  const bool has_icon = !result()->icon().icon.IsEmpty();
  result_image_->SetVisible(has_icon);
  if (!has_icon || result()->icon().is_placeholder) {
    if (!pulsing_block_view_) {
      CreatePulsingBlockView();
    }
  } else {
    if (pulsing_block_view_) {
      RemoveChildViewT(pulsing_block_view_.get());
      pulsing_block_view_ = nullptr;
    }
  }

  if (has_icon) {
    gfx::ImageSkia image = result()->icon().icon.Rasterize(GetColorProvider());
    if (!GetContentsBounds().IsEmpty()) {
      image = gfx::ImageSkiaOperations::CreateResizedImage(
          image, skia::ImageOperations::RESIZE_BEST,
          GetContentsBounds().size());
    }

    result_image_->SetImageModel(views::Button::STATE_NORMAL,
                                 ui::ImageModel::FromImageSkia(image));
  }
  SetTooltipText(result()->title());
}

SearchResultImageView::~SearchResultImageView() = default;

BEGIN_METADATA(SearchResultImageView)
END_METADATA

}  // namespace ash