Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
538 changes: 537 additions & 1 deletion Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/docs/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ args = []

## adaptive-theme

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.
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.

```toml
[adaptive-theme]
Expand Down
47 changes: 47 additions & 0 deletions frontends/rioterm/src/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,25 @@ impl Application<'_> {
rio_backend::config::config_dir_path(),
event_proxy.clone(),
);

// Start monitoring system theme changes on Linux
#[cfg(all(
unix,
not(any(target_os = "redox", target_family = "wasm", target_os = "macos"))
))]
{
let theme_event_proxy = event_proxy.clone();
let _ = rio_window::platform::linux::theme_monitor::start_theme_monitor(
move || {
// Request config update to apply the new theme
theme_event_proxy.send_event(
RioEventType::Rio(RioEvent::UpdateConfig),
rio_backend::event::WindowId::from(0),
);
},
);
}

let scheduler = Scheduler::new(proxy);
event_loop.listen_device_events(DeviceEvents::Never);

Expand Down Expand Up @@ -351,13 +370,18 @@ impl ApplicationHandler<EventPayload> for Application<'_> {
}
}
RioEventType::Rio(RioEvent::PrepareUpdateConfig) => {
eprintln!(
"[Rio] PrepareUpdateConfig event received for window: {:?}",
window_id
);
let timer_id = TimerId::new(Topic::UpdateConfig, 0);
let event = EventPayload::new(
RioEventType::Rio(RioEvent::UpdateConfig),
window_id,
);

if !self.scheduler.scheduled(timer_id) {
eprintln!("[Rio] Scheduling UpdateConfig event");
self.scheduler.schedule(
event,
Duration::from_millis(250),
Expand Down Expand Up @@ -396,7 +420,30 @@ impl ApplicationHandler<EventPayload> for Application<'_> {
for (_id, route) in self.router.routes.iter_mut() {
// Apply system theme to ensure colors are consistent
if !has_checked_adaptive_colors {
// On Linux, read cached theme (non-blocking)
#[cfg(all(
unix,
not(any(
target_os = "redox",
target_family = "wasm",
target_os = "macos"
))
))]
let system_theme =
rio_window::platform::linux::theme_monitor::get_cached_theme(
);

// On other platforms, use window.theme()
#[cfg(not(all(
unix,
not(any(
target_os = "redox",
target_family = "wasm",
target_os = "macos"
))
)))]
let system_theme = route.window.winit_window.theme();

update_colors_based_on_theme(&mut self.config, system_theme);
has_checked_adaptive_colors = true;
}
Expand Down
3 changes: 3 additions & 0 deletions rio-window/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,10 @@ winres = "0.1.12"

[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "macos"))))'.dependencies]
ahash = { version = "0.8.7", features = ["no-rng"], optional = true }
ashpd = { version = "0.9", default-features = false, features = ["tokio"] }
bytemuck = { version = "1.13.1", default-features = false, optional = true }
futures-util = "0.3"
tokio = { version = "1", features = ["rt", "rt-multi-thread"] }
calloop = "0.13.0"
libc = { workspace = true }
memmap2 = { workspace = true, optional = true }
Expand Down
9 changes: 9 additions & 0 deletions rio-window/src/platform/linux.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//! Linux-specific functionality.

#[cfg(any(x11_platform, wayland_platform))]
#[doc(inline)]
pub use crate::platform_impl::common::theme_monitor;

#[cfg(any(x11_platform, wayland_platform))]
#[doc(inline)]
pub use crate::platform_impl::common::xdg_desktop_portal;
2 changes: 2 additions & 0 deletions rio-window/src/platform/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
//!
//! Only the modules corresponding to the platform you're compiling to will be available.

#[cfg(any(x11_platform, wayland_platform, docsrs))]
pub mod linux;
#[cfg(any(macos_platform, docsrs))]
pub mod macos;
#[cfg(any(orbital_platform, docsrs))]
Expand Down
2 changes: 2 additions & 0 deletions rio-window/src/platform_impl/linux/common/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
pub mod theme_monitor;
pub mod xdg_desktop_portal;
pub mod xkb;
122 changes: 122 additions & 0 deletions rio-window/src/platform_impl/linux/common/theme_monitor.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
//! Background monitor for system theme changes via XDG Desktop Portal.
//!
//! This module sets up a listener for the SettingsChanged signal from the
//! XDG Desktop Portal, specifically monitoring for color-scheme preference changes.

use crate::window::Theme;
use std::sync::atomic::{AtomicU8, Ordering};
use std::sync::Arc;

// Cache for the current theme (0 = None, 1 = Dark, 2 = Light)
static CACHED_THEME: AtomicU8 = AtomicU8::new(0);

/// Get the cached theme without blocking
pub fn get_cached_theme() -> Option<Theme> {
match CACHED_THEME.load(Ordering::Relaxed) {
1 => Some(Theme::Dark),
2 => Some(Theme::Light),
_ => None,
}
}

pub(crate) fn set_cached_theme(theme: Option<Theme>) {
let value = match theme {
Some(Theme::Dark) => 1,
Some(Theme::Light) => 2,
None => 0,
};
CACHED_THEME.store(value, Ordering::Relaxed);
}

/// Starts monitoring for system theme changes in a background thread.
///
/// When the system color scheme preference changes (dark/light), the provided
/// callback will be invoked. This allows applications to respond to theme
/// changes in real-time without needing to restart.
///
/// # Arguments
///
/// * `on_change` - Callback function to invoke when theme changes are detected
///
/// # Returns
///
/// Returns `Ok(())` if the monitor was successfully started, or `Err` if
/// the XDG Desktop Portal is not available or there was an error setting up
/// the signal listener.
pub fn start_theme_monitor<F>(on_change: F) -> Result<(), Box<dyn std::error::Error>>
where
F: Fn() + Send + Sync + 'static,
{
let callback = Arc::new(on_change);

std::thread::spawn(move || {
// Create a new tokio runtime for this thread
let rt = match tokio::runtime::Runtime::new() {
Ok(rt) => rt,
Err(_) => return,
};

rt.block_on(async {
let _ = monitor_theme_changes(callback).await;
});
});

Ok(())
}

async fn monitor_theme_changes<F>(
on_change: Arc<F>,
) -> Result<(), Box<dyn std::error::Error>>
where
F: Fn() + Send + Sync + 'static,
{
use ashpd::zbus::fdo::DBusProxy;
use ashpd::zbus::{Connection, MatchRule, MessageStream};
use futures_util::stream::StreamExt;

// Connect to session bus
let connection = Connection::session().await?;

// Create match rule for Settings.SettingChanged signal
let match_rule = MatchRule::builder()
.msg_type(ashpd::zbus::message::Type::Signal)
.interface("org.freedesktop.portal.Settings")?
.member("SettingChanged")?
.build();

let dbus_proxy = DBusProxy::new(&connection).await?;
dbus_proxy.add_match_rule(match_rule.clone()).await?;

// Create message stream
let mut stream =
MessageStream::for_match_rule(match_rule, &connection, Some(100)).await?;

// Process signals as they arrive
while let Some(msg) = stream.next().await {
let msg = msg?;

// Try to parse the signal arguments
if let Ok((namespace, key, value)) =
msg.body()
.deserialize::<(String, String, ashpd::zbus::zvariant::Value)>()
{
if namespace == "org.freedesktop.appearance" && key == "color-scheme" {
// Extract the theme value (uint32: 0=no pref, 1=dark, 2=light)
if let Ok(variant) = value.downcast::<ashpd::zbus::zvariant::Value>() {
if let Ok(scheme_value) = variant.downcast::<u32>() {
let theme = match scheme_value {
1 => Some(Theme::Dark),
2 => Some(Theme::Light),
_ => None,
};
set_cached_theme(theme);
// Invoke the callback to notify the application
on_change();
}
}
}
}
}

Ok(())
}
50 changes: 50 additions & 0 deletions rio-window/src/platform_impl/linux/common/xdg_desktop_portal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//! XDG Desktop Portal integration for reading system preferences.
//!
//! This module provides access to system settings via the XDG Desktop Portal,
//! which is the standard cross-desktop API on Linux systems.

use crate::window::Theme;

/// Queries the system color scheme preference via XDG Desktop Portal.
///
/// This function uses the org.freedesktop.portal.Settings interface to read
/// the color-scheme preference from org.freedesktop.appearance namespace.
///
/// The color-scheme value is a uint32 where:
/// - 0: No preference
/// - 1: Prefer dark appearance
/// - 2: Prefer light appearance
///
/// Returns `None` if the portal is not available, the setting doesn't exist,
/// or if there's an error querying the portal.
pub fn get_color_scheme() -> Option<Theme> {
// Use blocking API since this is called during event loop initialization
// and we need the result immediately
let result = std::panic::catch_unwind(|| {
tokio::runtime::Runtime::new()
.ok()?
.block_on(async { query_color_scheme_async().await })
});

let theme = match result {
Ok(Some(theme)) => Some(theme),
_ => None,
};

// Also update the cached theme
super::theme_monitor::set_cached_theme(theme);
theme
}

async fn query_color_scheme_async() -> Option<Theme> {
use ashpd::desktop::settings::{ColorScheme, Settings};

let settings = Settings::new().await.ok()?;
let color_scheme = settings.color_scheme().await.ok()?;

match color_scheme {
ColorScheme::PreferDark => Some(Theme::Dark),
ColorScheme::PreferLight => Some(Theme::Light),
ColorScheme::NoPreference => None,
}
}
2 changes: 1 addition & 1 deletion rio-window/src/platform_impl/linux/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ pub(crate) use crate::cursor::OnlyCursorImageSource as PlatformCustomCursorSourc
pub(crate) use crate::icon::RgbaIcon as PlatformIcon;
pub(crate) use crate::platform_impl::Fullscreen;

pub(crate) mod common;
pub mod common;
#[cfg(wayland_platform)]
pub(crate) mod wayland;
#[cfg(x11_platform)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -721,7 +721,7 @@ impl ActiveEventLoop {
}

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

#[inline]
Expand Down
2 changes: 1 addition & 1 deletion rio-window/src/platform_impl/linux/x11/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -766,7 +766,7 @@ impl ActiveEventLoop {
}

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

pub(crate) fn exit_code(&self) -> Option<i32> {
Expand Down
5 changes: 2 additions & 3 deletions sugarloaf/src/components/layer/atlas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,9 +175,8 @@ impl Atlas {
let offset = row * padded_width;
let src_row_bytes = (bytes_per_pixel * width) as usize;

padded_data[offset..offset + src_row_bytes].copy_from_slice(
&data[row * src_row_bytes..(row + 1) * src_row_bytes],
)
padded_data[offset..offset + src_row_bytes]
.copy_from_slice(&data[row * src_row_bytes..(row + 1) * src_row_bytes])
}

match &entry {
Expand Down
Loading