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