diff --git a/.changes/linux-system-color-scheme.md b/.changes/linux-system-color-scheme.md new file mode 100644 index 000000000..541cd6c52 --- /dev/null +++ b/.changes/linux-system-color-scheme.md @@ -0,0 +1,5 @@ +--- +tao: minor +--- + +Use the Linux XDG Desktop Portal to add support for the system color scheme. Needs `dbus` feature flag. diff --git a/Cargo.lock b/Cargo.lock index 34720444d..0bf49d70b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -424,6 +424,17 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" +[[package]] +name = "dbus" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b3aa68d7e7abee336255bd7248ea965cc393f3e70411135a6f6a4b651345d4" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.59.0", +] + [[package]] name = "dispatch2" version = "0.3.0" @@ -1293,6 +1304,15 @@ version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + [[package]] name = "libfuzzer-sys" version = "0.4.8" @@ -2221,6 +2241,7 @@ dependencies = [ "core-foundation", "core-graphics", "crossbeam-channel", + "dbus", "dispatch2", "dlopen2", "dpi", diff --git a/Cargo.toml b/Cargo.toml index 407a88b29..00fc50b96 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ categories = ["gui"] include = ["/README.md", "src/**/*.rs", "examples/**/*.rs", "LICENSE*"] [package.metadata.docs.rs] -features = ["rwh_04", "rwh_05", "rwh_06", "serde", "x11"] +features = ["rwh_04", "rwh_05", "rwh_06", "serde", "x11", "dbus"] default-target = "x86_64-unknown-linux-gnu" targets = [ "i686-pc-windows-msvc", @@ -28,12 +28,13 @@ targets = [ ] [features] -default = ["rwh_06", "x11"] +default = ["rwh_06", "x11", "dbus"] serde = ["dep:serde", "dpi/serde"] rwh_04 = ["dep:rwh_04"] rwh_05 = ["dep:rwh_05"] rwh_06 = ["dep:rwh_06"] x11 = ["dep:gdkx11-sys", "dep:x11-dl"] +dbus = ["dep:dbus"] [workspace] members = ["tao-macros"] @@ -157,3 +158,4 @@ gdkwayland-sys = "0.18.0" x11-dl = { version = "2.21", optional = true } parking_lot = "0.12" dlopen2 = "0.8.0" +dbus = { version = "0.9", optional = true } diff --git a/src/event.rs b/src/event.rs index 54b77ce7d..5a546b96e 100644 --- a/src/event.rs +++ b/src/event.rs @@ -412,7 +412,7 @@ pub enum WindowEvent<'a> { /// /// ## Platform-specific /// - /// - **Linux / Android / iOS:** Unsupported + /// - **Android / iOS:** Unsupported ThemeChanged(Theme), /// The window decorations has been clicked. diff --git a/src/platform_impl/linux/event_loop.rs b/src/platform_impl/linux/event_loop.rs index 2152fabc6..4e9fe979a 100644 --- a/src/platform_impl/linux/event_loop.rs +++ b/src/platform_impl/linux/event_loop.rs @@ -284,6 +284,15 @@ impl EventLoop { let mut taskbar = TaskbarIndicator::new(); let is_wayland = window_target.is_wayland(); + // Receive portal events + #[cfg(feature = "dbus")] + { + let tx_requests_clone = window_target.window_requests_tx.clone(); + if let Err(e) = super::portal::receive_theme_changed(tx_requests_clone) { + log::debug!("Unable to receive theme changed events: {e}"); + } + } + // Window Request window_requests_rx.attach(Some(&context), move |(id, request)| { if let Some(window) = app_.window_by_id(id.0) { @@ -947,9 +956,15 @@ impl EventLoop { } WindowRequest::SetTheme(theme) => { if let Some(settings) = Settings::default() { - match theme { - Some(Theme::Dark) => settings.set_gtk_application_prefer_dark_theme(true), - Some(Theme::Light) | None => settings.set_gtk_application_prefer_dark_theme(false), + settings.set_gtk_application_prefer_dark_theme(theme == Some(Theme::Dark)); + if let Err(e) = event_tx.send(Event::WindowEvent { + window_id: RootWindowId(id), + event: WindowEvent::ThemeChanged(theme.unwrap_or_default()), + }) { + log::warn!( + "Failed to send window theme changed event to event channel: {}", + e + ); } } } diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index df9981356..0473f67e5 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -9,6 +9,8 @@ mod icon; mod keyboard; mod keycode; mod monitor; +#[cfg(feature = "dbus")] +mod portal; mod util; mod window; diff --git a/src/platform_impl/linux/portal.rs b/src/platform_impl/linux/portal.rs new file mode 100644 index 000000000..c0df6a43d --- /dev/null +++ b/src/platform_impl/linux/portal.rs @@ -0,0 +1,78 @@ +#[cfg(feature = "dbus")] +use dbus::{ + arg::Variant, + blocking::{Connection, SyncConnection}, + message::MatchRule, + Error, +}; +use gtk::glib::{ControlFlow, MainContext, Priority, Sender}; +use log::warn; +use std::{thread, time::Duration}; + +use crate::{ + platform_impl::{platform::window::WindowRequest, WindowId}, + window::Theme, +}; + +pub fn theme() -> Result { + let conn = Connection::new_session()?; + let proxy = conn.with_proxy( + "org.freedesktop.portal.Desktop", + "/org/freedesktop/portal/desktop", + Duration::from_secs(5), + ); + + let result: (Variant>,) = proxy.method_call( + "org.freedesktop.portal.Settings", + "Read", + ("org.freedesktop.appearance", "color-scheme"), + )?; + + Ok(color_scheme_to_theme(result.0 .0 .0)) +} + +pub fn receive_theme_changed(window_tx: Sender<(WindowId, WindowRequest)>) -> Result<(), Error> { + let conn = SyncConnection::new_session()?; + let match_rule = MatchRule::new_signal("org.freedesktop.portal.Settings", "SettingChanged"); + let (tx, rx) = MainContext::channel(Priority::DEFAULT); + + conn.add_match(match_rule, move |_: (), _, msg| { + let mut iter = msg.iter_init(); + if let (Ok("org.freedesktop.appearance"), Ok("color-scheme"), Ok(value)) = ( + iter.read::<&str>(), // Namespace + iter.read::<&str>(), // Key + iter.read::>(), // Value + ) { + if let Err(e) = tx.send(color_scheme_to_theme(value.0)) { + warn!("Failed to send theme change via channel: {}", e); + } + } + true + })?; + + rx.attach(None, move |theme| { + if let Err(e) = window_tx.send((WindowId::dummy(), WindowRequest::SetTheme(Some(theme)))) { + warn!("Failed to send theme change request: {}", e); + ControlFlow::Break + } else { + ControlFlow::Continue + } + }); + + thread::spawn(move || loop { + if let Err(e) = conn.process(Duration::from_secs(5)) { + warn!("D-Bus message processing error: {}", e); + break; + } + }); + + Ok(()) +} + +fn color_scheme_to_theme(color_scheme: u32) -> Theme { + match color_scheme { + 1 => Theme::Dark, // Prefer Dark + 2 => Theme::Light, // Prefer Light + _ => Theme::Light, // No Preference, default to Light + } +} diff --git a/src/platform_impl/linux/window.rs b/src/platform_impl/linux/window.rs index 923169f75..fca4c308c 100644 --- a/src/platform_impl/linux/window.rs +++ b/src/platform_impl/linux/window.rs @@ -43,10 +43,6 @@ impl WindowId { } } -// Currently GTK doesn't provide feature for detect theme, so we need to check theme manually. -// ref: https://github.com/WebKit/WebKit/blob/e44ffaa0d999a9807f76f1805943eea204cfdfbc/Source/WebKit/UIProcess/API/gtk/PageClientImpl.cpp#L587 -const GTK_THEME_SUFFIX_LIST: [&str; 3] = ["-dark", "-Dark", "-Darker"]; - pub struct Window { /// Window id. pub(crate) window_id: WindowId, @@ -197,29 +193,17 @@ impl Window { window.stick(); } - let preferred_theme = if let Some(settings) = Settings::default() { - if let Some(preferred_theme) = attributes.preferred_theme { - match preferred_theme { - Theme::Dark => settings.set_gtk_application_prefer_dark_theme(true), - Theme::Light => { - if let Some(theme) = settings.gtk_theme_name() { - let theme = theme.as_str(); - // Remove dark variant. - if let Some(theme) = GTK_THEME_SUFFIX_LIST - .iter() - .find(|t| theme.ends_with(*t)) - .map(|v| theme.strip_suffix(v)) - { - settings.set_gtk_theme_name(theme); - } - } - } - } + // Set initial `preferred_theme` value to current portal color-scheme + #[cfg(feature = "dbus")] + let preferred_theme = super::portal::theme().ok(); + #[cfg(not(feature = "dbus"))] + let preferred_theme = None; + + if let Some(theme) = preferred_theme { + if let Some(settings) = Settings::default() { + settings.set_gtk_application_prefer_dark_theme(theme == Theme::Dark); } - attributes.preferred_theme - } else { - None - }; + } if attributes.visible { window.show_all(); @@ -1029,11 +1013,9 @@ impl Window { return theme; } - if let Some(theme) = Settings::default().and_then(|s| s.gtk_theme_name()) { - let theme = theme.as_str(); - if GTK_THEME_SUFFIX_LIST.iter().any(|t| theme.ends_with(t)) { - return Theme::Dark; - } + #[cfg(feature = "dbus")] + if let Ok(portal_theme) = super::portal::theme() { + return portal_theme; } Theme::Light