cosmic_theme/model/
theme.rs

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