| // Copyright 2013 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/common/input/synthetic_smooth_move_gesture.h" |
| |
| #include <stdint.h> |
| |
| #include "base/check_op.h" |
| #include "base/notreached.h" |
| #include "base/time/time.h" |
| #include "third_party/blink/public/common/input/synthetic_web_input_event_builders.h" |
| #include "ui/gfx/geometry/point_f.h" |
| |
| namespace content { |
| namespace { |
| |
| gfx::Vector2dF ProjectScalarOntoVector(float scalar, |
| const gfx::Vector2dF& vector) { |
| return gfx::ScaleVector2d(vector, scalar / vector.Length()); |
| } |
| |
| // returns the animation progress along an arctan curve to provide simple |
| // ease-in ease-out behavior. |
| float GetCurvedRatio(const base::TimeTicks& current, |
| const base::TimeTicks& start, |
| const base::TimeTicks& end, |
| int speed_in_pixels_s) { |
| // Increasing this would make the start and the end of the curv smoother. |
| // Hence the higher value for the higher speed. |
| const float kArctanRange = sqrt(static_cast<double>(speed_in_pixels_s)) / 100; |
| |
| const float kMaxArctan = std::atan(kArctanRange / 2); |
| const float kMinArctan = std::atan(-kArctanRange / 2); |
| |
| float linear_ratio = (current - start) / (end - start); |
| return (std::atan(kArctanRange * linear_ratio - kArctanRange / 2) - |
| kMinArctan) / |
| (kMaxArctan - kMinArctan); |
| } |
| |
| } // namespace |
| |
| SyntheticSmoothMoveGestureParams::SyntheticSmoothMoveGestureParams() = default; |
| |
| SyntheticSmoothMoveGestureParams::SyntheticSmoothMoveGestureParams( |
| const SyntheticSmoothMoveGestureParams& other) = default; |
| |
| SyntheticSmoothMoveGestureParams::~SyntheticSmoothMoveGestureParams() = default; |
| |
| SyntheticGestureParams::GestureType |
| SyntheticSmoothMoveGestureParams::GetGestureType() const { |
| return SMOOTH_MOVE_GESTURE; |
| } |
| |
| SyntheticSmoothMoveGesture::SyntheticSmoothMoveGesture( |
| const SyntheticSmoothMoveGestureParams& gesture_params) |
| : SyntheticGestureBase(gesture_params), |
| current_move_segment_start_position_(params().start_point) { |
| CHECK_EQ(SyntheticGestureParams::SMOOTH_MOVE_GESTURE, |
| gesture_params.GetGestureType()); |
| } |
| |
| SyntheticSmoothMoveGesture::~SyntheticSmoothMoveGesture() {} |
| |
| SyntheticGesture::Result SyntheticSmoothMoveGesture::ForwardInputEvents( |
| const base::TimeTicks& timestamp, |
| SyntheticGestureTarget* target) { |
| CHECK(dispatching_controller_); |
| |
| // Keep this on the stack so we can check if the forwarded event caused the |
| // deletion of the controller (which owns `this`). |
| base::WeakPtr<SyntheticGestureController> weak_controller = |
| dispatching_controller_; |
| |
| if (state_ == SETUP) { |
| state_ = STARTED; |
| current_move_segment_ = -1; |
| current_move_segment_stop_time_ = timestamp; |
| } |
| |
| switch (params().input_type) { |
| case SyntheticSmoothMoveGestureParams::TOUCH_INPUT: |
| if (!synthetic_pointer_driver_) |
| synthetic_pointer_driver_ = SyntheticPointerDriver::Create( |
| content::mojom::GestureSourceType::kTouchInput, |
| params().from_devtools_debugger); |
| ForwardTouchInputEvents(timestamp, target); |
| break; |
| case SyntheticSmoothMoveGestureParams::MOUSE_DRAG_INPUT: |
| if (!synthetic_pointer_driver_) |
| synthetic_pointer_driver_ = SyntheticPointerDriver::Create( |
| content::mojom::GestureSourceType::kMouseInput, |
| params().from_devtools_debugger); |
| ForwardMouseClickInputEvents(timestamp, target); |
| break; |
| case SyntheticSmoothMoveGestureParams::MOUSE_WHEEL_INPUT: |
| ForwardMouseWheelInputEvents(timestamp, target); |
| // A mousewheel should not be able to close the WebContents. |
| CHECK(weak_controller); |
| break; |
| default: |
| return SyntheticGesture::GESTURE_SOURCE_TYPE_NOT_IMPLEMENTED; |
| } |
| if (!weak_controller) { |
| // A pointer gesture can cause the controller (and therefore `this`) to be |
| // synchronously deleted (e.g. clicking tab-close). Return immediately in |
| // this case. |
| return SyntheticGesture::GESTURE_ABORT; |
| } |
| |
| return (state_ == DONE) ? SyntheticGesture::GESTURE_FINISHED |
| : SyntheticGesture::GESTURE_RUNNING; |
| } |
| |
| // TODO(ssid): Clean up the switch statements by adding functions instead of |
| // large code, in the Forward*Events functions. Move the actions for all input |
| // types to different class (SyntheticInputDevice) which generates input events |
| // for all input types. The gesture class can use instance of device actions. |
| // Refer: crbug.com/461825 |
| |
| // CAUTION: forwarding a pointer press/release can cause `this` to be deleted. |
| void SyntheticSmoothMoveGesture::ForwardTouchInputEvents( |
| const base::TimeTicks& timestamp, |
| SyntheticGestureTarget* target) { |
| // Keep this on the stack so we can check if the forwarded event caused the |
| // deletion of the controller (which owns `this`). |
| base::WeakPtr<SyntheticGestureController> weak_controller = |
| dispatching_controller_; |
| switch (state_) { |
| case STARTED: |
| if (MoveIsNoOp()) { |
| state_ = DONE; |
| break; |
| } |
| if (params().add_slop) { |
| AddTouchSlopToFirstDistance(target); |
| } |
| ComputeNextMoveSegment(); |
| PressPoint(target, timestamp); |
| if (!weak_controller) { |
| return; |
| } |
| state_ = MOVING; |
| break; |
| case MOVING: { |
| base::TimeTicks event_timestamp = ClampTimestamp(timestamp); |
| gfx::Vector2dF delta = GetPositionDeltaAtTime(event_timestamp); |
| MovePoint(target, delta, event_timestamp); |
| // A move should never be able to cause deletion of the controller. |
| CHECK(weak_controller); |
| |
| if (FinishedCurrentMoveSegment(event_timestamp)) { |
| if (!IsLastMoveSegment()) { |
| current_move_segment_start_position_ += |
| params().distances[current_move_segment_]; |
| ComputeNextMoveSegment(); |
| } else if (params().prevent_fling) { |
| state_ = STOPPING; |
| } else { |
| ReleasePoint(target, event_timestamp); |
| if (!weak_controller) { |
| return; |
| } |
| state_ = DONE; |
| } |
| } |
| } break; |
| case STOPPING: |
| if (timestamp - current_move_segment_stop_time_ >= |
| target->PointerAssumedStoppedTime()) { |
| base::TimeTicks event_timestamp = current_move_segment_stop_time_ + |
| target->PointerAssumedStoppedTime(); |
| ReleasePoint(target, event_timestamp); |
| if (!weak_controller) { |
| return; |
| } |
| state_ = DONE; |
| } |
| break; |
| case SETUP: |
| NOTREACHED() |
| << "State SETUP invalid for synthetic scroll using touch input."; |
| case DONE: |
| NOTREACHED() |
| << "State DONE invalid for synthetic scroll using touch input."; |
| } |
| } |
| |
| void SyntheticSmoothMoveGesture::ForwardMouseWheelInputEvents( |
| const base::TimeTicks& timestamp, |
| SyntheticGestureTarget* target) { |
| switch (state_) { |
| case STARTED: |
| if (MoveIsNoOp()) { |
| state_ = DONE; |
| break; |
| } |
| ComputeNextMoveSegment(); |
| state_ = MOVING; |
| break; |
| case MOVING: { |
| base::TimeTicks event_timestamp = ClampTimestamp(timestamp); |
| gfx::Vector2dF delta = GetPositionDeltaAtTime(event_timestamp) - |
| current_move_segment_total_delta_; |
| if (delta.x() || delta.y()) { |
| blink::WebMouseWheelEvent::Phase phase = |
| needs_scroll_begin_ ? blink::WebMouseWheelEvent::kPhaseBegan |
| : blink::WebMouseWheelEvent::kPhaseChanged; |
| ForwardMouseWheelEvent(target, delta, phase, event_timestamp, |
| params().modifiers); |
| current_move_segment_total_delta_ += delta; |
| needs_scroll_begin_ = false; |
| } |
| |
| if (FinishedCurrentMoveSegment(event_timestamp)) { |
| if (!IsLastMoveSegment()) { |
| current_move_segment_total_delta_ = gfx::Vector2dF(); |
| ComputeNextMoveSegment(); |
| } else { |
| state_ = DONE; |
| |
| // Start flinging on the swipe action. |
| if (!params().prevent_fling && (params().fling_velocity_x != 0 || |
| params().fling_velocity_y != 0)) { |
| ForwardFlingGestureEvent( |
| target, blink::WebGestureEvent::Type::kGestureFlingStart); |
| } else { |
| // Forward a wheel event with phase ended and zero deltas. |
| ForwardMouseWheelEvent(target, gfx::Vector2d(), |
| blink::WebMouseWheelEvent::kPhaseEnded, |
| event_timestamp, params().modifiers); |
| } |
| needs_scroll_begin_ = true; |
| } |
| } |
| } break; |
| case SETUP: |
| NOTREACHED() << "State SETUP invalid for synthetic scroll using mouse " |
| "wheel input."; |
| case STOPPING: |
| NOTREACHED() << "State STOPPING invalid for synthetic scroll using mouse " |
| "wheel input."; |
| case DONE: |
| NOTREACHED() |
| << "State DONE invalid for synthetic scroll using mouse wheel input."; |
| } |
| } |
| |
| // CAUTION: forwarding a pointer press/release can cause `this` to be deleted. |
| void SyntheticSmoothMoveGesture::ForwardMouseClickInputEvents( |
| const base::TimeTicks& timestamp, |
| SyntheticGestureTarget* target) { |
| // Keep this on the stack so we can check if the forwarded event caused the |
| // deletion of the controller (which owns `this`). |
| base::WeakPtr<SyntheticGestureController> weak_controller = |
| dispatching_controller_; |
| switch (state_) { |
| case STARTED: |
| if (MoveIsNoOp()) { |
| state_ = DONE; |
| break; |
| } |
| ComputeNextMoveSegment(); |
| PressPoint(target, timestamp); |
| if (!weak_controller) { |
| return; |
| } |
| state_ = MOVING; |
| break; |
| case MOVING: { |
| base::TimeTicks event_timestamp = ClampTimestamp(timestamp); |
| gfx::Vector2dF delta = GetPositionDeltaAtTime(event_timestamp); |
| MovePoint(target, delta, event_timestamp); |
| |
| if (FinishedCurrentMoveSegment(event_timestamp)) { |
| if (!IsLastMoveSegment()) { |
| current_move_segment_start_position_ += |
| params().distances[current_move_segment_]; |
| ComputeNextMoveSegment(); |
| } else { |
| ReleasePoint(target, event_timestamp); |
| if (!weak_controller) { |
| return; |
| } |
| state_ = DONE; |
| } |
| } |
| } break; |
| case STOPPING: |
| NOTREACHED() |
| << "State STOPPING invalid for synthetic drag using mouse input."; |
| case SETUP: |
| NOTREACHED() |
| << "State SETUP invalid for synthetic drag using mouse input."; |
| case DONE: |
| NOTREACHED() |
| << "State DONE invalid for synthetic drag using mouse input."; |
| } |
| } |
| |
| void SyntheticSmoothMoveGesture::ForwardMouseWheelEvent( |
| SyntheticGestureTarget* target, |
| const gfx::Vector2dF& delta, |
| const blink::WebMouseWheelEvent::Phase phase, |
| const base::TimeTicks& timestamp, |
| int modifiers) const { |
| if (params().from_devtools_debugger) { |
| modifiers |= blink::WebInputEvent::kFromDebugger; |
| } |
| blink::WebMouseWheelEvent mouse_wheel_event = |
| blink::SyntheticWebMouseWheelEventBuilder::Build( |
| 0, 0, delta.x(), delta.y(), modifiers, params().granularity); |
| |
| mouse_wheel_event.SetPositionInWidget( |
| current_move_segment_start_position_.x(), |
| current_move_segment_start_position_.y()); |
| mouse_wheel_event.phase = phase; |
| |
| mouse_wheel_event.SetTimeStamp(timestamp); |
| |
| target->DispatchInputEventToPlatform(mouse_wheel_event); |
| } |
| |
| void SyntheticSmoothMoveGesture::ForwardFlingGestureEvent( |
| SyntheticGestureTarget* target, |
| const blink::WebInputEvent::Type type) const { |
| blink::WebGestureEvent fling_gesture_event = |
| blink::SyntheticWebGestureEventBuilder::Build( |
| type, blink::WebGestureDevice::kTouchpad); |
| fling_gesture_event.data.fling_start.velocity_x = params().fling_velocity_x; |
| fling_gesture_event.data.fling_start.velocity_y = params().fling_velocity_y; |
| fling_gesture_event.SetPositionInWidget(current_move_segment_start_position_); |
| target->DispatchInputEventToPlatform(fling_gesture_event); |
| } |
| |
| void SyntheticSmoothMoveGesture::PressPoint(SyntheticGestureTarget* target, |
| const base::TimeTicks& timestamp) { |
| DCHECK_EQ(current_move_segment_, 0); |
| synthetic_pointer_driver_->Press(current_move_segment_start_position_.x(), |
| current_move_segment_start_position_.y()); |
| synthetic_pointer_driver_->DispatchEvent(target, timestamp); |
| } |
| |
| void SyntheticSmoothMoveGesture::MovePoint(SyntheticGestureTarget* target, |
| const gfx::Vector2dF& delta, |
| const base::TimeTicks& timestamp) { |
| DCHECK_GE(current_move_segment_, 0); |
| DCHECK_LT(current_move_segment_, static_cast<int>(params().distances.size())); |
| gfx::PointF new_position = current_move_segment_start_position_ + delta; |
| synthetic_pointer_driver_->Move(new_position.x(), new_position.y()); |
| synthetic_pointer_driver_->DispatchEvent(target, timestamp); |
| } |
| |
| void SyntheticSmoothMoveGesture::ReleasePoint( |
| SyntheticGestureTarget* target, |
| const base::TimeTicks& timestamp) { |
| DCHECK_EQ(current_move_segment_, |
| static_cast<int>(params().distances.size()) - 1); |
| gfx::PointF position; |
| if (params().input_type == |
| SyntheticSmoothMoveGestureParams::MOUSE_DRAG_INPUT) { |
| position = current_move_segment_start_position_ + |
| GetPositionDeltaAtTime(timestamp); |
| } |
| synthetic_pointer_driver_->Release(); |
| synthetic_pointer_driver_->DispatchEvent(target, timestamp); |
| } |
| |
| void SyntheticSmoothMoveGesture::AddTouchSlopToFirstDistance( |
| SyntheticGestureTarget* target) { |
| DCHECK_GE(params().distances.size(), 1ul); |
| gfx::Vector2dF& first_move_distance = params().distances[0]; |
| DCHECK_GT(first_move_distance.Length(), 0); |
| first_move_distance += ProjectScalarOntoVector(target->GetTouchSlopInDips(), |
| first_move_distance); |
| } |
| |
| gfx::Vector2dF SyntheticSmoothMoveGesture::GetPositionDeltaAtTime( |
| const base::TimeTicks& timestamp) const { |
| // Make sure the final delta is correct. Using the computation below can lead |
| // to issues with floating point precision. |
| // TODO(bokan): This comment makes it sound like we have pixel perfect |
| // precision. In fact, gestures can accumulate a significant amount of |
| // error (e.g. due to snapping to physical pixels on each event). |
| if (FinishedCurrentMoveSegment(timestamp)) |
| return params().distances[current_move_segment_]; |
| |
| return gfx::ScaleVector2d( |
| params().distances[current_move_segment_], |
| GetCurvedRatio(timestamp, current_move_segment_start_time_, |
| current_move_segment_stop_time_, |
| params().speed_in_pixels_s)); |
| } |
| |
| void SyntheticSmoothMoveGesture::ComputeNextMoveSegment() { |
| current_move_segment_++; |
| DCHECK_LT(current_move_segment_, static_cast<int>(params().distances.size())); |
| // Percentage based scrolls do not require velocity and are delivered in a |
| // single segment. No need to compute another segment |
| const auto duration = |
| base::Seconds(double{params().distances[current_move_segment_].Length()} / |
| params().speed_in_pixels_s); |
| current_move_segment_start_time_ = current_move_segment_stop_time_; |
| current_move_segment_stop_time_ = current_move_segment_start_time_ + duration; |
| } |
| |
| base::TimeTicks SyntheticSmoothMoveGesture::ClampTimestamp( |
| const base::TimeTicks& timestamp) const { |
| return std::min(timestamp, current_move_segment_stop_time_); |
| } |
| |
| bool SyntheticSmoothMoveGesture::FinishedCurrentMoveSegment( |
| const base::TimeTicks& timestamp) const { |
| return timestamp >= current_move_segment_stop_time_; |
| } |
| |
| bool SyntheticSmoothMoveGesture::IsLastMoveSegment() const { |
| DCHECK_LT(current_move_segment_, static_cast<int>(params().distances.size())); |
| return current_move_segment_ == |
| static_cast<int>(params().distances.size()) - 1; |
| } |
| |
| bool SyntheticSmoothMoveGesture::MoveIsNoOp() const { |
| return params().distances.size() == 0 || params().distances[0].IsZero(); |
| } |
| |
| } // namespace content |