diff --git a/CMakeLists.txt b/CMakeLists.txt index 09d3900..e0d6636 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -126,6 +126,8 @@ PRIVATE ui/platform/win/ui_window_title_win.h ui/platform/win/ui_window_win.cpp ui/platform/win/ui_window_win.h + ui/platform/win/ui_windows_direct_manipulation.cpp + ui/platform/win/ui_windows_direct_manipulation.h ui/platform/win/ui_utility_win.cpp ui/platform/win/ui_utility_win.h ui/platform/ui_platform_window_title.cpp @@ -294,6 +296,10 @@ if (NOT DESKTOP_APP_USE_PACKAGED_FONTS) nice_target_sources(lib_ui ${src_loc} PRIVATE fonts/fonts.qrc) endif() +if (WIN32) + nuget_add_winrt(lib_ui) +endif() + if (DESKTOP_APP_DISABLE_WAYLAND_INTEGRATION) remove_target_sources(lib_ui ${src_loc} ui/platform/linux/ui_linux_wayland_integration.cpp) elseif(LINUX) diff --git a/ui/platform/win/ui_window_win.cpp b/ui/platform/win/ui_window_win.cpp index 53c0068..c691ef2 100644 --- a/ui/platform/win/ui_window_win.cpp +++ b/ui/platform/win/ui_window_win.cpp @@ -8,6 +8,7 @@ #include "ui/inactive_press.h" #include "ui/platform/win/ui_window_title_win.h" +#include "ui/platform/win/ui_windows_direct_manipulation.h" #include "ui/platform/ui_platform_utility.h" #include "ui/widgets/rp_window.h" #include "base/platform/win/base_windows_safe_library.h" @@ -19,6 +20,7 @@ #include "styles/style_widgets.h" #include +#include #include #include #include @@ -30,6 +32,8 @@ Q_DECLARE_METATYPE(QMargins); +bool qt_sendSpontaneousEvent(QObject *receiver, QEvent *event); + namespace Ui::Platform { namespace { @@ -402,6 +406,46 @@ void WindowHelper::init() { handleStateChanged); initialShadowUpdate(); + + auto dm = std::make_unique(window()); + if (dm->valid()) { + _directManipulation = std::move(dm); + _directManipulation->events( + ) | rpl::start_with_next([=](const DirectManipulationEvent &event) { + handleDirectManipulationEvent(event); + }, window()->lifetime()); + } +} + +void WindowHelper::handleDirectManipulationEvent( + const DirectManipulationEvent &event) { + using Type = DirectManipulationEventType; + const auto send = [&](Qt::ScrollPhase phase) { + if (const auto windowHandle = window()->windowHandle()) { + const auto global = QCursor::pos(); + const auto local = windowHandle->mapFromGlobal(global); + auto e = QWheelEvent( + QPointF(local), + QPointF(global), + event.delta, + event.delta, + QGuiApplication::mouseButtons(), + QGuiApplication::keyboardModifiers(), + phase, + false, + Qt::MouseEventSynthesizedByApplication); + e.setTimestamp(crl::now()); + qt_sendSpontaneousEvent(windowHandle, &e); + } + }; + switch (event.type) { + case Type::ScrollStart: send(Qt::ScrollBegin); break; + case Type::Scroll: send(Qt::ScrollUpdate); break; + case Type::FlingStart: + case Type::Fling: send(Qt::ScrollMomentum); break; + case Type::ScrollStop: send(Qt::ScrollEnd); break; + case Type::FlingStop: send(Qt::ScrollEnd); break; + } } bool WindowHelper::handleNativeEvent( @@ -622,6 +666,13 @@ bool WindowHelper::handleNativeEvent( }); } return false; + case DM_POINTERHITTEST: { + if (_directManipulation) { + _directManipulation->handlePointerHitTest(wParam); + return true; + } + } return false; + } return false; } diff --git a/ui/platform/win/ui_window_win.h b/ui/platform/win/ui_window_win.h index d29fc11..490d87d 100644 --- a/ui/platform/win/ui_window_win.h +++ b/ui/platform/win/ui_window_win.h @@ -17,6 +17,8 @@ namespace Platform { class TitleWidget; struct HitTestRequest; enum class HitTestResult; +class DirectManipulation; +struct DirectManipulationEvent; class WindowHelper final : public BasicWindowHelper { public: @@ -55,6 +57,8 @@ private: void initialShadowUpdate(); void updateCornersRounding(); void fixMaximizedWindow(); + void handleDirectManipulationEvent( + const DirectManipulationEvent &event); [[nodiscard]] bool handleNativeEvent( UINT msg, WPARAM wParam, @@ -75,6 +79,7 @@ private: const HWND _handle = nullptr; const not_null _title; const not_null _body; + std::unique_ptr _directManipulation; rpl::event_stream> _hitTestRequests; rpl::event_stream _systemButtonOver; rpl::event_stream _systemButtonDown; diff --git a/ui/platform/win/ui_windows_direct_manipulation.cpp b/ui/platform/win/ui_windows_direct_manipulation.cpp new file mode 100644 index 0000000..8f24c93 --- /dev/null +++ b/ui/platform/win/ui_windows_direct_manipulation.cpp @@ -0,0 +1,439 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#include "ui/platform/win/ui_windows_direct_manipulation.h" + +#include "base/integration.h" +#include "base/platform/base_platform_info.h" +#include "ui/rp_widget.h" +#include "ui/platform/win/ui_window_win.h" + +namespace Ui::Platform { + +class DirectManipulation::Handler + : public IDirectManipulationViewportEventHandler + , public IDirectManipulationInteractionEventHandler { +public: + Handler(); + + void setViewportSize(QSize size); + + HRESULT STDMETHODCALLTYPE QueryInterface( + REFIID iid, + void **ppv) override; + + ULONG STDMETHODCALLTYPE AddRef() override { + return ++_ref; + } + ULONG STDMETHODCALLTYPE Release() override { + if (--_ref == 0) { + delete this; + return 0; + } + return _ref; + } + + [[nodiscard]] rpl::producer interacting() const { + return _interacting.value(); + } + [[nodiscard]] rpl::producer events() const { + return _events.events(); + } + +private: + ~Handler(); + + enum class State { + None, + Scroll, + Fling, + Pinch, + }; + + void transitionToState(State state); + + HRESULT STDMETHODCALLTYPE OnViewportStatusChanged( + _In_ IDirectManipulationViewport* viewport, + _In_ DIRECTMANIPULATION_STATUS current, + _In_ DIRECTMANIPULATION_STATUS previous) override; + + HRESULT STDMETHODCALLTYPE OnViewportUpdated( + _In_ IDirectManipulationViewport *viewport) override; + + HRESULT STDMETHODCALLTYPE OnContentUpdated( + _In_ IDirectManipulationViewport *viewport, + _In_ IDirectManipulationContent *content) override; + + HRESULT STDMETHODCALLTYPE OnInteraction( + _In_ IDirectManipulationViewport2 *viewport, + _In_ DIRECTMANIPULATION_INTERACTION_TYPE interaction) override; + + State _state = State::None; + int _width = 0; + int _height = 0; + rpl::variable _interacting = false; + rpl::event_stream _events; + float _scale = 1.0f; + float _xOffset = 0.f; + float _yOffset = 0.f; + bool _pendingScrollBegin = false; + + std::atomic _ref = 1; + +}; + +DirectManipulation::Handler::Handler() { +} + +DirectManipulation::Handler::~Handler() { +} + +void DirectManipulation::Handler::setViewportSize(QSize size) { + _width = size.width(); + _height = size.height(); +} + +STDMETHODIMP DirectManipulation::Handler::QueryInterface( + REFIID iid, + void **ppv) { + const IID IID_IDirectManipulationViewportEventHandler = + __uuidof(IDirectManipulationViewportEventHandler); + const IID IID_IDirectManipulationInteractionEventHandler = + __uuidof(IDirectManipulationInteractionEventHandler); + + if ((IID_IUnknown == iid) || + (IID_IDirectManipulationViewportEventHandler == iid)) { + *ppv = static_cast(this); + AddRef(); + return S_OK; + } + if (IID_IDirectManipulationInteractionEventHandler == iid) { + *ppv = static_cast( + this); + AddRef(); + return S_OK; + } + + return E_NOINTERFACE; +} + +void DirectManipulation::Handler::transitionToState(State state) { + if (_state == state) { + return; + } + + const auto was = _state; + _state = state; + + switch (was) { + case State::Scroll: { + if (state != State::Fling) { + _events.fire({ .type = Event::Type::ScrollStop }); + } + } break; + case State::Fling: { + _events.fire({ .type = Event::Type::FlingStop }); + } break; + case State::Pinch: { + // _events.fire({ .type = Event::Type::PinchStop }); + } break; + } + + switch (state) { + case State::Scroll: { + _pendingScrollBegin = true; + } break; + case State::Fling: { + Assert(was == State::Scroll); + _events.fire({ .type = Event::Type::FlingStart }); + } break; + case State::Pinch: { + //_events.fire({ .type = Event::Type::PinchStart }); + } break; + } +} + +HRESULT DirectManipulation::Handler::OnViewportStatusChanged( + IDirectManipulationViewport *viewport, + DIRECTMANIPULATION_STATUS current, + DIRECTMANIPULATION_STATUS previous) { + Expects(viewport != nullptr); + + if (current == previous) { + return S_OK; + } else if (current == DIRECTMANIPULATION_INERTIA) { + if (previous != DIRECTMANIPULATION_RUNNING + || _state != State::Scroll) { + return S_OK; + } + transitionToState(State::Fling); + } + + if (current == DIRECTMANIPULATION_RUNNING) { + if (previous == DIRECTMANIPULATION_INERTIA) { + transitionToState(State::None); + } + } + + if (current != DIRECTMANIPULATION_READY) { + return S_OK; + } + + if (_scale != 1.0f || _xOffset != 0. || _yOffset != 0.) { + const auto hr = viewport->ZoomToRect(0, 0, _width, _height, FALSE); + if (!SUCCEEDED(hr)) { + return hr; + } + } + + _scale = 1.0f; + _xOffset = 0.0f; + _yOffset = 0.0f; + + transitionToState(State::None); + + return S_OK; +} + +HRESULT DirectManipulation::Handler::OnViewportUpdated( + IDirectManipulationViewport *viewport) { + return S_OK; +} + +HRESULT DirectManipulation::Handler::OnContentUpdated( + IDirectManipulationViewport *viewport, + IDirectManipulationContent *content) { + Expects(viewport != nullptr); + Expects(content != nullptr); + + float xform[6]; + const auto hr = content->GetContentTransform(xform, ARRAYSIZE(xform)); + if (!SUCCEEDED(hr)) { + return hr; + } + + float scale = xform[0]; + float xOffset = xform[4]; + float yOffset = xform[5]; + + if (scale == 0.0f) { + return hr; + } else if (qFuzzyCompare(scale, _scale) + && xOffset == _xOffset + && yOffset == _yOffset) { + return hr; + } + if (qFuzzyCompare(scale, 1.0f)) { + if (_state == State::None) { + transitionToState(State::Scroll); + } + } else { + transitionToState(State::Pinch); + } + + auto getIntDeltaPart = [](float &was, float now) { + if (was < now) { + const auto result = std::floor(now - was); + was += result; + return int(result); + } else { + const auto result = std::floor(was - now); + was -= result; + return -int(result); + } + }; + const auto d = QPoint( + getIntDeltaPart(_xOffset, xOffset), + getIntDeltaPart(_yOffset, yOffset)); + if ((_state == State::Scroll || _state == State::Fling) && d.isNull()) { + return S_OK; + } + if (_state == State::Scroll) { + if (_pendingScrollBegin) { + _events.fire({ .type = Event::Type::ScrollStart, .delta = d }); + _pendingScrollBegin = false; + } else { + _events.fire({ .type = Event::Type::Scroll, .delta = d }); + } + } else if (_state == State::Fling) { + _events.fire({ .type = Event::Type::Fling, .delta = d }); + } else { + //_events.fire({ .type = Event::Type::Pinch, .delta = ... }); + } + _scale = scale; + + return hr; +} + +HRESULT DirectManipulation::Handler::OnInteraction( + IDirectManipulationViewport2 *viewport, + DIRECTMANIPULATION_INTERACTION_TYPE interaction) { + if (interaction == DIRECTMANIPULATION_INTERACTION_BEGIN) { + _interacting = true; + } else if (interaction == DIRECTMANIPULATION_INTERACTION_END) { + _interacting = false; + } + return S_OK; +} + +DirectManipulation::DirectManipulation(not_null widget) +: _handle(GetWindowHandle(widget)) +, _interacting([=] { _updateManager->Update(nullptr); }) { + if (!init(widget)) { + destroy(); + } +} + +DirectManipulation::~DirectManipulation() { + destroy(); +} + +bool DirectManipulation::valid() const { + return _manager != nullptr; +} + +bool DirectManipulation::init(not_null widget) { + if (!_handle || !::Platform::IsWindows10OrGreater()) { + return false; + } + _manager = base::WinRT::TryCreateInstance( + CLSID_DirectManipulationManager); + if (!_manager) { + return false; + } + + auto hr = S_OK; + hr = _manager->GetUpdateManager(IID_PPV_ARGS(_updateManager.put())); + if (!SUCCEEDED(hr) || !_updateManager) { + return false; + } + + hr = _manager->CreateViewport( + nullptr, + _handle, + IID_PPV_ARGS(_viewport.put())); + if (!SUCCEEDED(hr) || !_viewport) { + return false; + } + + const auto configuration = DIRECTMANIPULATION_CONFIGURATION_INTERACTION + | DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_X + | DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_Y + | DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_INERTIA + | DIRECTMANIPULATION_CONFIGURATION_RAILS_X + | DIRECTMANIPULATION_CONFIGURATION_RAILS_Y; + + hr = _viewport->ActivateConfiguration(configuration); + if (!SUCCEEDED(hr)) { + return false; + } + hr = _viewport->SetViewportOptions( + DIRECTMANIPULATION_VIEWPORT_OPTIONS_MANUALUPDATE); + if (!SUCCEEDED(hr)) { + return false; + } + + _handler.attach(new Handler()); + _handler->interacting( + ) | rpl::start_with_next([=](bool interacting) { + base::Integration::Instance().enterFromEventLoop([&] { + if (interacting) { + _interacting.start(); + } else { + _interacting.stop(); + } + }); + }, _lifetime); + + widget->sizeValue() | rpl::start_with_next([=](QSize size) { + const auto r = QRect(QPoint(), size * widget->devicePixelRatio()); + _handler->setViewportSize(r.size()); + const auto rect = RECT{ r.left(), r.top(), r.right(), r.bottom() }; + _viewport->SetViewportRect(&rect); + }, _lifetime); + + hr = _viewport->AddEventHandler(_handle, _handler.get(), &_cookie); + if (!SUCCEEDED(hr)) { + return false; + } + + RECT rect = { 0, 0, 1024, 1024 }; + hr = _viewport->SetViewportRect(&rect); + if (!SUCCEEDED(hr)) { + return false; + } + + hr = _manager->Activate(_handle); + if (!SUCCEEDED(hr)) { + return false; + } + + hr = _viewport->Enable(); + if (!SUCCEEDED(hr)) { + return false; + } + + hr = _updateManager->Update(nullptr); + if (!SUCCEEDED(hr)) { + return false; + } + + return true; +} + +auto DirectManipulation::events() const -> rpl::producer { + if (!_handler) { + return rpl::never(); + } + return [events = _handler->events()](auto consumer) mutable { + auto result = rpl::lifetime(); + std::move( + events + ) | rpl::start_with_next([=](Event &&event) { + base::Integration::Instance().enterFromEventLoop([&] { + consumer.put_next(std::move(event)); + }); + }, result); + return result; + }; +} + +void DirectManipulation::handlePointerHitTest(WPARAM wParam) { + const auto id = UINT32(GET_POINTERID_WPARAM(wParam)); + auto type = POINTER_INPUT_TYPE(); + if (::GetPointerType(id, &type) && type == PT_TOUCHPAD) { + _viewport->SetContact(id); + } +} + +void DirectManipulation::destroy() { + _interacting.stop(); + + if (_handler) { + _handler = nullptr; + } + + if (_viewport) { + _viewport->Stop(); + if (_cookie) { + _viewport->RemoveEventHandler(_cookie); + _cookie = 0; + } + _viewport->Abandon(); + _viewport = nullptr; + } + + if (_updateManager) { + _updateManager = nullptr; + } + + if (_manager) { + _manager->Deactivate(_handle); + _manager = nullptr; + } +} + +} // namespace Ui::Platform diff --git a/ui/platform/win/ui_windows_direct_manipulation.h b/ui/platform/win/ui_windows_direct_manipulation.h new file mode 100644 index 0000000..22c34a0 --- /dev/null +++ b/ui/platform/win/ui_windows_direct_manipulation.h @@ -0,0 +1,71 @@ +// This file is part of Desktop App Toolkit, +// a set of libraries for developing nice desktop applications. +// +// For license and copyright information please follow this link: +// https://github.com/desktop-app/legal/blob/master/LEGAL +// +#pragma once + +#include "base/platform/win/base_windows_winrt.h" +#include "base/platform/win/base_windows_h.h" +#include "ui/effects/animations.h" + +#include + +#include + +namespace Ui { +class RpWidget; +} // namespace Ui + +namespace Ui::Platform { + +enum class DirectManipulationEventType { + ScrollStart, + Scroll, + ScrollStop, + FlingStart, + Fling, + FlingStop, +}; + +struct DirectManipulationEvent { + using Type = DirectManipulationEventType; + Type type = Type (); + QPoint delta; +}; + +class DirectManipulation final { +public: + explicit DirectManipulation(not_null widget); + ~DirectManipulation(); + + [[nodiscard]] bool valid() const; + + void handlePointerHitTest(WPARAM wParam); + + using Event = DirectManipulationEvent; + [[nodiscard]] rpl::producer events() const; + +private: + class Handler; + + bool init(not_null widget); + void destroy(); + + HWND _handle = nullptr; + winrt::com_ptr _manager; + winrt::com_ptr _updateManager; + winrt::com_ptr _viewport; + winrt::com_ptr _handler; + DWORD _cookie = 0; + //bool has_animation_observer_ = false; + + Ui::Animations::Basic _interacting; + rpl::event_stream _events; + rpl::lifetime _lifetime; + +}; + + +} // namespace Ui::Platform