cosmic_theme/model/
theme.rs

1use crate::{
2    Component, Container, CornerRadii, CosmicPalette, CosmicPaletteInner, DARK_PALETTE,
3    LIGHT_PALETTE, NAME, Spacing, ThemeMode,
4    composite::over,
5    steps::{color_index, get_small_widget_color, get_surface_color, get_text, steps},
6};
7use cosmic_config::{Config, CosmicConfigEntry};
8use palette::{
9    IntoColor, Oklcha, Srgb, Srgba, WithAlpha, color_difference::Wcag21RelativeContrast, rgb::Rgb,
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(),
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            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
1107        let accent_text = if is_dark {
1108            (primary.base.relative_contrast(accent.color) < 4.).then(|| {
1109                let step_array = steps(accent, NonZeroUsize::new(100).unwrap());
1110                let primary_color_index = color_index(primary.base, 100);
1111                let steps = if is_high_contrast { 60 } else { 50 };
1112                let accent_text = get_surface_color(
1113                    primary_color_index,
1114                    steps,
1115                    &step_array,
1116                    is_dark,
1117                    &Srgba::new(1., 1., 1., 1.),
1118                );
1119                if primary.base.relative_contrast(accent_text.color) < 4. {
1120                    Srgba::new(1., 1., 1., 1.)
1121                } else {
1122                    accent_text
1123                }
1124            })
1125        } else {
1126            let darkest = if bg.relative_luminance().luma < primary.base.relative_luminance().luma {
1127                bg
1128            } else {
1129                primary.base
1130            };
1131
1132            (darkest.relative_contrast(accent.color) < 4.).then(|| {
1133                let step_array = steps(accent, NonZeroUsize::new(100).unwrap());
1134                let primary_color_index = color_index(darkest, 100);
1135                let steps = if is_high_contrast { 60 } else { 50 };
1136                let accent_text = get_surface_color(
1137                    primary_color_index,
1138                    steps,
1139                    &step_array,
1140                    is_dark,
1141                    &Srgba::new(1., 1., 1., 1.),
1142                );
1143                if darkest.relative_contrast(accent_text.color) < 4. {
1144                    Srgba::new(0., 0., 0., 1.)
1145                } else {
1146                    accent_text
1147                }
1148            })
1149        };
1150
1151        let mut theme: Theme = Theme {
1152            name: palette.name().to_string(),
1153            shade: if palette.is_dark() {
1154                Srgba::new(0., 0., 0., 0.32)
1155            } else {
1156                Srgba::new(0., 0., 0., 0.08)
1157            },
1158            background: Container::new(
1159                Component::component(
1160                    bg_component,
1161                    accent,
1162                    on_bg_component,
1163                    component_hovered_overlay,
1164                    component_pressed_overlay,
1165                    is_high_contrast,
1166                    control_steps_array[8],
1167                ),
1168                bg,
1169                get_text(
1170                    bg_index,
1171                    &step_array,
1172                    &control_steps_array[8],
1173                    text_steps_array.as_deref(),
1174                ),
1175                get_small_widget_color(bg_index, 5, &neutral_steps, &control_steps_array[6]),
1176                is_high_contrast,
1177            ),
1178            primary,
1179            secondary: {
1180                let container_bg = if let Some(secondary_container_bg) = secondary_container_bg {
1181                    secondary_container_bg
1182                } else {
1183                    get_surface_color(bg_index, 10, &step_array, is_dark, &control_steps_array[2])
1184                };
1185
1186                let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap());
1187                let base_index = color_index(container_bg, step_array.len());
1188                let secondary_component =
1189                    get_surface_color(base_index, 3, &step_array, is_dark, &control_steps_array[4]);
1190
1191                component_hovered_overlay = if base_index < 91 {
1192                    control_steps_array[10]
1193                } else {
1194                    control_steps_array[0]
1195                };
1196                component_hovered_overlay.alpha = 0.1;
1197
1198                component_pressed_overlay = component_hovered_overlay;
1199                component_pressed_overlay.alpha = 0.2;
1200
1201                Container::new(
1202                    Component::component(
1203                        secondary_component,
1204                        accent,
1205                        get_text(
1206                            color_index(secondary_component, step_array.len()),
1207                            &step_array,
1208                            &control_steps_array[8],
1209                            text_steps_array.as_deref(),
1210                        ),
1211                        component_hovered_overlay,
1212                        component_pressed_overlay,
1213                        is_high_contrast,
1214                        control_steps_array[8],
1215                    ),
1216                    container_bg,
1217                    get_text(
1218                        base_index,
1219                        &step_array,
1220                        &control_steps_array[8],
1221                        text_steps_array.as_deref(),
1222                    ),
1223                    get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]),
1224                    is_high_contrast,
1225                )
1226            },
1227            accent: Component::colored_component(
1228                accent,
1229                control_steps_array[0],
1230                accent,
1231                button_hovered_overlay,
1232                button_pressed_overlay,
1233            ),
1234            accent_button: Component::colored_button(
1235                accent,
1236                control_steps_array[1],
1237                control_steps_array[0],
1238                accent,
1239                button_hovered_overlay,
1240                button_pressed_overlay,
1241            ),
1242            button: Component::component(
1243                button_bg,
1244                accent,
1245                on_bg_component,
1246                button_hovered_overlay,
1247                button_pressed_overlay,
1248                is_high_contrast,
1249                control_steps_array[8],
1250            ),
1251            destructive: Component::colored_component(
1252                destructive,
1253                control_steps_array[0],
1254                accent,
1255                button_hovered_overlay,
1256                button_pressed_overlay,
1257            ),
1258            destructive_button: Component::colored_button(
1259                destructive,
1260                control_steps_array[1],
1261                control_steps_array[0],
1262                accent,
1263                button_hovered_overlay,
1264                button_pressed_overlay,
1265            ),
1266            icon_button: Component::component(
1267                Srgba::new(0.0, 0.0, 0.0, 0.0),
1268                accent,
1269                control_steps_array[8],
1270                button_hovered_overlay,
1271                button_pressed_overlay,
1272                is_high_contrast,
1273                control_steps_array[8],
1274            ),
1275            link_button: {
1276                let mut component = Component::component(
1277                    Srgba::new(0.0, 0.0, 0.0, 0.0),
1278                    accent,
1279                    accent_text.unwrap_or(accent),
1280                    Srgba::new(0.0, 0.0, 0.0, 0.0),
1281                    Srgba::new(0.0, 0.0, 0.0, 0.0),
1282                    is_high_contrast,
1283                    control_steps_array[8],
1284                );
1285
1286                component.on_disabled = over(component.on.with_alpha(0.5), component.base);
1287                component
1288            },
1289            success: Component::colored_component(
1290                success,
1291                control_steps_array[0],
1292                accent,
1293                button_hovered_overlay,
1294                button_pressed_overlay,
1295            ),
1296            success_button: Component::colored_button(
1297                success,
1298                control_steps_array[1],
1299                control_steps_array[0],
1300                accent,
1301                button_hovered_overlay,
1302                button_pressed_overlay,
1303            ),
1304            text_button: Component::component(
1305                Srgba::new(0.0, 0.0, 0.0, 0.0),
1306                accent,
1307                accent_text.unwrap_or(accent),
1308                button_hovered_overlay,
1309                button_pressed_overlay,
1310                is_high_contrast,
1311                control_steps_array[8],
1312            ),
1313            warning: Component::colored_component(
1314                warning,
1315                control_steps_array[0],
1316                accent,
1317                button_hovered_overlay,
1318                button_pressed_overlay,
1319            ),
1320            warning_button: Component::colored_button(
1321                warning,
1322                control_steps_array[10],
1323                control_steps_array[0],
1324                accent,
1325                button_hovered_overlay,
1326                button_pressed_overlay,
1327            ),
1328            palette: palette.inner(),
1329            spacing,
1330            corner_radii,
1331            is_dark,
1332            is_high_contrast,
1333            gaps,
1334            active_hint,
1335            window_hint,
1336            is_frosted,
1337            accent_text,
1338            control_tint: neutral_tint,
1339            text_tint,
1340        };
1341        theme.spacing = spacing;
1342        theme.corner_radii = corner_radii;
1343        theme
1344    }
1345
1346    #[inline]
1347    /// Get the builder for the dark config
1348    pub fn dark_config() -> Result<Config, cosmic_config::Error> {
1349        Config::new(DARK_THEME_BUILDER_ID, Self::VERSION)
1350    }
1351
1352    #[inline]
1353    /// Get the builder for the light config
1354    pub fn light_config() -> Result<Config, cosmic_config::Error> {
1355        Config::new(LIGHT_THEME_BUILDER_ID, Self::VERSION)
1356    }
1357}