// 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 "ash/accessibility/magnifier/magnifier_glass.h"
#include "ash/shell.h"
#include "base/check_op.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/paint_recorder.h"
#include "ui/gfx/shadow_value.h"
#include "ui/gfx/skia_paint_util.h"
#include "ui/views/widget/widget.h"
namespace ash {
namespace {
// Inset on the zoom filter.
constexpr int kZoomInset = 0;
// Vertical offset between the center of the magnifier and the tip of the
// pointer. TODO(jdufault): The vertical offset should only apply to the window
// location, not the magnified contents. See crbug.com/637617.
constexpr int kVerticalOffset = 0;
// Name of the magnifier window.
constexpr char kMagniferGlassWindowName[] = "MagnifierGlassWindow";
int GetShadowOffset(const MagnifierGlass::Params& params) {
return std::max(params.bottom_shadow.y(), params.top_shadow.y());
}
int GetShadowThickness(const MagnifierGlass::Params& params) {
return std::max(params.bottom_shadow.blur(), params.top_shadow.blur());
}
gfx::Size GetWindowSize(const MagnifierGlass::Params& params) {
// The diameter of the window is the diameter of the magnifier, border and
// shadow combined. We apply the larger shadow offset on all sides, despite
// the shadow offsets potentially being unequal, so as to keep the circle
// centered in the view and keep calculations (border rendering and content
// masking) simpler.
int window_diameter = (params.radius + params.border_size +
GetShadowThickness(params) + GetShadowOffset(params)) *
2;
return gfx::Size(window_diameter, window_diameter);
}
gfx::Rect GetBounds(const MagnifierGlass::Params& params,
const gfx::Point& point) {
gfx::Size size = GetWindowSize(params);
gfx::Point origin(point.x() - (size.width() / 2),
point.y() - (size.height() / 2) - kVerticalOffset);
return gfx::Rect(origin, size);
}
} // namespace
// The border renderer draws the border as well as outline on both the outer and
// inner radius to increase visibility. The border renderer also handles drawing
// the shadow.
class MagnifierGlass::BorderRenderer : public ui::LayerDelegate {
public:
BorderRenderer(const gfx::Rect& window_bounds,
const MagnifierGlass::Params& params)
: magnifier_window_bounds_(window_bounds), params_(params) {
magnifier_shadows_.push_back(params_.bottom_shadow);
magnifier_shadows_.push_back(params_.top_shadow);
}
BorderRenderer(const BorderRenderer&) = delete;
BorderRenderer& operator=(const BorderRenderer&) = delete;
~BorderRenderer() override = default;
private:
// ui::LayerDelegate:
void OnPaintLayer(const ui::PaintContext& context) override {
ui::PaintRecorder recorder(context, magnifier_window_bounds_.size());
// Draw the shadow.
cc::PaintFlags shadow_flags;
shadow_flags.setAntiAlias(true);
shadow_flags.setColor(SK_ColorTRANSPARENT);
shadow_flags.setLooper(gfx::CreateShadowDrawLooper(magnifier_shadows_));
gfx::Rect shadow_bounds(magnifier_window_bounds_.size());
recorder.canvas()->DrawCircle(shadow_bounds.CenterPoint(),
shadow_bounds.width() / 2 -
GetShadowThickness(params_) -
GetShadowOffset(params_),
shadow_flags);
// The radius of the magnifier and its border.
const int magnifier_radius = params_.radius + params_.border_size;
// Clear the shadow for the magnified area.
cc::PaintFlags mask_flags;
mask_flags.setAntiAlias(true);
mask_flags.setBlendMode(SkBlendMode::kClear);
mask_flags.setStyle(cc::PaintFlags::kFill_Style);
recorder.canvas()->DrawCircle(
magnifier_window_bounds_.CenterPoint(),
magnifier_radius - params_.border_outline_thickness / 2, mask_flags);
cc::PaintFlags border_flags;
border_flags.setAntiAlias(true);
border_flags.setStyle(cc::PaintFlags::kStroke_Style);
// Draw the inner border.
border_flags.setStrokeWidth(params_.border_size);
border_flags.setColor(params_.border_color);
recorder.canvas()->DrawCircle(magnifier_window_bounds_.CenterPoint(),
magnifier_radius - params_.border_size / 2,
border_flags);
// Draw border outer outline and then draw the border inner outline.
border_flags.setStrokeWidth(params_.border_outline_thickness);
border_flags.setColor(params_.border_outline_color);
recorder.canvas()->DrawCircle(
magnifier_window_bounds_.CenterPoint(),
magnifier_radius - params_.border_outline_thickness / 2, border_flags);
recorder.canvas()->DrawCircle(magnifier_window_bounds_.CenterPoint(),
magnifier_radius - params_.border_size +
params_.border_outline_thickness / 2,
border_flags);
}
void OnDeviceScaleFactorChanged(float old_device_scale_factor,
float new_device_scale_factor) override {}
const gfx::Rect magnifier_window_bounds_;
const Params params_;
std::vector<gfx::ShadowValue> magnifier_shadows_;
};
MagnifierGlass::MagnifierGlass(Params params) : params_(std::move(params)) {}
MagnifierGlass::~MagnifierGlass() {
CloseMagnifierWindow();
CHECK(!views::WidgetObserver::IsInObserverList());
}
void MagnifierGlass::ShowFor(aura::Window* root_window,
const gfx::Point& location_in_root) {
if (!host_widget_) {
CreateMagnifierWindow(root_window, location_in_root);
return;
}
if (root_window != host_widget_->GetNativeView()->GetRootWindow()) {
CloseMagnifierWindow();
CreateMagnifierWindow(root_window, location_in_root);
return;
}
host_widget_->SetBounds(GetBounds(params_, location_in_root));
}
void MagnifierGlass::Close() {
CloseMagnifierWindow();
}
void MagnifierGlass::OnWindowDestroying(aura::Window* window) {
CloseMagnifierWindow();
}
void MagnifierGlass::OnWidgetDestroying(views::Widget* widget) {
DCHECK_EQ(widget, host_widget_);
RemoveZoomWidgetObservers();
host_widget_ = nullptr;
}
void MagnifierGlass::CreateMagnifierWindow(aura::Window* root_window,
const gfx::Point& location_in_root) {
if (host_widget_ || !root_window)
return;
root_window->AddObserver(this);
host_widget_ = new views::Widget;
views::Widget::InitParams params(
views::Widget::InitParams::NATIVE_WIDGET_OWNS_WIDGET,
views::Widget::InitParams::TYPE_WINDOW_FRAMELESS);
params.activatable = views::Widget::InitParams::Activatable::kNo;
params.accept_events = false;
params.bounds = GetBounds(params_, location_in_root);
params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent;
params.name = kMagniferGlassWindowName;
params.parent = root_window;
host_widget_->Init(std::move(params));
host_widget_->set_focus_on_creation(false);
host_widget_->Show();
ui::Layer* root_layer = host_widget_->GetNativeView()->layer();
const gfx::Size window_size = GetWindowSize(params_);
const gfx::Rect window_bounds = gfx::Rect(window_size);
zoom_layer_ = std::make_unique<ui::Layer>(ui::LAYER_SOLID_COLOR);
zoom_layer_->SetBounds(window_bounds);
zoom_layer_->SetBackgroundZoom(params_.scale, kZoomInset);
zoom_layer_->SetFillsBoundsOpaquely(false);
root_layer->Add(zoom_layer_.get());
// Create a rounded rect clip, so that only we see a circle of the zoomed
// content. This circle radius should match that of the drawn border.
const gfx::RoundedCornersF kRoundedCorners{
static_cast<float>(params_.radius)};
zoom_layer_->SetRoundedCornerRadius(kRoundedCorners);
gfx::Rect clip_rect = window_bounds;
clip_rect.ClampToCenteredSize(
gfx::Size(params_.radius * 2, params_.radius * 2));
zoom_layer_->SetClipRect(clip_rect);
border_layer_ = std::make_unique<ui::Layer>();
border_layer_->SetBounds(window_bounds);
border_renderer_ = std::make_unique<BorderRenderer>(window_bounds, params_);
border_layer_->set_delegate(border_renderer_.get());
border_layer_->SetFillsBoundsOpaquely(false);
root_layer->Add(border_layer_.get());
host_widget_->AddObserver(this);
}
void MagnifierGlass::CloseMagnifierWindow() {
if (host_widget_) {
RemoveZoomWidgetObservers();
host_widget_->Close();
host_widget_ = nullptr;
}
}
void MagnifierGlass::RemoveZoomWidgetObservers() {
DCHECK(host_widget_);
host_widget_->RemoveObserver(this);
aura::Window* root_window = host_widget_->GetNativeView()->GetRootWindow();
DCHECK(root_window);
root_window->RemoveObserver(this);
}
} // namespace ash