SDL: Added SDL_GetSystemTheme() to return whether the system is using a dark or light color theme, and...

From 8994878767cfb9403f525d12c0770c1e149a4d08 Mon Sep 17 00:00:00 2001
From: Sam Lantinga <[EMAIL REDACTED]>
Date: Tue, 7 Mar 2023 00:01:34 -0800
Subject: [PATCH] Added SDL_GetSystemTheme() to return whether the system is
 using a dark or light color theme, and SDL_EVENT_SYSTEM_THEME_CHANGED is sent
 when this changes

Fixes https://github.com/libsdl-org/SDL/issues/5334
Fixes https://github.com/libsdl-org/SDL/issues/6958
Closes https://github.com/libsdl-org/SDL/pull/6440
---
 WhatsNew.txt                                  |  3 ++
 .../main/java/org/libsdl/app/SDLActivity.java | 19 ++++++++
 include/SDL3/SDL_events.h                     |  2 +
 include/SDL3/SDL_video.h                      | 43 +++++++++++++------
 src/core/android/SDL_android.c                | 11 +++++
 src/dynapi/SDL_dynapi.sym                     |  1 +
 src/dynapi/SDL_dynapi_overrides.h             |  1 +
 src/dynapi/SDL_dynapi_procs.h                 |  1 +
 src/events/SDL_events.c                       |  7 +++
 src/events/SDL_events_c.h                     |  1 +
 src/test/SDL_test_common.c                    | 18 ++++++++
 src/video/SDL_sysvideo.h                      |  2 +
 src/video/SDL_video.c                         | 21 ++++++++-
 src/video/android/SDL_androidvideo.c          | 17 ++++++++
 src/video/android/SDL_androidvideo.h          |  1 +
 src/video/cocoa/SDL_cocoaevents.m             | 20 ++++++++-
 src/video/cocoa/SDL_cocoavideo.h              |  1 +
 src/video/cocoa/SDL_cocoavideo.m              | 13 ++++++
 src/video/uikit/SDL_uikitvideo.h              |  2 +
 src/video/uikit/SDL_uikitvideo.m              | 22 ++++++++--
 src/video/uikit/SDL_uikitviewcontroller.h     |  2 +
 src/video/uikit/SDL_uikitviewcontroller.m     |  5 +++
 src/video/windows/SDL_windowsevents.c         |  4 ++
 src/video/windows/SDL_windowsvideo.c          | 23 +++++++++-
 src/video/windows/SDL_windowsvideo.h          |  1 +
 src/video/windows/SDL_windowswindow.c         | 22 ++++++++++
 src/video/windows/SDL_windowswindow.h         |  1 +
 27 files changed, 243 insertions(+), 21 deletions(-)

diff --git a/WhatsNew.txt b/WhatsNew.txt
index 08f6f323a671..880ca0e4208c 100644
--- a/WhatsNew.txt
+++ b/WhatsNew.txt
@@ -12,6 +12,9 @@ General:
 * The preprocessor symbol __IPHONEOS__ has been renamed __IOS__
 * SDL_stdinc.h no longer includes stdio.h, stdlib.h, etc., it only provides the SDL C runtime functionality
 * SDL_intrin.h now includes the intrinsics headers that were in SDL_cpuinfo.h
+* Added SDL_GetSystemTheme() to return whether the system is using a dark or light color theme, and SDL_EVENT_SYSTEM_THEME_CHANGED is sent when this changes
+* Added SDL_GetDisplays() to return a list of connected displays
+* Added SDL_GetPrimaryDisplay() to get the instance ID of the primary display
 * Added SDL_CreateSurface() and SDL_CreateSurfaceFrom() which replace SDL_CreateRGBSurface*(), and can also be used to create YUV surfaces
 * Added SDL_GetJoysticks(), SDL_GetJoystickInstanceName(), SDL_GetJoystickInstancePath(), SDL_GetJoystickInstancePlayerIndex(), SDL_GetJoystickInstanceGUID(), SDL_GetJoystickInstanceVendor(), SDL_GetJoystickInstanceProduct(), SDL_GetJoystickInstanceProductVersion(), and SDL_GetJoystickInstanceType() to directly query the list of available joysticks
 * Added SDL_GetGamepads(), SDL_GetGamepadInstanceName(), SDL_GetGamepadInstancePath(), SDL_GetGamepadInstancePlayerIndex(), SDL_GetGamepadInstanceGUID(), SDL_GetGamepadInstanceVendor(), SDL_GetGamepadInstanceProduct(), SDL_GetGamepadInstanceProductVersion(), and SDL_GetGamepadInstanceType() to directly query the list of available gamepads
