SDL icon indicating copy to clipboard operation
SDL copied to clipboard

Feature request: dark/light theme preference detection

Open albin-johansson opened this issue 2 years ago • 4 comments

Hi!

It would be useful for SDL to provide a simple API for querying the current system-wide theme preference. There might be good reasons for why this isn't provided (or maybe I've missed something), but I'd thought I ask and see what you would think.

Rationale

Many applications (including web applications) have begun providing options to automatically adjust their appearance according to the user's preferred theme. Below is an example from the GitHub Desktop client. It would be great if it was easy to create SDL applications that respect the system appearance preference.

gitHub-desktop-options

Possible API

This feature would not be very big and does not have to be very detailed, so something along the lines of a function returning an enum value should be good enough. Other names could be used as well, e.g. SDL_SystemAppearance.

typedef enum 
{
  SDL_SYSTEM_THEME_UNKNOWN,
  SDL_SYSTEM_THEME_LIGHT,
  SDL_SYSTEM_THEME_DARK
} SDL_SystemTheme;

SDL_SystemTheme SDL_GetSystemTheme(void);

Cost of Implementation

I'm by no means an expert on any of the supported native platform APIs, but this feature should not be too complicated to implement. For instance, I think this feature could be implemented by reading a registry variable on Windows.

Possibly Related

  • #4776

albin-johansson avatar Feb 08 '22 14:02 albin-johansson

I'm not sure about the version requirements, or the best practices related to contributing new functions to SDL in general. But here are outlines for possible implementations for at least Win32 and macOS.

The Win32 implementation just reads a registry entry, this probably needs to be adjusted to work with versions other than Windows 10.

SDL_SystemTheme
SDL_GetSystemTheme(void)
{
    SDL_SystemTheme retval = SDL_SYSTEM_THEME_UNKNOWN;

    LPCWSTR path = L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize";
    HKEY hkey;

    if (RegOpenKeyEx(HKEY_CURRENT_USER, path, 0, KEY_QUERY_VALUE, &hkey) == ERROR_SUCCESS) {
        DWORD data = 0;
        DWORD dataSize = sizeof data;
        DWORD type = REG_DWORD;

        LSTATUS res;
        res = RegQueryValueEx(hkey, L"SystemUsesLightTheme", NULL, &type, (LPBYTE) &data, &dataSize);

        if (res == ERROR_SUCCESS) {
            if (data == 0) {
                retval = SDL_SYSTEM_THEME_DARK;
            } else if (data == 1) {
                retval = SDL_SYSTEM_THEME_LIGHT;
            }
        }

        RegCloseKey(hkey);
    }

    return retval;
}

The macOS implementation uses AppKit, tested on Monterey 12.2.

SDL_SystemTheme
SDL_GetSystemTheme(void)
{
    NSAppearance* appearance = [[NSApplication sharedApplication] effectiveAppearance];

    if ([appearance.name isEqualToString: NSAppearanceNameDarkAqua]) {
        return SDL_SYSTEM_THEME_DARK;
    } else if ([appearance.name isEqualToString: NSAppearanceNameAqua]) {
        return SDL_SYSTEM_THEME_LIGHT;
    }

    return SDL_SYSTEM_THEME_UNKNOWN;
}

albin-johansson avatar Feb 09 '22 18:02 albin-johansson

It seems like this would also need an event that is sent when the preference changes?

slouken avatar Feb 09 '22 18:02 slouken

Yeah, an accompanying event would be very handy! 👍🏻

albin-johansson avatar Feb 09 '22 18:02 albin-johansson

Additionally, it would be great to allow Windows applications to fully embrace dark mode by making the title bars dark as well. This is brought up in #4776. However, I thought I'd provide a more up to date and simplified example implementation of a function for setting the title bar appearance. I don't know if this should exposed as something like SDL_SetWindowTheme since it's quite specific to Windows, or just something that SDL handles automatically according to system theme events (maybe according to a hint?).

int
WIN_SetWindowTheme(_THIS, SDL_Window * window, SDL_SystemTheme theme)
{
    if (theme != SDL_SYSTEM_THEME_LIGHT && theme != SDL_SYSTEM_THEME_DARK) {
        return SDL_InvalidParamError("theme");
    }

    BOOL succeeded = FALSE;

    SDL_WindowData *data = (SDL_WindowData *) window->driverdata;
    HWND hwnd = data->hwnd;

    HMODULE dwmapi = LoadLibrary(L"dwmapi.dll");
    if (dwmapi == NULL) {
        return SDL_SetError("Could not load dwmapi.dll");
    }

    typedef HRESULT (*DwmSetWindowAttributePtr)(HWND, DWORD, LPCVOID, DWORD);
    DwmSetWindowAttributePtr DwmSetWindowAttribute = (DwmSetWindowAttributePtr) GetProcAddress(dwmapi, "DwmSetWindowAttribute");
    if (!DwmSetWindowAttribute) {
        FreeLibrary(dwmapi);
        return SDL_SetError("Did not find DwmSetWindowAttribute function in dwmapi.dll");
    }

    BOOL mode = (theme == SDL_SYSTEM_THEME_DARK) ? 1 : 0;
    if (DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, &mode, sizeof mode) == S_OK) {
        succeeded = TRUE;
    }
    else {
        SDL_SetError("Failed to update window theme");
    }

    FreeLibrary(dwmapi);

    return succeeded ? 0 : -1;
}

albin-johansson avatar Feb 09 '22 21:02 albin-johansson

For the event:

  • In Windows, window message WM_SETTINGCHANGE with (wchar_t *) lParam equal to ImmersiveColorSet can be used to detect if the preference has changed by re-reading the registry value.
  • In Android, https://stackoverflow.com/a/61223332 can be used as reference. SDL in Android already handles uiMode change so this is easy.

MikuAuahDark avatar Oct 03 '22 03:10 MikuAuahDark

How about Linux and macOS? I figure the API should support a wide range of OS.

thrust26 avatar Oct 03 '22 04:10 thrust26

I went ahead and implemented this API. Thanks everyone!

slouken avatar Mar 09 '23 11:03 slouken