cosmic_theme/model/
theme.rs

1use crate::{
2    composite::over,
3    steps::{color_index, get_small_widget_color, get_surface_color, get_text, steps},
4    Component, Container, CornerRadii, CosmicPalette, CosmicPaletteInner, Spacing, ThemeMode,
5    DARK_PALETTE, LIGHT_PALETTE, NAME,
6};
7use cosmic_config::{Config, CosmicConfigEntry};
8use palette::{
9    color_difference::Wcag21RelativeContrast, rgb::Rgb, IntoColor, Oklcha, Srgb, Srgba, WithAlpha,
10};
11use serde::{Deserialize, Serialize};
12use std::num::NonZeroUsize;
13
14/// ID for the current dark `ThemeBuilder` config
15pub const DARK_THEME_BUILDER_ID: &str = "com.system76.CosmicTheme.Dark.Builder";
16
17/// ID for the current dark Theme config
18pub const DARK_THEME_ID: &str = "com.system76.CosmicTheme.Dark";
19
20/// ID for the current light `ThemeBuilder`` config
21pub const LIGHT_THEME_BUILDER_ID: &str = "com.system76.CosmicTheme.Light.Builder";
22
23/// ID for the current light Theme config
24pub const LIGHT_THEME_ID: &str = "com.system76.CosmicTheme.Light";
25
26#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
27/// Theme layer type
28pub enum Layer {
29    /// Background layer
30    #[default]
31    Background,
32    /// Primary Layer
33    Primary,
34    /// Secondary Layer
35    Secondary,
36}
37
38#[must_use]
39/// Cosmic Theme data structure with all colors and its name
40#[derive(
41    Clone,
42    Debug,
43    Serialize,
44    Deserialize,
45    PartialEq,
46    cosmic_config::cosmic_config_derive::CosmicConfigEntry,
47)]
48#[version = 1]
49pub struct Theme {
50    /// name of the theme
51    pub name: String,
52    /// background element colors
53    pub background: Container,
54    /// primary element colors
55    pub primary: Container,
56    /// secondary element colors
57    pub secondary: Container,
58    /// accent element colors
59    pub accent: Component,
60    /// suggested element colors
61    pub success: Component,
62    /// destructive element colors
63    pub destructive: Component,
64    /// warning element colors
65    pub warning: Component,
66    /// accent button element colors
67    pub accent_button: Component,
68    /// suggested button element colors
69    pub success_button: Component,
70    /// destructive button element colors
71    pub destructive_button: Component,
72    /// warning button element colors
73    pub warning_button: Component,
74    /// icon button element colors
75    pub icon_button: Component,
76    /// link button element colors
77    pub link_button: Component,
78    /// text button element colors
79    pub text_button: Component,
80    /// button component styling
81    pub button: Component,
82    /// palette
83    pub palette: CosmicPaletteInner,
84    /// spacing
85    pub spacing: Spacing,
86    /// corner radii
87    pub corner_radii: CornerRadii,
88    /// is dark
89    pub is_dark: bool,
90    /// is high contrast
91    pub is_high_contrast: bool,
92    /// cosmic-comp window gaps size (outer, inner)
93    pub gaps: (u32, u32),
94    /// cosmic-comp active hint window outline width
95    pub active_hint: u32,
96    /// cosmic-comp custom window hint color
97    pub window_hint: Option<Srgb>,
98    /// enables blurred transparency
99    pub is_frosted: bool,
100    /// shade color for dialogs
101    pub shade: Srgba,
102    /// accent text colors
103    /// If None, accent base color is the accent text color.
104    pub accent_text: Option<Srgba>,
105    /// control tint color
106    pub control_tint: Option<Srgb>,
107    /// text tint color
108    pub text_tint: Option<Srgb>,
109}
110
111impl Default for Theme {
112    #[inline]
113    fn default() -> Self {
114        Self::preferred_theme()
115    }
116}
117
118/// Trait for layered themes
119pub trait LayeredTheme {
120    /// Set the layer of the theme
121    fn set_layer(&mut self, layer: Layer);
122}
123
124impl Theme {
125    #[must_use]
126    /// id of the theme
127    pub fn id() -> &'static str {
128        NAME
129    }
130
131    #[inline]
132    /// Get the config for the current dark theme
133    pub fn dark_config() -> Result<Config, cosmic_config::Error> {
134        Config::new(DARK_THEME_ID, Self::VERSION)
135    }
136
137    #[inline]
138    /// Get the config for the current light theme
139    pub fn light_config() -> Result<Config, cosmic_config::Error> {
140        Config::new(LIGHT_THEME_ID, Self::VERSION)
141    }
142
143    #[inline]
144    /// get the built in light theme
145    pub fn light_default() -> Self {
146        LIGHT_PALETTE.clone().into()
147    }
148
149    #[inline]
150    /// get the built in dark theme
151    pub fn dark_default() -> Self {
152        DARK_PALETTE.clone().into()
153    }
154
155    #[inline]
156    /// get the built in high contrast dark theme
157    pub fn high_contrast_dark_default() -> Self {
158        CosmicPalette::HighContrastDark(DARK_PALETTE.as_ref().clone()).into()
159    }
160
161    #[inline]
162    /// get the built in high contrast light theme
163    pub fn high_contrast_light_default() -> Self {
164        CosmicPalette::HighContrastLight(LIGHT_PALETTE.as_ref().clone()).into()
165    }
166
167    #[inline]
168    /// Convert the theme to a high-contrast variant
169    pub fn to_high_contrast(&self) -> Self {
170        todo!();
171    }
172
173    #[must_use]
174    #[allow(clippy::doc_markdown)]
175    #[inline]
176    /// get control_0 color
177    pub fn control_0(&self) -> Srgba {
178        self.tint_neutral(self.palette.neutral_0)
179    }
180
181    #[must_use]
182    #[allow(clippy::doc_markdown)]
183    #[inline]
184    /// get control_1 color
185    pub fn control_1(&self) -> Srgba {
186        self.tint_neutral(self.palette.neutral_1)
187    }
188
189    #[must_use]
190    #[allow(clippy::doc_markdown)]
191    #[inline]
192    /// get control_2 color
193    pub fn control_2(&self) -> Srgba {
194        self.tint_neutral(self.palette.neutral_2)
195    }
196
197    #[must_use]
198    #[allow(clippy::doc_markdown)]
199    #[inline]
200    /// get control_3 color
201    pub fn control_3(&self) -> Srgba {
202        self.tint_neutral(self.palette.neutral_3)
203    }
204
205    #[must_use]
206    #[allow(clippy::doc_markdown)]
207    #[inline]
208    /// get control_3 color
209    pub fn control_4(&self) -> Srgba {
210        self.tint_neutral(self.palette.neutral_4)
211    }
212
213    #[must_use]
214    #[allow(clippy::doc_markdown)]
215    #[inline]
216    /// get control_3 color
217    pub fn control_5(&self) -> Srgba {
218        self.tint_neutral(self.palette.neutral_5)
219    }
220
221    #[must_use]
222    #[allow(clippy::doc_markdown)]
223    #[inline]
224    /// get control_3 color
225    pub fn control_6(&self) -> Srgba {
226        self.tint_neutral(self.palette.neutral_6)
227    }
228
229    #[must_use]
230    #[allow(clippy::doc_markdown)]
231    #[inline]
232    /// get control_3 color
233    pub fn control_7(&self) -> Srgba {
234        self.tint_neutral(self.palette.neutral_7)
235    }
236
237    #[must_use]
238    #[allow(clippy::doc_markdown)]
239    #[inline]
240    /// get control_3 color
241    pub fn control_8(&self) -> Srgba {
242        self.tint_neutral(self.palette.neutral_8)
243    }
244
245    #[must_use]
246    #[allow(clippy::doc_markdown)]
247    #[inline]
248    /// get control_3 color
249    pub fn control_9(&self) -> Srgba {
250        self.tint_neutral(self.palette.neutral_9)
251    }
252
253    #[must_use]
254    #[allow(clippy::doc_markdown)]
255    #[inline]
256    /// get control_3 color
257    pub fn control_10(&self) -> Srgba {
258        self.tint_neutral(self.palette.neutral_10)
259    }
260
261    #[must_use]
262    #[allow(clippy::doc_markdown)]
263    #[inline]
264    /// get @accent_color
265    fn tint_neutral(&self, neutral: Srgba) -> Srgba {
266        let Some(tint) = self.control_tint else {
267            return neutral;
268        };
269        let mut oklch_neutral: Oklcha = neutral.into_color();
270        let oklch_tint: Oklcha = tint.into_color();
271        oklch_neutral.hue = oklch_tint.hue;
272        oklch_neutral.chroma = oklch_tint.chroma;
273        oklch_neutral.into_color()
274    }
275
276    // TODO convenient getter functions for each named color variable
277    #[must_use]
278    #[allow(clippy::doc_markdown)]
279    #[inline]
280    /// get @accent_color
281    pub fn accent_color(&self) -> Srgba {
282        self.accent.base
283    }
284
285    #[must_use]
286    #[allow(clippy::doc_markdown)]
287    #[inline]
288    /// get @success_color
289    pub fn success_color(&self) -> Srgba {
290        self.success.base
291    }
292
293    #[must_use]
294    #[allow(clippy::doc_markdown)]
295    #[inline]
296    /// get @destructive_color
297    pub fn destructive_color(&self) -> Srgba {
298        self.destructive.base
299    }
300
301    #[must_use]
302    #[allow(clippy::doc_markdown)]
303    #[inline]
304    /// get @warning_color
305    pub fn warning_color(&self) -> Srgba {
306        self.warning.base
307    }
308
309    #[must_use]
310    #[allow(clippy::doc_markdown)]
311    #[inline]
312    /// get @small_widget_divider
313    pub fn small_widget_divider(&self) -> Srgba {
314        self.palette.neutral_9.with_alpha(0.2)
315    }
316
317    // Containers
318    #[must_use]
319    #[allow(clippy::doc_markdown)]
320    #[inline]
321    /// get @bg_color
322    pub fn bg_color(&self) -> Srgba {
323        self.background.base
324    }
325
326    #[must_use]
327    #[allow(clippy::doc_markdown)]
328    #[inline]
329    /// get @bg_component_color
330    pub fn bg_component_color(&self) -> Srgba {
331        self.background.component.base
332    }
333
334    #[must_use]
335    #[allow(clippy::doc_markdown)]
336    #[inline]
337    /// get @primary_container_color
338    pub fn primary_container_color(&self) -> Srgba {
339        self.primary.base
340    }
341
342    #[must_use]
343    #[allow(clippy::doc_markdown)]
344    #[inline]
345    /// get @primary_component_color
346    pub fn primary_component_color(&self) -> Srgba {
347        self.primary.component.base
348    }
349
350    #[must_use]
351    #[allow(clippy::doc_markdown)]
352    #[inline]
353    /// get @secondary_container_color
354    pub fn secondary_container_color(&self) -> Srgba {
355        self.secondary.base
356    }
357
358    #[must_use]
359    #[allow(clippy::doc_markdown)]
360    #[inline]
361    /// get @secondary_component_color
362    pub fn secondary_component_color(&self) -> Srgba {
363        self.secondary.component.base
364    }
365
366    #[must_use]
367    #[allow(clippy::doc_markdown)]
368    #[inline]
369    /// get @button_bg_color
370    pub fn button_bg_color(&self) -> Srgba {
371        self.button.base
372    }
373
374    // Text
375    #[must_use]
376    #[allow(clippy::doc_markdown)]
377    #[inline]
378    /// get @on_bg_color
379    pub fn on_bg_color(&self) -> Srgba {
380        self.background.on
381    }
382
383    #[must_use]
384    #[allow(clippy::doc_markdown)]
385    #[inline]
386    /// get @on_bg_component_color
387    pub fn on_bg_component_color(&self) -> Srgba {
388        self.background.component.on
389    }
390
391    #[must_use]
392    #[allow(clippy::doc_markdown)]
393    #[inline]
394    /// get @on_primary_color
395    pub fn on_primary_container_color(&self) -> Srgba {
396        self.primary.on
397    }
398
399    #[must_use]
400    #[allow(clippy::doc_markdown)]
401    #[inline]
402    /// get @on_primary_component_color
403    pub fn on_primary_component_color(&self) -> Srgba {
404        self.primary.component.on
405    }
406
407    #[must_use]
408    #[allow(clippy::doc_markdown)]
409    #[inline]
410    /// get @on_secondary_color
411    pub fn on_secondary_container_color(&self) -> Srgba {
412        self.secondary.on
413    }
414
415    #[must_use]
416    #[allow(clippy::doc_markdown)]
417    #[inline]
418    /// get @on_secondary_component_color
419    pub fn on_secondary_component_color(&self) -> Srgba {
420        self.secondary.component.on
421    }
422
423    #[must_use]
424    #[allow(clippy::doc_markdown)]
425    #[inline]
426    /// get @accent_text_color
427    pub fn accent_text_color(&self) -> Srgba {
428        self.accent_text.unwrap_or(self.accent.base)
429    }
430
431    #[must_use]
432    #[allow(clippy::doc_markdown)]
433    #[inline]
434    /// get @success_text_color
435    pub fn success_text_color(&self) -> Srgba {
436        self.success.base
437    }
438
439    #[must_use]
440    #[allow(clippy::doc_markdown)]
441    #[inline]
442    /// get @warning_text_color
443    pub fn warning_text_color(&self) -> Srgba {
444        self.warning.base
445    }
446
447    #[must_use]
448    #[allow(clippy::doc_markdown)]
449    #[inline]
450    /// get @destructive_text_color
451    pub fn destructive_text_color(&self) -> Srgba {
452        self.destructive.base
453    }
454
455    #[must_use]
456    #[allow(clippy::doc_markdown)]
457    #[inline]
458    /// get @on_accent_color
459    pub fn on_accent_color(&self) -> Srgba {
460        self.accent.on
461    }
462
463    #[must_use]
464    #[allow(clippy::doc_markdown)]
465    #[inline]
466    /// get @on_success_color
467    pub fn on_success_color(&self) -> Srgba {
468        self.success.on
469    }
470
471    #[must_use]
472    #[allow(clippy::doc_markdown)]
473    #[inline]
474    /// get @on_warning_color
475    pub fn on_warning_color(&self) -> Srgba {
476        self.warning.on
477    }
478
479    #[must_use]
480    #[allow(clippy::doc_markdown)]
481    #[inline]
482    /// get @on_destructive_color
483    pub fn on_destructive_color(&self) -> Srgba {
484        self.destructive.on
485    }
486
487    #[must_use]
488    #[allow(clippy::doc_markdown)]
489    #[inline]
490    /// get @button_color
491    pub fn button_color(&self) -> Srgba {
492        self.button.on
493    }
494
495    // Borders and Dividers
496    #[must_use]
497    #[allow(clippy::doc_markdown)]
498    #[inline]
499    /// get @bg_divider
500    pub fn bg_divider(&self) -> Srgba {
501        self.background.divider
502    }
503
504    #[must_use]
505    #[allow(clippy::doc_markdown)]
506    #[inline]
507    /// get @bg_component_divider
508    pub fn bg_component_divider(&self) -> Srgba {
509        self.background.component.divider
510    }
511
512    #[must_use]
513    #[allow(clippy::doc_markdown)]
514    #[inline]
515    /// get @primary_container_divider
516    pub fn primary_container_divider(&self) -> Srgba {
517        self.primary.divider
518    }
519
520    #[must_use]
521    #[allow(clippy::doc_markdown)]
522    #[inline]
523    /// get @primary_component_divider
524    pub fn primary_component_divider(&self) -> Srgba {
525        self.primary.component.divider
526    }
527
528    #[must_use]
529    #[allow(clippy::doc_markdown)]
530    #[inline]
531    /// get @secondary_container_divider
532    pub fn secondary_container_divider(&self) -> Srgba {
533        self.secondary.divider
534    }
535
536    #[must_use]
537    #[allow(clippy::doc_markdown)]
538    #[inline]
539    /// get @button_divider
540    pub fn button_divider(&self) -> Srgba {
541        self.button.divider
542    }
543
544    #[must_use]
545    #[allow(clippy::doc_markdown)]
546    #[inline]
547    /// get @window_header_bg
548    pub fn window_header_bg(&self) -> Srgba {
549        self.background.base
550    }
551
552    #[must_use]
553    #[allow(clippy::doc_markdown)]
554    #[inline]
555    /// get @space_none
556    pub fn space_none(&self) -> u16 {
557        self.spacing.space_none
558    }
559
560    #[must_use]
561    #[allow(clippy::doc_markdown)]
562    #[inline]
563    /// get @space_xxxs
564    pub fn space_xxxs(&self) -> u16 {
565        self.spacing.space_xxxs
566    }
567
568    #[must_use]
569    #[allow(clippy::doc_markdown)]
570    #[inline]
571    /// get @space_xxs
572    pub fn space_xxs(&self) -> u16 {
573        self.spacing.space_xxs
574    }
575
576    #[must_use]
577    #[allow(clippy::doc_markdown)]
578    #[inline]
579    /// get @space_xs
580    pub fn space_xs(&self) -> u16 {
581        self.spacing.space_xs
582    }
583
584    #[must_use]
585    #[allow(clippy::doc_markdown)]
586    #[inline]
587    /// get @space_s
588    pub fn space_s(&self) -> u16 {
589        self.spacing.space_s
590    }
591
592    #[must_use]
593    #[allow(clippy::doc_markdown)]
594    #[inline]
595    /// get @space_m
596    pub fn space_m(&self) -> u16 {
597        self.spacing.space_m
598    }
599
600    #[must_use]
601    #[allow(clippy::doc_markdown)]
602    #[inline]
603    /// get @space_l
604    pub fn space_l(&self) -> u16 {
605        self.spacing.space_l
606    }
607
608    #[must_use]
609    #[allow(clippy::doc_markdown)]
610    #[inline]
611    /// get @space_xl
612    pub fn space_xl(&self) -> u16 {
613        self.spacing.space_xl
614    }
615
616    #[must_use]
617    #[allow(clippy::doc_markdown)]
618    #[inline]
619    /// get @space_xxl
620    pub fn space_xxl(&self) -> u16 {
621        self.spacing.space_xxl
622    }
623
624    #[must_use]
625    #[allow(clippy::doc_markdown)]
626    #[inline]
627    /// get @space_xxxl
628    pub fn space_xxxl(&self) -> u16 {
629        self.spacing.space_xxxl
630    }
631
632    #[must_use]
633    #[allow(clippy::doc_markdown)]
634    #[inline]
635    /// get @radius_0
636    pub fn radius_0(&self) -> [f32; 4] {
637        self.corner_radii.radius_0
638    }
639
640    #[must_use]
641    #[allow(clippy::doc_markdown)]
642    #[inline]
643    /// get @radius_xs
644    pub fn radius_xs(&self) -> [f32; 4] {
645        self.corner_radii.radius_xs
646    }
647
648    #[must_use]
649    #[allow(clippy::doc_markdown)]
650    #[inline]
651    /// get @radius_s
652    pub fn radius_s(&self) -> [f32; 4] {
653        self.corner_radii.radius_s
654    }
655
656    #[must_use]
657    #[allow(clippy::doc_markdown)]
658    #[inline]
659    /// get @radius_m
660    pub fn radius_m(&self) -> [f32; 4] {
661        self.corner_radii.radius_m
662    }
663
664    #[must_use]
665    #[allow(clippy::doc_markdown)]
666    #[inline]
667    /// get @radius_l
668    pub fn radius_l(&self) -> [f32; 4] {
669        self.corner_radii.radius_l
670    }
671
672    #[must_use]
673    #[allow(clippy::doc_markdown)]
674    #[inline]
675    /// get @radius_xl
676    pub fn radius_xl(&self) -> [f32; 4] {
677        self.corner_radii.radius_xl
678    }
679
680    #[must_use]
681    #[allow(clippy::doc_markdown)]
682    #[inline]
683    /// get @shade_color
684    pub fn shade_color(&self) -> Srgba {
685        self.shade
686    }
687
688    /// get the active theme
689    pub fn get_active() -> Result<Self, (Vec<cosmic_config::Error>, Self)> {
690        let config =
691            Config::new(Self::id(), Self::VERSION).map_err(|e| (vec![e], Self::default()))?;
692        let is_dark = ThemeMode::is_dark(&config).map_err(|e| (vec![e], Self::default()))?;
693        let config = if is_dark {
694            Self::dark_config()
695        } else {
696            Self::light_config()
697        }
698        .map_err(|e| (vec![e], Self::default()))?;
699        Self::get_entry(&config)
700    }
701
702    #[must_use]
703    /// Rebuild the current theme with the provided accent
704    pub fn with_accent(&self, c: Srgba) -> Self {
705        let mut oklcha: Oklcha = c.into_color();
706        let cur_oklcha: Oklcha = self.accent_color().into_color();
707        oklcha.l = cur_oklcha.l;
708        let adjusted_c: Srgb = oklcha.into_color();
709
710        let is_dark = self.is_dark;
711
712        let mut builder = if is_dark {
713            ThemeBuilder::dark_config()
714                .ok()
715                .and_then(|h| ThemeBuilder::get_entry(&h).ok())
716                .unwrap_or_else(ThemeBuilder::dark)
717        } else {
718            ThemeBuilder::light_config()
719                .ok()
720                .and_then(|h| ThemeBuilder::get_entry(&h).ok())
721                .unwrap_or_else(ThemeBuilder::light)
722        };
723        builder = builder.accent(adjusted_c);
724        builder.build()
725    }
726
727    /// choose default color palette based on preferred GTK color scheme
728    pub fn gtk_prefer_colorscheme() -> Self {
729        let gsettings = "/usr/bin/gsettings";
730
731        let cmd = std::process::Command::new(gsettings)
732            .arg("get")
733            .arg("org.gnome.desktop.interface")
734            .arg("color-scheme")
735            .output();
736
737        if let Ok(cmd) = cmd {
738            let color_scheme = String::from_utf8_lossy(&cmd.stdout);
739
740            if color_scheme.trim().contains("default") || color_scheme.trim().contains("light") {
741                return Self::light_default();
742            }
743        };
744
745        Self::dark_default()
746    }
747
748    /// check current desktop environment and preferred color scheme and set it as default
749    pub fn preferred_theme() -> Self {
750        let current_desktop = std::env::var("XDG_CURRENT_DESKTOP");
751
752        if let Ok(desktop) = current_desktop {
753            if desktop.trim().to_lowercase().contains("gnome") {
754                return Self::gtk_prefer_colorscheme();
755            }
756        }
757
758        Self::dark_default()
759    }
760}
761
762impl From<CosmicPalette> for Theme {
763    fn from(p: CosmicPalette) -> Self {
764        ThemeBuilder::palette(p).build()
765    }
766}
767
768#[must_use]
769/// Helper for building customized themes
770#[derive(
771    Clone,
772    Debug,
773    Serialize,
774    Deserialize,
775    cosmic_config::cosmic_config_derive::CosmicConfigEntry,
776    PartialEq,
777)]
778#[version = 1]
779pub struct ThemeBuilder {
780    /// override the palette for the builder
781    pub palette: CosmicPalette,
782    /// override spacing for the builder
783    pub spacing: Spacing,
784    /// override corner radii for the builder
785    pub corner_radii: CornerRadii,
786    /// override neutral_tint for the builder
787    pub neutral_tint: Option<Srgb>,
788    /// override bg_color for the builder
789    pub bg_color: Option<Srgba>,
790    /// override the primary container bg color for the builder
791    pub primary_container_bg: Option<Srgba>,
792    /// override the secontary container bg color for the builder
793    pub secondary_container_bg: Option<Srgba>,
794    /// override the text tint for the builder
795    pub text_tint: Option<Srgb>,
796    /// override the accent color for the builder
797    pub accent: Option<Srgb>,
798    /// override the success color for the builder
799    pub success: Option<Srgb>,
800    /// override the warning color for the builder
801    pub warning: Option<Srgb>,
802    /// override the destructive color for the builder
803    pub destructive: Option<Srgb>,
804    /// enabled blurred transparency
805    pub is_frosted: bool, // TODO handle
806    /// cosmic-comp window gaps size (outer, inner)
807    pub gaps: (u32, u32),
808    /// cosmic-comp active hint window outline width
809    pub active_hint: u32,
810    /// cosmic-comp custom window hint color
811    pub window_hint: Option<Srgb>,
812}
813
814impl Default for ThemeBuilder {
815    fn default() -> Self {
816        Self {
817            palette: DARK_PALETTE.to_owned().into(),
818            spacing: Spacing::default(),
819            corner_radii: CornerRadii::default(),
820            neutral_tint: Default::default(),
821            text_tint: Default::default(),
822            bg_color: Default::default(),
823            primary_container_bg: Default::default(),
824            secondary_container_bg: Default::default(),
825            accent: Default::default(),
826            success: Default::default(),
827            warning: Default::default(),
828            destructive: Default::default(),
829            is_frosted: false,
830            // cosmic-comp theme settings
831            gaps: (0, 8),
832            active_hint: 3,
833            window_hint: None,
834        }
835    }
836}
837
838impl ThemeBuilder {
839    #[inline]
840    /// Get a builder that is initialized with the default dark theme
841    pub fn dark() -> Self {
842        Self {
843            palette: DARK_PALETTE.to_owned(),
844            ..Default::default()
845        }
846    }
847
848    #[inline]
849    /// Get a builder that is initialized with the default light theme
850    pub fn light() -> Self {
851        Self {
852            palette: LIGHT_PALETTE.to_owned(),
853            ..Default::default()
854        }
855    }
856
857    #[inline]
858    /// Get a builder that is initialized with the default dark high contrast theme
859    pub fn dark_high_contrast() -> Self {
860        let palette: CosmicPalette = DARK_PALETTE.to_owned();
861        Self {
862            palette: CosmicPalette::HighContrastDark(palette.inner()),
863            ..Default::default()
864        }
865    }
866
867    #[inline]
868    /// Get a builder that is initialized with the default light high contrast theme
869    pub fn light_high_contrast() -> Self {
870        let palette: CosmicPalette = LIGHT_PALETTE.to_owned();
871        Self {
872            palette: CosmicPalette::HighContrastLight(palette.inner()),
873            ..Default::default()
874        }
875    }
876
877    #[inline]
878    /// Get a builder that is initialized with the provided palette
879    pub fn palette(palette: CosmicPalette) -> Self {
880        Self {
881            palette,
882            ..Default::default()
883        }
884    }
885
886    #[inline]
887    /// set the spacing of the builder
888    pub fn spacing(mut self, spacing: Spacing) -> Self {
889        self.spacing = spacing;
890        self
891    }
892
893    #[inline]
894    /// set the corner radii of the builder
895    pub fn corner_radii(mut self, corner_radii: CornerRadii) -> Self {
896        self.corner_radii = corner_radii;
897        self
898    }
899
900    #[inline]
901    /// apply a neutral tint to the palette
902    pub fn neutral_tint(mut self, tint: Srgb) -> Self {
903        self.neutral_tint = Some(tint);
904        self
905    }
906
907    #[inline]
908    /// apply a text tint to the palette
909    pub fn text_tint(mut self, tint: Srgb) -> Self {
910        self.text_tint = Some(tint);
911        self
912    }
913
914    #[inline]
915    /// apply a background color to the palette
916    pub fn bg_color(mut self, c: Srgba) -> Self {
917        self.bg_color = Some(c);
918        self
919    }
920
921    #[inline]
922    /// apply a primary container background color to the palette
923    pub fn primary_container_bg(mut self, c: Srgba) -> Self {
924        self.primary_container_bg = Some(c);
925        self
926    }
927
928    #[inline]
929    /// apply a accent color to the palette
930    pub fn accent(mut self, c: Srgb) -> Self {
931        self.accent = Some(c);
932        self
933    }
934
935    #[inline]
936    /// apply a success color to the palette
937    pub fn success(mut self, c: Srgb) -> Self {
938        self.success = Some(c);
939        self
940    }
941
942    #[inline]
943    /// apply a warning color to the palette
944    pub fn warning(mut self, c: Srgb) -> Self {
945        self.warning = Some(c);
946        self
947    }
948
949    #[inline]
950    /// apply a destructive color to the palette
951    pub fn destructive(mut self, c: Srgb) -> Self {
952        self.destructive = Some(c);
953        self
954    }
955
956    #[allow(clippy::too_many_lines)]
957    /// build the theme
958    pub fn build(self) -> Theme {
959        let Self {
960            palette,
961            spacing,
962            corner_radii,
963            neutral_tint,
964            text_tint,
965            bg_color,
966            primary_container_bg,
967            secondary_container_bg,
968            accent,
969            success,
970            warning,
971            destructive,
972            gaps,
973            active_hint,
974            window_hint,
975            is_frosted,
976        } = self;
977
978        let is_dark = palette.is_dark();
979        let is_high_contrast = palette.is_high_contrast();
980
981        let accent = if let Some(accent) = accent {
982            accent.into_color()
983        } else {
984            palette.as_ref().accent_blue
985        };
986
987        let success = if let Some(success) = success {
988            success.into_color()
989        } else {
990            palette.as_ref().accent_green
991        };
992
993        let warning = if let Some(warning) = warning {
994            warning.into_color()
995        } else {
996            palette.as_ref().accent_yellow
997        };
998
999        let destructive = if let Some(destructive) = destructive {
1000            destructive.into_color()
1001        } else {
1002            palette.as_ref().accent_red
1003        };
1004
1005        let text_steps_array = text_tint.map(|c| steps(c, NonZeroUsize::new(100).unwrap()));
1006
1007        let mut control_steps_array = if let Some(neutral_tint) = neutral_tint {
1008            steps(neutral_tint, NonZeroUsize::new(11).unwrap())
1009        } else {
1010            steps(palette.as_ref().neutral_2, NonZeroUsize::new(11).unwrap())
1011        };
1012        if !is_dark {
1013            control_steps_array.reverse();
1014        }
1015
1016        let p_ref = palette.as_ref();
1017
1018        let neutral_steps = steps(
1019            neutral_tint.unwrap_or(Rgb::new(0.0, 0.0, 0.0)),
1020            NonZeroUsize::new(100).unwrap(),
1021        );
1022
1023        let bg = if let Some(bg_color) = bg_color {
1024            bg_color
1025        } else {
1026            p_ref.gray_1
1027        };
1028
1029        let step_array = steps(bg, NonZeroUsize::new(100).unwrap());
1030        let bg_index = color_index(bg, step_array.len());
1031
1032        let mut component_hovered_overlay = if bg_index < 91 {
1033            control_steps_array[10]
1034        } else {
1035            control_steps_array[0]
1036        };
1037        component_hovered_overlay.alpha = 0.1;
1038
1039        let mut component_pressed_overlay = component_hovered_overlay;
1040        component_pressed_overlay.alpha = 0.2;
1041
1042        // Standard button background is neutral 7 with 25% opacity
1043        let button_bg = control_steps_array[7].with_alpha(0.25);
1044
1045        let (button_hovered_overlay, button_pressed_overlay) = (
1046            control_steps_array[5].with_alpha(0.2),
1047            control_steps_array[2].with_alpha(0.5),
1048        );
1049
1050        let bg_component = get_surface_color(bg_index, 8, &step_array, is_dark, &p_ref.neutral_2);
1051        let on_bg_component = get_text(
1052            color_index(bg_component, step_array.len()),
1053            &step_array,
1054            &control_steps_array[8],
1055            text_steps_array.as_deref(),
1056        );
1057
1058        let primary = {
1059            let container_bg = if let Some(primary_container_bg_color) = primary_container_bg {
1060                primary_container_bg_color
1061            } else {
1062                get_surface_color(bg_index, 5, &step_array, is_dark, &control_steps_array[1])
1063            };
1064
1065            let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap());
1066            let base_index: usize = color_index(container_bg, step_array.len());
1067            let component_base =
1068                get_surface_color(base_index, 6, &step_array, is_dark, &control_steps_array[3]);
1069
1070            component_hovered_overlay = if base_index < 91 {
1071                control_steps_array[10]
1072            } else {
1073                control_steps_array[0]
1074            };
1075            component_hovered_overlay.alpha = 0.1;
1076
1077            component_pressed_overlay = component_hovered_overlay;
1078            component_pressed_overlay.alpha = 0.2;
1079
1080            let container = Container::new(
1081                Component::component(
1082                    component_base,
1083                    accent,
1084                    get_text(
1085                        color_index(component_base, step_array.len()),
1086                        &step_array,
1087                        &control_steps_array[8],
1088                        text_steps_array.as_deref(),
1089                    ),
1090                    component_hovered_overlay,
1091                    component_pressed_overlay,
1092                    is_high_contrast,
1093                    control_steps_array[8],
1094                ),
1095                container_bg,
1096                get_text(
1097                    base_index,
1098                    &step_array,
1099                    &control_steps_array[8],
1100                    text_steps_array.as_deref(),
1101                ),
1102                get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]),
1103                is_high_contrast,
1104            );
1105
1106            container
1107        };
1108
1109        let accent_text = if is_dark {
1110            (primary.base.relative_contrast(accent.color) < 4.).then(|| {
1111                let step_array = steps(accent, NonZeroUsize::new(100).unwrap());
1112                let primary_color_index = color_index(primary.base, 100);
1113                let steps = if is_high_contrast { 60 } else { 50 };
1114                let accent_text = get_surface_color(
1115                    primary_color_index,
1116                    steps,
1117                    &step_array,
1118                    is_dark,
1119                    &Srgba::new(1., 1., 1., 1.),
1120                );
1121                if primary.base.relative_contrast(accent_text.color) < 4. {
1122                    Srgba::new(1., 1., 1., 1.)
1123                } else {
1124                    accent_text
1125                }
1126            })
1127        } else {
1128            let darkest = if bg.relative_luminance().luma < primary.base.relative_luminance().luma {
1129                bg
1130            } else {
1131                primary.base
1132            };
1133
1134            (darkest.relative_contrast(accent.color) < 4.).then(|| {
1135                let step_array = steps(accent, NonZeroUsize::new(100).unwrap());
1136                let primary_color_index = color_index(darkest, 100);
1137                let steps = if is_high_contrast { 60 } else { 50 };
1138                let accent_text = get_surface_color(
1139                    primary_color_index,
1140                    steps,
1141                    &step_array,
1142                    is_dark,
1143                    &Srgba::new(1., 1., 1., 1.),
1144                );
1145                if darkest.relative_contrast(accent_text.color) < 4. {
1146                    Srgba::new(0., 0., 0., 1.)
1147                } else {
1148                    accent_text
1149                }
1150            })
1151        };
1152
1153        let mut theme: Theme = Theme {
1154            name: palette.name().to_string(),
1155            shade: if palette.is_dark() {
1156                Srgba::new(0., 0., 0., 0.32)
1157            } else {
1158                Srgba::new(0., 0., 0., 0.08)
1159            },
1160            background: Container::new(
1161                Component::component(
1162                    bg_component,
1163                    accent,
1164                    on_bg_component,
1165                    component_hovered_overlay,
1166                    component_pressed_overlay,
1167                    is_high_contrast,
1168                    control_steps_array[8],
1169                ),
1170                bg,
1171                get_text(
1172                    bg_index,
1173                    &step_array,
1174                    &control_steps_array[8],
1175                    text_steps_array.as_deref(),
1176                ),
1177                get_small_widget_color(bg_index, 5, &neutral_steps, &control_steps_array[6]),
1178                is_high_contrast,
1179            ),
1180            primary,
1181            secondary: {
1182                let container_bg = if let Some(secondary_container_bg) = secondary_container_bg {
1183                    secondary_container_bg
1184                } else {
1185                    get_surface_color(bg_index, 10, &step_array, is_dark, &control_steps_array[2])
1186                };
1187
1188                let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap());
1189                let base_index = color_index(container_bg, step_array.len());
1190                let secondary_component =
1191                    get_surface_color(base_index, 3, &step_array, is_dark, &control_steps_array[4]);
1192
1193                component_hovered_overlay = if base_index < 91 {
1194                    control_steps_array[10]
1195                } else {
1196                    control_steps_array[0]
1197                };
1198                component_hovered_overlay.alpha = 0.1;
1199
1200                component_pressed_overlay = component_hovered_overlay;
1201                component_pressed_overlay.alpha = 0.2;
1202
1203                Container::new(
1204                    Component::component(
1205                        secondary_component,
1206                        accent,
1207                        get_text(
1208                            color_index(secondary_component, step_array.len()),
1209                            &step_array,
1210                            &control_steps_array[8],
1211                            text_steps_array.as_deref(),
1212                        ),
1213                        component_hovered_overlay,
1214                        component_pressed_overlay,
1215                        is_high_contrast,
1216                        control_steps_array[8],
1217                    ),
1218                    container_bg,
1219                    get_text(
1220                        base_index,
1221                        &step_array,
1222                        &control_steps_array[8],
1223                        text_steps_array.as_deref(),
1224                    ),
1225                    get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]),
1226                    is_high_contrast,
1227                )
1228            },
1229            accent: Component::colored_component(
1230                accent,
1231                control_steps_array[0],
1232                accent,
1233                button_hovered_overlay,
1234                button_pressed_overlay,
1235            ),
1236            accent_button: Component::colored_button(
1237                accent,
1238                control_steps_array[1],
1239                control_steps_array[0],
1240                accent,
1241                button_hovered_overlay,
1242                button_pressed_overlay,
1243            ),
1244            button: Component::component(
1245                button_bg,
1246                accent,
1247                on_bg_component,
1248                button_hovered_overlay,
1249                button_pressed_overlay,
1250                is_high_contrast,
1251                control_steps_array[8],
1252            ),
1253            destructive: Component::colored_component(
1254                destructive,
1255                control_steps_array[0],
1256                accent,
1257                button_hovered_overlay,
1258                button_pressed_overlay,
1259            ),
1260            destructive_button: Component::colored_button(
1261                destructive,
1262                control_steps_array[1],
1263                control_steps_array[0],
1264                accent,
1265                button_hovered_overlay,
1266                button_pressed_overlay,
1267            ),
1268            icon_button: Component::component(
1269                Srgba::new(0.0, 0.0, 0.0, 0.0),
1270                accent,
1271                control_steps_array[8],
1272                button_hovered_overlay,
1273                button_pressed_overlay,
1274                is_high_contrast,
1275                control_steps_array[8],
1276            ),
1277            link_button: {
1278                let mut component = Component::component(
1279                    Srgba::new(0.0, 0.0, 0.0, 0.0),
1280                    accent,
1281                    accent_text.unwrap_or(accent),
1282                    Srgba::new(0.0, 0.0, 0.0, 0.0),
1283                    Srgba::new(0.0, 0.0, 0.0, 0.0),
1284                    is_high_contrast,
1285                    control_steps_array[8],
1286                );
1287
1288                component.on_disabled = over(component.on.with_alpha(0.5), component.base);
1289                component
1290            },
1291            success: Component::colored_component(
1292                success,
1293                control_steps_array[0],
1294                accent,
1295                button_hovered_overlay,
1296                button_pressed_overlay,
1297            ),
1298            success_button: Component::colored_button(
1299                success,
1300                control_steps_array[1],
1301                control_steps_array[0],
1302                accent,
1303                button_hovered_overlay,
1304                button_pressed_overlay,
1305            ),
1306            text_button: Component::component(
1307                Srgba::new(0.0, 0.0, 0.0, 0.0),
1308                accent,
1309                accent_text.unwrap_or(accent),
1310                button_hovered_overlay,
1311                button_pressed_overlay,
1312                is_high_contrast,
1313                control_steps_array[8],
1314            ),
1315            warning: Component::colored_component(
1316                warning,
1317                control_steps_array[0],
1318                accent,
1319                button_hovered_overlay,
1320                button_pressed_overlay,
1321            ),
1322            warning_button: Component::colored_button(
1323                warning,
1324                control_steps_array[10],
1325                control_steps_array[0],
1326                accent,
1327                button_hovered_overlay,
1328                button_pressed_overlay,
1329            ),
1330            palette: palette.inner(),
1331            spacing,
1332            corner_radii,
1333            is_dark,
1334            is_high_contrast,
1335            gaps,
1336            active_hint,
1337            window_hint,
1338            is_frosted,
1339            accent_text,
1340            control_tint: neutral_tint,
1341            text_tint,
1342        };
1343        theme.spacing = spacing;
1344        theme.corner_radii = corner_radii;
1345        theme
1346    }
1347
1348    #[inline]
1349    /// Get the builder for the dark config
1350    pub fn dark_config() -> Result<Config, cosmic_config::Error> {
1351        Config::new(DARK_THEME_BUILDER_ID, Self::VERSION)
1352    }
1353
1354    #[inline]
1355    /// Get the builder for the light config
1356    pub fn light_config() -> Result<Config, cosmic_config::Error> {
1357        Config::new(LIGHT_THEME_BUILDER_ID, Self::VERSION)
1358    }
1359}