diff --git a/include/SDL3/SDL_video.h b/include/SDL3/SDL_video.h index d727457191aab..b19d14694d413 100644 --- a/include/SDL3/SDL_video.h +++ b/include/SDL3/SDL_video.h @@ -600,6 +600,19 @@ extern SDL_DECLSPEC const char * SDLCALL SDL_GetVideoDriver(int index); */ extern SDL_DECLSPEC const char * SDLCALL SDL_GetCurrentVideoDriver(void); +/** + * Set the preferred system theme. + * This is a hint to the system to use the preferred theme for the window + * + * \param theme the preferred system theme, light, dark, or unknown for auto. + * \threadsafety This function should only be called on the main thread. + * + * \since This function is available since SDL 3.6.0. + * + * \sa SDL_GetSystemTheme + */ +extern SDL_DECLSPEC void SDLCALL SDL_SetPreferredSystemTheme(SDL_SystemTheme theme); + /** * Get the current system theme. * diff --git a/src/core/windows/SDL_windows.c b/src/core/windows/SDL_windows.c index 7c433b252c788..d47aecacb263f 100644 --- a/src/core/windows/SDL_windows.c +++ b/src/core/windows/SDL_windows.c @@ -54,18 +54,6 @@ typedef enum { UXTHEME_APPMODE_MAX } UxthemePreferredAppMode; -typedef enum { - WCA_UNDEFINED = 0, - WCA_USEDARKMODECOLORS = 26, - WCA_LAST = 27 -} WINDOWCOMPOSITIONATTRIB; - -typedef struct { - WINDOWCOMPOSITIONATTRIB Attrib; - PVOID pvData; - SIZE_T cbData; -} WINDOWCOMPOSITIONATTRIBDATA; - typedef struct { ULONG dwOSVersionInfoSize; ULONG dwMajorVersion; @@ -80,7 +68,6 @@ typedef void (WINAPI *AllowDarkModeForWindow_t)(HWND, bool); typedef void (WINAPI *AllowDarkModeForApp_t)(bool); typedef void (WINAPI *RefreshImmersiveColorPolicyState_t)(void); typedef UxthemePreferredAppMode (WINAPI *SetPreferredAppMode_t)(UxthemePreferredAppMode); -typedef BOOL (WINAPI *SetWindowCompositionAttribute_t)(HWND, const WINDOWCOMPOSITIONATTRIBDATA *); typedef void (NTAPI *RtlGetVersion_t)(NT_OSVERSIONINFOW *); // Fake window to help with DirectInput events. @@ -438,12 +425,25 @@ bool WIN_WindowRectValid(const RECT *rect) return (rect->right > 0); } -void WIN_UpdateDarkModeForHWND(HWND hwnd) +UxthemePreferredAppMode WIN_PreferredUXTheme(const SDL_SystemTheme theme) +{ + switch (theme) { + case SDL_SYSTEM_THEME_DARK: + return UXTHEME_APPMODE_FORCE_DARK; + case SDL_SYSTEM_THEME_LIGHT: + return UXTHEME_APPMODE_FORCE_LIGHT; + case SDL_SYSTEM_THEME_UNKNOWN: + default: + return UXTHEME_APPMODE_ALLOW_DARK; + } +} + +BOOL WIN_UpdatePreferredTheme(SDL_SystemTheme preferredTheme) { #if !defined(SDL_PLATFORM_XBOXONE) && !defined(SDL_PLATFORM_XBOXSERIES) HMODULE ntdll = LoadLibrary(TEXT("ntdll.dll")); if (!ntdll) { - return; + return false; } // There is no function to get Windows build number, so let's get it here via RtlGetVersion RtlGetVersion_t RtlGetVersionFunc = (RtlGetVersion_t)GetProcAddress(ntdll, "RtlGetVersion"); @@ -457,52 +457,29 @@ void WIN_UpdateDarkModeForHWND(HWND hwnd) os_info.dwBuildNumber &= ~0xF0000000; if (os_info.dwBuildNumber < 17763) { // Too old to support dark mode - return; + return false; } HMODULE uxtheme = LoadLibrary(TEXT("uxtheme.dll")); if (!uxtheme) { - return; + return false; } RefreshImmersiveColorPolicyState_t RefreshImmersiveColorPolicyStateFunc = (RefreshImmersiveColorPolicyState_t)GetProcAddress(uxtheme, MAKEINTRESOURCEA(104)); - ShouldAppsUseDarkMode_t ShouldAppsUseDarkModeFunc = (ShouldAppsUseDarkMode_t)GetProcAddress(uxtheme, MAKEINTRESOURCEA(132)); - AllowDarkModeForWindow_t AllowDarkModeForWindowFunc = (AllowDarkModeForWindow_t)GetProcAddress(uxtheme, MAKEINTRESOURCEA(133)); if (os_info.dwBuildNumber < 18362) { AllowDarkModeForApp_t AllowDarkModeForAppFunc = (AllowDarkModeForApp_t)GetProcAddress(uxtheme, MAKEINTRESOURCEA(135)); if (AllowDarkModeForAppFunc) { - AllowDarkModeForAppFunc(true); + AllowDarkModeForAppFunc(preferredTheme != SDL_SYSTEM_THEME_LIGHT); } } else { SetPreferredAppMode_t SetPreferredAppModeFunc = (SetPreferredAppMode_t)GetProcAddress(uxtheme, MAKEINTRESOURCEA(135)); if (SetPreferredAppModeFunc) { - SetPreferredAppModeFunc(UXTHEME_APPMODE_ALLOW_DARK); + SetPreferredAppModeFunc(WIN_PreferredUXTheme(preferredTheme)); } } if (RefreshImmersiveColorPolicyStateFunc) { RefreshImmersiveColorPolicyStateFunc(); } - if (AllowDarkModeForWindowFunc) { - AllowDarkModeForWindowFunc(hwnd, true); - } - BOOL value; - // Check dark mode using ShouldAppsUseDarkMode, but use SDL_GetSystemTheme as a fallback - if (ShouldAppsUseDarkModeFunc) { - value = ShouldAppsUseDarkModeFunc() ? TRUE : FALSE; - } else { - value = (SDL_GetSystemTheme() == SDL_SYSTEM_THEME_DARK) ? TRUE : FALSE; - } FreeLibrary(uxtheme); - if (os_info.dwBuildNumber < 18362) { - SetProp(hwnd, TEXT("UseImmersiveDarkModeColors"), SDL_reinterpret_cast(HANDLE, SDL_static_cast(INT_PTR, value))); - } else { - HMODULE user32 = GetModuleHandle(TEXT("user32.dll")); - if (user32) { - SetWindowCompositionAttribute_t SetWindowCompositionAttributeFunc = (SetWindowCompositionAttribute_t)GetProcAddress(user32, "SetWindowCompositionAttribute"); - if (SetWindowCompositionAttributeFunc) { - WINDOWCOMPOSITIONATTRIBDATA data = { WCA_USEDARKMODECOLORS, &value, sizeof(value) }; - SetWindowCompositionAttributeFunc(hwnd, &data); - } - } - } + return true; #endif } diff --git a/src/core/windows/SDL_windows.h b/src/core/windows/SDL_windows.h index 80eb0997e5374..0a37d38eaec9d 100644 --- a/src/core/windows/SDL_windows.h +++ b/src/core/windows/SDL_windows.h @@ -194,7 +194,7 @@ extern void WIN_RectToRECT(const SDL_Rect *sdlrect, RECT *winrect); // Returns false if a window client rect is not valid extern bool WIN_WindowRectValid(const RECT *rect); -extern void WIN_UpdateDarkModeForHWND(HWND hwnd); +extern BOOL WIN_UpdatePreferredTheme(SDL_SystemTheme preferredTheme); extern HICON WIN_CreateIconFromSurface(SDL_Surface *surface); diff --git a/src/dynapi/SDL_dynapi.sym b/src/dynapi/SDL_dynapi.sym index 9feb8a0fc6f7d..de7ec69833895 100644 --- a/src/dynapi/SDL_dynapi.sym +++ b/src/dynapi/SDL_dynapi.sym @@ -1240,7 +1240,7 @@ SDL3_0.0.0 { SDL_GetDefaultTextureScaleMode; SDL_CreateGPURenderState; SDL_SetGPURenderStateFragmentUniforms; - SDL_SetGPURenderState; + SDL_SetRenderGPUState; SDL_DestroyGPURenderState; SDL_SetWindowProgressState; SDL_SetWindowProgressValue; @@ -1268,6 +1268,7 @@ SDL3_0.0.0 { SDL_GetPenDeviceType; SDL_CreateAnimatedCursor; SDL_RotateSurface; + SDL_SetPreferredTheme; # 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 1d17c7fcf0a2f..6ed6d8eca23bc 100644 --- a/src/dynapi/SDL_dynapi_overrides.h +++ b/src/dynapi/SDL_dynapi_overrides.h @@ -1294,3 +1294,4 @@ #define SDL_GetPenDeviceType SDL_GetPenDeviceType_REAL #define SDL_CreateAnimatedCursor SDL_CreateAnimatedCursor_REAL #define SDL_RotateSurface SDL_RotateSurface_REAL +#define SDL_SetPreferredSystemTheme SDL_SetPreferredSystemTheme_REAL diff --git a/src/dynapi/SDL_dynapi_procs.h b/src/dynapi/SDL_dynapi_procs.h index a2fc06fd7f14d..0280f8e2d2eba 100644 --- a/src/dynapi/SDL_dynapi_procs.h +++ b/src/dynapi/SDL_dynapi_procs.h @@ -1302,3 +1302,4 @@ SDL_DYNAPI_PROC(int,SDL_GetSystemPageSize,(void),(),return) SDL_DYNAPI_PROC(SDL_PenDeviceType,SDL_GetPenDeviceType,(SDL_PenID a),(a),return) SDL_DYNAPI_PROC(SDL_Cursor*,SDL_CreateAnimatedCursor,(SDL_CursorFrameInfo *a,int b,int c,int d),(a,b,c,d),return) SDL_DYNAPI_PROC(SDL_Surface*,SDL_RotateSurface,(SDL_Surface *a,float b),(a,b),return) +SDL_DYNAPI_PROC(void,SDL_SetPreferredSystemTheme,(SDL_SystemTheme theme),(theme),return) diff --git a/src/tray/windows/SDL_tray.c b/src/tray/windows/SDL_tray.c index 479c2e02b8037..53e3faf5fff6a 100644 --- a/src/tray/windows/SDL_tray.c +++ b/src/tray/windows/SDL_tray.c @@ -23,6 +23,7 @@ #include "../SDL_tray_utils.h" #include "../../core/windows/SDL_windows.h" +#include "../../video/windows/SDL_windowsvideo.h" #include #include @@ -100,6 +101,20 @@ static SDL_TrayEntry *find_entry_with_id(SDL_Tray *tray, UINT_PTR id) return find_entry_in_menu(tray->menu, id); } +static void TrayUpdateDarkMode(HWND hwnd, BOOL value) +{ + HMODULE user32 = GetModuleHandle(TEXT("user32.dll")); + if (!user32) { + return; + } + + WINDOWCOMPOSITIONATTRIBDATA data = { + WCA_USEDARKMODECOLORS, + &value, sizeof(value) + }; + SDL_GetVideoDevice()->internal->SetWindowCompositionAttribute(hwnd, &data); +} + LRESULT CALLBACK TrayWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { SDL_Tray *tray = (SDL_Tray *) GetWindowLongPtr(hwnd, GWLP_USERDATA); SDL_TrayEntry *entry = NULL; @@ -142,7 +157,9 @@ LRESULT CALLBACK TrayWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lPar case WM_SETTINGCHANGE: if (wParam == 0 && lParam != 0 && SDL_wcscmp((wchar_t *)lParam, L"ImmersiveColorSet") == 0) { - WIN_UpdateDarkModeForHWND(hwnd); + if (SDL_GetVideoDevice()->preferred_theme == SDL_SYSTEM_THEME_UNKNOWN) { + TrayUpdateDarkMode(hwnd, SDL_GetSystemTheme() == SDL_SYSTEM_THEME_DARK); + } } break; @@ -288,7 +305,7 @@ SDL_Tray *SDL_CreateTray(SDL_Surface *icon, const char *tooltip) tray->hwnd = CreateWindowEx(0, TEXT("SDL_TRAY"), NULL, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, NULL, NULL); - WIN_UpdateDarkModeForHWND(tray->hwnd); + TrayUpdateDarkMode(tray->hwnd, SDL_GetSystemTheme() == SDL_SYSTEM_THEME_DARK); SDL_zero(tray->nid); tray->nid.cbSize = sizeof(NOTIFYICONDATAW); diff --git a/src/video/SDL_sysvideo.h b/src/video/SDL_sysvideo.h index 27819d4e7c038..f3873c554dc5e 100644 --- a/src/video/SDL_sysvideo.h +++ b/src/video/SDL_sysvideo.h @@ -399,6 +399,12 @@ struct SDL_VideoDevice // Display the system-level window menu void (*ShowWindowSystemMenu)(SDL_Window *window, int x, int y); + /* + * Set the preferred theme (light, dark, etc) for the application + * This is a hint, and may be ignored by the platform. + */ + void (*SetPreferredTheme)(SDL_VideoDevice *_this, SDL_SystemTheme theme); + /* * * */ // Data common to all drivers SDL_ThreadID thread; @@ -421,6 +427,7 @@ struct SDL_VideoDevice bool setting_display_mode; Uint32 device_caps; SDL_SystemTheme system_theme; + SDL_SystemTheme preferred_theme; bool screen_keyboard_shown; /* * * */ diff --git a/src/video/SDL_video.c b/src/video/SDL_video.c index a622fa21ec919..a17f5748e1c7a 100644 --- a/src/video/SDL_video.c +++ b/src/video/SDL_video.c @@ -770,6 +770,16 @@ bool SDL_OnVideoThread(void) return (_this && SDL_GetCurrentThreadID() == _this->thread); } +void SDL_SetPreferredSystemTheme(SDL_SystemTheme theme) +{ + if (_this && theme != _this->preferred_theme) { + _this->preferred_theme = theme; + if (_this->SetPreferredTheme) { + _this->SetPreferredTheme(_this, theme); + } + } +} + void SDL_SetSystemTheme(SDL_SystemTheme theme) { if (_this && theme != _this->system_theme) { diff --git a/src/video/windows/SDL_windowsevents.c b/src/video/windows/SDL_windowsevents.c index 9854767c3054e..a26a37890a2d5 100644 --- a/src/video/windows/SDL_windowsevents.c +++ b/src/video/windows/SDL_windowsevents.c @@ -2416,8 +2416,10 @@ LRESULT CALLBACK WIN_WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lPara case WM_SETTINGCHANGE: if (wParam == 0 && lParam != 0 && SDL_wcscmp((wchar_t *)lParam, L"ImmersiveColorSet") == 0) { - SDL_SetSystemTheme(WIN_GetSystemTheme()); - WIN_UpdateDarkModeForHWND(hwnd); + if (SDL_GetVideoDevice()->preferred_theme == SDL_SYSTEM_THEME_UNKNOWN) { + WIN_SetDarkModeColorsForHWND(SDL_GetVideoDevice(), data->window, WIN_GetSystemTheme() == SDL_SYSTEM_THEME_DARK ? TRUE : FALSE); + SDL_SetSystemTheme(WIN_GetSystemTheme()); + } } 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 e840b0507e6db..f9936ca4f702d 100644 --- a/src/video/windows/SDL_windowsvideo.c +++ b/src/video/windows/SDL_windowsvideo.c @@ -237,6 +237,10 @@ static SDL_VideoDevice *WIN_CreateDevice(void) } device->internal = data; device->system_theme = WIN_GetSystemTheme(); + device->preferred_theme = SDL_SYSTEM_THEME_UNKNOWN; + device->SetPreferredTheme = WIN_SetPreferredTheme; + + WIN_SetPreferredTheme(device, device->system_theme); #if !defined(SDL_PLATFORM_XBOXONE) && !defined(SDL_PLATFORM_XBOXSERIES) data->userDLL = SDL_LoadObject("USER32.DLL"); @@ -260,6 +264,7 @@ static SDL_VideoDevice *WIN_CreateDevice(void) data->DisplayConfigGetDeviceInfo = (LONG (WINAPI *)(DISPLAYCONFIG_DEVICE_INFO_HEADER*))SDL_LoadFunction(data->userDLL, "DisplayConfigGetDeviceInfo"); data->GetPointerType = (BOOL (WINAPI *)(UINT32, POINTER_INPUT_TYPE *))SDL_LoadFunction(data->userDLL, "GetPointerType"); data->GetPointerPenInfo = (BOOL (WINAPI *)(UINT32, POINTER_PEN_INFO *))SDL_LoadFunction(data->userDLL, "GetPointerPenInfo"); + data->SetWindowCompositionAttribute = (BOOL (WINAPI *)(HWND, const WINDOWCOMPOSITIONATTRIBDATA *))SDL_LoadFunction(data->userDLL, "SetWindowCompositionAttribute"); /* *INDENT-ON* */ // clang-format on } else { SDL_ClearError(); @@ -877,4 +882,26 @@ bool WIN_IsPerMonitorV2DPIAware(SDL_VideoDevice *_this) return false; } +void WIN_SetPreferredTheme(SDL_VideoDevice *_this, SDL_SystemTheme preferred) +{ + SDL_Window *window; + SDL_SystemTheme theme; + + if (!WIN_UpdatePreferredTheme(preferred)) { + return; + } + + if (preferred == SDL_SYSTEM_THEME_UNKNOWN) { + theme = WIN_GetSystemTheme(); + } else { + theme = preferred; + } + + for (window = _this->windows; window; window = window->next) { + WIN_SetDarkModeColorsForHWND(_this, window, preferred == SDL_SYSTEM_THEME_DARK); + } + + SDL_SetSystemTheme(theme); +} + #endif // SDL_VIDEO_DRIVER_WINDOWS diff --git a/src/video/windows/SDL_windowsvideo.h b/src/video/windows/SDL_windowsvideo.h index b192fb664c2b0..bd8d4f1874b8b 100644 --- a/src/video/windows/SDL_windowsvideo.h +++ b/src/video/windows/SDL_windowsvideo.h @@ -510,6 +510,18 @@ typedef struct BOOL transition_on_maxed; } DWM_BLURBEHIND; +typedef enum { + WCA_UNDEFINED = 0, + WCA_USEDARKMODECOLORS = 26, + WCA_LAST = 27 +} WINDOWCOMPOSITIONATTRIB; + +typedef struct { + WINDOWCOMPOSITIONATTRIB Attrib; + PVOID pvData; + SIZE_T cbData; +} WINDOWCOMPOSITIONATTRIBDATA; + // Private display data struct SDL_VideoData @@ -572,6 +584,9 @@ struct SDL_VideoData UINT *dpiX, UINT *dpiY ); HRESULT (WINAPI *SetProcessDpiAwareness)(PROCESS_DPI_AWARENESS dpiAwareness); + + BOOL (WINAPI *SetWindowCompositionAttribute)(HWND, const WINDOWCOMPOSITIONATTRIBDATA *); + /* *INDENT-ON* */ // clang-format on #endif // !defined(SDL_PLATFORM_XBOXONE) && !defined(SDL_PLATFORM_XBOXSERIES) @@ -661,4 +676,6 @@ extern bool D3D_LoadDLL(void **pD3DDLL, IDirect3D9 **pDirect3D9Interface); extern SDL_SystemTheme WIN_GetSystemTheme(void); extern bool WIN_IsPerMonitorV2DPIAware(SDL_VideoDevice *_this); +extern void WIN_SetPreferredTheme(SDL_VideoDevice *_this, SDL_SystemTheme theme); + #endif // SDL_windowsvideo_h_ diff --git a/src/video/windows/SDL_windowswindow.c b/src/video/windows/SDL_windowswindow.c index 02ed14018ce3f..b635f7fbc2529 100644 --- a/src/video/windows/SDL_windowswindow.c +++ b/src/video/windows/SDL_windowswindow.c @@ -722,8 +722,6 @@ bool WIN_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Properties return WIN_SetError("Couldn't create window"); } - WIN_UpdateDarkModeForHWND(hwnd); - WIN_PumpEvents(_this); if (!SetupWindowData(_this, window, hwnd, parent)) { @@ -734,6 +732,8 @@ bool WIN_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Properties return false; } + WIN_SetDarkModeColorsForHWND(_this, window, _this->system_theme == SDL_SYSTEM_THEME_DARK ? TRUE : FALSE); + #if !defined(SDL_PLATFORM_XBOXONE) && !defined(SDL_PLATFORM_XBOXSERIES) // Ensure that the IME isn't active on the new window until explicitly requested. WIN_StopTextInput(_this, window); @@ -826,6 +826,18 @@ bool WIN_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_Properties return true; } +void WIN_SetDarkModeColorsForHWND(SDL_VideoDevice *_this, SDL_Window *window, BOOL value) +{ +#if !defined(SDL_PLATFORM_XBOXONE) && !defined(SDL_PLATFORM_XBOXSERIES) + HWND hwnd = window->internal->hwnd; + WINDOWCOMPOSITIONATTRIBDATA data = { + WCA_USEDARKMODECOLORS, + &value, sizeof(value) + }; + _this->internal->SetWindowCompositionAttribute(hwnd, &data); +#endif +} + void WIN_SetWindowTitle(SDL_VideoDevice *_this, SDL_Window *window) { #if !defined(SDL_PLATFORM_XBOXONE) && !defined(SDL_PLATFORM_XBOXSERIES) diff --git a/src/video/windows/SDL_windowswindow.h b/src/video/windows/SDL_windowswindow.h index 5d22b4fb9ea12..ebba4b1744a57 100644 --- a/src/video/windows/SDL_windowswindow.h +++ b/src/video/windows/SDL_windowswindow.h @@ -107,6 +107,7 @@ struct SDL_WindowData }; extern bool WIN_CreateWindow(SDL_VideoDevice *_this, SDL_Window *window, SDL_PropertiesID create_props); +extern void WIN_SetDarkModeColorsForHWND(SDL_VideoDevice *_this, SDL_Window *window, BOOL value); extern void WIN_SetWindowTitle(SDL_VideoDevice *_this, SDL_Window *window); extern bool WIN_SetWindowIcon(SDL_VideoDevice *_this, SDL_Window *window, SDL_Surface *icon); extern bool WIN_SetWindowPosition(SDL_VideoDevice *_this, SDL_Window *window);