Skip to content

Commit ed6b5c9

Browse files
committed
Add scrollbar and text selection detection to prevent unwanted mouse bridging
- Detect native Windows scrollbars via WM_NCHITTEST (HTVSCROLL) - Detect text selection in edit controls (Edit, RichEdit, TextBox, Scintilla) - Detect modern overlay scrollbars by checking proximity to window's right edge - Prevent mouse teleportation during scrollbar drag and text selection operations
1 parent e7d6250 commit ed6b5c9

1 file changed

Lines changed: 126 additions & 10 deletions

File tree

MouseTrap/src/Service/MouseBridgeService.cs

Lines changed: 126 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,119 @@
1+
using System.Runtime.InteropServices;
12
using System.ComponentModel;
23
using MouseTrap.Models;
34
using MouseTrap.Native;
45

5-
66
namespace MouseTrap.Service;
77

88
public class MouseBridgeService : IService {
99
private ScreenConfigCollection _screens;
1010

11+
private bool _wasMouseDown = false;
12+
private bool _suppressBridge = false;
13+
14+
[DllImport("user32.dll")]
15+
private static extern short GetAsyncKeyState(int vKey);
16+
17+
[DllImport("user32.dll")]
18+
private static extern IntPtr WindowFromPoint(Point pt);
19+
20+
[DllImport("user32.dll")]
21+
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
22+
23+
[DllImport("user32.dll")]
24+
private static extern int GetClassName(IntPtr hWnd, System.Text.StringBuilder lpClassName, int nMaxCount);
25+
26+
[DllImport("user32.dll")]
27+
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
28+
29+
[DllImport("user32.dll")]
30+
private static extern bool GetCursorInfo(ref CURSORINFO pci);
31+
32+
[DllImport("user32.dll")]
33+
private static extern IntPtr LoadCursor(IntPtr hInstance, int lpCursorName);
34+
35+
[StructLayout(LayoutKind.Sequential)]
36+
private struct RECT
37+
{
38+
public int Left;
39+
public int Top;
40+
public int Right;
41+
public int Bottom;
42+
}
43+
44+
[StructLayout(LayoutKind.Sequential)]
45+
private struct CURSORINFO
46+
{
47+
public int cbSize;
48+
public int flags;
49+
public IntPtr hCursor;
50+
public Point ptScreenPos;
51+
}
52+
53+
private const int WM_NCHITTEST = 0x84;
54+
private const int HTVSCROLL = 7;
55+
private const int IDC_IBEAM = 32513;
56+
57+
private bool IsLeftMouseDown()
58+
{
59+
return (GetAsyncKeyState(0x01) & 0x8000) != 0;
60+
}
61+
62+
private bool IsIBeamCursor()
63+
{
64+
var ci = new CURSORINFO();
65+
ci.cbSize = Marshal.SizeOf(ci);
66+
if (!GetCursorInfo(ref ci))
67+
return false;
68+
69+
IntPtr ibeam = LoadCursor(IntPtr.Zero, IDC_IBEAM);
70+
return ibeam != IntPtr.Zero && ci.hCursor == ibeam;
71+
}
72+
73+
private bool ShouldSuppressBridgeOnDrag(Point pos)
74+
{
75+
var hwnd = WindowFromPoint(pos);
76+
if (hwnd == IntPtr.Zero)
77+
return false;
78+
79+
// 1. Check for native/classic scrollbar via hit test
80+
int lParam = (pos.Y << 16) | (pos.X & 0xFFFF);
81+
var hitTest = (int)SendMessage(hwnd, WM_NCHITTEST, IntPtr.Zero, (IntPtr)lParam);
82+
if (hitTest == HTVSCROLL)
83+
return true;
84+
85+
// 2. Check window class - if it's a text control, suppress on any drag
86+
var className = new System.Text.StringBuilder(256);
87+
if (GetClassName(hwnd, className, className.Capacity) > 0)
88+
{
89+
string cn = className.ToString();
90+
if (cn == "Edit" ||
91+
cn.StartsWith("RichEdit") ||
92+
cn == "Scintilla" ||
93+
cn == "WindowsForms10.EDIT.app.0" ||
94+
cn == "Chrome_RenderWidgetHostHWND" ||
95+
cn == "MozillaWindowClass")
96+
{
97+
return true;
98+
}
99+
}
100+
101+
// 3. Right-edge heuristic for custom scrollbars
102+
if (GetWindowRect(hwnd, out RECT windowRect))
103+
{
104+
int distanceFromRight = windowRect.Right - pos.X;
105+
if (distanceFromRight < 50 && distanceFromRight > 0)
106+
return true;
107+
}
108+
109+
// 4. Cursor shape fallback — catches CMD, PuTTY, and anything else
110+
// with an I-beam that we didn't explicitly enumerate
111+
if (IsIBeamCursor())
112+
return true;
113+
114+
return false;
115+
}
116+
11117
public MouseBridgeService()
12118
{
13119
_screens = ScreenConfigCollection.Load();
@@ -52,18 +158,35 @@ public void OnExit()
52158
MouseTrapClear();
53159
}
54160

55-
56161
private void Loop(CancellationToken token)
57162
{
58163
while (!token.IsCancellationRequested) {
59-
// on win-logon etc..
60164
if (!Mouse.IsInputDesktop()) {
61165
MouseTrapClear();
62166
Thread.Sleep(1);
63167
continue;
64168
}
65169

66170
var position = GetPosition();
171+
var isDown = IsLeftMouseDown();
172+
173+
if (isDown && !_wasMouseDown)
174+
{
175+
_suppressBridge = ShouldSuppressBridgeOnDrag(position);
176+
}
177+
178+
if (!isDown)
179+
{
180+
_suppressBridge = false;
181+
}
182+
183+
_wasMouseDown = isDown;
184+
185+
if (_suppressBridge)
186+
{
187+
Thread.Sleep(1);
188+
continue;
189+
}
67190

68191
var current = _screens.FirstOrDefault(_ => _.Bounds.Contains(position));
69192
if (current != null && current.HasBridges) {
@@ -101,7 +224,6 @@ private void Loop(CancellationToken token)
101224
}
102225
}
103226

104-
105227
// ^
106228
hotspace = current.TopHotSpace;
107229
if (direction.HasFlag(Direction.ToTop) && hotspace.Contains(position)) {
@@ -137,7 +259,6 @@ private void Loop(CancellationToken token)
137259
}
138260
}
139261

140-
141262
private Point GetPosition()
142263
{
143264
if (!Mouse.TryGetPosition(out var pos)) {
@@ -147,7 +268,6 @@ private Point GetPosition()
147268
return pos;
148269
}
149270

150-
151271
private int _posOldx;
152272
private int _posOldy;
153273

@@ -156,25 +276,21 @@ private Direction GetDirection(in Point pos)
156276
var ret = Direction.None;
157277
if (_posOldx < pos.X) {
158278
_posOldx = pos.X;
159-
160279
ret |= Direction.ToRight;
161280
}
162281

163282
if (_posOldx > pos.X) {
164283
_posOldx = pos.X;
165-
166284
ret |= Direction.ToLeft;
167285
}
168286

169287
if (_posOldy < pos.Y) {
170288
_posOldy = pos.Y;
171-
172289
ret |= Direction.ToBottom;
173290
}
174291

175292
if (_posOldy > pos.Y) {
176293
_posOldy = pos.Y;
177-
178294
ret |= Direction.ToTop;
179295
}
180296

0 commit comments

Comments
 (0)