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