fix(windows): route touch input over video
This commit is contained in:
@@ -13,6 +13,9 @@ struct InnerWindowSubclassState {
|
||||
HWND hwnd = nullptr;
|
||||
std::atomic<HWND> forward_target{nullptr};
|
||||
std::atomic<bool> active{false};
|
||||
std::atomic<uint64_t> forwarded_pointer_token{0};
|
||||
std::atomic<LPARAM> forwarded_pointer_position{0};
|
||||
std::mutex forwarded_pointer_mutex;
|
||||
UINT_PTR subclass_id = 0;
|
||||
// Guarded by g_inner_subclasses_mutex.
|
||||
bool installed = false;
|
||||
@@ -67,27 +70,93 @@ flutter::EncodableValue NodeToEncodableValue(const mpv_node* node) {
|
||||
// and consumes input over the video (WS_EX_TRANSPARENT hit-test skipping is
|
||||
// same-thread-only, and disabling the subtree makes the system drop the input
|
||||
// entirely instead of routing it to a sibling). Use the common-controls
|
||||
// subclass chain with per-window reference data, and forward mouse/pointer
|
||||
// input to the Flutter view. Pointer messages must be sent synchronously:
|
||||
// Flutter calls GetPointerInfo while handling them, and Windows only retains
|
||||
// that data for the current or forwarded message.
|
||||
constexpr bool IsFlutterPointerMessage(UINT message) {
|
||||
// subclass chain with per-window reference data. Mouse messages can be posted
|
||||
// directly, but WM_POINTER metadata remains associated with the receiving
|
||||
// window thread, so translate the primary contact to mouse input before
|
||||
// forwarding it to Flutter.
|
||||
constexpr bool IsForwardedPointerMessage(UINT message) {
|
||||
switch (message) {
|
||||
case WM_POINTERDOWN:
|
||||
case WM_POINTERUPDATE:
|
||||
case WM_POINTERUP:
|
||||
case WM_POINTERLEAVE:
|
||||
case WM_POINTERCAPTURECHANGED:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static_assert(IsFlutterPointerMessage(WM_POINTERDOWN));
|
||||
static_assert(IsFlutterPointerMessage(WM_POINTERUPDATE));
|
||||
static_assert(IsFlutterPointerMessage(WM_POINTERUP));
|
||||
static_assert(IsFlutterPointerMessage(WM_POINTERLEAVE));
|
||||
static_assert(!IsFlutterPointerMessage(WM_MOUSEMOVE));
|
||||
static_assert(IsForwardedPointerMessage(WM_POINTERDOWN));
|
||||
static_assert(IsForwardedPointerMessage(WM_POINTERUPDATE));
|
||||
static_assert(IsForwardedPointerMessage(WM_POINTERUP));
|
||||
static_assert(IsForwardedPointerMessage(WM_POINTERLEAVE));
|
||||
static_assert(IsForwardedPointerMessage(WM_POINTERCAPTURECHANGED));
|
||||
static_assert(!IsForwardedPointerMessage(WM_MOUSEMOVE));
|
||||
|
||||
uint64_t PointerToken(WPARAM wparam) { return static_cast<uint64_t>(GET_POINTERID_WPARAM(wparam)) + 1; }
|
||||
|
||||
LPARAM PointerPositionInView(HWND view, LPARAM lparam) {
|
||||
POINT point = {GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam)};
|
||||
::ScreenToClient(view, &point);
|
||||
return MAKELPARAM(point.x, point.y);
|
||||
}
|
||||
void ReleaseForwardedPointer(InnerWindowSubclassState& state, HWND view) {
|
||||
std::lock_guard<std::mutex> lock(state.forwarded_pointer_mutex);
|
||||
if (state.forwarded_pointer_token.exchange(0, std::memory_order_acq_rel) == 0 || !view) {
|
||||
return;
|
||||
}
|
||||
const LPARAM position = state.forwarded_pointer_position.load(std::memory_order_acquire);
|
||||
::PostMessageW(view, WM_LBUTTONUP, 0, position);
|
||||
}
|
||||
|
||||
bool ForwardPointerAsMouse(InnerWindowSubclassState& state, HWND view, UINT message, WPARAM wparam, LPARAM lparam) {
|
||||
if (!IsForwardedPointerMessage(message)) return false;
|
||||
|
||||
std::lock_guard<std::mutex> lock(state.forwarded_pointer_mutex);
|
||||
if (!state.active.load(std::memory_order_acquire) || state.forward_target.load(std::memory_order_acquire) != view) {
|
||||
return true;
|
||||
}
|
||||
const uint64_t pointer_token = PointerToken(wparam);
|
||||
if (message == WM_POINTERDOWN) {
|
||||
if (!IS_POINTER_PRIMARY_WPARAM(wparam) || !IS_POINTER_FIRSTBUTTON_WPARAM(wparam)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
uint64_t expected = 0;
|
||||
if (!state.forwarded_pointer_token.compare_exchange_strong(expected, pointer_token)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const LPARAM position = PointerPositionInView(view, lparam);
|
||||
state.forwarded_pointer_position.store(position, std::memory_order_release);
|
||||
::PostMessageW(view, WM_LBUTTONDOWN, MK_LBUTTON, position);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (state.forwarded_pointer_token.load(std::memory_order_acquire) != pointer_token) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message == WM_POINTERUPDATE) {
|
||||
const LPARAM position = PointerPositionInView(view, lparam);
|
||||
state.forwarded_pointer_position.store(position, std::memory_order_release);
|
||||
::PostMessageW(view, WM_MOUSEMOVE, MK_LBUTTON, position);
|
||||
return true;
|
||||
}
|
||||
|
||||
uint64_t expected = pointer_token;
|
||||
if (!state.forwarded_pointer_token.compare_exchange_strong(expected, 0)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const LPARAM position = message == WM_POINTERCAPTURECHANGED
|
||||
? state.forwarded_pointer_position.load(std::memory_order_acquire)
|
||||
: PointerPositionInView(view, lparam);
|
||||
state.forwarded_pointer_position.store(position, std::memory_order_release);
|
||||
::PostMessageW(view, WM_LBUTTONUP, 0, position);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::mutex g_inner_subclasses_mutex;
|
||||
std::unordered_map<HWND, std::shared_ptr<InnerWindowSubclassState>> g_inner_subclasses;
|
||||
@@ -111,11 +180,7 @@ LRESULT CALLBACK MpvInnerSubclassProc(
|
||||
|
||||
const bool active = state->active.load(std::memory_order_acquire);
|
||||
HWND view = active ? state->forward_target.load(std::memory_order_acquire) : nullptr;
|
||||
if (active && view && IsFlutterPointerMessage(message)) {
|
||||
// WM_POINTER coordinates are already in screen space. SendMessage also
|
||||
// preserves the message association required by GetPointerInfo in the
|
||||
// Flutter view's window procedure.
|
||||
::SendMessageW(view, message, wparam, lparam);
|
||||
if (active && view && ForwardPointerAsMouse(*state, view, message, wparam, lparam)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -136,7 +201,8 @@ LRESULT CALLBACK MpvInnerSubclassProc(
|
||||
|
||||
if (message == WM_NCDESTROY) {
|
||||
state->active.store(false, std::memory_order_release);
|
||||
state->forward_target.store(nullptr, std::memory_order_release);
|
||||
const HWND view = state->forward_target.exchange(nullptr, std::memory_order_acq_rel);
|
||||
ReleaseForwardedPointer(*state, view);
|
||||
::RemoveWindowSubclass(hwnd, MpvInnerSubclassProc, subclass_id);
|
||||
std::lock_guard<std::mutex> lock(g_inner_subclasses_mutex);
|
||||
const auto it = g_inner_subclasses.find(hwnd);
|
||||
@@ -325,6 +391,7 @@ std::shared_ptr<InnerWindowSubclassState> InstallMpvInnerSubclass(HWND inner, HW
|
||||
// A timed-out detach that never reached the window thread leaves the
|
||||
// helper-chain entry installed. Adopt that exact generation rather than
|
||||
// stacking a duplicate subclass or retaining a permanently inert entry.
|
||||
retained->forwarded_pointer_token.store(0, std::memory_order_release);
|
||||
retained->forward_target.store(forward_target, std::memory_order_release);
|
||||
retained->active.store(true, std::memory_order_release);
|
||||
return retained;
|
||||
@@ -373,6 +440,7 @@ void DetachMpvInnerSubclassState(const std::shared_ptr<InnerWindowSubclassState>
|
||||
// already holding this generation can still call DefSubclassProc, but can
|
||||
// no longer target a replacement Flutter/player generation.
|
||||
|
||||
HWND forward_target = nullptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_inner_subclasses_mutex);
|
||||
const auto it = g_inner_subclasses.find(state->hwnd);
|
||||
@@ -380,9 +448,10 @@ void DetachMpvInnerSubclassState(const std::shared_ptr<InnerWindowSubclassState>
|
||||
return;
|
||||
}
|
||||
state->active.store(false, std::memory_order_release);
|
||||
state->forward_target.store(nullptr, std::memory_order_release);
|
||||
forward_target = state->forward_target.exchange(nullptr, std::memory_order_acq_rel);
|
||||
state->removal_pending = true;
|
||||
}
|
||||
ReleaseForwardedPointer(*state, forward_target);
|
||||
|
||||
auto action = std::make_shared<SubclassOwnershipAction>();
|
||||
action->state = state;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#include <windowsx.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <future>
|
||||
@@ -45,6 +47,14 @@ void Check(bool condition, const char* message) {
|
||||
}
|
||||
|
||||
std::atomic<int> g_forwarded_mouse_messages{0};
|
||||
std::atomic<int> g_forwarded_touch_moves{0};
|
||||
std::atomic<int> g_forwarded_mouse_down_messages{0};
|
||||
std::atomic<int> g_forwarded_mouse_up_messages{0};
|
||||
std::atomic<int> g_forwarded_pointer_messages{0};
|
||||
std::atomic<LPARAM> g_forwarded_mouse_down_position{0};
|
||||
std::atomic<LPARAM> g_forwarded_mouse_up_position{0};
|
||||
std::atomic<WPARAM> g_forwarded_mouse_down_flags{0};
|
||||
std::atomic<WPARAM> g_forwarded_mouse_up_flags{0};
|
||||
constexpr UINT kBlockWindowThreadMessage = WM_APP + 0x0505;
|
||||
std::atomic<HANDLE> g_block_entered{nullptr};
|
||||
std::atomic<HANDLE> g_block_release{nullptr};
|
||||
@@ -63,7 +73,27 @@ LRESULT CALLBACK CountingWindowProc(HWND hwnd, UINT message, WPARAM wparam, LPAR
|
||||
return 0;
|
||||
}
|
||||
if (message == WM_MOUSEMOVE) {
|
||||
g_forwarded_mouse_messages.fetch_add(1, std::memory_order_relaxed);
|
||||
if ((wparam & MK_LBUTTON) != 0) {
|
||||
g_forwarded_touch_moves.fetch_add(1, std::memory_order_relaxed);
|
||||
} else {
|
||||
g_forwarded_mouse_messages.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (message == WM_LBUTTONDOWN) {
|
||||
g_forwarded_mouse_down_position.store(lparam, std::memory_order_relaxed);
|
||||
g_forwarded_mouse_down_flags.store(wparam, std::memory_order_relaxed);
|
||||
g_forwarded_mouse_down_messages.fetch_add(1, std::memory_order_relaxed);
|
||||
return 0;
|
||||
}
|
||||
if (message == WM_LBUTTONUP) {
|
||||
g_forwarded_mouse_up_position.store(lparam, std::memory_order_relaxed);
|
||||
g_forwarded_mouse_up_flags.store(wparam, std::memory_order_relaxed);
|
||||
g_forwarded_mouse_up_messages.fetch_add(1, std::memory_order_relaxed);
|
||||
return 0;
|
||||
}
|
||||
if (message >= WM_POINTERUPDATE && message <= WM_POINTERCAPTURECHANGED) {
|
||||
g_forwarded_pointer_messages.fetch_add(1, std::memory_order_relaxed);
|
||||
return 0;
|
||||
}
|
||||
return ::DefWindowProcW(hwnd, message, wparam, lparam);
|
||||
@@ -174,10 +204,111 @@ void TestInnerSubclassOwnershipIsSerializedAndDetached() {
|
||||
}
|
||||
Check(g_forwarded_mouse_messages.load(std::memory_order_relaxed) == 1, "active generation must forward mouse input");
|
||||
|
||||
constexpr UINT32 kPrimaryPointerId = 7;
|
||||
constexpr UINT32 kSecondaryPointerId = 8;
|
||||
constexpr UINT32 kCancelledPointerId = 9;
|
||||
constexpr UINT32 kDetachedPointerId = 10;
|
||||
constexpr UINT32 kDestroyedPointerId = 11;
|
||||
constexpr UINT32 kPointerDownFlags = POINTER_MESSAGE_FLAG_NEW | POINTER_MESSAGE_FLAG_INRANGE |
|
||||
POINTER_MESSAGE_FLAG_INCONTACT | POINTER_MESSAGE_FLAG_FIRSTBUTTON |
|
||||
POINTER_MESSAGE_FLAG_PRIMARY;
|
||||
constexpr UINT32 kPointerMoveFlags = POINTER_MESSAGE_FLAG_INRANGE | POINTER_MESSAGE_FLAG_INCONTACT |
|
||||
POINTER_MESSAGE_FLAG_FIRSTBUTTON | POINTER_MESSAGE_FLAG_PRIMARY;
|
||||
constexpr UINT32 kPointerUpFlags = POINTER_MESSAGE_FLAG_INRANGE | POINTER_MESSAGE_FLAG_PRIMARY;
|
||||
|
||||
POINT down_position = {14, 18};
|
||||
::ClientToScreen(windows.inner, &down_position);
|
||||
POINT expected_down_position = down_position;
|
||||
::ScreenToClient(windows.target, &expected_down_position);
|
||||
POINT up_position = {30, 36};
|
||||
::ClientToScreen(windows.inner, &up_position);
|
||||
POINT expected_up_position = up_position;
|
||||
::ScreenToClient(windows.target, &expected_up_position);
|
||||
|
||||
::SendMessageW(
|
||||
windows.inner, WM_POINTERDOWN, MAKEWPARAM(kPrimaryPointerId, kPointerDownFlags),
|
||||
MAKELPARAM(down_position.x, down_position.y));
|
||||
::SendMessageW(
|
||||
windows.inner, WM_POINTERUPDATE, MAKEWPARAM(kPrimaryPointerId, kPointerMoveFlags),
|
||||
MAKELPARAM(up_position.x, up_position.y));
|
||||
::SendMessageW(
|
||||
windows.inner, WM_POINTERUP, MAKEWPARAM(kPrimaryPointerId, kPointerUpFlags),
|
||||
MAKELPARAM(up_position.x, up_position.y));
|
||||
for (int attempt = 0; attempt < 100 && g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) < 1; ++attempt) {
|
||||
::Sleep(10);
|
||||
}
|
||||
|
||||
Check(g_forwarded_mouse_down_messages.load(std::memory_order_relaxed) == 1, "primary touch must press once");
|
||||
Check(g_forwarded_touch_moves.load(std::memory_order_relaxed) == 1, "primary touch movement must drag once");
|
||||
Check(g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) == 1, "primary touch must release once");
|
||||
Check(
|
||||
g_forwarded_pointer_messages.load(std::memory_order_relaxed) == 0, "raw pointer messages must not reach Flutter");
|
||||
Check(
|
||||
g_forwarded_mouse_down_flags.load(std::memory_order_relaxed) == MK_LBUTTON,
|
||||
"touch down must hold the mouse button");
|
||||
Check(g_forwarded_mouse_up_flags.load(std::memory_order_relaxed) == 0, "touch up must release the mouse button");
|
||||
const LPARAM forwarded_down_position = g_forwarded_mouse_down_position.load(std::memory_order_relaxed);
|
||||
Check(
|
||||
GET_X_LPARAM(forwarded_down_position) == expected_down_position.x &&
|
||||
GET_Y_LPARAM(forwarded_down_position) == expected_down_position.y,
|
||||
"touch down must use Flutter-view client coordinates");
|
||||
const LPARAM forwarded_up_position = g_forwarded_mouse_up_position.load(std::memory_order_relaxed);
|
||||
Check(
|
||||
GET_X_LPARAM(forwarded_up_position) == expected_up_position.x &&
|
||||
GET_Y_LPARAM(forwarded_up_position) == expected_up_position.y,
|
||||
"touch up must use Flutter-view client coordinates");
|
||||
|
||||
const WPARAM secondary_down = MAKEWPARAM(kSecondaryPointerId, kPointerDownFlags & ~POINTER_MESSAGE_FLAG_PRIMARY);
|
||||
const WPARAM secondary_up = MAKEWPARAM(kSecondaryPointerId, kPointerUpFlags & ~POINTER_MESSAGE_FLAG_PRIMARY);
|
||||
::SendMessageW(windows.inner, WM_POINTERDOWN, secondary_down, MAKELPARAM(down_position.x, down_position.y));
|
||||
::SendMessageW(windows.inner, WM_POINTERUP, secondary_up, MAKELPARAM(up_position.x, up_position.y));
|
||||
::Sleep(30);
|
||||
Check(
|
||||
g_forwarded_mouse_down_messages.load(std::memory_order_relaxed) == 1 &&
|
||||
g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) == 1,
|
||||
"secondary touch must not synthesize another click");
|
||||
|
||||
::SendMessageW(
|
||||
windows.inner, WM_POINTERDOWN, MAKEWPARAM(kCancelledPointerId, kPointerDownFlags),
|
||||
MAKELPARAM(down_position.x, down_position.y));
|
||||
::SendMessageW(
|
||||
windows.inner, WM_POINTERCAPTURECHANGED, MAKEWPARAM(kCancelledPointerId, kPointerUpFlags),
|
||||
reinterpret_cast<LPARAM>(windows.host));
|
||||
for (int attempt = 0; attempt < 100 && g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) < 2; ++attempt) {
|
||||
::Sleep(10);
|
||||
}
|
||||
Check(
|
||||
g_forwarded_mouse_down_messages.load(std::memory_order_relaxed) == 2 &&
|
||||
g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) == 2,
|
||||
"capture loss must release an active synthetic mouse press");
|
||||
|
||||
::SendMessageW(
|
||||
windows.inner, WM_POINTERDOWN, MAKEWPARAM(kDetachedPointerId, kPointerDownFlags),
|
||||
MAKELPARAM(down_position.x, down_position.y));
|
||||
for (int attempt = 0; attempt < 100 && g_forwarded_mouse_down_messages.load(std::memory_order_relaxed) < 3;
|
||||
++attempt) {
|
||||
::Sleep(10);
|
||||
}
|
||||
|
||||
MpvPlayerPropertyContractTestPeer::DetachInnerSubclass(player);
|
||||
Check(
|
||||
reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(windows.inner, GWLP_WNDPROC)) == windows.inner_original,
|
||||
"detach must restore the original procedure before window destruction");
|
||||
for (int attempt = 0; attempt < 100 && g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) < 3; ++attempt) {
|
||||
::Sleep(10);
|
||||
}
|
||||
Check(
|
||||
g_forwarded_mouse_down_messages.load(std::memory_order_relaxed) == 3 &&
|
||||
g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) == 3,
|
||||
"detach must release an active synthetic mouse press");
|
||||
|
||||
::SendMessageW(
|
||||
windows.inner, WM_POINTERUP, MAKEWPARAM(kDetachedPointerId, kPointerUpFlags),
|
||||
MAKELPARAM(up_position.x, up_position.y));
|
||||
::Sleep(30);
|
||||
Check(
|
||||
g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) == 3,
|
||||
"a late pointer up after detach must not release twice");
|
||||
|
||||
::SendMessageW(windows.inner, WM_MOUSEMOVE, 0, MAKELPARAM(8, 9));
|
||||
::Sleep(30);
|
||||
@@ -201,9 +332,34 @@ void TestInnerSubclassOwnershipIsSerializedAndDetached() {
|
||||
reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(windows.inner, GWLP_WNDPROC)) == windows.inner_original,
|
||||
"replacement detach must restore the original procedure");
|
||||
|
||||
MpvPlayerPropertyContractTestPeer::ConfigureInnerSubclass(player, windows.host, windows.target);
|
||||
MpvPlayerPropertyContractTestPeer::EnsureInnerSubclass(player);
|
||||
const auto destroy_generation = reinterpret_cast<WNDPROC>(::GetWindowLongPtrW(windows.inner, GWLP_WNDPROC));
|
||||
Check(
|
||||
destroy_generation && destroy_generation != windows.inner_original,
|
||||
"destroy test must install a fresh subclass generation");
|
||||
|
||||
::SendMessageW(
|
||||
windows.inner, WM_POINTERDOWN, MAKEWPARAM(kDestroyedPointerId, kPointerDownFlags),
|
||||
MAKELPARAM(down_position.x, down_position.y));
|
||||
::SendMessageW(windows.inner, WM_CLOSE, 0, 0);
|
||||
for (int attempt = 0; attempt < 100 && g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) < 4; ++attempt) {
|
||||
::Sleep(10);
|
||||
}
|
||||
Check(!::IsWindow(windows.inner), "destroy test must close the inner window");
|
||||
Check(
|
||||
g_forwarded_mouse_down_messages.load(std::memory_order_relaxed) == 4 &&
|
||||
g_forwarded_mouse_up_messages.load(std::memory_order_relaxed) == 4,
|
||||
"window destruction must release an active synthetic mouse press");
|
||||
MpvPlayerPropertyContractTestPeer::ReleaseTestWindows(player);
|
||||
|
||||
::PostThreadMessageW(windows.owner_thread, WM_QUIT, 0, 0);
|
||||
window_owner.join();
|
||||
g_forwarded_mouse_messages.store(0, std::memory_order_relaxed);
|
||||
g_forwarded_touch_moves.store(0, std::memory_order_relaxed);
|
||||
g_forwarded_mouse_down_messages.store(0, std::memory_order_relaxed);
|
||||
g_forwarded_mouse_up_messages.store(0, std::memory_order_relaxed);
|
||||
g_forwarded_pointer_messages.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
void TestTimedOutSubclassDetachCanBeAdopted() {
|
||||
|
||||
Reference in New Issue
Block a user