Skip to content

Commit

Permalink
Add PickerGifView with basic gif showing functionality.
Browse files Browse the repository at this point in the history
This is for showing gifs in the new ChromeOS picker feature. Various
details are still TBD (e.g. how many gifs can be shown, whether to
always animate or only animate on hover, resolution requirements), so
just add some basic functionality to experiment with for now.

Gifs are planned to be fetched from Google's tenor API:
https://developers.google.com/tenor

Then, the gifs are decoded out of process using the data_decoder
service, via ash::image_util API. The decoded frames are stored in
PickerGifView, which handles frame updates. Note that the gifs are
downscaled when they are decoded if needed to reduce the total size of
all frames to `kMaxImageSizeInBytes`:
https://source.chromium.org/chromium/chromium/src/+/main:ash/public/cpp/image_util.cc;l=156;drc=4597c83760e5b831ec153cba625e571d0a4ec6f9

Bug: b:316817118, b:316936723

Change-Id: Ia1bed51d6a640bb4aa1dbfc5ee1d823b35fa9564
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5124918
Reviewed-by: Scott Violet <sky@chromium.org>
Reviewed-by: Theodore Olsauskas-Warren <sauski@google.com>
Commit-Queue: Michelle Chen <michellegc@google.com>
Reviewed-by: Darren Shen <shend@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1243889}
  • Loading branch information
