Skip to content

Commit 6642f67

Browse files
committed
Adaptive theme support for linux
1 parent ab26ffd commit 6642f67

File tree

13 files changed

+778
-8
lines changed

13 files changed

+778
-8
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.

docs/docs/config.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ args = []
3535

3636
## adaptive-theme
3737

38-
Rio supports adaptive themes that automatically switch between light and dark themes based on the system theme. This feature works on Web, MacOS, and Windows platforms.
38+
Rio supports adaptive themes that automatically switch between light and dark themes based on the system theme. This feature works on Linux, Web, MacOS, and Windows platforms.
3939

4040
```toml
4141
[adaptive-theme]

frontends/rioterm/src/application.rs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,25 @@ 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(
71+
move || {
72+
// Request config update to apply the new theme
73+
theme_event_proxy.send_event(
74+
RioEventType::Rio(RioEvent::UpdateConfig),
75+
rio_backend::event::WindowId::from(0),
76+
);
77+
},
78+
);
79+
}
80+
6281
let scheduler = Scheduler::new(proxy);
6382
event_loop.listen_device_events(DeviceEvents::Never);
6483

@@ -351,13 +370,18 @@ impl ApplicationHandler<EventPayload> for Application<'_> {
351370
}
352371
}
353372
RioEventType::Rio(RioEvent::PrepareUpdateConfig) => {
373+
eprintln!(
374+
"[Rio] PrepareUpdateConfig event received for window: {:?}",
375+
window_id
376+
);
354377
let timer_id = TimerId::new(Topic::UpdateConfig, 0);
355378
let event = EventPayload::new(
356379
RioEventType::Rio(RioEvent::UpdateConfig),
357380
window_id,
358381
);
359382

360383
if !self.scheduler.scheduled(timer_id) {
384+
eprintln!("[Rio] Scheduling UpdateConfig event");
361385
self.scheduler.schedule(
362386
event,
363387
Duration::from_millis(250),
@@ -396,7 +420,30 @@ impl ApplicationHandler<EventPayload> for Application<'_> {
396420
for (_id, route) in self.router.routes.iter_mut() {
397421
// Apply system theme to ensure colors are consistent
398422
if !has_checked_adaptive_colors {
423+
// On Linux, read cached theme (non-blocking)
424+
#[cfg(all(
425+
unix,
426+
not(any(
427+
target_os = "redox",
428+
target_family = "wasm",
429+
target_os = "macos"
430+
))
431+
))]
432+
let system_theme =
433+
rio_window::platform::linux::theme_monitor::get_cached_theme(
434+
);
435+
436+
// On other platforms, use window.theme()
437+
#[cfg(not(all(
438+
unix,
439+
not(any(
440+
target_os = "redox",
441+
target_family = "wasm",
442+
target_os = "macos"
443+
))
444+
)))]
399445
let system_theme = route.window.winit_window.theme();
446+
400447
update_colors_based_on_theme(&mut self.config, system_theme);
401448
has_checked_adaptive_colors = true;
402449
}

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
@@ -2,6 +2,8 @@
22
//!
33
//! Only the modules corresponding to the platform you're compiling to will be available.
44
5+
#[cfg(any(x11_platform, wayland_platform, docsrs))]
6+
pub mod linux;
57
#[cfg(any(macos_platform, docsrs))]
68
pub mod macos;
79
#[cfg(any(orbital_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: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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>(
68+
on_change: Arc<F>,
69+
) -> Result<(), Box<dyn std::error::Error>>
70+
where
71+
F: Fn() + Send + Sync + 'static,
72+
{
73+
use ashpd::zbus::fdo::DBusProxy;
74+
use ashpd::zbus::{Connection, MatchRule, MessageStream};
75+
use futures_util::stream::StreamExt;
76+
77+
// Connect to session bus
78+
let connection = Connection::session().await?;
79+
80+
// Create match rule for Settings.SettingChanged signal
81+
let match_rule = MatchRule::builder()
82+
.msg_type(ashpd::zbus::message::Type::Signal)
83+
.interface("org.freedesktop.portal.Settings")?
84+
.member("SettingChanged")?
85+
.build();
86+
87+
let dbus_proxy = DBusProxy::new(&connection).await?;
88+
dbus_proxy.add_match_rule(match_rule.clone()).await?;
89+
90+
// Create message stream
91+
let mut stream =
92+
MessageStream::for_match_rule(match_rule, &connection, Some(100)).await?;
93+
94+
// Process signals as they arrive
95+
while let Some(msg) = stream.next().await {
96+
let msg = msg?;
97+
98+
// Try to parse the signal arguments
99+
if let Ok((namespace, key, value)) =
100+
msg.body()
101+
.deserialize::<(String, String, ashpd::zbus::zvariant::Value)>()
102+
{
103+
if namespace == "org.freedesktop.appearance" && key == "color-scheme" {
104+
// Extract the theme value (uint32: 0=no pref, 1=dark, 2=light)
105+
if let Ok(variant) = value.downcast::<ashpd::zbus::zvariant::Value>() {
106+
if let Ok(scheme_value) = variant.downcast::<u32>() {
107+
let theme = match scheme_value {
108+
1 => Some(Theme::Dark),
109+
2 => Some(Theme::Light),
110+
_ => None,
111+
};
112+
set_cached_theme(theme);
113+
// Invoke the callback to notify the application
114+
on_change();
115+
}
116+
}
117+
}
118+
}
119+
}
120+
121+
Ok(())
122+
}
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()
25+
.ok()?
26+
.block_on(async { query_color_scheme_async().await })
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)]

0 commit comments

Comments
 (0)