| // 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/fullscreen_magnification_controller.h" |
| |
| #include "base/numerics/ranges.h" |
| #include "chromecast/graphics/gestures/cast_gesture_handler.h" |
| #include "third_party/skia/include/core/SkPaint.h" |
| #include "third_party/skia/include/core/SkPath.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/gfx/canvas.h" |
| #include "ui/gfx/transform.h" |
| #include "ui/gfx/transform_util.h" |
| |
| namespace chromecast { |
| |
| namespace { |
| // Default ratio of magnifier scale. |
| constexpr float kDefaultMagnificationScale = 2.f; |
| |
| constexpr float kMaxMagnifiedScale = 20.0f; |
| constexpr float kMinMagnifiedScaleThreshold = 1.1f; |
| constexpr float kNonMagnifiedScale = 1.0f; |
| |
| constexpr float kZoomGestureLockThreshold = 0.1f; |
| constexpr float kScrollGestureLockThreshold = 20000.0f; |
| |
| // The color of the highlight ring. |
| constexpr SkColor kHighlightRingColor = SkColorSetRGB(247, 152, 58); |
| constexpr int kHighlightShadowRadius = 10; |
| constexpr int kHighlightShadowAlpha = 90; |
| |
| // Convert point locations to DIP by using the original transform, rather than |
| // the one currently installed on the window tree host (which might be our |
| // magnifier). |
| gfx::Point ConvertPixelsToDIPWithOriginalTransform( |
| const gfx::Transform& transform, |
| const gfx::Point& point) { |
| gfx::Transform invert; |
| if (!transform.GetInverse(&invert)) { |
| // Some transforms can't be inverted, so we do the same as window tree |
| // host's DIP conversion and just use the transform as is in that case. |
| invert = transform; |
| } |
| gfx::PointF dip_point(point); |
| invert.TransformPoint(&dip_point); |
| return gfx::ToFlooredPoint(dip_point); |
| } |
| |
| // Correct the given scale value if necessary. |
| void ValidateScale(float* scale) { |
| *scale = base::ClampToRange(*scale, kNonMagnifiedScale, kMaxMagnifiedScale); |
| DCHECK(kNonMagnifiedScale <= *scale && *scale <= kMaxMagnifiedScale); |
| } |
| |
| } // namespace |
| |
| class FullscreenMagnificationController::GestureProviderClient |
| : public ui::GestureProviderAuraClient { |
| public: |
| GestureProviderClient() = default; |
| ~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. |
| // MagnificationController is interested only in pinch and scroll |
| // gestures. |
| DCHECK_NE(ui::ET_GESTURE_SCROLL_BEGIN, event->type()); |
| DCHECK_NE(ui::ET_GESTURE_SCROLL_END, event->type()); |
| DCHECK_NE(ui::ET_GESTURE_SCROLL_UPDATE, event->type()); |
| DCHECK_NE(ui::ET_GESTURE_PINCH_BEGIN, event->type()); |
| DCHECK_NE(ui::ET_GESTURE_PINCH_END, event->type()); |
| DCHECK_NE(ui::ET_GESTURE_PINCH_UPDATE, event->type()); |
| } |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(GestureProviderClient); |
| }; |
| |
| FullscreenMagnificationController::FullscreenMagnificationController( |
| aura::Window* root_window, |
| CastGestureHandler* cast_gesture_handler) |
| : root_window_(root_window), |
| magnification_scale_(kDefaultMagnificationScale), |
| cast_gesture_handler_(cast_gesture_handler) { |
| DCHECK(root_window); |
| root_window->GetHost()->GetEventSource()->AddEventRewriter(this); |
| |
| gesture_provider_client_ = std::make_unique<GestureProviderClient>(); |
| gesture_provider_ = std::make_unique<ui::GestureProviderAura>( |
| this, gesture_provider_client_.get()); |
| } |
| |
| FullscreenMagnificationController::~FullscreenMagnificationController() {} |
| |
| void FullscreenMagnificationController::SetEnabled(bool enabled) { |
| if (is_enabled_ == enabled) |
| return; |
| if (!is_enabled_) { |
| // Stash the original root window transform so we can restore it after we're |
| // done. |
| original_transform_ = root_window_->transform(); |
| } |
| is_enabled_ = enabled; |
| auto magnifier_transform(GetMagnifierTransform()); |
| root_window_->SetTransform(magnifier_transform); |
| if (enabled) { |
| // Add the highlight ring. |
| if (!highlight_ring_layer_) { |
| AddHighlightLayer(); |
| } |
| UpdateHighlightLayerTransform(magnifier_transform); |
| } else { |
| // Remove the highlight ring. |
| if (highlight_ring_layer_) { |
| root_window_->layer()->Remove(highlight_ring_layer_.get()); |
| highlight_ring_layer_.reset(); |
| } |
| } |
| } |
| |
| bool FullscreenMagnificationController::IsEnabled() const { |
| return is_enabled_; |
| } |
| |
| void FullscreenMagnificationController::SetMagnificationScale( |
| float magnification_scale) { |
| magnification_scale_ = magnification_scale; |
| root_window_->SetTransform(GetMagnifierTransform()); |
| } |
| |
| gfx::Transform FullscreenMagnificationController::GetMagnifierTransform() |
| const { |
| gfx::Transform transform = original_transform_; |
| if (IsEnabled()) { |
| transform.Scale(magnification_scale_, magnification_scale_); |
| |
| // Top corner of window. |
| gfx::Point offset = gfx::ToFlooredPoint(magnification_origin_); |
| transform.Translate(-offset.x(), -offset.y()); |
| } |
| |
| return transform; |
| } |
| |
| // Overridden from ui::EventRewriter |
| ui::EventRewriteStatus FullscreenMagnificationController::RewriteEvent( |
| const ui::Event& event, |
| std::unique_ptr<ui::Event>* rewritten_event) { |
| if (!IsEnabled()) { |
| return ui::EventRewriteStatus::EVENT_REWRITE_CONTINUE; |
| } |
| if (!event.IsTouchEvent()) |
| return ui::EVENT_REWRITE_CONTINUE; |
| |
| const ui::TouchEvent* touch_event = event.AsTouchEvent(); |
| |
| // Touch events come through in screen pixels, but untransformed. This is the |
| // raw coordinate not yet mapped to the root window's coordinate system or the |
| // screen. Convert it into the root window's coordinate system, in DIP which |
| // is what the rest of this class expects. |
| gfx::Point location = ConvertPixelsToDIPWithOriginalTransform( |
| original_transform_, touch_event->location()); |
| gfx::Point root_location = ConvertPixelsToDIPWithOriginalTransform( |
| original_transform_, touch_event->root_location()); |
| |
| // We now need a TouchEvent that has its coordinates mapped into root window |
| // DIP. |
| ui::TouchEvent touch_event_dip = *touch_event; |
| touch_event_dip.set_location(location); |
| touch_event_dip.set_root_location(root_location); |
| |
| // Track finger presses so we can look for our two-finger drag. |
| if (touch_event_dip.type() == ui::ET_TOUCH_PRESSED) { |
| touch_points_++; |
| press_event_map_[touch_event_dip.pointer_details().id] = |
| std::make_unique<ui::TouchEvent>(*touch_event); |
| } else if (touch_event_dip.type() == ui::ET_TOUCH_RELEASED) { |
| touch_points_--; |
| press_event_map_.erase(touch_event_dip.pointer_details().id); |
| } |
| |
| if (gesture_provider_->OnTouchEvent(&touch_event_dip)) { |
| gesture_provider_->OnTouchEventAck( |
| touch_event_dip.unique_event_id(), false /* event_consumed */, |
| false /* is_source_touch_event_set_non_blocking */); |
| } else { |
| return ui::EVENT_REWRITE_DISCARD; |
| } |
| |
| // The user can change the zoom level with two fingers pinch and pan around |
| // with two fingers scroll. Once we detect one of those two gestures, we start |
| // consuming all touch events by cancelling existing touches. If |
| // cancel_pressed_touches is set to true, ET_TOUCH_CANCELLED |
| // events are dispatched for existing touches after the next for-loop. |
| bool cancel_pressed_touches = ProcessGestures(); |
| |
| if (cancel_pressed_touches) { |
| // Start consuming all touch events after we cancel existing touches. |
| consume_touch_event_ = true; |
| |
| if (!press_event_map_.empty()) { |
| auto it = press_event_map_.begin(); |
| |
| std::unique_ptr<ui::TouchEvent> rewritten_touch_event = |
| std::make_unique<ui::TouchEvent>(ui::ET_TOUCH_CANCELLED, gfx::Point(), |
| touch_event_dip.time_stamp(), |
| it->second->pointer_details()); |
| rewritten_touch_event->set_location_f(it->second->location_f()); |
| rewritten_touch_event->set_root_location_f(it->second->root_location_f()); |
| rewritten_touch_event->set_flags(it->second->flags()); |
| *rewritten_event = std::move(rewritten_touch_event); |
| |
| // The other event is cancelled in NextDispatchEvent. |
| press_event_map_.erase(it); |
| |
| return ui::EVENT_REWRITE_DISPATCH_ANOTHER; |
| } |
| } |
| |
| bool discard = consume_touch_event_; |
| |
| // Reset state once no point is touched on the screen. |
| if (touch_points_ == 0) { |
| consume_touch_event_ = false; |
| locked_gesture_ = NO_GESTURE; |
| |
| // Jump back to exactly 1.0 if we are just a tiny bit zoomed in. |
| if (magnification_scale_ < kMinMagnifiedScaleThreshold) |
| SetMagnificationScale(kNonMagnifiedScale); |
| } |
| |
| if (discard) |
| return ui::EVENT_REWRITE_DISCARD; |
| |
| return ui::EVENT_REWRITE_CONTINUE; |
| } |
| |
| ui::EventRewriteStatus FullscreenMagnificationController::NextDispatchEvent( |
| const ui::Event& last_event, |
| std::unique_ptr<ui::Event>* new_event) { |
| DCHECK_EQ(1u, press_event_map_.size()); |
| |
| auto it = press_event_map_.begin(); |
| |
| std::unique_ptr<ui::TouchEvent> event = std::make_unique<ui::TouchEvent>( |
| ui::ET_TOUCH_CANCELLED, gfx::Point(), last_event.time_stamp(), |
| it->second->pointer_details()); |
| event->set_location_f(it->second->location_f()); |
| event->set_root_location_f(it->second->root_location_f()); |
| event->set_flags(it->second->flags()); |
| *new_event = std::move(event); |
| |
| press_event_map_.erase(it); |
| |
| DCHECK_EQ(0u, press_event_map_.size()); |
| |
| return ui::EVENT_REWRITE_REWRITTEN; |
| } |
| |
| bool FullscreenMagnificationController::RedrawDIP( |
| const gfx::PointF& position_in_dip, |
| float scale) { |
| 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 = root_window_->bounds().size(); |
| const float scaled_width = host_size_in_dip.width() / scale; |
| const float scaled_height = host_size_in_dip.height() / scale; |
| float max_x = host_size_in_dip.width() - scaled_width; |
| float max_y = host_size_in_dip.height() - scaled_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. |
| if (magnification_origin_.x() == x && magnification_origin_.y() == y && |
| scale == magnification_scale_) { |
| return false; |
| } |
| |
| magnification_origin_.set_x(x); |
| magnification_origin_.set_y(y); |
| magnification_scale_ = scale; |
| |
| auto magnifier_transform = GetMagnifierTransform(); |
| root_window_->SetTransform(magnifier_transform); |
| |
| UpdateHighlightLayerTransform(magnifier_transform); |
| |
| return true; |
| } |
| |
| bool FullscreenMagnificationController::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 (gesture->type() == ui::ET_GESTURE_END || |
| gesture->type() == ui::ET_GESTURE_BEGIN) { |
| locked_gesture_ = NO_GESTURE; |
| cancel_pressed_touches = false; |
| } |
| |
| if (details.touch_points() != 2) |
| continue; |
| |
| if (gesture->type() == ui::ET_GESTURE_PINCH_BEGIN) { |
| original_magnification_scale_ = magnification_scale_; |
| |
| // Start consuming touch events with cancelling existing touches. |
| if (!consume_touch_event_) |
| cancel_pressed_touches = true; |
| } else if (gesture->type() == ui::ET_GESTURE_PINCH_UPDATE && |
| (locked_gesture_ == NO_GESTURE || locked_gesture_ == ZOOM)) { |
| float scale = magnification_scale_ * details.scale(); |
| ValidateScale(&scale); |
| |
| // Lock to zoom mode if the difference between our new scale and old scale |
| // passes our zoom lock threshold. |
| if (locked_gesture_ == NO_GESTURE && |
| std::abs(scale - original_magnification_scale_) > |
| kZoomGestureLockThreshold) { |
| locked_gesture_ = FullscreenMagnificationController::ZOOM; |
| } |
| |
| // |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. |
| root_window_->GetHost()->GetInverseRootTransform().TransformPoint( |
| &gesture_center); |
| |
| // Calculate new origin to keep the distance between |gesture_center| |
| // and |origin| same in screen coordinate. This means the following |
| // equation. |
| // (gesture_center.x - magnification_origin_.x) * magnification_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() - |
| (magnification_scale_ / scale) * |
| (gesture_center.x() - magnification_origin_.x()), |
| gesture_center.y() - |
| (magnification_scale_ / scale) * |
| (gesture_center.y() - magnification_origin_.y())); |
| |
| RedrawDIP(origin, scale); |
| |
| // Invoke the tap gesture so we don't go idle in the UI while zooming. |
| cast_gesture_handler_->HandleTapGesture( |
| gfx::ToFlooredPoint(gesture_center)); |
| } else if (gesture->type() == ui::ET_GESTURE_SCROLL_BEGIN) { |
| original_magnification_origin_ = magnification_origin_; |
| |
| // Start consuming all touch events with cancelling existing touches. |
| if (!consume_touch_event_) |
| cancel_pressed_touches = true; |
| } else if (gesture->type() == ui::ET_GESTURE_SCROLL_UPDATE && |
| (locked_gesture_ == NO_GESTURE || locked_gesture_ == SCROLL)) { |
| // If we're not zoomed, scroll is a no-op. |
| if (magnification_scale_ <= kNonMagnifiedScale) { |
| continue; |
| } |
| float new_x = magnification_origin_.x() + |
| (-1.0f * details.scroll_x() / magnification_scale_); |
| float new_y = magnification_origin_.y() + |
| (-1.0f * details.scroll_y() / magnification_scale_); |
| |
| // Lock to scroll mode if the squared distance from the old position |
| // passes our scroll lock threshold. |
| if (locked_gesture_ == NO_GESTURE) { |
| float diff_x = |
| (new_x - original_magnification_origin_.x()) * magnification_scale_; |
| float diff_y = |
| (new_y - original_magnification_origin_.y()) * magnification_scale_; |
| float squared_distance = (diff_x * diff_x) + (diff_y * diff_y); |
| if (squared_distance > kScrollGestureLockThreshold) { |
| locked_gesture_ = SCROLL; |
| } |
| } |
| RedrawDIP(gfx::PointF(new_x, new_y), magnification_scale_); |
| |
| // Invoke the tap gesture so we don't go idle in the UI whiole dragging. |
| cast_gesture_handler_->HandleTapGesture(gfx::Point(new_x, new_y)); |
| } |
| } |
| |
| return cancel_pressed_touches; |
| } |
| |
| void FullscreenMagnificationController::AddHighlightLayer() { |
| ui::Layer* root_layer = root_window_->layer(); |
| highlight_ring_layer_ = std::make_unique<ui::Layer>(ui::LAYER_TEXTURED); |
| highlight_ring_layer_->set_name("MagnificationHighlightLayer"); |
| root_layer->Add(highlight_ring_layer_.get()); |
| highlight_ring_layer_->parent()->StackAtTop(highlight_ring_layer_.get()); |
| gfx::Rect bounds(root_layer->bounds()); |
| highlight_ring_layer_->SetBounds(bounds); |
| highlight_ring_layer_->set_delegate(this); |
| highlight_ring_layer_->SetFillsBoundsOpaquely(false); |
| } |
| |
| void FullscreenMagnificationController::UpdateHighlightLayerTransform( |
| const gfx::Transform& magnifier_transform) { |
| // The highlight ring layer needs to be drawn unmagnified, so take the inverse |
| // of the magnification transform. |
| gfx::Transform inverse_transform; |
| if (!magnifier_transform.GetInverse(&inverse_transform)) { |
| LOG(ERROR) << "Unable to apply inverse transform to magnifier ring"; |
| return; |
| } |
| gfx::Transform highlight_layer_transform(original_transform_); |
| highlight_layer_transform.ConcatTransform(inverse_transform); |
| highlight_ring_layer_->SetTransform(highlight_layer_transform); |
| |
| // Make sure the highlight ring layer is on top. |
| highlight_ring_layer_->parent()->StackAtTop(highlight_ring_layer_.get()); |
| |
| // Repaint. |
| highlight_ring_layer_->SchedulePaint(root_window_->layer()->bounds()); |
| } |
| |
| void FullscreenMagnificationController::OnPaintLayer( |
| const ui::PaintContext& context) { |
| ui::PaintRecorder recorder(context, highlight_ring_layer_->size()); |
| |
| cc::PaintFlags flags; |
| flags.setAntiAlias(true); |
| flags.setStyle(cc::PaintFlags::kStroke_Style); |
| flags.setStrokeWidth(2); |
| |
| flags.setColor(kHighlightRingColor); |
| |
| gfx::Rect bounds(highlight_ring_layer_->bounds()); |
| for (int i = 0; i < 10; i++) { |
| // Fade out alpha quadratically. |
| flags.setAlpha( |
| (kHighlightShadowAlpha * std::pow(kHighlightShadowRadius - i, 2)) / |
| std::pow(kHighlightShadowRadius, 2)); |
| |
| gfx::Rect outsetRect = bounds; |
| outsetRect.Inset(i, i, i, i); |
| recorder.canvas()->DrawRect(outsetRect, flags); |
| } |
| } |
| |
| void FullscreenMagnificationController::OnDeviceScaleFactorChanged( |
| float old_device_scale_factor, |
| float new_device_scale_factor) {} |
| |
| } // namespace chromecast |