SDL: video: Add a hint to disable auto mode switching if an exclusive fullscreen window moves between displays

From 0bfe0497f398d0027db522ac7c1853327c7fc638 Mon Sep 17 00:00:00 2001
From: Frank Praznik <[EMAIL REDACTED]>
Date: Fri, 14 Mar 2025 11:50:25 -0400
Subject: [PATCH] video: Add a hint to disable auto mode switching if an
 exclusive fullscreen window moves between displays

The existing behavior helps clients that don't expect exclusive fullscreen windows to move by maintaining a consistent size and mode, however, some are aware that this can occur and want to handle mode selection themselves.

Add a hint to disable auto mode switching when an exclusive fullscreen window moves to accommodate this use case, and don't override fullscreen changes that may occur in an event watcher between the display changed event being posted and SDL running the display changed handler, as the mode switch may have already been handled there by the client.
---
 include/SDL3/SDL_hints.h      | 19 +++++++++++++++++++
 src/events/SDL_windowevents.c |  1 +
 src/video/SDL_sysvideo.h      |  1 +
 src/video/SDL_video.c         | 19 +++++++++++++++----
 4 files changed, 36 insertions(+), 4 deletions(-)

diff --git a/include/SDL3/SDL_hints.h b/include/SDL3/SDL_hints.h
index 2793d09421cb5..2b073b6abe2f1 100644
--- a/include/SDL3/SDL_hints.h
+++ b/include/SDL3/SDL_hints.h
@@ -3405,6 +3405,25 @@ extern "C" {
  */
 #define SDL_HINT_VIDEO_MAC_FULLSCREEN_MENU_VISIBILITY "SDL_VIDEO_MAC_FULLSCREEN_MENU_VISIBILITY"
 
+/**
+ * A variable controlling whether SDL will attempt to automatically set the
+ * destination display to a mode most closely matching that of the previous
+ * display if an exclusive fullscreen window is moved onto it.
+ *
+ * The variable can be set to the following values:
+ *
+ * - "0": SDL will not attempt to automatically set a matching mode on the destination display.
+ *   If an exclusive fullscreen window is moved to a new display, the window will become
+ *   fullscreen desktop.
+ * - "1": SDL will attempt to automatically set a mode on the destination display that most closely
+ *   matches the mode of the display that the exclusive fullscreen window was previously on. (default)
+ *
+ * This hint can be set anytime.
+ *
+ * \since This hint is available since SDL 3.2.10.
+ */
+#define SDL_HINT_VIDEO_MATCH_EXCLUSIVE_MODE_ON_MOVE "SDL_VIDEO_MATCH_EXCLUSIVE_MODE_ON_MOVE"
+
 /**
  * A variable controlling whether fullscreen windows are minimized when they
  * lose focus.
diff --git a/src/events/SDL_windowevents.c b/src/events/SDL_windowevents.c
index e20cd3ab0da7a..fa5488a640124 100644
--- a/src/events/SDL_windowevents.c
+++ b/src/events/SDL_windowevents.c
@@ -188,6 +188,7 @@ bool SDL_SendWindowEvent(SDL_Window *window, SDL_EventType windowevent, int data
         if (data1 == 0 || (SDL_DisplayID)data1 == window->last_displayID) {
             return false;
         }
+        window->update_fullscreen_on_display_changed = true;
         window->last_displayID = (SDL_DisplayID)data1;
         break;
     case SDL_EVENT_WINDOW_OCCLUDED:
diff --git a/src/video/SDL_sysvideo.h b/src/video/SDL_sysvideo.h
index 6da8bd2e1853e..cf856b096d100 100644
--- a/src/video/SDL_sysvideo.h
+++ b/src/video/SDL_sysvideo.h
@@ -103,6 +103,7 @@ struct SDL_Window
     bool restore_on_show; // Child was hidden recursively by the parent, restore when shown.
     bool last_position_pending; // This should NOT be cleared by the backend, as it is used for fullscreen positioning.
     bool last_size_pending; // This should be cleared by the backend if the new size cannot be applied.
+    bool update_fullscreen_on_display_changed;
     bool is_destroying;
     bool is_dropping; // drag/drop in progress, expecting SDL_SendDropComplete().
 
diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c
index 3ed49941e46a9..a940cb8c7c002 100644
--- a/src/video/SDL_video.c
+++ b/src/video/SDL_video.c
@@ -1854,6 +1854,7 @@ bool SDL_UpdateFullscreenMode(SDL_Window *window, SDL_FullscreenOp fullscreen, b
     CHECK_WINDOW_MAGIC(window, false);
 
     window->fullscreen_exclusive = false;
+    window->update_fullscreen_on_display_changed = false;
 
     // If we are in the process of hiding don't go back to fullscreen
     if (window->is_destroying || window->is_hiding) {
@@ -3940,16 +3941,26 @@ void SDL_OnWindowHidden(SDL_Window *window)
 
 void SDL_OnWindowDisplayChanged(SDL_Window *window)
 {
-    if (window->flags & SDL_WINDOW_FULLSCREEN) {
-        SDL_DisplayID displayID = SDL_GetDisplayForWindowPosition(window);
+    // Don't run this if a fullscreen change was made in an event watcher callback in response to a display changed event.
+    if (window->update_fullscreen_on_display_changed && (window->flags & SDL_WINDOW_FULLSCREEN)) {
+        const bool auto_mode_switch = SDL_GetHintBoolean(SDL_HINT_VIDEO_MATCH_EXCLUSIVE_MODE_ON_MOVE, true);
 
-        if (window->requested_fullscreen_mode.w != 0 || window->requested_fullscreen_mode.h != 0) {
+        if (auto_mode_switch && (window->requested_fullscreen_mode.w != 0 || window->requested_fullscreen_mode.h != 0)) {
+            SDL_DisplayID displayID = SDL_GetDisplayForWindowPosition(window);
             bool include_high_density_modes = false;
 
             if (window->requested_fullscreen_mode.pixel_density > 1.0f) {
                 include_high_density_modes = true;
             }
-            SDL_GetClosestFullscreenDisplayMode(displayID, window->requested_fullscreen_mode.w, window->requested_fullscreen_mode.h, window->requested_fullscreen_mode.refresh_rate, include_high_density_modes, &window->current_fullscreen_mode);
+            const bool found_match = SDL_GetClosestFullscreenDisplayMode(displayID, window->requested_fullscreen_mode.w, window->requested_fullscreen_mode.h,
+                                                                         window->requested_fullscreen_mode.refresh_rate, include_high_density_modes, &window->current_fullscreen_mode);
+
+            // If a mode without matching dimensions was not found, just go to fullscreen desktop.
+            if (!found_match ||
+                window->requested_fullscreen_mode.w != window->current_fullscreen_mode.w ||
+                window->requested_fullscreen_mode.h != window->current_fullscreen_mode.h) {
+                SDL_zero(window->current_fullscreen_mode);
+            }
         } else {
             SDL_zero(window->current_fullscreen_mode);
         }