Skip to main content

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(xdg_portal)]
7pub mod portal;
8pub mod style;
9
10use ::iced::Alignment;
11use cosmic_config::{CosmicConfigEntry, config_subscription};
12use cosmic_theme::{Component, LayeredTheme, Spacing, ThemeMode};
13use iced_futures::Subscription;
14use iced_runtime::{Appearance, DefaultStyle};
15use std::sync::{Arc, LazyLock, Mutex};
16pub use style::*;
17
18pub type CosmicColor = ::palette::rgb::Srgba;
19pub type CosmicComponent = cosmic_theme::Component;
20pub type CosmicTheme = cosmic_theme::Theme;
21
22pub static COSMIC_DARK: LazyLock<CosmicTheme> = LazyLock::new(CosmicTheme::dark_default);
23
24pub static COSMIC_HC_DARK: LazyLock<CosmicTheme> =
25    LazyLock::new(CosmicTheme::high_contrast_dark_default);
26
27pub static COSMIC_LIGHT: LazyLock<CosmicTheme> = LazyLock::new(CosmicTheme::light_default);
28
29pub static COSMIC_HC_LIGHT: LazyLock<CosmicTheme> =
30    LazyLock::new(CosmicTheme::high_contrast_light_default);
31
32pub static TRANSPARENT_COMPONENT: LazyLock<Component> = LazyLock::new(|| Component {
33    base: CosmicColor::new(0.0, 0.0, 0.0, 0.0),
34    hover: CosmicColor::new(0.0, 0.0, 0.0, 0.0),
35    pressed: CosmicColor::new(0.0, 0.0, 0.0, 0.0),
36    selected: CosmicColor::new(0.0, 0.0, 0.0, 0.0),
37    selected_text: CosmicColor::new(0.0, 0.0, 0.0, 0.0),
38    focus: CosmicColor::new(0.0, 0.0, 0.0, 0.0),
39    disabled: CosmicColor::new(0.0, 0.0, 0.0, 0.0),
40    on: CosmicColor::new(0.0, 0.0, 0.0, 0.0),
41    on_disabled: CosmicColor::new(0.0, 0.0, 0.0, 0.0),
42    divider: CosmicColor::new(0.0, 0.0, 0.0, 0.0),
43    border: CosmicColor::new(0.0, 0.0, 0.0, 0.0),
44    disabled_border: CosmicColor::new(0.0, 0.0, 0.0, 0.0),
45});
46
47pub(crate) static THEME: Mutex<Theme> = Mutex::new(Theme {
48    theme_type: ThemeType::Dark,
49    layer: cosmic_theme::Layer::Background,
50    transparent: false,
51    list_item_position: None,
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
88pub fn system_dark() -> Theme {
89    let Ok(helper) = crate::cosmic_theme::Theme::dark_config() else {
90        return Theme::dark();
91    };
92
93    let t = crate::cosmic_theme::Theme::get_entry(&helper).unwrap_or_else(|(errors, theme)| {
94        for error in errors.into_iter().filter(cosmic_config::Error::is_err) {
95            tracing::error!(?error, "error loading system dark theme");
96        }
97        theme
98    });
99
100    Theme::system(Arc::new(t))
101}
102
103pub fn system_light() -> Theme {
104    let Ok(helper) = crate::cosmic_theme::Theme::light_config() else {
105        return Theme::light();
106    };
107
108    let t = crate::cosmic_theme::Theme::get_entry(&helper).unwrap_or_else(|(errors, theme)| {
109        for error in errors.into_iter().filter(cosmic_config::Error::is_err) {
110            tracing::error!(?error, "error loading system light theme");
111        }
112        theme
113    });
114
115    Theme::system(Arc::new(t))
116}
117
118/// Loads the preferred system theme from `cosmic-config`.
119pub fn system_preference() -> Theme {
120    let Ok(mode_config) = ThemeMode::config() else {
121        return Theme::dark();
122    };
123
124    let Ok(is_dark) = ThemeMode::is_dark(&mode_config) else {
125        return Theme::dark();
126    };
127    if is_dark {
128        system_dark()
129    } else {
130        system_light()
131    }
132}
133
134#[must_use]
135#[derive(Debug, Clone, PartialEq, Default)]
136pub enum ThemeType {
137    #[default]
138    Dark,
139    Light,
140    HighContrastDark,
141    HighContrastLight,
142    Custom(Arc<CosmicTheme>),
143    System {
144        prefer_dark: Option<bool>,
145        theme: Arc<CosmicTheme>,
146    },
147}
148
149impl ThemeType {
150    /// Whether the theme has a dark preference.
151    #[must_use]
152    #[inline]
153    pub fn is_dark(&self) -> bool {
154        match self {
155            Self::Dark | Self::HighContrastDark => true,
156            Self::Light | Self::HighContrastLight => false,
157            Self::Custom(theme) | Self::System { theme, .. } => theme.is_dark,
158        }
159    }
160
161    /// Whether the theme has a high contrast.
162    #[inline]
163    #[must_use]
164    pub fn is_high_contrast(&self) -> bool {
165        match self {
166            Self::Dark | Self::Light => false,
167            Self::HighContrastDark | Self::HighContrastLight => true,
168            Self::Custom(theme) | Self::System { theme, .. } => theme.is_high_contrast,
169        }
170    }
171
172    #[inline]
173    /// Prefer dark or light theme.
174    /// If `None`, the system preference is used.
175    pub fn prefer_dark(&mut self, new_prefer_dark: Option<bool>) {
176        if let Self::System { prefer_dark, .. } = self {
177            *prefer_dark = new_prefer_dark;
178        }
179    }
180}
181
182#[must_use]
183#[derive(Debug, Clone, PartialEq, Default)]
184pub struct Theme {
185    pub theme_type: ThemeType,
186    pub layer: cosmic_theme::Layer,
187    pub transparent: bool,
188    /// Only meaningful for widgets that must be in a list. Otherwise it should be ignored.
189    pub list_item_position: Option<(Alignment, usize)>,
190}
191
192impl Theme {
193    #[inline]
194    pub fn cosmic(&self) -> &cosmic_theme::Theme {
195        match self.theme_type {
196            ThemeType::Dark => &COSMIC_DARK,
197            ThemeType::Light => &COSMIC_LIGHT,
198            ThemeType::HighContrastDark => &COSMIC_HC_DARK,
199            ThemeType::HighContrastLight => &COSMIC_HC_LIGHT,
200            ThemeType::Custom(ref t) | ThemeType::System { theme: ref t, .. } => t.as_ref(),
201        }
202    }
203
204    #[inline]
205    pub fn dark() -> Self {
206        Self {
207            theme_type: ThemeType::Dark,
208            ..Default::default()
209        }
210    }
211
212    #[inline]
213    pub fn light() -> Self {
214        Self {
215            theme_type: ThemeType::Light,
216            ..Default::default()
217        }
218    }
219
220    #[inline]
221    pub fn dark_hc() -> Self {
222        Self {
223            theme_type: ThemeType::HighContrastDark,
224            ..Default::default()
225        }
226    }
227
228    #[inline]
229    pub fn light_hc() -> Self {
230        Self {
231            theme_type: ThemeType::HighContrastLight,
232            ..Default::default()
233        }
234    }
235
236    #[inline]
237    pub fn custom(theme: Arc<CosmicTheme>) -> Self {
238        Self {
239            theme_type: ThemeType::Custom(theme),
240            ..Default::default()
241        }
242    }
243
244    #[inline]
245    pub fn system(theme: Arc<CosmicTheme>) -> Self {
246        Self {
247            theme_type: ThemeType::System {
248                theme,
249                prefer_dark: None,
250            },
251            ..Default::default()
252        }
253    }
254
255    #[inline]
256    /// get current container
257    /// can be used in a component that is intended to be a child of a `CosmicContainer`
258    pub fn current_container(&self) -> &cosmic_theme::Container {
259        match self.layer {
260            cosmic_theme::Layer::Background => self.cosmic().background(self.transparent),
261            cosmic_theme::Layer::Primary => self.cosmic().primary(self.transparent),
262            cosmic_theme::Layer::Secondary => self.cosmic().secondary(self.transparent),
263        }
264    }
265
266    #[inline]
267    /// set the theme
268    pub fn set_theme(&mut self, theme: ThemeType) {
269        self.theme_type = theme;
270    }
271
272    #[inline]
273    /// Clone theme with a list position
274    pub fn with_list_item_position(&self, position: Option<(Alignment, usize)>) -> Self {
275        let mut new = self.clone();
276        new.list_item_position = position;
277        new
278    }
279}
280
281impl LayeredTheme for Theme {
282    #[inline]
283    fn set_layer(&mut self, layer: cosmic_theme::Layer) {
284        self.layer = layer;
285    }
286}
287
288impl DefaultStyle for Theme {
289    fn default_style(&self) -> Appearance {
290        let cosmic = self.cosmic();
291        Appearance {
292            icon_color: cosmic.on_bg_color().into(),
293            background_color: cosmic.bg_color().into(),
294            text_color: cosmic.on_bg_color().into(),
295        }
296    }
297}