use crate::{ steps::*, Component, Container, CornerRadii, CosmicPalette, CosmicPaletteInner, Spacing, DARK_PALETTE, LIGHT_PALETTE, NAME, }; use cosmic_config::{Config, ConfigGet, ConfigSet, CosmicConfigEntry}; use palette::{Srgb, Srgba}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{fmt, num::NonZeroUsize}; #[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] /// Theme layer type pub enum Layer { /// Background layer #[default] Background, /// Primary Layer Primary, /// Secondary Layer Secondary, } /// Cosmic Theme data structure with all colors and its name #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct Theme { /// name of the theme pub name: String, /// background element colors pub background: Container, /// primary element colors pub primary: Container, /// secondary element colors pub secondary: Container, /// accent element colors pub accent: Component, /// suggested element colors pub success: Component, /// destructive element colors pub destructive: Component, /// warning element colors pub warning: Component, /// palette pub palette: CosmicPaletteInner, /// spacing pub spacing: Spacing, /// corner radii pub corner_radii: CornerRadii, /// is dark pub is_dark: bool, /// is high contrast pub is_high_contrast: bool, } impl CosmicConfigEntry for Theme { fn write_entry(&self, config: &Config) -> Result<(), cosmic_config::Error> { let self_ = self.clone(); // TODO do as transaction let tx = config.transaction(); tx.set("name", self_.name)?; tx.set("background", self_.background)?; tx.set("primary", self_.primary)?; tx.set("secondary", self_.secondary)?; tx.set("accent", self_.accent)?; tx.set("success", self_.success)?; tx.set("destructive", self_.destructive)?; tx.set("warning", self_.warning)?; tx.set("palette", self_.palette)?; tx.set("is_dark", self_.is_dark)?; tx.set("is_high_contrast", self_.is_high_contrast)?; tx.commit() } fn get_entry(config: &Config) -> Result, Self)> { let mut default = Self::default(); let mut errors = Vec::new(); match config.get::("name") { Ok(name) => default.name = name, Err(e) => errors.push(e), } match config.get::>("background") { Ok(background) => default.background = background, Err(e) => errors.push(e), } match config.get::>("primary") { Ok(primary) => default.primary = primary, Err(e) => errors.push(e), } match config.get::>("secondary") { Ok(secondary) => default.secondary = secondary, Err(e) => errors.push(e), } match config.get::>("accent") { Ok(accent) => default.accent = accent, Err(e) => errors.push(e), } match config.get::>("success") { Ok(success) => default.success = success, Err(e) => errors.push(e), } match config.get::>("destructive") { Ok(destructive) => default.destructive = destructive, Err(e) => errors.push(e), } match config.get::>("warning") { Ok(warning) => default.warning = warning, Err(e) => errors.push(e), } match config.get::>("palette") { Ok(palette) => default.palette = palette, Err(e) => errors.push(e), } match config.get::("is_dark") { Ok(is_dark) => default.is_dark = is_dark, Err(e) => errors.push(e), } match config.get::("is_high_contrast") { Ok(is_high_contrast) => default.is_high_contrast = is_high_contrast, Err(e) => errors.push(e), } match config.get::("spacing") { Ok(spacing) => default.spacing = spacing, Err(e) => errors.push(e), } match config.get::("corner_radii") { Ok(corner_radii) => default.corner_radii = corner_radii, Err(e) => errors.push(e), } if errors.is_empty() { Ok(default) } else { Err((errors, default)) } } } impl Default for Theme { fn default() -> Self { Self::dark_default() } } /// Trait for layered themes pub trait LayeredTheme { /// Set the layer of the theme fn set_layer(&mut self, layer: Layer); } impl Theme { /// version of the theme pub fn version() -> u64 { 1 } /// id of the theme pub fn id() -> &'static str { NAME } } impl Theme where C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, { /// Convert the theme to a high-contrast variant pub fn to_high_contrast(&self) -> Self { todo!(); } // TODO convenient getter functions for each named color variable /// get @accent_color pub fn accent_color(&self) -> Srgba { self.accent.base.clone().into() } /// get @success_color pub fn success_color(&self) -> Srgba { self.success.base.clone().into() } /// get @destructive_color pub fn destructive_color(&self) -> Srgba { self.destructive.base.clone().into() } /// get @warning_color pub fn warning_color(&self) -> Srgba { self.warning.base.clone().into() } // Containers /// get @bg_color pub fn bg_color(&self) -> Srgba { self.background.base.clone().into() } /// get @bg_component_color pub fn bg_component_color(&self) -> Srgba { self.background.component.base.clone().into() } /// get @primary_container_color pub fn primary_container_color(&self) -> Srgba { self.primary.base.clone().into() } /// get @primary_component_color pub fn primary_component_color(&self) -> Srgba { self.primary.component.base.clone().into() } /// get @secondary_container_color pub fn secondary_container_color(&self) -> Srgba { self.secondary.base.clone().into() } /// get @secondary_component_color pub fn secondary_component_color(&self) -> Srgba { self.secondary.component.base.clone().into() } // Text /// get @on_bg_color pub fn on_bg_color(&self) -> Srgba { self.background.on.clone().into() } /// get @on_bg_component_color pub fn on_bg_component_color(&self) -> Srgba { self.background.component.on.clone().into() } /// get @on_primary_color pub fn on_primary_container_color(&self) -> Srgba { self.primary.on.clone().into() } /// get @on_primary_component_color pub fn on_primary_component_color(&self) -> Srgba { self.primary.component.on.clone().into() } /// get @on_secondary_color pub fn on_secondary_container_color(&self) -> Srgba { self.secondary.on.clone().into() } /// get @on_secondary_component_color pub fn on_secondary_component_color(&self) -> Srgba { self.secondary.component.on.clone().into() } /// get @accent_text_color pub fn accent_text_color(&self) -> Srgba { self.accent.base.clone().into() } /// get @success_text_color pub fn success_text_color(&self) -> Srgba { self.success.base.clone().into() } /// get @warning_text_color pub fn warning_text_color(&self) -> Srgba { self.warning.base.clone().into() } /// get @destructive_text_color pub fn destructive_text_color(&self) -> Srgba { self.destructive.base.clone().into() } /// get @on_accent_color pub fn on_accent_color(&self) -> Srgba { self.accent.on.clone().into() } /// get @on_success_color pub fn on_success_color(&self) -> Srgba { self.success.on.clone().into() } /// get @oon_warning_color pub fn on_warning_color(&self) -> Srgba { self.warning.on.clone().into() } /// get @on_destructive_color pub fn on_destructive_color(&self) -> Srgba { self.destructive.on.clone().into() } // Borders and Dividers /// get @bg_divider pub fn bg_divider(&self) -> Srgba { self.background.divider.clone().into() } /// get @bg_component_divider pub fn bg_component_divider(&self) -> Srgba { self.background.component.divider.clone().into() } /// get @primary_container_divider pub fn primary_container_divider(&self) -> Srgba { self.primary.divider.clone().into() } /// get @primary_component_divider pub fn primary_component_divider(&self) -> Srgba { self.primary.component.divider.clone().into() } /// get @secondary_container_divider pub fn secondary_container_divider(&self) -> Srgba { self.secondary.divider.clone().into() } /// get @secondary_component_divider pub fn secondary_component_divider(&self) -> Srgba { self.secondary.component.divider.clone().into() } /// get @window_header_bg pub fn window_header_bg(&self) -> Srgba { self.background.base.clone().into() } /// get @space_none pub fn space_none(&self) -> u16 { self.spacing.space_none } /// get @space_xxxs pub fn space_xxxs(&self) -> u16 { self.spacing.space_xxxs } /// get @space_xxs pub fn space_xxs(&self) -> u16 { self.spacing.space_xxs } /// get @space_xs pub fn space_xs(&self) -> u16 { self.spacing.space_xs } /// get @space_s pub fn space_s(&self) -> u16 { self.spacing.space_s } /// get @space_m pub fn space_m(&self) -> u16 { self.spacing.space_m } /// get @space_l pub fn space_l(&self) -> u16 { self.spacing.space_l } /// get @space_xl pub fn space_xl(&self) -> u16 { self.spacing.space_xl } /// get @space_xxl pub fn space_xxl(&self) -> u16 { self.spacing.space_xxl } /// get @space_xxxl pub fn space_xxxl(&self) -> u16 { self.spacing.space_xxxl } /// get @radius_0 pub fn radius_0(&self) -> [u16; 4] { self.corner_radii.radius_0 } /// get @radius_xs pub fn radius_xs(&self) -> [u16; 4] { self.corner_radii.radius_xs } /// get @radius_s pub fn radius_s(&self) -> [u16; 4] { self.corner_radii.radius_s } /// get @radius_m pub fn radius_m(&self) -> [u16; 4] { self.corner_radii.radius_m } /// get @radius_l pub fn radius_l(&self) -> [u16; 4] { self.corner_radii.radius_l } /// get @radius_xl pub fn radius_xl(&self) -> [u16; 4] { self.corner_radii.radius_xl } } impl Theme { /// get the built in light theme pub fn light_default() -> Self { LIGHT_PALETTE.clone().into() } /// get the built in dark theme pub fn dark_default() -> Self { DARK_PALETTE.clone().into() } /// get the built in high contrast dark theme pub fn high_contrast_dark_default() -> Self { CosmicPalette::HighContrastDark(DARK_PALETTE.as_ref().clone()).into() } /// get the built in high contrast light theme pub fn high_contrast_light_default() -> Self { CosmicPalette::HighContrastLight(LIGHT_PALETTE.as_ref().clone()).into() } } impl From> for Theme where CosmicPalette: Into>, { fn from(p: CosmicPalette) -> Self { ThemeBuilder::palette(p.into()).build() } } /// Helper for building customized themes #[derive(Debug, Serialize, Deserialize)] pub struct ThemeBuilder { palette: CosmicPalette, spacing: Spacing, corner_radii: CornerRadii, neutral_tint: Option, bg_color: Option, primary_container_bg: Option, secondary_container_bg: Option, text_tint: Option, accent: Option, } impl Default for ThemeBuilder { fn default() -> Self { Self { palette: DARK_PALETTE.to_owned().into(), spacing: Spacing::default(), corner_radii: CornerRadii::default(), neutral_tint: Default::default(), text_tint: Default::default(), bg_color: Default::default(), primary_container_bg: Default::default(), secondary_container_bg: Default::default(), accent: Default::default(), } } } impl ThemeBuilder { /// Get a builder that is initialized with the default dark theme pub fn dark() -> Self { Self { palette: DARK_PALETTE.to_owned().into(), ..Default::default() } } /// Get a builder that is initialized with the default light theme pub fn light() -> Self { Self { palette: LIGHT_PALETTE.to_owned().into(), ..Default::default() } } /// Get a builder that is initialized with the default dark high contrast theme pub fn dark_high_contrast() -> Self { let palette: CosmicPalette = DARK_PALETTE.to_owned().into(); Self { palette: CosmicPalette::HighContrastLight(palette.inner()), ..Default::default() } } /// Get a builder that is initialized with the default light high contrast theme pub fn light_high_contrast() -> Self { let palette: CosmicPalette = LIGHT_PALETTE.to_owned().into(); Self { palette: CosmicPalette::HighContrastLight(palette.inner()), ..Default::default() } } /// Get a builder that is initialized with the provided palette pub fn palette(palette: CosmicPalette) -> Self { Self { palette, ..Default::default() } } /// set the spacing of the builder pub fn spacing(mut self, spacing: Spacing) -> Self { self.spacing = spacing; self } /// set the corner_radii of the builder pub fn corner_radii(mut self, corner_radii: CornerRadii) -> Self { self.corner_radii = corner_radii; self } /// apply a neutral tint to the palette pub fn neutral_tint(mut self, tint: Srgb) -> Self { self.neutral_tint = Some(tint); self } /// apply a text tint to the palette pub fn text_tint(mut self, tint: Srgb) -> Self { self.text_tint = Some(tint); self } /// apply a background color to the palette pub fn bg_color(mut self, c: Srgba) -> Self { self.bg_color = Some(c); self } /// apply a primary container background color to the palette pub fn primary_container_bg(mut self, c: Srgba) -> Self { self.primary_container_bg = Some(c); self } /// apply a accent color to the palette pub fn accent(mut self, c: Srgb) -> Self { self.accent = Some(c); self } /// build the theme pub fn build(self) -> Theme { let Self { mut palette, spacing, corner_radii, neutral_tint, text_tint, bg_color, primary_container_bg, secondary_container_bg, accent, } = self; let is_dark = palette.is_dark(); let is_high_contrast = palette.is_high_contrast(); if let Some(accent) = accent { palette.as_mut().accent = accent.into(); } let text_steps_array = text_tint.map(|c| steps(c, NonZeroUsize::new(100).unwrap())); if let Some(neutral_tint) = neutral_tint { let mut neutral_steps_arr = steps(neutral_tint, NonZeroUsize::new(11).unwrap()); if !is_dark { neutral_steps_arr.reverse(); } let p = palette.as_mut(); p.neutral_0 = neutral_steps_arr[0]; p.neutral_1 = neutral_steps_arr[1]; p.neutral_2 = neutral_steps_arr[2]; p.neutral_3 = neutral_steps_arr[3]; p.neutral_4 = neutral_steps_arr[4]; p.neutral_5 = neutral_steps_arr[5]; p.neutral_6 = neutral_steps_arr[6]; p.neutral_7 = neutral_steps_arr[7]; p.neutral_8 = neutral_steps_arr[8]; p.neutral_9 = neutral_steps_arr[9]; p.neutral_10 = neutral_steps_arr[10]; } if let Some(accent) = accent { palette.as_mut().accent = accent.into(); } let p_ref = palette.as_ref(); let bg = if let Some(bg_color) = bg_color { bg_color } else { p_ref.gray_1.clone() }; let step_array = steps(bg, NonZeroUsize::new(100).unwrap()); let bg_index = color_index(bg, step_array.len()); let primary_container_bg = if let Some(primary_container_bg_color) = primary_container_bg { primary_container_bg_color } else { get_color(bg_index, 5, &step_array, is_dark, &p_ref.neutral_1) }; let secondary_container_bg = if let Some(secondary_container_bg) = secondary_container_bg { secondary_container_bg } else { get_color(bg_index, 10, &step_array, is_dark, &p_ref.neutral_2) }; let bg_component = get_color(bg_index, 8, &step_array, is_dark, &p_ref.neutral_2); let on_bg_component = get_text( color_index(bg_component, step_array.len()), &step_array, is_dark, &p_ref.neutral_8, text_steps_array.as_ref(), ); let bg_component = Component::component( bg_component, p_ref.neutral_0.to_owned(), p_ref.accent.to_owned(), on_bg_component, is_high_contrast, ); let primary_index = color_index(primary_container_bg, step_array.len()); let primary_component = get_color(primary_index, 6, &step_array, is_dark, &p_ref.neutral_3); let on_primary_component = get_text( color_index(primary_component, step_array.len()), &step_array, is_dark, &p_ref.neutral_8, text_steps_array.as_ref(), ); let primary_component = Component::component( primary_component, p_ref.neutral_0.to_owned(), p_ref.accent.to_owned(), on_primary_component, is_high_contrast, ); let secondary_index = color_index(secondary_container_bg, step_array.len()); let secondary_component = get_color(secondary_index, 3, &step_array, is_dark, &p_ref.neutral_4); let on_secondary_component = get_text( color_index(secondary_component, step_array.len()), &step_array, is_dark, &p_ref.neutral_10, text_steps_array.as_ref(), ); let secondary_component = Component::component( secondary_component, p_ref.neutral_0.to_owned(), p_ref.accent.to_owned(), on_secondary_component, is_high_contrast, ); let mut theme: Theme = Theme { name: palette.name().to_string(), background: Container::new( bg_component, bg, get_text( bg_index, &step_array, is_dark, &p_ref.neutral_8, text_steps_array.as_ref(), ), ), primary: Container::new( primary_component, primary_container_bg, get_text( primary_index, &step_array, is_dark, &p_ref.neutral_8, text_steps_array.as_ref(), ), ), secondary: Container::new( secondary_component, secondary_container_bg, get_text( secondary_index, &step_array, is_dark, &p_ref.neutral_8, text_steps_array.as_ref(), ), ), accent: Component::colored_component( p_ref.accent.to_owned(), p_ref.neutral_0.to_owned(), p_ref.accent.to_owned(), ), success: Component::colored_component( p_ref.green.to_owned(), p_ref.neutral_0.to_owned(), p_ref.accent.to_owned(), ), destructive: Component::colored_component( p_ref.red.to_owned(), p_ref.neutral_0.to_owned(), p_ref.accent.to_owned(), ), warning: Component::colored_component( p_ref.yellow.to_owned(), p_ref.neutral_0.to_owned(), p_ref.accent.to_owned(), ), palette: palette.inner(), spacing, corner_radii, is_dark, is_high_contrast, }; theme.spacing = spacing; theme.corner_radii = corner_radii; theme } }