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