#include "SDL_internal.h"
#ifdef SDL_VIDEO_DRIVER_COCOA
#include "SDL_cocoamouse.h"
#include "SDL_cocoavideo.h"
#include "../../events/SDL_mouse_c.h"
#if 0#endif
#ifdef DEBUG_COCOAMOUSE
#define DLog(fmt, ...) printf("%s: " fmt "\n", SDL_FUNCTION, ##__VA_ARGS__)
#else
#define DLog(...) \
do { \
} while (0)
#endif
@implementation NSCursor (InvisibleCursor)
+ (NSCursor *)invisibleCursor
{
static NSCursor *invisibleCursor = NULL;
if (!invisibleCursor) {
const int size = 32;
NSImage *cursorImage = [[NSImage alloc] initWithSize:NSMakeSize(size, size)];
NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL
pixelsWide:size
pixelsHigh:size
bitsPerSample:8
samplesPerPixel:4
hasAlpha:YES
isPlanar:NO
colorSpaceName:NSDeviceRGBColorSpace
bytesPerRow:(size * 4)
bitsPerPixel:32];
[cursorImage addRepresentation:imgrep];
invisibleCursor = [[NSCursor alloc] initWithImage:cursorImage
hotSpot:NSZeroPoint];
}
return invisibleCursor;
}
@end
static SDL_Cursor *Cocoa_CreateAnimatedCursor(SDL_CursorFrameInfo *frames, int frame_count, int hot_x, int hot_y)
{
@autoreleasepool {
NSImage *nsimage;
NSCursor *nscursor = NULL;
SDL_Cursor *cursor = NULL;
cursor = SDL_calloc(1, sizeof(*cursor));
if (cursor) {
SDL_CursorData *cdata = SDL_calloc(1, sizeof(*cdata) + (sizeof(*cdata->frames) * frame_count));
if (!cdata) {
SDL_free(cursor);
return NULL;
}
cursor->internal = cdata;
for (int i = 0; i < frame_count; ++i) {
nsimage = Cocoa_CreateImage(frames[i].surface);
if (nsimage) {
nscursor = [[NSCursor alloc] initWithImage:nsimage hotSpot:NSMakePoint(hot_x, hot_y)];
}
if (nscursor) {
++cdata->num_cursors;
cdata->frames[i].cursor = (void *)CFBridgingRetain(nscursor);
cdata->frames[i].duration = frames[i].duration;
} else {
for (int j = 0; j < i; ++j) {
CFBridgingRelease(cdata->frames[i].cursor);
}
SDL_free(cdata);
SDL_free(cursor);
cursor = NULL;
break;
}
}
return cursor;
}
}
return NULL;
}
static SDL_Cursor *Cocoa_CreateCursor(SDL_Surface *surface, int hot_x, int hot_y)
{
SDL_CursorFrameInfo frame = {
surface, 0
};
return Cocoa_CreateAnimatedCursor(&frame, 1, hot_x, hot_y);
}
static NSCursor *LoadHiddenSystemCursor(NSString *cursorName, SEL fallback)
{
NSString *cursorPath = [@"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Resources/cursors" stringByAppendingPathComponent:cursorName];
NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:[cursorPath stringByAppendingPathComponent:@"info.plist"]];
const int frames = (int)[[info valueForKey:@"frames"] integerValue];
NSCursor *cursor;
NSImage *image = [[NSImage alloc] initWithContentsOfFile:[cursorPath stringByAppendingPathComponent:@"cursor.pdf"]];
if ((image == nil) || (image.isValid == NO)) {
return [NSCursor performSelector:fallback];
}
if (frames > 1) {
#ifdef MAC_OS_VERSION_12_0
const NSCompositingOperation operation = NSCompositingOperationCopy;
#else
const NSCompositingOperation operation = NSCompositeCopy;
#endif
const NSSize cropped_size = NSMakeSize(image.size.width, (int)(image.size.height / frames));
NSImage *cropped = [[NSImage alloc] initWithSize:cropped_size];
if (cropped == nil) {
return [NSCursor performSelector:fallback];
}
[cropped lockFocus];
{
const NSRect cropped_rect = NSMakeRect(0, 0, cropped_size.width, cropped_size.height);
[image drawInRect:cropped_rect fromRect:cropped_rect operation:operation fraction:1];
}
[cropped unlockFocus];
image = cropped;
}
cursor = [[NSCursor alloc] initWithImage:image hotSpot:NSMakePoint([[info valueForKey:@"hotx"] doubleValue], [[info valueForKey:@"hoty"] doubleValue])];
return cursor;
}
static SDL_Cursor *Cocoa_CreateSystemCursor(SDL_SystemCursor id)
{
@autoreleasepool {
NSCursor *nscursor = NULL;
SDL_Cursor *cursor = NULL;
switch (id) {
case SDL_SYSTEM_CURSOR_DEFAULT:
nscursor = [NSCursor arrowCursor];
break;
case SDL_SYSTEM_CURSOR_TEXT:
nscursor = [NSCursor IBeamCursor];
break;
case SDL_SYSTEM_CURSOR_CROSSHAIR:
nscursor = [NSCursor crosshairCursor];
break;
case SDL_SYSTEM_CURSOR_WAIT: nscursor = LoadHiddenSystemCursor(@"busybutclickable", @selector(arrowCursor));
break;
case SDL_SYSTEM_CURSOR_PROGRESS: nscursor = LoadHiddenSystemCursor(@"busybutclickable", @selector(arrowCursor));
break;
case SDL_SYSTEM_CURSOR_NWSE_RESIZE:
nscursor = LoadHiddenSystemCursor(@"resizenorthwestsoutheast", @selector(closedHandCursor));
break;
case SDL_SYSTEM_CURSOR_NESW_RESIZE:
nscursor = LoadHiddenSystemCursor(@"resizenortheastsouthwest", @selector(closedHandCursor));
break;
case SDL_SYSTEM_CURSOR_EW_RESIZE:
nscursor = LoadHiddenSystemCursor(@"resizeeastwest", @selector(resizeLeftRightCursor));
break;
case SDL_SYSTEM_CURSOR_NS_RESIZE:
nscursor = LoadHiddenSystemCursor(@"resizenorthsouth", @selector(resizeUpDownCursor));
break;
case SDL_SYSTEM_CURSOR_MOVE:
nscursor = LoadHiddenSystemCursor(@"move", @selector(closedHandCursor));
break;
case SDL_SYSTEM_CURSOR_NOT_ALLOWED:
nscursor = [NSCursor operationNotAllowedCursor];
break;
case SDL_SYSTEM_CURSOR_POINTER:
nscursor = [NSCursor pointingHandCursor];
break;
case SDL_SYSTEM_CURSOR_NW_RESIZE:
nscursor = LoadHiddenSystemCursor(@"resizenorthwestsoutheast", @selector(closedHandCursor));
break;
case SDL_SYSTEM_CURSOR_N_RESIZE:
nscursor = LoadHiddenSystemCursor(@"resizenorthsouth", @selector(resizeUpDownCursor));
break;
case SDL_SYSTEM_CURSOR_NE_RESIZE:
nscursor = LoadHiddenSystemCursor(@"resizenortheastsouthwest", @selector(closedHandCursor));
break;
case SDL_SYSTEM_CURSOR_E_RESIZE:
nscursor = LoadHiddenSystemCursor(@"resizeeastwest", @selector(resizeLeftRightCursor));
break;
case SDL_SYSTEM_CURSOR_SE_RESIZE:
nscursor = LoadHiddenSystemCursor(@"resizenorthwestsoutheast", @selector(closedHandCursor));
break;
case SDL_SYSTEM_CURSOR_S_RESIZE:
nscursor = LoadHiddenSystemCursor(@"resizenorthsouth", @selector(resizeUpDownCursor));
break;
case SDL_SYSTEM_CURSOR_SW_RESIZE:
nscursor = LoadHiddenSystemCursor(@"resizenortheastsouthwest", @selector(closedHandCursor));
break;
case SDL_SYSTEM_CURSOR_W_RESIZE:
nscursor = LoadHiddenSystemCursor(@"resizeeastwest", @selector(resizeLeftRightCursor));
break;
default:
SDL_assert(!"Unknown system cursor");
return NULL;
}
if (nscursor) {
cursor = SDL_calloc(1, sizeof(*cursor));
if (cursor) {
SDL_CursorData *cdata = SDL_calloc(1, sizeof(*cdata) + sizeof(*cdata->frames));
cursor->internal = cdata;
cdata->frames[0].cursor = (void *)CFBridgingRetain(nscursor);
cdata->num_cursors = 1;
}
}
return cursor;
}
}
static SDL_Cursor *Cocoa_CreateDefaultCursor(void)
{
SDL_SystemCursor id = SDL_GetDefaultSystemCursor();
return Cocoa_CreateSystemCursor(id);
}
static void Cocoa_FreeCursor(SDL_Cursor *cursor)
{
@autoreleasepool {
SDL_CursorData *cdata = cursor->internal;
if (cdata->frameTimer) {
[cdata->frameTimer invalidate];
}
for (int i = 0; i < cdata->num_cursors; ++i) {
CFBridgingRelease(cdata->frames[i].cursor);
}
SDL_free(cdata);
SDL_free(cursor);
}
}
static bool Cocoa_ShowCursor(SDL_Cursor *cursor)
{
@autoreleasepool {
SDL_VideoDevice *device = SDL_GetVideoDevice();
SDL_Window *window = (device ? device->windows : NULL);
if (cursor != NULL) {
SDL_CursorData *cdata = cursor->internal;
cdata->current_frame = 0;
if (cdata->frameTimer) {
[cdata->frameTimer invalidate];
cdata->frameTimer = nil;
}
}
for (; window != NULL; window = window->next) {
SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
if (data) {
[data.nswindow performSelectorOnMainThread:@selector(invalidateCursorRectsForView:)
withObject:[data.nswindow contentView]
waitUntilDone:NO];
}
}
return true;
}
}
static SDL_Window *SDL_FindWindowAtPoint(const float x, const float y)
{
const SDL_FPoint pt = { x, y };
SDL_Window *i;
for (i = SDL_GetVideoDevice()->windows; i; i = i->next) {
const SDL_FRect r = { (float)i->x, (float)i->y, (float)i->w, (float)i->h };
if (SDL_PointInRectFloat(&pt, &r)) {
return i;
}
}
return NULL;
}
static bool Cocoa_WarpMouseGlobal(float x, float y)
{
CGPoint point;
SDL_Mouse *mouse = SDL_GetMouse();
if (mouse->focus) {
SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)mouse->focus->internal;
if ([data.listener isMovingOrFocusClickPending]) {
DLog("Postponing warp, window being moved or focused.");
[data.listener setPendingMoveX:x Y:y];
return true;
}
}
point = CGPointMake(x, y);
Cocoa_HandleMouseWarp(point.x, point.y);
CGWarpMouseCursorPosition(point);
if (!mouse->relative_mode) {
CGAssociateMouseAndMouseCursorPosition(YES);
}
if (!mouse->relative_mode) {
SDL_Window *win = SDL_FindWindowAtPoint(x, y);
SDL_SetMouseFocus(win);
if (win) {
SDL_assert(win == mouse->focus);
SDL_SendMouseMotion(0, win, SDL_GLOBAL_MOUSE_ID, false, x - win->x, y - win->y);
}
}
return true;
}
static bool Cocoa_WarpMouse(SDL_Window *window, float x, float y)
{
return Cocoa_WarpMouseGlobal(window->x + x, window->y + y);
}
static bool Cocoa_SetRelativeMouseMode(bool enabled)
{
CGError result;
if (enabled) {
SDL_Window *window = SDL_GetKeyboardFocus();
if (window) {
SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
if ([data.listener isMovingOrFocusClickPending]) {
return true;
}
const CGPoint point = CGPointMake((float)(window->x + (window->w / 2)), (float)(window->y + (window->h / 2)));
Cocoa_HandleMouseWarp(point.x, point.y);
CGWarpMouseCursorPosition(point);
}
DLog("Turning on.");
result = CGAssociateMouseAndMouseCursorPosition(NO);
} else {
DLog("Turning off.");
result = CGAssociateMouseAndMouseCursorPosition(YES);
}
if (result != kCGErrorSuccess) {
return SDL_SetError("CGAssociateMouseAndMouseCursorPosition() failed");
}
if (enabled) {
[NSCursor hide];
} else {
[NSCursor unhide];
}
return true;
}
static bool Cocoa_CaptureMouse(SDL_Window *window)
{
return true;
}
static SDL_MouseButtonFlags Cocoa_GetGlobalMouseState(float *x, float *y)
{
const NSUInteger cocoaButtons = [NSEvent pressedMouseButtons];
const NSPoint cocoaLocation = [NSEvent mouseLocation];
SDL_MouseButtonFlags result = 0;
SDL_VideoDevice *device = SDL_GetVideoDevice();
SDL_CocoaVideoData *videodata = (__bridge SDL_CocoaVideoData *)device->internal;
*x = cocoaLocation.x;
*y = (videodata.mainDisplayHeight - cocoaLocation.y);
result |= (cocoaButtons & (1 << 0)) ? SDL_BUTTON_LMASK : 0;
result |= (cocoaButtons & (1 << 1)) ? SDL_BUTTON_RMASK : 0;
result |= (cocoaButtons & (1 << 2)) ? SDL_BUTTON_MMASK : 0;
result |= (cocoaButtons & (1 << 3)) ? SDL_BUTTON_X1MASK : 0;
result |= (cocoaButtons & (1 << 4)) ? SDL_BUTTON_X2MASK : 0;
return result;
}
bool Cocoa_InitMouse(SDL_VideoDevice *_this)
{
NSPoint location;
SDL_Mouse *mouse = SDL_GetMouse();
SDL_MouseData *data = (SDL_MouseData *)SDL_calloc(1, sizeof(SDL_MouseData));
if (data == NULL) {
return false;
}
mouse->internal = data;
mouse->CreateCursor = Cocoa_CreateCursor;
mouse->CreateAnimatedCursor = Cocoa_CreateAnimatedCursor;
mouse->CreateSystemCursor = Cocoa_CreateSystemCursor;
mouse->ShowCursor = Cocoa_ShowCursor;
mouse->FreeCursor = Cocoa_FreeCursor;
mouse->WarpMouse = Cocoa_WarpMouse;
mouse->WarpMouseGlobal = Cocoa_WarpMouseGlobal;
mouse->SetRelativeMouseMode = Cocoa_SetRelativeMouseMode;
mouse->CaptureMouse = Cocoa_CaptureMouse;
mouse->GetGlobalMouseState = Cocoa_GetGlobalMouseState;
SDL_SetDefaultCursor(Cocoa_CreateDefaultCursor());
location = [NSEvent mouseLocation];
data->lastMoveX = location.x;
data->lastMoveY = location.y;
return true;
}
static void Cocoa_HandleTitleButtonEvent(SDL_VideoDevice *_this, NSEvent *event)
{
SDL_Window *window;
NSWindow *nswindow = [event window];
if (_this == NULL) {
return;
}
for (window = _this->windows; window; window = window->next) {
SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal;
if (data && data.nswindow == nswindow) {
switch ([event type]) {
case NSEventTypeLeftMouseDown:
case NSEventTypeRightMouseDown:
case NSEventTypeOtherMouseDown:
[data.listener setFocusClickPending:[event buttonNumber]];
break;
case NSEventTypeLeftMouseUp:
case NSEventTypeRightMouseUp:
case NSEventTypeOtherMouseUp:
[data.listener clearFocusClickPending:[event buttonNumber]];
break;
default:
break;
}
break;
}
}
}
static NSWindow *Cocoa_MouseFocus;
NSWindow *Cocoa_GetMouseFocus()
{
return Cocoa_MouseFocus;
}
static void Cocoa_ReconcileButtonState(NSEvent *event)
{
Uint32 buttons = SDL_GetMouseState(NULL, NULL);
if (buttons && ![NSEvent pressedMouseButtons]) {
Uint8 button = SDL_BUTTON_LEFT;
while (buttons) {
if (buttons & 0x01) {
SDL_SendMouseButton(Cocoa_GetEventTimestamp([event timestamp]), SDL_GetMouseFocus(), SDL_GLOBAL_MOUSE_ID, button, false);
}
++button;
buttons >>= 1;
}
}
}
void Cocoa_HandleMouseEvent(SDL_VideoDevice *_this, NSEvent *event)
{
SDL_MouseID mouseID = SDL_DEFAULT_MOUSE_ID;
SDL_Mouse *mouse;
SDL_MouseData *data;
NSPoint location;
CGFloat lastMoveX, lastMoveY;
float deltaX, deltaY;
bool seenWarp;
NSEventType event_type = [event type];
if (event_type == NSEventTypeMouseExited) {
Cocoa_MouseFocus = NULL;
} else {
if ([event window] != NULL) {
Cocoa_MouseFocus = [event window];
Cocoa_ReconcileButtonState(event);
}
}
switch (event_type) {
case NSEventTypeMouseEntered:
case NSEventTypeMouseExited:
return;
case NSEventTypeMouseMoved:
case NSEventTypeLeftMouseDragged:
case NSEventTypeRightMouseDragged:
case NSEventTypeOtherMouseDragged:
break;
case NSEventTypeLeftMouseDown:
case NSEventTypeLeftMouseUp:
case NSEventTypeRightMouseDown:
case NSEventTypeRightMouseUp:
case NSEventTypeOtherMouseDown:
case NSEventTypeOtherMouseUp:
if ([event window]) {
NSRect windowRect = [[[event window] contentView] frame];
if (!NSMouseInRect([event locationInWindow], windowRect, NO)) {
Cocoa_HandleTitleButtonEvent(_this, event);
return;
}
}
return;
default:
return;
}
mouse = SDL_GetMouse();
data = (SDL_MouseData *)mouse->internal;
if (!data) {
return; }
seenWarp = data->seenWarp;
data->seenWarp = NO;
location = [NSEvent mouseLocation];
lastMoveX = data->lastMoveX;
lastMoveY = data->lastMoveY;
data->lastMoveX = location.x;
data->lastMoveY = location.y;
DLog("Last seen mouse: (%g, %g)", location.x, location.y);
if (!mouse->relative_mode) {
return;
}
if ([event window]) {
NSRect windowRect = [[[event window] contentView] frame];
if (!NSMouseInRect([event locationInWindow], windowRect, NO)) {
return;
}
}
deltaX = [event deltaX];
deltaY = [event deltaY];
if (seenWarp) {
SDL_CocoaVideoData *videodata = (__bridge SDL_CocoaVideoData *)_this->internal;
deltaX += (lastMoveX - data->lastWarpX);
deltaY += ((videodata.mainDisplayHeight - lastMoveY) - data->lastWarpY);
DLog("Motion was (%g, %g), offset to (%g, %g)", [event deltaX], [event deltaY], deltaX, deltaY);
}
SDL_SendMouseMotion(Cocoa_GetEventTimestamp([event timestamp]), mouse->focus, mouseID, true, deltaX, deltaY);
}
void Cocoa_HandleMouseWheel(SDL_Window *window, NSEvent *event)
{
SDL_MouseID mouseID = SDL_DEFAULT_MOUSE_ID;
SDL_MouseWheelDirection direction;
CGFloat x, y;
x = -[event deltaX];
y = [event deltaY];
direction = SDL_MOUSEWHEEL_NORMAL;
if ([event isDirectionInvertedFromDevice] == YES) {
direction = SDL_MOUSEWHEEL_FLIPPED;
}
if (![event hasPreciseScrollingDeltas]) {
if (x > 0) {
x = SDL_ceil(x);
} else if (x < 0) {
x = SDL_floor(x);
}
if (y > 0) {
y = SDL_ceil(y);
} else if (y < 0) {
y = SDL_floor(y);
}
}
SDL_SendMouseWheel(Cocoa_GetEventTimestamp([event timestamp]), window, mouseID, x, y, direction);
}
void Cocoa_HandleMouseWarp(CGFloat x, CGFloat y)
{
SDL_MouseData *data = (SDL_MouseData *)SDL_GetMouse()->internal;
data->lastWarpX = x;
data->lastWarpY = y;
data->seenWarp = true;
DLog("(%g, %g)", x, y);
}
void Cocoa_QuitMouse(SDL_VideoDevice *_this)
{
}
#endif