Skip to content

Commit 9026f56

Browse files
committed
Adaptive theme support for linux
1 parent ab26ffd commit 9026f56

File tree

11 files changed

+758
-4
lines changed

11 files changed

+758
-4
lines changed

Cargo.lock

Lines changed: 537 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontends/rioterm/src/application.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,23 @@ impl Application<'_> {
5959
rio_backend::config::config_dir_path(),
6060
event_proxy.clone(),
6161
);
62+
63+
// Start monitoring system theme changes on Linux
64+
#[cfg(all(
65+
unix,
66+
not(any(target_os = "redox", target_family = "wasm", target_os = "macos"))
67+
))]
68+
{
69+
let theme_event_proxy = event_proxy.clone();
70+
let _ = rio_window::platform::linux::theme_monitor::start_theme_monitor(move || {
71+
// Request config update to apply the new theme
72+
theme_event_proxy.send_event(
73+
RioEventType::Rio(RioEvent::UpdateConfig),
74+
rio_backend::event::WindowId::from(0),
75+
);
76+
});
77+
}
78+
6279
let scheduler = Scheduler::new(proxy);
6380
event_loop.listen_device_events(DeviceEvents::Never);
6481

@@ -351,13 +368,15 @@ impl ApplicationHandler<EventPayload> for Application<'_> {
351368
}
352369
}
353370
RioEventType::Rio(RioEvent::PrepareUpdateConfig) => {
371+
eprintln!("[Rio] PrepareUpdateConfig event received for window: {:?}", window_id);
354372
let timer_id = TimerId::new(Topic::UpdateConfig, 0);
355373
let event = EventPayload::new(
356374
RioEventType::Rio(RioEvent::UpdateConfig),
357375
window_id,
358376
);
359377

360378
if !self.scheduler.scheduled(timer_id) {
379+
eprintln!("[Rio] Scheduling UpdateConfig event");
361380
self.scheduler.schedule(
362381
event,
363382
Duration::from_millis(250),
@@ -396,7 +415,20 @@ impl ApplicationHandler<EventPayload> for Application<'_> {
396415
for (_id, route) in self.router.routes.iter_mut() {
397416
// Apply system theme to ensure colors are consistent
398417
if !has_checked_adaptive_colors {
418+
// On Linux, read cached theme (non-blocking)
419+
#[cfg(all(
420+
unix,
421+
not(any(target_os = "redox", target_family = "wasm", target_os = "macos"))
422+
))]
423+
let system_theme = rio_window::platform::linux::theme_monitor::get_cached_theme();
424+
425+
// On other platforms, use window.theme()
426+
#[cfg(not(all(
427+
unix,
428+
not(any(target_os = "redox", target_family = "wasm", target_os = "macos"))
429+
)))]
399430
let system_theme = route.window.winit_window.theme();
431+
400432
update_colors_based_on_theme(&mut self.config, system_theme);
401433
has_checked_adaptive_colors = true;
402434
}

rio-window/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,10 @@ winres = "0.1.12"
154154

155155
[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "macos"))))'.dependencies]
156156
ahash = { version = "0.8.7", features = ["no-rng"], optional = true }
157+
ashpd = { version = "0.9", default-features = false, features = ["tokio"] }
157158
bytemuck = { version = "1.13.1", default-features = false, optional = true }
159+
futures-util = "0.3"
160+
tokio = { version = "1", features = ["rt", "rt-multi-thread"] }
158161
calloop = "0.13.0"
159162
libc = { workspace = true }
160163
memmap2 = { workspace = true, optional = true }

rio-window/src/platform/linux.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
//! Linux-specific functionality.
2+
3+
#[cfg(any(x11_platform, wayland_platform))]
4+
#[doc(inline)]
5+
pub use crate::platform_impl::common::theme_monitor;
6+
7+
#[cfg(any(x11_platform, wayland_platform))]
8+
#[doc(inline)]
9+
pub use crate::platform_impl::common::xdg_desktop_portal;

