winit icon indicating copy to clipboard operation
winit copied to clipboard

Theme mode API

Open dhardy opened this issue 5 years ago • 29 comments

The (minimal) documentation for WindowEvent::ThemeChanged does not clarify whether apps should query the theme on startup or simply wait for a ThemeChanged event.

From a quick look at the code, I think the only way to determine the theme at startup is via a platform-specific is_dark_mode function. This is problematic in three ways: (1) lack of documentation, (2) API is only partially cross-platform, (3) is_dark_mode has a completely different (and less flexible) API to ThemeChanged(theme).

Suggestion 1: always send ThemeChanged on start if the theme is not Theme::Light (or whatever the default theme is called), and document this.

Suggestion 2: add a Window::theme method.

Related: #1217

dhardy avatar Apr 29 '20 16:04 dhardy

I did some research on detecting light/dark mode on linux. Basically, you have to get the current theme, and see if it ends in -dark (just a convention, I'm sure some themes break it. There is no better way unfortunately).

  • Gnome - gsettings get org.gnome.desktop.interface
  • KDE - Something to do with kreadconfig I believe (Uses QT over gtk, I'm unsure if the breeze dark variant (default dark theme for KDE) theme name ends in -dark)
  • Xfce - Something to do with xfconf-query I believe
  • Cinnamon - Uses gsettings like gnome. I don't know the exact key.
  • Pantheon - Unknown
  • Mate - Uses gsettings like gnome. I don't know the exact key.

As you can see, it's kinda a mess. I would either:

  1. Support only Gnome/KDE, assuming Gnome unless the kreadconfig program is present. This is kinda brittle, and if a winit app is isolated with Flatpak or something, probably going to fail.
  2. Add a WINIT_THEME=dark/light environment variable, and leave it up to the user to set it with their own scripts.

JMS55 avatar May 18 '20 04:05 JMS55

I'm unsure how useful just detecting if using a 'dark theme' would be on KDE (and perhaps Gnome and related ones?). A theme can be anything, and there are ones that even do odd things like have a dark style system but really bright windows that are well used. The proper way would be to read the actual theme data and use or at least expose to the user the proper theme values for things like background color and text color and the 200+ other settings. As I recall Windows also had very detailed theme settings like that as well. Just having dark or light mode would not be sufficient to be able to make something that 'fits in'. Even just among, say, dark themes some use a dark slate gray like I do, some use a very dark blue like my wife, some use a very dark purple like a friend of mine, etc...

In addition KDE (and I'm pretty sure Gnome as well) can set per-application or even per-window themes (fantastic for, say, running things as root or distinguish window themes based on whether related for work or play or so).

And don't forget that theme changes can happen in real-time with KDE as well.

OvermindDL1 avatar Jul 01 '20 14:07 OvermindDL1

Here's my proposal for how to determine light/dark (And I agree the event should always be sent on startup):

  • Windows: Continue with whatever winit is already doing I assume, I haven't looked at it.

  • macOS Mojave or later: Subscribe to when NSApplication.effectiveAppearance changes. Check if NSAppearance.Name contains dark as a case-insensitive substring.

  • Linux: Either only send once on startup, or poll every X milliseconds in a background thread or something. Detect linux desktop by searching for case-insensitive substring in $XDG_CURRENT_DESKTOP. Probably doesn't work when sandboxed in a flatpak.

    • Gnome: Call gsettings get org.gnome.desktop.interface gtk-theme, and check if it contains dark as a case-insensitive substring.
    • KDE: Call kreadconfig5 --group General --key ColorScheme, and check if it contains dark as a case-insensitive substring.
  • All other cases (older OS version, shelling out to a command failed, substring not found, etc): Default to light.


As you can see, for Linux there is no good option. At best, you can support a subset of desktop environments (too many to maintain support for realistically), and when not sandboxed.

JMS55 avatar Aug 24 '20 03:08 JMS55

I've just found out about https://github.com/elementary/os/wiki/Dark-Style-Preference, which looks like it would be perfect. Desktops set prefers dark/light, and then apps get notified when it changes, and choose whether to render with a dark, light, or other theme.

Unfortunately, only elementary os supports it right now, as it's not an official spec.

JMS55 avatar Aug 27 '20 16:08 JMS55

An update on this:

  • Windows 10+: Check the registry key HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize\AppsUseLightTheme.
  • macOS Mojave+: Check NSApplication.effectiveAppearance.Name for the substring dark.
  • Linux (gnome 42+, elementary os 6+, eventually KDE): Check the dbus key org.freedesktop.appearance.color-scheme.
  • Web: Check the media key prefers-color-scheme.
  • Android/IOS: Didn't look into it.

Where the system API exists, winit should return Some(Dark/Light). Where the system API doesn't exist, or where Linux returns default, winit should return None.

Apps are free to follow the system preference or ignore it, it's only a preference.

JMS55 avatar Oct 05 '21 18:10 JMS55

Just for some information on the org.freedesktop.appearance.color-scheme bit, this is not a pre-existing standard thing, rather it was developed by I think gnome first and is looked favorably upon by KDE and others that it looks like they will support it "fairly soon(tm)", so just because you may not see it in your DBUS viewer right now doesn't mean it won't exist in (short) time. Though optimally it would be nice to get a color scheme from things like KDE that support it rather than 'just' light/dark if possible, but that's a good fallback regardless.

OvermindDL1 avatar Oct 19 '21 22:10 OvermindDL1

Can we have the API to force a dark/light mode from application itself? Or even better a way to provide own colours for decoration.

akhilman avatar Dec 30 '21 04:12 akhilman

Can we have the API to force a dark/light mode from application itself? Or even better a way to provide own colours for decoration.

I assume you're talking about macOS/Windows decorations. Linux (wayland) has no standard decorations - you have to draw them yourself, so you could make them anything you want. Someone with more knowledge of the relevant platform API's would have to comment on whether it's possible or not.

JMS55 avatar Dec 30 '21 20:12 JMS55

There is a dark-light crate which does only this. Linux support is in progress using the new XDG Desktop Portal dbus API with fallbacks to checking if the theme name has "dark" in it.

I propose considering removing ThemeChanged from winit's public API. This is not trivial to implement cross platform and seems to me to be tangential to the purpose of winit. Dark versus light mode is a system setting, not a per-window setting, even if Windows exposes it in its windowing event loop.

Be-ing avatar Jan 06 '22 04:01 Be-ing

A problem with the dark-light crate is that it requires checking continually whether the theme has changed.

madsmtm avatar Jan 06 '22 06:01 madsmtm

There is an open issue for providing a callback to notify of changes to light/dark mode in dark-light.

Be-ing avatar Jan 06 '22 06:01 Be-ing

I'll get on that as soon as I'm done with this 👍🏼

edfloreshz avatar Jan 06 '22 07:01 edfloreshz

interesting that winit considered this feature in scope. the flutter engine also provides access to other related options that winit may want to expose in the future too (although I've not looked into how it's implemented for each platform):

  • always use 24 hour format
  • text scale factor

which together with platform brightness gets grouped into a UserSettings struct and various settings grouped into an AccessibilityFeatures struct:

  • accessible navigation
  • invert colors
  • disable animations
  • bold text
  • reduce motion
  • high contrast

also related is a LocaleChanged event.

dvc94ch avatar Mar 16 '22 16:03 dvc94ch

Can we have the API to force a dark/light mode from application itself? Or even better a way to provide own colours for decoration.

Yes, will this issue provide the ability to force light or dark modes for applications that use winit?

I created this Alacritty issue asking to set light or dark at the application level rather than just using the global desktop preference. In my case I use a light desktop but I much prefer a dark theme just for the terminal. The Alacritty maintainer (@chrisduerr) referred to this upstream issue since it appears Alacritty can't set light or dark on macOS or Windows by itself.

Hopefully this issue will look at getting (aka detecting theme type) and setting (per application light/dark mode configuration).

Best regards.

bluz71 avatar Dec 12 '22 07:12 bluz71

The proper way to check for dark / light is via an XDG portal call.

https://flatpak.github.io/xdg-desktop-portal/

Read ( "org.freedesktop.apperance", "color-scheme", INTEGER )

Then there is a signal for SettingsChanged and you can sync with that.

Doc

org.freedesktop.appearance color-scheme u

Indicates the system's preferred color scheme. Supported values are:

0: No preference 1: Prefer dark appearance 2: Prefer light appearance

Unknown values should be treated as 0 (no preference).

Doomsdayrs avatar Jan 19 '23 07:01 Doomsdayrs

In Linux, with freedesktop portal supported, the color theme preference as well as changes can be detected using zbus. A similar implementation can be found in ashpd.

d2weber avatar Aug 13 '23 13:08 d2weber

Using side channels like dbus is out of scope as of now.

kchibisov avatar Aug 13 '23 18:08 kchibisov

Can you elaborate how its a side channel? FreeDesktop portals are the DE-independent standard for this type of GUI interaction/requests.

BlackHoleFox avatar Aug 13 '23 20:08 BlackHoleFox

dbus is a side channel, it's coming from neither Wayland nor X11 directly. You can use it in your code just fine without any help from winit.

kchibisov avatar Aug 13 '23 20:08 kchibisov

A problem with the dark-light crate is that it requires checking continually whether the theme has changed.

Just FYI, there's an open PR that implements a way to provide a closure to handle changes in the color scheme.

edfloreshz avatar Aug 13 '23 20:08 edfloreshz

I do not understand how the xdg-desktop-portal protocol is a side-channel. It is explicitly designed to allow each desktop environment to take control of the portal and give settings (like appearance and color preference) in a desktop independent manner: https://flatpak.github.io/xdg-desktop-portal/.

4e554c4c avatar Jun 16 '25 12:06 4e554c4c

it's going through neither wayland nor x11 display, thus a side channel(dbus).

kchibisov avatar Jun 16 '25 12:06 kchibisov

The definition for "what is a desktop protocol" is platform dependent, and on Linux with Wayland, this includes dbus

Furthermore, it doesn't make sense for winit to provide this API for Windows and macOS but not Linux. If it's essential for Windows, then it should be exposed on other platforms

4e554c4c avatar Jun 16 '25 13:06 4e554c4c

My 2c (just as someone who uses winit) is that winit not supporting theme mode detection on Linux is fine, provided that it's clearly documented which platforms the theme mode API is applicable to.

Using the dark-light crate instead on applicable platforms is fine, albeit a little bit tedious. There's also reason not to use it if you don't need it: it adds 245 items to cargo tree and approx 3 MB to release builds by my count. (Yes, dark-light has a closed issue about zbus being a heavy dependency, but preferred since it's a pure-rust crate.)

dhardy avatar Jun 16 '25 18:06 dhardy

Using the dark-light crate instead on applicable platforms is fine, albeit a little bit tedious.

The dark-light crate has removed subscription functionality. It no longer works for this use-case.

The issue is that in order to subscribe to dark/light changes on macOS(/windows?) requires being in control of the event loop, which is exactly why it's implemented here in winit.

In essence, there is currently no crate which implements theme functionality on these three platforms. That is why i've raised the issue again here.

4e554c4c avatar Jun 17 '25 01:06 4e554c4c

The issue is that in order to subscribe to dark/light changes on ... requires being in control of the event loop...

Not really true ime. On macOS one can observe the effectiveAppearance property of [NSApplication sharedApplication] independently from any thread, you just need to be running inside of an AppKit app and therefore have something driving the main thread. That permits the observation callback to run on the main thread transparently. There's so many ways to execute your own code on the main event loop on macOS.

BlackHoleFox avatar Jun 17 '25 01:06 BlackHoleFox

The dark-light crate has removed subscription functionality. It no longer works for this use-case.

A while back I made a crate that for exactly this (and more prefs like the accent color): https://github.com/bash/mundy/ It works on macOS, Windows, Linux and Web. On Linux it does add a few dependencies because I pull in zbus.

The caveats boil down to: Call it from the main thread and make sure to initialize the event loop beforehand.

tautropfli avatar Jun 17 '25 18:06 tautropfli

That seems fair enough. This feature can be encapsulated into a crate that is not winit.

Now, the issue still stands that winit provides this API on all platforms, but in only works on two of them (macOS and Windows), when it really applies to macOS, windows, linux, iOS and android.

In my opinion, either this API should be removed from winit or it should be #[cfg(any(windows,target_os = "macos"))] to avoid confusion (yes, this is clearly documented, but there are plenty of vibe coders nowadays that will try to use the API if the compiler doesn't scream in their face).

4e554c4c avatar Jun 17 '25 19:06 4e554c4c

Nothing stops external crate to use dbus, accept &mut ApplicationHandler during dispatch, and call into the corresponding method, thus merging it with the rest of the platforms. Like at the current state of things it doesn't really have to be in winit if you want to have dbus and also sink into the same place.

In general, I'm in favor of moving theme stuff into e.g. separate trait, etc.

kchibisov avatar Jun 22 '25 09:06 kchibisov