SDL: Implemented mouse relative mode for iOS 14.1 and newer

From 46f19c311deb4c6d0f61cc19a198ac40922bb595 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Thu, 8 Jul 2021 07:23:29 -0700
Subject: [PATCH] Implemented mouse relative mode for iOS 14.1 and newer

---
 Xcode/SDL/SDL.xcodeproj/project.pbxproj   |  0
 build-scripts/config.guess                |  0
 build-scripts/config.sub                  |  0
 src/video/uikit/SDL_uikitevents.h         |  1 +
 src/video/uikit/SDL_uikitevents.m         | 46 ++++++++++++++++++-----
 src/video/uikit/SDL_uikitvideo.m          |  9 +++--
 src/video/uikit/SDL_uikitviewcontroller.m | 20 ++++++++--
 src/video/uikit/SDL_uikitwindow.h         |  1 +
 src/video/uikit/SDL_uikitwindow.m         | 26 ++++++++++---
 9 files changed, 80 insertions(+), 23 deletions(-)
 mode change 100644 => 100755 Xcode/SDL/SDL.xcodeproj/project.pbxproj
 mode change 100755 => 100644 build-scripts/config.guess
 mode change 100755 => 100644 build-scripts/config.sub

diff --git a/Xcode/SDL/SDL.xcodeproj/project.pbxproj b/Xcode/SDL/SDL.xcodeproj/project.pbxproj
old mode 100644
new mode 100755
diff --git a/build-scripts/config.guess b/build-scripts/config.guess
old mode 100755
new mode 100644
diff --git a/build-scripts/config.sub b/build-scripts/config.sub
old mode 100755
new mode 100644
diff --git a/src/video/uikit/SDL_uikitevents.h b/src/video/uikit/SDL_uikitevents.h
index 58aaf35b2c..610a468807 100644
--- a/src/video/uikit/SDL_uikitevents.h
+++ b/src/video/uikit/SDL_uikitevents.h
@@ -31,6 +31,7 @@ extern void SDL_QuitGCKeyboard(void);
 
 extern void SDL_InitGCMouse(void);
 extern SDL_bool SDL_HasGCMouse(void);
+extern SDL_bool SDL_GCMouseRelativeMode(void);
 extern void SDL_QuitGCMouse(void);
 
 #endif /* SDL_uikitevents_h_ */
diff --git a/src/video/uikit/SDL_uikitevents.m b/src/video/uikit/SDL_uikitevents.m
index 4219164c34..54f88879c3 100644
--- a/src/video/uikit/SDL_uikitevents.m
+++ b/src/video/uikit/SDL_uikitevents.m
@@ -91,9 +91,9 @@ static void OnGCKeyboardConnected(GCKeyboard *keyboard) API_AVAILABLE(macos(11.0
         SDL_SendKeyboardKey(pressed ? SDL_PRESSED : SDL_RELEASED, (SDL_Scancode)keyCode);
     };
 
-	dispatch_queue_t queue = dispatch_queue_create( "org.libsdl.input.keyboard", DISPATCH_QUEUE_SERIAL );
-	dispatch_set_target_queue( queue, dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_HIGH, 0 ) );
-	keyboard.handlerQueue = queue;
+    dispatch_queue_t queue = dispatch_queue_create( "org.libsdl.input.keyboard", DISPATCH_QUEUE_SERIAL );
+    dispatch_set_target_queue( queue, dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_HIGH, 0 ) );
+    keyboard.handlerQueue = queue;
 }
 
 static void OnGCKeyboardDisconnected(GCKeyboard *keyboard) API_AVAILABLE(macos(11.0), ios(14.0), tvos(14.0))
@@ -182,10 +182,22 @@ void SDL_QuitGCKeyboard(void)
 static int mice_connected = 0;
 static id mouse_connect_observer = nil;
 static id mouse_disconnect_observer = nil;
+static bool mouse_relative_mode = SDL_FALSE;
+
+static void UpdateMouseGrab()
+{
+    SDL_VideoDevice *_this = SDL_GetVideoDevice();
+    SDL_Window *window;
+
+    for (window = _this->windows; window != NULL; window = window->next) {
+        SDL_UpdateWindowGrab(window);
+    }
+}
 
 static int SetGCMouseRelativeMode(SDL_bool enabled)
 {
-    /* We'll always send relative motion and we can't warp, so nothing to do here */
+	mouse_relative_mode = enabled;
+    UpdateMouseGrab();
     return 0;
 }
 
@@ -222,14 +234,16 @@ static void OnGCMouseConnected(GCMouse *mouse) API_AVAILABLE(macos(11.0), ios(14
 
     mouse.mouseInput.mouseMovedHandler = ^(GCMouseInput *mouse, float deltaX, float deltaY)
     {
-		SDL_SendMouseMotion(SDL_GetMouseFocus(), mouseID, SDL_TRUE, (int)deltaX, -(int)deltaY);
+        SDL_SendMouseMotion(SDL_GetMouseFocus(), mouseID, SDL_TRUE, (int)deltaX, -(int)deltaY);
     };
 
-	dispatch_queue_t queue = dispatch_queue_create( "org.libsdl.input.mouse", DISPATCH_QUEUE_SERIAL );
-	dispatch_set_target_queue( queue, dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_HIGH, 0 ) );
-	mouse.handlerQueue = queue;
+    dispatch_queue_t queue = dispatch_queue_create( "org.libsdl.input.mouse", DISPATCH_QUEUE_SERIAL );
+    dispatch_set_target_queue( queue, dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_HIGH, 0 ) );
+    mouse.handlerQueue = queue;
 
     ++mice_connected;
+
+    UpdateMouseGrab();
 }
 
 static void OnGCMouseDisconnected(GCMouse *mouse) API_AVAILABLE(macos(11.0), ios(14.0), tvos(14.0))
@@ -245,12 +259,14 @@ static void OnGCMouseDisconnected(GCMouse *mouse) API_AVAILABLE(macos(11.0), ios
     for (GCControllerButtonInput *button in mouse.mouseInput.auxiliaryButtons) {
         button.pressedChangedHandler = nil;
     }
+
+    UpdateMouseGrab();
 }
 
 void SDL_InitGCMouse(void)
 {
-	@autoreleasepool {
-		/* There is a bug where mouse accumulates duplicate deltas over time in iOS 14.0 */
+    @autoreleasepool {
+        /* There is a bug where mouse accumulates duplicate deltas over time in iOS 14.0 */
         if (@available(iOS 14.1, tvOS 14.1, *)) {
             NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
 
@@ -284,6 +300,11 @@ SDL_bool SDL_HasGCMouse(void)
     return (mice_connected > 0);
 }
 
+SDL_bool SDL_GCMouseRelativeMode(void)
+{
+    return mouse_relative_mode;
+}
+
 void SDL_QuitGCMouse(void)
 {
     @autoreleasepool {
@@ -320,6 +341,11 @@ SDL_bool SDL_HasGCMouse(void)
     return SDL_FALSE;
 }
 
+SDL_bool SDL_GCMouseRelativeMode(void)
+{
+    return SDL_FALSE;
+}
+
 void SDL_QuitGCMouse(void)
 {
 }
diff --git a/src/video/uikit/SDL_uikitvideo.m b/src/video/uikit/SDL_uikitvideo.m
index f302e6494f..ed95f536d1 100644
--- a/src/video/uikit/SDL_uikitvideo.m
+++ b/src/video/uikit/SDL_uikitvideo.m
@@ -93,6 +93,7 @@ static void UIKit_DeleteDevice(SDL_VideoDevice * device)
         device->RaiseWindow = UIKit_RaiseWindow;
         device->SetWindowBordered = UIKit_SetWindowBordered;
         device->SetWindowFullscreen = UIKit_SetWindowFullscreen;
+        device->SetWindowMouseGrab = UIKit_SetWindowMouseGrab;
         device->DestroyWindow = UIKit_DestroyWindow;
         device->GetWindowWMInfo = UIKit_GetWindowWMInfo;
         device->GetDisplayUsableBounds = UIKit_GetDisplayUsableBounds;
@@ -159,8 +160,8 @@ static void UIKit_DeleteDevice(SDL_VideoDevice * device)
         return -1;
     }
 
-	SDL_InitGCKeyboard();
-	SDL_InitGCMouse();
+    SDL_InitGCKeyboard();
+    SDL_InitGCMouse();
 
     return 0;
 }
@@ -168,8 +169,8 @@ static void UIKit_DeleteDevice(SDL_VideoDevice * device)
 void
 UIKit_VideoQuit(_THIS)
 {
-	SDL_QuitGCKeyboard();
-	SDL_QuitGCMouse();
+    SDL_QuitGCKeyboard();
+    SDL_QuitGCMouse();
 
     UIKit_QuitModes(_this);
 }
diff --git a/src/video/uikit/SDL_uikitviewcontroller.m b/src/video/uikit/SDL_uikitviewcontroller.m
index 7b677e9833..c51d1aed25 100644
--- a/src/video/uikit/SDL_uikitviewcontroller.m
+++ b/src/video/uikit/SDL_uikitviewcontroller.m
@@ -27,8 +27,9 @@
 #include "../SDL_sysvideo.h"
 #include "../../events/SDL_events_c.h"
 
-#import "SDL_uikitviewcontroller.h"
-#import "SDL_uikitmessagebox.h"
+#include "SDL_uikitviewcontroller.h"
+#include "SDL_uikitmessagebox.h"
+#include "SDL_uikitevents.h"
 #include "SDL_uikitvideo.h"
 #include "SDL_uikitmodes.h"
 #include "SDL_uikitwindow.h"
@@ -246,7 +247,20 @@ - (UIRectEdge)preferredScreenEdgesDeferringSystemGestures
         return UIRectEdgeNone;
     }
 }
-#endif
+
+- (BOOL)prefersPointerLocked
+{
+    SDL_VideoDevice *_this = SDL_GetVideoDevice();
+    
+    if (SDL_HasGCMouse() &&
+        (SDL_GCMouseRelativeMode() || _this->grabbed_window == window)) {
+        return YES;
+    } else {
+        return NO;
+    }
+}
+
+#endif /* !TARGET_OS_TV */
 
 /*
  ---- Keyboard related functionality below this line ----
diff --git a/src/video/uikit/SDL_uikitwindow.h b/src/video/uikit/SDL_uikitwindow.h
index 4d9fd34a2c..23beea7769 100644
--- a/src/video/uikit/SDL_uikitwindow.h
+++ b/src/video/uikit/SDL_uikitwindow.h
@@ -33,6 +33,7 @@ extern void UIKit_HideWindow(_THIS, SDL_Window * window);
 extern void UIKit_RaiseWindow(_THIS, SDL_Window * window);
 extern void UIKit_SetWindowBordered(_THIS, SDL_Window * window, SDL_bool bordered);
 extern void UIKit_SetWindowFullscreen(_THIS, SDL_Window * window, SDL_VideoDisplay * display, SDL_bool fullscreen);
+extern void UIKit_SetWindowMouseGrab(_THIS, SDL_Window * window, SDL_bool grabbed);
 extern void UIKit_DestroyWindow(_THIS, SDL_Window * window);
 extern SDL_bool UIKit_GetWindowWMInfo(_THIS, SDL_Window * window,
                                       struct SDL_SysWMinfo * info);
diff --git a/src/video/uikit/SDL_uikitwindow.m b/src/video/uikit/SDL_uikitwindow.m
index 39e51813b7..1c84203265 100644
--- a/src/video/uikit/SDL_uikitwindow.m
+++ b/src/video/uikit/SDL_uikitwindow.m
@@ -161,14 +161,14 @@ - (void)layoutSubviews
     @autoreleasepool {
         SDL_VideoDisplay *display = SDL_GetDisplayForWindow(window);
         SDL_DisplayData *data = (__bridge SDL_DisplayData *) display->driverdata;
-		SDL_Window *other;
+        SDL_Window *other;
 
         /* We currently only handle a single window per display on iOS */
-		for (other = _this->windows; other; other = other->next) {
-			if (other != window && SDL_GetDisplayForWindow(other) == display) {
-				return SDL_SetError("Only one window allowed per display.");
-			}
-		}
+        for (other = _this->windows; other; other = other->next) {
+            if (other != window && SDL_GetDisplayForWindow(other) == display) {
+                return SDL_SetError("Only one window allowed per display.");
+            }
+        }
 
         /* If monitor has a resolution of 0x0 (hasn't been explicitly set by the
          * user, so it's in standby), try to force the display to a resolution
@@ -320,6 +320,20 @@ - (void)layoutSubviews
     }
 }
 
+void
+UIKit_SetWindowMouseGrab(_THIS, SDL_Window * window, SDL_bool grabbed)
+{
+#if !TARGET_OS_TV
+    @autoreleasepool {
+        SDL_WindowData *data = (__bridge SDL_WindowData *) window->driverdata;
+        SDL_uikitviewcontroller *viewcontroller = data.viewcontroller;
+        if (@available(iOS 14.0, *)) {
+            [viewcontroller setNeedsUpdateOfPrefersPointerLocked];
+        }
+    }
+#endif /* !TARGET_OS_TV */
+}
+
 void
 UIKit_DestroyWindow(_THIS, SDL_Window * window)
 {