Repository: microsoft/terminal (language: cpp)

Issue:
Mouse events are not sent when the mouse is outside the top of the WT
### Windows Terminal version

Versão: 1.20.11781.0

### Windows build number

10.0.22631.0

### Other Software

_No response_

### Steps to reproduce

Please clone this GitHub repository [MouseTests](https://github.com/BDisp/MouseTests.git) and run. Keep pressing the mouse inside the console and move around it, as the gif show.

### Expected Behavior

The mouse events should be sent while the mouse button is pressed and moving inside or outside the console until it's released.

### Actual Behavior

The mouse events are sent as expected except from outside of the console on top.

![MouseTests](https://github.com/user-attachments/assets/aa84e08d-a96f-43e2-a1ee-8ae199ba74d3)

With the `conhost` the mouse events are always sent from anywhere while the mouse isn't released.


Discussion / hints:
Yep, that's super weird alright!

Our theory is that we're getting into the mouse-select-drag-scroll state, and attempting to "scroll" the terminal, and suppressing the mouse event
Yep, I already noted that and that why I made my example writing always on the same line to avoiding scrolling. I noted that moving outside the console ws causing to scroll. This would be valid if I was pressed the scroll bar but that wasn't the case. Maybe acceptable only if the scroll bar is visible. Thanks.

Fix patch:
diff --git a/src/cascadia/TerminalControl/ControlInteractivity.cpp b/src/cascadia/TerminalControl/ControlInteractivity.cpp
index 7b59babd2e5..f18851c2fbb 100644
--- a/src/cascadia/TerminalControl/ControlInteractivity.cpp
+++ b/src/cascadia/TerminalControl/ControlInteractivity.cpp
@@ -329,7 +329,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         _touchAnchor = contactPoint;
     }
 
-    void ControlInteractivity::PointerMoved(Control::MouseButtonState buttonState,
+    bool ControlInteractivity::PointerMoved(Control::MouseButtonState buttonState,
                                             const unsigned int pointerUpdateKind,
                                             const ::Microsoft::Terminal::Core::ControlKeyStates modifiers,
                                             const bool focused,
@@ -337,11 +337,14 @@ namespace winrt::Microsoft::Terminal::Control::implementation
                                             const bool pointerPressedInBounds)
     {
         const auto terminalPosition = _getTerminalPosition(til::point{ pixelPosition });
+        // Returning true from this function indicates that the caller should do no further processing of this movement.
+        bool handledCompletely = false;
 
         // Short-circuit isReadOnly check to avoid warning dialog
         if (focused && !_core->IsInReadOnlyMode() && _canSendVTMouseInput(modifiers))
         {
             _sendMouseEventHelper(terminalPosition, pointerUpdateKind, modifiers, 0, buttonState);
+            handledCompletely = true;
         }
         // GH#4603 - don't modify the selection if the pointer press didn't
         // actually start _in_ the control bounds. Case in point - someone drags
@@ -378,6 +381,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         }
 
         _core->SetHoveredCell(terminalPosition.to_core_point());
+        return handledCompletely;
     }
 
     void ControlInteractivity::TouchMoved(const Core::Point newTouchPoint,
@@ -682,13 +686,10 @@ namespace winrt::Microsoft::Terminal::Control::implementation
                                                      const SHORT wheelDelta,
                                                      Control::MouseButtonState buttonState)
     {
-        const auto adjustment = _core->ScrollOffset() > 0 ? _core->BufferHeight() - _core->ScrollOffset() - _core->ViewHeight() : 0;
-        // If the click happened outside the active region, just don't send any mouse event
-        if (const auto adjustedY = terminalPosition.y - adjustment; adjustedY >= 0)
-        {
-            return _core->SendMouseEvent({ terminalPosition.x, adjustedY }, pointerUpdateKind, modifiers, wheelDelta, toInternalMouseState(buttonState));
-        }
-        return false;
+        const auto adjustment = _core->BufferHeight() - _core->ScrollOffset() - _core->ViewHeight();
+        // If the click happened outside the active region, core should get a chance to filter it out or clamp it.
+        const auto adjustedY = terminalPosition.y - adjustment;
+        return _core->SendMouseEvent({ terminalPosition.x, adjustedY }, pointerUpdateKind, modifiers, wheelDelta, toInternalMouseState(buttonState));
     }
 
     // Method Description:
diff --git a/src/cascadia/TerminalControl/ControlInteractivity.h b/src/cascadia/TerminalControl/ControlInteractivity.h
index 38789827ec3..22eaa95c397 100644
--- a/src/cascadia/TerminalControl/ControlInteractivity.h
+++ b/src/cascadia/TerminalControl/ControlInteractivity.h
@@ -58,7 +58,7 @@ namespace winrt::Microsoft::Terminal::Control::implementation
                             const Core::Point pixelPosition);
         void TouchPressed(const Core::Point contactPoint);
 
