Atlas - SDL_cocoamouse.m

Home / ext / SDL / src / video / cocoa Lines: 2 | Size: 32545 bytes [Download] [Show on GitHub] [Search similar files] [Raw] [Raw (proxy)]
[FILE BEGIN]
1/* 2 Simple DirectMedia Layer 3 Copyright (C) 1997-2026 Sam Lantinga <[email protected]> 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#ifdef SDL_VIDEO_DRIVER_COCOA 24 25#include "SDL_cocoamouse.h" 26#include "SDL_cocoavideo.h" 27 28#include "../../events/SDL_mouse_c.h" 29 30#import <GameController/GameController.h> 31 32#if 0 33#define DEBUG_COCOAMOUSE 34#endif 35 36//#define USE_GCMOUSE_SCROLL 37 38#ifdef DEBUG_COCOAMOUSE 39#define DLog(fmt, ...) printf("%s: " fmt "\n", SDL_FUNCTION, ##__VA_ARGS__) 40#else 41#define DLog(...) \ 42 do { \ 43 } while (0) 44#endif 45 46@implementation NSCursor (InvisibleCursor) 47+ (NSCursor *)invisibleCursor 48{ 49 static NSCursor *invisibleCursor = NULL; 50 if (!invisibleCursor) { 51 const int size = 32; 52 NSImage *cursorImage = [[NSImage alloc] initWithSize:NSMakeSize(size, size)]; 53 NSBitmapImageRep *imgrep = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL 54 pixelsWide:size 55 pixelsHigh:size 56 bitsPerSample:8 57 samplesPerPixel:4 58 hasAlpha:YES 59 isPlanar:NO 60 colorSpaceName:NSDeviceRGBColorSpace 61 bytesPerRow:(size * 4) 62 bitsPerPixel:32]; 63 [cursorImage addRepresentation:imgrep]; 64 65 invisibleCursor = [[NSCursor alloc] initWithImage:cursorImage 66 hotSpot:NSZeroPoint]; 67 } 68 69 return invisibleCursor; 70} 71@end 72 73static SDL_Cursor *Cocoa_CreateAnimatedCursor(SDL_CursorFrameInfo *frames, int frame_count, int hot_x, int hot_y) 74{ 75 @autoreleasepool { 76 NSImage *nsimage; 77 NSCursor *nscursor = NULL; 78 SDL_Cursor *cursor = NULL; 79 80 cursor = SDL_calloc(1, sizeof(*cursor)); 81 if (cursor) { 82 SDL_CursorData *cdata = SDL_calloc(1, sizeof(*cdata) + (sizeof(*cdata->frames) * frame_count)); 83 if (!cdata) { 84 SDL_free(cursor); 85 return NULL; 86 } 87 88 cursor->internal = cdata; 89 90 for (int i = 0; i < frame_count; ++i) { 91 nsimage = Cocoa_CreateImage(frames[i].surface); 92 if (nsimage) { 93 nscursor = [[NSCursor alloc] initWithImage:nsimage hotSpot:NSMakePoint(hot_x, hot_y)]; 94 } 95 96 if (nscursor) { 97 ++cdata->num_cursors; 98 cdata->frames[i].cursor = (void *)CFBridgingRetain(nscursor); 99 cdata->frames[i].duration = frames[i].duration; 100 } else { 101 for (int j = 0; j < i; ++j) { 102 CFBridgingRelease(cdata->frames[i].cursor); 103 } 104 105 SDL_free(cdata); 106 SDL_free(cursor); 107 cursor = NULL; 108 break; 109 } 110 } 111 112 return cursor; 113 } 114 } 115 116 return NULL; 117} 118 119static SDL_Cursor *Cocoa_CreateCursor(SDL_Surface *surface, int hot_x, int hot_y) 120{ 121 SDL_CursorFrameInfo frame = { 122 surface, 0 123 }; 124 125 return Cocoa_CreateAnimatedCursor(&frame, 1, hot_x, hot_y); 126} 127 128/* there are .pdf files of some of the cursors we need, installed by default on macOS, but not available through NSCursor. 129 If we can load them ourselves, use them, otherwise fallback to something standard but not super-great. 130 Since these are under /System, they should be available even to sandboxed apps. */ 131static NSCursor *LoadHiddenSystemCursor(NSString *cursorName, SEL fallback) 132{ 133 NSString *cursorPath = [@"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/HIServices.framework/Versions/A/Resources/cursors" stringByAppendingPathComponent:cursorName]; 134 NSDictionary *info = [NSDictionary dictionaryWithContentsOfFile:[cursorPath stringByAppendingPathComponent:@"info.plist"]]; 135 // we can't do animation atm. :/ 136 const int frames = (int)[[info valueForKey:@"frames"] integerValue]; 137 NSCursor *cursor; 138 NSImage *image = [[NSImage alloc] initWithContentsOfFile:[cursorPath stringByAppendingPathComponent:@"cursor.pdf"]]; 139 if ((image == nil) || (image.isValid == NO)) { 140 return [NSCursor performSelector:fallback]; 141 } 142 143 if (frames > 1) { 144#ifdef MAC_OS_VERSION_12_0 // same value as deprecated symbol. 145 const NSCompositingOperation operation = NSCompositingOperationCopy; 146#else 147 const NSCompositingOperation operation = NSCompositeCopy; 148#endif 149 const NSSize cropped_size = NSMakeSize(image.size.width, (int)(image.size.height / frames)); 150 NSImage *cropped = [[NSImage alloc] initWithSize:cropped_size]; 151 if (cropped == nil) { 152 return [NSCursor performSelector:fallback]; 153 } 154 155 [cropped lockFocus]; 156 { 157 const NSRect cropped_rect = NSMakeRect(0, 0, cropped_size.width, cropped_size.height); 158 [image drawInRect:cropped_rect fromRect:cropped_rect operation:operation fraction:1]; 159 } 160 [cropped unlockFocus]; 161 image = cropped; 162 } 163 164 cursor = [[NSCursor alloc] initWithImage:image hotSpot:NSMakePoint([[info valueForKey:@"hotx"] doubleValue], [[info valueForKey:@"hoty"] doubleValue])]; 165 return cursor; 166} 167 168static SDL_Cursor *Cocoa_CreateSystemCursor(SDL_SystemCursor id) 169{ 170 @autoreleasepool { 171 NSCursor *nscursor = NULL; 172 SDL_Cursor *cursor = NULL; 173 174 switch (id) { 175 case SDL_SYSTEM_CURSOR_DEFAULT: 176 nscursor = [NSCursor arrowCursor]; 177 break; 178 case SDL_SYSTEM_CURSOR_TEXT: 179 nscursor = [NSCursor IBeamCursor]; 180 break; 181 case SDL_SYSTEM_CURSOR_CROSSHAIR: 182 nscursor = [NSCursor crosshairCursor]; 183 break; 184 case SDL_SYSTEM_CURSOR_WAIT: // !!! FIXME: this is more like WAITARROW 185 nscursor = LoadHiddenSystemCursor(@"busybutclickable", @selector(arrowCursor)); 186 break; 187 case SDL_SYSTEM_CURSOR_PROGRESS: // !!! FIXME: this is meant to be animated 188 nscursor = LoadHiddenSystemCursor(@"busybutclickable", @selector(arrowCursor)); 189 break; 190 case SDL_SYSTEM_CURSOR_NWSE_RESIZE: 191 nscursor = LoadHiddenSystemCursor(@"resizenorthwestsoutheast", @selector(closedHandCursor)); 192 break; 193 case SDL_SYSTEM_CURSOR_NESW_RESIZE: 194 nscursor = LoadHiddenSystemCursor(@"resizenortheastsouthwest", @selector(closedHandCursor)); 195 break; 196 case SDL_SYSTEM_CURSOR_EW_RESIZE: 197 nscursor = LoadHiddenSystemCursor(@"resizeeastwest", @selector(resizeLeftRightCursor)); 198 break; 199 case SDL_SYSTEM_CURSOR_NS_RESIZE: 200 nscursor = LoadHiddenSystemCursor(@"resizenorthsouth", @selector(resizeUpDownCursor)); 201 break; 202 case SDL_SYSTEM_CURSOR_MOVE: 203 nscursor = LoadHiddenSystemCursor(@"move", @selector(closedHandCursor)); 204 break; 205 case SDL_SYSTEM_CURSOR_NOT_ALLOWED: 206 nscursor = [NSCursor operationNotAllowedCursor]; 207 break; 208 case SDL_SYSTEM_CURSOR_POINTER: 209 nscursor = [NSCursor pointingHandCursor]; 210 break; 211 case SDL_SYSTEM_CURSOR_NW_RESIZE: 212 nscursor = LoadHiddenSystemCursor(@"resizenorthwestsoutheast", @selector(closedHandCursor)); 213 break; 214 case SDL_SYSTEM_CURSOR_N_RESIZE: 215 nscursor = LoadHiddenSystemCursor(@"resizenorthsouth", @selector(resizeUpDownCursor)); 216 break; 217 case SDL_SYSTEM_CURSOR_NE_RESIZE: 218 nscursor = LoadHiddenSystemCursor(@"resizenortheastsouthwest", @selector(closedHandCursor)); 219 break; 220 case SDL_SYSTEM_CURSOR_E_RESIZE: 221 nscursor = LoadHiddenSystemCursor(@"resizeeastwest", @selector(resizeLeftRightCursor)); 222 break; 223 case SDL_SYSTEM_CURSOR_SE_RESIZE: 224 nscursor = LoadHiddenSystemCursor(@"resizenorthwestsoutheast", @selector(closedHandCursor)); 225 break; 226 case SDL_SYSTEM_CURSOR_S_RESIZE: 227 nscursor = LoadHiddenSystemCursor(@"resizenorthsouth", @selector(resizeUpDownCursor)); 228 break; 229 case SDL_SYSTEM_CURSOR_SW_RESIZE: 230 nscursor = LoadHiddenSystemCursor(@"resizenortheastsouthwest", @selector(closedHandCursor)); 231 break; 232 case SDL_SYSTEM_CURSOR_W_RESIZE: 233 nscursor = LoadHiddenSystemCursor(@"resizeeastwest", @selector(resizeLeftRightCursor)); 234 break; 235 default: 236 SDL_assert(!"Unknown system cursor"); 237 return NULL; 238 } 239 240 if (nscursor) { 241 cursor = SDL_calloc(1, sizeof(*cursor)); 242 if (cursor) { 243 SDL_CursorData *cdata = SDL_calloc(1, sizeof(*cdata) + sizeof(*cdata->frames)); 244 // We'll free it later, so retain it here 245 cursor->internal = cdata; 246 cdata->frames[0].cursor = (void *)CFBridgingRetain(nscursor); 247 cdata->num_cursors = 1; 248 } 249 } 250 251 return cursor; 252 } 253} 254 255static SDL_Cursor *Cocoa_CreateDefaultCursor(void) 256{ 257 SDL_SystemCursor id = SDL_GetDefaultSystemCursor(); 258 return Cocoa_CreateSystemCursor(id); 259} 260 261// GCMouse support for raw (unaccelerated) mouse input on macOS 11.0+ 262static id cocoa_mouse_connect_observer = nil; 263static id cocoa_mouse_disconnect_observer = nil; 264// Atomic for thread-safe access during high-frequency mouse input 265static SDL_AtomicInt cocoa_gcmouse_relative_mode; 266static bool cocoa_has_gcmouse = false; 267 268 269#ifdef USE_GCMOUSE_SCROLL 270static SDL_MouseWheelDirection cocoa_mouse_scroll_direction = SDL_MOUSEWHEEL_NORMAL; 271 272static void Cocoa_UpdateGCMouseScrollDirection(void) 273{ 274 Boolean keyExistsAndHasValidFormat = NO; 275 Boolean naturalScrollDirection = CFPreferencesGetAppBooleanValue( 276 CFSTR("com.apple.swipescrolldirection"), 277 kCFPreferencesAnyApplication, 278 &keyExistsAndHasValidFormat); 279 if (!keyExistsAndHasValidFormat) { 280 // Couldn't read the preference, assume natural scrolling direction 281 naturalScrollDirection = YES; 282 } 283 if (naturalScrollDirection) { 284 cocoa_mouse_scroll_direction = SDL_MOUSEWHEEL_FLIPPED; 285 } else { 286 cocoa_mouse_scroll_direction = SDL_MOUSEWHEEL_NORMAL; 287 } 288} 289#endif 290 291static bool Cocoa_SetGCMouseRelativeMode(bool enabled) 292{ 293 SDL_SetAtomicInt(&cocoa_gcmouse_relative_mode, enabled ? 1 : 0); 294 return true; 295} 296 297static void Cocoa_OnGCMouseButtonChanged(SDL_MouseID mouseID, Uint8 button, 298 BOOL pressed) 299{ 300 Uint64 timestamp = SDL_GetTicksNS(); 301 SDL_SendMouseButton(timestamp, SDL_GetMouseFocus(), mouseID, button, 302 pressed); 303} 304 305static void Cocoa_OnGCMouseConnected(GCMouse *mouse) 306 API_AVAILABLE(macos(11.0)) 307{ 308 SDL_MouseID mouseID = (SDL_MouseID)(uintptr_t)mouse; 309 310 SDL_AddMouse(mouseID, NULL); 311 cocoa_has_gcmouse = true; 312 313 // Sync with SDL's current relative mode state (may have been set before 314 // GCMouse connected) 315 SDL_Mouse *sdl_mouse = SDL_GetMouse(); 316 if (sdl_mouse && sdl_mouse->relative_mode) { 317 SDL_SetAtomicInt(&cocoa_gcmouse_relative_mode, 1); 318 } 319 320 mouse.mouseInput.leftButton.pressedChangedHandler = 321 ^(GCControllerButtonInput *button, float value, BOOL pressed) { 322 Cocoa_OnGCMouseButtonChanged(mouseID, SDL_BUTTON_LEFT, pressed); 323 }; 324 mouse.mouseInput.middleButton.pressedChangedHandler = 325 ^(GCControllerButtonInput *button, float value, BOOL pressed) { 326 Cocoa_OnGCMouseButtonChanged(mouseID, SDL_BUTTON_MIDDLE, pressed); 327 }; 328 mouse.mouseInput.rightButton.pressedChangedHandler = 329 ^(GCControllerButtonInput *button, float value, BOOL pressed) { 330 Cocoa_OnGCMouseButtonChanged(mouseID, SDL_BUTTON_RIGHT, pressed); 331 }; 332 333 int auxiliary_button = SDL_BUTTON_X1; 334 for (GCControllerButtonInput *btn in mouse.mouseInput.auxiliaryButtons) { 335 const int current_button = auxiliary_button; 336 btn.pressedChangedHandler = 337 ^(GCControllerButtonInput *button, float value, BOOL pressed) { 338 Cocoa_OnGCMouseButtonChanged(mouseID, current_button, pressed); 339 }; 340 ++auxiliary_button; 341 } 342 343 mouse.mouseInput.mouseMovedHandler = 344 ^(GCMouseInput *mouseInput, float deltaX, float deltaY) { 345 if (Cocoa_GCMouseRelativeMode()) { 346 // Skip raw input if user wants system-scaled (accelerated) deltas 347 SDL_Mouse *m = SDL_GetMouse(); 348 if (m && m->enable_relative_system_scale) { 349 return; 350 } 351 Uint64 timestamp = SDL_GetTicksNS(); 352 SDL_SendMouseMotion(timestamp, SDL_GetMouseFocus(), mouseID, 353 true, deltaX, -deltaY); 354 } 355 }; 356 357 #ifdef USE_GCMOUSE_SCROLL 358 /* 359 18/04/2026 360 There seems to be a bug in the CGMouse API, at least when using some mouse types. 361 An event is fired only for the first scroll in one direction. Repeated 1-step 362 scrolls in the same direction do not raise an event. 363 Observed on macOS 26.3.1 with 2 different USB mice. 364 */ 365 mouse.mouseInput.scroll.valueChangedHandler = 366 ^(GCControllerDirectionPad *dpad, float xValue, float yValue) { 367 DLog("GCMouse scroll: %f, %f", xValue, yValue); 368 Uint64 timestamp = SDL_GetTicksNS(); 369 float vertical = yValue; 370 float horizontal = xValue; 371 372 if (cocoa_mouse_scroll_direction == SDL_MOUSEWHEEL_FLIPPED) { 373 vertical = -vertical; 374 horizontal = -horizontal; 375 } 376 SDL_SendMouseWheel(timestamp, SDL_GetMouseFocus(), mouseID, 377 horizontal, vertical, 378 cocoa_mouse_scroll_direction); 379 }; 380 Cocoa_UpdateGCMouseScrollDirection(); 381 #endif // USE_GCMOUSE_SCROLL 382 383 // Use high-priority queue for low-latency input 384 dispatch_queue_t queue = dispatch_queue_create("org.libsdl.input.mouse", 385 DISPATCH_QUEUE_SERIAL); 386 dispatch_set_target_queue(queue, 387 dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)); 388 mouse.handlerQueue = queue; 389} 390 391static void Cocoa_OnGCMouseDisconnected(GCMouse *mouse) 392 API_AVAILABLE(macos(11.0)) 393{ 394 SDL_MouseID mouseID = (SDL_MouseID)(uintptr_t)mouse; 395 396 mouse.mouseInput.mouseMovedHandler = nil; 397 mouse.mouseInput.leftButton.pressedChangedHandler = nil; 398 mouse.mouseInput.middleButton.pressedChangedHandler = nil; 399 mouse.mouseInput.rightButton.pressedChangedHandler = nil; 400 mouse.mouseInput.scroll.valueChangedHandler = nil; 401 402 for (GCControllerButtonInput *button in mouse.mouseInput.auxiliaryButtons) { 403 button.pressedChangedHandler = nil; 404 } 405 406 SDL_RemoveMouse(mouseID); 407 408 // Check if any GCMouse devices remain 409 if (@available(macOS 11.0, *)) { 410 cocoa_has_gcmouse = ([GCMouse mice].count > 0); 411 } 412} 413 414void Cocoa_InitGCMouse(void) 415{ 416 @autoreleasepool { 417 if (@available(macOS 11.0, *)) { 418 NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; 419 420 cocoa_mouse_connect_observer = [center 421 addObserverForName:GCMouseDidConnectNotification 422 object:nil 423 queue:nil 424 usingBlock:^(NSNotification *note) { 425 GCMouse *mouse = note.object; 426 Cocoa_OnGCMouseConnected(mouse); 427 }]; 428 429 cocoa_mouse_disconnect_observer = [center 430 addObserverForName:GCMouseDidDisconnectNotification 431 object:nil 432 queue:nil 433 usingBlock:^(NSNotification *note) { 434 GCMouse *mouse = note.object; 435 Cocoa_OnGCMouseDisconnected(mouse); 436 }]; 437 438 // Enumerate already-connected mice 439 for (GCMouse *mouse in [GCMouse mice]) { 440 Cocoa_OnGCMouseConnected(mouse); 441 } 442 } 443 } 444} 445 446bool Cocoa_GCMouseRelativeMode(void) 447{ 448 return SDL_GetAtomicInt(&cocoa_gcmouse_relative_mode) != 0; 449} 450 451bool Cocoa_HasGCMouse(void) 452{ 453 return cocoa_has_gcmouse; 454} 455 456void Cocoa_QuitGCMouse(void) 457{ 458 @autoreleasepool { 459 if (@available(macOS 11.0, *)) { 460 NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; 461 462 if (cocoa_mouse_connect_observer) { 463 [center removeObserver:cocoa_mouse_connect_observer 464 name:GCMouseDidConnectNotification 465 object:nil]; 466 cocoa_mouse_connect_observer = nil; 467 } 468 469 if (cocoa_mouse_disconnect_observer) { 470 [center removeObserver:cocoa_mouse_disconnect_observer 471 name:GCMouseDidDisconnectNotification 472 object:nil]; 473 cocoa_mouse_disconnect_observer = nil; 474 } 475 476 for (GCMouse *mouse in [GCMouse mice]) { 477 Cocoa_OnGCMouseDisconnected(mouse); 478 } 479 480 cocoa_has_gcmouse = false; 481 SDL_SetAtomicInt(&cocoa_gcmouse_relative_mode, 0); 482 } 483 } 484} 485 486static void Cocoa_FreeCursor(SDL_Cursor *cursor) 487{ 488 @autoreleasepool { 489 SDL_CursorData *cdata = cursor->internal; 490 if (cdata->frameTimer) { 491 [cdata->frameTimer invalidate]; 492 } 493 for (int i = 0; i < cdata->num_cursors; ++i) { 494 CFBridgingRelease(cdata->frames[i].cursor); 495 } 496 SDL_free(cdata); 497 SDL_free(cursor); 498 } 499} 500 501static bool Cocoa_ShowCursor(SDL_Cursor *cursor) 502{ 503 @autoreleasepool { 504 SDL_VideoDevice *device = SDL_GetVideoDevice(); 505 SDL_Window *window = (device ? device->windows : NULL); 506 507 if (cursor != NULL) { 508 SDL_CursorData *cdata = cursor->internal; 509 cdata->current_frame = 0; 510 if (cdata->frameTimer) { 511 [cdata->frameTimer invalidate]; 512 cdata->frameTimer = nil; 513 } 514 } 515 516 for (; window != NULL; window = window->next) { 517 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; 518 if (data) { 519 [data.nswindow performSelectorOnMainThread:@selector(invalidateCursorRectsForView:) 520 withObject:[data.nswindow contentView] 521 waitUntilDone:NO]; 522 } 523 } 524 return true; 525 } 526} 527 528static SDL_Window *SDL_FindWindowAtPoint(const float x, const float y) 529{ 530 const SDL_FPoint pt = { x, y }; 531 SDL_Window *i; 532 for (i = SDL_GetVideoDevice()->windows; i; i = i->next) { 533 const SDL_FRect r = { (float)i->x, (float)i->y, (float)i->w, (float)i->h }; 534 if (SDL_PointInRectFloat(&pt, &r)) { 535 return i; 536 } 537 } 538 539 return NULL; 540} 541 542static bool Cocoa_WarpMouseGlobal(float x, float y) 543{ 544 CGPoint point; 545 SDL_Mouse *mouse = SDL_GetMouse(); 546 if (mouse->focus) { 547 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)mouse->focus->internal; 548 if ([data.listener isMovingOrFocusClickPending]) { 549 DLog("Postponing warp, window being moved or focused."); 550 [data.listener setPendingMoveX:x Y:y]; 551 return true; 552 } 553 } 554 point = CGPointMake(x, y); 555 556 Cocoa_HandleMouseWarp(point.x, point.y); 557 558 CGWarpMouseCursorPosition(point); 559 560 /* CGWarpMouse causes a short delay by default, which is preventable by 561 * Calling this directly after. CGSetLocalEventsSuppressionInterval can also 562 * prevent it, but it's deprecated as macOS 10.6. 563 */ 564 if (!mouse->relative_mode) { 565 CGAssociateMouseAndMouseCursorPosition(YES); 566 } 567 568 /* CGWarpMouseCursorPosition doesn't generate a window event, unlike our 569 * other implementations' APIs. Send what's appropriate. 570 */ 571 if (!mouse->relative_mode) { 572 SDL_Window *win = SDL_FindWindowAtPoint(x, y); 573 SDL_SetMouseFocus(win); 574 if (win) { 575 SDL_assert(win == mouse->focus); 576 SDL_SendMouseMotion(0, win, SDL_GLOBAL_MOUSE_ID, false, x - win->x, y - win->y); 577 } 578 } 579 580 return true; 581} 582 583static bool Cocoa_WarpMouse(SDL_Window *window, float x, float y) 584{ 585 return Cocoa_WarpMouseGlobal(window->x + x, window->y + y); 586} 587 588static bool Cocoa_SetRelativeMouseMode(bool enabled) 589{ 590 CGError result; 591 592 // Update GCMouse relative mode state if available 593 if (Cocoa_HasGCMouse()) { 594 Cocoa_SetGCMouseRelativeMode(enabled); 595 } 596 597 if (enabled) { 598 SDL_Window *window = SDL_GetKeyboardFocus(); 599 if (window) { 600 /* We will re-apply the relative mode when the window finishes 601 * being moved, if it is being moved right now. 602 */ 603 SDL_CocoaWindowData *data = 604 (__bridge SDL_CocoaWindowData *)window->internal; 605 if ([data.listener isMovingOrFocusClickPending]) { 606 return true; 607 } 608 609 // Make sure the mouse isn't at the corner of the window, as this 610 // can confuse things if macOS thinks a window resize is happening 611 // on the first click. 612 const CGPoint point = CGPointMake( 613 (float)(window->x + (window->w / 2)), 614 (float)(window->y + (window->h / 2))); 615 Cocoa_HandleMouseWarp(point.x, point.y); 616 CGWarpMouseCursorPosition(point); 617 } 618 DLog("Turning on."); 619 result = CGAssociateMouseAndMouseCursorPosition(NO); 620 } else { 621 DLog("Turning off."); 622 result = CGAssociateMouseAndMouseCursorPosition(YES); 623 } 624 if (result != kCGErrorSuccess) { 625 return SDL_SetError("CGAssociateMouseAndMouseCursorPosition() failed"); 626 } 627 628 /* The hide/unhide calls are redundant most of the time, but they fix 629 * https://bugzilla.libsdl.org/show_bug.cgi?id=2550 630 */ 631 if (enabled) { 632 [NSCursor hide]; 633 } else { 634 [NSCursor unhide]; 635 } 636 return true; 637} 638 639static bool Cocoa_CaptureMouse(SDL_Window *window) 640{ 641 /* our Cocoa event code already tracks the mouse outside the window, 642 so all we have to do here is say "okay" and do what we always do. */ 643 return true; 644} 645 646static SDL_MouseButtonFlags Cocoa_GetGlobalMouseState(float *x, float *y) 647{ 648 const NSUInteger cocoaButtons = [NSEvent pressedMouseButtons]; 649 const NSPoint cocoaLocation = [NSEvent mouseLocation]; 650 SDL_MouseButtonFlags result = 0; 651 SDL_VideoDevice *device = SDL_GetVideoDevice(); 652 SDL_CocoaVideoData *videodata = (__bridge SDL_CocoaVideoData *)device->internal; 653 654 *x = cocoaLocation.x; 655 *y = (videodata.mainDisplayHeight - cocoaLocation.y); 656 657 result |= (cocoaButtons & (1 << 0)) ? SDL_BUTTON_LMASK : 0; 658 result |= (cocoaButtons & (1 << 1)) ? SDL_BUTTON_RMASK : 0; 659 result |= (cocoaButtons & (1 << 2)) ? SDL_BUTTON_MMASK : 0; 660 result |= (cocoaButtons & (1 << 3)) ? SDL_BUTTON_X1MASK : 0; 661 result |= (cocoaButtons & (1 << 4)) ? SDL_BUTTON_X2MASK : 0; 662 663 return result; 664} 665 666bool Cocoa_InitMouse(SDL_VideoDevice *_this) 667{ 668 NSPoint location; 669 SDL_Mouse *mouse = SDL_GetMouse(); 670 SDL_MouseData *data = (SDL_MouseData *)SDL_calloc(1, sizeof(SDL_MouseData)); 671 if (data == NULL) { 672 return false; 673 } 674 675 mouse->internal = data; 676 mouse->CreateCursor = Cocoa_CreateCursor; 677 mouse->CreateAnimatedCursor = Cocoa_CreateAnimatedCursor; 678 mouse->CreateSystemCursor = Cocoa_CreateSystemCursor; 679 mouse->ShowCursor = Cocoa_ShowCursor; 680 mouse->FreeCursor = Cocoa_FreeCursor; 681 mouse->WarpMouse = Cocoa_WarpMouse; 682 mouse->WarpMouseGlobal = Cocoa_WarpMouseGlobal; 683 mouse->SetRelativeMouseMode = Cocoa_SetRelativeMouseMode; 684 mouse->CaptureMouse = Cocoa_CaptureMouse; 685 mouse->GetGlobalMouseState = Cocoa_GetGlobalMouseState; 686 687 SDL_SetDefaultCursor(Cocoa_CreateDefaultCursor()); 688 689 location = [NSEvent mouseLocation]; 690 data->lastMoveX = location.x; 691 data->lastMoveY = location.y; 692 return true; 693} 694 695static void Cocoa_HandleTitleButtonEvent(SDL_VideoDevice *_this, NSEvent *event) 696{ 697 SDL_Window *window; 698 NSWindow *nswindow = [event window]; 699 700 /* You might land in this function before SDL_Init if showing a message box. 701 Don't dereference a NULL pointer if that happens. */ 702 if (_this == NULL) { 703 return; 704 } 705 706 for (window = _this->windows; window; window = window->next) { 707 SDL_CocoaWindowData *data = (__bridge SDL_CocoaWindowData *)window->internal; 708 if (data && data.nswindow == nswindow) { 709 switch ([event type]) { 710 case NSEventTypeLeftMouseDown: 711 case NSEventTypeRightMouseDown: 712 case NSEventTypeOtherMouseDown: 713 [data.listener setFocusClickPending:[event buttonNumber]]; 714 break; 715 case NSEventTypeLeftMouseUp: 716 case NSEventTypeRightMouseUp: 717 case NSEventTypeOtherMouseUp: 718 [data.listener clearFocusClickPending:[event buttonNumber]]; 719 break; 720 default: 721 break; 722 } 723 break; 724 } 725 } 726} 727 728static NSWindow *Cocoa_MouseFocus; 729 730NSWindow *Cocoa_GetMouseFocus() 731{ 732 return Cocoa_MouseFocus; 733} 734 735static void Cocoa_ReconcileButtonState(NSEvent *event) 736{ 737 // Send mouse up events for any buttons that are no longer pressed 738 Uint32 buttons = SDL_GetMouseState(NULL, NULL); 739 if (buttons && ![NSEvent pressedMouseButtons]) { 740 Uint8 button = SDL_BUTTON_LEFT; 741 while (buttons) { 742 if (buttons & 0x01) { 743 SDL_SendMouseButton(Cocoa_GetEventTimestamp([event timestamp]), SDL_GetMouseFocus(), SDL_GLOBAL_MOUSE_ID, button, false); 744 } 745 ++button; 746 buttons >>= 1; 747 } 748 } 749} 750 751void Cocoa_HandleMouseEvent(SDL_VideoDevice *_this, NSEvent *event) 752{ 753 SDL_MouseID mouseID = SDL_DEFAULT_MOUSE_ID; 754 SDL_Mouse *mouse; 755 SDL_MouseData *data; 756 NSPoint location; 757 CGFloat lastMoveX, lastMoveY; 758 float deltaX, deltaY; 759 bool seenWarp; 760 761 // All events except NSEventTypeMouseExited can only happen if the window 762 // has mouse focus, so we'll always set the focus even if we happen to miss 763 // NSEventTypeMouseEntered, which apparently happens if the window is 764 // created under the mouse on macOS 12.7. But, only set the focus if 765 // the event actually has a non-NULL window, otherwise what would happen 766 // is that after an NSEventTypeMouseEntered there would sometimes be 767 // NSEventTypeMouseMoved without a window causing us to suppress subsequent 768 // mouse move events. 769 NSEventType event_type = [event type]; 770 if (event_type == NSEventTypeMouseExited) { 771 Cocoa_MouseFocus = NULL; 772 } else { 773 if ([event window] != NULL) { 774 Cocoa_MouseFocus = [event window]; 775 Cocoa_ReconcileButtonState(event); 776 } 777 } 778 779 switch (event_type) { 780 case NSEventTypeMouseEntered: 781 case NSEventTypeMouseExited: 782 // Focus is handled above 783 return; 784 785 case NSEventTypeMouseMoved: 786 case NSEventTypeLeftMouseDragged: 787 case NSEventTypeRightMouseDragged: 788 case NSEventTypeOtherMouseDragged: 789 break; 790 791 case NSEventTypeLeftMouseDown: 792 case NSEventTypeLeftMouseUp: 793 case NSEventTypeRightMouseDown: 794 case NSEventTypeRightMouseUp: 795 case NSEventTypeOtherMouseDown: 796 case NSEventTypeOtherMouseUp: 797 if ([event window]) { 798 NSRect windowRect = [[[event window] contentView] frame]; 799 if (!NSMouseInRect([event locationInWindow], windowRect, NO)) { 800 Cocoa_HandleTitleButtonEvent(_this, event); 801 return; 802 } 803 } 804 return; 805 806 default: 807 // Ignore any other events. 808 return; 809 } 810 811 mouse = SDL_GetMouse(); 812 data = (SDL_MouseData *)mouse->internal; 813 if (!data) { 814 return; // can happen when returning from fullscreen Space on shutdown 815 } 816 817 seenWarp = data->seenWarp; 818 data->seenWarp = NO; 819 820 location = [NSEvent mouseLocation]; 821 lastMoveX = data->lastMoveX; 822 lastMoveY = data->lastMoveY; 823 data->lastMoveX = location.x; 824 data->lastMoveY = location.y; 825 DLog("Last seen mouse: (%g, %g)", location.x, location.y); 826 827 // Non-relative movement is handled in -[SDL3Cocoa_WindowListener mouseMoved:] 828 if (!mouse->relative_mode) { 829 return; 830 } 831 832 // When GCMouse is active in relative mode, it handles motion events 833 // directly with raw (unaccelerated) deltas. Skip NSEvent-based motion 834 // unless the user wants system-scaled (accelerated) input. 835 if (Cocoa_HasGCMouse() && Cocoa_GCMouseRelativeMode()) { 836 if (!mouse->enable_relative_system_scale) { 837 // GCMouse is providing raw input, skip NSEvent deltas 838 return; 839 } 840 // SYSTEM_SCALE is enabled: use NSEvent accelerated deltas instead 841 } 842 843 // Ignore events that aren't inside the client area (i.e. title bar.) 844 if ([event window]) { 845 NSRect windowRect = [[[event window] contentView] frame]; 846 if (!NSMouseInRect([event locationInWindow], windowRect, NO)) { 847 return; 848 } 849 } 850 851 deltaX = [event deltaX]; 852 deltaY = [event deltaY]; 853 854 if (seenWarp) { 855 SDL_CocoaVideoData *videodata = (__bridge SDL_CocoaVideoData *)_this->internal; 856 deltaX += (lastMoveX - data->lastWarpX); 857 deltaY += ((videodata.mainDisplayHeight - lastMoveY) - data->lastWarpY); 858 859 DLog("Motion was (%g, %g), offset to (%g, %g)", [event deltaX], 860 [event deltaY], deltaX, deltaY); 861 } 862 863 SDL_SendMouseMotion(Cocoa_GetEventTimestamp([event timestamp]), 864 mouse->focus, mouseID, true, deltaX, deltaY); 865} 866 867void Cocoa_HandleMouseWheel(SDL_Window *window, NSEvent *event) 868{ 869 #ifdef USE_GCMOUSE_SCROLL 870 // GCMouse handles scroll events directly, skip NSEvent path to avoid duplicates 871 if (Cocoa_HasGCMouse()) { 872 return; 873 } 874 #endif 875 876 SDL_MouseID mouseID = SDL_DEFAULT_MOUSE_ID; 877 SDL_MouseWheelDirection direction; 878 CGFloat x, y; 879 880 x = -[event scrollingDeltaX]; 881 y = [event scrollingDeltaY]; 882 direction = SDL_MOUSEWHEEL_NORMAL; 883 884 if ([event isDirectionInvertedFromDevice] == YES) { 885 direction = SDL_MOUSEWHEEL_FLIPPED; 886 } 887 888 if ([event hasPreciseScrollingDeltas]) { 889 x *= 0.1; 890 y *= 0.1; 891 } 892 893 SDL_SendMouseWheel(Cocoa_GetEventTimestamp([event timestamp]), window, mouseID, x, y, direction); 894} 895 896void Cocoa_HandleMouseWarp(CGFloat x, CGFloat y) 897{ 898 /* This makes Cocoa_HandleMouseEvent ignore the delta caused by the warp, 899 * since it gets included in the next movement event. 900 */ 901 SDL_MouseData *data = (SDL_MouseData *)SDL_GetMouse()->internal; 902 data->lastWarpX = x; 903 data->lastWarpY = y; 904 data->seenWarp = true; 905 906 DLog("(%g, %g)", x, y); 907} 908 909void Cocoa_QuitMouse(SDL_VideoDevice *_this) 910{ 911} 912 913#endif // SDL_VIDEO_DRIVER_COCOA 914
[FILE END]
(C) 2025 0x4248 (C) 2025 4248 Media and 4248 Systems, All part of 0x4248 See LICENCE files for more information. Not all files are by 0x4248 always check Licencing.