From 6bc274286fb1036eb603d64f6118e241089ab968 Mon Sep 17 00:00:00 2001 From: reuk Date: Mon, 16 Jun 2025 16:59:23 +0100 Subject: [PATCH] Windows: Fix mouse state tracking when mouse leaves window 467f20a7a12df0 introduced a change to start processing WM_NCMOUSELEAVE messages as mouse-exit events. This behaviour is not quite correct, because NCMOUSELEAVE may be triggered when moving the cursor from the nonclient area to the client area, in which case the mouse is still over the window. We now check whether the mouse is really over the window inside doMouseExit(), and continue to track it if necessary. --- .../native/juce_Windowing_windows.cpp | 107 ++++++++++++------ 1 file changed, 72 insertions(+), 35 deletions(-) diff --git a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp index f43f9b428f..32d985bd0f 100644 --- a/modules/juce_gui_basics/native/juce_Windowing_windows.cpp +++ b/modules/juce_gui_basics/native/juce_Windowing_windows.cpp @@ -2686,41 +2686,14 @@ private: client, }; - std::optional doMouseMove (const LPARAM lParam, bool isMouseDownEvent, WindowArea area) + std::optional doMouseMoveAtPoint (bool isMouseDownEvent, WindowArea area, Point position) { - // Check if the mouse has moved since being pressed in the caption area. - // If it has, then we defer to DefWindowProc to handle the mouse movement. - // Allowing DefWindowProc to handle WM_NCLBUTTONDOWN directly will pause message - // processing (and therefore painting) when the mouse is clicked in the caption area, - // which is why we wait until the mouse is *moved* before asking the system to take over. - // Letting the system handle the move is important for things like Aero Snap to work. - if (area == WindowArea::nonclient && captionMouseDown.has_value() && *captionMouseDown != lParam) - { - captionMouseDown.reset(); - - // When clicking and dragging on the caption area, a new modal loop is started - // inside DefWindowProc. This modal loop appears to consume some mouse events, - // without forwarding them back to our own window proc. In particular, we never - // get to see the WM_NCLBUTTONUP event with the HTCAPTION argument, or any other - // kind of mouse-up event to signal that the loop exited, so - // ModifierKeys::currentModifiers gets left in the wrong state. As a workaround, we - // manually update the modifier keys after DefWindowProc exits, and update the - // capture state if necessary. - const auto result = DefWindowProc (hwnd, WM_NCLBUTTONDOWN, HTCAPTION, lParam); - getMouseModifiers(); - releaseCaptureIfNecessary(); - return result; - } - auto modsToSend = ModifierKeys::getCurrentModifiers(); // this will be handled by WM_TOUCH if (isTouchEvent() || areOtherTouchSourcesActive()) return {}; - const auto position = area == WindowArea::client ? getPointFromLocalLParam (lParam) - : getLocalPointFromScreenLParam (lParam); - if (! isMouseOver) { isMouseOver = true; @@ -2746,10 +2719,9 @@ private: if (area == WindowArea::client) Desktop::getInstance().getMainMouseSource().forceMouseCursorUpdate(); } - else if (! isDragging) + else if (! isDragging && ! contains (position.roundToInt(), false)) { - if (! contains (position.roundToInt(), false)) - return {}; + return {}; } static uint32 lastMouseTime = 0; @@ -2768,6 +2740,38 @@ private: return {}; } + std::optional doMouseMove (const LPARAM lParam, bool isMouseDownEvent, WindowArea area) + { + // Check if the mouse has moved since being pressed in the caption area. + // If it has, then we defer to DefWindowProc to handle the mouse movement. + // Allowing DefWindowProc to handle WM_NCLBUTTONDOWN directly will pause message + // processing (and therefore painting) when the mouse is clicked in the caption area, + // which is why we wait until the mouse is *moved* before asking the system to take over. + // Letting the system handle the move is important for things like Aero Snap to work. + if (area == WindowArea::nonclient && captionMouseDown.has_value() && *captionMouseDown != lParam) + { + captionMouseDown.reset(); + + // When clicking and dragging on the caption area, a new modal loop is started + // inside DefWindowProc. This modal loop appears to consume some mouse events, + // without forwarding them back to our own window proc. In particular, we never + // get to see the WM_NCLBUTTONUP event with the HTCAPTION argument, or any other + // kind of mouse-up event to signal that the loop exited, so + // ModifierKeys::currentModifiers gets left in the wrong state. As a workaround, we + // manually update the modifier keys after DefWindowProc exits, and update the + // capture state if necessary. + const auto result = DefWindowProc (hwnd, WM_NCLBUTTONDOWN, HTCAPTION, lParam); + getMouseModifiers(); + releaseCaptureIfNecessary(); + return result; + } + + const auto position = area == WindowArea::client ? getPointFromLocalLParam (lParam) + : getLocalPointFromScreenLParam (lParam); + + return doMouseMoveAtPoint (isMouseDownEvent, area, position); + } + void updateModifiersFromModProvider() const { #if JUCE_MODULE_AVAILABLE_juce_audio_plugin_client @@ -2843,17 +2847,50 @@ private: doMouseUp (getCurrentMousePos(), (WPARAM) 0, false); } - void doMouseExit() + /* The parameter specifies the area the cursor just left. */ + void doMouseExit (WindowArea area) { isMouseOver = false; - if (! areOtherTouchSourcesActive()) + const auto messagePos = GetMessagePos(); + + // If the system tells us that the mouse left an area, but the cursor is still over that + // area, respect the system's decision and treat this as a mouse-leave event. + // Starting a native drag-n-drop or dragging the window caption may cause the system to send + // a mouse-leave event while the mouse is still within the window's bounds. + const auto shouldRestartTracking = std::invoke ([&] + { + auto* peer = getOwnerOfWindow (WindowFromPoint (getPOINTFromLParam (messagePos))); + + if (peer != this) + return false; + + const auto newAreaNative = peerWindowProc (hwnd, WM_NCHITTEST, 0, messagePos); + + if (newAreaNative == HTNOWHERE || newAreaNative == HTTRANSPARENT) + return false; + + if (newAreaNative == HTCLIENT) + return area == WindowArea::nonclient; + + return area == WindowArea::client; + }); + + if (shouldRestartTracking) + { + doMouseMoveAtPoint (false, + area == WindowArea::client ? WindowArea::nonclient : WindowArea::client, + getLocalPointFromScreenLParam (messagePos)); + } + else if (! areOtherTouchSourcesActive()) + { doMouseEvent (getCurrentMousePos(), MouseInputSource::defaultPressure); + } } std::tuple> findPeerUnderMouse() { - auto currentMousePos = getPOINTFromLParam ((LPARAM) GetMessagePos()); + auto currentMousePos = getPOINTFromLParam (GetMessagePos()); auto* peer = getOwnerOfWindow (WindowFromPoint (currentMousePos)); @@ -3967,7 +4004,7 @@ private: case WM_POINTERLEAVE: case WM_NCMOUSELEAVE: case WM_MOUSELEAVE: - doMouseExit(); + doMouseExit (message == WM_NCMOUSELEAVE ? WindowArea::nonclient : WindowArea::client); return 0; case WM_LBUTTONDOWN: