SDL: cocoa: Implemented display hotplugging support.

https://github.com/libsdl-org/SDL/commit/38176bfe9ab6eda2620bebc9a96f95a644671d25

From 38176bfe9ab6eda2620bebc9a96f95a644671d25 Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Fri, 10 Jan 2025 13:23:55 -0500
Subject: [PATCH] cocoa: Implemented display hotplugging support.

Fixes #7764.
---
 src/video/SDL_sysvideo.h          |   2 +
 src/video/SDL_video.c             |   2 +-
 src/video/cocoa/SDL_cocoamodes.h  |   1 +
 src/video/cocoa/SDL_cocoamodes.m  | 225 +++++++++++++++++++++++-------
 src/video/cocoa/SDL_cocoawindow.m |  14 +-
 5 files changed, 183 insertions(+), 61 deletions(-)

diff --git a/src/video/SDL_sysvideo.h b/src/video/SDL_sysvideo.h
index 5ac9db26ee3fe..418b8239f168d 100644
--- a/src/video/SDL_sysvideo.h
+++ b/src/video/SDL_sysvideo.h
@@ -586,6 +586,8 @@ extern bool SDL_ShouldAllowTopmost(void);
 
 extern void SDL_ToggleDragAndDropSupport(void);
 
+extern void SDL_UpdateDesktopBounds(void);
+
 extern SDL_TextInputType SDL_GetTextInputType(SDL_PropertiesID props);
 extern SDL_Capitalization SDL_GetTextInputCapitalization(SDL_PropertiesID props);
 extern bool SDL_GetTextInputAutocorrect(SDL_PropertiesID props);
diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c
index 6808e14a40d21..3b5fa4d84d7fd 100644
--- a/src/video/SDL_video.c
+++ b/src/video/SDL_video.c
@@ -743,7 +743,7 @@ SDL_SystemTheme SDL_GetSystemTheme(void)
     }
 }
 
-static void SDL_UpdateDesktopBounds(void)
+void SDL_UpdateDesktopBounds(void)
 {
     SDL_Rect rect;
     SDL_zero(rect);
diff --git a/src/video/cocoa/SDL_cocoamodes.h b/src/video/cocoa/SDL_cocoamodes.h
index fd4e18998d6a8..37f3aa584029c 100644
--- a/src/video/cocoa/SDL_cocoamodes.h
+++ b/src/video/cocoa/SDL_cocoamodes.h
@@ -40,5 +40,6 @@ extern bool Cocoa_GetDisplayUsableBounds(SDL_VideoDevice *_this, SDL_VideoDispla
 extern bool Cocoa_GetDisplayModes(SDL_VideoDevice *_this, SDL_VideoDisplay *display);
 extern bool Cocoa_SetDisplayMode(SDL_VideoDevice *_this, SDL_VideoDisplay *display, SDL_DisplayMode *mode);
 extern void Cocoa_QuitModes(SDL_VideoDevice *_this);
+extern SDL_VideoDisplay *Cocoa_FindSDLDisplayByCGDirectDisplayID(SDL_VideoDevice *_this, CGDirectDisplayID displayid);
 
 #endif // SDL_cocoamodes_h_
diff --git a/src/video/cocoa/SDL_cocoamodes.m b/src/video/cocoa/SDL_cocoamodes.m
index b309c3ab3343b..218571860d6b9 100644
--- a/src/video/cocoa/SDL_cocoamodes.m
+++ b/src/video/cocoa/SDL_cocoamodes.m
@@ -23,6 +23,7 @@
 #ifdef SDL_VIDEO_DRIVER_COCOA
 
 #include "SDL_cocoavideo.h"
+#include "../../events/SDL_events_c.h"
 
 // We need this for IODisplayCreateInfoDictionary and kIODisplayOnlyPreferredName
 #include <IOKit/graphics/IOGraphicsLib.h>
@@ -97,6 +98,17 @@ static bool CG_SetError(const char *prefix, CGDisplayErr result)
     return nil;
 }
 
+SDL_VideoDisplay *Cocoa_FindSDLDisplayByCGDirectDisplayID(SDL_VideoDevice *_this, CGDirectDisplayID displayid)
+{
+    for (int i = 0; i < _this->num_displays; i++) {
+        const SDL_DisplayData *displaydata = _this->displays[i]->internal;
+        if (displaydata && (displaydata->display == displayid)) {
+            return _this->displays[i];
+        }
+    }
+    return NULL;
+}
+
 static float GetDisplayModeRefreshRate(CGDisplayModeRef vidmode, CVDisplayLinkRef link)
 {
     float refreshRate = (float)CGDisplayModeGetRefreshRate(vidmode);
@@ -153,7 +165,7 @@ static Uint32 GetDisplayModePixelFormat(CGDisplayModeRef vidmode)
     return pixelformat;
 }
 
-static bool GetDisplayMode(SDL_VideoDevice *_this, CGDisplayModeRef vidmode, bool vidmodeCurrent, CFArrayRef modelist, CVDisplayLinkRef link, SDL_DisplayMode *mode)
+static bool GetDisplayMode(CGDisplayModeRef vidmode, bool vidmodeCurrent, CFArrayRef modelist, CVDisplayLinkRef link, SDL_DisplayMode *mode)
 {
     SDL_DisplayModeData *data;
     bool usableForGUI = CGDisplayModeIsUsableForDesktopGUI(vidmode);
@@ -309,21 +321,169 @@ static void Cocoa_GetHDRProperties(CGDirectDisplayID displayID, SDL_HDROutputPro
 #endif
 }
 
+
+bool Cocoa_AddDisplay(CGDirectDisplayID display, bool send_event)
+{
+    CGDisplayModeRef moderef = CGDisplayCopyDisplayMode(display);
+    if (!moderef) {
+        return false;
+    }
+
+    SDL_DisplayData *displaydata = (SDL_DisplayData *)SDL_malloc(sizeof(*displaydata));
+    if (!displaydata) {
+        CGDisplayModeRelease(moderef);
+        return false;
+    }
+    displaydata->display = display;
+
+    CVDisplayLinkRef link = NULL;
+    CVDisplayLinkCreateWithCGDisplay(display, &link);
+
+    SDL_VideoDisplay viddisplay;
+    SDL_zero(viddisplay);
+    viddisplay.name = Cocoa_GetDisplayName(display); // this returns a strdup'ed string
+
+    SDL_DisplayMode mode;
+    if (!GetDisplayMode(moderef, true, NULL, link, &mode)) {
+        CVDisplayLinkRelease(link);
+        CGDisplayModeRelease(moderef);
+        SDL_free(viddisplay.name);
+        SDL_free(displaydata);
+        return false;
+    }
+
+    CVDisplayLinkRelease(link);
+    CGDisplayModeRelease(moderef);
+
+    Cocoa_GetHDRProperties(displaydata->display, &viddisplay.HDR);
+
+    viddisplay.desktop_mode = mode;
+    viddisplay.internal = displaydata;
+    const bool retval = SDL_AddVideoDisplay(&viddisplay, send_event);
+    SDL_free(viddisplay.name);
+    return retval;
+}
+
+static void Cocoa_DisplayReconfigurationCallback(CGDirectDisplayID displayid, CGDisplayChangeSummaryFlags flags, void *userInfo)
+{
+    #if 0
+    SDL_Log("COCOA DISPLAY RECONFIG CALLBACK! display=%u", (unsigned int) displayid);
+    #define CHECK_DISPLAY_RECONFIG_FLAG(x) if (flags & x) { SDL_Log(" - " #x); }
+    CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayBeginConfigurationFlag);
+    CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayMovedFlag);
+    CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplaySetMainFlag);
+    CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplaySetModeFlag);
+    CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayAddFlag);
+    CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayRemoveFlag);
+    CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayEnabledFlag);
+    CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayDisabledFlag);
+    CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayMirrorFlag);
+    CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayUnMirrorFlag);
+    CHECK_DISPLAY_RECONFIG_FLAG(kCGDisplayDesktopShapeChangedFlag);
+    #undef CHECK_DISPLAY_RECONFIG_FLAG
+    #endif
+
+    SDL_VideoDevice *_this = (SDL_VideoDevice *) userInfo;
+    SDL_VideoDisplay *display = Cocoa_FindSDLDisplayByCGDirectDisplayID(_this, displayid);  // will be NULL for newly-added (or newly-unmirrored) displays!
+
+    if (flags & kCGDisplayDisabledFlag) {
+        flags |= kCGDisplayRemoveFlag;  // treat this like a display leaving, even though it's still plugged in.
+    }
+
+    if (flags & kCGDisplayEnabledFlag) {
+        flags |= kCGDisplayAddFlag;  // treat this like a display leaving, even though it's still plugged in.
+    }
+
+    if (flags & kCGDisplayMirrorFlag) {
+        flags |= kCGDisplayRemoveFlag;  // treat this like a display leaving, even though it's still actually here.
+    }
+
+    if (flags & kCGDisplayUnMirrorFlag) {
+        flags |= kCGDisplayAddFlag;  // treat this like a new display arriving, even though it was here all along.
+    }
+
+    if ((flags & kCGDisplayAddFlag) && (flags & kCGDisplayRemoveFlag)) {
+        // both adding _and_ removing? Treat it as a remove exclusively. This can happen if a display is unmirroring because it's being disabled, etc.
+        flags &= ~kCGDisplayAddFlag;
+    }
+
+    if (flags & kCGDisplayAddFlag) {
+        if (!display) {
+            if (!Cocoa_AddDisplay(displayid, true)) {
+                return;  // oh well.
+            }
+            display = Cocoa_FindSDLDisplayByCGDirectDisplayID(_this, displayid);
+            SDL_assert(display != NULL);
+        }
+    }
+
+    if (flags & kCGDisplayRemoveFlag) {
+        if (display) {
+            SDL_DelVideoDisplay(display->id, true);
+            display = NULL;
+        }
+    }
+
+    if (flags & kCGDisplaySetModeFlag) {
+        if (display) {
+            CGDisplayModeRef moderef = CGDisplayCopyDisplayMode(displayid);
+            if (moderef) {
+                CVDisplayLinkRef link = NULL;
+                CVDisplayLinkCreateWithCGDisplay(displayid, &link);
+                if (link) {
+                    SDL_DisplayMode mode;
+                    if (GetDisplayMode(moderef, true, NULL, link, &mode)) {
+                        SDL_SetCurrentDisplayMode(display, &mode);
+                    }
+                    CVDisplayLinkRelease(link);
+                }
+                CGDisplayModeRelease(moderef);
+            }
+        }
+    }
+
+    if (flags & kCGDisplaySetMainFlag) {
+        if (display) {
+            for (int i = 0; i < _this->num_displays; i++) {
+                if (_this->displays[i] == display) {
+                    if (i > 0) {
+                        // move this display to the front of _this->displays so it's treated as primary.
+                        SDL_memmove(&_this->displays[1], &_this->displays[0], sizeof (*_this->displays) * i);
+                        _this->displays[0] = display;
+                    }
+                    flags |= kCGDisplayMovedFlag;  // we don't have an SDL event atm for "this display became primary," so at least let everyone know it "moved".
+                    break;
+                }
+            }
+        }
+    }
+
+    if (flags & kCGDisplayMovedFlag) {
+        if (display) {
+            SDL_SendDisplayEvent(display, SDL_EVENT_DISPLAY_MOVED, 0, 0);
+        }
+    }
+
+    if (flags & kCGDisplayDesktopShapeChangedFlag) {
+        SDL_UpdateDesktopBounds();
+    }
+}
+
 void Cocoa_InitModes(SDL_VideoDevice *_this)
 {
     @autoreleasepool {
         CGDisplayErr result;
-        CGDirectDisplayID *displays;
-        CGDisplayCount numDisplays;
-        bool isstack;
-        int pass, i;
+        CGDisplayCount numDisplays = 0;
 
         result = CGGetOnlineDisplayList(0, NULL, &numDisplays);
         if (result != kCGErrorSuccess) {
             CG_SetError("CGGetOnlineDisplayList()", result);
             return;
         }
-        displays = SDL_small_alloc(CGDirectDisplayID, numDisplays, &isstack);
+
+        bool isstack;
+        CGDirectDisplayID *displays = SDL_small_alloc(CGDirectDisplayID, numDisplays, &isstack);
+
         result = CGGetOnlineDisplayList(numDisplays, displays, &numDisplays);
         if (result != kCGErrorSuccess) {
             CG_SetError("CGGetOnlineDisplayList()", result);
@@ -331,15 +491,12 @@ void Cocoa_InitModes(SDL_VideoDevice *_this)
             return;
         }
 
-        // Pick up the primary display in the first pass, then get the rest
-        for (pass = 0; pass < 2; ++pass) {
-            for (i = 0; i < numDisplays; ++i) {
-                SDL_VideoDisplay display;
-                SDL_DisplayData *displaydata;
-                SDL_DisplayMode mode;
-                CGDisplayModeRef moderef = NULL;
-                CVDisplayLinkRef link = NULL;
+        // future updates to the display graph will come through this callback.
+        CGDisplayRegisterReconfigurationCallback(Cocoa_DisplayReconfigurationCallback, _this);
 
+        // Pick up the primary display in the first pass, then get the rest
+        for (int pass = 0; pass < 2; ++pass) {
+            for (int i = 0; i < numDisplays; ++i) {
                 if (pass == 0) {
                     if (!CGDisplayIsMain(displays[i])) {
                         continue;
@@ -354,41 +511,7 @@ void Cocoa_InitModes(SDL_VideoDevice *_this)
                     continue;
                 }
 
-                moderef = CGDisplayCopyDisplayMode(displays[i]);
-
-                if (!moderef) {
-                    continue;
-                }
-
-                displaydata = (SDL_DisplayData *)SDL_malloc(sizeof(*displaydata));
-                if (!displaydata) {
-                    CGDisplayModeRelease(moderef);
-                    continue;
-                }
-                displaydata->display = displays[i];
-
-                CVDisplayLinkCreateWithCGDisplay(displays[i], &link);
-
-                SDL_zero(display);
-                // this returns a strdup'ed string
-                display.name = Cocoa_GetDisplayName(displays[i]);
-                if (!GetDisplayMode(_this, moderef, true, NULL, link, &mode)) {
-                    CVDisplayLinkRelease(link);
-                    CGDisplayModeRelease(moderef);
-                    SDL_free(display.name);
-                    SDL_free(displaydata);
-                    continue;
-                }
-
-                CVDisplayLinkRelease(link);
-                CGDisplayModeRelease(moderef);
-
-                Cocoa_GetHDRProperties(displaydata->display, &display.HDR);
-
-                display.desktop_mode = mode;
-                display.internal = displaydata;
-                SDL_AddVideoDisplay(&display, false);
-                SDL_free(display.name);
+                Cocoa_AddDisplay(displays[i], false);
             }
         }
         SDL_small_free(displays, isstack);
@@ -486,7 +609,7 @@ bool Cocoa_GetDisplayModes(SDL_VideoDevice *_this, SDL_VideoDisplay *display)
             CGDisplayModeRef moderef = (CGDisplayModeRef)CFArrayGetValueAtIndex(modes, i);
             SDL_DisplayMode mode;
 
-            if (GetDisplayMode(_this, moderef, false, modes, link, &mode)) {
+            if (GetDisplayMode(moderef, false, modes, link, &mode)) {
                 if (!SDL_AddFullscreenDisplayMode(display, &mode)) {
                     CFRelease(mode.internal->modes);
                     SDL_free(mode.internal);
@@ -559,6 +682,8 @@ void Cocoa_QuitModes(SDL_VideoDevice *_this)
 {
     int i, j;
 
+    CGDisplayRemoveReconfigurationCallback(Cocoa_DisplayReconfigurationCallback, _this);
+
     for (i = 0; i < _this->num_displays; ++i) {
         SDL_VideoDisplay *display = _this->displays[i];
         SDL_DisplayModeData *mode;
diff --git a/src/video/cocoa/SDL_cocoawindow.m b/src/video/cocoa/SDL_cocoawindow.m
index cfc1d2315985f..9ae7b16350612 100644
--- a/src/video/cocoa/SDL_cocoawindow.m
+++ b/src/video/cocoa/SDL_cocoawindow.m
@@ -2934,17 +2934,11 @@ SDL_DisplayID Cocoa_GetDisplayForWindow(SDL_VideoDevice *_this, SDL_Window *wind
         screen = data.nswindow.screen;
 
         if (screen != nil) {
-            CGDirectDisplayID displayid;
-            int i;
-
             // https://developer.apple.com/documentation/appkit/nsscreen/1388360-devicedescription?language=objc
-            displayid = [[screen.deviceDescription objectForKey:@"NSScreenNumber"] unsignedIntValue];
-
-            for (i = 0; i < _this->num_displays; i++) {
-                SDL_DisplayData *displaydata = _this->displays[i]->internal;
-                if (displaydata != NULL && displaydata->display == displayid) {
-                    return _this->displays[i]->id;
-                }
+            CGDirectDisplayID displayid = [[screen.deviceDescription objectForKey:@"NSScreenNumber"] unsignedIntValue];
+            SDL_VideoDisplay *display = Cocoa_FindSDLDisplayByCGDirectDisplayID(_this, displayid);
+            if (display) {
+                return display->id;
             }
         }