| // Copyright 2014 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 "content/browser/renderer_host/input/touch_emulator.h" |
| |
| #include <memory> |
| |
| #include "base/containers/queue.h" |
| #include "base/time/time.h" |
| #include "build/build_config.h" |
| #include "content/browser/renderer_host/input/motion_event_web.h" |
| #include "content/browser/renderer_host/render_widget_host_view_base.h" |
| #include "content/browser/renderer_host/ui_events_helper.h" |
| #include "content/common/input/web_touch_event_traits.h" |
| #include "content/grit/content_resources.h" |
| #include "content/public/common/content_client.h" |
| #include "content/public/common/content_switches.h" |
| #include "third_party/blink/public/common/input/web_keyboard_event.h" |
| #include "third_party/blink/public/common/input/web_mouse_event.h" |
| #include "ui/base/cursor/cursor.h" |
| #include "ui/base/cursor/mojom/cursor_type.mojom-shared.h" |
| #include "ui/base/ui_base_types.h" |
| #include "ui/events/base_event_utils.h" |
| #include "ui/events/blink/blink_event_util.h" |
| #include "ui/events/gesture_detection/gesture_provider_config_helper.h" |
| #include "ui/gfx/image/image.h" |
| |
| using blink::WebGestureEvent; |
| using blink::WebInputEvent; |
| using blink::WebKeyboardEvent; |
| using blink::WebMouseEvent; |
| using blink::WebMouseWheelEvent; |
| using blink::WebTouchEvent; |
| using blink::WebTouchPoint; |
| |
| namespace content { |
| |
| namespace { |
| |
| ui::GestureProvider::Config GetEmulatorGestureProviderConfig( |
| ui::GestureProviderConfigType config_type, |
| TouchEmulator::Mode mode) { |
| ui::GestureProvider::Config config = |
| ui::GetGestureProviderConfig(config_type); |
| config.gesture_begin_end_types_enabled = false; |
| config.gesture_detector_config.swipe_enabled = false; |
| config.gesture_detector_config.two_finger_tap_enabled = false; |
| if (mode == TouchEmulator::Mode::kInjectingTouchEvents) { |
| config.gesture_detector_config.longpress_timeout = base::TimeDelta::Max(); |
| config.gesture_detector_config.showpress_timeout = base::TimeDelta::Max(); |
| } |
| return config; |
| } |
| |
| int ModifiersWithoutMouseButtons(const WebInputEvent& event) { |
| const int all_buttons = WebInputEvent::kLeftButtonDown | |
| WebInputEvent::kMiddleButtonDown | |
| WebInputEvent::kRightButtonDown; |
| return event.GetModifiers() & ~all_buttons; |
| } |
| |
| // Time between two consecutive mouse moves, during which second mouse move |
| // is not converted to touch. |
| constexpr base::TimeDelta kMouseMoveDropInterval = base::Milliseconds(5); |
| |
| } // namespace |
| |
| TouchEmulator::TouchEmulator(TouchEmulatorClient* client, |
| float device_scale_factor) |
| : client_(client), |
| gesture_provider_config_type_( |
| ui::GestureProviderConfigType::CURRENT_PLATFORM), |
| double_tap_enabled_(true), |
| use_2x_cursors_(false), |
| pinch_gesture_mode_for_testing_(false), |
| emulated_stream_active_sequence_count_(0), |
| native_stream_active_sequence_count_(0), |
| last_emulated_start_target_(nullptr), |
| pending_taps_count_(0) { |
| DCHECK(client_); |
| ResetState(); |
| InitCursors(device_scale_factor, true); |
| } |
| |
| TouchEmulator::~TouchEmulator() { |
| // We cannot cleanup properly in destructor, as we need roundtrip to the |
| // renderer for ack. Instead, the owner should call Disable, and only |
| // destroy this object when renderer is dead. |
| } |
| |
| void TouchEmulator::ResetState() { |
| last_mouse_event_was_move_ = false; |
| last_mouse_move_timestamp_ = base::TimeTicks(); |
| mouse_pressed_ = false; |
| shift_pressed_ = false; |
| suppress_next_fling_cancel_ = false; |
| pinch_scale_ = 1.f; |
| pinch_gesture_active_ = false; |
| } |
| |
| void TouchEmulator::Enable(Mode mode, |
| ui::GestureProviderConfigType config_type) { |
| if (gesture_provider_ && mode_ != mode) |
| client_->SetCursor(pointer_cursor_); |
| |
| if (!gesture_provider_ || gesture_provider_config_type_ != config_type || |
| mode_ != mode) { |
| mode_ = mode; |
| gesture_provider_config_type_ = config_type; |
| gesture_provider_ = std::make_unique<ui::FilteredGestureProvider>( |
| GetEmulatorGestureProviderConfig(config_type, mode), this); |
| gesture_provider_->SetDoubleTapSupportForPageEnabled(double_tap_enabled_); |
| // TODO(dgozman): Use synthetic secondary touch to support multi-touch. |
| gesture_provider_->SetMultiTouchZoomSupportEnabled( |
| mode != Mode::kEmulatingTouchFromMouse); |
| } |
| |
| UpdateCursor(); |
| } |
| |
| void TouchEmulator::Disable() { |
| if (!enabled()) |
| return; |
| |
| mode_ = Mode::kEmulatingTouchFromMouse; |
| CancelTouch(); |
| gesture_provider_.reset(); |
| base::queue<base::OnceClosure> empty; |
| injected_touch_completion_callbacks_.swap(empty); |
| client_->SetCursor(pointer_cursor_); |
| ResetState(); |
| } |
| |
| void TouchEmulator::SetDeviceScaleFactor(float device_scale_factor) { |
| if (!InitCursors(device_scale_factor, false)) |
| return; |
| if (enabled()) |
| UpdateCursor(); |
| } |
| |
| void TouchEmulator::SetDoubleTapSupportForPageEnabled(bool enabled) { |
| double_tap_enabled_ = enabled; |
| if (gesture_provider_) |
| gesture_provider_->SetDoubleTapSupportForPageEnabled(enabled); |
| } |
| |
| bool TouchEmulator::InitCursors(float device_scale_factor, bool force) { |
| bool use_2x = device_scale_factor > 1.5f; |
| if (use_2x == use_2x_cursors_ && !force) |
| return false; |
| use_2x_cursors_ = use_2x; |
| float cursor_scale_factor = use_2x ? 2.f : 1.f; |
| cursor_size_ = InitCursorFromResource(&touch_cursor_, |
| cursor_scale_factor, |
| use_2x ? IDR_DEVTOOLS_TOUCH_CURSOR_ICON_2X : |
| IDR_DEVTOOLS_TOUCH_CURSOR_ICON); |
| InitCursorFromResource(&pinch_cursor_, |
| cursor_scale_factor, |
| use_2x ? IDR_DEVTOOLS_PINCH_CURSOR_ICON_2X : |
| IDR_DEVTOOLS_PINCH_CURSOR_ICON); |
| |
| pointer_cursor_ = WebCursor(ui::mojom::CursorType::kPointer); |
| return true; |
| } |
| |
| gfx::SizeF TouchEmulator::InitCursorFromResource( |
| WebCursor* cursor, float scale, int resource_id) { |
| gfx::Image& cursor_image = |
| content::GetContentClient()->GetNativeImageNamed(resource_id); |
| ui::Cursor cursor_info(ui::mojom::CursorType::kCustom); |
| cursor_info.set_image_scale_factor(scale); |
| cursor_info.set_custom_bitmap(cursor_image.AsBitmap()); |
| cursor_info.set_custom_hotspot( |
| gfx::Point(cursor_image.Width() / 2, cursor_image.Height() / 2)); |
| |
| *cursor = WebCursor(cursor_info); |
| return gfx::ScaleSize(gfx::SizeF(cursor_image.Size()), 1.f / scale); |
| } |
| |
| bool TouchEmulator::HandleMouseEvent(const WebMouseEvent& mouse_event, |
| RenderWidgetHostViewBase* target_view) { |
| if (!enabled() || mode_ != Mode::kEmulatingTouchFromMouse) |
| return false; |
| |
| UpdateCursor(); |
| |
| if (mouse_event.button == WebMouseEvent::Button::kRight && |
| mouse_event.GetType() == WebInputEvent::Type::kMouseDown) { |
| client_->ShowContextMenuAtPoint( |
| gfx::Point(mouse_event.PositionInWidget().x(), |
| mouse_event.PositionInWidget().y()), |
| ui::MENU_SOURCE_MOUSE, target_view); |
| } |
| |
| if (mouse_event.button != WebMouseEvent::Button::kLeft) |
| return true; |
| |
| if (mouse_event.GetType() == WebInputEvent::Type::kMouseMove) { |
| if (last_mouse_event_was_move_ && |
| mouse_event.TimeStamp() < |
| last_mouse_move_timestamp_ + kMouseMoveDropInterval) |
| return true; |
| |
| last_mouse_event_was_move_ = true; |
| last_mouse_move_timestamp_ = mouse_event.TimeStamp(); |
| } else { |
| last_mouse_event_was_move_ = false; |
| } |
| |
| if (mouse_event.GetType() == WebInputEvent::Type::kMouseDown) |
| mouse_pressed_ = true; |
| else if (mouse_event.GetType() == WebInputEvent::Type::kMouseUp) |
| mouse_pressed_ = false; |
| |
| UpdateShiftPressed((mouse_event.GetModifiers() & WebInputEvent::kShiftKey) != |
| 0); |
| |
| if (mouse_event.GetType() != WebInputEvent::Type::kMouseDown && |
| mouse_event.GetType() != WebInputEvent::Type::kMouseMove && |
| mouse_event.GetType() != WebInputEvent::Type::kMouseUp) { |
| return true; |
| } |
| |
| gfx::PointF pos_in_root = mouse_event.PositionInWidget(); |
| if (target_view) |
| pos_in_root = target_view->TransformPointToRootCoordSpaceF(pos_in_root); |
| FillTouchEventAndPoint(mouse_event, pos_in_root); |
| HandleEmulatedTouchEvent(touch_event_, target_view); |
| |
| // Do not pass mouse events to the renderer. |
| return true; |
| } |
| |
| bool TouchEmulator::HandleMouseWheelEvent(const WebMouseWheelEvent& event) { |
| if (!enabled() || mode_ != Mode::kEmulatingTouchFromMouse) |
| return false; |
| |
| // Send mouse wheel for easy scrolling when there is no active touch. |
| return emulated_stream_active_sequence_count_ > 0; |
| } |
| |
| bool TouchEmulator::HandleKeyboardEvent(const WebKeyboardEvent& event) { |
| if (!enabled() || mode_ != Mode::kEmulatingTouchFromMouse) |
| return false; |
| |
| if (!UpdateShiftPressed((event.GetModifiers() & WebInputEvent::kShiftKey) != |
| 0)) |
| return false; |
| |
| if (!mouse_pressed_) |
| return false; |
| |
| // Note: The necessary pinch events will be lazily inserted by |
| // |OnGestureEvent| depending on the state of |shift_pressed_|, using the |
| // scroll stream as the event driver. |
| if (shift_pressed_) { |
| // TODO(dgozman): Add secondary touch point and set anchor. |
| } else { |
| // TODO(dgozman): Remove secondary touch point and anchor. |
| } |
| |
| // Never block keyboard events. |
| return false; |
| } |
| |
| bool TouchEmulator::HandleTouchEvent(const blink::WebTouchEvent& event) { |
| // Block native event when emulated touch stream is active. |
| if (emulated_stream_active_sequence_count_) |
| return true; |
| |
| bool is_sequence_start = WebTouchEventTraits::IsTouchSequenceStart(event); |
| // Do not allow middle-sequence event to pass through, if start was blocked. |
| if (!native_stream_active_sequence_count_ && !is_sequence_start) |
| return true; |
| |
| if (is_sequence_start) |
| native_stream_active_sequence_count_++; |
| return false; |
| } |
| |
| bool TouchEmulator::HandleEmulatedTouchEvent( |
| blink::WebTouchEvent event, |
| RenderWidgetHostViewBase* target_view) { |
| DCHECK(gesture_provider_); |
| event.unique_touch_event_id = ui::GetNextTouchEventId(); |
| auto result = gesture_provider_->OnTouchEvent(MotionEventWeb(event)); |
| if (!result.succeeded) |
| return true; |
| |
| const bool event_consumed = true; |
| const bool is_source_touch_event_set_blocking = false; |
| // Block emulated event when emulated native stream is active. |
| if (native_stream_active_sequence_count_) { |
| gesture_provider_->OnTouchEventAck(event.unique_touch_event_id, |
| event_consumed, |
| is_source_touch_event_set_blocking); |
| return true; |
| } |
| |
| bool is_sequence_start = WebTouchEventTraits::IsTouchSequenceStart(event); |
| // Do not allow middle-sequence event to pass through, if start was blocked. |
| if (!emulated_stream_active_sequence_count_ && !is_sequence_start) { |
| gesture_provider_->OnTouchEventAck(event.unique_touch_event_id, |
| event_consumed, |
| is_source_touch_event_set_blocking); |
| return true; |
| } |
| |
| if (is_sequence_start) { |
| emulated_stream_active_sequence_count_++; |
| last_emulated_start_target_ = target_view; |
| } |
| |
| event.moved_beyond_slop_region = result.moved_beyond_slop_region; |
| client_->ForwardEmulatedTouchEvent(event, target_view); |
| return false; |
| } |
| |
| bool TouchEmulator::HandleTouchEventAck( |
| const blink::WebTouchEvent& event, |
| blink::mojom::InputEventResultState ack_result) { |
| bool is_sequence_end = WebTouchEventTraits::IsTouchSequenceEnd(event); |
| if (emulated_stream_active_sequence_count_) { |
| if (is_sequence_end) |
| emulated_stream_active_sequence_count_--; |
| |
| int taps_count_before = pending_taps_count_; |
| const bool event_consumed = |
| ack_result == blink::mojom::InputEventResultState::kConsumed; |
| if (gesture_provider_) { |
| gesture_provider_->OnTouchEventAck( |
| event.unique_touch_event_id, event_consumed, |
| InputEventResultStateIsSetNonBlocking(ack_result)); |
| } |
| if (pending_taps_count_ == taps_count_before) |
| OnInjectedTouchCompleted(); |
| return true; |
| } |
| |
| // We may have not seen native touch sequence start (when created in the |
| // middle of a sequence), so don't decrement sequence count below zero. |
| if (is_sequence_end && native_stream_active_sequence_count_) |
| native_stream_active_sequence_count_--; |
| return false; |
| } |
| |
| void TouchEmulator::OnGestureEventAck(const WebGestureEvent& event, |
| RenderWidgetHostViewBase*) { |
| if (event.GetType() != WebInputEvent::Type::kGestureTap) |
| return; |
| if (pending_taps_count_) { |
| pending_taps_count_--; |
| OnInjectedTouchCompleted(); |
| } |
| } |
| |
| void TouchEmulator::OnViewDestroyed(RenderWidgetHostViewBase* destroyed_view) { |
| if (destroyed_view != last_emulated_start_target_) |
| return; |
| |
| emulated_stream_active_sequence_count_ = 0; |
| // If we aren't enabled, just reset out target. |
| if (!gesture_provider_) { |
| last_emulated_start_target_ = nullptr; |
| return; |
| } |
| |
| // TouchEmulator is still enabled. To reset the state go through a |
| // Disable/Enable sequence to destroy the GestureRecognizer otherwise it will |
| // be left in an unknown state because the associated view was destroyed. |
| ui::GestureProviderConfigType config_type = gesture_provider_config_type_; |
| Mode mode = mode_; |
| Disable(); |
| Enable(mode, config_type); |
| } |
| |
| void TouchEmulator::OnGestureEvent(const ui::GestureEventData& gesture) { |
| WebGestureEvent gesture_event = |
| ui::CreateWebGestureEventFromGestureEventData(gesture); |
| |
| DCHECK(gesture_event.unique_touch_event_id); |
| |
| switch (gesture_event.GetType()) { |
| case WebInputEvent::Type::kUndefined: |
| NOTREACHED() << "Undefined WebInputEvent type"; |
| // Bail without sending the junk event to the client. |
| return; |
| |
| case WebInputEvent::Type::kGestureScrollBegin: |
| client_->ForwardEmulatedGestureEvent(gesture_event); |
| // PinchBegin must always follow ScrollBegin. |
| if (InPinchGestureMode()) |
| PinchBegin(gesture_event); |
| break; |
| |
| case WebInputEvent::Type::kGestureScrollUpdate: |
| if (InPinchGestureMode()) { |
| // Convert scrolls to pinches while shift is pressed. |
| if (!pinch_gesture_active_) |
| PinchBegin(gesture_event); |
| else |
| PinchUpdate(gesture_event); |
| } else { |
| // Pass scroll update further. If shift was released, end the pinch. |
| if (pinch_gesture_active_) |
| PinchEnd(gesture_event); |
| client_->ForwardEmulatedGestureEvent(gesture_event); |
| } |
| break; |
| |
| case WebInputEvent::Type::kGestureScrollEnd: |
| // PinchEnd must precede ScrollEnd. |
| if (pinch_gesture_active_) |
| PinchEnd(gesture_event); |
| client_->ForwardEmulatedGestureEvent(gesture_event); |
| break; |
| |
| case WebInputEvent::Type::kGestureFlingStart: |
| // PinchEnd must precede FlingStart. |
| if (pinch_gesture_active_) |
| PinchEnd(gesture_event); |
| if (InPinchGestureMode()) { |
| // No fling in pinch mode. Forward scroll end instead of fling start. |
| suppress_next_fling_cancel_ = true; |
| ScrollEnd(gesture_event); |
| } else { |
| suppress_next_fling_cancel_ = false; |
| client_->ForwardEmulatedGestureEvent(gesture_event); |
| } |
| break; |
| |
| case WebInputEvent::Type::kGestureFlingCancel: |
| // If fling start was suppressed, we should not send fling cancel either. |
| if (!suppress_next_fling_cancel_) |
| client_->ForwardEmulatedGestureEvent(gesture_event); |
| suppress_next_fling_cancel_ = false; |
| break; |
| |
| case WebInputEvent::Type::kGestureTap: |
| pending_taps_count_++; |
| client_->ForwardEmulatedGestureEvent(gesture_event); |
| break; |
| |
| default: |
| // Everything else goes through. |
| client_->ForwardEmulatedGestureEvent(gesture_event); |
| } |
| } |
| |
| bool TouchEmulator::RequiresDoubleTapGestureEvents() const { |
| return true; |
| } |
| |
| void TouchEmulator::InjectTouchEvent(const blink::WebTouchEvent& event, |
| RenderWidgetHostViewBase* target_view, |
| base::OnceClosure callback) { |
| DCHECK(enabled() && mode_ == Mode::kInjectingTouchEvents); |
| touch_event_ = event; |
| injected_touch_completion_callbacks_.push(std::move(callback)); |
| if (HandleEmulatedTouchEvent(touch_event_, target_view)) |
| OnInjectedTouchCompleted(); |
| } |
| |
| void TouchEmulator::OnInjectedTouchCompleted() { |
| if (injected_touch_completion_callbacks_.empty()) |
| return; |
| if (!injected_touch_completion_callbacks_.front().is_null()) |
| std::move(injected_touch_completion_callbacks_.front()).Run(); |
| injected_touch_completion_callbacks_.pop(); |
| } |
| |
| void TouchEmulator::CancelTouch() { |
| if (!emulated_stream_active_sequence_count_ || !enabled() || |
| mode_ != Mode::kEmulatingTouchFromMouse) { |
| return; |
| } |
| |
| WebTouchEventTraits::ResetTypeAndTouchStates( |
| WebInputEvent::Type::kTouchCancel, ui::EventTimeForNow(), &touch_event_); |
| DCHECK(gesture_provider_); |
| if (gesture_provider_->GetCurrentDownEvent()) |
| HandleEmulatedTouchEvent(touch_event_, last_emulated_start_target_); |
| } |
| |
| void TouchEmulator::UpdateCursor() { |
| DCHECK(enabled()); |
| if (mode_ == Mode::kEmulatingTouchFromMouse) |
| client_->SetCursor(InPinchGestureMode() ? pinch_cursor_ : touch_cursor_); |
| } |
| |
| bool TouchEmulator::UpdateShiftPressed(bool shift_pressed) { |
| DCHECK(enabled() && mode_ == Mode::kEmulatingTouchFromMouse); |
| if (shift_pressed_ == shift_pressed) |
| return false; |
| shift_pressed_ = shift_pressed; |
| UpdateCursor(); |
| return true; |
| } |
| |
| void TouchEmulator::PinchBegin(const WebGestureEvent& event) { |
| DCHECK(InPinchGestureMode()); |
| DCHECK(!pinch_gesture_active_); |
| pinch_gesture_active_ = true; |
| pinch_anchor_ = event.PositionInWidget(); |
| pinch_scale_ = 1.f; |
| WebGestureEvent pinch_event = |
| GetPinchGestureEvent(WebInputEvent::Type::kGesturePinchBegin, event); |
| client_->ForwardEmulatedGestureEvent(pinch_event); |
| } |
| |
| void TouchEmulator::PinchUpdate(const WebGestureEvent& event) { |
| DCHECK(pinch_gesture_active_); |
| float dy = pinch_anchor_.y() - event.PositionInWidget().y(); |
| float scale = exp(dy * 0.002f); |
| WebGestureEvent pinch_event = |
| GetPinchGestureEvent(WebInputEvent::Type::kGesturePinchUpdate, event); |
| pinch_event.data.pinch_update.scale = scale / pinch_scale_; |
| client_->ForwardEmulatedGestureEvent(pinch_event); |
| pinch_scale_ = scale; |
| } |
| |
| void TouchEmulator::PinchEnd(const WebGestureEvent& event) { |
| DCHECK(pinch_gesture_active_); |
| pinch_gesture_active_ = false; |
| WebGestureEvent pinch_event = |
| GetPinchGestureEvent(WebInputEvent::Type::kGesturePinchEnd, event); |
| client_->ForwardEmulatedGestureEvent(pinch_event); |
| } |
| |
| void TouchEmulator::ScrollEnd(const WebGestureEvent& event) { |
| WebGestureEvent scroll_event(WebInputEvent::Type::kGestureScrollEnd, |
| ModifiersWithoutMouseButtons(event), |
| event.TimeStamp(), |
| blink::WebGestureDevice::kTouchscreen); |
| scroll_event.unique_touch_event_id = event.unique_touch_event_id; |
| client_->ForwardEmulatedGestureEvent(scroll_event); |
| } |
| |
| WebGestureEvent TouchEmulator::GetPinchGestureEvent( |
| WebInputEvent::Type type, |
| const WebGestureEvent& original_event) { |
| WebGestureEvent event(type, ModifiersWithoutMouseButtons(original_event), |
| original_event.TimeStamp(), |
| blink::WebGestureDevice::kTouchscreen); |
| event.SetPositionInWidget(pinch_anchor_); |
| event.unique_touch_event_id = original_event.unique_touch_event_id; |
| return event; |
| } |
| |
| void TouchEmulator::FillTouchEventAndPoint(const WebMouseEvent& mouse_event, |
| const gfx::PointF& pos_in_root) { |
| WebInputEvent::Type eventType; |
| switch (mouse_event.GetType()) { |
| case WebInputEvent::Type::kMouseDown: |
| eventType = WebInputEvent::Type::kTouchStart; |
| break; |
| case WebInputEvent::Type::kMouseMove: |
| eventType = WebInputEvent::Type::kTouchMove; |
| break; |
| case WebInputEvent::Type::kMouseUp: |
| eventType = WebInputEvent::Type::kTouchEnd; |
| break; |
| default: |
| eventType = WebInputEvent::Type::kUndefined; |
| NOTREACHED() << "Invalid event for touch emulation: " |
| << mouse_event.GetType(); |
| } |
| touch_event_.touches_length = 1; |
| touch_event_.SetModifiers(ModifiersWithoutMouseButtons(mouse_event)); |
| WebTouchEventTraits::ResetTypeAndTouchStates( |
| eventType, mouse_event.TimeStamp(), &touch_event_); |
| WebTouchPoint& point = touch_event_.touches[0]; |
| point.id = 0; |
| point.radius_x = 0.5f * cursor_size_.width(); |
| point.radius_y = 0.5f * cursor_size_.height(); |
| point.force = eventType == WebInputEvent::Type::kTouchEnd ? 0.f : 1.f; |
| point.rotation_angle = 0.f; |
| // We need to convert this to the root-view's coord space, otherwise the |
| // GestureRecognizer will potentially receive events for a moving widget, |
| // for example when scroll bubbling is taking place. The GestureRecognizer |
| // isn't designed to handle that. |
| point.SetPositionInWidget(pos_in_root); |
| point.SetPositionInScreen(mouse_event.PositionInScreen().x(), |
| mouse_event.PositionInScreen().y()); |
| point.tilt_x = 0; |
| point.tilt_y = 0; |
| point.pointer_type = blink::WebPointerProperties::PointerType::kTouch; |
| } |
| |
| bool TouchEmulator::InPinchGestureMode() const { |
| return shift_pressed_ || pinch_gesture_mode_for_testing_; |
| } |
| |
| void TouchEmulator::SetPinchGestureModeForTesting(bool pinch_gesture_mode) { |
| pinch_gesture_mode_for_testing_ = pinch_gesture_mode; |
| } |
| |
| } // namespace content |