SDL: Better implementation of SDL_SetWindowMouseGrab() and SDL_SetWindowMouseRect() on macOS

From 35d90f17e1c7d3740c75641ef94b5e5c938c20c6 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Fri, 12 Nov 2021 03:00:57 -0800
Subject: [PATCH] Better implementation of SDL_SetWindowMouseGrab() and
 SDL_SetWindowMouseRect() on macOS

---
 src/video/cocoa/SDL_cocoawindow.m | 107 ++++++++++++++++++++++--------
 1 file changed, 78 insertions(+), 29 deletions(-)

diff --git a/src/video/cocoa/SDL_cocoawindow.m b/src/video/cocoa/SDL_cocoawindow.m
index 248607106a..7908646b6b 100644
--- a/src/video/cocoa/SDL_cocoawindow.m
+++ b/src/video/cocoa/SDL_cocoawindow.m
@@ -59,6 +59,11 @@
 #define NSAppKitVersionNumber10_14 1671
 #endif
 
+/* This is available as of 10.13.2, but isn't in public headers */
+@interface NSWindow (SDL)
+@property (nonatomic) NSRect mouseConfinementRect;
+@end
+
 @interface SDLWindow : NSWindow <NSDraggingDestination>
 /* These are needed for borderless/fullscreen windows */
 - (BOOL)canBecomeKeyWindow;
@@ -381,6 +386,60 @@ static void ConvertNSRect(NSScreen *screen, BOOL fullscreen, NSRect *r)
     return SDL_FALSE;
 }
 
+static void
+Cocoa_UpdateClipCursor(SDL_Window * window)
+{
+    SDL_WindowData *data = (SDL_WindowData *) window->driverdata;
+
+    if (@available(macOS 10.13.2, *)) {
+        NSWindow *nswindow = data->nswindow;
+        SDL_Rect mouse_rect;
+
+        SDL_zero(mouse_rect);
+
+        if (ShouldAdjustCoordinatesForGrab(window)) {
+            SDL_Rect window_rect;
+
+            window_rect.x = 0;
+            window_rect.y = 0;
+            window_rect.w = window->w;
+            window_rect.h = window->h;
+
+            if (window->mouse_rect.w > 0 && window->mouse_rect.h > 0) {
+                SDL_IntersectRect(&window->mouse_rect, &window_rect, &mouse_rect);
+            }
+
+            if ((window->flags & SDL_WINDOW_MOUSE_GRABBED) != 0 &&
+                SDL_RectEmpty(&mouse_rect)) {
+                SDL_memcpy(&mouse_rect, &window_rect, sizeof(mouse_rect));
+            }
+        }
+
+        if (SDL_RectEmpty(&mouse_rect)) {
+            nswindow.mouseConfinementRect = NSZeroRect;
+        } else {
+            NSRect rect;
+            rect.origin.x = mouse_rect.x;
+            rect.origin.y = [nswindow contentLayoutRect].size.height - mouse_rect.y - mouse_rect.h;
+            rect.size.width = mouse_rect.w;
+            rect.size.height = mouse_rect.h;
+            data->nswindow.mouseConfinementRect = rect;
+        }
+    } else {
+        /* Move the cursor to the nearest point in the window */
+        if (ShouldAdjustCoordinatesForGrab(window)) {
+            int x, y;
+            CGPoint cgpoint;
+
+            SDL_GetGlobalMouseState(&x, &y);
+            if (AdjustCoordinatesForGrab(window, x, y, &cgpoint)) {
+                Cocoa_HandleMouseWarp(cgpoint.x, cgpoint.y);
+                CGDisplayMoveCursorToPoint(kCGDirectMainDisplay, cgpoint);
+            }
+        }
+    }
+}
+
 
 @implementation Cocoa_WindowListener
 
@@ -628,6 +687,8 @@ - (void)onMovingOrFocusClickPendingStateCleared
             }
 
             mouse->SetRelativeMouseMode(SDL_TRUE);
+        } else {
+            Cocoa_UpdateClipCursor(_data->window);
         }
     }
 }