Michelle authored and Chromium LUCI CQ committed Jan 8, 2024
1 parent e4a5da6 commit 21f7c3f
Show file tree
Hide file tree
Showing 14 changed files with 393 additions and 0 deletions.
3 changes: 3 additions & 0 deletions ash/BUILD.gn
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,8 @@ component("ash") {
"picker/picker_session_metrics.h",
"picker/views/picker_contents_view.cc",
"picker/views/picker_contents_view.h",
"picker/views/picker_gif_view.cc",
"picker/views/picker_gif_view.h",
"picker/views/picker_search_field_view.cc",
"picker/views/picker_search_field_view.h",
"picker/views/picker_search_results_view.cc",
Expand Down Expand Up @@ -3405,6 +3407,7 @@ test("ash_unittests") {
"picker/picker_insert_media_request_unittest.cc",
"picker/picker_session_metrics_unittest.cc",
"picker/views/picker_contents_view_unittest.cc",
"picker/views/picker_gif_view_unittest.cc",
"picker/views/picker_search_field_view_unittest.cc",
"picker/views/picker_search_results_view_unittest.cc",
"picker/views/picker_view_unittest.cc",
Expand Down
10 changes: 10 additions & 0 deletions ash/picker/picker_controller.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@
#include "ash/picker/picker_controller.h"

#include <string_view>
#include <utility>

#include "ash/constants/ash_switches.h"
#include "ash/picker/model/picker_search_results.h"
#include "ash/picker/picker_insert_media_request.h"
#include "ash/picker/views/picker_view.h"
#include "ash/picker/views/picker_view_delegate.h"
#include "ash/public/cpp/ash_web_view_factory.h"
#include "ash/public/cpp/image_util.h"
#include "ash/public/cpp/picker/picker_client.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/hash/sha1.h"

namespace ash {
Expand Down Expand Up @@ -97,6 +100,13 @@ std::unique_ptr<AshWebView> PickerController::CreateWebView(
return client_->CreateWebView(params);
}

void PickerController::LoadAndDecodeGif(const GURL& url,
DecodeGifCallback callback) {
client_->DownloadGifToString(
url,
base::BindOnce(&image_util::DecodeAnimationData, std::move(callback)));
}

void PickerController::StartSearch(const std::u16string& query,
SearchResultsCallback callback) {
// TODO(b/310088338): Do a real search.
Expand Down
1 change: 1 addition & 0 deletions ash/picker/picker_controller.h
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class ASH_EXPORT PickerController : public PickerViewDelegate {
// PickerViewDelegate:
std::unique_ptr<AshWebView> CreateWebView(
const AshWebView::InitParams& params) override;
void LoadAndDecodeGif(const GURL& url, DecodeGifCallback callback) override;
void StartSearch(const std::u16string& query,
SearchResultsCallback callback) override;
void InsertResultOnNextFocus(const PickerSearchResult& result) override;
Expand Down
3 changes: 3 additions & 0 deletions ash/picker/picker_controller_unittest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ class TestPickerClient : public PickerClient {
return web_view_factory_.Create(params);
}

void DownloadGifToString(const GURL& url,
DownloadGifToStringCallback callback) override {}

private:
TestAshWebViewFactory web_view_factory_;
raw_ptr<PickerController> controller_ = nullptr;
Expand Down
98 changes: 98 additions & 0 deletions ash/picker/views/picker_gif_view.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright 2024 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/picker/views/picker_gif_view.h"

#include "ash/public/cpp/image_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/time/time.h"
#include "base/timer/timer.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/gfx/geometry/size.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/gfx/image/image_skia_operations.h"
#include "ui/views/background.h"
#include "ui/views/controls/image_view.h"

namespace ash {

namespace {

constexpr int kPickerGifCornerRadius = 8;

// We use a duration of 100ms for frames that specify a duration of <= 10ms.
// This is to follow the behavior of blink (see http://webkit.org/b/36082 for
// more information).
constexpr base::TimeDelta kShortFrameDurationThreshold = base::Milliseconds(10);
constexpr base::TimeDelta kAdjustedDurationForShortFrames =
base::Milliseconds(100);

} // namespace

PickerGifView::PickerGifView(FramesFetcher frames_fetcher,
const gfx::Size& image_size)
: image_size_(image_size) {
// Show a placeholder rect while the gif loads.
SetBackground(views::CreateThemedRoundedRectBackground(
cros_tokens::kCrosSysAppBaseShaded, kPickerGifCornerRadius));
SetImage(
ui::ImageModel::FromImageSkia(image_util::CreateEmptyImage(image_size)));

std::move(frames_fetcher)
.Run(base::BindOnce(&PickerGifView::OnFramesFetched,
weak_factory_.GetWeakPtr()));
}

PickerGifView::~PickerGifView() = default;

void PickerGifView::OnBoundsChanged(const gfx::Rect& previous_bounds) {
views::ImageView::OnBoundsChanged(previous_bounds);

SkPath path;
path.addRoundRect(gfx::RectToSkRect(GetImageBounds()),
SkIntToScalar(kPickerGifCornerRadius),
SkIntToScalar(kPickerGifCornerRadius));
SetClipPath(path);
}

void PickerGifView::UpdateFrame() {
CHECK(next_frame_index_ < frames_.size());
SetImage(ui::ImageModel::FromImageSkia(frames_[next_frame_index_].image));

// Schedule next frame update.
update_frame_timer_.Start(
FROM_HERE, frames_[next_frame_index_].duration,
base::BindOnce(&PickerGifView::UpdateFrame, weak_factory_.GetWeakPtr()));
next_frame_index_ = (next_frame_index_ + 1) % frames_.size();
}

void PickerGifView::OnFramesFetched(
std::vector<image_util::AnimationFrame> frames) {
if (frames.empty()) {
// TODO: b/316936723 - Handle frames being empty.
return;
}

frames_.reserve(frames.size());
for (auto& frame : frames) {
frame.image = gfx::ImageSkiaOperations::CreateResizedImage(
frame.image, skia::ImageOperations::RESIZE_BEST, image_size_);
if (frame.duration <= kShortFrameDurationThreshold) {
frame.duration = kAdjustedDurationForShortFrames;
}
frames_.push_back(std::move(frame));
}

// Start gif from the first frame.
next_frame_index_ = 0;
UpdateFrame();
}

BEGIN_METADATA(PickerGifView, views::ImageView)
END_METADATA

} // namespace ash
61 changes: 61 additions & 0 deletions ash/picker/views/picker_gif_view.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef ASH_PICKER_VIEWS_PICKER_GIF_VIEW_H_
#define ASH_PICKER_VIEWS_PICKER_GIF_VIEW_H_

#include <optional>
#include <vector>

#include "ash/ash_export.h"
#include "base/functional/callback_forward.h"
#include "base/timer/timer.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/gfx/geometry/size.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/view.h"

namespace ash {

namespace image_util {
struct AnimationFrame;
} // namespace image_util

class ASH_EXPORT PickerGifView : public views::ImageView {
METADATA_HEADER(PickerGifView, views::View)

public:
using FramesFetchedCallback =
base::OnceCallback<void(std::vector<image_util::AnimationFrame>)>;
using FramesFetcher = base::OnceCallback<void(FramesFetchedCallback)>;

PickerGifView(FramesFetcher frames_fetcher, const gfx::Size& image_size);
PickerGifView(const PickerGifView&) = delete;
PickerGifView& operator=(const PickerGifView&) = delete;
~PickerGifView() override;

// views::ImageViewBase:
void OnBoundsChanged(const gfx::Rect& previous_bounds) override;

private:
void UpdateFrame();
void OnFramesFetched(std::vector<image_util::AnimationFrame> frames);

gfx::Size image_size_;

// The decoded gif frames.
std::vector<image_util::AnimationFrame> frames_;

// Timer to call `UpdateFrame` when the next frame should be shown.
base::OneShotTimer update_frame_timer_;

// Index of the frame to show on the next call to `UpdateFrame`.
size_t next_frame_index_ = 0;

base::WeakPtrFactory<PickerGifView> weak_factory_{this};
};

} // namespace ash

#endif // ASH_PICKER_VIEWS_PICKER_GIF_VIEW_H_
93 changes: 93 additions & 0 deletions ash/picker/views/picker_gif_view_unittest.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright 2024 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/picker/views/picker_gif_view.h"

#include <utility>
#include <vector>

#include "ash/public/cpp/image_util.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/image_skia.h"

namespace ash {
namespace {

constexpr gfx::Size kImageSize(100, 100);

image_util::AnimationFrame CreateGifFrame(base::TimeDelta duration) {
return {.image = image_util::CreateEmptyImage(kImageSize),
.duration = duration};
}

void FetchGifFrames(std::vector<image_util::AnimationFrame> frames,
PickerGifView::FramesFetchedCallback callback) {
std::move(callback).Run(frames);
}

gfx::ImageSkia GetImage(const PickerGifView& gif_view) {
return gif_view.GetImageModel().GetImage().AsImageSkia();
}

TEST(PickerGifViewTest, ImageSize) {
base::test::SingleThreadTaskEnvironment task_environment;

constexpr gfx::Size kPreferredImageSize(200, 300);
const std::vector<image_util::AnimationFrame> frames = {
CreateGifFrame(base::Milliseconds(30)),
CreateGifFrame(base::Milliseconds(40))};
PickerGifView gif_view(base::BindOnce(&FetchGifFrames, frames),
kPreferredImageSize);

EXPECT_EQ(gif_view.GetImageModel().Size(), kPreferredImageSize);
EXPECT_EQ(gif_view.GetPreferredSize(), kPreferredImageSize);
}

TEST(PickerGifViewTest, FrameDurations) {
base::test::SingleThreadTaskEnvironment task_environment(
base::test::TaskEnvironment::TimeSource::MOCK_TIME);

const std::vector<image_util::AnimationFrame> frames = {
CreateGifFrame(base::Milliseconds(30)),
CreateGifFrame(base::Milliseconds(40)),
CreateGifFrame(base::Milliseconds(50))};
PickerGifView gif_view(base::BindOnce(&FetchGifFrames, frames), kImageSize);
EXPECT_TRUE(GetImage(gif_view).BackedBySameObjectAs(frames[0].image));

task_environment.FastForwardBy(frames[0].duration);
EXPECT_TRUE(GetImage(gif_view).BackedBySameObjectAs(frames[1].image));

task_environment.FastForwardBy(frames[1].duration);
EXPECT_TRUE(GetImage(gif_view).BackedBySameObjectAs(frames[2].image));

task_environment.FastForwardBy(frames[2].duration);
EXPECT_TRUE(GetImage(gif_view).BackedBySameObjectAs(frames[0].image));
}

TEST(PickerGifViewTest, AdjustsShortFrameDurations) {
base::test::SingleThreadTaskEnvironment task_environment(
base::test::TaskEnvironment::TimeSource::MOCK_TIME);

const std::vector<image_util::AnimationFrame> frames = {
CreateGifFrame(base::Milliseconds(0)),
CreateGifFrame(base::Milliseconds(30))};
PickerGifView gif_view(base::BindOnce(&FetchGifFrames, frames), kImageSize);

// We use a duration of 100ms for frames that specify a duration of <= 10ms
// (to follow the behavior of blink).
task_environment.FastForwardBy(base::Milliseconds(20));
EXPECT_TRUE(GetImage(gif_view).BackedBySameObjectAs(frames[0].image));

task_environment.FastForwardBy(base::Milliseconds(20));
EXPECT_TRUE(GetImage(gif_view).BackedBySameObjectAs(frames[0].image));

task_environment.FastForwardBy(base::Milliseconds(60));
EXPECT_TRUE(GetImage(gif_view).BackedBySameObjectAs(frames[1].image));
}

} // namespace
} // namespace ash
12 changes: 12 additions & 0 deletions ash/picker/views/picker_view_delegate.h
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@

#include "ash/ash_export.h"
#include "ash/public/cpp/ash_web_view.h"
#include "ash/public/cpp/image_util.h"

class GURL;

namespace ash {

Expand All @@ -20,12 +23,21 @@ class ASH_EXPORT PickerViewDelegate {
public:
using SearchResultsCallback =
base::RepeatingCallback<void(const PickerSearchResults& results)>;
// TODO: b/316936723 - Pass `frames` by reference to avoid a copy.
using DecodeGifCallback =
base::OnceCallback<void(std::vector<image_util::AnimationFrame> frames)>;

virtual ~PickerViewDelegate() {}

virtual std::unique_ptr<AshWebView> CreateWebView(
const AshWebView::InitParams& params) = 0;

// Loads and decodes a gif from `url`. If successful, the decoded gif frames
// will be returned via `callback`. Otherwise, `callback` is run with an empty
// vector of frames.
virtual void LoadAndDecodeGif(const GURL& url,
DecodeGifCallback callback) = 0;

// Starts a search for `query`. Results will be returned via `callback`,
// which may be called multiples times to update the results.
virtual void StartSearch(const std::u16string& query,
Expand Down
2 changes: 2 additions & 0 deletions ash/picker/views/picker_view_unittest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ class FakePickerViewDelegate : public PickerViewDelegate {
return ash_web_view_factory_.Create(params);
}

void LoadAndDecodeGif(const GURL& url, DecodeGifCallback callback) override {}

void StartSearch(const std::u16string& query,
SearchResultsCallback callback) override {
callback.Run(search_function_.Run(query));
Expand Down
15 changes: 15 additions & 0 deletions ash/public/cpp/picker/picker_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,32 @@
#ifndef ASH_PUBLIC_CPP_PICKER_PICKER_CLIENT_H_
#define ASH_PUBLIC_CPP_PICKER_PICKER_CLIENT_H_

#include <memory>
#include <string>

#include "ash/public/cpp/ash_public_export.h"
#include "ash/public/cpp/ash_web_view.h"
#include "base/functional/callback_forward.h"

class GURL;

namespace ash {

// Lets PickerController in Ash to communicate with the browser.
class ASH_PUBLIC_EXPORT PickerClient {
public:
using DownloadGifToStringCallback =
base::OnceCallback<void(const std::string& gif_data)>;

virtual std::unique_ptr<ash::AshWebView> CreateWebView(
const ash::AshWebView::InitParams& params) = 0;

// Downloads a gif from `url`. If the download is successful, the gif is
// passed to `callback` as a string of encoded bytes in gif format. Otherwise,
// `callback` is run with an empty string.
virtual void DownloadGifToString(const GURL& url,
DownloadGifToStringCallback callback) = 0;

protected:
PickerClient();
virtual ~PickerClient();
Expand Down

0 comments on commit 21f7c3f

Please sign in to comment.