lib_ui/ui/platform/win/ui_windows_direct_manipulation.cpp

563 lines
13 KiB
C++

// 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 "base/platform/win/base_windows_safe_library.h"
#include "ui/platform/win/ui_window_win.h"
#include "ui/ui_utility.h" // kPixelToAngleDelta
#include "ui/rp_widget.h"
#include <qpa/qwindowsysteminterface.h>
#include <qpa/qwindowsysteminterface_p.h>
namespace Ui::Platform {
namespace {
[[nodiscard]] Qt::KeyboardModifiers LookupModifiers() {
const auto check = [](int key) {
return (GetKeyState(key) & 0x8000) != 0;
};
auto result = Qt::KeyboardModifiers();
if (check(VK_SHIFT)) {
result |= Qt::ShiftModifier;
}
// NB AltGr key (i.e., VK_RMENU on some keyboard layout) is not handled.
if (check(VK_RMENU) || check(VK_MENU)) {
result |= Qt::AltModifier;
}
if (check(VK_CONTROL)) {
result |= Qt::ControlModifier;
}
if (check(VK_LWIN) || check(VK_RWIN)) {
result |= Qt::MetaModifier;
}
return result;
}
UINT(__stdcall *GetDpiForWindow)(_In_ HWND hwnd);
BOOL(__stdcall *GetPointerType)(
UINT32 pointerId,
POINTER_INPUT_TYPE *pointerType);
[[nodiscard]] bool GetDpiForWindowSupported() {
static const auto Result = [&] {
#define LOAD_SYMBOL(lib, name) base::Platform::LoadMethod(lib, #name, name)
const auto user32 = base::Platform::SafeLoadLibrary(L"User32.dll");
return LOAD_SYMBOL(user32, GetDpiForWindow);
#undef LOAD_SYMBOL
}();
return Result;
}
[[nodiscard]] bool GetPointerTypeSupported() {
static const auto Result = [&] {
#define LOAD_SYMBOL(lib, name) base::Platform::LoadMethod(lib, #name, name)
const auto user32 = base::Platform::SafeLoadLibrary(L"User32.dll");
return LOAD_SYMBOL(user32, GetPointerType);
#undef LOAD_SYMBOL
}();
return Result;
}
} // namespace
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<bool> interacting() const {
return _interacting.value();
}
[[nodiscard]] rpl::producer<Event> events() const {
return _events.events();
}
[[nodiscard]] rpl::lifetime &lifetime() {
return _lifetime;
}
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<bool> _interacting = false;
rpl::event_stream<Event> _events;
rpl::lifetime _lifetime;
float _scale = 1.0f;
float _xOffset = 0.f;
float _yOffset = 0.f;
bool _pendingScrollBegin = false;
std::atomic<ULONG> _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) {
if ((IID_IUnknown == iid) ||
(IID_IDirectManipulationViewportEventHandler == iid)) {
*ppv = static_cast<IDirectManipulationViewportEventHandler*>(this);
AddRef();
return S_OK;
}
if (IID_IDirectManipulationInteractionEventHandler == iid) {
*ppv = static_cast<IDirectManipulationInteractionEventHandler*>(
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<RpWidget*> widget)
: NativeEventFilter(widget)
, _interacting([=] { _updateManager->Update(nullptr); }) {
widget->sizeValue() | rpl::on_next([=](QSize size) {
sizeUpdated(size * widget->devicePixelRatio());
}, _lifetime);
widget->winIdValue() | rpl::on_next([=](WId winId) {
destroy();
if (const auto hwnd = reinterpret_cast<HWND>(winId)) {
if (init(hwnd)) {
sizeUpdated(widget->size() * widget->devicePixelRatio());
} else {
destroy();
}
}
}, _lifetime);
}
DirectManipulation::~DirectManipulation() {
destroy();
}
void DirectManipulation::sizeUpdated(QSize nativeSize) {
if (const auto handler = _handler.get()) {
const auto r = QRect(QPoint(), nativeSize);
handler->setViewportSize(r.size());
if (const auto viewport = _viewport.get()) {
const auto rect = RECT{ r.x(), r.y(), r.right(), r.bottom() };
viewport->SetViewportRect(&rect);
}
}
}
bool DirectManipulation::init(HWND hwnd) {
if (!hwnd || !::Platform::IsWindows10OrGreater()) {
return false;
}
_manager = base::WinRT::TryCreateInstance<IDirectManipulationManager>(
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,
hwnd,
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::on_next([=](bool interacting) {
base::Integration::Instance().enterFromEventLoop([&] {
if (interacting) {
_interacting.start();
} else {
_interacting.stop();
}
});
}, _handler->lifetime());
_handler->events() | rpl::on_next([=](Event &&event) {
base::Integration::Instance().enterFromEventLoop([&] {
_events.fire(std::move(event));
});
}, _handler->lifetime());
hr = _viewport->AddEventHandler(hwnd, _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(hwnd);
if (!SUCCEEDED(hr)) {
return false;
}
_managerHandle = hwnd;
hr = _viewport->Enable();
if (!SUCCEEDED(hr)) {
return false;
}
hr = _updateManager->Update(nullptr);
if (!SUCCEEDED(hr)) {
return false;
}
return true;
}
bool DirectManipulation::filterNativeEvent(
UINT msg,
WPARAM wParam,
LPARAM lParam,
LRESULT *result) {
switch (msg) {
case DM_POINTERHITTEST:
if (_viewport && GetPointerTypeSupported()) {
const auto id = UINT32(GET_POINTERID_WPARAM(wParam));
auto type = POINTER_INPUT_TYPE();
if (GetPointerType(id, &type) && type == PT_TOUCHPAD) {
_viewport->SetContact(id);
}
return true;
}
break;
}
return false;
}
auto DirectManipulation::events() const -> rpl::producer<Event> {
return _events.events();
}
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) {
if (_managerHandle) {
_manager->Deactivate(_managerHandle);
}
_manager = nullptr;
}
}
void ActivateDirectManipulation(not_null<RpWidget*> window) {
auto dm = std::make_unique<DirectManipulation>(window);
dm->events(
) | rpl::on_next([=](const DirectManipulationEvent &event) {
using Type = DirectManipulationEventType;
const auto send = [&](Qt::ScrollPhase phase) {
const auto windowHandle = window->windowHandle();
const auto hwnd = reinterpret_cast<HWND>(window->winId());
if (!windowHandle || !hwnd) {
return;
}
auto global = POINT();
::GetCursorPos(&global);
auto local = global;
::ScreenToClient(hwnd, &local);
const auto dpi = GetDpiForWindowSupported()
? GetDpiForWindow(hwnd)
: 0;
const auto scale = dpi ? (96. / dpi) : 1.;
const auto delta = QPointF(event.delta) * scale;
const auto inverted = true;
QWindowSystemInterface::handleWheelEvent(
windowHandle,
QWindowSystemInterfacePrivate::eventTime.elapsed(),
QPointF(local.x, local.y),
QPointF(global.x, global.y),
delta.toPoint(),
(delta * kPixelToAngleDelta).toPoint(),
LookupModifiers(),
phase,
Qt::MouseEventSynthesizedBySystem,
inverted);
};
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;
}
}, window->lifetime());
window->lifetime().add([owned = std::move(dm)] {});
}
} // namespace Ui::Platform