@@ -726,6 +787,10 @@ - (void)windowDidResize:(NSNotification *)aNotification
 
 - (void)windowDidMiniaturize:(NSNotification *)aNotification
 {
+    if (focusClickPending) {
+        focusClickPending = 0;
+        [self onMovingOrFocusClickPendingStateCleared];
+    }
     SDL_SendWindowEvent(_data->window, SDL_WINDOWEVENT_MINIMIZED, 0, 0);
 }
 
@@ -1215,12 +1280,16 @@ - (void)mouseMoved:(NSEvent *)theEvent
     x = (int)point.x;
     y = (int)(window->h - point.y);
 
-    CGPoint cgpoint;
-    if (ShouldAdjustCoordinatesForGrab(window) &&
-        AdjustCoordinatesForGrab(window, window->x + x, window->y + y, &cgpoint)) {
-        Cocoa_HandleMouseWarp(cgpoint.x, cgpoint.y);
-        CGDisplayMoveCursorToPoint(kCGDirectMainDisplay, cgpoint);
-        CGAssociateMouseAndMouseCursorPosition(YES);
+    if (@available(macOS 10.13.2, *)) {
+        /* Mouse grab is taken care of by the confinement rect */
+    } else {
+        CGPoint cgpoint;
+        if (ShouldAdjustCoordinatesForGrab(window) &&
+            AdjustCoordinatesForGrab(window, window->x + x, window->y + y, &cgpoint)) {
+            Cocoa_HandleMouseWarp(cgpoint.x, cgpoint.y);
+            CGDisplayMoveCursorToPoint(kCGDirectMainDisplay, cgpoint);
+            CGAssociateMouseAndMouseCursorPosition(YES);
+        }
     }
 
     SDL_SendMouseMotion(window, mouseID, 0, x, y);
@@ -2054,7 +2123,7 @@ - (BOOL)acceptsFirstMouse:(NSEvent *)theEvent
     if (iccProfileData == nil) {
         SDL_SetError("Could not get ICC profile data.");
         return NULL;
-	}
+    }
 
     retIccProfileData = SDL_malloc([iccProfileData length]);
     if (!retIccProfileData) {
@@ -2094,17 +2163,7 @@ - (BOOL)acceptsFirstMouse:(NSEvent *)theEvent
 void
 Cocoa_SetWindowMouseRect(_THIS, SDL_Window * window)
 {
-    /* Move the cursor to the nearest point in the mouse rect */
-    if (ShouldAdjustCoordinatesForGrab(window)) {
-        int x, y;
-        CGPoint cgpoint;
-
-        SDL_GetGlobalMouseState(&x, &y);
-        if (AdjustCoordinatesForGrab(window, x, y, &cgpoint)) {
-            Cocoa_HandleMouseWarp(cgpoint.x, cgpoint.y);
-            CGDisplayMoveCursorToPoint(kCGDirectMainDisplay, cgpoint);
-        }
-    }
+    Cocoa_UpdateClipCursor(window);
 }
 
 void
@@ -2112,17 +2171,7 @@ - (BOOL)acceptsFirstMouse:(NSEvent *)theEvent
 {
     SDL_WindowData *data = (SDL_WindowData *) window->driverdata;
 
-    /* Move the cursor to the nearest point in the window */
-    if (ShouldAdjustCoordinatesForGrab(window)) {
-        int x, y;
-        CGPoint cgpoint;
-
-        SDL_GetGlobalMouseState(&x, &y);
-        if (AdjustCoordinatesForGrab(window, x, y, &cgpoint)) {
-            Cocoa_HandleMouseWarp(cgpoint.x, cgpoint.y);
-            CGDisplayMoveCursorToPoint(kCGDirectMainDisplay, cgpoint);
-        }
-    }
+    Cocoa_UpdateClipCursor(window);
 
     if (data && (window->flags & SDL_WINDOW_FULLSCREEN)) {
         if (SDL_ShouldAllowTopmost() && (window->flags & SDL_WINDOW_INPUT_FOCUS)