fix(windows): route touch input over video

This commit is contained in:
edde746
2026-07-26 14:58:37 +02:00
parent ecc55d9b36
commit 8ef977d890
2 changed files with 243 additions and 18 deletions
+86 -17
View File
@@ -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() {