| // Copyright (c) 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chromecast/graphics/accessibility/partial_magnification_controller.h" |
| |
| #include "third_party/skia/include/core/SkDrawLooper.h" |
| #include "ui/aura/window.h" |
| #include "ui/aura/window_event_dispatcher.h" |
| #include "ui/aura/window_tree_host.h" |
| #include "ui/compositor/layer.h" |
| #include "ui/compositor/paint_recorder.h" |
| #include "ui/events/event.h" |
| #include "ui/events/event_constants.h" |
| #include "ui/gfx/shadow_value.h" |
| #include "ui/gfx/skia_paint_util.h" |
| #include "ui/views/widget/widget.h" |
| #include "ui/wm/core/coordinate_conversion.h" |
| |
| namespace chromecast { |
| namespace { |
| |
| // Default ratio of magnifier scale. |
| const float kDefaultMagnificationScale = 2.f; |
| |
| // Radius of the magnifying glass in DIP. This does not include the thickness |
| // of the magnifying glass shadow and border. |
| const int kMagnifierRadius = 188; |
| // Size of the border around the magnifying glass in DIP. |
| const int kBorderSize = 10; |
| // Thickness of the outline around magnifying glass border in DIP. |
| const int kBorderOutlineThickness = 1; |
| // Thickness of the shadow around the magnifying glass in DIP. |
| const int kShadowThickness = 24; |
| // Offset of the shadow around the magnifying glass in DIP. One of the shadows |
| // is lowered a bit, so we have to include |kShadowOffset| in our calculations |
| // to compensate. |
| const int kShadowOffset = 24; |
| // The color of the border and its outlines. The border has an outline on both |
| // sides, producing a black/white/black ring. |
| const SkColor kBorderColor = SkColorSetARGB(204, 255, 255, 255); |
| const SkColor kBorderOutlineColor = SkColorSetARGB(51, 0, 0, 0); |
| // The colors of the two shadow around the magnifiying glass. |
| const SkColor kTopShadowColor = SkColorSetARGB(26, 0, 0, 0); |
| const SkColor kBottomShadowColor = SkColorSetARGB(61, 0, 0, 0); |
| // Inset on the zoom filter. |
| const int kZoomInset = 0; |
| // Vertical offset between the center of the magnifier and the tip of the |
| // pointer. |
| const int kVerticalOffset = 0; |
| |
| // Name of the magnifier window. |
| const char kPartialMagniferWindowName[] = "PartialMagnifierWindow"; |
| |
| gfx::Size GetWindowSize() { |
| // The diameter of the window is the diameter of the magnifier, border and |
| // shadow combined. We apply |kShadowOffset| on all sides even though the |
| // shadow is only thicker on the bottom so as to keep the circle centered in |
| // the view and keep calculations (border rendering and content masking) |
| // simpler. |
| int window_diameter = |
| (kMagnifierRadius + kBorderSize + kShadowThickness + kShadowOffset) * 2; |
| return gfx::Size(window_diameter, window_diameter); |
| } |
| |
| gfx::Rect GetBounds(gfx::Point mouse) { |
| gfx::Size size = GetWindowSize(); |
| gfx::Point origin(mouse.x() - (size.width() / 2), |
| mouse.y() - (size.height() / 2) - kVerticalOffset); |
| return gfx::Rect(origin, size); |
| } |
| |
| } // namespace |
| |
| // The content mask provides a clipping layer for the magnification window so we |
| // can show a circular magnifier. |
| class PartialMagnificationController::ContentMask : public ui::LayerDelegate { |
| public: |
| // If |is_border| is true, the circle will be a stroke. This is useful if we |
| // wish to clip a border. |
| ContentMask(bool is_border, gfx::Size mask_bounds) |
| : layer_(ui::LAYER_TEXTURED), is_border_(is_border) { |
| layer_.set_delegate(this); |
| layer_.SetFillsBoundsOpaquely(false); |
| layer_.SetBounds(gfx::Rect(mask_bounds)); |
| } |
| |
| ~ContentMask() override { layer_.set_delegate(nullptr); } |
| |
| ui::Layer* layer() { return &layer_; } |
| |
| private: |
| // ui::LayerDelegate: |
| void OnPaintLayer(const ui::PaintContext& context) override { |
| ui::PaintRecorder recorder(context, layer()->size()); |
| |
| cc::PaintFlags flags; |
| flags.setAlpha(255); |
| flags.setAntiAlias(true); |
| // Stroke is used for clipping the border which consists of the rendered |
| // border |kBorderSize| and the magnifier shadow |kShadowThickness| and |
| // |kShadowOffset|. |
| flags.setStrokeWidth(kBorderSize + kShadowThickness + kShadowOffset); |
| flags.setStyle(is_border_ ? cc::PaintFlags::kStroke_Style |
| : cc::PaintFlags::kFill_Style); |
| |
| // If we want to clip the magnifier zone use the magnifiers radius. |
| // Otherwise we want to clip the border, shadow and shadow offset so we |
| // start |
| // at the halfway point of the stroke width. |
| gfx::Rect rect(layer()->bounds().size()); |
| int clipping_radius = kMagnifierRadius; |
| if (is_border_) |
| clipping_radius += (kShadowThickness + kShadowOffset + kBorderSize) / 2; |
| recorder.canvas()->DrawCircle(rect.CenterPoint(), clipping_radius, flags); |
| } |
| |
| void OnDeviceScaleFactorChanged(float old_device_scale_factor, |
| float new_device_scale_factor) override { |
| // Redrawing will take care of scale factor change. |
| } |
| |
| ui::Layer layer_; |
| bool is_border_; |
| DISALLOW_COPY_AND_ASSIGN(ContentMask); |
| }; |
| |
| // 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 PartialMagnificationController::BorderRenderer |
| : public ui::LayerDelegate { |
| public: |
| explicit BorderRenderer(const gfx::Rect& window_bounds) |
| : magnifier_window_bounds_(window_bounds) { |
| magnifier_shadows_.push_back(gfx::ShadowValue( |
| gfx::Vector2d(0, kShadowOffset), kShadowThickness, kBottomShadowColor)); |
| magnifier_shadows_.push_back(gfx::ShadowValue( |
| gfx::Vector2d(0, 0), kShadowThickness, kTopShadowColor)); |
| } |
| |
| ~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 - kShadowThickness - kShadowOffset, |
| shadow_flags); |
| |
| cc::PaintFlags border_flags; |
| border_flags.setAntiAlias(true); |
| border_flags.setStyle(cc::PaintFlags::kStroke_Style); |
| |
| // The radius of the magnifier and its border. |
| const int magnifier_radius = kMagnifierRadius + kBorderSize; |
| |
| // Draw the inner border. |
| border_flags.setStrokeWidth(kBorderSize); |
| border_flags.setColor(kBorderColor); |
| recorder.canvas()->DrawCircle(magnifier_window_bounds_.CenterPoint(), |
| magnifier_radius - kBorderSize / 2, |
| border_flags); |
| |
| // Draw border outer outline and then draw the border inner outline. |
| border_flags.setStrokeWidth(kBorderOutlineThickness); |
| border_flags.setColor(kBorderOutlineColor); |
| recorder.canvas()->DrawCircle( |
| magnifier_window_bounds_.CenterPoint(), |
| magnifier_radius - kBorderOutlineThickness / 2, border_flags); |
| recorder.canvas()->DrawCircle( |
| magnifier_window_bounds_.CenterPoint(), |
| magnifier_radius - kBorderSize + kBorderOutlineThickness / 2, |
| border_flags); |
| } |
| |
| void OnDeviceScaleFactorChanged(float old_device_scale_factor, |
| float new_device_scale_factor) override {} |
| |
| gfx::Rect magnifier_window_bounds_; |
| std::vector<gfx::ShadowValue> magnifier_shadows_; |
| |
| DISALLOW_COPY_AND_ASSIGN(BorderRenderer); |
| }; |
| |
| PartialMagnificationController::PartialMagnificationController( |
| aura::Window* root_window) |
| : magnification_scale_(kDefaultMagnificationScale), |
| root_window_(root_window) { |
| root_window_->AddPreTargetHandler(this); |
| } |
| |
| PartialMagnificationController::~PartialMagnificationController() { |
| CloseMagnifierWindow(); |
| root_window_->RemovePreTargetHandler(this); |
| } |
| |
| void PartialMagnificationController::SetEnabled(bool enabled) { |
| is_enabled_ = enabled; |
| SetActive(false); |
| } |
| |
| bool PartialMagnificationController::IsEnabled() const { |
| return is_enabled_; |
| } |
| |
| void PartialMagnificationController::SwitchTargetRootWindowIfNeeded( |
| aura::Window* new_root_window) { |
| if (host_widget_ && |
| new_root_window == host_widget_->GetNativeView()->GetRootWindow()) |
| return; |
| |
| if (!new_root_window) |
| new_root_window = root_window_; |
| |
| if (is_enabled_ && is_active_) { |
| CloseMagnifierWindow(); |
| CreateMagnifierWindow(new_root_window); |
| } |
| } |
| |
| void PartialMagnificationController::OnWindowDestroying(aura::Window* window) { |
| CloseMagnifierWindow(); |
| |
| aura::Window* new_root_window = root_window_; |
| if (new_root_window != window) |
| SwitchTargetRootWindowIfNeeded(new_root_window); |
| } |
| |
| void PartialMagnificationController::OnWidgetDestroying(views::Widget* widget) { |
| DCHECK_EQ(widget, host_widget_); |
| RemoveZoomWidgetObservers(); |
| host_widget_ = nullptr; |
| } |
| |
| void PartialMagnificationController::SetActive(bool active) { |
| // Fail if we're trying to activate while disabled. |
| DCHECK(is_enabled_ || !active); |
| |
| is_active_ = active; |
| if (is_active_) { |
| CreateMagnifierWindow(root_window_); |
| } else { |
| CloseMagnifierWindow(); |
| } |
| } |
| |
| void PartialMagnificationController::OnTouchEvent(ui::TouchEvent* event) { |
| if (!is_enabled_) { |
| return; |
| } |
| |
| // Compute the event location in screen space. |
| aura::Window* target = static_cast<aura::Window*>(event->target()); |
| aura::Window* event_root = target->GetRootWindow(); |
| gfx::Point screen_point = event->root_location(); |
| wm::ConvertPointToScreen(event_root, &screen_point); |
| |
| // TODO(rdaum): Touch pressed is probably not what we want here, we'll |
| // probably want a specific gesture for dragging the magnifier around. |
| if (event->type() == ui::ET_TOUCH_PRESSED) { |
| SetActive(true); |
| } |
| |
| if (event->type() == ui::ET_TOUCH_RELEASED) |
| SetActive(false); |
| |
| if (!is_active_) |
| return; |
| |
| // If the previous root window was detached host_widget_ will be null; |
| // reconstruct it. We also need to change the root window if the cursor has |
| // crossed display boundries. |
| SwitchTargetRootWindowIfNeeded(root_window_); |
| |
| // If that failed for any reason return. |
| if (!host_widget_) { |
| SetActive(false); |
| return; |
| } |
| |
| // Remap point from where it was captured to the display it is actually on. |
| gfx::Point point = event->root_location(); |
| aura::Window::ConvertPointToTarget( |
| event_root, host_widget_->GetNativeView()->GetRootWindow(), &point); |
| host_widget_->SetBounds(GetBounds(point)); |
| } |
| |
| void PartialMagnificationController::CreateMagnifierWindow( |
| aura::Window* root_window) { |
| if (host_widget_ || !root_window) |
| return; |
| |
| root_window->AddObserver(this); |
| |
| gfx::Point mouse( |
| root_window->GetHost()->dispatcher()->GetLastMouseLocationInRoot()); |
| |
| host_widget_ = new views::Widget; |
| views::Widget::InitParams params( |
| views::Widget::InitParams::TYPE_WINDOW_FRAMELESS); |
| params.activatable = views::Widget::InitParams::ACTIVATABLE_NO; |
| params.accept_events = false; |
| params.bounds = GetBounds(mouse); |
| params.opacity = views::Widget::InitParams::TRANSLUCENT_WINDOW; |
| params.parent = root_window; |
| host_widget_->Init(params); |
| host_widget_->set_focus_on_creation(false); |
| host_widget_->Show(); |
| |
| aura::Window* window = host_widget_->GetNativeView(); |
| window->SetName(kPartialMagniferWindowName); |
| |
| ui::Layer* root_layer = host_widget_->GetNativeView()->layer(); |
| |
| zoom_layer_.reset(new ui::Layer(ui::LayerType::LAYER_SOLID_COLOR)); |
| zoom_layer_->SetBounds(gfx::Rect(GetWindowSize())); |
| zoom_layer_->SetBackgroundZoom(magnification_scale_, kZoomInset); |
| root_layer->Add(zoom_layer_.get()); |
| |
| border_layer_.reset(new ui::Layer(ui::LayerType::LAYER_TEXTURED)); |
| border_layer_->SetBounds(gfx::Rect(GetWindowSize())); |
| border_renderer_.reset(new BorderRenderer(gfx::Rect(GetWindowSize()))); |
| border_layer_->set_delegate(border_renderer_.get()); |
| border_layer_->SetFillsBoundsOpaquely(false); |
| root_layer->Add(border_layer_.get()); |
| |
| border_mask_.reset(new ContentMask(true, GetWindowSize())); |
| border_layer_->SetMaskLayer(border_mask_->layer()); |
| |
| zoom_mask_.reset(new ContentMask(false, GetWindowSize())); |
| zoom_layer_->SetMaskLayer(zoom_mask_->layer()); |
| |
| host_widget_->AddObserver(this); |
| } |
| |
| void PartialMagnificationController::CloseMagnifierWindow() { |
| if (host_widget_) { |
| RemoveZoomWidgetObservers(); |
| host_widget_->Close(); |
| host_widget_ = nullptr; |
| } |
| } |
| |
| void PartialMagnificationController::RemoveZoomWidgetObservers() { |
| DCHECK(host_widget_); |
| host_widget_->RemoveObserver(this); |
| aura::Window* root_window = host_widget_->GetNativeView()->GetRootWindow(); |
| DCHECK(root_window); |
| root_window->RemoveObserver(this); |
| } |
| |
| void PartialMagnificationController::SetMagnificationScale( |
| float magnification_scale) { |
| magnification_scale_ = magnification_scale; |
| // TODO(rdaum): This is probably going to require a redraw/refresh if the |
| // magnifier is currently visible. |
| } |
| |
| } // namespace chromecast |