// Copyright 2012 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/fullscreen_magnifier_controller.h"
#include <algorithm>
#include <memory>
#include <utility>
#include <vector>
#include "ash/accessibility/accessibility_controller.h"
#include "ash/accessibility/accessibility_delegate.h"
#include "ash/accessibility/magnifier/magnifier_utils.h"
#include "ash/display/root_window_transformers.h"
#include "ash/host/ash_window_tree_host.h"
#include "ash/host/root_window_transformer.h"
#include "ash/keyboard/ui/keyboard_ui_controller.h"
#include "ash/public/cpp/accessibility_controller_enums.h"
#include "ash/public/cpp/shell_window_ids.h"
#include "ash/root_window_controller.h"
#include "ash/shell.h"
#include "ui/accessibility/accessibility_switches.h"
#include "ui/aura/client/cursor_client.h"
#include "ui/aura/window.h"
#include "ui/aura/window_tree_host.h"
#include "ui/compositor/layer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/events/event.h"
#include "ui/events/event_handler.h"
#include "ui/events/gestures/gesture_provider_aura.h"
#include "ui/events/types/event_type.h"
#include "ui/gfx/geometry/dip_util.h"
#include "ui/gfx/geometry/point3_f.h"
#include "ui/gfx/geometry/point_conversions.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/wm/core/coordinate_conversion.h"
namespace ash {
namespace {
constexpr float kMaxMagnifiedScale = 20.0f;
constexpr float kMinMagnifiedScaleThreshold = 1.1f;
constexpr float kNonMagnifiedScale = 1.0f;
constexpr float kInitialMagnifiedScale = 2.0f;
constexpr float kScrollScaleChangeFactor = 0.00125f;
// Default animation parameters for redrawing the magnification window.
constexpr gfx::Tween::Type kDefaultAnimationTweenType = gfx::Tween::EASE_OUT;
constexpr int kDefaultAnimationDurationInMs = 100;
// Use linear transformation to make the magnifier window move smoothly
// to center the focus when user types in a text input field.
constexpr gfx::Tween::Type kCenterCaretAnimationTweenType = gfx::Tween::LINEAR;
// Threshold of panning. If the cursor moves to within pixels (in DIP) of
// |kCursorPanningMargin| from the edge, the view-port moves.
constexpr int kCursorPanningMargin = 100;
// Threshold of panning at the bottom when the virtual keyboard is up. If the
// cursor moves to within pixels (in DIP) of |kKeyboardBottomPanningMargin| from
// the bottom edge, the view-port moves. This is only used by
// MoveMagnifierWindowFollowPoint() when |reduce_bottom_margin| is true.
constexpr int kKeyboardBottomPanningMargin = 10;
} // namespace
class FullscreenMagnifierController::GestureProviderClient
: public ui::GestureProviderAuraClient {
public:
GestureProviderClient() = default;
GestureProviderClient(const GestureProviderClient&) = delete;
GestureProviderClient& operator=(const GestureProviderClient&) = delete;
~GestureProviderClient() override = default;
// ui::GestureProviderAuraClient overrides:
void OnGestureEvent(GestureConsumer* consumer,
ui::GestureEvent* event) override {
// Do nothing. OnGestureEvent is for timer based gesture events, e.g. tap.
// FullscreenMagnifierController is interested only in pinch and scroll
// gestures.
DCHECK_NE(ui::EventType::kGestureScrollBegin, event->type());
DCHECK_NE(ui::EventType::kGestureScrollEnd, event->type());
DCHECK_NE(ui::EventType::kGestureScrollUpdate, event->type());
DCHECK_NE(ui::EventType::kGesturePinchBegin, event->type());
DCHECK_NE(ui::EventType::kGesturePinchEnd, event->type());
DCHECK_NE(ui::EventType::kGesturePinchUpdate, event->type());
}
};
FullscreenMagnifierController::FullscreenMagnifierController()
: root_window_(Shell::GetPrimaryRootWindow()),
scale_(kNonMagnifiedScale),
original_scale_(kNonMagnifiedScale) {
Shell::Get()->AddAccessibilityEventHandler(
this,
AccessibilityEventHandlerManager::HandlerType::kFullscreenMagnifier);
root_window_->AddObserver(this);
root_window_->GetHost()->GetEventSource()->AddEventRewriter(this);
point_of_interest_in_root_ = root_window_->bounds().CenterPoint();
gesture_provider_client_ = std::make_unique<GestureProviderClient>();
gesture_provider_ = std::make_unique<ui::GestureProviderAura>(
this, gesture_provider_client_.get());
magnifier_debug_draw_rect_ = ::switches::IsMagnifierDebugDrawRectEnabled();
}
FullscreenMagnifierController::~FullscreenMagnifierController() {
root_window_->GetHost()->GetEventSource()->RemoveEventRewriter(this);
root_window_->RemoveObserver(this);
Shell::Get()->RemoveAccessibilityEventHandler(this);
}
void FullscreenMagnifierController::SetEnabled(bool enabled) {
if (enabled) {
Shell* shell = Shell::Get();
float scale =
shell->accessibility_delegate()->GetSavedScreenMagnifierScale();
if (scale <= 0.0f)
scale = kInitialMagnifiedScale;
ValidateScale(&scale);
// Do nothing, if already enabled with same scale.
if (is_enabled_ && scale == scale_)
return;
is_enabled_ = enabled;
RedrawKeepingMousePosition(scale, true, false);
shell->accessibility_delegate()->SaveScreenMagnifierScale(scale);
} else {
// Do nothing, if already disabled.
if (!is_enabled_)
return;
RedrawKeepingMousePosition(kNonMagnifiedScale, true, false);
is_enabled_ = enabled;
}
// Keyboard overscroll creates layout issues with fullscreen magnification
// so it needs to be disabled when magnification is enabled.
// TODO(spqchan): Fix the keyboard overscroll issues.
auto config = keyboard::KeyboardUIController::Get()->keyboard_config();
config.overscroll_behavior =
is_enabled_ ? keyboard::KeyboardOverscrollBehavior::kDisabled
: keyboard::KeyboardOverscrollBehavior::kDefault;
keyboard::KeyboardUIController::Get()->UpdateKeyboardConfig(config);
}
bool FullscreenMagnifierController::IsEnabled() const {
return is_enabled_;
}
void FullscreenMagnifierController::SetScale(float scale, bool animate) {
if (!is_enabled_)
return;
ValidateScale(&scale);
Shell::Get()->accessibility_delegate()->SaveScreenMagnifierScale(scale);
RedrawKeepingMousePosition(scale, animate, false);
}
void FullscreenMagnifierController::StepToNextScaleValue(int delta_index) {
SetScale(magnifier_utils::GetNextMagnifierScaleValue(
delta_index, GetScale(), kNonMagnifiedScale, kMaxMagnifiedScale),
true /* animate */);
}
void FullscreenMagnifierController::MoveWindow(int x, int y, bool animate) {
if (!is_enabled_)
return;
Redraw(gfx::PointF(x, y), scale_, animate);
}
void FullscreenMagnifierController::MoveWindow(const gfx::Point& point,
bool animate) {
if (!is_enabled_)
return;
Redraw(gfx::PointF(point), scale_, animate);
}
gfx::Point FullscreenMagnifierController::GetWindowPosition() const {
return gfx::ToFlooredPoint(origin_);
}
void FullscreenMagnifierController::SetScrollDirection(
ScrollDirection direction) {
scroll_direction_ = direction;
StartOrStopScrollIfNecessary();
}
gfx::Rect FullscreenMagnifierController::GetViewportRect() const {
return gfx::ToEnclosingRect(GetWindowRectDIP(scale_));
}
void FullscreenMagnifierController::CenterOnPoint(
const gfx::Point& point_in_screen) {
gfx::Point point_in_root = point_in_screen;
::wm::ConvertPointFromScreen(root_window_, &point_in_root);
MoveMagnifierWindowCenterPoint(point_in_root);
}
void FullscreenMagnifierController::HandleMoveMagnifierToRect(
const gfx::Rect& rect_in_screen) {
gfx::Rect node_bounds_in_root = rect_in_screen;
::wm::ConvertRectFromScreen(root_window_, &node_bounds_in_root);
if (GetViewportRect().Contains(node_bounds_in_root))
return;
// Hide the cursor since this can cause jumps.
Shell::Get()->cursor_manager()->HideCursor();
MoveMagnifierWindowFollowRect(node_bounds_in_root);
}
void FullscreenMagnifierController::SwitchTargetRootWindow(
aura::Window* new_root_window,
bool redraw_original_root_window) {
DCHECK(new_root_window);
if (new_root_window == root_window_)
return;
// Stores the previous scale.
float scale = GetScale();
// Unmagnify the previous root window.
root_window_->RemoveObserver(this);
// TODO: This may need to remove the IME observer from the old root window
// and add it to the new root window. https://crbug.com/820464
// Do not move mouse back to its original position (point at border of the
// root window) after redrawing as doing so will trigger root window switch
// again.
if (redraw_original_root_window)
RedrawKeepingMousePosition(1.0f, true, true);
root_window_ = new_root_window;
RedrawKeepingMousePosition(scale, true, true);
root_window_->AddObserver(this);
}
gfx::Transform FullscreenMagnifierController::GetMagnifierTransform() const {
gfx::Transform transform;
if (IsEnabled()) {
transform.Scale(scale_, scale_);
gfx::Point offset = GetWindowPosition();
transform.Translate(-offset.x(), -offset.y());
}
return transform;
}
void FullscreenMagnifierController::OnImplicitAnimationsCompleted() {
if (move_cursor_after_animation_) {
MoveCursorTo(position_after_animation_);
move_cursor_after_animation_ = false;
aura::client::CursorClient* cursor_client =
aura::client::GetCursorClient(root_window_);
if (cursor_client)
cursor_client->EnableMouseEvents();
}
is_on_animation_ = false;
StartOrStopScrollIfNecessary();
}
void FullscreenMagnifierController::OnWindowDestroying(
aura::Window* root_window) {
if (root_window == root_window_) {
// There must be at least one root window because this controller is
// destroyed before the root windows get destroyed.
DCHECK(root_window);
aura::Window* target_root_window = Shell::GetRootWindowForNewWindows();
CHECK(target_root_window);
// The destroyed root window must not be target.
CHECK_NE(target_root_window, root_window);
// Don't redraw the old root window as it's being destroyed.
SwitchTargetRootWindow(target_root_window, false);
point_of_interest_in_root_ = target_root_window->bounds().CenterPoint();
}
}
void FullscreenMagnifierController::OnWindowBoundsChanged(
aura::Window* window,
const gfx::Rect& old_bounds,
const gfx::Rect& new_bounds,
ui::PropertyChangeReason reason) {
}
void FullscreenMagnifierController::OnMouseEvent(ui::MouseEvent* event) {
aura::Window* target = static_cast<aura::Window*>(event->target());
aura::Window* current_root = target->GetRootWindow();
gfx::PointF root_location_f = event->root_location_f();
// Used for screen bounds checking.
gfx::Point root_location = event->root_location();
if (event->type() == ui::EventType::kMouseDragged) {
auto* screen = display::Screen::GetScreen();
const gfx::Point cursor_screen_location = screen->GetCursorScreenPoint();
auto* window = screen->GetWindowAtScreenPoint(cursor_screen_location);
// Update the |current_root| to be the one that contains the cursor
// currently. This will make sure the magnifier be activated in the display
// that contains the cursor while drag a window across displays.
current_root =
window ? window->GetRootWindow() : Shell::GetPrimaryRootWindow();
root_location = cursor_screen_location;
wm::ConvertPointFromScreen(current_root, &root_location);
root_location_f = gfx::PointF(root_location);
}
if (current_root->bounds().Contains(root_location)) {
// This must be before |SwitchTargetRootWindow()|.
if (event->type() != ui::EventType::kMouseCaptureChanged) {
point_of_interest_in_root_ = root_location;
}
if (current_root != root_window_) {
DCHECK(current_root);
SwitchTargetRootWindow(current_root, true);
}
const bool dragged_or_moved = event->type() == ui::EventType::kMouseMoved ||
event->type() == ui::EventType::kMouseDragged;
if (IsMagnified() && dragged_or_moved &&
event->pointer_details().pointer_type != ui::EventPointerType::kPen) {
OnMouseMove(root_location_f);
}
}
}
void FullscreenMagnifierController::OnScrollEvent(ui::ScrollEvent* event) {
if (event->IsAltDown() && event->IsControlDown()) {
if (event->type() == ui::EventType::kScrollFlingStart) {
event->StopPropagation();
return;
} else if (event->type() == ui::EventType::kScrollFlingCancel) {
float scale = GetScale();
// Jump back to exactly 1.0 if we are just a tiny bit zoomed in.
// TODO(katie): These events are not fired after every scroll, which means
// we don't always jump back to 1.0. Look into why they are missing.
if (scale < kMinMagnifiedScaleThreshold) {
scale = kNonMagnifiedScale;
SetScale(scale, true);
}
event->StopPropagation();
return;
}
if (event->type() == ui::EventType::kScroll) {
SetScale(magnifier_utils::GetScaleFromScroll(
event->y_offset() * kScrollScaleChangeFactor, GetScale(),
kMaxMagnifiedScale, kNonMagnifiedScale),
false /* animate */);
event->StopPropagation();
return;
}
}
}
void FullscreenMagnifierController::OnTouchEvent(ui::TouchEvent* event) {
aura::Window* target = static_cast<aura::Window*>(event->target());
aura::Window* current_root = target->GetRootWindow();
gfx::Rect root_bounds = current_root->bounds();
if (!root_bounds.Contains(event->root_location()))
return;
point_of_interest_in_root_ = event->root_location();
if (current_root != root_window_)
SwitchTargetRootWindow(current_root, true);
}
ui::EventDispatchDetails FullscreenMagnifierController::RewriteEvent(
const ui::Event& event,
const Continuation continuation) {
if (!IsEnabled())
return SendEvent(continuation, &event);
if (!event.IsTouchEvent())
return SendEvent(continuation, &event);
const ui::TouchEvent* touch_event = event.AsTouchEvent();
if (touch_event->type() == ui::EventType::kTouchPressed) {
touch_points_++;
press_event_map_[touch_event->pointer_details().id] =
std::make_unique<ui::TouchEvent>(*touch_event);
} else if (touch_event->type() == ui::EventType::kTouchReleased ||
touch_event->type() == ui::EventType::kTouchCancelled) {
touch_points_--;
press_event_map_.erase(touch_event->pointer_details().id);
}
ui::TouchEvent touch_event_copy = *touch_event;
if (gesture_provider_->OnTouchEvent(&touch_event_copy)) {
gesture_provider_->OnTouchEventAck(
touch_event_copy.unique_event_id(), false /* event_consumed */,
false /* is_source_touch_event_set_blocking */);
} else {
return DiscardEvent(continuation);
}
// User can change zoom level with two fingers pinch and pan around with two
// fingers scroll. Once FullscreenMagnifierController detects one of those two
// gestures, it starts consuming all touch events with cancelling existing
// touches. If cancel_pressed_touches is set to true,
// EventType::kTouchCancelled events are dispatched for existing touches after
// the next for-loop.
bool cancel_pressed_touches = ProcessGestures();
if (cancel_pressed_touches) {
DCHECK_EQ(2u, press_event_map_.size());
// FullscreenMagnifierController starts consuming all touch events after it
// cancells existing touches.
consume_touch_event_ = true;
for (const auto& it : press_event_map_) {
ui::TouchEvent touch_cancel_event(ui::EventType::kTouchCancelled,
gfx::Point(), touch_event->time_stamp(),
it.second->pointer_details());
touch_cancel_event.set_location_f(it.second->location_f());
touch_cancel_event.set_root_location_f(it.second->root_location_f());
touch_cancel_event.SetFlags(it.second->flags());
// TouchExplorationController is watching event stream and managing its
// internal state. If an event rewriter (FullscreenMagnifierController)
// rewrites event stream, the next event rewriter won't get the event,
// which makes TouchExplorationController confused. Send cancelled event
// for recorded touch events to the next event rewriter here instead of
// rewriting an event in the stream.
ui::EventDispatchDetails details =
SendEvent(continuation, &touch_cancel_event);
if (details.dispatcher_destroyed || details.target_destroyed)
return details;
}
press_event_map_.clear();
}
bool discard = consume_touch_event_;
// Reset state once no point is touched on the screen.
if (touch_points_ == 0) {
consume_touch_event_ = false;
// Jump back to exactly 1.0 if we are just a tiny bit zoomed in.
if (scale_ < kMinMagnifiedScaleThreshold) {
SetScale(kNonMagnifiedScale, true /* animate */);
} else {
// Store current magnifier scale in pref. We don't need to call this if we
// call SetScale (the above case) as SetScale does this.
Shell::Get()->accessibility_delegate()->SaveScreenMagnifierScale(scale_);
}
}
if (discard)
return DiscardEvent(continuation);
return SendEvent(continuation, &event);
}
const std::string& FullscreenMagnifierController::GetName() const {
static const std::string name("FullscreenMagnifierController");
return name;
}
bool FullscreenMagnifierController::Redraw(
const gfx::PointF& position_in_physical_pixels,
float scale,
bool animate) {
gfx::PointF position =
gfx::ConvertPointToDips(position_in_physical_pixels,
root_window_->layer()->device_scale_factor());
return RedrawDIP(position, scale, animate ? kDefaultAnimationDurationInMs : 0,
kDefaultAnimationTweenType);
}
bool FullscreenMagnifierController::RedrawDIP(
const gfx::PointF& position_in_dip,
float scale,
int duration_in_ms,
gfx::Tween::Type tween_type) {
DCHECK(root_window_);
float x = position_in_dip.x();
float y = position_in_dip.y();
ValidateScale(&scale);
if (x < 0)
x = 0;
if (y < 0)
y = 0;
const gfx::Size host_size_in_dip = GetHostSizeDIP();
const gfx::SizeF window_size_in_dip = GetWindowRectDIP(scale).size();
float max_x = host_size_in_dip.width() - window_size_in_dip.width();
float max_y = host_size_in_dip.height() - window_size_in_dip.height();
if (x > max_x)
x = max_x;
if (y > max_y)
y = max_y;
// Does nothing if both the origin and the scale are not changed.
// Cast origin points back to int, as viewport can only be integer values.
if (static_cast<int>(origin_.x()) == static_cast<int>(x) &&
static_cast<int>(origin_.y()) == static_cast<int>(y) && scale == scale_) {
return false;
}
origin_.set_x(x);
origin_.set_y(y);
scale_ = scale;
const ui::LayerAnimator::PreemptionStrategy strategy =
ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET;
const base::TimeDelta duration = base::Milliseconds(duration_in_ms);
ui::ScopedLayerAnimationSettings root_layer_settings(
root_window_->layer()->GetAnimator());
root_layer_settings.AddObserver(this);
root_layer_settings.SetPreemptionStrategy(strategy);
root_layer_settings.SetTweenType(tween_type);
root_layer_settings.SetTransitionDuration(duration);
display::Display display =
display::Screen::GetScreen()->GetDisplayNearestWindow(root_window_);
std::unique_ptr<RootWindowTransformer> transformer(
CreateRootWindowTransformerForDisplay(display));
// Inverse the transformation on the keyboard container and display
// identification highlight so the keyboard will remain zoomed out and the
// highlight will render around the edges of the display. Apply the same
// animation settings to it. Note: if |scale_| is 1.0f, the transform matrix
// will be an identity matrix. Applying the inverse of an identity matrix will
// not change the transformation.
// TODO(spqchan): Find a way to sync the layer animations together.
gfx::Transform inverse_transform;
if (GetMagnifierTransform().GetInverse(&inverse_transform)) {
std::vector<aura::Window*> undo_transform_windows = {
root_window_->GetChildById(kShellWindowId_ImeWindowParentContainer)};
aura::Window* display_identification_highlight =
root_window_->GetChildById(kShellWindowId_ScreenAnimationContainer)
->GetChildById(kShellWindowId_DisplayIdentificationHighlightWindow);
if (display_identification_highlight)
undo_transform_windows.push_back(display_identification_highlight);
for (auto* window : undo_transform_windows) {
ui::ScopedLayerAnimationSettings layer_settings(
window->layer()->GetAnimator());
layer_settings.SetPreemptionStrategy(strategy);
layer_settings.SetTweenType(tween_type);
layer_settings.SetTransitionDuration(duration);
window->SetTransform(inverse_transform);
}
}
if (!magnifier_debug_draw_rect_) {
RootWindowController::ForWindow(root_window_)
->ash_host()
->SetRootWindowTransformer(std::move(transformer));
}
if (duration_in_ms > 0)
is_on_animation_ = true;
Shell::Get()->accessibility_controller()->MagnifierBoundsChanged(
GetViewportRect());
return true;
}
void FullscreenMagnifierController::StartOrStopScrollIfNecessary() {
// This value controls the scrolling speed.
const int kMoveOffset = 40;
if (is_on_animation_) {
if (scroll_direction_ == SCROLL_NONE)
root_window_->layer()->GetAnimator()->StopAnimating();
return;
}
gfx::PointF new_origin = origin_;
switch (scroll_direction_) {
case SCROLL_NONE:
// No need to take action.
return;
case SCROLL_LEFT:
new_origin.Offset(-kMoveOffset, 0);
break;
case SCROLL_RIGHT:
new_origin.Offset(kMoveOffset, 0);
break;
case SCROLL_UP:
new_origin.Offset(0, -kMoveOffset);
break;
case SCROLL_DOWN:
new_origin.Offset(0, kMoveOffset);
break;
}
RedrawDIP(new_origin, scale_, kDefaultAnimationDurationInMs,
kDefaultAnimationTweenType);
}
void FullscreenMagnifierController::RedrawKeepingMousePosition(
float scale,
bool animate,
bool ignore_mouse_change) {
gfx::Point mouse_in_root = point_of_interest_in_root_;
// mouse_in_root is invalid value when the cursor is hidden.
if (!root_window_->bounds().Contains(mouse_in_root))
mouse_in_root = root_window_->bounds().CenterPoint();
const gfx::PointF origin = gfx::PointF(
mouse_in_root.x() - (scale_ / scale) * (mouse_in_root.x() - origin_.x()),
mouse_in_root.y() - (scale_ / scale) * (mouse_in_root.y() - origin_.y()));
bool changed =
RedrawDIP(origin, scale, animate ? kDefaultAnimationDurationInMs : 0,
kDefaultAnimationTweenType);
if (!ignore_mouse_change && changed)
AfterAnimationMoveCursorTo(mouse_in_root);
}
void FullscreenMagnifierController::OnMouseMove(
const gfx::PointF& location_in_dip) {
DCHECK(root_window_);
gfx::Point center_point_in_dip(std::round(location_in_dip.x()),
std::round(location_in_dip.y()));
int margin = kCursorPanningMargin / scale_; // No need to consider DPI.
// Edge mouse following mode.
int x_margin = margin;
int y_margin = margin;
if (mouse_following_mode_ == MagnifierMouseFollowingMode::kCentered ||
mouse_following_mode_ == MagnifierMouseFollowingMode::kContinuous) {
const gfx::Rect window_rect = GetViewportRect();
x_margin = window_rect.width() / 2;
y_margin = window_rect.height() / 2;
}
if (mouse_following_mode_ == MagnifierMouseFollowingMode::kContinuous) {
// Continuous mouse panning mode is similar to centered mouse panning mode,
// in that the screen moves behind the cursor when the user moves the mouse.
// Unlike centered mouse panning mode however, the cursor is not centered in
// the middle of the screen, but is able to freely move around, with the
// screen moving in the opposite direction; for example, when the cursor
// approaches the top left corner, the screen also scrolls behind it, so
// that more of the top left portion of the screen is visible, until the
// cursor reaches and meets up with the corner of the screen. This logic
// calculates where the center point of the magnified region should be,
// such that where the cursor is located in the magnified region corresponds
// in proportion to where the cursor is located on the screen overall.
// Screen size.
const gfx::Size host_size_in_dip = GetHostSizeDIP();
// Mouse position.
const float x = location_in_dip.x();
const float y = location_in_dip.y();
// Viewport dimensions for calculation, increased by variable padding:
// The cursor can never reach the bottom or right of the screen, it's always
// at least one DIP away so that you can see it. (Note the cursor can reach
// the top left at (0, 0)). Calculate the viewport size, adding some scaled
// viewport padding as we move down and right so that the padding 0 in the
// top/left and greater in the bottom right to account for the cursor not
// being able to access the bottom corner.
const float height =
host_size_in_dip.height() / scale_ + 5 * y / host_size_in_dip.height();
const float width =
host_size_in_dip.width() / scale_ + 5 * x / host_size_in_dip.width();
// The viewport center point is the mouse center point, minus the scaled
// mouse center point to get to the viewport left/top edge, plus half
// the viewport size.
// In the example below, the host size is 12 units in width, the
// mouse point x is at 7, and the viewport width is 3 (scale is 4.0).
// The center_point_in_dip_x should be 6, with some integer rounding.
// 6 = int(7 - (7 / 4.0) + (3 / 2.0))
// ____________
// | | host
// | ___ |
// | | *| | <-- mouse x = 7, viewport width = 3
// | |___| |
// |____________|
// 012345678901 <-- Indexes
const int center_point_in_dip_x = x - x / scale_ + width / 2.0;
const int center_point_in_dip_y = y - y / scale_ + height / 2.0;
center_point_in_dip = {center_point_in_dip_x, center_point_in_dip_y};
}
// Reduce the bottom margin if the keyboard is visible.
bool reduce_bottom_margin =
keyboard::KeyboardUIController::Get()->IsKeyboardVisible();
MoveMagnifierWindowFollowPoint(center_point_in_dip, x_margin, y_margin,
reduce_bottom_margin);
}
void FullscreenMagnifierController::AfterAnimationMoveCursorTo(
const gfx::Point& location) {
DCHECK(root_window_);
aura::client::CursorClient* cursor_client =
aura::client::GetCursorClient(root_window_);
if (cursor_client) {
// When cursor is invisible, do not move or show the cursor after the
// animation.
if (!cursor_client->IsCursorVisible())
return;
cursor_client->DisableMouseEvents();
}
move_cursor_after_animation_ = true;
position_after_animation_ = location;
}
bool FullscreenMagnifierController::IsMagnified() const {
return scale_ >= kMinMagnifiedScaleThreshold;
}
gfx::RectF FullscreenMagnifierController::GetWindowRectDIP(float scale) const {
const gfx::Size size_in_dip = GetHostSizeDIP();
const float width = size_in_dip.width() / scale;
const float height = size_in_dip.height() / scale;
return gfx::RectF(origin_.x(), origin_.y(), width, height);
}
gfx::Size FullscreenMagnifierController::GetHostSizeDIP() const {
return root_window_->bounds().size();
}
void FullscreenMagnifierController::ValidateScale(float* scale) {
*scale = std::clamp(*scale, kNonMagnifiedScale, kMaxMagnifiedScale);
DCHECK(kNonMagnifiedScale <= *scale && *scale <= kMaxMagnifiedScale);
}
bool FullscreenMagnifierController::ProcessGestures() {
bool cancel_pressed_touches = false;
std::vector<std::unique_ptr<ui::GestureEvent>> gestures =
gesture_provider_->GetAndResetPendingGestures();
for (const auto& gesture : gestures) {
const ui::GestureEventDetails& details = gesture->details();
if (details.touch_points() != 2)
continue;
if (gesture->type() == ui::EventType::kGesturePinchBegin) {
original_scale_ = scale_;
// Start consuming touch events with cancelling existing touches.
if (!consume_touch_event_)
cancel_pressed_touches = true;
} else if (gesture->type() == ui::EventType::kGesturePinchUpdate) {
float scale = GetScale() * details.scale();
ValidateScale(&scale);
// |details.bounding_box().CenterPoint()| return center of touch points
// of gesture in non-dip screen coordinate.
gfx::PointF gesture_center =
gfx::PointF(details.bounding_box().CenterPoint());
// Root transform does dip scaling, screen magnification scaling and
// translation. Apply inverse transform to convert non-dip screen
// coordinate to dip logical coordinate.
gesture_center =
root_window_->GetHost()->GetInverseRootTransform().MapPoint(
gesture_center);
// Calcualte new origin to keep the distance between |gesture_center|
// and |origin| same in screen coordinate. This means the following
// equation.
// (gesture_center.x - origin_.x) * scale_ =
// (gesture_center.x - new_origin.x) * scale
// If you solve it for |new_origin|, you will get the following formula.
const gfx::PointF origin = gfx::PointF(
gesture_center.x() -
(scale_ / scale) * (gesture_center.x() - origin_.x()),
gesture_center.y() -
(scale_ / scale) * (gesture_center.y() - origin_.y()));
RedrawDIP(origin, scale, 0, kDefaultAnimationTweenType);
} else if (gesture->type() == ui::EventType::kGestureScrollBegin) {
original_origin_ = origin_;
// Start consuming all touch events with cancelling existing touches.
if (!consume_touch_event_)
cancel_pressed_touches = true;
} else if (gesture->type() == ui::EventType::kGestureScrollUpdate) {
// The scroll offsets are apparently in pixels and does not take into
// account the display rotation. Convert back to dip by applying the
// inverse transform of the rotation (these are offsets, so we don't care
// about scale or translation. We'll take care of the scale below).
// https://crbug.com/867537.
const auto display =
display::Screen::GetScreen()->GetDisplayNearestWindow(root_window_);
gfx::Transform rotation_transform;
rotation_transform.Rotate(display.PanelRotationAsDegree());
gfx::Transform rotation_inverse_transform =
rotation_transform.GetCheckedInverse();
gfx::PointF scroll = rotation_inverse_transform.MapPoint(
gfx::PointF(details.scroll_x(), details.scroll_y()));
// Divide by scale to keep scroll speed same at any scale.
float new_x = origin_.x() + (-scroll.x() / scale_);
float new_y = origin_.y() + (-scroll.y() / scale_);
RedrawDIP(gfx::PointF(new_x, new_y), scale_, 0,
kDefaultAnimationTweenType);
}
}
return cancel_pressed_touches;
}
void FullscreenMagnifierController::MoveMagnifierWindowFollowPoint(
const gfx::Point& point,
int x_margin,
int y_margin,
bool reduce_bottom_margin) {
DCHECK(root_window_);
bool start_zoom = false;
// Current position.
const gfx::Rect window_rect = GetViewportRect();
const int top = window_rect.y();
const int bottom = window_rect.bottom();
int x_diff = 0;
if (point.x() < window_rect.x() + x_margin) {
// Panning left.
x_diff = point.x() - (window_rect.x() + x_margin);
start_zoom = true;
} else if (point.x() > window_rect.right() - x_margin) {
// Panning right.
x_diff = point.x() - (window_rect.right() - x_margin);
start_zoom = true;
}
int x = window_rect.x() + x_diff;
// If |reduce_bottom_margin| is true, use kKeyboardBottomPanningMargin instead
// of |y_margin|. This is to prevent the magnifier from panning when
// the user is trying to interact with the bottom of the keyboard.
const int bottom_panning_margin =
reduce_bottom_margin ? kKeyboardBottomPanningMargin / scale_ : y_margin;
int y_diff = 0;
if (point.y() < top + y_margin) {
// Panning up.
y_diff = point.y() - (top + y_margin);
start_zoom = true;
} else if (bottom - bottom_panning_margin < point.y()) {
// Panning down.
const int bottom_target_margin =
reduce_bottom_margin ? std::min(bottom_panning_margin, y_margin)
: y_margin;
y_diff = point.y() - (bottom - bottom_target_margin);
start_zoom = true;
}
int y = top + y_diff;
if (start_zoom && !is_on_animation_) {
bool ret = RedrawDIP(gfx::PointF(x, y), scale_,
0, // No animation on panning.
kDefaultAnimationTweenType);
if (ret &&
mouse_following_mode_ != MagnifierMouseFollowingMode::kContinuous) {
// If the magnified region is moved, hides the mouse cursor and moves it,
// unless we're in continuous mode (in which case mouse position is
// good already).
if ((x_diff != 0 || y_diff != 0)) {
MoveCursorTo(point);
}
}
}
}
void FullscreenMagnifierController::MoveMagnifierWindowCenterPoint(
const gfx::Point& point) {
DCHECK(root_window_);
gfx::Rect window_rect = GetViewportRect();
// Reduce the viewport bounds if the keyboard is up.
if (keyboard::KeyboardUIController::Get()->IsEnabled()) {
gfx::Rect keyboard_rect = keyboard::KeyboardUIController::Get()
->GetKeyboardWindow()
->GetBoundsInScreen();
window_rect.set_height(window_rect.height() -
keyboard_rect.height() / scale_);
}
if (point == window_rect.CenterPoint())
return;
if (!is_on_animation_) {
// With animation on panning.
RedrawDIP(
gfx::PointF(window_rect.origin() + (point - window_rect.CenterPoint())),
scale_, kDefaultAnimationDurationInMs, kCenterCaretAnimationTweenType);
}
}
void FullscreenMagnifierController::MoveMagnifierWindowFollowRect(
const gfx::Rect& rect) {
DCHECK(root_window_);
bool should_pan = false;
const gfx::Rect viewport_rect = GetViewportRect();
const int left = viewport_rect.x();
const int right = viewport_rect.right();
const gfx::Point rect_center = rect.CenterPoint();
int x = left;
if (rect.x() < left || right < rect.right()) {
// Panning horizontally.
x = rect_center.x() - viewport_rect.width() / 2;
should_pan = true;
}
const int top = viewport_rect.y();
const int bottom = viewport_rect.bottom();
int y = top;
if (rect.y() < top || bottom < rect.bottom()) {
// Panning vertically.
y = rect_center.y() - viewport_rect.height() / 2;
should_pan = true;
}
// If rect is too wide to fit in viewport, include as much as we can, starting
// with the left edge.
if (rect.width() > viewport_rect.width())
x = rect.x() - magnifier_utils::kLeftEdgeContextPadding;
if (should_pan) {
if (is_on_animation_) {
root_window_->layer()->GetAnimator()->StopAnimating();
is_on_animation_ = false;
}
RedrawDIP(gfx::PointF(x, y), scale_, kDefaultAnimationDurationInMs,
kDefaultAnimationTweenType);
}
}
void FullscreenMagnifierController::MoveCursorTo(
const gfx::Point& root_location) {
aura::WindowTreeHost* host = root_window_->GetHost();
host->MoveCursorToLocationInPixels(gfx::ToCeiledPoint(
host->GetRootTransform().MapPoint(gfx::PointF(root_location))));
if (cursor_moved_callback_for_testing_) {
cursor_moved_callback_for_testing_.Run(root_location);
}
}
} // namespace ash