diff --git a/Cargo.toml b/Cargo.toml index d5a1477..6bf3390 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,7 +57,7 @@ xdg-portal = ["ashpd"] [dependencies] apply = "0.3.0" -ashpd = { version = "0.6.8", default-features = false, optional = true } +ashpd = { version = "0.7.0", default-features = false, optional = true } async-fs = { version = "2.1", optional = true } cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "e65fa5e", optional = true } cosmic-config = { path = "cosmic-config" } diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 5965a7c..d7dccd6 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -5,7 +5,7 @@ use crate::{ DARK_PALETTE, LIGHT_PALETTE, NAME, }; use cosmic_config::{Config, CosmicConfigEntry}; -use palette::{IntoColor, Srgb, Srgba}; +use palette::{IntoColor, Oklcha, Srgb, Srgba}; use serde::{Deserialize, Serialize}; use std::num::NonZeroUsize; @@ -495,6 +495,31 @@ impl Theme { .map_err(|e| (vec![e], Self::default()))?; Self::get_entry(&config) } + + #[must_use] + /// Rebuild the current theme with the provided accent + pub fn with_accent(&self, c: Srgba) -> Self { + let mut oklcha: Oklcha = c.into_color(); + let cur_oklcha: Oklcha = self.accent_color().into_color(); + oklcha.l = cur_oklcha.l; + let adjusted_c: Srgb = oklcha.into_color(); + + let is_dark = self.is_dark; + + let mut builder = if is_dark { + ThemeBuilder::dark_config() + .ok() + .and_then(|h| ThemeBuilder::get_entry(&h).ok()) + .unwrap_or_else(ThemeBuilder::dark) + } else { + ThemeBuilder::light_config() + .ok() + .and_then(|h| ThemeBuilder::get_entry(&h).ok()) + .unwrap_or_else(ThemeBuilder::light) + }; + builder = builder.accent(adjusted_c); + builder.build() + } } impl From for Theme { diff --git a/examples/cosmic/Cargo.toml b/examples/cosmic/Cargo.toml index d89f652..194ce88 100644 --- a/examples/cosmic/Cargo.toml +++ b/examples/cosmic/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] apply = "0.3.0" fraction = "0.14.0" -libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "dbus-config", "a11y", "wgpu" ] } +libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "dbus-config", "a11y", "wgpu", "xdg-portal"] } once_cell = "1.18" slotmap = "1.0.6" env_logger = "0.10" diff --git a/src/app/core.rs b/src/app/core.rs index 38ca72c..072d53a 100644 --- a/src/app/core.rs +++ b/src/app/core.rs @@ -6,6 +6,7 @@ use std::collections::HashMap; use cosmic_config::CosmicConfigEntry; use cosmic_theme::ThemeMode; use iced_core::window::Id; +use palette::Srgba; use crate::Theme; @@ -59,9 +60,15 @@ pub struct Core { /// Last known system theme pub(super) system_theme: Theme, - /// Theme mode + /// Configured theme mode pub(super) system_theme_mode: ThemeMode, + pub(super) portal_is_dark: Option, + + pub(super) portal_accent: Option, + + pub(super) portal_is_high_contrast: Option, + pub(super) title: HashMap, pub window: Window, @@ -121,6 +128,9 @@ impl Default for Core { single_instance: false, #[cfg(feature = "dbus-config")] settings_daemon: None, + portal_is_dark: None, + portal_accent: None, + portal_is_high_contrast: None, } } } @@ -260,4 +270,11 @@ impl Core { T::VERSION, ) } + + /// Whether the application should use a dark theme, according to the system + #[must_use] + pub fn system_is_dark(&self) -> bool { + self.portal_is_dark + .unwrap_or(self.system_theme_mode.is_dark) + } } diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 1d6258e..88a06dc 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -26,6 +26,7 @@ use iced_futures::event::listen_with; use iced_runtime::command::Action; #[cfg(not(feature = "wayland"))] use iced_runtime::window::Action as WindowAction; +use palette::color_difference::EuclideanDistance; /// A message managed internally by COSMIC. #[derive(Clone, Debug)] @@ -72,6 +73,8 @@ pub enum Message { /// Activate the application Activate(String), ShowWindowMenu, + #[cfg(feature = "xdg-portal")] + DesktopSettings(crate::theme::portal::Desktop), } #[derive(Default)] @@ -210,6 +213,10 @@ where .single_instance .then(|| super::single_instance_subscription::()) .unwrap_or_else(Subscription::none), + #[cfg(feature = "xdg-portal")] + crate::theme::portal::desktop_settings() + .map(|e| Message::DesktopSettings(e)) + .map(super::Message::Cosmic), ]; if self.app.core().keyboard_nav { @@ -363,7 +370,15 @@ impl Cosmic { // Apply last-known system theme if the system theme is preferred. if let ThemeType::System(_) = theme.theme_type { self.app.core_mut().theme_sub_counter += 1; + theme = self.app.core().system_theme.clone(); + let portal_accent = self.app.core().portal_accent; + if let Some(a) = portal_accent { + let t_inner = theme.cosmic(); + if a.distance_squared(*t_inner.accent_color()) > 0.00001 { + theme = Theme::system(Arc::new(t_inner.with_accent(a))); + } + }; } THEME.with(move |t| { @@ -375,12 +390,23 @@ impl Cosmic { Message::SystemThemeChange(theme) => { // Record the last-known system theme in event that the current theme is custom. self.app.core_mut().system_theme = theme.clone(); + let portal_accent = self.app.core().portal_accent; THEME.with(move |t| { let mut cosmic_theme = t.borrow_mut(); // Only apply update if the theme is set to load a system theme if let ThemeType::System(_) = cosmic_theme.theme_type { - cosmic_theme.set_theme(theme.theme_type); + let new_theme = if let Some(a) = portal_accent { + let t_inner = theme.cosmic(); + if a.distance_squared(*t_inner.accent_color()) > 0.00001 { + Theme::system(Arc::new(t_inner.with_accent(a))) + } else { + theme + } + } else { + theme + }; + cosmic_theme.set_theme(new_theme.theme_type); } }); } @@ -395,11 +421,29 @@ impl Cosmic { } Message::SystemThemeModeChange(mode) => { let core = self.app.core_mut(); - let changed = core.system_theme_mode.is_dark != mode.is_dark; + let prev_is_dark = core.system_is_dark(); core.system_theme_mode = mode; - core.theme_sub_counter += 1; + let is_dark = core.system_is_dark(); + let changed = prev_is_dark != is_dark; if changed { - let new_theme = crate::theme::system_preference(); + core.theme_sub_counter += 1; + let mut new_theme = if is_dark { + crate::theme::system_dark() + } else { + crate::theme::system_light() + }; + + new_theme = if let Some(a) = core.portal_accent { + let t_inner = new_theme.cosmic(); + if a.distance_squared(*t_inner.accent_color()) > 0.00001 { + Theme::system(Arc::new(t_inner.with_accent(a))) + } else { + new_theme + } + } else { + new_theme + }; + core.system_theme = new_theme.clone(); THEME.with(move |t| { let mut cosmic_theme = t.borrow_mut(); @@ -428,6 +472,64 @@ impl Cosmic { #[cfg(not(feature = "wayland"))] return window::show_window_menu(window::Id::MAIN); } + #[cfg(feature = "xdg-portal")] + Message::DesktopSettings(crate::theme::portal::Desktop::ColorScheme(s)) => { + use ashpd::desktop::settings::ColorScheme; + let is_dark = match s { + ColorScheme::NoPreference => None, + ColorScheme::PreferDark => Some(true), + ColorScheme::PreferLight => Some(false), + }; + let core = self.app.core_mut(); + let prev_is_dark = core.system_is_dark(); + core.portal_is_dark = is_dark; + let is_dark = core.system_is_dark(); + let changed = prev_is_dark != is_dark; + if changed { + core.theme_sub_counter += 1; + let new_theme = if is_dark { + crate::theme::system_dark() + } else { + crate::theme::system_light() + }; + core.system_theme = new_theme.clone(); + THEME.with(move |t| { + let mut cosmic_theme = t.borrow_mut(); + + // Only apply update if the theme is set to load a system theme + if let ThemeType::System(_) = cosmic_theme.theme_type { + cosmic_theme.set_theme(new_theme.theme_type); + } + }); + } + } + #[cfg(feature = "xdg-portal")] + Message::DesktopSettings(crate::theme::portal::Desktop::Accent(c)) => { + use palette::{IntoColor, Oklch, Oklcha, Srgb, Srgba}; + + let c = Srgba::new(c.red() as f32, c.green() as f32, c.blue() as f32, 1.0); + let core = self.app.core_mut(); + core.portal_accent = Some(c); + let cur_accent = core.system_theme.cosmic().accent_color(); + + if cur_accent.distance_squared(*c) < 0.00001 { + // skip calculations if we already have the same color + return iced::Command::none(); + } + + THEME.with(move |t| { + let mut cosmic_theme = t.borrow_mut(); + + // Only apply update if the theme is set to load a system theme + if let ThemeType::System(t) = cosmic_theme.theme_type.clone() { + cosmic_theme.set_theme(ThemeType::System(Arc::new(t.with_accent(c)))); + } + }); + } + #[cfg(feature = "xdg-portal")] + Message::DesktopSettings(crate::theme::portal::Desktop::Contrast(_)) => { + // TODO when high contrast is integrated in settings and all custom themes + } } iced::Command::none() diff --git a/src/theme/mod.rs b/src/theme/mod.rs index d75ecb1..1dae5b5 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -3,6 +3,8 @@ //! Contains the [`Theme`] type and its widget stylesheet implementations. +#[cfg(feature = "xdg-portal")] +pub mod portal; pub mod style; use cosmic_theme::ThemeMode; pub use style::*; @@ -94,23 +96,8 @@ pub fn subscription(is_dark: bool) -> Subscription { }) } -/// Loads the preferred system theme from `cosmic-config`. -pub fn system_preference() -> Theme { - let Ok(mode_config) = ThemeMode::config() else { - return Theme::dark(); - }; - - let Ok(is_dark) = ThemeMode::is_dark(&mode_config) else { - return Theme::dark(); - }; - - let helper = if is_dark { - crate::cosmic_theme::Theme::dark_config() - } else { - crate::cosmic_theme::Theme::light_config() - }; - - let Ok(helper) = helper else { +pub fn system_dark() -> Theme { + let Ok(helper) = crate::cosmic_theme::Theme::dark_config() else { return Theme::dark(); }; @@ -124,6 +111,37 @@ pub fn system_preference() -> Theme { Theme::system(Arc::new(t)) } +pub fn system_light() -> Theme { + let Ok(helper) = crate::cosmic_theme::Theme::dark_config() else { + return Theme::dark(); + }; + + let t = crate::cosmic_theme::Theme::get_entry(&helper).unwrap_or_else(|(errors, theme)| { + for err in errors { + tracing::error!("{:?}", err); + } + theme + }); + + Theme::system(Arc::new(t)) +} + +/// Loads the preferred system theme from `cosmic-config`. +pub fn system_preference() -> Theme { + let Ok(mode_config) = ThemeMode::config() else { + return Theme::dark(); + }; + + let Ok(is_dark) = ThemeMode::is_dark(&mode_config) else { + return Theme::dark(); + }; + if is_dark { + system_dark() + } else { + system_light() + } +} + #[must_use] #[derive(Debug, Clone, PartialEq, Default)] pub enum ThemeType { diff --git a/src/theme/portal.rs b/src/theme/portal.rs new file mode 100644 index 0000000..b37dfe4 --- /dev/null +++ b/src/theme/portal.rs @@ -0,0 +1,77 @@ +use ashpd::desktop::settings::{ColorScheme, Contrast}; +use ashpd::desktop::Color; +use iced::futures::{self, select, FutureExt, SinkExt, StreamExt}; +use iced_futures::subscription; + +#[derive(Debug, Clone)] +pub enum Desktop { + Accent(Color), + ColorScheme(ColorScheme), + Contrast(Contrast), +} + +pub fn desktop_settings() -> iced_futures::Subscription { + subscription::channel(std::any::TypeId::of::(), 10, |mut tx| { + async move { + let Ok(settings) = ashpd::desktop::settings::Settings::new().await else { + // wait forever + futures::future::pending::<()>().await; + unreachable!() + }; + let mut color_scheme_stream = settings.receive_color_scheme_changed().await.ok(); + let mut accent_stream = settings.receive_accent_color_changed().await.ok(); + let mut contrast_stream = settings.receive_contrast_changed().await.ok(); + + loop { + let next_color_scheme = async { + if let Some(s) = color_scheme_stream.as_mut() { + return s.next().await; + } + futures::future::pending().await + }; + let next_accent = async { + if let Some(s) = accent_stream.as_mut() { + // Item type is wrong in this version + // updating requires updating to zbus 4 + return if s.next().await.is_some() { + settings.accent_color().await.ok() + } else { + None + }; + } + futures::future::pending().await + }; + let next_contrast = async { + if let Some(s) = contrast_stream.as_mut() { + return s.next().await; + } + futures::future::pending().await + }; + + select! { + s = next_color_scheme.fuse() => { + if let Some(s) = s { + _ = tx.send(Desktop::ColorScheme(s)).await; + } else { + color_scheme_stream = None; + } + }, + a = next_accent.fuse() => { + if let Some(a) = a { + _ = tx.send(Desktop::Accent(a)).await; + } else { + accent_stream = None; + } + }, + c = next_contrast.fuse() => { + if let Some(c) = c { + _ = tx.send(Desktop::Contrast(c)).await; + } else { + contrast_stream = None; + } + } + }; + } + } + }) +}