1/*
2  Simple DirectMedia Layer
3  Copyright (C) 1997-2020 Sam Lantinga <slouken@libsdl.org>
4
5  This software is provided 'as-is', without any express or implied
6  warranty.  In no event will the authors be held liable for any damages
7  arising from the use of this software.
8
9  Permission is granted to anyone to use this software for any purpose,
10  including commercial applications, and to alter it and redistribute it
11  freely, subject to the following restrictions:
12
13  1. The origin of this software must not be misrepresented; you must not
14     claim that you wrote the original software. If you use this software
15     in a product, an acknowledgment in the product documentation would be
16     appreciated but is not required.
17  2. Altered source versions must be plainly marked as such, and must not be
18     misrepresented as being the original software.
19  3. This notice may not be removed or altered from any source distribution.
20*/
21#include "../../SDL_internal.h"
22
23#if SDL_VIDEO_DRIVER_COCOA
24
25#include "SDL_assert.h"
26#include "SDL_events.h"
27#include "SDL_cocoamouse.h"
28#include "SDL_cocoamousetap.h"
29#include "SDL_cocoavideo.h"
30
31#include "../../events/SDL_mouse_c.h"
32
33/* #define DEBUG_COCOAMOUSE */
34
35#ifdef DEBUG_COCOAMOUSE
36#define DLog(fmt, ...) printf("%s: " fmt "\n", __func__, ##__VA_ARGS__)
37#else
38#define DLog(...) do { } while (0)
39#endif
40
41@implementation NSCursor (InvisibleCursor)
42+ (NSCursor *)invisibleCursor
43{
44    static NSCursor *invisibleCursor = NULL;
45    if (!invisibleCursor) {
46        /* RAW 16x16 transparent GIF */
47        static unsigned char cursorBytes[] = {
48            0x47, 0x49, 0x46, 0x38, 0x37, 0x61, 0x10, 0x00, 0x10, 0x00, 0x80,
49            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x21, 0xF9, 0x04,
50            0x01, 0x00, 0x00, 0x01, 0x00, 0x2C, 0x00, 0x00, 0x00, 0x00, 0x10,
51            0x00, 0x10, 0x00, 0x00, 0x02, 0x0E, 0x8C, 0x8F, 0xA9, 0xCB, 0xED,
52            0x0F, 0xA3, 0x9C, 0xB4, 0xDA, 0x8B, 0xB3, 0x3E, 0x05, 0x00, 0x3B
53        };
54
55        NSData *cursorData = [NSData dataWithBytesNoCopy:&cursorBytes[0]
56                                                  length:sizeof(cursorBytes)
57                                            freeWhenDone:NO];
58        NSImage *cursorImage = [[[NSImage alloc] initWithData:cursorData] autorelease];
59        invisibleCursor = [[NSCursor alloc] initWithImage:cursorImage
60                                                  hotSpot:NSZeroPoint];
61    }
62
63    return invisibleCursor;
64}
65@end
66
67
68static SDL_Cursor *
69Cocoa_CreateDefaultCursor()
70{ @autoreleasepool
71{
72    NSCursor *nscursor;
73    SDL_Cursor *cursor = NULL;
74
75    nscursor = [NSCursor arrowCursor];
76
77    if (nscursor) {
78        cursor = SDL_calloc(1, sizeof(*cursor));
79        if (cursor) {
80            cursor->driverdata = nscursor;
81            [nscursor retain];
82        }
83    }
84
85    return cursor;
86}}
87
88static SDL_Cursor *
89Cocoa_CreateCursor(SDL_Surface * surface, int hot_x, int hot_y)
90{ @autoreleasepool
91{
92    NSImage *nsimage;
93    NSCursor *nscursor = NULL;
94    SDL_Cursor *cursor = NULL;
95
96    nsimage = Cocoa_CreateImage(surface);
97    if (nsimage) {
98        nscursor = [[NSCursor alloc] initWithImage: nsimage hotSpot: NSMakePoint(hot_x, hot_y)];
99    }
100
101    if (nscursor) {
102        cursor = SDL_calloc(1, sizeof(*cursor));
103        if (cursor) {
104            cursor->driverdata = nscursor;
105        } else {
106            [nscursor release];
107        }
108    }
109
110    return cursor;
111}}
112
113static SDL_Cursor *
114Cocoa_CreateSystemCursor(SDL_SystemCursor id)
115{ @autoreleasepool
116{
117    NSCursor *nscursor = NULL;
118    SDL_Cursor *cursor = NULL;
119
120    switch(id) {
121    case SDL_SYSTEM_CURSOR_ARROW:
122        nscursor = [NSCursor arrowCursor];
123        break;
124    case SDL_SYSTEM_CURSOR_IBEAM:
125        nscursor = [NSCursor IBeamCursor];
126        break;
127    case SDL_SYSTEM_CURSOR_WAIT:
128        nscursor = [NSCursor arrowCursor];
129        break;
130    case SDL_SYSTEM_CURSOR_CROSSHAIR:
131        nscursor = [NSCursor crosshairCursor];
132        break;
133    case SDL_SYSTEM_CURSOR_WAITARROW:
134        nscursor = [NSCursor arrowCursor];
135        break;
136    case SDL_SYSTEM_CURSOR_SIZENWSE:
137    case SDL_SYSTEM_CURSOR_SIZENESW:
138        nscursor = [NSCursor closedHandCursor];
139        break;
140    case SDL_SYSTEM_CURSOR_SIZEWE:
141        nscursor = [NSCursor resizeLeftRightCursor];
142        break;
143    case SDL_SYSTEM_CURSOR_SIZENS:
144        nscursor = [NSCursor resizeUpDownCursor];
145        break;
146    case SDL_SYSTEM_CURSOR_SIZEALL:
147        nscursor = [NSCursor closedHandCursor];
148        break;
149    case SDL_SYSTEM_CURSOR_NO:
150        nscursor = [NSCursor operationNotAllowedCursor];
151        break;
152    case SDL_SYSTEM_CURSOR_HAND:
153        nscursor = [NSCursor pointingHandCursor];
154        break;
155    default:
156        SDL_assert(!"Unknown system cursor");
157        return NULL;
158    }
159
160    if (nscursor) {
161        cursor = SDL_calloc(1, sizeof(*cursor));
162        if (cursor) {
163            /* We'll free it later, so retain it here */
164            [nscursor retain];
165            cursor->driverdata = nscursor;
166        }
167    }
168
169    return cursor;
170}}
171
172static void
173Cocoa_FreeCursor(SDL_Cursor * cursor)
174{ @autoreleasepool
175{
176    NSCursor *nscursor = (NSCursor *)cursor->driverdata;
177
178    [nscursor release];
179    SDL_free(cursor);
180}}
181
182static int
183Cocoa_ShowCursor(SDL_Cursor * cursor)
184{ @autoreleasepool
185{
186    SDL_VideoDevice *device = SDL_GetVideoDevice();
187    SDL_Window *window = (device ? device->windows : NULL);
188    for (; window != NULL; window = window->next) {
189        SDL_WindowData *driverdata = (SDL_WindowData *)window->driverdata;
190        if (driverdata) {
191            [driverdata->nswindow performSelectorOnMainThread:@selector(invalidateCursorRectsForView:)
192                                                   withObject:[driverdata->nswindow contentView]
193                                                waitUntilDone:NO];
194        }
195    }
196    return 0;
197}}
198
199static SDL_Window *
200SDL_FindWindowAtPoint(const int x, const int y)
201{
202    const SDL_Point pt = { x, y };
203    SDL_Window *i;
204    for (i = SDL_GetVideoDevice()->windows; i; i = i->next) {
205        const SDL_Rect r = { i->x, i->y, i->w, i->h };
206        if (SDL_PointInRect(&pt, &r)) {
207            return i;
208        }
209    }
210
211    return NULL;
212}
213
214static int
215Cocoa_WarpMouseGlobal(int x, int y)
216{
217    SDL_Mouse *mouse = SDL_GetMouse();
218    if (mouse->focus) {
219        SDL_WindowData *data = (SDL_WindowData *) mouse->focus->driverdata;
220        if ([data->listener isMoving]) {
221            DLog("Postponing warp, window being moved.");
222            [data->listener setPendingMoveX:x Y:y];
223            return 0;
224        }
225    }
226    const CGPoint point = CGPointMake((float)x, (float)y);
227
228    Cocoa_HandleMouseWarp(point.x, point.y);
229
230    CGWarpMouseCursorPosition(point);
231
232    /* CGWarpMouse causes a short delay by default, which is preventable by
233     * Calling this directly after. CGSetLocalEventsSuppressionInterval can also
234     * prevent it, but it's deprecated as of OS X 10.6.
235     */
236    if (!mouse->relative_mode) {
237        CGAssociateMouseAndMouseCursorPosition(YES);
238    }
239
240    /* CGWarpMouseCursorPosition doesn't generate a window event, unlike our
241     * other implementations' APIs. Send what's appropriate.
242     */
243    if (!mouse->relative_mode) {
244        SDL_Window *win = SDL_FindWindowAtPoint(x, y);
245        SDL_SetMouseFocus(win);
246        if (win) {
247            SDL_assert(win == mouse->focus);
248            SDL_SendMouseMotion(win, mouse->mouseID, 0, x - win->x, y - win->y);
249        }
250    }
251
252    return 0;
253}
254
255static void
256Cocoa_WarpMouse(SDL_Window * window, int x, int y)
257{
258    Cocoa_WarpMouseGlobal(x + window->x, y + window->y);
259}
260
261static int
262Cocoa_SetRelativeMouseMode(SDL_bool enabled)
263{
264    /* We will re-apply the relative mode when the window gets focus, if it
265     * doesn't have focus right now.
266     */
267    SDL_Window *window = SDL_GetMouseFocus();
268    if (!window) {
269      return 0;
270    }
271
272    /* We will re-apply the relative mode when the window finishes being moved,
273     * if it is being moved right now.
274     */
275    SDL_WindowData *data = (SDL_WindowData *) window->driverdata;
276    if ([data->listener isMoving]) {
277        return 0;
278    }
279
280    CGError result;
281    if (enabled) {
282        DLog("Turning on.");
283        result = CGAssociateMouseAndMouseCursorPosition(NO);
284    } else {
285        DLog("Turning off.");
286        result = CGAssociateMouseAndMouseCursorPosition(YES);
287    }
288    if (result != kCGErrorSuccess) {
289        return SDL_SetError("CGAssociateMouseAndMouseCursorPosition() failed");
290    }
291
292    /* The hide/unhide calls are redundant most of the time, but they fix
293     * https://bugzilla.libsdl.org/show_bug.cgi?id=2550
294     */
295    if (enabled) {
296        [NSCursor hide];
297    } else {
298        [NSCursor unhide];
299    }
300    return 0;
301}
302
303static int
304Cocoa_CaptureMouse(SDL_Window *window)
305{
306    /* our Cocoa event code already tracks the mouse outside the window,
307        so all we have to do here is say "okay" and do what we always do. */
308    return 0;
309}
310
311static Uint32
312Cocoa_GetGlobalMouseState(int *x, int *y)
313{
314    const NSUInteger cocoaButtons = [NSEvent pressedMouseButtons];
315    const NSPoint cocoaLocation = [NSEvent mouseLocation];
316    Uint32 retval = 0;
317
318    *x = (int) cocoaLocation.x;
319    *y = (int) (CGDisplayPixelsHigh(kCGDirectMainDisplay) - cocoaLocation.y);
320
321    retval |= (cocoaButtons & (1 << 0)) ? SDL_BUTTON_LMASK : 0;
322    retval |= (cocoaButtons & (1 << 1)) ? SDL_BUTTON_RMASK : 0;
323    retval |= (cocoaButtons & (1 << 2)) ? SDL_BUTTON_MMASK : 0;
324    retval |= (cocoaButtons & (1 << 3)) ? SDL_BUTTON_X1MASK : 0;
325    retval |= (cocoaButtons & (1 << 4)) ? SDL_BUTTON_X2MASK : 0;
326
327    return retval;
328}
329
330int
331Cocoa_InitMouse(_THIS)
332{
333    SDL_Mouse *mouse = SDL_GetMouse();
334    SDL_MouseData *driverdata = (SDL_MouseData*) SDL_calloc(1, sizeof(SDL_MouseData));
335    if (driverdata == NULL) {
336        return SDL_OutOfMemory();
337    }
338
339    mouse->driverdata = driverdata;
340    mouse->CreateCursor = Cocoa_CreateCursor;
341    mouse->CreateSystemCursor = Cocoa_CreateSystemCursor;
342    mouse->ShowCursor = Cocoa_ShowCursor;
343    mouse->FreeCursor = Cocoa_FreeCursor;
344    mouse->WarpMouse = Cocoa_WarpMouse;
345    mouse->WarpMouseGlobal = Cocoa_WarpMouseGlobal;
346    mouse->SetRelativeMouseMode = Cocoa_SetRelativeMouseMode;
347    mouse->CaptureMouse = Cocoa_CaptureMouse;
348    mouse->GetGlobalMouseState = Cocoa_GetGlobalMouseState;
349
350    SDL_SetDefaultCursor(Cocoa_CreateDefaultCursor());
351
352    Cocoa_InitMouseEventTap(driverdata);
353
354    const NSPoint location =  [NSEvent mouseLocation];
355    driverdata->lastMoveX = location.x;
356    driverdata->lastMoveY = location.y;
357    return 0;
358}
359
360void
361Cocoa_HandleMouseEvent(_THIS, NSEvent *event)
362{
363    switch ([event type]) {
364        case NSEventTypeMouseMoved:
365        case NSEventTypeLeftMouseDragged:
366        case NSEventTypeRightMouseDragged:
367        case NSEventTypeOtherMouseDragged:
368            break;
369
370        default:
371            /* Ignore any other events. */
372            return;
373    }
374
375    SDL_Mouse *mouse = SDL_GetMouse();
376    SDL_MouseData *driverdata = (SDL_MouseData*)mouse->driverdata;
377    if (!driverdata) {
378        return;  /* can happen when returning from fullscreen Space on shutdown */
379    }
380
381    SDL_MouseID mouseID = mouse ? mouse->mouseID : 0;
382    const SDL_bool seenWarp = driverdata->seenWarp;
383    driverdata->seenWarp = NO;
384
385    const NSPoint location =  [NSEvent mouseLocation];
386    const CGFloat lastMoveX = driverdata->lastMoveX;
387    const CGFloat lastMoveY = driverdata->lastMoveY;
388    driverdata->lastMoveX = location.x;
389    driverdata->lastMoveY = location.y;
390    DLog("Last seen mouse: (%g, %g)", location.x, location.y);
391
392    /* Non-relative movement is handled in -[Cocoa_WindowListener mouseMoved:] */
393    if (!mouse->relative_mode) {
394        return;
395    }
396
397    /* Ignore events that aren't inside the client area (i.e. title bar.) */
398    if ([event window]) {
399        NSRect windowRect = [[[event window] contentView] frame];
400        if (!NSMouseInRect([event locationInWindow], windowRect, NO)) {
401            return;
402        }
403    }
404
405    float deltaX = [event deltaX];
406    float deltaY = [event deltaY];
407
408    if (seenWarp) {
409        deltaX += (lastMoveX - driverdata->lastWarpX);
410        deltaY += ((CGDisplayPixelsHigh(kCGDirectMainDisplay) - lastMoveY) - driverdata->lastWarpY);
411
412        DLog("Motion was (%g, %g), offset to (%g, %g)", [event deltaX], [event deltaY], deltaX, deltaY);
413    }
414
415    SDL_SendMouseMotion(mouse->focus, mouseID, 1, (int)deltaX, (int)deltaY);
416}
417
418void
419Cocoa_HandleMouseWheel(SDL_Window *window, NSEvent *event)
420{
421    SDL_Mouse *mouse = SDL_GetMouse();
422    if (!mouse) {
423        return;
424    }
425
426    SDL_MouseID mouseID = mouse->mouseID;
427    CGFloat x = -[event deltaX];
428    CGFloat y = [event deltaY];
429    SDL_MouseWheelDirection direction = SDL_MOUSEWHEEL_NORMAL;
430
431    if ([event respondsToSelector:@selector(isDirectionInvertedFromDevice)]) {
432        if ([event isDirectionInvertedFromDevice] == YES) {
433            direction = SDL_MOUSEWHEEL_FLIPPED;
434        }
435    }
436
437    if (x > 0) {
438        x = SDL_ceil(x);
439    } else if (x < 0) {
440        x = SDL_floor(x);
441    }
442    if (y > 0) {
443        y = SDL_ceil(y);
444    } else if (y < 0) {
445        y = SDL_floor(y);
446    }
447
448    SDL_SendMouseWheel(window, mouseID, x, y, direction);
449}
450
451void
452Cocoa_HandleMouseWarp(CGFloat x, CGFloat y)
453{
454    /* This makes Cocoa_HandleMouseEvent ignore the delta caused by the warp,
455     * since it gets included in the next movement event.
456     */
457    SDL_MouseData *driverdata = (SDL_MouseData*)SDL_GetMouse()->driverdata;
458    driverdata->lastWarpX = x;
459    driverdata->lastWarpY = y;
460    driverdata->seenWarp = SDL_TRUE;
461
462    DLog("(%g, %g)", x, y);
463}
464
465void
466Cocoa_QuitMouse(_THIS)
467{
468    SDL_Mouse *mouse = SDL_GetMouse();
469    if (mouse) {
470        if (mouse->driverdata) {
471            Cocoa_QuitMouseEventTap(((SDL_MouseData*)mouse->driverdata));
472
473            SDL_free(mouse->driverdata);
474            mouse->driverdata = NULL;
475        }
476    }
477}
478
479#endif /* SDL_VIDEO_DRIVER_COCOA */
480
481/* vi: set ts=4 sw=4 expandtab: */
482