cosmic/theme/
mod.rs

1// Copyright 2022 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4//! Contains the [`Theme`] type and its widget stylesheet implementations.
5
6#[cfg(feature = "xdg-portal")]
7pub mod portal;
8pub mod style;
9
10use cosmic_config::CosmicConfigEntry;
11use cosmic_config::config_subscription;
12use cosmic_theme::Component;
13use cosmic_theme::LayeredTheme;
14use cosmic_theme::Spacing;
15use cosmic_theme::ThemeMode;
16use iced_futures::Subscription;
17use iced_runtime::{Appearance, DefaultStyle};
18use std::sync::{Arc, Mutex};
19pub use style::*;
20
21#[cfg(feature = "dbus-config")]
22use cosmic_config::dbus;
23
24pub type CosmicColor = ::palette::rgb::Srgba;
25pub type CosmicComponent = cosmic_theme::Component;
26pub type CosmicTheme = cosmic_theme::Theme;
27
28lazy_static::lazy_static! {
29    pub static ref COSMIC_DARK: CosmicTheme = CosmicTheme::dark_default();
30    pub static ref COSMIC_HC_DARK: CosmicTheme = CosmicTheme::high_contrast_dark_default();
31    pub static ref COSMIC_LIGHT: CosmicTheme = CosmicTheme::light_default();
32    pub static ref COSMIC_HC_LIGHT: CosmicTheme = CosmicTheme::high_contrast_light_default();
33    pub static ref TRANSPARENT_COMPONENT: Component = Component {
34        base: CosmicColor::new(0.0, 0.0, 0.0, 0.0),
35        hover: CosmicColor::new(0.0, 0.0, 0.0, 0.0),
36        pressed: CosmicColor::new(0.0, 0.0, 0.0, 0.0),
37        selected: CosmicColor::new(0.0, 0.0, 0.0, 0.0),
38        selected_text: CosmicColor::new(0.0, 0.0, 0.0, 0.0),
39        focus: CosmicColor::new(0.0, 0.0, 0.0, 0.0),
40        disabled: CosmicColor::new(0.0, 0.0, 0.0, 0.0),
41        on: CosmicColor::new(0.0, 0.0, 0.0, 0.0),
42        on_disabled: CosmicColor::new(0.0, 0.0, 0.0, 0.0),
43        divider: CosmicColor::new(0.0, 0.0, 0.0, 0.0),
44        border: CosmicColor::new(0.0, 0.0, 0.0, 0.0),
45        disabled_border: CosmicColor::new(0.0, 0.0, 0.0, 0.0),
46    };
47}
48
49pub(crate) static THEME: Mutex<Theme> = Mutex::new(Theme {
50    theme_type: ThemeType::Dark,
51    layer: cosmic_theme::Layer::Background,
52});
53
54/// Currently-defined theme.
55#[inline]
56#[allow(clippy::missing_panics_doc)]
57pub fn active() -> Theme {
58    THEME.lock().unwrap().clone()
59}
60
61/// Currently-defined theme type.
62#[inline]
63#[allow(clippy::missing_panics_doc)]
64pub fn active_type() -> ThemeType {
65    THEME.lock().unwrap().theme_type.clone()
66}
67
68/// Preferred interface spacing parameters defined by the active theme.
69#[inline]
70pub fn spacing() -> Spacing {
71    active().cosmic().spacing
72}
73
74/// Whether the active theme has a dark preference.
75#[inline]
76#[must_use]
77pub fn is_dark() -> bool {
78    active_type().is_dark()
79}
80
81/// Whether the active theme is high contrast.
82#[inline]
83#[must_use]
84pub fn is_high_contrast() -> bool {
85    active_type().is_high_contrast()
86}
87
88/// Watches for changes to the system's theme preference.
89#[cold]
90pub fn subscription(is_dark: bool) -> Subscription<crate::theme::Theme> {
91    config_subscription::<_, crate::cosmic_theme::Theme>(
92        (
93            std::any::TypeId::of::<crate::cosmic_theme::Theme>(),
94            is_dark,
95        ),
96        if is_dark {
97            cosmic_theme::DARK_THEME_ID
98        } else {
99            cosmic_theme::LIGHT_THEME_ID
100        }
101        .into(),
102        crate::cosmic_theme::Theme::VERSION,
103    )
104    .map(|res| {
105        for error in res.errors.into_iter().filter(cosmic_config::Error::is_err) {
106            tracing::error!(
107                ?error,
108                "error while watching system theme preference changes"
109            );
110        }
111
112        Theme::system(Arc::new(res.config))
113    })
114}
115
116pub fn system_dark() -> Theme {
117    let Ok(helper) = crate::cosmic_theme::Theme::dark_config() else {
118        return Theme::dark();
119    };
120
121    let t = crate::cosmic_theme::Theme::get_entry(&helper).unwrap_or_else(|(errors, theme)| {
122        for error in errors.into_iter().filter(cosmic_config::Error::is_err) {
123            tracing::error!(?error, "error loading system dark theme");
124        }
125        theme
126    });
127
128    Theme::system(Arc::new(t))
129}
130
131pub fn system_light() -> Theme {
132    let Ok(helper) = crate::cosmic_theme::Theme::light_config() else {
133        return Theme::light();
134    };
135
136    let t = crate::cosmic_theme::Theme::get_entry(&helper).unwrap_or_else(|(errors, theme)| {
137        for error in errors.into_iter().filter(cosmic_config::Error::is_err) {
138            tracing::error!(?error, "error loading system light theme");
139        }
140        theme
141    });
142
143    Theme::system(Arc::new(t))
144}
145
146/// Loads the preferred system theme from `cosmic-config`.
147pub fn system_preference() -> Theme {
148    let Ok(mode_config) = ThemeMode::config() else {
149        return Theme::dark();
150    };
151
152    let Ok(is_dark) = ThemeMode::is_dark(&mode_config) else {
153        return Theme::dark();
154    };
155    if is_dark {
156        system_dark()
157    } else {
158        system_light()
159    }
160}
161
162#[must_use]
163#[derive(Debug, Clone, PartialEq, Default)]
164pub enum ThemeType {
165    #[default]
166    Dark,
167    Light,
168    HighContrastDark,
169    HighContrastLight,
170    Custom(Arc<CosmicTheme>),
171    System {
172        prefer_dark: Option<bool>,
173        theme: Arc<CosmicTheme>,
174    },
175}
176
177impl ThemeType {
178    /// Whether the theme has a dark preference.
179    #[must_use]
180    #[inline]
181    pub fn is_dark(&self) -> bool {
182        match self {
183            Self::Dark | Self::HighContrastDark => true,
184            Self::Light | Self::HighContrastLight => false,
185            Self::Custom(theme) | Self::System { theme, .. } => theme.is_dark,
186        }
187    }
188
189    /// Whether the theme has a high contrast.
190    #[inline]
191    #[must_use]
192    pub fn is_high_contrast(&self) -> bool {
193        match self {
194            Self::Dark | Self::Light => false,
195            Self::HighContrastDark | Self::HighContrastLight => true,
196            Self::Custom(theme) | Self::System { theme, .. } => theme.is_high_contrast,
197        }
198    }
199
200    #[inline]
201    /// Prefer dark or light theme.
202    /// If `None`, the system preference is used.
203    pub fn prefer_dark(&mut self, new_prefer_dark: Option<bool>) {
204        if let Self::System { prefer_dark, .. } = self {
205            *prefer_dark = new_prefer_dark;
206        }
207    }
208}
209
210#[must_use]
211#[derive(Debug, Clone, PartialEq, Default)]
212pub struct Theme {
213    pub theme_type: ThemeType,
214    pub layer: cosmic_theme::Layer,
215}
216
217impl Theme {
218    #[inline]
219    pub fn cosmic(&self) -> &cosmic_theme::Theme {
220        match self.theme_type {
221            ThemeType::Dark => &COSMIC_DARK,
222            ThemeType::Light => &COSMIC_LIGHT,
223            ThemeType::HighContrastDark => &COSMIC_HC_DARK,
224            ThemeType::HighContrastLight => &COSMIC_HC_LIGHT,
225            ThemeType::Custom(ref t) | ThemeType::System { theme: ref t, .. } => t.as_ref(),
226        }
227    }
228
229    #[inline]
230    pub fn dark() -> Self {
231        Self {
232            theme_type: ThemeType::Dark,
233            ..Default::default()
234        }
235    }
236
237    #[inline]
238    pub fn light() -> Self {
239        Self {
240            theme_type: ThemeType::Light,
241            ..Default::default()
242        }
243    }
244
245    #[inline]
246    pub fn dark_hc() -> Self {
247        Self {
248            theme_type: ThemeType::HighContrastDark,
249            ..Default::default()
250        }
251    }
252
253    #[inline]
254    pub fn light_hc() -> Self {
255        Self {
256            theme_type: ThemeType::HighContrastLight,
257            ..Default::default()
258        }
259    }
260
261    #[inline]
262    pub fn custom(theme: Arc<CosmicTheme>) -> Self {
263        Self {
264            theme_type: ThemeType::Custom(theme),
265            ..Default::default()
266        }
267    }
268
269    #[inline]
270    pub fn system(theme: Arc<CosmicTheme>) -> Self {
271        Self {
272            theme_type: ThemeType::System {
273                theme,
274                prefer_dark: None,
275            },
276            ..Default::default()
277        }
278    }
279
280    #[inline]
281    /// get current container
282    /// can be used in a component that is intended to be a child of a `CosmicContainer`
283    pub fn current_container(&self) -> &cosmic_theme::Container {
284        match self.layer {
285            cosmic_theme::Layer::Background => &self.cosmic().background,
286            cosmic_theme::Layer::Primary => &self.cosmic().primary,
287            cosmic_theme::Layer::Secondary => &self.cosmic().secondary,
288        }
289    }
290
291    #[inline]
292    /// set the theme
293    pub fn set_theme(&mut self, theme: ThemeType) {
294        self.theme_type = theme;
295    }
296}
297
298impl LayeredTheme for Theme {
299    #[inline]
300    fn set_layer(&mut self, layer: cosmic_theme::Layer) {
301        self.layer = layer;
302    }
303}
304
305impl DefaultStyle for Theme {
306    fn default_style(&self) -> Appearance {
307        let cosmic = self.cosmic();
308        Appearance {
309            icon_color: cosmic.bg_color().into(),
310            background_color: cosmic.bg_color().into(),
311            text_color: cosmic.on_bg_color().into(),
312        }
313    }
314}