blob: 518a3ba2325e8c2117a32dad0d4dce7ec367955b [file] [log] [blame]
// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "components/eye_dropper/eye_dropper_view.h"
#include <utility>
#include "base/memory/raw_ptr.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "components/color/color_id.h"
#include "components/eye_dropper/features.h"
#include "content/public/browser/desktop_capture.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/web_contents.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/display/screen.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/gfx/native_widget_types.h"
#include "ui/views/widget/widget.h"
#if BUILDFLAG(IS_WIN)
#include "base/win/windows_version.h"
#endif
#if BUILDFLAG(IS_CHROMEOS_ASH)
#include "ui/aura/window_tree_host.h"
#endif
namespace eye_dropper {
class EyeDropperView::ViewPositionHandler {
public:
explicit ViewPositionHandler(EyeDropperView* owner);
ViewPositionHandler(const ViewPositionHandler&) = delete;
ViewPositionHandler& operator=(const ViewPositionHandler&) = delete;
~ViewPositionHandler();
private:
void UpdateViewPosition();
// Timer used for updating the window location.
base::RepeatingTimer timer_;
raw_ptr<EyeDropperView> owner_;
};
EyeDropperView::ViewPositionHandler::ViewPositionHandler(EyeDropperView* owner)
: owner_(owner) {
// TODO(iopopesc): Use SetCapture instead of a timer when support for
// activating the eye dropper without closing the color popup is added.
timer_.Start(FROM_HERE, base::Hertz(60), this,
&EyeDropperView::ViewPositionHandler::UpdateViewPosition);
}
EyeDropperView::ViewPositionHandler::~ViewPositionHandler() {
timer_.AbandonAndStop();
}
void EyeDropperView::ViewPositionHandler::UpdateViewPosition() {
owner_->OnCursorPositionUpdate(
display::Screen::GetScreen()->GetCursorScreenPoint());
}
class EyeDropperView::ScreenCapturer
: public webrtc::DesktopCapturer::Callback {
public:
explicit ScreenCapturer(EyeDropperView* owner);
ScreenCapturer(const ScreenCapturer&) = delete;
ScreenCapturer& operator=(const ScreenCapturer&) = delete;
~ScreenCapturer() override = default;
// webrtc::DesktopCapturer::Callback:
void OnCaptureResult(webrtc::DesktopCapturer::Result result,
std::unique_ptr<webrtc::DesktopFrame> frame) override;
void CaptureScreen(std::optional<webrtc::DesktopCapturer::SourceId> screen);
SkBitmap GetBitmap() const;
SkColor GetColor(int x, int y) const;
int original_offset_x() const;
int original_offset_y() const;
private:
raw_ptr<EyeDropperView> owner_;
std::unique_ptr<webrtc::DesktopCapturer> capturer_;
SkBitmap frame_;
int original_offset_x_;
int original_offset_y_;
};
EyeDropperView::ScreenCapturer::ScreenCapturer(EyeDropperView* owner)
: owner_(owner) {
static bool allow_wgc_screen_capture =
#if BUILDFLAG(IS_WIN)
// Allow WGC screen capture if Windows version is greater or equal
// than 10.0.20348.0, as the following API, which controls if a border is
// to be painted around the captured content, needs to be present as the
// border should not be shown during eye dropper color selection:
// https://learn.microsoft.com/en-us/uwp/api/windows.graphics.capture.graphicscapturesession.isborderrequired
base::win::GetVersion() >= base::win::Version::SERVER_2022 &&
#endif // BUILDFLAG(IS_WIN)
base::FeatureList::IsEnabled(features::kAllowEyeDropperWGCScreenCapture);
// TODO(iopopesc): Update the captured frame after a period of time to match
// latest content on screen.
capturer_ =
content::desktop_capture::CreateScreenCapturer(allow_wgc_screen_capture);
if (capturer_) {
capturer_->Start(this);
if (allow_wgc_screen_capture) {
capturer_->SelectSource(webrtc::kFullDesktopScreenId);
}
}
// On ChromeOS this will capture `Screen::GetDisplayForNewWindows()`.
CaptureScreen(std::nullopt);
}
void EyeDropperView::ScreenCapturer::CaptureScreen(
std::optional<webrtc::DesktopCapturer::SourceId> screen) {
if (capturer_) {
if (screen) {
capturer_->SelectSource(*screen);
}
capturer_->CaptureFrame();
}
}
void EyeDropperView::ScreenCapturer::OnCaptureResult(
webrtc::DesktopCapturer::Result result,
std::unique_ptr<webrtc::DesktopFrame> frame) {
if (result != webrtc::DesktopCapturer::Result::SUCCESS) {
return;
}
frame_.allocN32Pixels(frame->size().width(), frame->size().height(), true);
memcpy(frame_.getAddr32(0, 0), frame->data(),
frame->size().height() * frame->stride());
frame_.setImmutable();
// The captured frame is in full desktop coordinates. E.g. the top left
// monitor should start from (0, 0), so we need to compute the correct
// origins.
original_offset_x_ = 0;
original_offset_y_ = 0;
for (const auto& display : display::Screen::GetScreen()->GetAllDisplays()) {
#if BUILDFLAG(IS_WIN)
// The window parameter is intentionally passed as nullptr on Windows
// because a non-null window parameter causes errors when restoring windows
// to saved positions in variable-DPI situations. See
// https://crbug.com/1224715 for details.
gfx::Rect scaled_bounds =
display::Screen::GetScreen()->DIPToScreenRectInWindow(
/*window=*/nullptr, display.bounds());
#else
gfx::Rect scaled_bounds = gfx::ScaleToEnclosingRect(
display.bounds(), display.device_scale_factor());
#endif
if (scaled_bounds.origin().x() < original_offset_x_) {
original_offset_x_ = scaled_bounds.origin().x();
}
if (scaled_bounds.origin().y() < original_offset_y_) {
original_offset_y_ = scaled_bounds.origin().y();
}
}
owner_->OnScreenCaptured();
}
SkBitmap EyeDropperView::ScreenCapturer::GetBitmap() const {
return frame_;
}
SkColor EyeDropperView::ScreenCapturer::GetColor(int x, int y) const {
// It's not clear how control can reach here with out-of-bounds coordinates,
// but avoid a crash if it does.
return (x < 0 || x >= frame_.width() || y < 0 || y >= frame_.height())
? gfx::kPlaceholderColor
: frame_.getColor(x, y);
}
int EyeDropperView::ScreenCapturer::original_offset_x() const {
return original_offset_x_;
}
int EyeDropperView::ScreenCapturer::original_offset_y() const {
return original_offset_y_;
}
EyeDropperView::EyeDropperView(gfx::NativeView parent,
gfx::NativeView event_handler,
content::EyeDropperListener* listener)
: listener_(listener),
view_position_handler_(std::make_unique<ViewPositionHandler>(this)),
screen_capturer_(std::make_unique<ScreenCapturer>(this)) {
SetModalType(ui::MODAL_TYPE_WINDOW);
// This is owned as a unique_ptr<EyeDropper> elsewhere.
SetOwnedByWidget(false);
// TODO(pbos): Remove this, perhaps by separating the contents view from the
// EyeDropper/WidgetDelegate.
set_owned_by_client();
SetPreferredSize(GetSize());
#if BUILDFLAG(IS_LINUX)
// Use TYPE_MENU for Linux to ensure that the eye dropper view is displayed
// above the color picker.
views::Widget::InitParams params(
views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
views::Widget::InitParams::TYPE_MENU);
#else
views::Widget::InitParams params(
views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
views::Widget::InitParams::TYPE_POPUP);
#endif
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
// Use software compositing to prevent situations when the widget is not
// translucent when moved fast.
// TODO(iopopesc): Investigate if this is a compositor bug or this is indeed
// an intentional limitation.
params.force_software_compositing = true;
params.z_order = ui::ZOrderLevel::kFloatingWindow;
params.name = "MagnifierHost";
params.parent = parent;
params.delegate = this;
views::Widget* widget = new views::Widget();
widget->Init(std::move(params));
widget->SetContentsView(this);
HideCursor();
pre_dispatch_handler_ =
std::make_unique<PreEventDispatchHandler>(this, event_handler);
widget->Show();
CaptureInput();
auto* screen = display::Screen::GetScreen();
gfx::Point initial_position = screen->GetCursorScreenPoint();
#if BUILDFLAG(IS_CHROMEOS_ASH)
if (screen->InTabletMode()) {
initial_position =
screen->GetDisplayForNewWindows().work_area().CenterPoint();
}
#endif
UpdatePosition(initial_position);
// The ignore selection time should be long enough to allow the user to see
// the UI.
ignore_selection_time_ = base::TimeTicks::Now() + base::Milliseconds(500);
#if BUILDFLAG(IS_CHROMEOS_ASH)
// Add an observation so the capture can be updated as the eye dropper window
// moves between displays.
window_observation_.Observe(GetWidget()->GetNativeWindow());
#endif
}
EyeDropperView::~EyeDropperView() {
if (GetWidget()) {
GetWidget()->CloseNow();
}
}
void EyeDropperView::OnCursorPositionUpdate(gfx::Point cursor_position) {
// The view can be moved by either mouse or touch. Only move it to the cursor
// position when cursor changes.
if (std::exchange(last_cursor_position_, cursor_position) !=
cursor_position) {
UpdatePosition(std::move(cursor_position));
}
}
void EyeDropperView::OnPaint(gfx::Canvas* view_canvas) {
if (screen_capturer_->GetBitmap().drawsNothing()) {
return;
}
const float diameter = GetDiameter();
constexpr float kPixelSize = 10;
const gfx::Size padding((size().width() - diameter) / 2,
(size().height() - diameter) / 2);
if (views::Widget::IsWindowCompositingSupported()) {
// Clip circle for magnified projection only when the widget
// supports translucency.
SkPath clip_path;
clip_path.addOval(SkRect::MakeXYWH(padding.width(), padding.height(),
diameter, diameter));
clip_path.close();
view_canvas->ClipPath(clip_path, true);
}
// Project pixels.
const int pixel_count = diameter / kPixelSize;
const SkBitmap frame = screen_capturer_->GetBitmap();
gfx::Point center_position_px;
#if BUILDFLAG(IS_CHROMEOS_ASH)
// ChromeOS only captures a single display at a time, and we need to convert
// the cursor position to display (root window) local pixel coordinates.
aura::Window* window = GetWidget()->GetNativeWindow();
const gfx::Point center_position =
window->GetBoundsInRootWindow().CenterPoint();
center_position_px =
window->GetHost()->GetRootTransform().MapPoint(center_position);
#else
// The captured frame is not scaled so we need to use widget's bounds in
// pixels to have the magnified region match cursor position.
center_position_px =
display::Screen::GetScreen()
->DIPToScreenRectInWindow(GetWidget()->GetNativeWindow(),
GetWidget()->GetWindowBoundsInScreen())
.CenterPoint();
center_position_px.Offset(-screen_capturer_->original_offset_x(),
-screen_capturer_->original_offset_y());
#endif
view_canvas->DrawImageInt(gfx::ImageSkia::CreateFrom1xBitmap(frame),
center_position_px.x() - pixel_count / 2,
center_position_px.y() - pixel_count / 2,
pixel_count, pixel_count, padding.width(),
padding.height(), diameter, diameter, false);
// Store the pixel color under the cursor as it is the last color seen
// by the user before selection.
selected_color_ = screen_capturer_->GetColor(center_position_px.x(),
center_position_px.y());
// Paint grid.
const auto* color_provider = GetColorProvider();
cc::PaintFlags flags;
flags.setStrokeWidth(1);
flags.setStyle(cc::PaintFlags::kStroke_Style);
flags.setColor(color_provider->GetColor(color::kColorEyedropperGrid));
for (int i = 0; i < pixel_count; ++i) {
view_canvas->DrawLine(
gfx::PointF(padding.width() + i * kPixelSize, padding.height()),
gfx::PointF(padding.width() + i * kPixelSize,
size().height() - padding.height()),
flags);
view_canvas->DrawLine(
gfx::PointF(padding.width(), padding.height() + i * kPixelSize),
gfx::PointF(size().width() - padding.width(),
padding.height() + i * kPixelSize),
flags);
}
// Paint central pixel.
gfx::RectF pixel((size().width() - kPixelSize) / 2,
(size().height() - kPixelSize) / 2, kPixelSize, kPixelSize);
flags.setAntiAlias(true);
flags.setColor(
color_provider->GetColor(color::kColorEyedropperCentralPixelOuterRing));
flags.setStrokeWidth(2);
pixel.Inset(-0.5f);
view_canvas->DrawRect(pixel, flags);
flags.setColor(
color_provider->GetColor(color::kColorEyedropperCentralPixelInnerRing));
flags.setStrokeWidth(1);
pixel.Inset(0.5f);
view_canvas->DrawRect(pixel, flags);
// Paint outline.
flags.setStrokeWidth(2);
flags.setColor(color_provider->GetColor(color::kColorEyedropperBoundary));
flags.setAntiAlias(true);
if (views::Widget::IsWindowCompositingSupported()) {
view_canvas->DrawCircle(
gfx::PointF(size().width() / 2, size().height() / 2), diameter / 2,
flags);
} else {
view_canvas->DrawRect(bounds(), flags);
}
OnPaintBorder(view_canvas);
}
void EyeDropperView::WindowClosing() {
ShowCursor();
pre_dispatch_handler_.reset();
}
void EyeDropperView::OnWidgetMove() {
// Trigger a repaint since because the widget was moved, the content of the
// view needs to be updated.
SchedulePaint();
}
#if BUILDFLAG(IS_CHROMEOS_ASH)
void EyeDropperView::OnWindowAddedToRootWindow(aura::Window* window) {
display::Display display =
display::Screen::GetScreen()->GetDisplayNearestWindow(window);
CaptureScreen(display.id());
}
void EyeDropperView::OnWindowDestroying(aura::Window* window) {
window_observation_.Reset();
}
#endif
void EyeDropperView::CaptureScreen(
std::optional<webrtc::DesktopCapturer::SourceId> screen) {
screen_capturer_->CaptureScreen(screen);
}
void EyeDropperView::OnScreenCaptured() {
SchedulePaint();
}
void EyeDropperView::UpdatePosition(gfx::Point position) {
GetWidget()->SetBounds(
gfx::Rect(gfx::Point(position.x() - size().width() / 2,
position.y() - size().height() / 2),
size()));
}
void EyeDropperView::OnColorSelected() {
if (!selected_color_.has_value()) {
listener_->ColorSelectionCanceled();
return;
}
// Prevent the user from selecting a color for a period of time.
if (base::TimeTicks::Now() <= ignore_selection_time_) {
return;
}
// Use the last selected color and notify listener.
listener_->ColorSelected(selected_color_.value());
}
void EyeDropperView::OnColorSelectionCanceled() {
listener_->ColorSelectionCanceled();
}
BEGIN_METADATA(EyeDropperView)
ADD_READONLY_PROPERTY_METADATA(gfx::Size, Size)
ADD_READONLY_PROPERTY_METADATA(float, Diameter)
END_METADATA
} // namespace eye_dropper