1 /++
2  + Authors: Avaxar <avaxar@nekkl.org>
3  + Copyright: Copyright © 2023, Avaxar
4  + License: $(LINK2 https://mit-license.org, MIT License)
5  +/
6 
7 module dsdl2.mouse;
8 @safe:
9 
10 import bindbc.sdl;
11 import dsdl2.sdl;
12 import dsdl2.surface;
13 import dsdl2.window;
14 
15 import core.memory : GC;
16 import std.bitmanip : bitfields;
17 import std.format : format;
18 
19 /++
20  + Wraps `SDL_GetMouseFocus` which gets the mouse-focused window
21  +
22  + This function is marked as `@system` due to the potential of referencing invalid memory.
23  +
24  + Returns: `dsdl2.Window` proxy of the window with the mouse focus; `null` if no window is focused
25  +/
26 Window getMouseFocusedWindow() @system {
27     SDL_Window* sdlWindow = SDL_GetMouseFocus();
28     if (sdlWindow is null) {
29         return null;
30     }
31 
32     return new Window(sdlWindow, false);
33 }
34 
35 /++
36  + Wraps `SDL_GetMouseState` which gets the states of the mouse buttons
37  +
38  + Returns: `dsdl2.MouseState` specifying the pressed-states of the mouse buttons
39  +/
40 MouseState getMouseState() @trusted {
41     return MouseState(SDL_GetMouseState(null, null));
42 }
43 
44 /++
45  + Wraps `SDL_GetMouseState` which gets the mouse position relative to a focused window
46  +
47  + Returns: coordinate of the mouse in the focused window
48  +/
49 int[2] getMousePosition() @trusted {
50     int[2] pos = void;
51     SDL_GetMouseState(&pos[0], &pos[1]);
52     return pos;
53 }
54 
55 /++
56  + Wraps `SDL_WarpMouseInWindow` which sets the mouse position relative to a focused window
57  +
58  + Params:
59  +   newPosition = new coordinate of the mouse in the focused window
60  +/
61 void setMousePosition(int[2] newPosition) @trusted {
62     SDL_Window* window = SDL_GetMouseFocus();
63     if (window !is null) {
64         SDL_WarpMouseInWindow(window, newPosition[0], newPosition[1]);
65     }
66 }
67 
68 /++
69  + Wraps `SDL_GetRelativeMouseMode` which checks whether relative mouse mode is enabled or not
70  +
71  + Returns: `true` if relative mouse mode is enabled, otherwise `false`
72  +/
73 bool getRelativeMouseMode() @trusted {
74     return SDL_GetRelativeMouseMode() == SDL_TRUE;
75 }
76 
77 /++
78  + Wraps `SDL_SetRelativeMouseMode` which sets relative mouse mode
79  +
80  + Params:
81  +   newRelativeMouseMode = `true` to enable relative mouse mode, otherwise `false`
82  + Throws: `dsdl2.SDLException` if failed to toggle relative mouse mode
83  +/
84 void setRelativeMouseMode(bool newRelativeMouseMode) @trusted {
85     if (SDL_SetRelativeMouseMode(newRelativeMouseMode) != 0) {
86         throw new SDLException;
87     }
88 }
89 
90 /++
91  + Wraps `SDL_GetRelativeMouseState` which gets the relative states of the mouse buttons
92  +
93  + Returns: `dsdl2.MouseState` specifying the relative pressed-states of the mouse buttons
94  +/
95 MouseState getRelativeMouseState() @trusted {
96     return MouseState(SDL_GetRelativeMouseState(null, null));
97 }
98 
99 /++
100  + Wraps `SDL_GetRelativeMouseState` which gets the relative mouse position
101  +
102  + Returns: relative coordinate of the mouse
103  +/
104 int[2] getRelativeMousePosition() @trusted {
105     int[2] pos = void;
106     SDL_GetRelativeMouseState(&pos[0], &pos[1]);
107     return pos;
108 }
109 
110 static if (sdlSupport >= SDLSupport.v2_0_4) {
111     /++
112      + Wraps `SDL_CaptureMouse` (from SDL 2.0.4) which sets mouse capture
113      +
114      + Params:
115      +   newMouseCapture = `true` to enable mouse capture, otherwise `false`
116      + Throws: `dsdl2.SDLException` if failed to toggle mouse capture
117      +/
118     void setMouseCapture(bool newMouseCapture) @trusted
119     in {
120         assert(getVersion() >= Version(2, 0, 4));
121     }
122     do {
123         if (SDL_CaptureMouse(newMouseCapture) != 0) {
124             throw new SDLException;
125         }
126     }
127 
128     /++
129      + Wraps `SDL_GetGlobalMouseState` (from SDL 2.0.4) which gets the global states of the mouse buttons
130      +
131      + Returns: `dsdl2.MouseState` specifying the global pressed-states of the mouse buttons
132      +/
133     MouseState getGlobalMouseState() @trusted
134     in {
135         assert(getVersion() >= Version(2, 0, 4));
136     }
137     do {
138         return MouseState(SDL_GetGlobalMouseState(null, null));
139     }
140 
141     /++
142      + Wraps `SDL_GetGlobalMouseState` (from SDL 2.0.4) which gets the mouse position globally
143      +
144      + Returns: coordinate of the mouse in the display
145      +/
146     int[2] getGlobalMousePosition() @trusted
147     in {
148         assert(getVersion() >= Version(2, 0, 4));
149     }
150     do {
151         int[2] pos = void;
152         SDL_GetGlobalMouseState(&pos[0], &pos[1]);
153         return pos;
154     }
155 
156     /++
157      + Wraps `SDL_WarpMouseGlobal` (from SDL 2.0.4) which sets the mouse position globally in the display
158      +
159      + Params:
160      +   newPosition = new coordinate of the mouse in the display
161      +/
162     void setGlobalMousePosition(int[2] newPosition) @trusted
163     in {
164         assert(getVersion() >= Version(2, 0, 4));
165     }
166     do {
167         SDL_WarpMouseGlobal(newPosition[0], newPosition[1]);
168     }
169 }
170 
171 /++
172  + D enum that wraps `SDL_BUTTON_*` enumerations
173  +/
174 enum MouseButton {
175     /++
176      + Wraps `SDL_BUTTON_*` enumeration constants
177      +/
178     left = SDL_BUTTON_LEFT,
179     middle = SDL_BUTTON_MIDDLE, /// ditto
180     right = SDL_BUTTON_RIGHT, /// ditto
181     x1 = SDL_BUTTON_X1, /// ditto
182     x2 = SDL_BUTTON_X2
183 }
184 
185 static if (sdlSupport >= SDLSupport.v2_0_4) {
186     /++
187      + D enum that wraps `SDL_MOUSEWHEEL_*` enumerations (from SDL 2.0.4)
188      +/
189     enum MouseWheel {
190         /++
191          + Wraps `SDL_MOUSEWHEEL_*` enumeration constants
192          +/
193         normal = SDL_MOUSEWHEEL_NORMAL,
194         flipped = SDL_MOUSEWHEEL_FLIPPED /// ditto
195     }
196 }
197 
198 /++
199  + D struct that encapsulates mouse button state flags
200  +/
201 struct MouseState {
202     mixin(bitfields!(
203             bool, "left", 1,
204             bool, "middle", 1,
205             bool, "right", 1,
206             bool, "x1", 1,
207             bool, "x2", 1,
208             bool, "", 3));
209 
210     this() @disable;
211 
212     /++
213      + Constructs a `dsdl2.MouseState` from a vanilla SDL mouse state flag
214      +
215      + Params:
216      +   sdlMouseState = the `uint` flag
217      +/
218     this(uint sdlMouseState) {
219         this.left = (sdlMouseState & SDL_BUTTON_LMASK) != 0;
220         this.middle = (sdlMouseState & SDL_BUTTON_MMASK) != 0;
221         this.right = (sdlMouseState & SDL_BUTTON_RMASK) != 0;
222         this.x1 = (sdlMouseState & SDL_BUTTON_X1MASK) != 0;
223         this.x2 = (sdlMouseState & SDL_BUTTON_X2MASK) != 0;
224     }
225 
226     /++
227      + Constructs a `dsdl2.MouseState` by providing the flags
228      +
229      + Params:
230      +   base = base flag to assign (`0` for none)
231      +   left = whether the left mouse button is pressed
232      +   middle = whether the middle mouse button is pressed
233      +   right = whether the right mouse button is pressed
234      +   x1 = whether the X1 mouse button is pressed
235      +   x2 = whether the X2 mouse button is pressed
236      +/
237     this(uint base, bool left = false, bool middle = false, bool right = false, bool x1 = false, bool x2 = false) {
238         this(base);
239         this.left = left;
240         this.middle = middle;
241         this.right = right;
242         this.x1 = x1;
243         this.x2 = x2;
244     }
245 
246     /++
247      + Formats the `dsdl2.MouseState` into its construction representation:
248      + `"dsdl2.MouseState(<sdlMouseState>, <left>, <middle>, <right>, <x1>, <x2>)"`
249      +
250      + Returns: the formatted `string`
251      +/
252     string toString() const {
253         return "dsdl2.MouseState(%d, %s, %s, %s, %s, %s)".format(this.sdlMouseState, this.left, this.middle,
254             this.right, this.x1, this.x2);
255     }
256 
257     /++
258      + Gets the internal flag representation
259      +
260      + Returns: `uint` with the appropriate bitflags toggled
261      +/
262     uint sdlMouseState() const @property {
263         return (this.left ? SDL_BUTTON_LMASK : 0)
264             | (this.middle ? SDL_BUTTON_MMASK : 0)
265             | (this.right ? SDL_BUTTON_RMASK
266                     : 0)
267             | (this.x1 ? SDL_BUTTON_X1MASK : 0)
268             | (this.x2 ? SDL_BUTTON_X2MASK : 0);
269     }
270 }
271 
272 /++
273  + Wraps `SDL_GetCursor` which gets the current set cursor
274  +
275  + Returns: `dsdl2.Cursor` proxy of the set cursor
276  +/
277 Cursor getCursor() @trusted {
278     return new Cursor(SDL_GetCursor(), false);
279 }
280 
281 /++
282  + Wraps `SDL_GetDefaultCursor` which gets the default cursor
283  +
284  + Returns: `dsdl2.Cursor` proxy of the default cursor
285  +/
286 Cursor getDefaultCursor() @trusted {
287     return new Cursor(SDL_GetDefaultCursor(), false);
288 }
289 
290 /++
291  + Wraps `SDL_ShowCursor` which sets the visibility of the cursor
292  +
293  + Returns: `true` if cursor is visible, otherwise `false`
294  +/
295 bool getCursorVisibility() @trusted {
296     return SDL_ShowCursor(SDL_QUERY) != SDL_ENABLE;
297 }
298 
299 /++
300  + Wraps `SDL_ShowCursor` which sets the visibility of the cursor
301  +
302  + Params:
303  +   visible = `true` to make cursor visible, otherwise `false`
304  + Throws: `dsdl2.SDLException` if failed to set cursor visibility
305  +/
306 void setCursorVisibility(bool visible) @trusted {
307     if (SDL_ShowCursor(visible ? SDL_ENABLE : SDL_DISABLE) != 0) {
308         throw new SDLException;
309     }
310 }
311 
312 /++
313  + D class that wraps `SDL_Cursor` which sets cursor appearance
314  +/
315 final class Cursor {
316     static Cursor _multiton(SDL_SystemCursor sdlSystemCursor)() @trusted {
317         static Cursor cursor = null;
318         if (cursor is null) {
319             SDL_Cursor* sdlCursor = SDL_CreateSystemCursor(sdlSystemCursor);
320             if (sdlCursor is null) {
321                 throw new SDLException;
322             }
323 
324             cursor = new Cursor(sdlCursor);
325         }
326 
327         return cursor;
328     }
329 
330     /++
331      + Retrieves one of the `dsdl2.Cursor` system cursor presets from `SDL_SYSTEM_CURSOR_*` enumeration constants
332      +/
333     static alias systemArrow = _multiton!SDL_SYSTEM_CURSOR_ARROW; /// dutto
334     static alias systemIBeam = _multiton!SDL_SYSTEM_CURSOR_IBEAM; /// ditto
335     static alias systemWait = _multiton!SDL_SYSTEM_CURSOR_WAIT; /// ditto
336     static alias systemCrosshair = _multiton!SDL_SYSTEM_CURSOR_CROSSHAIR; /// ditto
337     static alias systemWaitArrow = _multiton!SDL_SYSTEM_CURSOR_WAITARROW; /// ditto
338     static alias systemSizeNWSE = _multiton!SDL_SYSTEM_CURSOR_SIZENWSE; /// ditto
339     static alias systemSizeNESW = _multiton!SDL_SYSTEM_CURSOR_SIZENESW; /// ditto
340     static alias systemSizeWE = _multiton!SDL_SYSTEM_CURSOR_SIZEWE; /// ditto
341     static alias systemSizeNS = _multiton!SDL_SYSTEM_CURSOR_SIZENS; /// ditto
342     static alias systemSizeAll = _multiton!SDL_SYSTEM_CURSOR_SIZEALL; /// ditto
343     static alias systemNo = _multiton!SDL_SYSTEM_CURSOR_NO; /// ditto
344     static alias systemHand = _multiton!SDL_SYSTEM_CURSOR_HAND; /// ditto
345 
346     private bool isOwner = true;
347     private void* userRef = null;
348 
349     @system SDL_Cursor* sdlCursor = null; /// Internal `SDL_Cursor` pointer
350 
351     /++
352      + Constructs a `dsdl2.Cursor` from a vanilla `SDL_Cursor*` from bindbc-sdl
353      +
354      + Params:
355      +   sdlCursor = the `SDL_Cursor` pointer to manage
356      +   isOwner = whether the instance owns the given `SDL_Cursor*` and should destroy it on its own
357      +   userRef = optional pointer to maintain reference link, avoiding GC cleanup
358      +/
359     this(SDL_Cursor* sdlCursor, bool isOwner = true, void* userRef = null) @system
360     in {
361         assert(sdlCursor !is null);
362     }
363     do {
364         this.sdlCursor = sdlCursor;
365         this.isOwner = isOwner;
366         this.userRef = userRef;
367     }
368 
369     /++
370      + Constructs a `dsdl2.Cursor` from a `dsdl2.Surface`, which wraps `SDL_CreateColorCursor`
371      +
372      + Params:
373      +   surface = surface image of the cursor
374      +   hotPosition = pixel position of the cursor hotspot
375      + Throws: `dsdl2.Exception` if cursor creation failed
376      +/
377     this(Surface surface, uint[2] hotPosition) @trusted {
378         this.sdlCursor = SDL_CreateColorCursor(surface.sdlSurface, hotPosition[0], hotPosition[1]);
379         if (this.sdlCursor is null) {
380             throw new SDLException;
381         }
382     }
383 
384     ~this() @trusted {
385         if (this.isOwner) {
386             SDL_FreeCursor(this.sdlCursor);
387         }
388     }
389 
390     @trusted invariant { // @suppress(dscanner.trust_too_much)
391         // Instance might be in an invalid state due to holding a non-owned externally-freed object when
392         // destructed in an unpredictable order.
393         if (!this.isOwner && GC.inFinalizer) {
394             return;
395         }
396 
397         assert(this.sdlCursor !is null);
398     }
399 
400     /++
401      + Equality operator overload
402      +/
403     bool opEquals(const Cursor rhs) const @trusted {
404         return this.sdlCursor is rhs.sdlCursor;
405     }
406 
407     /++
408      + Gets the hash of the `dsdl2.Cursor`
409      +
410      + Returns: unique hash for the instance being the pointer of the internal `SDL_Cursor` pointer
411      +/
412     override hash_t toHash() const @trusted {
413         return cast(hash_t) this.sdlCursor;
414     }
415 
416     /++
417      + Formats the `dsdl2.Cursor` into its construction representation: `"dsdl2.Cursor(<sdlCursor>)"`
418      +
419      + Returns: the formatted `string`
420      +/
421     override string toString() const @trusted {
422         return "dsdl2.Cursor(0x%x)".format(this.sdlCursor);
423     }
424 
425     /++
426      + Wraps `SDL_SetCursor` which sets the `dsdl2.Cursor` to be the cursor
427      +/
428     void set() const @trusted {
429         SDL_SetCursor(cast(SDL_Cursor*) this.sdlCursor);
430     }
431 }