A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System;
5using System.Drawing;
6using osu.Framework.Extensions.EnumExtensions;
7using osu.Framework.Input.Handlers.Mouse;
8using osu.Framework.Input.StateChanges;
9using osu.Framework.Platform.Windows.Native;
10using osuTK;
11using SDL2;
12
13// ReSharper disable UnusedParameter.Local (Class regularly handles native events where we don't consume all parameters)
14
15namespace osu.Framework.Platform.Windows
16{
17 /// <summary>
18 /// A windows specific mouse input handler which overrides the SDL2 implementation of raw input.
19 /// This is done to better handle quirks of some devices.
20 /// </summary>
21 internal unsafe class WindowsMouseHandler : MouseHandler
22 {
23 private const int raw_input_coordinate_space = 65535;
24
25 private SDL.SDL_WindowsMessageHook callback;
26 private SDL2DesktopWindow window;
27
28 public override bool IsActive => Enabled.Value;
29
30 public override bool Initialize(GameHost host)
31 {
32 if (!(host.Window is SDL2DesktopWindow desktopWindow))
33 return false;
34
35 window = desktopWindow;
36 callback = (ptr, wnd, u, param, l) => onWndProc(ptr, wnd, u, param, l);
37
38 Enabled.BindValueChanged(enabled =>
39 {
40 host.InputThread.Scheduler.Add(() => SDL.SDL_SetWindowsMessageHook(enabled.NewValue ? callback : null, IntPtr.Zero));
41 }, true);
42
43 return base.Initialize(host);
44 }
45
46 protected override void HandleMouseMoveRelative(Vector2 delta)
47 {
48 // handled via custom logic below.
49 }
50
51 private IntPtr onWndProc(IntPtr userData, IntPtr hWnd, uint message, ulong wParam, long lParam)
52 {
53 if (!Enabled.Value)
54 return IntPtr.Zero;
55
56 if (message != Native.Input.WM_INPUT)
57 return IntPtr.Zero;
58
59 int payloadSize = sizeof(RawInputData);
60
61 Native.Input.GetRawInputData((IntPtr)lParam, RawInputCommand.Input, out var data, ref payloadSize, sizeof(RawInputHeader));
62
63 if (data.Header.Type != RawInputType.Mouse)
64 return IntPtr.Zero;
65
66 var mouse = data.Mouse;
67
68 //TODO: this isn't correct.
69 if (mouse.ExtraInformation > 0)
70 {
71 // i'm not sure if there is a valid case where we need to handle packets with this present
72 // but the osu!tablet fires noise events with non-zero values, which we want to ignore.
73 // return IntPtr.Zero;
74 }
75
76 var position = new Vector2(mouse.LastX, mouse.LastY);
77 float sensitivity = (float)Sensitivity.Value;
78
79 if (mouse.Flags.HasFlagFast(RawMouseFlags.MoveAbsolute))
80 {
81 var screenRect = mouse.Flags.HasFlagFast(RawMouseFlags.VirtualDesktop) ? Native.Input.VirtualScreenRect : new Rectangle(window.Position, window.ClientSize);
82
83 Vector2 screenSize = new Vector2(screenRect.Width, screenRect.Height);
84
85 if (mouse.LastX == 0 && mouse.LastY == 0)
86 {
87 // not sure if this is the case for all tablets, but on osu!tablet these can appear and are noise.
88 return IntPtr.Zero;
89 }
90
91 // i am not sure what this 64 flag is, but it's set on the osu!tablet at very least.
92 // using it here as a method of determining where the coordinate space is incorrect.
93 if (((int)mouse.Flags & 64) == 0)
94 {
95 position /= raw_input_coordinate_space;
96 position *= screenSize;
97 }
98
99 if (Sensitivity.Value != 1)
100 {
101 // apply absolute sensitivity adjustment from the centre of the screen area.
102 Vector2 halfScreenSize = (screenSize / 2);
103
104 position -= halfScreenSize;
105 position *= (float)Sensitivity.Value;
106 position += halfScreenSize;
107 }
108
109 // map from screen to client coordinate space.
110 // not using Window's PointToClient implementation to keep floating point precision here.
111 position -= new Vector2(window.Position.X, window.Position.Y);
112 position *= window.Scale;
113
114 PendingInputs.Enqueue(new MousePositionAbsoluteInput { Position = position });
115 }
116 else
117 {
118 PendingInputs.Enqueue(new MousePositionRelativeInput { Delta = new Vector2(mouse.LastX, mouse.LastY) * sensitivity });
119 }
120
121 return IntPtr.Zero;
122 }
123 }
124}