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
12pub const DARK_THEME_BUILDER_ID: &str = "com.system76.CosmicTheme.Dark.Builder";
14
15pub const DARK_THEME_ID: &str = "com.system76.CosmicTheme.Dark";
17
18pub const LIGHT_THEME_BUILDER_ID: &str = "com.system76.CosmicTheme.Light.Builder";
20
21pub const LIGHT_THEME_ID: &str = "com.system76.CosmicTheme.Light";
23
24#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
25pub enum Layer {
27 #[default]
29 Background,
30 Primary,
32 Secondary,
34}
35
36#[must_use]
37#[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 pub name: String,
50 pub background: Container,
52 pub primary: Container,
54 pub secondary: Container,
56 pub accent: Component,
58 pub success: Component,
60 pub destructive: Component,
62 pub warning: Component,
64 pub accent_button: Component,
66 pub success_button: Component,
68 pub destructive_button: Component,
70 pub warning_button: Component,
72 pub icon_button: Component,
74 pub link_button: Component,
76 pub text_button: Component,
78 pub button: Component,
80 pub palette: CosmicPaletteInner,
82 pub spacing: Spacing,
84 pub corner_radii: CornerRadii,
86 pub is_dark: bool,
88 pub is_high_contrast: bool,
90 pub gaps: (u32, u32),
92 pub active_hint: u32,
94 pub window_hint: Option<Srgb>,
96 pub is_frosted: bool,
98 pub shade: Srgba,
100 pub accent_text: Option<Srgba>,
103}
104
105impl Default for Theme {
106 #[inline]
107 fn default() -> Self {
108 Self::preferred_theme()
109 }
110}
111
112pub trait LayeredTheme {
114 fn set_layer(&mut self, layer: Layer);
116}
117
118impl Theme {
119 #[must_use]
120 pub fn id() -> &'static str {
122 NAME
123 }
124
125 #[inline]
126 pub fn dark_config() -> Result<Config, cosmic_config::Error> {
128 Config::new(DARK_THEME_ID, Self::VERSION)
129 }
130
131 #[inline]
132 pub fn light_config() -> Result<Config, cosmic_config::Error> {
134 Config::new(LIGHT_THEME_ID, Self::VERSION)
135 }
136
137 #[inline]
138 pub fn light_default() -> Self {
140 LIGHT_PALETTE.clone().into()
141 }
142
143 #[inline]
144 pub fn dark_default() -> Self {
146 DARK_PALETTE.clone().into()
147 }
148
149 #[inline]
150 pub fn high_contrast_dark_default() -> Self {
152 CosmicPalette::HighContrastDark(DARK_PALETTE.as_ref().clone()).into()
153 }
154
155 #[inline]
156 pub fn high_contrast_light_default() -> Self {
158 CosmicPalette::HighContrastLight(LIGHT_PALETTE.as_ref().clone()).into()
159 }
160
161 #[inline]
162 pub fn to_high_contrast(&self) -> Self {
164 todo!();
165 }
166
167 #[must_use]
169 #[allow(clippy::doc_markdown)]
170 #[inline]
171 pub fn accent_color(&self) -> Srgba {
173 self.accent.base
174 }
175
176 #[must_use]
177 #[allow(clippy::doc_markdown)]
178 #[inline]
179 pub fn success_color(&self) -> Srgba {
181 self.success.base
182 }
183
184 #[must_use]
185 #[allow(clippy::doc_markdown)]
186 #[inline]
187 pub fn destructive_color(&self) -> Srgba {
189 self.destructive.base
190 }
191
192 #[must_use]
193 #[allow(clippy::doc_markdown)]
194 #[inline]
195 pub fn warning_color(&self) -> Srgba {
197 self.warning.base
198 }
199
200 #[must_use]
201 #[allow(clippy::doc_markdown)]
202 #[inline]
203 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 #[must_use]
212 #[allow(clippy::doc_markdown)]
213 #[inline]
214 pub fn bg_color(&self) -> Srgba {
216 self.background.base
217 }
218
219 #[must_use]
220 #[allow(clippy::doc_markdown)]
221 #[inline]
222 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 pub fn primary_container_color(&self) -> Srgba {
232 self.primary.base
233 }
234
235 #[must_use]
236 #[allow(clippy::doc_markdown)]
237 #[inline]
238 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 pub fn secondary_container_color(&self) -> Srgba {
248 self.secondary.base
249 }
250
251 #[must_use]
252 #[allow(clippy::doc_markdown)]
253 #[inline]
254 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 pub fn button_bg_color(&self) -> Srgba {
264 self.button.base
265 }
266
267 #[must_use]
269 #[allow(clippy::doc_markdown)]
270 #[inline]
271 pub fn on_bg_color(&self) -> Srgba {
273 self.background.on
274 }
275
276 #[must_use]
277 #[allow(clippy::doc_markdown)]
278 #[inline]
279 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 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 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 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 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 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 pub fn success_text_color(&self) -> Srgba {
329 self.success.base
330 }
331
332 #[must_use]
333 #[allow(clippy::doc_markdown)]
334 #[inline]
335 pub fn warning_text_color(&self) -> Srgba {
337 self.warning.base
338 }
339
340 #[must_use]
341 #[allow(clippy::doc_markdown)]
342 #[inline]
343 pub fn destructive_text_color(&self) -> Srgba {
345 self.destructive.base
346 }
347
348 #[must_use]
349 #[allow(clippy::doc_markdown)]
350 #[inline]
351 pub fn on_accent_color(&self) -> Srgba {
353 self.accent.on
354 }
355
356 #[must_use]
357 #[allow(clippy::doc_markdown)]
358 #[inline]
359 pub fn on_success_color(&self) -> Srgba {
361 self.success.on
362 }
363
364 #[must_use]
365 #[allow(clippy::doc_markdown)]
366 #[inline]
367 pub fn on_warning_color(&self) -> Srgba {
369 self.warning.on
370 }
371
372 #[must_use]
373 #[allow(clippy::doc_markdown)]
374 #[inline]
375 pub fn on_destructive_color(&self) -> Srgba {
377 self.destructive.on
378 }
379
380 #[must_use]
381 #[allow(clippy::doc_markdown)]
382 #[inline]
383 pub fn button_color(&self) -> Srgba {
385 self.button.on
386 }
387
388 #[must_use]
390 #[allow(clippy::doc_markdown)]
391 #[inline]
392 pub fn bg_divider(&self) -> Srgba {
394 self.background.divider
395 }
396
397 #[must_use]
398 #[allow(clippy::doc_markdown)]
399 #[inline]
400 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 pub fn primary_container_divider(&self) -> Srgba {
410 self.primary.divider
411 }
412
413 #[must_use]
414 #[allow(clippy::doc_markdown)]
415 #[inline]
416 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 pub fn secondary_container_divider(&self) -> Srgba {
426 self.secondary.divider
427 }
428
429 #[must_use]
430 #[allow(clippy::doc_markdown)]
431 #[inline]
432 pub fn button_divider(&self) -> Srgba {
434 self.button.divider
435 }
436
437 #[must_use]
438 #[allow(clippy::doc_markdown)]
439 #[inline]
440 pub fn window_header_bg(&self) -> Srgba {
442 self.background.base
443 }
444
445 #[must_use]
446 #[allow(clippy::doc_markdown)]
447 #[inline]
448 pub fn space_none(&self) -> u16 {
450 self.spacing.space_none
451 }
452
453 #[must_use]
454 #[allow(clippy::doc_markdown)]
455 #[inline]
456 pub fn space_xxxs(&self) -> u16 {
458 self.spacing.space_xxxs
459 }
460
461 #[must_use]
462 #[allow(clippy::doc_markdown)]
463 #[inline]
464 pub fn space_xxs(&self) -> u16 {
466 self.spacing.space_xxs
467 }
468
469 #[must_use]
470 #[allow(clippy::doc_markdown)]
471 #[inline]
472 pub fn space_xs(&self) -> u16 {
474 self.spacing.space_xs
475 }
476
477 #[must_use]
478 #[allow(clippy::doc_markdown)]
479 #[inline]
480 pub fn space_s(&self) -> u16 {
482 self.spacing.space_s
483 }
484
485 #[must_use]
486 #[allow(clippy::doc_markdown)]
487 #[inline]
488 pub fn space_m(&self) -> u16 {
490 self.spacing.space_m
491 }
492
493 #[must_use]
494 #[allow(clippy::doc_markdown)]
495 #[inline]
496 pub fn space_l(&self) -> u16 {
498 self.spacing.space_l
499 }
500
501 #[must_use]
502 #[allow(clippy::doc_markdown)]
503 #[inline]
504 pub fn space_xl(&self) -> u16 {
506 self.spacing.space_xl
507 }
508
509 #[must_use]
510 #[allow(clippy::doc_markdown)]
511 #[inline]
512 pub fn space_xxl(&self) -> u16 {
514 self.spacing.space_xxl
515 }
516
517 #[must_use]
518 #[allow(clippy::doc_markdown)]
519 #[inline]
520 pub fn space_xxxl(&self) -> u16 {
522 self.spacing.space_xxxl
523 }
524
525 #[must_use]
526 #[allow(clippy::doc_markdown)]
527 #[inline]
528 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 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 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 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 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 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 pub fn shade_color(&self) -> Srgba {
578 self.shade
579 }
580
581 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 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 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 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#[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 pub palette: CosmicPalette,
675 pub spacing: Spacing,
677 pub corner_radii: CornerRadii,
679 pub neutral_tint: Option<Srgb>,
681 pub bg_color: Option<Srgba>,
683 pub primary_container_bg: Option<Srgba>,
685 pub secondary_container_bg: Option<Srgba>,
687 pub text_tint: Option<Srgb>,
689 pub accent: Option<Srgb>,
691 pub success: Option<Srgb>,
693 pub warning: Option<Srgb>,
695 pub destructive: Option<Srgb>,
697 pub is_frosted: bool, pub gaps: (u32, u32),
701 pub active_hint: u32,
703 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 gaps: (0, 8),
725 active_hint: 3,
726 window_hint: None,
727 }
728 }
729}
730
731impl ThemeBuilder {
732 #[inline]
733 pub fn dark() -> Self {
735 Self {
736 palette: DARK_PALETTE.to_owned(),
737 ..Default::default()
738 }
739 }
740
741 #[inline]
742 pub fn light() -> Self {
744 Self {
745 palette: LIGHT_PALETTE.to_owned(),
746 ..Default::default()
747 }
748 }
749
750 #[inline]
751 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 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 pub fn palette(palette: CosmicPalette) -> Self {
773 Self {
774 palette,
775 ..Default::default()
776 }
777 }
778
779 #[inline]
780 pub fn spacing(mut self, spacing: Spacing) -> Self {
782 self.spacing = spacing;
783 self
784 }
785
786 #[inline]
787 pub fn corner_radii(mut self, corner_radii: CornerRadii) -> Self {
789 self.corner_radii = corner_radii;
790 self
791 }
792
793 #[inline]
794 pub fn neutral_tint(mut self, tint: Srgb) -> Self {
796 self.neutral_tint = Some(tint);
797 self
798 }
799
800 #[inline]
801 pub fn text_tint(mut self, tint: Srgb) -> Self {
803 self.text_tint = Some(tint);
804 self
805 }
806
807 #[inline]
808 pub fn bg_color(mut self, c: Srgba) -> Self {
810 self.bg_color = Some(c);
811 self
812 }
813
814 #[inline]
815 pub fn primary_container_bg(mut self, c: Srgba) -> Self {
817 self.primary_container_bg = Some(c);
818 self
819 }
820
821 #[inline]
822 pub fn accent(mut self, c: Srgb) -> Self {
824 self.accent = Some(c);
825 self
826 }
827
828 #[inline]
829 pub fn success(mut self, c: Srgb) -> Self {
831 self.success = Some(c);
832 self
833 }
834
835 #[inline]
836 pub fn warning(mut self, c: Srgb) -> Self {
838 self.warning = Some(c);
839 self
840 }
841
842 #[inline]
843 pub fn destructive(mut self, c: Srgb) -> Self {
845 self.destructive = Some(c);
846 self
847 }
848
849 #[allow(clippy::too_many_lines)]
850 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 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 pub fn dark_config() -> Result<Config, cosmic_config::Error> {
1258 Config::new(DARK_THEME_BUILDER_ID, Self::VERSION)
1259 }
1260
1261 #[inline]
1262 pub fn light_config() -> Result<Config, cosmic_config::Error> {
1264 Config::new(LIGHT_THEME_BUILDER_ID, Self::VERSION)
1265 }
1266}