diff --git a/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java b/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java
index a95dc45d67e3..de03ba790b28 100644
--- a/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java
+++ b/android-project/app/src/main/java/org/libsdl/app/SDLActivity.java
@@ -412,6 +412,15 @@ public void onClick(DialogInterface dialog,int id) {
         } catch(Exception ignored) {
         }
 
+        switch (getContext().getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) {
+        case Configuration.UI_MODE_NIGHT_NO:
+            SDLActivity.onNativeDarkModeChanged(false);
+            break;
+        case Configuration.UI_MODE_NIGHT_YES:
+            SDLActivity.onNativeDarkModeChanged(true);
+            break;
+        }
+
         setContentView(mLayout);
 
         setWindowStyle(false);
@@ -577,6 +586,15 @@ public void onConfigurationChanged(Configuration newConfig) {
             mCurrentLocale = newConfig.locale;
             SDLActivity.onNativeLocaleChanged();
         }
+
+        switch (newConfig.uiMode & Configuration.UI_MODE_NIGHT_MASK) {
+        case Configuration.UI_MODE_NIGHT_NO:
+            SDLActivity.onNativeDarkModeChanged(false);
+            break;
+        case Configuration.UI_MODE_NIGHT_YES:
+            SDLActivity.onNativeDarkModeChanged(true);
+            break;
+        }
     }
 
     @Override