rio-window/src/platform/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ pub mod startup_notify;
1212
pub mod wayland;
1313
#[cfg(any(web_platform, docsrs))]
1414
pub mod web;
15+
#[cfg(any(x11_platform, wayland_platform, docsrs))]
16+
pub mod linux;
1517
#[cfg(any(windows_platform, docsrs))]
1618
pub mod windows;
1719
#[cfg(any(x11_platform, docsrs))]
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1+
pub mod theme_monitor;
2+
pub mod xdg_desktop_portal;
13
pub mod xkb;
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
//! Background monitor for system theme changes via XDG Desktop Portal.
2+
//!
3+
//! This module sets up a listener for the SettingsChanged signal from the
4+
//! XDG Desktop Portal, specifically monitoring for color-scheme preference changes.
5+
6+
use crate::window::Theme;
7+
use std::sync::atomic::{AtomicU8, Ordering};
8+
use std::sync::Arc;
9+
10+
// Cache for the current theme (0 = None, 1 = Dark, 2 = Light)
11+
static CACHED_THEME: AtomicU8 = AtomicU8::new(0);
12+
13+
/// Get the cached theme without blocking
14+
pub fn get_cached_theme() -> Option<Theme> {
15+
match CACHED_THEME.load(Ordering::Relaxed) {
16+
1 => Some(Theme::Dark),
17+
2 => Some(Theme::Light),
18+
_ => None,
19+
}
20+
}
21+
22+
pub(crate) fn set_cached_theme(theme: Option<Theme>) {
23+
let value = match theme {
24+
Some(Theme::Dark) => 1,
25+
Some(Theme::Light) => 2,
26+
None => 0,
27+
};
28+
CACHED_THEME.store(value, Ordering::Relaxed);
29+
}
30+
31+
/// Starts monitoring for system theme changes in a background thread.
32+
///
33+
/// When the system color scheme preference changes (dark/light), the provided
34+
/// callback will be invoked. This allows applications to respond to theme
35+
/// changes in real-time without needing to restart.
36+
///
37+
/// # Arguments
38+
///
39+
/// * `on_change` - Callback function to invoke when theme changes are detected
40+
///
41+
/// # Returns
42+
///
43+
/// Returns `Ok(())` if the monitor was successfully started, or `Err` if
44+
/// the XDG Desktop Portal is not available or there was an error setting up
45+
/// the signal listener.
46+
pub fn start_theme_monitor<F>(on_change: F) -> Result<(), Box<dyn std::error::Error>>
47+
where
48+
F: Fn() + Send + Sync + 'static,
49+
{
50+
let callback = Arc::new(on_change);
51+
52+
std::thread::spawn(move || {
53+
// Create a new tokio runtime for this thread
54+
let rt = match tokio::runtime::Runtime::new() {
55+
Ok(rt) => rt,
56+
Err(_) => return,
57+
};
58+
59+
rt.block_on(async {
60+
let _ = monitor_theme_changes(callback).await;
61+
});
62+
});
63+
64+
Ok(())
65+
}
66+
67+
async fn monitor_theme_changes<F>(on_change: Arc<F>) -> Result<(), Box<dyn std::error::Error>>
68+
where
69+
F: Fn() + Send + Sync + 'static,
70+
{
71+
use ashpd::zbus::{Connection, MatchRule, MessageStream};
72+
use ashpd::zbus::fdo::DBusProxy;
73+
use futures_util::stream::StreamExt;
74+
75+
// Connect to session bus
76+
let connection = Connection::session().await?;
77+
78+
// Create match rule for Settings.SettingChanged signal
79+
let match_rule = MatchRule::builder()
80+
.msg_type(ashpd::zbus::message::Type::Signal)
81+
.interface("org.freedesktop.portal.Settings")?
82+
.member("SettingChanged")?
83+
.build();
84+
85+
let dbus_proxy = DBusProxy::new(&connection).await?;
86+
dbus_proxy.add_match_rule(match_rule.clone()).await?;
87+
88+
// Create message stream
89+
let mut stream = MessageStream::for_match_rule(
90+
match_rule,
91+
&connection,
92+
Some(100),
93+
).await?;
94+
95+
// Process signals as they arrive
96+
while let Some(msg) = stream.next().await {
97+
let msg = msg?;
98+
99+
// Try to parse the signal arguments
100+
if let Ok((namespace, key, value)) = msg.body().deserialize::<(String, String, ashpd::zbus::zvariant::Value)>() {
101+
if namespace == "org.freedesktop.appearance" && key == "color-scheme" {
102+
// Extract the theme value (uint32: 0=no pref, 1=dark, 2=light)
103+
if let Ok(variant) = value.downcast::<ashpd::zbus::zvariant::Value>() {
104+
if let Ok(scheme_value) = variant.downcast::<u32>() {
105+
let theme = match scheme_value {
106+
1 => Some(Theme::Dark),
107+
2 => Some(Theme::Light),
108+
_ => None,
109+
};
110+
set_cached_theme(theme);
111+
// Invoke the callback to notify the application
112+
on_change();
113+
}
114+
}
115+
}
116+
}
117+
}
118+
119+
Ok(())
120+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
//! XDG Desktop Portal integration for reading system preferences.
2+
//!
3+
//! This module provides access to system settings via the XDG Desktop Portal,
4+
//! which is the standard cross-desktop API on Linux systems.
5+
6+
use crate::window::Theme;
7+
8+
/// Queries the system color scheme preference via XDG Desktop Portal.
9+
///
10+
/// This function uses the org.freedesktop.portal.Settings interface to read
11+
/// the color-scheme preference from org.freedesktop.appearance namespace.
12+
///
13+
/// The color-scheme value is a uint32 where:
14+
/// - 0: No preference
15+
/// - 1: Prefer dark appearance
16+
/// - 2: Prefer light appearance
17+
///
18+
/// Returns `None` if the portal is not available, the setting doesn't exist,
19+
/// or if there's an error querying the portal.
20+
pub fn get_color_scheme() -> Option<Theme> {
21+
// Use blocking API since this is called during event loop initialization
22+
// and we need the result immediately
23+
let result = std::panic::catch_unwind(|| {
24+
tokio::runtime::Runtime::new().ok()?.block_on(async {
25+
query_color_scheme_async().await
26+
})
27+
});
28+
29+
let theme = match result {
30+
Ok(Some(theme)) => Some(theme),
31+
_ => None,
32+
};
33+
34+
// Also update the cached theme
35+
super::theme_monitor::set_cached_theme(theme);
36+
theme
37+
}
38+
39+
async fn query_color_scheme_async() -> Option<Theme> {
40+
use ashpd::desktop::settings::{ColorScheme, Settings};
41+
42+
let settings = Settings::new().await.ok()?;
43+
let color_scheme = settings.color_scheme().await.ok()?;
44+
45+
match color_scheme {
46+
ColorScheme::PreferDark => Some(Theme::Dark),
47+
ColorScheme::PreferLight => Some(Theme::Light),
48+
ColorScheme::NoPreference => None,
49+
}
50+
}

rio-window/src/platform_impl/linux/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ pub(crate) use crate::cursor::OnlyCursorImageSource as PlatformCustomCursorSourc
4141
pub(crate) use crate::icon::RgbaIcon as PlatformIcon;
4242
pub(crate) use crate::platform_impl::Fullscreen;
4343

44-
pub(crate) mod common;
44+
pub mod common;
4545
#[cfg(wayland_platform)]
4646
pub(crate) mod wayland;
4747
#[cfg(x11_platform)]

rio-window/src/platform_impl/linux/wayland/event_loop/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -721,7 +721,7 @@ impl ActiveEventLoop {
721721
}
722722

723723
pub(crate) fn system_theme(&self) -> Option<Theme> {
724-
None
724+
super::super::common::xdg_desktop_portal::get_color_scheme()
725725
}
726726

727727
#[inline]

0 commit comments

Comments
 (0)