diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index 4787934ca71fb..3497ad1dcf95e 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -2286,6 +2286,9 @@ FILE: ../../../flutter/shell/platform/windows/client_wrapper/include/flutter/plu FILE: ../../../flutter/shell/platform/windows/client_wrapper/plugin_registrar_windows_unittests.cc FILE: ../../../flutter/shell/platform/windows/cursor_handler.cc FILE: ../../../flutter/shell/platform/windows/cursor_handler.h +FILE: ../../../flutter/shell/platform/windows/direct_manipulation.cc +FILE: ../../../flutter/shell/platform/windows/direct_manipulation.h +FILE: ../../../flutter/shell/platform/windows/direct_manipulation_unittests.cc FILE: ../../../flutter/shell/platform/windows/dpi_utils_win32.cc FILE: ../../../flutter/shell/platform/windows/dpi_utils_win32.h FILE: ../../../flutter/shell/platform/windows/dpi_utils_win32_unittests.cc diff --git a/shell/platform/windows/BUILD.gn b/shell/platform/windows/BUILD.gn index 2244e80866b73..52abb4d0aea3b 100644 --- a/shell/platform/windows/BUILD.gn +++ b/shell/platform/windows/BUILD.gn @@ -44,6 +44,8 @@ source_set("flutter_windows_source") { "angle_surface_manager.h", "cursor_handler.cc", "cursor_handler.h", + "direct_manipulation.cc", + "direct_manipulation.h", "dpi_utils_win32.cc", "dpi_utils_win32.h", "event_watcher_win32.cc", @@ -130,6 +132,7 @@ source_set("flutter_windows_source") { deps = [ ":flutter_windows_headers", + "//flutter/fml:fml", "//flutter/shell/platform/common:common_cpp", "//flutter/shell/platform/common:common_cpp_input", "//flutter/shell/platform/common:common_cpp_switches", @@ -170,6 +173,7 @@ executable("flutter_windows_unittests") { # Common Windows test sources. sources = [ "accessibility_bridge_delegate_win32_unittests.cc", + "direct_manipulation_unittests.cc", "dpi_utils_win32_unittests.cc", "flutter_project_bundle_unittests.cc", "flutter_window_win32_unittests.cc", diff --git a/shell/platform/windows/direct_manipulation.cc b/shell/platform/windows/direct_manipulation.cc new file mode 100644 index 0000000000000..60eba79fb84dc --- /dev/null +++ b/shell/platform/windows/direct_manipulation.cc @@ -0,0 +1,222 @@ +// Copyright 2013 The Flutter 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 "flutter/fml/logging.h" + +#include "flutter/shell/platform/windows/direct_manipulation.h" +#include "flutter/shell/platform/windows/window_binding_handler_delegate.h" +#include "flutter/shell/platform/windows/window_win32.h" + +#define RETURN_IF_FAILED(operation) \ + if (FAILED(operation)) { \ + FML_LOG(ERROR) << #operation << " failed"; \ + manager_ = nullptr; \ + updateManager_ = nullptr; \ + viewport_ = nullptr; \ + return -1; \ + } + +#define WARN_IF_FAILED(operation) \ + if (FAILED(operation)) { \ + FML_LOG(ERROR) << #operation << " failed"; \ + } + +namespace flutter { + +STDMETHODIMP DirectManipulationEventHandler::QueryInterface(REFIID iid, + void** ppv) { + if ((iid == IID_IUnknown) || + (iid == IID_IDirectManipulationViewportEventHandler)) { + *ppv = static_cast(this); + AddRef(); + return S_OK; + } else if (iid == IID_IDirectManipulationInteractionEventHandler) { + *ppv = static_cast(this); + AddRef(); + return S_OK; + } + return E_NOINTERFACE; +} + +HRESULT DirectManipulationEventHandler::OnViewportStatusChanged( + IDirectManipulationViewport* viewport, + DIRECTMANIPULATION_STATUS current, + DIRECTMANIPULATION_STATUS previous) { + if (during_synthesized_reset_ && previous == DIRECTMANIPULATION_RUNNING) { + during_synthesized_reset_ = false; + } else if (current == DIRECTMANIPULATION_RUNNING) { + if (!during_synthesized_reset_) { + // Not a false event. + if (owner_->binding_handler_delegate) { + owner_->binding_handler_delegate->OnPointerPanZoomStart( + (int32_t) reinterpret_cast(this)); + } + } + } else if (previous == DIRECTMANIPULATION_RUNNING) { + if (owner_->binding_handler_delegate) { + owner_->binding_handler_delegate->OnPointerPanZoomEnd( + (int32_t) reinterpret_cast(this)); + } + // Need to reset the content transform to its original position + // so that we are ready for the next gesture. + // Use during_synthesized_reset_ flag to prevent sending reset also to the + // framework. + during_synthesized_reset_ = true; + RECT rect; + HRESULT hr = viewport->GetViewportRect(&rect); + if (FAILED(hr)) { + FML_LOG(ERROR) << "Failed to get the current viewport rect"; + return E_FAIL; + } + hr = viewport->ZoomToRect(rect.left, rect.top, rect.right, rect.bottom, + false); + if (FAILED(hr)) { + FML_LOG(ERROR) << "Failed to reset the gesture using ZoomToRect"; + return E_FAIL; + } + } + return S_OK; +} + +HRESULT DirectManipulationEventHandler::OnViewportUpdated( + IDirectManipulationViewport* viewport) { + return S_OK; +} + +HRESULT DirectManipulationEventHandler::OnContentUpdated( + IDirectManipulationViewport* viewport, + IDirectManipulationContent* content) { + float transform[6]; + HRESULT hr = content->GetContentTransform(transform, ARRAYSIZE(transform)); + if (FAILED(hr)) { + FML_LOG(ERROR) << "GetContentTransform failed"; + return S_OK; + } + if (!during_synthesized_reset_) { + // DirectManipulation provides updates with very high precision. If the user + // holds their fingers steady on a trackpad, DirectManipulation sends + // jittery updates. This calculation will reduce the precision of the scale + // value of the event to avoid jitter. + const int mantissa_bits_chop = 2; + const float factor = (1 << mantissa_bits_chop) + 1; + float c = factor * transform[0]; + float scale = c - (c - transform[0]); + float pan_x = transform[4]; + float pan_y = transform[5]; + if (owner_->binding_handler_delegate) { + owner_->binding_handler_delegate->OnPointerPanZoomUpdate( + (int32_t) reinterpret_cast(this), pan_x, pan_y, scale, 0); + } + } + return S_OK; +} + +HRESULT DirectManipulationEventHandler::OnInteraction( + IDirectManipulationViewport2* viewport, + DIRECTMANIPULATION_INTERACTION_TYPE interaction) { + return S_OK; +} + +ULONG STDMETHODCALLTYPE DirectManipulationEventHandler::AddRef() { + RefCountedThreadSafe::AddRef(); + return 0; +} + +ULONG STDMETHODCALLTYPE DirectManipulationEventHandler::Release() { + RefCountedThreadSafe::Release(); + return 0; +} + +DirectManipulationOwner::DirectManipulationOwner(WindowWin32* window) + : window_(window) {} + +int DirectManipulationOwner::Init(unsigned int width, unsigned int height) { + RETURN_IF_FAILED(CoCreateInstance(CLSID_DirectManipulationManager, nullptr, + CLSCTX_INPROC_SERVER, + IID_IDirectManipulationManager, &manager_)); + RETURN_IF_FAILED(manager_->GetUpdateManager( + IID_IDirectManipulationUpdateManager, &updateManager_)); + RETURN_IF_FAILED(manager_->CreateViewport(nullptr, window_->GetWindowHandle(), + IID_IDirectManipulationViewport, + &viewport_)); + DIRECTMANIPULATION_CONFIGURATION configuration = + DIRECTMANIPULATION_CONFIGURATION_INTERACTION | + DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_X | + DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_Y | + DIRECTMANIPULATION_CONFIGURATION_SCALING; + RETURN_IF_FAILED(viewport_->ActivateConfiguration(configuration)); + RETURN_IF_FAILED(viewport_->SetViewportOptions( + DIRECTMANIPULATION_VIEWPORT_OPTIONS_MANUALUPDATE)); + handler_ = fml::MakeRefCounted(this); + RETURN_IF_FAILED(viewport_->AddEventHandler( + window_->GetWindowHandle(), handler_.get(), &viewportHandlerCookie_)); + RECT rect = {0, 0, (LONG)width, (LONG)height}; + RETURN_IF_FAILED(viewport_->SetViewportRect(&rect)); + RETURN_IF_FAILED(manager_->Activate(window_->GetWindowHandle())); + RETURN_IF_FAILED(viewport_->Enable()); + RETURN_IF_FAILED(updateManager_->Update(nullptr)); + return 0; +} + +void DirectManipulationOwner::ResizeViewport(unsigned int width, + unsigned int height) { + if (viewport_) { + RECT rect = {0, 0, (LONG)width, (LONG)height}; + WARN_IF_FAILED(viewport_->SetViewportRect(&rect)); + } +} + +void DirectManipulationOwner::Destroy() { + if (handler_) { + handler_->owner_ = nullptr; + } + + if (viewport_) { + WARN_IF_FAILED(viewport_->Disable()); + WARN_IF_FAILED(viewport_->Disable()); + WARN_IF_FAILED(viewport_->RemoveEventHandler(viewportHandlerCookie_)); + WARN_IF_FAILED(viewport_->Abandon()); + } + + if (window_ && manager_) { + WARN_IF_FAILED(manager_->Deactivate(window_->GetWindowHandle())); + } + + handler_ = nullptr; + viewport_ = nullptr; + updateManager_ = nullptr; + manager_ = nullptr; + window_ = nullptr; +} + +void DirectManipulationOwner::SetContact(UINT contactId) { + if (viewport_) { + viewport_->SetContact(contactId); + } +} + +void DirectManipulationOwner::SetBindingHandlerDelegate( + WindowBindingHandlerDelegate* delegate) { + binding_handler_delegate = delegate; +} + +void DirectManipulationOwner::Update() { + if (updateManager_) { + HRESULT hr = updateManager_->Update(nullptr); + if (FAILED(hr)) { + FML_LOG(ERROR) << "updateManager_->Update failed"; + auto error = GetLastError(); + FML_LOG(ERROR) << error; + LPWSTR message = nullptr; + size_t size = FormatMessageW( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&message), 0, NULL); + FML_LOG(ERROR) << message; + } + } +} + +} // namespace flutter diff --git a/shell/platform/windows/direct_manipulation.h b/shell/platform/windows/direct_manipulation.h new file mode 100644 index 0000000000000..bca747173e4f8 --- /dev/null +++ b/shell/platform/windows/direct_manipulation.h @@ -0,0 +1,116 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_DIRECT_MANIPULATION_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_DIRECT_MANIPULATION_H_ + +#include "flutter/fml/memory/ref_counted.h" + +#include +#include "directmanipulation.h" + +namespace flutter { + +class WindowWin32; +class WindowBindingHandlerDelegate; + +class DirectManipulationEventHandler; + +// Owner for a DirectManipulation event handler, contains the link between +// DirectManipulation and WindowBindingHandlerDelegate. +class DirectManipulationOwner { + public: + explicit DirectManipulationOwner(WindowWin32* window); + // Initialize a DirectManipulation viewport with specified width and height. + // These should match the width and height of the application window. + int Init(unsigned int width, unsigned int height); + // Resize the DirectManipulation viewport. Should be called when the + // application window is resized. + void ResizeViewport(unsigned int width, unsigned int height); + // Set the WindowBindingHandlerDelegate which will receive callbacks based on + // DirectManipulation updates. + void SetBindingHandlerDelegate( + WindowBindingHandlerDelegate* binding_handler_delegate); + // Called when DM_POINTERHITTEST occurs with an acceptable pointer type. Will + // start DirectManipulation for that interaction. + void SetContact(UINT contactId); + // Called to get updates from DirectManipulation. Should be called frequently + // to provide smooth updates. + void Update(); + // Release child event handler and OS resources. + void Destroy(); + // The target that should be updated when DirectManipulation provides a new + // pan/zoom transformation. + WindowBindingHandlerDelegate* binding_handler_delegate; + + private: + // The window gesture input is occuring on. + WindowWin32* window_; + // Cookie needed to register child event handler with viewport. + DWORD viewportHandlerCookie_; + // Object needed for operation of the DirectManipulation API. + Microsoft::WRL::ComPtr manager_; + // Object needed for operation of the DirectManipulation API. + Microsoft::WRL::ComPtr updateManager_; + // Object needed for operation of the DirectManipulation API. + Microsoft::WRL::ComPtr viewport_; + // Child needed for operation of the DirectManipulation API. + fml::RefPtr handler_; +}; + +// Implements DirectManipulation event handling interfaces, receives calls from +// system when gesture events occur. +class DirectManipulationEventHandler + : public fml::RefCountedThreadSafe, + public IDirectManipulationViewportEventHandler, + public IDirectManipulationInteractionEventHandler { + friend class DirectManipulationOwner; + FML_FRIEND_REF_COUNTED_THREAD_SAFE(DirectManipulationEventHandler); + FML_FRIEND_MAKE_REF_COUNTED(DirectManipulationEventHandler); + + public: + explicit DirectManipulationEventHandler(DirectManipulationOwner* owner) + : owner_(owner) {} + + // |IUnknown| + STDMETHODIMP QueryInterface(REFIID iid, void** ppv) override; + + // |IUnknown| + ULONG STDMETHODCALLTYPE AddRef() override; + + // |IUnknown| + ULONG STDMETHODCALLTYPE Release() override; + + // |IDirectManipulationViewportEventHandler| + HRESULT STDMETHODCALLTYPE + OnViewportStatusChanged(IDirectManipulationViewport* viewport, + DIRECTMANIPULATION_STATUS current, + DIRECTMANIPULATION_STATUS previous) override; + + // |IDirectManipulationViewportEventHandler| + HRESULT STDMETHODCALLTYPE + OnViewportUpdated(IDirectManipulationViewport* viewport) override; + + // |IDirectManipulationViewportEventHandler| + HRESULT STDMETHODCALLTYPE + OnContentUpdated(IDirectManipulationViewport* viewport, + IDirectManipulationContent* content) override; + + // |IDirectManipulationInteractionEventHandler| + HRESULT STDMETHODCALLTYPE + OnInteraction(IDirectManipulationViewport2* viewport, + DIRECTMANIPULATION_INTERACTION_TYPE interaction) override; + + private: + // Parent object, used to store the target for gesture event updates. + DirectManipulationOwner* owner_; + // We need to reset some parts of DirectManipulation after each gesture + // A flag is needed to ensure that false events created as the reset occurs + // are not sent to the flutter framework. + bool during_synthesized_reset_ = false; +}; + +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_DIRECT_MANIPULATION_H_ diff --git a/shell/platform/windows/direct_manipulation_unittests.cc b/shell/platform/windows/direct_manipulation_unittests.cc new file mode 100644 index 0000000000000..5975103e849b8 --- /dev/null +++ b/shell/platform/windows/direct_manipulation_unittests.cc @@ -0,0 +1,263 @@ +// Copyright 2013 The Flutter 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 "flutter/shell/platform/windows/direct_manipulation.h" +#include "flutter/shell/platform/windows/testing/mock_window_binding_handler_delegate.h" + +#include "gtest/gtest.h" + +using testing::_; + +namespace flutter { +namespace testing { + +class MockIDirectManipulationViewport : public IDirectManipulationViewport { + public: + MockIDirectManipulationViewport() {} + + // Prevent copying. + MockIDirectManipulationViewport(MockIDirectManipulationViewport const&) = + delete; + MockIDirectManipulationViewport& operator=( + MockIDirectManipulationViewport const&) = delete; + MOCK_METHOD0_WITH_CALLTYPE(STDMETHODCALLTYPE, AddRef, ULONG()); + MOCK_METHOD0_WITH_CALLTYPE(STDMETHODCALLTYPE, Release, ULONG()); + MOCK_METHOD2_WITH_CALLTYPE(STDMETHODCALLTYPE, + QueryInterface, + HRESULT(REFIID, void**)); + MOCK_METHOD0_WITH_CALLTYPE(STDMETHODCALLTYPE, Abandon, HRESULT()); + MOCK_METHOD1_WITH_CALLTYPE(STDMETHODCALLTYPE, + ActivateConfiguration, + HRESULT(DIRECTMANIPULATION_CONFIGURATION)); + MOCK_METHOD1_WITH_CALLTYPE(STDMETHODCALLTYPE, + AddConfiguration, + HRESULT(DIRECTMANIPULATION_CONFIGURATION)); + MOCK_METHOD1_WITH_CALLTYPE(STDMETHODCALLTYPE, + AddContent, + HRESULT(IDirectManipulationContent*)); + MOCK_METHOD3_WITH_CALLTYPE(STDMETHODCALLTYPE, + AddEventHandler, + HRESULT(HWND, + IDirectManipulationViewportEventHandler*, + DWORD*)); + MOCK_METHOD0_WITH_CALLTYPE(STDMETHODCALLTYPE, Disable, HRESULT()); + MOCK_METHOD0_WITH_CALLTYPE(STDMETHODCALLTYPE, Enable, HRESULT()); + MOCK_METHOD2_WITH_CALLTYPE(STDMETHODCALLTYPE, + GetPrimaryContent, + HRESULT(REFIID, void**)); + MOCK_METHOD1_WITH_CALLTYPE(STDMETHODCALLTYPE, + GetStatus, + HRESULT(DIRECTMANIPULATION_STATUS*)); + MOCK_METHOD3_WITH_CALLTYPE(STDMETHODCALLTYPE, + GetTag, + HRESULT(REFIID, void**, UINT32*)); + MOCK_METHOD1_WITH_CALLTYPE(STDMETHODCALLTYPE, + GetViewportRect, + HRESULT(RECT*)); + MOCK_METHOD0_WITH_CALLTYPE(STDMETHODCALLTYPE, ReleaseAllContacts, HRESULT()); + MOCK_METHOD1_WITH_CALLTYPE(STDMETHODCALLTYPE, + ReleaseContact, + HRESULT(UINT32)); + MOCK_METHOD1_WITH_CALLTYPE(STDMETHODCALLTYPE, + RemoveConfiguration, + HRESULT(DIRECTMANIPULATION_CONFIGURATION)); + MOCK_METHOD1_WITH_CALLTYPE(STDMETHODCALLTYPE, + RemoveContent, + HRESULT(IDirectManipulationContent*)); + MOCK_METHOD1_WITH_CALLTYPE(STDMETHODCALLTYPE, + RemoveEventHandler, + HRESULT(DWORD)); + MOCK_METHOD1_WITH_CALLTYPE(STDMETHODCALLTYPE, + SetChaining, + HRESULT(DIRECTMANIPULATION_MOTION_TYPES)); + MOCK_METHOD1_WITH_CALLTYPE(STDMETHODCALLTYPE, SetContact, HRESULT(UINT32)); + MOCK_METHOD1_WITH_CALLTYPE(STDMETHODCALLTYPE, + SetInputMode, + HRESULT(DIRECTMANIPULATION_INPUT_MODE)); + MOCK_METHOD1_WITH_CALLTYPE(STDMETHODCALLTYPE, + SetManualGesture, + HRESULT(DIRECTMANIPULATION_GESTURE_CONFIGURATION)); + MOCK_METHOD2_WITH_CALLTYPE(STDMETHODCALLTYPE, + SetTag, + HRESULT(IUnknown*, UINT32)); + MOCK_METHOD1_WITH_CALLTYPE(STDMETHODCALLTYPE, + SetUpdateMode, + HRESULT(DIRECTMANIPULATION_INPUT_MODE)); + MOCK_METHOD1_WITH_CALLTYPE(STDMETHODCALLTYPE, + SetViewportOptions, + HRESULT(DIRECTMANIPULATION_VIEWPORT_OPTIONS)); + MOCK_METHOD1_WITH_CALLTYPE(STDMETHODCALLTYPE, + SetViewportRect, + HRESULT(const RECT*)); + MOCK_METHOD2_WITH_CALLTYPE(STDMETHODCALLTYPE, + SetViewportTransform, + HRESULT(const float*, DWORD)); + MOCK_METHOD0_WITH_CALLTYPE(STDMETHODCALLTYPE, Stop, HRESULT()); + MOCK_METHOD2_WITH_CALLTYPE(STDMETHODCALLTYPE, + SyncDisplayTransform, + HRESULT(const float*, DWORD)); + MOCK_METHOD5_WITH_CALLTYPE( + STDMETHODCALLTYPE, + ZoomToRect, + HRESULT(const float, const float, const float, const float, BOOL)); +}; + +class MockIDirectManipulationContent : public IDirectManipulationContent { + public: + MockIDirectManipulationContent() {} + + // Prevent copying. + MockIDirectManipulationContent(MockIDirectManipulationContent const&) = + delete; + MockIDirectManipulationContent& operator=( + MockIDirectManipulationContent const&) = delete; + MOCK_METHOD0_WITH_CALLTYPE(STDMETHODCALLTYPE, AddRef, ULONG()); + MOCK_METHOD0_WITH_CALLTYPE(STDMETHODCALLTYPE, Release, ULONG()); + MOCK_METHOD2_WITH_CALLTYPE(STDMETHODCALLTYPE, + QueryInterface, + HRESULT(REFIID, void**)); + MOCK_METHOD1_WITH_CALLTYPE(STDMETHODCALLTYPE, GetContentRect, HRESULT(RECT*)); + MOCK_METHOD2_WITH_CALLTYPE(STDMETHODCALLTYPE, + GetContentTransform, + HRESULT(float*, DWORD)); + MOCK_METHOD2_WITH_CALLTYPE(STDMETHODCALLTYPE, + GetOutputTransform, + HRESULT(float*, DWORD)); + MOCK_METHOD3_WITH_CALLTYPE(STDMETHODCALLTYPE, + GetTag, + HRESULT(REFIID, void**, UINT32*)); + MOCK_METHOD2_WITH_CALLTYPE(STDMETHODCALLTYPE, + GetViewport, + HRESULT(REFIID, void**)); + MOCK_METHOD1_WITH_CALLTYPE(STDMETHODCALLTYPE, + SetContentRect, + HRESULT(const RECT*)); + MOCK_METHOD2_WITH_CALLTYPE(STDMETHODCALLTYPE, + SetTag, + HRESULT(IUnknown*, UINT32)); + MOCK_METHOD2_WITH_CALLTYPE(STDMETHODCALLTYPE, + SyncContentTransform, + HRESULT(const float*, DWORD)); +}; + +TEST(DirectManipulationTest, TestGesture) { + MockIDirectManipulationContent content; + MockWindowBindingHandlerDelegate delegate; + MockIDirectManipulationViewport viewport; + const float scale = 1.5; + const float pan_x = 32.0; + const float pan_y = 16.0; + const int DISPLAY_WIDTH = 800; + const int DISPLAY_HEIGHT = 600; + auto owner = std::make_unique(nullptr); + owner->SetBindingHandlerDelegate(&delegate); + auto handler = + fml::MakeRefCounted(owner.get()); + int32_t device_id = (int32_t) reinterpret_cast(handler.get()); + EXPECT_CALL(delegate, OnPointerPanZoomStart(device_id)); + handler->OnViewportStatusChanged((IDirectManipulationViewport*)&viewport, + DIRECTMANIPULATION_RUNNING, + DIRECTMANIPULATION_READY); + EXPECT_CALL(content, GetContentTransform(_, 6)) + .WillOnce(::testing::Invoke( + [scale, pan_x, pan_y](float* transform, DWORD size) { + transform[0] = scale; + transform[4] = pan_x; + transform[5] = pan_y; + return S_OK; + })); + EXPECT_CALL(delegate, + OnPointerPanZoomUpdate(device_id, pan_x, pan_y, scale, 0)); + handler->OnContentUpdated((IDirectManipulationViewport*)&viewport, + (IDirectManipulationContent*)&content); + EXPECT_CALL(delegate, OnPointerPanZoomEnd(device_id)); + EXPECT_CALL(viewport, GetViewportRect(_)) + .WillOnce(::testing::Invoke([DISPLAY_WIDTH, DISPLAY_HEIGHT](RECT* rect) { + rect->left = 0; + rect->top = 0; + rect->right = DISPLAY_WIDTH; + rect->bottom = DISPLAY_HEIGHT; + return S_OK; + })); + EXPECT_CALL(viewport, ZoomToRect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, false)) + .WillOnce(::testing::Return(S_OK)); + handler->OnViewportStatusChanged((IDirectManipulationViewport*)&viewport, + DIRECTMANIPULATION_INERTIA, + DIRECTMANIPULATION_RUNNING); + handler->OnViewportStatusChanged((IDirectManipulationViewport*)&viewport, + DIRECTMANIPULATION_READY, + DIRECTMANIPULATION_INERTIA); +} + +// Verify that scale mantissa rounding works as expected +TEST(DirectManipulationTest, TestRounding) { + MockIDirectManipulationContent content; + MockWindowBindingHandlerDelegate delegate; + MockIDirectManipulationViewport viewport; + const float scale = 1.5; + const int DISPLAY_WIDTH = 800; + const int DISPLAY_HEIGHT = 600; + auto owner = std::make_unique(nullptr); + owner->SetBindingHandlerDelegate(&delegate); + auto handler = + fml::MakeRefCounted(owner.get()); + int32_t device_id = (int32_t) reinterpret_cast(handler.get()); + EXPECT_CALL(delegate, OnPointerPanZoomStart(device_id)); + handler->OnViewportStatusChanged((IDirectManipulationViewport*)&viewport, + DIRECTMANIPULATION_RUNNING, + DIRECTMANIPULATION_READY); + EXPECT_CALL(content, GetContentTransform(_, 6)) + .WillOnce(::testing::Invoke([scale](float* transform, DWORD size) { + transform[0] = 1.5000001f; + transform[4] = 4.0; + transform[5] = 0.0; + return S_OK; + })) + .RetiresOnSaturation(); + EXPECT_CALL(delegate, + OnPointerPanZoomUpdate(device_id, 4.0, 0, 1.5000001f, 0)) + .Times(0); + EXPECT_CALL(delegate, OnPointerPanZoomUpdate(device_id, 4.0, 0, 1.5f, 0)) + .Times(1) + .RetiresOnSaturation(); + EXPECT_CALL(content, GetContentTransform(_, 6)) + .WillOnce(::testing::Invoke([scale](float* transform, DWORD size) { + transform[0] = 1.50000065f; + transform[4] = 2.0; + transform[5] = 0.0; + return S_OK; + })) + .RetiresOnSaturation(); + EXPECT_CALL(delegate, + OnPointerPanZoomUpdate(device_id, 2.0, 0, 1.50000065f, 0)) + .Times(0); + EXPECT_CALL(delegate, + OnPointerPanZoomUpdate(device_id, 2.0, 0, 1.50000047f, 0)) + .Times(1) + .RetiresOnSaturation(); + EXPECT_CALL(delegate, OnPointerPanZoomEnd(device_id)); + EXPECT_CALL(viewport, GetViewportRect(_)) + .WillOnce(::testing::Invoke([DISPLAY_WIDTH, DISPLAY_HEIGHT](RECT* rect) { + rect->left = 0; + rect->top = 0; + rect->right = DISPLAY_WIDTH; + rect->bottom = DISPLAY_HEIGHT; + return S_OK; + })); + EXPECT_CALL(viewport, ZoomToRect(0, 0, DISPLAY_WIDTH, DISPLAY_HEIGHT, false)) + .WillOnce(::testing::Return(S_OK)); + handler->OnContentUpdated((IDirectManipulationViewport*)&viewport, + (IDirectManipulationContent*)&content); + handler->OnContentUpdated((IDirectManipulationViewport*)&viewport, + (IDirectManipulationContent*)&content); + handler->OnViewportStatusChanged((IDirectManipulationViewport*)&viewport, + DIRECTMANIPULATION_INERTIA, + DIRECTMANIPULATION_RUNNING); + handler->OnViewportStatusChanged((IDirectManipulationViewport*)&viewport, + DIRECTMANIPULATION_READY, + DIRECTMANIPULATION_INERTIA); +} + +} // namespace testing +} // namespace flutter diff --git a/shell/platform/windows/flutter_window_win32.cc b/shell/platform/windows/flutter_window_win32.cc index 6943f59c9fecc..1815a6c0ddef9 100644 --- a/shell/platform/windows/flutter_window_win32.cc +++ b/shell/platform/windows/flutter_window_win32.cc @@ -75,6 +75,7 @@ FlutterWindowWin32::~FlutterWindowWin32() {} void FlutterWindowWin32::SetView(WindowBindingHandlerDelegate* window) { binding_handler_delegate_ = window; + direct_manipulation_owner_->SetBindingHandlerDelegate(window); } WindowsRenderTarget FlutterWindowWin32::GetRenderTarget() { @@ -265,4 +266,11 @@ gfx::NativeViewAccessible FlutterWindowWin32::GetNativeViewAccessible() { return binding_handler_delegate_->GetNativeViewAccessible(); } +PointerLocation FlutterWindowWin32::GetPrimaryPointerLocation() { + POINT point; + GetCursorPos(&point); + ScreenToClient(GetWindowHandle(), &point); + return {(size_t)point.x, (size_t)point.y}; +} + } // namespace flutter diff --git a/shell/platform/windows/flutter_window_win32.h b/shell/platform/windows/flutter_window_win32.h index dff83aac789ef..6713bae826a13 100644 --- a/shell/platform/windows/flutter_window_win32.h +++ b/shell/platform/windows/flutter_window_win32.h @@ -136,6 +136,9 @@ class FlutterWindowWin32 : public WindowWin32, public WindowBindingHandler { size_t row_bytes, size_t height) override; + // |FlutterWindowBindingHandler| + PointerLocation GetPrimaryPointerLocation() override; + private: // A pointer to a FlutterWindowsView that can be used to update engine // windowing and input state. diff --git a/shell/platform/windows/flutter_window_win32_unittests.cc b/shell/platform/windows/flutter_window_win32_unittests.cc index 62b90d01255b5..063f51a77e96d 100644 --- a/shell/platform/windows/flutter_window_win32_unittests.cc +++ b/shell/platform/windows/flutter_window_win32_unittests.cc @@ -11,6 +11,7 @@ #include "flutter/shell/platform/windows/testing/engine_modifier.h" #include "flutter/shell/platform/windows/testing/flutter_window_win32_test.h" #include "flutter/shell/platform/windows/testing/mock_window_binding_handler.h" +#include "flutter/shell/platform/windows/testing/mock_window_binding_handler_delegate.h" #include "flutter/shell/platform/windows/testing/test_keyboard.h" #include "flutter/shell/platform/windows/text_input_plugin.h" #include "flutter/shell/platform/windows/text_input_plugin_delegate.h" @@ -139,53 +140,6 @@ class MockFlutterWindowWin32 : public FlutterWindowWin32 { } }; -class MockWindowBindingHandlerDelegate : public WindowBindingHandlerDelegate { - public: - MockWindowBindingHandlerDelegate() {} - - // Prevent copying. - MockWindowBindingHandlerDelegate(MockWindowBindingHandlerDelegate const&) = - delete; - MockWindowBindingHandlerDelegate& operator=( - MockWindowBindingHandlerDelegate const&) = delete; - - MOCK_METHOD2(OnWindowSizeChanged, void(size_t, size_t)); - MOCK_METHOD4(OnPointerMove, - void(double, double, FlutterPointerDeviceKind, int32_t)); - MOCK_METHOD5(OnPointerDown, - void(double, - double, - FlutterPointerDeviceKind, - int32_t, - FlutterPointerMouseButtons)); - MOCK_METHOD5(OnPointerUp, - void(double, - double, - FlutterPointerDeviceKind, - int32_t, - FlutterPointerMouseButtons)); - MOCK_METHOD4(OnPointerLeave, - void(double, double, FlutterPointerDeviceKind, int32_t)); - MOCK_METHOD1(OnText, void(const std::u16string&)); - MOCK_METHOD7(OnKey, - void(int, int, int, char32_t, bool, bool, KeyEventCallback)); - MOCK_METHOD0(OnComposeBegin, void()); - MOCK_METHOD0(OnComposeCommit, void()); - MOCK_METHOD0(OnComposeEnd, void()); - MOCK_METHOD2(OnComposeChange, void(const std::u16string&, int)); - MOCK_METHOD1(OnUpdateSemanticsEnabled, void(bool)); - MOCK_METHOD0(GetNativeViewAccessible, gfx::NativeViewAccessible()); - MOCK_METHOD7(OnScroll, - void(double, - double, - double, - double, - int, - FlutterPointerDeviceKind, - int32_t)); - MOCK_METHOD0(OnPlatformBrightnessChanged, void()); -}; - // A FlutterWindowsView that overrides the RegisterKeyboardHandlers function // to register the keyboard hook handlers that can be spied upon. class TestFlutterWindowsView : public FlutterWindowsView { diff --git a/shell/platform/windows/flutter_windows_view.cc b/shell/platform/windows/flutter_windows_view.cc index baf9389e541f5..5d645eea143d0 100644 --- a/shell/platform/windows/flutter_windows_view.cc +++ b/shell/platform/windows/flutter_windows_view.cc @@ -203,6 +203,23 @@ void FlutterWindowsView::OnPointerLeave(double x, SendPointerLeave(x, y, GetOrCreatePointerState(device_kind, device_id)); } +void FlutterWindowsView::OnPointerPanZoomStart(int32_t device_id) { + PointerLocation point = binding_handler_->GetPrimaryPointerLocation(); + SendPointerPanZoomStart(device_id, point.x, point.y); +} + +void FlutterWindowsView::OnPointerPanZoomUpdate(int32_t device_id, + double pan_x, + double pan_y, + double scale, + double rotation) { + SendPointerPanZoomUpdate(device_id, pan_x, pan_y, scale, rotation); +} + +void FlutterWindowsView::OnPointerPanZoomEnd(int32_t device_id) { + SendPointerPanZoomEnd(device_id); +} + void FlutterWindowsView::OnText(const std::u16string& text) { SendText(text); } @@ -381,6 +398,48 @@ void FlutterWindowsView::SendPointerLeave(double x, SendPointerEventWithData(event, state); } +void FlutterWindowsView::SendPointerPanZoomStart(int32_t device_id, + double x, + double y) { + auto state = + GetOrCreatePointerState(kFlutterPointerDeviceKindTrackpad, device_id); + state->pan_zoom_start_x = x; + state->pan_zoom_start_y = y; + FlutterPointerEvent event = {}; + event.x = x; + event.y = y; + event.phase = FlutterPointerPhase::kPanZoomStart; + SendPointerEventWithData(event, state); +} + +void FlutterWindowsView::SendPointerPanZoomUpdate(int32_t device_id, + double pan_x, + double pan_y, + double scale, + double rotation) { + auto state = + GetOrCreatePointerState(kFlutterPointerDeviceKindTrackpad, device_id); + FlutterPointerEvent event = {}; + event.x = state->pan_zoom_start_x; + event.y = state->pan_zoom_start_y; + event.pan_x = pan_x; + event.pan_y = pan_y; + event.scale = scale; + event.rotation = rotation; + event.phase = FlutterPointerPhase::kPanZoomUpdate; + SendPointerEventWithData(event, state); +} + +void FlutterWindowsView::SendPointerPanZoomEnd(int32_t device_id) { + auto state = + GetOrCreatePointerState(kFlutterPointerDeviceKindTrackpad, device_id); + FlutterPointerEvent event = {}; + event.x = state->pan_zoom_start_x; + event.y = state->pan_zoom_start_y; + event.phase = FlutterPointerPhase::kPanZoomEnd; + SendPointerEventWithData(event, state); +} + void FlutterWindowsView::SendText(const std::u16string& text) { text_input_plugin_->TextHook(text); } diff --git a/shell/platform/windows/flutter_windows_view.h b/shell/platform/windows/flutter_windows_view.h index 33da447b7d267..6366c16f3d1c6 100644 --- a/shell/platform/windows/flutter_windows_view.h +++ b/shell/platform/windows/flutter_windows_view.h @@ -125,6 +125,19 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate, FlutterPointerDeviceKind device_kind, int32_t device_id = 0) override; + // |WindowBindingHandlerDelegate| + virtual void OnPointerPanZoomStart(int32_t device_id) override; + + // |WindowBindingHandlerDelegate| + virtual void OnPointerPanZoomUpdate(int32_t device_id, + double pan_x, + double pan_y, + double scale, + double rotation) override; + + // |WindowBindingHandlerDelegate| + virtual void OnPointerPanZoomEnd(int32_t device_id) override; + // |WindowBindingHandlerDelegate| void OnText(const std::u16string&) override; @@ -207,6 +220,12 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate, // The currently pressed buttons, as represented in FlutterPointerEvent. uint64_t buttons = 0; + + // The x position where the last pan/zoom started. + double pan_zoom_start_x = 0; + + // The y position where the last pan/zoom started. + double pan_zoom_start_y = 0; }; // States a resize event can be in. @@ -247,6 +266,16 @@ class FlutterWindowsView : public WindowBindingHandlerDelegate, // event is called. void SendPointerLeave(double x, double y, PointerState* state); + void SendPointerPanZoomStart(int32_t device_id, double x, double y); + + void SendPointerPanZoomUpdate(int32_t device_id, + double pan_x, + double pan_y, + double scale, + double rotation); + + void SendPointerPanZoomEnd(int32_t device_id); + // Reports a keyboard character to Flutter engine. void SendText(const std::u16string&); diff --git a/shell/platform/windows/testing/mock_window_binding_handler.h b/shell/platform/windows/testing/mock_window_binding_handler.h index a5395c96cca02..a9eac85ba4320 100644 --- a/shell/platform/windows/testing/mock_window_binding_handler.h +++ b/shell/platform/windows/testing/mock_window_binding_handler.h @@ -35,6 +35,7 @@ class MockWindowBindingHandler : public WindowBindingHandler { MOCK_METHOD0(OnResetImeComposing, void()); MOCK_METHOD3(OnBitmapSurfaceUpdated, bool(const void* allocation, size_t row_bytes, size_t height)); + MOCK_METHOD0(GetPrimaryPointerLocation, PointerLocation()); }; } // namespace testing diff --git a/shell/platform/windows/testing/mock_window_binding_handler_delegate.h b/shell/platform/windows/testing/mock_window_binding_handler_delegate.h new file mode 100644 index 0000000000000..e4f962bad0375 --- /dev/null +++ b/shell/platform/windows/testing/mock_window_binding_handler_delegate.h @@ -0,0 +1,68 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_MOCK_WINDOW_BINDING_HANDLER_DELEGATE_H_ +#define FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_MOCK_WINDOW_BINDING_HANDLER_DELEGATE_H_ + +#include "flutter/shell/platform/windows/window_binding_handler_delegate.h" +#include "gmock/gmock.h" + +namespace flutter { +namespace testing { + +class MockWindowBindingHandlerDelegate : public WindowBindingHandlerDelegate { + public: + MockWindowBindingHandlerDelegate() {} + + // Prevent copying. + MockWindowBindingHandlerDelegate(MockWindowBindingHandlerDelegate const&) = + delete; + MockWindowBindingHandlerDelegate& operator=( + MockWindowBindingHandlerDelegate const&) = delete; + + MOCK_METHOD2(OnWindowSizeChanged, void(size_t, size_t)); + MOCK_METHOD4(OnPointerMove, + void(double, double, FlutterPointerDeviceKind, int32_t)); + MOCK_METHOD5(OnPointerDown, + void(double, + double, + FlutterPointerDeviceKind, + int32_t, + FlutterPointerMouseButtons)); + MOCK_METHOD5(OnPointerUp, + void(double, + double, + FlutterPointerDeviceKind, + int32_t, + FlutterPointerMouseButtons)); + MOCK_METHOD4(OnPointerLeave, + void(double, double, FlutterPointerDeviceKind, int32_t)); + MOCK_METHOD1(OnPointerPanZoomStart, void(int32_t)); + MOCK_METHOD5(OnPointerPanZoomUpdate, + void(int32_t, double, double, double, double)); + MOCK_METHOD1(OnPointerPanZoomEnd, void(int32_t)); + MOCK_METHOD1(OnText, void(const std::u16string&)); + MOCK_METHOD7(OnKey, + void(int, int, int, char32_t, bool, bool, KeyEventCallback)); + MOCK_METHOD0(OnComposeBegin, void()); + MOCK_METHOD0(OnComposeCommit, void()); + MOCK_METHOD0(OnComposeEnd, void()); + MOCK_METHOD2(OnComposeChange, void(const std::u16string&, int)); + MOCK_METHOD1(OnUpdateSemanticsEnabled, void(bool)); + MOCK_METHOD0(GetNativeViewAccessible, gfx::NativeViewAccessible()); + MOCK_METHOD7(OnScroll, + void(double, + double, + double, + double, + int, + FlutterPointerDeviceKind, + int32_t)); + MOCK_METHOD0(OnPlatformBrightnessChanged, void()); +}; + +} // namespace testing +} // namespace flutter + +#endif // FLUTTER_SHELL_PLATFORM_WINDOWS_TESTING_MOCK_WINDOW_BINDING_HANDLER_DELEGATE_H_ diff --git a/shell/platform/windows/window_binding_handler.h b/shell/platform/windows/window_binding_handler.h index 40788092ab98d..594ef355a988d 100644 --- a/shell/platform/windows/window_binding_handler.h +++ b/shell/platform/windows/window_binding_handler.h @@ -24,6 +24,13 @@ struct PhysicalWindowBounds { size_t height; }; +// Structure containing the position of a mouse pointer in the coordinate system +// specified by the function where it's used. +struct PointerLocation { + size_t x; + size_t y; +}; + // Type representing an underlying platform window. using PlatformWindow = HWND; @@ -78,6 +85,10 @@ class WindowBindingHandler { // Invoked when the app ends IME composing, such when the active text input // client is cleared. virtual void OnResetImeComposing() = 0; + + // Returns the last known position of the primary pointer in window + // coordinates. + virtual PointerLocation GetPrimaryPointerLocation() = 0; }; } // namespace flutter diff --git a/shell/platform/windows/window_binding_handler_delegate.h b/shell/platform/windows/window_binding_handler_delegate.h index b69d8a6369c37..03adebbf91bb9 100644 --- a/shell/platform/windows/window_binding_handler_delegate.h +++ b/shell/platform/windows/window_binding_handler_delegate.h @@ -52,6 +52,22 @@ class WindowBindingHandlerDelegate { FlutterPointerDeviceKind device_kind, int32_t device_id) = 0; + // Notifies delegate that a pan/zoom gesture has started. + // Typically called by DirectManipulationEventHandler + virtual void OnPointerPanZoomStart(int32_t device_id) = 0; + + // Notifies delegate that a pan/zoom gesture has updated. + // Typically called by DirectManipulationEventHandler + virtual void OnPointerPanZoomUpdate(int32_t device_id, + double pan_x, + double pan_y, + double scale, + double rotation) = 0; + + // Notifies delegate that a pan/zoom gesture has ended. + // Typically called by DirectManipulationEventHandler + virtual void OnPointerPanZoomEnd(int32_t device_id) = 0; + // Notifies delegate that backing window has received text. // Typically called by currently configured WindowBindingHandler virtual void OnText(const std::u16string&) = 0; diff --git a/shell/platform/windows/window_win32.cc b/shell/platform/windows/window_win32.cc index 0240137f9bbde..8837c87e37af9 100644 --- a/shell/platform/windows/window_win32.cc +++ b/shell/platform/windows/window_win32.cc @@ -95,6 +95,22 @@ void WindowWin32::InitializeChild(const char* title, OutputDebugString(message); LocalFree(message); } + DEVMODE dmi; + ZeroMemory(&dmi, sizeof(dmi)); + dmi.dmSize = sizeof(dmi); + if (EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &dmi)) { + directManipulationPollingRate_ = dmi.dmDisplayFrequency; + } else { + OutputDebugString( + L"Failed to get framerate, will use default of 60 Hz for gesture " + L"polling."); + } + SetUserObjectInformationA(GetCurrentProcess(), + UOI_TIMERPROC_EXCEPTION_SUPPRESSION, FALSE, 1); + SetTimer(result, kDirectManipulationTimer, + 1000 / directManipulationPollingRate_, nullptr); + direct_manipulation_owner_ = std::make_unique(this); + direct_manipulation_owner_->Init(width, height); } std::wstring WindowWin32::NarrowToWide(const char* source) { @@ -444,6 +460,25 @@ WindowWin32::HandleMessage(UINT const message, } break; } + case WM_TIMER: + if (wparam == kDirectManipulationTimer) { + direct_manipulation_owner_->Update(); + SetTimer(window_handle_, kDirectManipulationTimer, + 1000 / directManipulationPollingRate_, nullptr); + return 0; + } + break; + case DM_POINTERHITTEST: { + if (direct_manipulation_owner_) { + UINT contactId = GET_POINTERID_WPARAM(wparam); + POINTER_INPUT_TYPE pointerType; + if (GetPointerType(contactId, &pointerType) && + pointerType == PT_TOUCHPAD) { + direct_manipulation_owner_->SetContact(contactId); + } + } + break; + } case WM_INPUTLANGCHANGE: // TODO(cbracken): pass this to TextInputManager to aid with // language-specific issues. @@ -531,6 +566,9 @@ void WindowWin32::Destroy() { void WindowWin32::HandleResize(UINT width, UINT height) { current_width_ = width; current_height_ = height; + if (direct_manipulation_owner_) { + direct_manipulation_owner_->ResizeViewport(width, height); + } OnResize(width, height); } diff --git a/shell/platform/windows/window_win32.h b/shell/platform/windows/window_win32.h index ee9168798741f..cc142e699468e 100644 --- a/shell/platform/windows/window_win32.h +++ b/shell/platform/windows/window_win32.h @@ -14,6 +14,7 @@ #include #include "flutter/shell/platform/embedder/embedder.h" +#include "flutter/shell/platform/windows/direct_manipulation.h" #include "flutter/shell/platform/windows/keyboard_manager_win32.h" #include "flutter/shell/platform/windows/sequential_id_generator.h" #include "flutter/shell/platform/windows/text_input_manager_win32.h" @@ -211,6 +212,10 @@ class WindowWin32 : public KeyboardManagerWin32::WindowDelegate { // Returns the root view accessibility node, or nullptr if none. virtual gfx::NativeViewAccessible GetNativeViewAccessible() = 0; + // Handles running DirectManipulation on the window to receive trackpad + // gestures. + std::unique_ptr direct_manipulation_owner_; + private: // Release OS resources associated with window. void Destroy(); @@ -260,6 +265,12 @@ class WindowWin32 : public KeyboardManagerWin32::WindowDelegate { // Generates touch point IDs for touch events. SequentialIdGenerator touch_id_generator_; + + // Timer identifier for DirectManipulation gesture polling. + const static int kDirectManipulationTimer = 1; + + // Frequency (Hz) to poll for DirectManipulation updates. + int directManipulationPollingRate_ = 60; }; } // namespace flutter