@@ -931,6 +949,7 @@ public static native void onNativeTouch(int touchDevId, int pointerFingerId,
     public static native void nativeAddTouch(int touchId, String name);
     public static native void nativePermissionResult(int requestCode, boolean result);
     public static native void onNativeLocaleChanged();
+    public static native void onNativeDarkModeChanged(boolean enabled);
 
     /**
      * This method is called by SDL using JNI.
diff --git a/include/SDL3/SDL_events.h b/include/SDL3/SDL_events.h
index 482fc6d3cc51..8059da3522b8 100644
--- a/include/SDL3/SDL_events.h
+++ b/include/SDL3/SDL_events.h
@@ -87,6 +87,8 @@ typedef enum
 
     SDL_EVENT_LOCALE_CHANGED,  /**< The user's locale preferences have changed. */
 
+    SDL_EVENT_SYSTEM_THEME_CHANGED, /**< The system theme changed */
+
     /* Display events */
     /* 0x150 was SDL_DISPLAYEVENT, reserve the number for sdl2-compat */
     SDL_EVENT_DISPLAY_ORIENTATION = 0x151, /**< Display orientation has changed to data1 */
diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h
index ebdc8a779a6d..ae11aefc2a1f 100644
--- a/include/SDL3/SDL_video.h
+++ b/include/SDL3/SDL_video.h
@@ -43,6 +43,16 @@ extern "C" {
 typedef Uint32 SDL_DisplayID;
 typedef Uint32 SDL_WindowID;
 
+/**
+ *  \brief System theme
+ */
+typedef enum
+{
+    SDL_SYSTEM_THEME_UNKNOWN,   /**< Unknown system theme */
+    SDL_SYSTEM_THEME_LIGHT,     /**< Light colored system theme */
+    SDL_SYSTEM_THEME_DARK,      /**< Dark colored system theme */
+} SDL_SystemTheme;
+
 /**
  *  \brief  The structure that defines a display mode
  *
@@ -65,6 +75,18 @@ typedef struct
     void *driverdata;           /**< driver-specific data, initialize to 0 */
 } SDL_DisplayMode;
 
+/**
+ *  \brief Display orientation
+ */
+typedef enum
+{
+    SDL_ORIENTATION_UNKNOWN,            /**< The display orientation can't be determined */
+    SDL_ORIENTATION_LANDSCAPE,          /**< The display is in landscape mode, with the right side up, relative to portrait mode */
+    SDL_ORIENTATION_LANDSCAPE_FLIPPED,  /**< The display is in landscape mode, with the left side up, relative to portrait mode */
+    SDL_ORIENTATION_PORTRAIT,           /**< The display is in portrait mode */
+    SDL_ORIENTATION_PORTRAIT_FLIPPED    /**< The display is in portrait mode, upside down */
+} SDL_DisplayOrientation;
+
 /**
  *  \brief The type used to identify a window
  *
@@ -151,18 +173,6 @@ typedef enum
 #define SDL_WINDOWPOS_ISCENTERED(X)    \
             (((X)&0xFFFF0000) == SDL_WINDOWPOS_CENTERED_MASK)
 
-/**
- *  \brief Display orientation
- */
-typedef enum
-{
-    SDL_ORIENTATION_UNKNOWN,            /**< The display orientation can't be determined */
-    SDL_ORIENTATION_LANDSCAPE,          /**< The display is in landscape mode, with the right side up, relative to portrait mode */
-    SDL_ORIENTATION_LANDSCAPE_FLIPPED,  /**< The display is in landscape mode, with the left side up, relative to portrait mode */
-    SDL_ORIENTATION_PORTRAIT,           /**< The display is in portrait mode */
-    SDL_ORIENTATION_PORTRAIT_FLIPPED    /**< The display is in portrait mode, upside down */
-} SDL_DisplayOrientation;
-
 /**
  *  \brief Window flash operation
  */
@@ -297,6 +307,15 @@ extern DECLSPEC const char *SDLCALL SDL_GetVideoDriver(int index);
  */
 extern DECLSPEC const char *SDLCALL SDL_GetCurrentVideoDriver(void);
 
+/**
+ * Get the current system theme
+ *
+ * \returns the current system theme, light, dark, or unknown
+ *
+ * \since This function is available since SDL 3.0.0.
+ */
+extern DECLSPEC SDL_SystemTheme SDLCALL SDL_GetSystemTheme(void);
+
 /**
  * Get a list of currently connected displays.
  *
diff --git a/src/core/android/SDL_android.c b/src/core/android/SDL_android.c
index f01267fae8f4..2508f4331737 100644
--- a/src/core/android/SDL_android.c
+++ b/src/core/android/SDL_android.c
@@ -124,6 +124,9 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeLowMemory)(
 JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeLocaleChanged)(
     JNIEnv *env, jclass cls);
 
+JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeDarkModeChanged)(
+    JNIEnv *env, jclass cls, jboolean enabled);
+
 JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSendQuit)(
     JNIEnv *env, jclass cls);
 
@@ -183,6 +186,7 @@ static JNINativeMethod SDLActivity_tab[] = {
     { "onNativeClipboardChanged", "()V", SDL_JAVA_INTERFACE(onNativeClipboardChanged) },
     { "nativeLowMemory", "()V", SDL_JAVA_INTERFACE(nativeLowMemory) },
     { "onNativeLocaleChanged", "()V", SDL_JAVA_INTERFACE(onNativeLocaleChanged) },
+    { "onNativeDarkModeChanged", "(Z)V", SDL_JAVA_INTERFACE(onNativeDarkModeChanged) },
     { "nativeSendQuit", "()V", SDL_JAVA_INTERFACE(nativeSendQuit) },
     { "nativeQuit", "()V", SDL_JAVA_INTERFACE(nativeQuit) },
     { "nativePause", "()V", SDL_JAVA_INTERFACE(nativePause) },
@@ -1199,6 +1203,13 @@ JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeLocaleChanged)(
     SDL_SendAppEvent(SDL_EVENT_LOCALE_CHANGED);
 }
 
+/* Dark mode */
+JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(onNativeDarkModeChanged)(
+    JNIEnv *env, jclass cls, jboolean enabled)
+{
+    Android_SetDarkMode(enabled);
+}
+
 /* Send Quit event to "SDLThread" thread */
 JNIEXPORT void JNICALL SDL_JAVA_INTERFACE(nativeSendQuit)(
     JNIEnv *env, jclass cls)
diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym
index a9f6680f36bb..025d992d5075 100644
--- a/src/dynapi/SDL_dynapi.sym
+++ b/src/dynapi/SDL_dynapi.sym
@@ -838,6 +838,7 @@ SDL3_0.0.0 {
     SDL_SetRenderScale;
     SDL_GetRenderScale;
     SDL_GetRenderWindowSize;
+    SDL_GetSystemTheme;
     # extra symbols go here (don't modify this line)
   local: *;
 };
diff --git a/src/dynapi/SDL_dynapi_overrides.h b/src/dynapi/SDL_dynapi_overrides.h
index f45dbbfebe3f..5d86ddf8b3ff 100644
--- a/src/dynapi/SDL_dynapi_overrides.h
+++ b/src/dynapi/SDL_dynapi_overrides.h
@@ -865,3 +865,4 @@
 #define SDL_SetRenderScale SDL_SetRenderScale_REAL
 #define SDL_GetRenderScale SDL_GetRenderScale_REAL
 #define SDL_GetRenderWindowSize SDL_GetRenderWindowSize_REAL
+#define SDL_GetSystemTheme SDL_GetSystemTheme_REAL
diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h
index fab380c95903..94d61004d41a 100644
--- a/src/dynapi/SDL_dynapi_procs.h
+++ b/src/dynapi/SDL_dynapi_procs.h
@@ -910,3 +910,4 @@ SDL_DYNAPI_PROC(int,SDL_ConvertEventToRenderCoordinates,(SDL_Renderer *a, SDL_Ev
 SDL_DYNAPI_PROC(int,SDL_SetRenderScale,(SDL_Renderer *a, float b, float c),(a,b,c),return)
 SDL_DYNAPI_PROC(int,SDL_GetRenderScale,(SDL_Renderer *a, float *b, float *c),(a,b,c),return)
 SDL_DYNAPI_PROC(int,SDL_GetRenderWindowSize,(SDL_Renderer *a, int *b, int *c),(a,b,c),return)
+SDL_DYNAPI_PROC(SDL_SystemTheme,SDL_GetSystemTheme,(void),(),return)
diff --git a/src/events/SDL_events.c b/src/events/SDL_events.c
index 669c0801c446..772173e8e9f8 100644
--- a/src/events/SDL_events.c
+++ b/src/events/SDL_events.c
@@ -204,6 +204,8 @@ static void SDL_LogEvent(const SDL_Event *event)
         break;
         SDL_EVENT_CASE(SDL_EVENT_LOCALE_CHANGED)
         break;
+        SDL_EVENT_CASE(SDL_EVENT_SYSTEM_THEME_CHANGED)
+        break;
         SDL_EVENT_CASE(SDL_EVENT_KEYMAP_CHANGED)
         break;
         SDL_EVENT_CASE(SDL_EVENT_CLIPBOARD_UPDATE)
@@ -1346,6 +1348,11 @@ int SDL_SendLocaleChangedEvent(void)
     return SDL_SendAppEvent(SDL_EVENT_LOCALE_CHANGED);
 }
 
+int SDL_SendSystemThemeChangedEvent(void)
+{
+    return SDL_SendAppEvent(SDL_EVENT_SYSTEM_THEME_CHANGED);
+}
+
 int SDL_InitEvents(void)
 {
 #if !SDL_JOYSTICK_DISABLED
diff --git a/src/events/SDL_events_c.h b/src/events/SDL_events_c.h
index faf0a626b8ff..44c3eb8d110f 100644
--- a/src/events/SDL_events_c.h
+++ b/src/events/SDL_events_c.h
@@ -44,6 +44,7 @@ extern int SDL_SendAppEvent(SDL_EventType eventType);
 extern int SDL_SendSysWMEvent(SDL_SysWMmsg *message);
 extern int SDL_SendKeymapChangedEvent(void);
 extern int SDL_SendLocaleChangedEvent(void);
+extern int SDL_SendSystemThemeChangedEvent(void);
 
 extern int SDL_SendQuit(void);
 
diff --git a/src/test/SDL_test_common.c b/src/test/SDL_test_common.c
index 0aecf4d72ac1..7b049b3dfc01 100644
--- a/src/test/SDL_test_common.c
+++ b/src/test/SDL_test_common.c
@@ -1438,6 +1438,21 @@ SDLTest_CommonInit(SDLTest_CommonState *state)
     return SDL_TRUE;
 }
 
+static const char *SystemThemeName(void)
+{
+    switch (SDL_GetSystemTheme()) {
+#define CASE(X)               \
+    case SDL_SYSTEM_THEME_##X: \
+        return #X
+        CASE(UNKNOWN);
+        CASE(LIGHT);
+        CASE(DARK);
+#undef CASE
+    default:
+        return "???";
+    }
+}
+
 static const char *DisplayOrientationName(int orientation)
 {
     switch (orientation) {
@@ -1505,6 +1520,9 @@ static const char *GamepadButtonName(const SDL_GamepadButton button)
 static void SDLTest_PrintEvent(SDL_Event *event)
 {
     switch (event->type) {
+    case SDL_EVENT_SYSTEM_THEME_CHANGED:
+        SDL_Log("SDL EVENT: System theme changed to %s\n", SystemThemeName());
+        break;
     case SDL_EVENT_DISPLAY_CONNECTED:
         SDL_Log("SDL EVENT: Display %" SDL_PRIu32 " connected",
                 event->display.displayID);
diff --git a/src/video/SDL_sysvideo.h b/src/video/SDL_sysvideo.h
index 2c690cf81259..1942d0978863 100644
--- a/src/video/SDL_sysvideo.h
+++ b/src/video/SDL_sysvideo.h
@@ -354,6 +354,7 @@ struct SDL_VideoDevice
     char *primary_selection_text;
     SDL_bool setting_display_mode;
     Uint32 quirk_flags;
+    SDL_SystemTheme system_theme;
 
     /* * * */
     /* Data used by the GL drivers */
@@ -476,6 +477,7 @@ extern VideoBootStrap NGAGE_bootstrap;
 extern SDL_bool SDL_OnVideoThread(void);
 extern SDL_VideoDevice *SDL_GetVideoDevice(void);
 extern SDL_bool SDL_IsVideoContextExternal(void);
+extern void SDL_SetSystemTheme(SDL_SystemTheme theme);
 extern SDL_DisplayID SDL_AddBasicVideoDisplay(const SDL_DisplayMode *desktop_mode);
 extern SDL_DisplayID SDL_AddVideoDisplay(const SDL_VideoDisplay *display, SDL_bool send_event);
 extern void SDL_DelVideoDisplay(SDL_DisplayID display, SDL_bool send_event);
diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c
index 3bf7a5f80139..ccb51f11d98a 100644
--- a/src/video/SDL_video.c
+++ b/src/video/SDL_video.c
@@ -576,14 +576,31 @@ SDL_VideoDevice *SDL_GetVideoDevice(void)
     return _this;
 }
 
+SDL_bool SDL_OnVideoThread(void)
+{
+    return (_this && SDL_ThreadID() == _this->thread) ? SDL_TRUE : SDL_FALSE;
+}
+
 SDL_bool SDL_IsVideoContextExternal(void)
 {
     return SDL_GetHintBoolean(SDL_HINT_VIDEO_EXTERNAL_CONTEXT, SDL_FALSE);
 }
 
-SDL_bool SDL_OnVideoThread(void)
+void SDL_SetSystemTheme(SDL_SystemTheme theme)
 {
-    return (_this && SDL_ThreadID() == _this->thread) ? SDL_TRUE : SDL_FALSE;
+    if (_this && theme != _this->system_theme) {
+        _this->system_theme = theme;
+        SDL_SendSystemThemeChangedEvent();
+    }
+}
+
+SDL_SystemTheme SDL_GetSystemTheme(void)
+{
+    if (_this) {
+        return _this->system_theme;
+    } else {
+        return SDL_SYSTEM_THEME_UNKNOWN;
+    }
 }
 
 static void SDL_FinalizeDisplayMode(SDL_DisplayMode *mode)
diff --git a/src/video/android/SDL_androidvideo.c b/src/video/android/SDL_androidvideo.c
index 05ef30179378..a97ac6105768 100644
--- a/src/video/android/SDL_androidvideo.c
+++ b/src/video/android/SDL_androidvideo.c
@@ -65,6 +65,7 @@ static float Android_ScreenRate = 0.0f;
 SDL_sem *Android_PauseSem = NULL;
 SDL_sem *Android_ResumeSem = NULL;
 SDL_mutex *Android_ActivityMutex = NULL;
+static SDL_SystemTheme Android_SystemTheme;
 
 static int Android_SuspendScreenSaver(_THIS)
 {
@@ -98,6 +99,7 @@ static SDL_VideoDevice *Android_CreateDevice(void)
     }
 
     device->driverdata = data;
+    device->system_theme = Android_SystemTheme;
 
     /* Set the function pointers */
     device->VideoInit = Android_VideoInit;
@@ -284,4 +286,19 @@ void Android_SendResize(SDL_Window *window)
     }
 }
 
+void Android_SetDarkMode(SDL_bool enabled)
+{
+    SDL_VideoDevice *device = SDL_GetVideoDevice();
+
+    if (enabled) {
+        Android_SystemTheme = SDL_SYSTEM_THEME_DARK;
+    } else {
+        Android_SystemTheme = SDL_SYSTEM_THEME_LIGHT;
+    }
+
+    if (device) {
+        SDL_SetSystemTheme(Android_SystemTheme);
+    }
+}
+
 #endif /* SDL_VIDEO_DRIVER_ANDROID */
diff --git a/src/video/android/SDL_androidvideo.h b/src/video/android/SDL_androidvideo.h
index ae24b1f74b0d..7100b20e0faf 100644
--- a/src/video/android/SDL_androidvideo.h
+++ b/src/video/android/SDL_androidvideo.h
@@ -29,6 +29,7 @@
 extern void Android_SetScreenResolution(int surfaceWidth, int surfaceHeight, int deviceWidth, int deviceHeight, float density, float rate);
 extern void Android_SetFormat(int format_wanted, int format_got);
 extern void Android_SendResize(SDL_Window *window);
+extern void Android_SetDarkMode(SDL_bool enabled);
 
 /* Private display data */
 
diff --git a/src/video/cocoa/SDL_cocoaevents.m b/src/video/cocoa/SDL_cocoaevents.m
index 6cb6da45781d..a2823343ff0e 100644
--- a/src/video/cocoa/SDL_cocoaevents.m
+++ b/src/video/cocoa/SDL_cocoaevents.m
@@ -129,6 +129,10 @@ @interface SDLAppDelegate : NSObject <NSApplicationDelegate>
 
 - (id)init;
 - (void)localeDidChange:(NSNotification *)notification;
+- (void)observeValueForKeyPath:(NSString *)keyPath 
+                      ofObject:(id)object 
+                        change:(NSDictionary<NSKeyValueChangeKey, id> *)change 
+                       context:(void *)context;
 @end
 
 @implementation SDLAppDelegate : NSObject
@@ -154,6 +158,11 @@ - (id)init
                    selector:@selector(localeDidChange:)
                        name:NSCurrentLocaleDidChangeNotification
                      object:nil];
+
+        [NSApp addObserver:self
+                forKeyPath:@"effectiveAppearance"
+                   options:NSKeyValueObservingOptionInitial
+                   context:nil];
     }
 
     return self;
@@ -166,6 +175,7 @@ - (void)dealloc
     [center removeObserver:self name:NSWindowWillCloseNotification object:nil];
     [center removeObserver:self name:NSApplicationDidBecomeActiveNotification object:nil];
     [center removeObserver:self name:NSCurrentLocaleDidChangeNotification object:nil];
+    [NSApp removeObserver:self forKeyPath:@"effectiveAppearance"];
 
     /* Remove our URL event handler only if we set it */
     if ([NSApp delegate] == self) {
@@ -262,11 +272,19 @@ - (void)focusSomeWindow:(NSNotification *)aNotification
     }
 }
 