-        void PointerMoved(Control::MouseButtonState buttonState,
+        bool PointerMoved(Control::MouseButtonState buttonState,
                           const unsigned int pointerUpdateKind,
                           const ::Microsoft::Terminal::Core::ControlKeyStates modifiers,
                           const bool focused,
diff --git a/src/cascadia/TerminalControl/ControlInteractivity.idl b/src/cascadia/TerminalControl/ControlInteractivity.idl
index d1ca9720dc6..926c1e3b0a9 100644
--- a/src/cascadia/TerminalControl/ControlInteractivity.idl
+++ b/src/cascadia/TerminalControl/ControlInteractivity.idl
@@ -43,12 +43,12 @@ namespace Microsoft.Terminal.Control
                             Microsoft.Terminal.Core.Point pixelPosition);
         void TouchPressed(Microsoft.Terminal.Core.Point contactPoint);
 
-        void PointerMoved(MouseButtonState buttonState,
-                          UInt32 pointerUpdateKind,
-                          Microsoft.Terminal.Core.ControlKeyStates modifiers,
-                          Boolean focused,
-                          Microsoft.Terminal.Core.Point pixelPosition,
-                          Boolean pointerPressedInBounds);
+        Boolean PointerMoved(MouseButtonState buttonState,
+                             UInt32 pointerUpdateKind,
+                             Microsoft.Terminal.Core.ControlKeyStates modifiers,
+                             Boolean focused,
+                             Microsoft.Terminal.Core.Point pixelPosition,
+                             Boolean pointerPressedInBounds);
 
         void TouchMoved(Microsoft.Terminal.Core.Point newTouchPoint,
                         Boolean focused);
diff --git a/src/cascadia/TerminalControl/TermControl.cpp b/src/cascadia/TerminalControl/TermControl.cpp
index 045762a3ac0..f6874c35e75 100644
--- a/src/cascadia/TerminalControl/TermControl.cpp
+++ b/src/cascadia/TerminalControl/TermControl.cpp
@@ -1910,18 +1910,18 @@ namespace winrt::Microsoft::Terminal::Control::implementation
         if (type == Windows::Devices::Input::PointerDeviceType::Mouse ||
             type == Windows::Devices::Input::PointerDeviceType::Pen)
         {
-            _interactivity.PointerMoved(TermControl::GetPressedMouseButtons(point),
-                                        TermControl::GetPointerUpdateKind(point),
-                                        ControlKeyStates(args.KeyModifiers()),
-                                        _focused,
-                                        pixelPosition.to_core_point(),
-                                        _pointerPressedInBounds);
+            auto suppressFurtherHandling = _interactivity.PointerMoved(TermControl::GetPressedMouseButtons(point),
+                                                                       TermControl::GetPointerUpdateKind(point),
+                                                                       ControlKeyStates(args.KeyModifiers()),
+                                                                       _focused,
+                                                                       pixelPosition.to_core_point(),
+                                                                       _pointerPressedInBounds);
 
             // GH#9109 - Only start an auto-scroll when the drag actually
             // started within our bounds. Otherwise, someone could start a drag
             // outside the terminal control, drag into the padding, and trick us
             // into starting to scroll.
-            if (_focused && _pointerPressedInBounds && point.Properties().IsLeftButtonPressed())
+            if (!suppressFurtherHandling && _focused && _pointerPressedInBounds && point.Properties().IsLeftButtonPressed())
             {
                 // We want to find the distance relative to the bounds of the
                 // SwapChainPanel, not the entire control. If they drag out of
diff --git a/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp b/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp
index a5d443b915a..49fcd6d21eb 100644
--- a/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp
+++ b/src/cascadia/UnitTests_Control/ControlInteractivityTests.cpp
@@ -998,16 +998,15 @@ namespace ControlUnitTests
         VERIFY_IS_GREATER_THAN(core->ScrollOffset(), 0);
 
         // Viewport is now above the mutable viewport, so the mouse event
-        // straight up won't be sent to the terminal.
+        // will be clamped to the top line.
 
-        expectedOutput.push_back(L"sentinel"); // Clearly, it won't be this string
+        expectedOutput.push_back(L"\x1b[M &!"); // 5, 1
         interactivity->PointerPressed(leftMouseDown,
                                       WM_LBUTTONDOWN, //pointerUpdateKind
                                       0, // timestamp
                                       modifiers,
                                       cursorPosition0.to_core_point());
         // Flush it out.
-        conn->WriteInput(winrt_wstring_to_array_view(L"sentinel"));
         VERIFY_ARE_EQUAL(0u, expectedOutput.size(), L"Validate we drained all the expected output");
 
         // This is the part as mentioned in GH#12719


Test patch: