From 839e9fa4bc2c3aa551a11b00d5c77d22d8992356 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Sat, 24 Oct 2026 09:59:26 +0700 Subject: [PATCH] #623: the colour scheme watcher wakes the window on a change, reads GNOME's 0 (no preference) as light, and falls back to Read on portals before 1.17 --- apps/figcraft/src/main.rs | 23 ++++++------ apps/figcraft/src/services.rs | 68 ++++++++++++++++++++--------------- 2 files changed, 53 insertions(+), 38 deletions(-) diff --git a/apps/figcraft/src/main.rs b/apps/figcraft/src/main.rs index 0b71149a..633b6998 100644 --- a/apps/figcraft/src/main.rs +++ b/apps/figcraft/src/main.rs @@ -149,21 +149,23 @@ fn main() -> eframe::Result { // System fonts: index them in the background, then wake the UI (an idle window draws no frames) // to lay text out again with them. let ctx_slot: Arc>> = Arc::new(Mutex::new(None)); - let fonts_slot = ctx_slot.clone(); + // Wakes the window (once it is there) from another thread. + let wake = |slot: &Arc>>| { + let slot = slot.clone(); + move || { + if let Some(c) = slot.lock().unwrap_or_else(std::sync::PoisonError::into_inner).as_ref() { + c.request_repaint(); + } + } + }; + let fonts_wake = wake(&ctx_slot); std::thread::spawn(move || { figcraft_text::scan_system_fonts(); - if let Some(c) = fonts_slot.lock().unwrap_or_else(std::sync::PoisonError::into_inner).as_ref() { - c.request_repaint(); - } + fonts_wake(); }); let mut session = Session::empty(); // Live sessions: the network threads wake the window when something arrives. - let wake_slot = ctx_slot.clone(); - session.live.install(Box::new(figcraft_collab_net::NetLink::new(Arc::new(move || { - if let Some(c) = wake_slot.lock().unwrap_or_else(std::sync::PoisonError::into_inner).as_ref() { - c.request_repaint(); - } - })))); + session.live.install(Box::new(figcraft_collab_net::NetLink::new(Arc::new(wake(&ctx_slot))))); for f in &args.files { if let Err(e) = session.execute("file.open", &json!({"path": f})) { log::error!("{f}: {e}"); @@ -175,6 +177,7 @@ fn main() -> eframe::Result { // The preferences first: they hold the online files a recovered document may be one of. let recovery = recovery::Shared::default(); let svc = services::Desktop::load(recovery.clone()); + svc.follow_system_dark(wake(&ctx_slot)); if let Some(prefs) = svc.prefs.clone() { session.prefs = prefs; } diff --git a/apps/figcraft/src/services.rs b/apps/figcraft/src/services.rs index cee8765b..668100f7 100644 --- a/apps/figcraft/src/services.rs +++ b/apps/figcraft/src/services.rs @@ -57,18 +57,22 @@ impl Desktop { let flag = reduce_motion.clone(); std::thread::spawn(move || flag.store(system_reduce_motion(), Ordering::Relaxed)); let dark = Arc::new(AtomicU8::new(DARK_UNKNOWN)); - // winit reports no system theme on Linux and the BSDs: the freedesktop colour scheme - // answers instead, read once and then watched in the background. + Self { clipboard: arboard::Clipboard::new().ok(), dir, prefs, reduce_motion, dark, language: system_language(), recovery } + } + + /// winit reports no system theme on Linux and the BSDs: there the freedesktop colour scheme + /// answers instead, read once and then watched in the background, calling `wake` when it + /// changes (an idle window draws no frames). Elsewhere this does nothing. + pub fn follow_system_dark(&self, wake: impl Fn() + Send + 'static) { #[cfg(all(unix, not(target_os = "macos")))] if let Err(e) = std::thread::Builder::new().name("figcraft-theme".into()).spawn({ - let dark = dark.clone(); - move || watch_system_dark(dark) + let dark = self.dark.clone(); + move || watch_system_dark(&dark, &wake) }) { log::info!("colour scheme: can't start the watcher: {e}"); } #[cfg(not(all(unix, not(target_os = "macos"))))] - let _ = dark; - Self { clipboard: arboard::Clipboard::new().ok(), dir, prefs, reduce_motion, dark, language: system_language(), recovery } + let _ = wake; } } @@ -156,33 +160,44 @@ const DARK_DARK: u8 = 1; const DARK_LIGHT: u8 = 2; /// The desktop's colour scheme through the freedesktop settings portal -/// (`org.freedesktop.appearance` / `color-scheme`: 1 dark, 2 light), read once and then watched -/// for changes. Without a session bus or a portal the scheme stays unknown and the UI keeps its -/// light fallback. +/// (`org.freedesktop.appearance` / `color-scheme`: 1 dark, 2 light, 0 no preference, which is +/// light), read once and then watched for changes, each one calling `wake`. Without a session bus +/// or a portal the scheme stays unknown and the UI keeps its light fallback. #[cfg(all(unix, not(target_os = "macos")))] -fn watch_system_dark(dark: Arc) { +fn watch_system_dark(dark: &AtomicU8, wake: &dyn Fn()) { + use zbus::zvariant::Value; const PORTAL: &str = "org.freedesktop.portal.Desktop"; const SETTINGS: &str = "org.freedesktop.portal.Settings"; const APPEARANCE: &str = "org.freedesktop.appearance"; - let store = |v: u32| match v { - 1 => dark.store(DARK_DARK, Ordering::Relaxed), - 2 => dark.store(DARK_LIGHT, Ordering::Relaxed), - _ => {} + // `Read` (portals before 1.17, which lack `ReadOne`) wraps the value in one more variant. + let scheme = |v: &Value| match v { + Value::U32(x) => *x, + Value::Value(inner) => match **inner { + Value::U32(x) => x, + _ => 0, + }, + _ => 0, + }; + let store = |v: u32| { + let new = match v { + 1 => DARK_DARK, + 2 => DARK_LIGHT, + _ => DARK_UNKNOWN, + }; + if dark.swap(new, Ordering::Relaxed) != new { + wake(); + } }; let Ok(conn) = zbus::blocking::connection::Builder::session().and_then(|b| b.build()) else { log::info!("colour scheme: no session bus: the system theme isn't followed"); return; }; - let read = || -> zbus::Result { - let reply = conn.call_method(Some(PORTAL), "/org/freedesktop/portal/desktop", Some(SETTINGS), "ReadOne", &(APPEARANCE, "color-scheme"))?; + let read = |method: &str| -> zbus::Result { + let reply = conn.call_method(Some(PORTAL), "/org/freedesktop/portal/desktop", Some(SETTINGS), method, &(APPEARANCE, "color-scheme"))?; let body = reply.body(); - let value = body.deserialize::()?; - match value { - zbus::zvariant::Value::U32(v) => Ok(v), - _ => Ok(0), - } + Ok(scheme(&body.deserialize::()?)) }; - match read() { + match read("ReadOne").or_else(|_| read("Read")) { Ok(v) => store(v), Err(e) => log::info!("colour scheme: can't read the system setting: {e}"), } @@ -192,12 +207,9 @@ fn watch_system_dark(dark: Arc) { for msg in zbus::blocking::MessageIterator::for_match_rule(rule, &conn, Some(8))? { let msg = msg?; let body = msg.body(); - let Ok((namespace, key, value)) = body.deserialize::<(String, String, zbus::zvariant::Value<'_>)>() else { continue }; - if namespace == APPEARANCE - && key == "color-scheme" - && let zbus::zvariant::Value::U32(v) = value - { - store(v); + let Ok((namespace, key, value)) = body.deserialize::<(String, String, Value<'_>)>() else { continue }; + if namespace == APPEARANCE && key == "color-scheme" { + store(scheme(&value)); } } Ok(())