-- (void)localeDidChange:(NSNotification *)notification;
+- (void)localeDidChange:(NSNotification *)notification
 {
     SDL_SendLocaleChangedEvent();
 }
 
+- (void)observeValueForKeyPath:(NSString *)keyPath 
+                      ofObject:(id)object 
+                        change:(NSDictionary<NSKeyValueChangeKey, id> *)change 
+                       context:(void *)context
+{
+    SDL_SetSystemTheme(Cocoa_GetSystemTheme());
+}
+
 - (BOOL)application:(NSApplication *)theApplication openFile:(NSString *)filename
 {
     return (BOOL)SDL_SendDropFile(NULL, [filename UTF8String]) && SDL_SendDropComplete(NULL);
diff --git a/src/video/cocoa/SDL_cocoavideo.h b/src/video/cocoa/SDL_cocoavideo.h
index d88b19a916ea..2daa40498edb 100644
--- a/src/video/cocoa/SDL_cocoavideo.h
+++ b/src/video/cocoa/SDL_cocoavideo.h
@@ -108,6 +108,7 @@ DECLARE_ALERT_STYLE(Critical);
 @end
 
 /* Utility functions */
+extern SDL_SystemTheme Cocoa_GetSystemTheme(void);
 extern NSImage *Cocoa_CreateImage(SDL_Surface *surface);
 
 /* Fix build with the 10.11 SDK */
diff --git a/src/video/cocoa/SDL_cocoavideo.m b/src/video/cocoa/SDL_cocoavideo.m
index 1d46bc8bbc71..d849c63320a0 100644
--- a/src/video/cocoa/SDL_cocoavideo.m
+++ b/src/video/cocoa/SDL_cocoavideo.m
@@ -75,6 +75,7 @@ static void Cocoa_DeleteDevice(SDL_VideoDevice *device)
         }
         device->driverdata = (SDL_VideoData *)CFBridgingRetain(data);
         device->wakeup_lock = SDL_CreateMutex();
+        device->system_theme = Cocoa_GetSystemTheme();
 
         /* Set the function pointers */
         device->VideoInit = Cocoa_VideoInit;
@@ -220,6 +221,18 @@ void Cocoa_VideoQuit(_THIS)
     }
 }
 
+/* This function assumes that it's called from within an autorelease pool */
+SDL_SystemTheme Cocoa_GetSystemTheme(void)
+{
+    NSAppearance* appearance = [[NSApplication sharedApplication] effectiveAppearance];
+
+    if ([appearance.name containsString: @"Dark"]) {
+        return SDL_SYSTEM_THEME_DARK;
+    } else {
+        return SDL_SYSTEM_THEME_LIGHT;
+    }
+}
+
 /* This function assumes that it's called from within an autorelease pool */
 NSImage *Cocoa_CreateImage(SDL_Surface *surface)
 {
diff --git a/src/video/uikit/SDL_uikitvideo.h b/src/video/uikit/SDL_uikitvideo.h
index 227aceb61dae..c3892a077e9d 100644
--- a/src/video/uikit/SDL_uikitvideo.h
+++ b/src/video/uikit/SDL_uikitvideo.h
@@ -43,4 +43,6 @@ void UIKit_ForceUpdateHomeIndicator(void);
 
 SDL_bool UIKit_IsSystemVersionAtLeast(double version);
 
+SDL_SystemTheme UIKit_GetSystemTheme(void);
+
 #endif /* SDL_uikitvideo_h_ */
diff --git a/src/video/uikit/SDL_uikitvideo.m b/src/video/uikit/SDL_uikitvideo.m
index f12df8bdeae2..b3dde9c17f6c 100644
--- a/src/video/uikit/SDL_uikitvideo.m
+++ b/src/video/uikit/SDL_uikitvideo.m
@@ -74,6 +74,7 @@ static void UIKit_DeleteDevice(SDL_VideoDevice *device)
         }
 
         device->driverdata = (SDL_VideoData *)CFBridgingRetain(data);
