From 2177fb6eed6c63de4d748fb72832fb8decfb8396 Mon Sep 17 00:00:00 2001 From: YellowNest <59575587+YellowNest@users.noreply.github.com> Date: Mon, 5 Oct 2026 21:29:35 +0200 Subject: [PATCH] Route Windows 11 Start input through XAML --- Src/StartMenu/StartMenuDLL/StartMenuDLL.cpp | 213 ++++- .../StartMenuHelper/Win11StartButtonTap.cpp | 819 +++++++++++++++++- 2 files changed, 1013 insertions(+), 19 deletions(-) diff --git a/Src/StartMenu/StartMenuDLL/StartMenuDLL.cpp b/Src/StartMenu/StartMenuDLL/StartMenuDLL.cpp index 088569e08..b6d1f5286 100644 --- a/Src/StartMenu/StartMenuDLL/StartMenuDLL.cpp +++ b/Src/StartMenu/StartMenuDLL/StartMenuDLL.cpp @@ -50,6 +50,44 @@ static TOOLINFO g_StartButtonTool; static bool g_bHotkeyShift; static int g_HotkeyCSM, g_HotkeyWSM, g_HotkeyShiftID, g_HotkeyCSMID, g_HotkeyWSMID; static HHOOK g_ProgHook, g_StartHook, g_StartMouseHook, g_AppManagerHook, g_NewWindowHook, g_StartMenuHook; + +enum +{ + WIN11_START_INPUT_FALLBACK, + WIN11_START_INPUT_PROBING, + WIN11_START_INPUT_ACTIVE, +}; + +static volatile LONG g_Win11StartInputState=WIN11_START_INPUT_FALLBACK; + +static UINT GetWin11StartInputMessage( void ) +{ + static UINT message=RegisterWindowMessage(L"OpenShell.Win11StartInput"); + return message; +} + +static UINT GetWin11StartInputStateMessage( void ) +{ + static UINT message=RegisterWindowMessage(L"OpenShell.Win11StartInputState"); + return message; +} + +static void DrainWin11StartInputMessages( void ) +{ + if (!IsWin11()) + return; + + MSG msg; + const UINT inputMessage=GetWin11StartInputMessage(); + const UINT stateMessage=GetWin11StartInputStateMessage(); + while (PeekMessage(&msg,NULL,inputMessage,inputMessage,PM_REMOVE)) + { + } + while (PeekMessage(&msg,NULL,stateMessage,stateMessage,PM_REMOVE)) + { + } +} + static bool g_bAllProgramsTimer; static bool g_bInMenu; static DWORD g_LastClickTime; @@ -3081,6 +3119,7 @@ static void InitStartMenuDLL( void ) g_StartHook=SetWindowsHookEx(WH_GETMESSAGE,HookDesktopThread,NULL,GetCurrentThreadId()); if (IsWin11()) { + InterlockedExchange(&g_Win11StartInputState,WIN11_START_INPUT_FALLBACK); g_StartMouseHook=SetWindowsHookEx(WH_MOUSE,HookDesktopThreadMouse,NULL,GetCurrentThreadId()); // hook ShellRegisterHotKey to prevent twinui.dll to install shell hotkeys (Win, Ctrl+Esc) @@ -3293,6 +3332,11 @@ static void CleanStartMenuDLL( void ) { StopWin11StartButtonMonitor(); + // Stop the helper first, then remove any bridge messages that were already + // queued on Explorer's taskbar thread. This prevents stale pointer/state + // messages from surviving a StartMenuDLL unload/reload cycle. + DrainWin11StartInputMessages(); + ClearIatHook(g_DwmpBTRHook); g_DwmpBTRHook=NULL; ClearIatHook(g_DwmpTWWRHook); @@ -3334,6 +3378,7 @@ static void CleanStartMenuDLL( void ) UnhookWindowsHookEx(g_StartHook); if (g_StartMouseHook) UnhookWindowsHookEx(g_StartMouseHook); g_StartMouseHook=NULL; + InterlockedExchange(&g_Win11StartInputState,WIN11_START_INPUT_FALLBACK); if (g_AppManagerHook) UnhookWindowsHookEx(g_AppManagerHook); g_AppManagerHook=NULL; if (g_NewWindowHook) UnhookWindowsHookEx(g_NewWindowHook); @@ -3621,42 +3666,162 @@ static LRESULT CALLBACK HookProgManThread( int code, WPARAM wParam, LPARAM lPara return CallNextHookEx(NULL,code,wParam,lParam); } -// WH_MOUSE hook for taskbar thread (Win11+) +// WH_MOUSE fallback for the taskbar thread (Win11+). +// +// Once the XAML input route has proved that it receives pointer movement for +// the native Start slot, the fallback is removed. While probing, movement is +// allowed through so the XAML route can prove itself; button messages continue +// to use the established hook path until that transition is complete. static LRESULT CALLBACK HookDesktopThreadMouse(int code, WPARAM wParam, LPARAM lParam) { if (code == HC_ACTION) { - // we need to steal mouse messages that are issues in start button area - // so that they won't get to XAML framework that is handling original start button - auto info = (const MOUSEHOOKSTRUCT*)lParam; + LONG inputState=InterlockedCompareExchange(&g_Win11StartInputState,0,0); + auto info=(const MOUSEHOOKSTRUCT*)lParam; + auto taskBar=FindTaskBarInfoButton(info->hwnd); + if (!taskBar) { - auto taskBar = FindTaskBarInfoButton(info->hwnd); // click on start button - if (!taskBar) + taskBar=FindTaskBarInfoBar(GetAncestor(info->hwnd,GA_ROOT)); + if (taskBar && !PointAroundStartButton(taskBar->taskbarId)) + taskBar=NULL; + } + + if (taskBar && info->hwnd!=taskBar->startButton && taskBar->oldButton) + { + if (inputState==WIN11_START_INPUT_PROBING && wParam==WM_MOUSEMOVE) { - taskBar = FindTaskBarInfoBar(GetAncestor(info->hwnd, GA_ROOT)); // click on taskbar - if (taskBar && !PointAroundStartButton(taskBar->taskbarId)) - taskBar = NULL; + // Preserve existing Open-Shell hover behavior while also allowing + // this movement to reach XAML and prove the new route. A duplicate + // synthetic WM_MOUSEMOVE is harmless; button messages remain owned + // exclusively by the fallback until the route is confirmed. + PostMessage(taskBar->oldButton,WM_MOUSEMOVE,0,MAKELPARAM(info->pt.x,info->pt.y)); + return CallNextHookEx(NULL,code,wParam,lParam); } - if (taskBar && (info->hwnd != taskBar->startButton) && taskBar->oldButton) + // Preserve the existing behavior until XAML routing is confirmed. + PostMessage(taskBar->oldButton,(UINT)wParam,0,MAKELPARAM(info->pt.x,info->pt.y)); + return 1; + } + } + + return CallNextHookEx(NULL,code,wParam,lParam); +} + +static void SetWin11StartInputState( LONG state ) +{ + if (!IsWin11()) + return; + + if (state==WIN11_START_INPUT_ACTIVE) + { + if (g_StartMouseHook) + { + if (!UnhookWindowsHookEx(g_StartMouseHook)) { - // steal messages from other than our custom button window - PostMessage(taskBar->oldButton, (UINT)wParam, 0, MAKELPARAM(info->pt.x, info->pt.y)); - return 1; + DWORD error=GetLastError(); + InterlockedExchange(&g_Win11StartInputState,WIN11_START_INPUT_FALLBACK); + LogToFile(STARTUP_LOG,L"Win11StartInput: failed to remove WH_MOUSE fallback 0x%08X",error); + return; } + g_StartMouseHook=NULL; } + InterlockedExchange(&g_Win11StartInputState,WIN11_START_INPUT_ACTIVE); + LogToFile(STARTUP_LOG,L"Win11StartInput: XAML routing active; WH_MOUSE fallback removed"); + return; + } + + InterlockedExchange(&g_Win11StartInputState,state); + if (!g_StartMouseHook) + { + g_StartMouseHook=SetWindowsHookEx(WH_MOUSE,HookDesktopThreadMouse,NULL,GetCurrentThreadId()); + if (g_StartMouseHook) + LogToFile(STARTUP_LOG,L"Win11StartInput: WH_MOUSE fallback restored (%d)",state); + else + LogToFile(STARTUP_LOG,L"Win11StartInput: failed to restore WH_MOUSE fallback 0x%08X",GetLastError()); } +} + +static bool RewriteWin11StartInputMessage( MSG *msg ) +{ + TaskbarInfo *taskBar=FindTaskBarInfoBar(msg->hwnd); + if (!taskBar || !taskBar->oldButton) + { + SetWin11StartInputState(WIN11_START_INPUT_FALLBACK); + msg->message=WM_NULL; + return false; + } + + UINT mouseMessage=(UINT)msg->wParam; + if (!((mouseMessage>=WM_MOUSEFIRST && mouseMessage<=WM_MOUSELAST) || mouseMessage==WM_MOUSELEAVE)) + { + msg->message=WM_NULL; + return false; + } + + // The helper transports coordinates relative to the taskbar HWND rather + // than global virtual-screen coordinates. Preserve them for every routed + // mouse event, including WM_MOUSELEAVE, so later logic can use the event-time + // MSG.pt instead of sampling GetMessagePos() after queueing. + POINT taskbarPoint={(short)LOWORD(msg->lParam),(short)HIWORD(msg->lParam)}; + POINT screenPoint=taskbarPoint; + if (!ClientToScreen(taskBar->taskBar,&screenPoint)) + { + SetWin11StartInputState(WIN11_START_INPUT_FALLBACK); + msg->message=WM_NULL; + return false; + } + + msg->hwnd=taskBar->oldButton; + msg->message=mouseMessage; + msg->wParam=0; + msg->pt=screenPoint; - return CallNextHookEx(NULL, code, wParam, lParam); + if (mouseMessage==WM_MOUSELEAVE) + { + msg->lParam=0; + } + else + { + POINT clientPoint=screenPoint; + ScreenToClient(taskBar->oldButton,&clientPoint); + msg->lParam=MAKELPARAM(clientPoint.x,clientPoint.y); + } + return true; } // WH_GETMESSAGE hook for the taskbar thread static LRESULT CALLBACK HookDesktopThread( int code, WPARAM wParam, LPARAM lParam ) { + // Input-route state changes are safety-critical. Process them even while an + // Open-Shell popup menu is running its nested message loop; otherwise a + // fallback request can be lost while WH_MOUSE is detached. + if (code==HC_ACTION && wParam) + { + MSG *controlMsg=(MSG*)lParam; + if (controlMsg->message==GetWin11StartInputStateMessage()) + { + LONG state=(LONG)controlMsg->wParam; + if (state>=WIN11_START_INPUT_FALLBACK && state<=WIN11_START_INPUT_ACTIVE) + SetWin11StartInputState(state); + controlMsg->message=WM_NULL; + return CallNextHookEx(NULL,code,wParam,lParam); + } + if (g_bInMenu && controlMsg->message==GetWin11StartInputMessage()) + { + controlMsg->message=WM_NULL; + return CallNextHookEx(NULL,code,wParam,lParam); + } + } + if (code==HC_ACTION && wParam && !g_bInMenu) { MSG *msg=(MSG*)lParam; FindTaskBar(); + + bool win11RoutedInput=false; + if (msg->message==GetWin11StartInputMessage()) + win11RoutedInput=RewriteWin11StartInputMessage(msg); + if (IsSettingsMessage(msg)) { msg->message=WM_NULL; @@ -3983,7 +4148,11 @@ if (!g_bTrimHooks) { APPBARDATA appbar={sizeof(appbar)}; if (SHAppBarMessage(ABM_GETSTATE,&appbar)&ABS_AUTOHIDE) - SendMessage(taskBar->taskBar,WM_NCHITTEST,0,GetMessagePos()); + { + LPARAM hitTestPoint=win11RoutedInput ? + MAKELPARAM(msg->pt.x,msg->pt.y) : GetMessagePos(); + SendMessage(taskBar->taskBar,WM_NCHITTEST,0,hitTestPoint); + } } } } @@ -3998,7 +4167,11 @@ if (!g_bTrimHooks) { RECT rc; GetWindowRect(taskBar->oldButton,&rc); - CPoint pt(GetMessagePos()); + CPoint pt; + if (win11RoutedInput) + pt=msg->pt; + else + pt=CPoint(GetMessagePos()); if (PtInRect(&rc,pt)) { wchar_t className[256]={0}; @@ -4088,7 +4261,11 @@ if (!g_bTrimHooks) } if (taskBar) { - CPoint pt0(GetMessagePos()); + CPoint pt0; + if (win11RoutedInput) + pt0=msg->pt; + else + pt0=CPoint(GetMessagePos()); if (msg->message==WM_RBUTTONUP && msg->hwnd==taskBar->startButton && msg->lParam==MAKELPARAM(-1,-1)) { RECT rc; @@ -4238,7 +4415,7 @@ if (!g_bTrimHooks) } else if (bShowWin7) { - CPoint pt(GetMessagePos()); + CPoint pt(pt0); ScreenToClient(g_WinStartButton,&pt); PostMessage(g_WinStartButton,WM_RBUTTONUP,wParam,MAKELONG(pt.x,pt.y)); } diff --git a/Src/StartMenu/StartMenuHelper/Win11StartButtonTap.cpp b/Src/StartMenu/StartMenuHelper/Win11StartButtonTap.cpp index 6770e6431..931d1404c 100644 --- a/Src/StartMenu/StartMenuHelper/Win11StartButtonTap.cpp +++ b/Src/StartMenu/StartMenuHelper/Win11StartButtonTap.cpp @@ -15,15 +15,91 @@ #include "..\StartMenuDLL\LogManager.h" #include +#include +#include +#include #include #include +#include +#include +#include +#include +#include #include +#include #include static const GUID CLSID_OpenShellStartButtonTap = { 0x7d15741f, 0x2f3b, 0x4971, { 0xb8, 0x91, 0x6a, 0x5d, 0x42, 0xd7, 0x1a, 0x34 } }; +// windows.ui.xaml.hosting.desktopwindowxamlsource.h exposes this interface +// through projection-specific declarations that do not compose cleanly with +// this ATL project. Keep the documented COM ABI local instead of pulling a +// second WinRT projection into StartMenuHelper. +MIDL_INTERFACE("3cbcf1bf-2f76-4e9c-96ab-e84b37972554") +IOpenShellDesktopWindowXamlSourceNative : public IUnknown +{ +public: + virtual HRESULT STDMETHODCALLTYPE AttachToWindow( HWND parentWnd ) = 0; + virtual HRESULT STDMETHODCALLTYPE get_WindowHandle( HWND *hWnd ) = 0; +}; + +MIDL_INTERFACE("d585bfe1-00ff-51be-ba1d-a1329956ea0a") +IOpenShellDesktopWindowXamlSource : public IInspectable +{ +public: + virtual HRESULT STDMETHODCALLTYPE get_Content( + ABI::Windows::UI::Xaml::IUIElement **value ) = 0; +}; + static const UINT WM_OS_STARTBUTTON_APPLY = WM_APP + 0x35B; +static const UINT WM_OS_STARTBUTTON_REPROBE = WM_APP + 0x35C; + +enum +{ + WIN11_START_INPUT_FALLBACK, + WIN11_START_INPUT_PROBING, + WIN11_START_INPUT_ACTIVE, +}; + +static UINT GetWin11StartInputMessage( void ) +{ + static UINT message = RegisterWindowMessage(L"OpenShell.Win11StartInput"); + return message; +} + +static UINT GetWin11StartInputStateMessage( void ) +{ + static UINT message = RegisterWindowMessage(L"OpenShell.Win11StartInputState"); + return message; +} + +static bool IsTaskbarWindow( HWND hwnd ) +{ + if (!hwnd) + return false; + wchar_t className[64] = {}; + if (!GetClassName(hwnd, className, _countof(className))) + return false; + return wcscmp(className, L"Shell_TrayWnd") == 0 || + wcscmp(className, L"Shell_SecondaryTrayWnd") == 0; +} + +static HWND FindTaskbarAncestor( HWND hwnd ) +{ + for (int depth = 0; hwnd && depth < 16; depth++) + { + if (IsTaskbarWindow(hwnd)) + return hwnd; + HWND parent = GetParent(hwnd); + if (!parent) + break; + hwnd = parent; + } + + HWND root = hwnd ? GetAncestor(hwnd, GA_ROOT) : NULL; + return IsTaskbarWindow(root) ? root : NULL; +} static volatile LONG g_StartButtonActive = 0; static volatile LONG g_StartButtonEnabled = 0; @@ -60,7 +136,64 @@ struct StartElement } }; +struct StartInputRoute +{ + InstanceHandle startHandle; + InstanceHandle rootHandle; + InstanceHandle sourceHandle; + CComPtr startElement; + CComPtr startFramework; + CComPtr rootElement; + CComPtr handler; + bool verified; + bool pointerInside; + HWND taskbar; + DWORD lastPressTime; + POINT lastPressPoint; + UINT lastPressMessage; + + StartInputRoute( void ) + { + startHandle = 0; + rootHandle = 0; + sourceHandle = 0; + verified = false; + pointerInside = false; + taskbar = NULL; + lastPressTime = 0; + lastPressPoint.x = 0; + lastPressPoint.y = 0; + lastPressMessage = 0; + } +}; + class CWin11StartButtonTap; + +class CStartPointerHandler: + public Microsoft::WRL::RuntimeClass< + Microsoft::WRL::RuntimeClassFlags, + ABI::Windows::UI::Xaml::Input::IPointerEventHandler> +{ +public: + CStartPointerHandler( CWin11StartButtonTap *owner, InstanceHandle startHandle ); + ~CStartPointerHandler( void ); + + HRESULT STDMETHODCALLTYPE GetTrustLevel( TrustLevel *trustLevel ) override + { + if (!trustLevel) + return E_POINTER; + *trustLevel = BaseTrust; + return S_OK; + } + + HRESULT STDMETHODCALLTYPE Invoke( IInspectable *sender, + ABI::Windows::UI::Xaml::Input::IPointerRoutedEventArgs *args ) override; + +private: + CWin11StartButtonTap *m_Owner; + InstanceHandle m_StartHandle; +}; + static CWin11StartButtonTap *g_Tap = NULL; static SRWLOCK g_TapLock = SRWLOCK_INIT; @@ -99,12 +232,16 @@ class CWin11StartButtonTap: public IObjectWithSite, public IVisualTreeServiceCal m_Dispatch = NULL; m_PrimaryStart = 0; m_NextDiscoveryOrder = 0; + m_ExpectedInputRoutes = 0; + m_LastInputState = -1; InitializeCriticalSection(&m_Lock); _AtlModule.Lock(); } ~CWin11StartButtonTap( void ) { + if (m_Dispatch && GetWindowThreadProcessId(m_Dispatch, NULL) == GetCurrentThreadId()) + DetachAllInputRoutes(); if (m_Visual && m_Advised) m_Visual->UnadviseVisualTreeChange(static_cast(this)); if (m_Dispatch && GetWindowThreadProcessId(m_Dispatch, NULL) == GetCurrentThreadId()) @@ -163,8 +300,17 @@ class CWin11StartButtonTap: public IObjectWithSite, public IVisualTreeServiceCal } m_Advised = false; } + DetachAllInputRoutes(); + m_UIElementStatics.Release(); + m_PointerEnteredEvent.Release(); + m_PointerMovedEvent.Release(); + m_PointerPressedEvent.Release(); + m_PointerReleasedEvent.Release(); + m_PointerExitedEvent.Release(); + m_Diagnostics.Release(); m_Visual.Release(); m_Site.Release(); + m_LastInputState = -1; if (!site) { @@ -175,6 +321,7 @@ class CWin11StartButtonTap: public IObjectWithSite, public IVisualTreeServiceCal EnterCriticalSection(&m_Lock); m_Elements.clear(); + m_XamlSources.clear(); m_PrimaryStart = 0; m_NextDiscoveryOrder = 0; LeaveCriticalSection(&m_Lock); @@ -193,6 +340,7 @@ class CWin11StartButtonTap: public IObjectWithSite, public IVisualTreeServiceCal EnterCriticalSection(&m_Lock); m_Elements.clear(); + m_XamlSources.clear(); m_PrimaryStart = 0; m_NextDiscoveryOrder = 0; LeaveCriticalSection(&m_Lock); @@ -206,15 +354,24 @@ class CWin11StartButtonTap: public IObjectWithSite, public IVisualTreeServiceCal HRESULT hr = site->QueryInterface(__uuidof(IVisualTreeService), (void**)&m_Visual); if (FAILED(hr) || !m_Visual) { + m_Diagnostics.Release(); m_Visual.Release(); m_Site.Release(); InterlockedExchange(&g_ConnectStarted, 0); return hr; } + HRESULT diagnosticsHr = site->QueryInterface(__uuidof(IXamlDiagnostics), (void**)&m_Diagnostics); + if (FAILED(diagnosticsHr)) + { + m_Diagnostics.Release(); + LogToFile(STARTUP_LOG, L"Win11StartInput: IXamlDiagnostics unavailable 0x%08X; keeping WH_MOUSE fallback", diagnosticsHr); + } + if (!CreateDispatchWindow()) { DWORD error = GetLastError(); + m_Diagnostics.Release(); m_Visual.Release(); m_Site.Release(); InterlockedExchange(&g_ConnectStarted, 0); @@ -246,6 +403,7 @@ class CWin11StartButtonTap: public IObjectWithSite, public IVisualTreeServiceCal SetWindowLongPtr(dispatch, GWLP_USERDATA, 0); DestroyWindow(dispatch); } + m_Diagnostics.Release(); m_Visual.Release(); m_Site.Release(); InterlockedExchange(&g_ConnectStarted, 0); @@ -273,6 +431,7 @@ class CWin11StartButtonTap: public IObjectWithSite, public IVisualTreeServiceCal EnterCriticalSection(&m_Lock); if (mutationType == Remove) { + m_XamlSources.erase(element.Handle); auto it = m_Elements.find(element.Handle); if (it != m_Elements.end()) { @@ -305,12 +464,24 @@ class CWin11StartButtonTap: public IObjectWithSite, public IVisualTreeServiceCal record.discoveryOrder = ++m_NextDiscoveryOrder; } m_Elements[element.Handle] = record; + const bool isXamlSource = record.type == L"Windows.UI.Xaml.Hosting.DesktopWindowXamlSource"; + if (isXamlSource) + m_XamlSources.insert(element.Handle); - interesting = IsStartControlCandidate(record) || IsStartGlyph(record) || + interesting = isXamlSource || IsStartControlCandidate(record) || IsStartGlyph(record) || IsUnderStartButtonLocked(record.parent); } LeaveCriticalSection(&m_Lock); + // Do not mutate XAML routed-event handler collections from inside the + // visual-tree callback. If an active route is disappearing, queue the + // fallback transition first; the posted apply pass then removes the stale + // route after the diagnostics callback has unwound. + if (mutationType == Remove && InputRouteUsesHandle(element.Handle)) + { + ReportInputBridgeState(WIN11_START_INPUT_PROBING); + interesting = true; + } if (interesting) RequestApply(false); return S_OK; @@ -331,6 +502,196 @@ class CWin11StartButtonTap: public IObjectWithSite, public IVisualTreeServiceCal PostMessage(m_Dispatch, WM_OS_STARTBUTTON_APPLY, 0, 0); } + void RequestInputBridgeReprobe( void ) + { + if (m_Dispatch) + PostMessage(m_Dispatch, WM_OS_STARTBUTTON_REPROBE, 0, 0); + } + + HRESULT OnStartPointer( InstanceHandle startHandle, + ABI::Windows::UI::Xaml::Input::IPointerRoutedEventArgs *args ) + { + if (!args || !InterlockedCompareExchange(&g_StartButtonActive, 0, 0) || + !InterlockedCompareExchange(&g_StartButtonEnabled, 0, 0)) + return S_OK; + + StartInputRoute *route = FindInputRoute(startHandle); + if (!route) + return S_OK; + + CComPtr pointer; + HRESULT hr = args->get_Pointer(&pointer); + if (FAILED(hr) || !pointer) + { + InvalidateInputRoute(*route); + return S_OK; + } + + ABI::Windows::Devices::Input::PointerDeviceType deviceType = + ABI::Windows::Devices::Input::PointerDeviceType_Touch; + hr = pointer->get_PointerDeviceType(&deviceType); + if (FAILED(hr)) + { + InvalidateInputRoute(*route); + return S_OK; + } + if (deviceType != ABI::Windows::Devices::Input::PointerDeviceType_Mouse) + return S_OK; + + // This bridge currently routes mouse input only. GetCursorPos is sufficient + // for the screen-space mouse location and, unlike GetPointerInfo, does not + // add a Windows 8+ User32 import to StartMenuHelper. + POINT screenPoint = {}; + if (!GetCursorPos(&screenPoint)) + { + InvalidateInputRoute(*route); + return S_OK; + } + + CComPtr point; + hr = args->GetCurrentPoint(route->startElement, &point); + if (FAILED(hr) || !point) + { + InvalidateInputRoute(*route); + return S_OK; + } + + ABI::Windows::Foundation::Point position = {}; + DOUBLE width = 0; + DOUBLE height = 0; + if (FAILED(point->get_Position(&position)) || + FAILED(route->startFramework->get_ActualWidth(&width)) || + FAILED(route->startFramework->get_ActualHeight(&height))) + { + InvalidateInputRoute(*route); + return S_OK; + } + + bool inside = position.X >= 0 && position.Y >= 0 && + position.X < width && position.Y < height; + if (!inside) + { + if (route->pointerInside && route->taskbar) + PostInputMessage(route->taskbar, WM_MOUSELEAVE, screenPoint); + route->pointerInside = false; + return S_OK; + } + + HWND taskbar = route->taskbar; + if (!taskbar || !IsWindow(taskbar)) + { + InvalidateInputRoute(*route); + return S_OK; + } + + UINT mouseMessage = WM_MOUSEMOVE; + CComPtr properties; + hr = point->get_Properties(&properties); + if (FAILED(hr) || !properties) + { + // Do not guess the event kind. Treating an unreadable press/release as + // WM_MOUSEMOVE could swallow a click after the WH_MOUSE fallback has + // been retired. Re-arm the fallback and leave this XAML event untouched. + InvalidateInputRoute(*route); + return S_OK; + } + + ABI::Windows::UI::Input::PointerUpdateKind updateKind = + ABI::Windows::UI::Input::PointerUpdateKind_Other; + hr = properties->get_PointerUpdateKind(&updateKind); + if (FAILED(hr)) + { + InvalidateInputRoute(*route); + return S_OK; + } + + switch (updateKind) + { + case ABI::Windows::UI::Input::PointerUpdateKind_LeftButtonPressed: + mouseMessage = WM_LBUTTONDOWN; + break; + case ABI::Windows::UI::Input::PointerUpdateKind_LeftButtonReleased: + mouseMessage = WM_LBUTTONUP; + break; + case ABI::Windows::UI::Input::PointerUpdateKind_RightButtonPressed: + mouseMessage = WM_RBUTTONDOWN; + break; + case ABI::Windows::UI::Input::PointerUpdateKind_RightButtonReleased: + mouseMessage = WM_RBUTTONUP; + break; + case ABI::Windows::UI::Input::PointerUpdateKind_MiddleButtonPressed: + mouseMessage = WM_MBUTTONDOWN; + break; + case ABI::Windows::UI::Input::PointerUpdateKind_MiddleButtonReleased: + mouseMessage = WM_MBUTTONUP; + break; + default: + break; + } + + // WH_MOUSE used to deliver Windows-generated double-click messages. + // Routed pointer events expose button transitions instead, so preserve the + // same behavior before retiring the hook. + UINT doubleClickMessage = 0; + if (mouseMessage == WM_LBUTTONDOWN) + doubleClickMessage = WM_LBUTTONDBLCLK; + else if (mouseMessage == WM_RBUTTONDOWN) + doubleClickMessage = WM_RBUTTONDBLCLK; + else if (mouseMessage == WM_MBUTTONDOWN) + doubleClickMessage = WM_MBUTTONDBLCLK; + + if (doubleClickMessage) + { + DWORD now = GetTickCount(); + LONG dx = screenPoint.x - route->lastPressPoint.x; + LONG dy = screenPoint.y - route->lastPressPoint.y; + if (dx < 0) dx = -dx; + if (dy < 0) dy = -dy; + const int maxDx = GetSystemMetrics(SM_CXDOUBLECLK) / 2; + const int maxDy = GetSystemMetrics(SM_CYDOUBLECLK) / 2; + + if (route->lastPressMessage == mouseMessage && + now - route->lastPressTime <= GetDoubleClickTime() && + dx <= maxDx && dy <= maxDy) + { + mouseMessage = doubleClickMessage; + route->lastPressTime = 0; + route->lastPressMessage = 0; + } + else + { + route->lastPressTime = now; + route->lastPressPoint = screenPoint; + route->lastPressMessage = mouseMessage; + } + } + + if (!PostInputMessage(taskbar, mouseMessage, screenPoint)) + { + InvalidateInputRoute(*route); + return S_OK; + } + + route->pointerInside = true; + route->taskbar = taskbar; + hr = args->put_Handled(TRUE); + if (FAILED(hr)) + { + // The bridge must not be considered verified unless XAML accepted the + // handled state. Otherwise both the bridged event and native XAML path + // could remain active after the fallback hook is removed. + InvalidateInputRoute(*route); + return S_OK; + } + + if (!route->verified) + { + route->verified = true; + EvaluateInputBridgeState(); + } + return S_OK; + } + HRESULT Deactivate( void ) { // The diagnostics runtime retains this site beyond Open-Shell's lifetime. @@ -343,6 +704,390 @@ class CWin11StartButtonTap: public IObjectWithSite, public IVisualTreeServiceCal } private: + bool PostInputMessage( HWND taskbar, UINT mouseMessage, POINT screenPoint ) + { + if (!taskbar || !IsWindow(taskbar)) + return false; + + // Preserve the event-time cursor position even for WM_MOUSELEAVE. The + // receiving WH_GETMESSAGE path uses MSG.pt for routed input, so it must not + // fall back to a later GetMessagePos() sample after queueing. + POINT taskbarPoint = screenPoint; + if (!ScreenToClient(taskbar, &taskbarPoint)) + return false; + LPARAM position = MAKELPARAM(taskbarPoint.x, taskbarPoint.y); + return PostMessage(taskbar, GetWin11StartInputMessage(), mouseMessage, position) != FALSE; + } + + void ReprobeInputBridgeState( void ) + { + for (size_t i = 0; i < m_InputRoutes.size(); i++) + m_InputRoutes[i].verified = false; + m_LastInputState = -1; + ReportInputBridgeState( + InterlockedCompareExchange(&g_StartButtonEnabled, 0, 0) ? + WIN11_START_INPUT_PROBING : WIN11_START_INPUT_FALLBACK); + } + + void ReportInputBridgeState( LONG state ) + { + if (!InterlockedCompareExchange(&g_StartButtonActive, 0, 0)) + { + m_LastInputState = -1; + return; + } + if (m_LastInputState == state) + return; + + HWND taskbar = FindWindow(L"Shell_TrayWnd", NULL); + if (taskbar && PostMessage(taskbar, GetWin11StartInputStateMessage(), state, 0)) + { + m_LastInputState = state; + LogToFile(STARTUP_LOG, L"Win11StartInput: routing state %d", state); + } + } + + StartInputRoute *FindInputRoute( InstanceHandle startHandle ) + { + for (size_t i = 0; i < m_InputRoutes.size(); i++) + if (m_InputRoutes[i].startHandle == startHandle) + return &m_InputRoutes[i]; + return NULL; + } + + bool InputRouteUsesHandle( InstanceHandle handle ) const + { + for (size_t i = 0; i < m_InputRoutes.size(); i++) + if (m_InputRoutes[i].startHandle == handle || + m_InputRoutes[i].rootHandle == handle || + m_InputRoutes[i].sourceHandle == handle) + return true; + return false; + } + + void InvalidateInputRoute( StartInputRoute &route ) + { + route.verified = false; + ReportInputBridgeState(WIN11_START_INPUT_PROBING); + } + + bool EnsureUIElementStatics( void ) + { + if (m_UIElementStatics && m_PointerEnteredEvent && m_PointerMovedEvent && + m_PointerPressedEvent && m_PointerReleasedEvent && m_PointerExitedEvent) + return true; + + // Keep StartMenuHelper loadable on pre-Windows-8 systems. These WinRT + // entry points are resolved only when the Windows 11 XAML path is active, + // rather than becoming static DLL imports. + typedef HRESULT (WINAPI *WindowsCreateString_t)( PCNZWCH, UINT32, HSTRING* ); + typedef HRESULT (WINAPI *WindowsDeleteString_t)( HSTRING ); + typedef HRESULT (WINAPI *RoGetActivationFactory_t)( HSTRING, REFIID, void** ); + + HMODULE combase = GetModuleHandle(L"combase.dll"); + if (!combase) + { + LogToFile(STARTUP_LOG, L"Win11StartInput: combase is unavailable; keeping WH_MOUSE fallback"); + return false; + } + + WindowsCreateString_t createString = + (WindowsCreateString_t)GetProcAddress(combase, "WindowsCreateString"); + WindowsDeleteString_t deleteString = + (WindowsDeleteString_t)GetProcAddress(combase, "WindowsDeleteString"); + RoGetActivationFactory_t getActivationFactory = + (RoGetActivationFactory_t)GetProcAddress(combase, "RoGetActivationFactory"); + if (!createString || !deleteString || !getActivationFactory) + { + LogToFile(STARTUP_LOG, L"Win11StartInput: WinRT activation exports are unavailable; keeping WH_MOUSE fallback"); + return false; + } + + static const wchar_t CLASS_NAME[] = L"Windows.UI.Xaml.UIElement"; + HSTRING className = NULL; + HRESULT hr = createString(CLASS_NAME, _countof(CLASS_NAME) - 1, &className); + if (FAILED(hr)) + return false; + + hr = getActivationFactory(className, __uuidof(ABI::Windows::UI::Xaml::IUIElementStatics), + (void**)&m_UIElementStatics); + deleteString(className); + if (FAILED(hr) || !m_UIElementStatics) + { + LogToFile(STARTUP_LOG, L"Win11StartInput: UIElement statics unavailable 0x%08X", hr); + return false; + } + + if (FAILED(m_UIElementStatics->get_PointerEnteredEvent(&m_PointerEnteredEvent)) || + FAILED(m_UIElementStatics->get_PointerMovedEvent(&m_PointerMovedEvent)) || + FAILED(m_UIElementStatics->get_PointerPressedEvent(&m_PointerPressedEvent)) || + FAILED(m_UIElementStatics->get_PointerReleasedEvent(&m_PointerReleasedEvent)) || + FAILED(m_UIElementStatics->get_PointerExitedEvent(&m_PointerExitedEvent))) + { + LogToFile(STARTUP_LOG, L"Win11StartInput: pointer routed-event metadata unavailable"); + m_PointerEnteredEvent.Release(); + m_PointerMovedEvent.Release(); + m_PointerPressedEvent.Release(); + m_PointerReleasedEvent.Release(); + m_PointerExitedEvent.Release(); + m_UIElementStatics.Release(); + return false; + } + return true; + } + + bool GetInputRoot( InstanceHandle startHandle, InstanceHandle *rootHandle, + CComPtr &rootElement ) + { + if (!m_Diagnostics) + return false; + + std::vector ancestors; + EnterCriticalSection(&m_Lock); + auto it = m_Elements.find(startHandle); + InstanceHandle current = it != m_Elements.end() ? it->second.parent : 0; + for (int depth = 0; depth < 64 && current; depth++) + { + ancestors.push_back(current); + auto parent = m_Elements.find(current); + if (parent == m_Elements.end()) + break; + current = parent->second.parent; + } + LeaveCriticalSection(&m_Lock); + + for (auto it2 = ancestors.rbegin(); it2 != ancestors.rend(); ++it2) + { + CComPtr inspectable; + if (FAILED(m_Diagnostics->GetIInspectableFromHandle(*it2, &inspectable)) || !inspectable) + continue; + + CComPtr element; + if (SUCCEEDED(inspectable->QueryInterface(__uuidof(ABI::Windows::UI::Xaml::IUIElement), + (void**)&element)) && element) + { + *rootHandle = *it2; + rootElement = element; + return true; + } + } + return false; + } + + HWND ResolveInputTaskbar( + ABI::Windows::UI::Xaml::IUIElement *rootElement, InstanceHandle *sourceHandle ) + { + if (sourceHandle) + *sourceHandle = 0; + if (!m_Diagnostics || !rootElement) + return NULL; + + CComPtr rootIdentity; + if (FAILED(rootElement->QueryInterface(IID_PPV_ARGS(&rootIdentity))) || !rootIdentity) + return NULL; + + std::vector sources; + EnterCriticalSection(&m_Lock); + for (auto it = m_XamlSources.begin(); it != m_XamlSources.end(); ++it) + sources.push_back(*it); + LeaveCriticalSection(&m_Lock); + + for (size_t i = 0; i < sources.size(); i++) + { + CComPtr inspectable; + if (FAILED(m_Diagnostics->GetIInspectableFromHandle(sources[i], &inspectable)) || !inspectable) + continue; + + CComPtr source; + if (FAILED(inspectable->QueryInterface(IID_PPV_ARGS(&source))) || !source) + continue; + + CComPtr content; + if (FAILED(source->get_Content(&content)) || !content) + continue; + + CComPtr contentIdentity; + if (FAILED(content->QueryInterface(IID_PPV_ARGS(&contentIdentity))) || + contentIdentity != rootIdentity) + continue; + + CComPtr nativeSource; + if (FAILED(inspectable->QueryInterface(IID_PPV_ARGS(&nativeSource))) || !nativeSource) + continue; + + HWND xamlHwnd = NULL; + if (FAILED(nativeSource->get_WindowHandle(&xamlHwnd)) || !xamlHwnd) + continue; + + HWND taskbar = FindTaskbarAncestor(xamlHwnd); + if (taskbar) + { + if (sourceHandle) + *sourceHandle = sources[i]; + return taskbar; + } + } + return NULL; + } + + bool AttachInputRoute( InstanceHandle startHandle ) + { + if (FindInputRoute(startHandle)) + return true; + if (!m_Diagnostics || !EnsureUIElementStatics()) + return false; + + CComPtr inspectable; + if (FAILED(m_Diagnostics->GetIInspectableFromHandle(startHandle, &inspectable)) || !inspectable) + return false; + + StartInputRoute route; + route.startHandle = startHandle; + if (FAILED(inspectable->QueryInterface(__uuidof(ABI::Windows::UI::Xaml::IUIElement), + (void**)&route.startElement)) || !route.startElement) + return false; + if (FAILED(inspectable->QueryInterface(__uuidof(ABI::Windows::UI::Xaml::IFrameworkElement), + (void**)&route.startFramework)) || !route.startFramework) + return false; + if (!GetInputRoot(startHandle, &route.rootHandle, route.rootElement)) + return false; + route.taskbar = ResolveInputTaskbar(route.rootElement, &route.sourceHandle); + if (!route.taskbar) + { + LogToFile(STARTUP_LOG, L"Win11StartInput: unable to resolve taskbar HWND for handle %llu; keeping WH_MOUSE fallback", + (unsigned long long)startHandle); + return false; + } + + auto handler = Microsoft::WRL::Make(this, startHandle); + if (!handler) + return false; + HRESULT hr = handler->QueryInterface(IID_PPV_ARGS(&route.handler)); + if (FAILED(hr) || !route.handler) + return false; + + hr = route.rootElement->AddHandler(m_PointerEnteredEvent, route.handler, TRUE); + if (SUCCEEDED(hr)) + hr = route.rootElement->AddHandler(m_PointerMovedEvent, route.handler, TRUE); + if (SUCCEEDED(hr)) + hr = route.rootElement->AddHandler(m_PointerPressedEvent, route.handler, TRUE); + if (SUCCEEDED(hr)) + hr = route.rootElement->AddHandler(m_PointerReleasedEvent, route.handler, TRUE); + if (SUCCEEDED(hr)) + hr = route.rootElement->AddHandler(m_PointerExitedEvent, route.handler, TRUE); + if (FAILED(hr)) + { + route.rootElement->RemoveHandler(m_PointerEnteredEvent, route.handler); + route.rootElement->RemoveHandler(m_PointerMovedEvent, route.handler); + route.rootElement->RemoveHandler(m_PointerPressedEvent, route.handler); + route.rootElement->RemoveHandler(m_PointerReleasedEvent, route.handler); + route.rootElement->RemoveHandler(m_PointerExitedEvent, route.handler); + LogToFile(STARTUP_LOG, L"Win11StartInput: routed handler attach failed 0x%08X", hr); + return false; + } + + m_InputRoutes.push_back(route); + LogToFile(STARTUP_LOG, L"Win11StartInput: route attached for handle %llu via root %llu/source %llu", + (unsigned long long)startHandle, (unsigned long long)route.rootHandle, + (unsigned long long)route.sourceHandle); + return true; + } + + void DetachInputRoute( size_t index ) + { + if (index >= m_InputRoutes.size()) + return; + StartInputRoute &route = m_InputRoutes[index]; + + if (route.pointerInside && route.taskbar) + { + POINT point = {}; + GetCursorPos(&point); + PostInputMessage(route.taskbar, WM_MOUSELEAVE, point); + } + + if (route.rootElement && route.handler) + { + if (m_PointerEnteredEvent) route.rootElement->RemoveHandler(m_PointerEnteredEvent, route.handler); + if (m_PointerMovedEvent) route.rootElement->RemoveHandler(m_PointerMovedEvent, route.handler); + if (m_PointerPressedEvent) route.rootElement->RemoveHandler(m_PointerPressedEvent, route.handler); + if (m_PointerReleasedEvent) route.rootElement->RemoveHandler(m_PointerReleasedEvent, route.handler); + if (m_PointerExitedEvent) route.rootElement->RemoveHandler(m_PointerExitedEvent, route.handler); + } + m_InputRoutes.erase(m_InputRoutes.begin() + index); + } + + void DetachAllInputRoutes( void ) + { + while (!m_InputRoutes.empty()) + DetachInputRoute(m_InputRoutes.size() - 1); + m_ExpectedInputRoutes = 0; + } + + void SyncInputRoutes( const std::vector &targets ) + { + m_ExpectedInputRoutes = targets.size(); + + for (size_t i = 0; i < m_InputRoutes.size();) + { + bool keep = false; + for (size_t j = 0; j < targets.size(); j++) + if (targets[j] == m_InputRoutes[i].startHandle) + { + keep = true; + break; + } + + // Visual-tree removals are applied after the diagnostics callback + // unwinds. A root can disappear before its Start descendant is reported + // removed, so validate both handles before retaining the route. + if (keep) + { + EnterCriticalSection(&m_Lock); + keep = m_Elements.find(m_InputRoutes[i].startHandle) != m_Elements.end() && + m_Elements.find(m_InputRoutes[i].rootHandle) != m_Elements.end() && + m_Elements.find(m_InputRoutes[i].sourceHandle) != m_Elements.end(); + LeaveCriticalSection(&m_Lock); + } + + if (!keep) + DetachInputRoute(i); + else + i++; + } + + for (size_t i = 0; i < targets.size(); i++) + if (!FindInputRoute(targets[i])) + AttachInputRoute(targets[i]); + } + + void EvaluateInputBridgeState( void ) + { + if (!InterlockedCompareExchange(&g_StartButtonEnabled, 0, 0) || + !m_ExpectedInputRoutes || m_InputRoutes.size() != m_ExpectedInputRoutes) + { + ReportInputBridgeState(WIN11_START_INPUT_FALLBACK); + return; + } + + bool verified = true; + EnterCriticalSection(&m_Lock); + for (size_t i = 0; i < m_InputRoutes.size(); i++) + { + auto element = m_Elements.find(m_InputRoutes[i].startHandle); + if (element == m_Elements.end() || !element->second.hitTestOverride) + { + verified = false; + break; + } + if (!m_InputRoutes[i].verified) + verified = false; + } + LeaveCriticalSection(&m_Lock); + + ReportInputBridgeState(verified ? WIN11_START_INPUT_ACTIVE : WIN11_START_INPUT_PROBING); + } + static LRESULT CALLBACK DispatchProc( HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam ) { CWin11StartButtonTap *tap = (CWin11StartButtonTap*)GetWindowLongPtr(hwnd, GWLP_USERDATA); @@ -352,11 +1097,22 @@ class CWin11StartButtonTap: public IObjectWithSite, public IVisualTreeServiceCal tap = (CWin11StartButtonTap*)create->lpCreateParams; SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)tap); } + if (msg == WM_OS_STARTBUTTON_REPROBE && tap) + { + tap->AddRef(); + tap->ReprobeInputBridgeState(); + tap->Release(); + return 0; + } if (msg == WM_OS_STARTBUTTON_APPLY && tap) { + // GWLP_USERDATA is a raw pointer. Hold the TAP alive while ApplyState + // may detach the last routed-event handler that also references it. + tap->AddRef(); bool enabled = InterlockedCompareExchange(&g_StartButtonActive, 0, 0) != 0 && InterlockedCompareExchange(&g_StartButtonEnabled, 0, 0) != 0; tap->ApplyState(enabled); + tap->Release(); return 0; } return DefWindowProc(hwnd, msg, wParam, lParam); @@ -632,6 +1388,31 @@ class CWin11StartButtonTap: public IObjectWithSite, public IVisualTreeServiceCal const bool allTaskbars = InterlockedCompareExchange(&g_AllTaskbars, 0, 0) != 0; + size_t startControlCount = 0; + for (size_t i = 0; i < elements.size(); i++) + if (elements[i].second.isStartControl) + startControlCount++; + + // The fallback hook is thread-global. It can be retired only when every + // native Start slot has an Open-Shell replacement/input route. With + // multiple taskbars and AllTaskbars disabled, secondary native Start + // controls must remain hit-testable for touch/pen and keep the established + // WH_MOUSE fallback for mouse input. + const bool canRetireMouseHook = allTaskbars || startControlCount <= 1; + std::vector inputTargets; + if (enabled && canRetireMouseHook) + { + for (size_t i = 0; i < elements.size(); i++) + { + if (!elements[i].second.isStartControl) + continue; + InstanceHandle handle = elements[i].first; + if (allTaskbars || !primaryStart || handle == primaryStart) + inputTargets.push_back(handle); + } + } + SyncInputRoutes(inputTargets); + for (size_t i = 0; i < elements.size(); i++) { InstanceHandle handle = elements[i].first; @@ -694,6 +1475,8 @@ class CWin11StartButtonTap: public IObjectWithSite, public IVisualTreeServiceCal } } } + + EvaluateInputBridgeState(); } LONG m_Refs; @@ -702,11 +1485,41 @@ class CWin11StartButtonTap: public IObjectWithSite, public IVisualTreeServiceCal CRITICAL_SECTION m_Lock; CComPtr m_Site; CComPtr m_Visual; + CComPtr m_Diagnostics; + CComPtr m_UIElementStatics; + CComPtr m_PointerEnteredEvent; + CComPtr m_PointerMovedEvent; + CComPtr m_PointerPressedEvent; + CComPtr m_PointerReleasedEvent; + CComPtr m_PointerExitedEvent; InstanceHandle m_PrimaryStart; unsigned int m_NextDiscoveryOrder; + size_t m_ExpectedInputRoutes; + LONG m_LastInputState; std::unordered_map m_Elements; + std::unordered_set m_XamlSources; + std::vector m_InputRoutes; }; +CStartPointerHandler::CStartPointerHandler( CWin11StartButtonTap *owner, InstanceHandle startHandle ) + : m_Owner(owner), m_StartHandle(startHandle) +{ + if (m_Owner) + m_Owner->AddRef(); +} + +CStartPointerHandler::~CStartPointerHandler( void ) +{ + if (m_Owner) + m_Owner->Release(); +} + +HRESULT STDMETHODCALLTYPE CStartPointerHandler::Invoke( IInspectable *, + ABI::Windows::UI::Xaml::Input::IPointerRoutedEventArgs *args ) +{ + return m_Owner ? m_Owner->OnStartPointer(m_StartHandle, args) : S_OK; +} + static CWin11StartButtonTap *GetTapRef( void ) { CWin11StartButtonTap *tap = NULL; @@ -888,6 +1701,7 @@ extern "C" void StartWin11StartButtonTap( BOOL enabled, BOOL allTaskbars ) CWin11StartButtonTap *tap = GetTapRef(); if (tap) { + tap->RequestInputBridgeReprobe(); tap->RequestApply(false); tap->Release(); return; @@ -904,6 +1718,9 @@ extern "C" void UpdateWin11StartButtonTap( BOOL enabled, BOOL allTaskbars ) CWin11StartButtonTap *tap = GetTapRef(); if (tap) { + // Re-arm fallback/probing on every taskbar/settings update. Existing + // route verification may describe XAML handles from the previous layout. + tap->RequestInputBridgeReprobe(); tap->RequestApply(false); tap->Release(); }