mirror of
https://github.com/storytold/soundcraft.git
synced 2026-10-24 02:26:46 -06:00
Merge pull request #977 from MarekSpodBiedry/issue-951
Follow the desktop light/dark setting for the System theme on Linux (#951)
This commit is contained in:
Generated
+1
@@ -3800,6 +3800,7 @@ dependencies = [
|
|||||||
"soundcraft-video",
|
"soundcraft-video",
|
||||||
"soundcraft-vst3-host",
|
"soundcraft-vst3-host",
|
||||||
"toml",
|
"toml",
|
||||||
|
"zbus",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ serde_json.workspace = true
|
|||||||
log.workspace = true
|
log.workspace = true
|
||||||
image.workspace = true
|
image.workspace = true
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
zbus = "5"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] }
|
egui_kittest = { workspace = true, features = ["wgpu", "snapshot"] }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
//! The desktop's dark/light setting for the System theme on Linux, where eframe/winit reports no
|
||||||
|
//! system theme to egui.
|
||||||
|
//!
|
||||||
|
//! The XDG settings portal (`org.freedesktop.appearance` / `color-scheme`) is read once on a worker
|
||||||
|
//! thread, which then follows its `SettingChanged` signal and wakes the UI only when the value
|
||||||
|
//! changes. Nothing is polled. Elsewhere egui's own report (`Context::system_theme`) is enough.
|
||||||
|
|
||||||
|
/// egui's own report when it has one, otherwise the desktop's.
|
||||||
|
pub fn resolve(reported: Option<egui::Theme>, desktop: impl FnOnce() -> Option<egui::Theme>) -> Option<egui::Theme> {
|
||||||
|
reported.or_else(desktop)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The desktop's appearance, or `None` when it is unknown or this platform has no portal.
|
||||||
|
/// Unit tests never ask the real desktop, so they do not depend on the machine's setting.
|
||||||
|
pub fn desktop_theme(ctx: &egui::Context) -> Option<egui::Theme> {
|
||||||
|
#[cfg(all(target_os = "linux", not(test)))]
|
||||||
|
{
|
||||||
|
linux::current(ctx)
|
||||||
|
}
|
||||||
|
#[cfg(any(not(target_os = "linux"), test))]
|
||||||
|
{
|
||||||
|
let _ = ctx;
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[cfg_attr(test, allow(dead_code))]
|
||||||
|
mod linux {
|
||||||
|
use std::sync::atomic::{AtomicU8, Ordering};
|
||||||
|
use std::sync::{Arc, OnceLock};
|
||||||
|
|
||||||
|
const PORTAL_NAMESPACE: &str = "org.freedesktop.appearance";
|
||||||
|
const PORTAL_KEY: &str = "color-scheme";
|
||||||
|
|
||||||
|
/// The last `color-scheme` seen: 0 = unknown or no preference, 1 = dark, 2 = light.
|
||||||
|
static STATE: OnceLock<Arc<AtomicU8>> = OnceLock::new();
|
||||||
|
|
||||||
|
pub fn current(ctx: &egui::Context) -> Option<egui::Theme> {
|
||||||
|
let state = STATE.get_or_init(|| {
|
||||||
|
let value = Arc::new(AtomicU8::new(0));
|
||||||
|
let (worker_value, worker_ctx) = (Arc::clone(&value), ctx.clone());
|
||||||
|
// If the thread cannot start, the value stays 0 and System keeps following egui only.
|
||||||
|
let _ = std::thread::Builder::new().name("appearance-portal".into()).spawn(move || watch(&worker_value, &worker_ctx));
|
||||||
|
value
|
||||||
|
});
|
||||||
|
decode(u32::from(state.load(Ordering::Relaxed)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn portal_proxy() -> Option<zbus::blocking::Proxy<'static>> {
|
||||||
|
let conn = zbus::blocking::connection::Builder::session().ok()?.method_timeout(std::time::Duration::from_secs(2)).build().ok()?;
|
||||||
|
zbus::blocking::Proxy::new(&conn, "org.freedesktop.portal.Desktop", "/org/freedesktop/portal/desktop", "org.freedesktop.portal.Settings").ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn watch(value: &AtomicU8, ctx: &egui::Context) {
|
||||||
|
let Some(proxy) = portal_proxy() else { return };
|
||||||
|
// Subscribe before Read so a change during startup is queued rather than lost.
|
||||||
|
let Ok(signals) = proxy.receive_signal("SettingChanged") else { return };
|
||||||
|
let initial: Option<zbus::zvariant::OwnedValue> = proxy.call("Read", &(PORTAL_NAMESPACE, PORTAL_KEY)).ok();
|
||||||
|
publish(value, ctx, initial.and_then(portal_code).and_then(decode));
|
||||||
|
// Blocks until the portal sends a signal: no polling.
|
||||||
|
for message in signals {
|
||||||
|
let Ok((namespace, key, changed)) = message.body().deserialize::<(String, String, zbus::zvariant::OwnedValue)>() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if namespace == PORTAL_NAMESPACE && key == PORTAL_KEY {
|
||||||
|
publish(value, ctx, portal_code(changed).and_then(decode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn publish(value: &AtomicU8, ctx: &egui::Context, theme: Option<egui::Theme>) {
|
||||||
|
if value.swap(code_of(theme), Ordering::Relaxed) != code_of(theme) {
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The `u32` inside a portal value. `Settings.Read` answers a variant, and some portals nest
|
||||||
|
/// another variant inside it.
|
||||||
|
fn portal_code(value: zbus::zvariant::OwnedValue) -> Option<u32> {
|
||||||
|
let mut value: zbus::zvariant::Value<'_> = value.into();
|
||||||
|
for _ in 0..4 {
|
||||||
|
match value {
|
||||||
|
zbus::zvariant::Value::Value(inner) => value = *inner,
|
||||||
|
other => return u32::try_from(other).ok(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The portal's `color-scheme`: 0 = no preference, 1 = prefer dark, 2 = prefer light.
|
||||||
|
pub(super) fn decode(code: u32) -> Option<egui::Theme> {
|
||||||
|
match code {
|
||||||
|
1 => Some(egui::Theme::Dark),
|
||||||
|
2 => Some(egui::Theme::Light),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn code_of(theme: Option<egui::Theme>) -> u8 {
|
||||||
|
match theme {
|
||||||
|
Some(egui::Theme::Dark) => 1,
|
||||||
|
Some(egui::Theme::Light) => 2,
|
||||||
|
None => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn desktop_setting_is_used_only_when_egui_reports_nothing() {
|
||||||
|
assert_eq!(resolve(None, || Some(egui::Theme::Light)), Some(egui::Theme::Light));
|
||||||
|
assert_eq!(resolve(None, || None), None);
|
||||||
|
assert_eq!(resolve(Some(egui::Theme::Dark), || Some(egui::Theme::Light)), Some(egui::Theme::Dark));
|
||||||
|
let asked = std::cell::Cell::new(false);
|
||||||
|
let theme = resolve(Some(egui::Theme::Light), || {
|
||||||
|
asked.set(true);
|
||||||
|
None
|
||||||
|
});
|
||||||
|
assert_eq!(theme, Some(egui::Theme::Light));
|
||||||
|
assert!(!asked.get(), "must not ask the desktop");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
#[test]
|
||||||
|
fn portal_color_scheme_codes() {
|
||||||
|
assert_eq!(linux::decode(1), Some(egui::Theme::Dark));
|
||||||
|
assert_eq!(linux::decode(2), Some(egui::Theme::Light));
|
||||||
|
assert_eq!(linux::decode(0), None);
|
||||||
|
assert_eq!(linux::decode(7), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
//! executing commands. It can be replaced without touching anything below it.
|
//! executing commands. It can be replaced without touching anything below it.
|
||||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||||
|
|
||||||
|
pub mod appearance;
|
||||||
pub mod bounce;
|
pub mod bounce;
|
||||||
pub mod control;
|
pub mod control;
|
||||||
pub mod credits;
|
pub mod credits;
|
||||||
|
|||||||
@@ -393,12 +393,13 @@ pub fn is_light() -> bool {
|
|||||||
LIGHT_THEME.load(Ordering::Relaxed)
|
LIGHT_THEME.load(Ordering::Relaxed)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolve each frame from the integration's current OS appearance. Unknown uses Dark.
|
/// Resolve each frame from the integration's current OS appearance, or on Linux the desktop's own
|
||||||
|
/// setting when egui reports none. Unknown uses Dark.
|
||||||
pub fn wants_light(ctx: &egui::Context, mode: ThemeMode) -> bool {
|
pub fn wants_light(ctx: &egui::Context, mode: ThemeMode) -> bool {
|
||||||
match mode {
|
match mode {
|
||||||
ThemeMode::Light => true,
|
ThemeMode::Light => true,
|
||||||
ThemeMode::Dark => false,
|
ThemeMode::Dark => false,
|
||||||
ThemeMode::System => ctx.system_theme() == Some(egui::Theme::Light),
|
ThemeMode::System => crate::appearance::resolve(ctx.system_theme(), || crate::appearance::desktop_theme(ctx)) == Some(egui::Theme::Light),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -83,9 +83,11 @@ script events such as `{"system_theme": "dark"}` to change it on subsequent fram
|
|||||||
|
|
||||||
Appearance defaults to Dark. `ui.theme {"mode":"system"}` follows the appearance supplied by
|
Appearance defaults to Dark. `ui.theme {"mode":"system"}` follows the appearance supplied by
|
||||||
the native or web integration at launch and on later frames, while the saved choice stays System.
|
the native or web integration at launch and on later frames, while the saved choice stays System.
|
||||||
When the integration cannot report an appearance, System uses Dark. Manual Light and Dark stay
|
When the integration cannot report an appearance, as on Linux, System reads the desktop's
|
||||||
fixed when the OS appearance changes. An offscreen render without `--system-theme` supplies no
|
settings portal (`org.freedesktop.appearance` `color-scheme`) and follows its changes; without
|
||||||
OS appearance and therefore resolves System to Dark.
|
a portal answer it uses Dark. Manual Light and Dark stay fixed when the OS appearance changes.
|
||||||
|
An offscreen render without `--system-theme` supplies no OS appearance and therefore resolves
|
||||||
|
System to Dark, or on Linux to the desktop's portal setting when one is available.
|
||||||
|
|
||||||
## Panel docking
|
## Panel docking
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user