+        device->system_theme = UIKit_GetSystemTheme();
 
         /* Set the function pointers */
         device->VideoInit = UIKit_VideoInit;
@@ -175,14 +176,27 @@ int UIKit_SuspendScreenSaver(_THIS)
     return 0;
 }
 
-SDL_bool
-UIKit_IsSystemVersionAtLeast(double version)
+SDL_bool UIKit_IsSystemVersionAtLeast(double version)
 {
     return [[UIDevice currentDevice].systemVersion doubleValue] >= version;
 }
 
-CGRect
-UIKit_ComputeViewFrame(SDL_Window *window, UIScreen *screen)
+SDL_SystemTheme UIKit_GetSystemTheme(void)
+{
+    if (@available(iOS 12.0, tvOS 10.0, *)) {
+        switch ([UIScreen mainScreen].traitCollection.userInterfaceStyle) {
+        case UIUserInterfaceStyleDark:
+            return SDL_SYSTEM_THEME_DARK;
+        case UIUserInterfaceStyleLight:
+            return SDL_SYSTEM_THEME_LIGHT;
+        default:
+            break;
+        }
+    }
+    return SDL_SYSTEM_THEME_UNKNOWN;
+}
+
+CGRect UIKit_ComputeViewFrame(SDL_Window *window, UIScreen *screen)
 {
     SDL_UIKitWindowData *data = (__bridge SDL_UIKitWindowData *)window->driverdata;
     CGRect frame = screen.bounds;
diff --git a/src/video/uikit/SDL_uikitviewcontroller.h b/src/video/uikit/SDL_uikitviewcontroller.h
index f3e2f5de93f4..0e33f6020d33 100644
--- a/src/video/uikit/SDL_uikitviewcontroller.h
+++ b/src/video/uikit/SDL_uikitviewcontroller.h
@@ -45,6 +45,8 @@
 
 - (instancetype)initWithSDLWindow:(SDL_Window *)_window;
 
+- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection;
+
 - (void)setAnimationCallback:(int)interval
                     callback:(void (*)(void *))callback
                callbackParam:(void *)callbackParam;
diff --git a/src/video/uikit/SDL_uikitviewcontroller.m b/src/video/uikit/SDL_uikitviewcontroller.m
index 425e4f92e01d..758fe4332e9d 100644
--- a/src/video/uikit/SDL_uikitviewcontroller.m
+++ b/src/video/uikit/SDL_uikitviewcontroller.m
@@ -136,6 +136,11 @@ - (void)dealloc
 #endif
 }
 
+- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
+{
+    SDL_SetSystemTheme(UIKit_GetSystemTheme());
+}
+
 - (void)setAnimationCallback:(int)interval
                     callback:(void (*)(void *))callback
                callbackParam:(void *)callbackParam
diff --git a/src/video/windows/SDL_windowsevents.c b/src/video/windows/SDL_windowsevents.c
index a7444fbae585..dc7312f64958 100644
--- a/src/video/windows/SDL_windowsevents.c
+++ b/src/video/windows/SDL_windowsevents.c
@@ -1723,6 +1723,10 @@ WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
         break;
 
     case WM_SETTINGCHANGE:
+        if (wParam == 0 && lParam != 0 && SDL_wcscmp((wchar_t *)lParam, L"ImmersiveColorSet") == 0) {
+            SDL_SetSystemTheme(WIN_GetSystemTheme());
+            WIN_UpdateDarkModeForHWND(hwnd);
+        }
         if (wParam == SPI_SETMOUSE || wParam == SPI_SETMOUSESPEED) {
             WIN_UpdateMouseSystemScale();
         }
diff --git a/src/video/windows/SDL_windowsvideo.c b/src/video/windows/SDL_windowsvideo.c
index 7afe97f1d1cd..519c1ef204c6 100644
--- a/src/video/windows/SDL_windowsvideo.c
+++ b/src/video/windows/SDL_windowsvideo.c
@@ -116,6 +116,7 @@ static SDL_VideoDevice *WIN_CreateDevice(void)
     }
     device->driverdata = data;
     device->wakeup_lock = SDL_CreateMutex();
+    device->system_theme = WIN_GetSystemTheme();
 
 #if !defined(__XBOXONE__) && !defined(__XBOXSERIES__)
     data->userDLL = SDL_LoadObject("USER32.DLL");
@@ -675,8 +676,26 @@ SDL_bool SDL_DXGIGetOutputInfo(SDL_DisplayID displayID, int *adapterIndex, int *
 #endif
 }
 
-SDL_bool
-WIN_IsPerMonitorV2DPIAware(_THIS)
+SDL_SystemTheme WIN_GetSystemTheme(void)
+{
+    DWORD type;
+    DWORD value;
+    DWORD count = sizeof(value);
+    LSTATUS status;
+
+    /* Technically this isn't the system theme, but it's the preference for applications */
+    status = RegGetValue(HKEY_CURRENT_USER,
+                         TEXT("Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"),
+                         TEXT("AppsUseLightTheme"),
+                         RRF_RT_REG_DWORD, &type, &value, &count);
+    if (status == ERROR_SUCCESS && type == REG_DWORD && value == 0) {
+        return SDL_SYSTEM_THEME_DARK;
+    } else {
+        return SDL_SYSTEM_THEME_LIGHT;
+    }
+}
+
+SDL_bool WIN_IsPerMonitorV2DPIAware(_THIS)
 {
 #if !defined(__XBOXONE__) && !defined(__XBOXSERIES__)
     SDL_VideoData *data = _this->driverdata;
diff --git a/src/video/windows/SDL_windowsvideo.h b/src/video/windows/SDL_windowsvideo.h
index 27484a9f8a75..b7cdc7cb8c1f 100644
--- a/src/video/windows/SDL_windowsvideo.h
+++ b/src/video/windows/SDL_windowsvideo.h
@@ -466,6 +466,7 @@ extern SDL_bool g_WindowFrameUsableWhileCursorHidden;
 typedef struct IDirect3D9 IDirect3D9;
 extern SDL_bool D3D_LoadDLL(void **pD3DDLL, IDirect3D9 **pDirect3D9Interface);
 
+extern SDL_SystemTheme WIN_GetSystemTheme(void);
 extern SDL_bool WIN_IsPerMonitorV2DPIAware(_THIS);
 
 #endif /* SDL_windowsvideo_h_ */
diff --git a/src/video/windows/SDL_windowswindow.c b/src/video/windows/SDL_windowswindow.c
index ba482e47c9b6..62a8ba5a1431 100644
--- a/src/video/windows/SDL_windowswindow.c
+++ b/src/video/windows/SDL_windowswindow.c
@@ -40,6 +40,12 @@
 
 #include <SDL3/SDL_syswm.h>
 
+/* Dark mode support */
+#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE
+#define DWMWA_USE_IMMERSIVE_DARK_MODE 20
+#endif
+typedef HRESULT (WINAPI *DwmSetWindowAttribute_t)(HWND hwnd, DWORD dwAttribute, LPCVOID pvAttribute, DWORD cbAttribute);
+
 /* Windows CE compatibility */
 #ifndef SWP_NOCOPYBITS
 #define SWP_NOCOPYBITS 0
@@ -511,6 +517,8 @@ int WIN_CreateWindow(_THIS, SDL_Window *window)
         return WIN_SetError("Couldn't create window");
     }
 
+    WIN_UpdateDarkModeForHWND(hwnd);
+
     WIN_PumpEvents(_this);
 
     if (SetupWindowData(_this, window, hwnd, parent, SDL_TRUE) < 0) {
@@ -1459,4 +1467,18 @@ int WIN_FlashWindow(_THIS, SDL_Window *window, SDL_FlashOperation operation)
 }
 #endif /*!defined(__XBOXONE__) && !defined(__XBOXSERIES__)*/
 
+void WIN_UpdateDarkModeForHWND(HWND hwnd)
+{
+    void *handle = SDL_LoadObject("dwmapi.dll");
+    if (handle) {
+        DwmSetWindowAttribute_t DwmSetWindowAttributeFunc = (DwmSetWindowAttribute_t)SDL_LoadFunction(handle, "DwmSetWindowAttribute");
+        if (DwmSetWindowAttributeFunc) {
+            /* FIXME: Do we need to traverse children? */
+            BOOL value = (SDL_GetSystemTheme() == SDL_SYSTEM_THEME_DARK) ? TRUE : FALSE;
+            DwmSetWindowAttributeFunc(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &value, sizeof(value));
+        }
+        SDL_UnloadObject(handle);
+    }
+}
+
 #endif /* SDL_VIDEO_DRIVER_WINDOWS */
diff --git a/src/video/windows/SDL_windowswindow.h b/src/video/windows/SDL_windowswindow.h
index c27ba0341bc0..9e31e583c629 100644
--- a/src/video/windows/SDL_windowswindow.h
+++ b/src/video/windows/SDL_windowswindow.h
@@ -110,6 +110,7 @@ extern void WIN_ClientPointFromSDL(const SDL_Window *window, int *x, int *y);
 extern void WIN_ClientPointFromSDLFloat(const SDL_Window *window, float x, float y, LONG *xOut, LONG *yOut);
 extern void WIN_AcceptDragAndDrop(SDL_Window *window, SDL_bool accept);
 extern int WIN_FlashWindow(_THIS, SDL_Window *window, SDL_FlashOperation operation);
+extern void WIN_UpdateDarkModeForHWND(HWND hwnd);
 
 /* Ends C function definitions when using C++ */
 #ifdef __cplusplus