diff --git a/Cargo.toml b/Cargo.toml index dea5d0c..b0ad0d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,3 +97,6 @@ exclude = [ [patch."https://github.com/pop-os/libcosmic"] libcosmic = { path = "./", features = ["wayland", "tokio", "a11y"]} + +[patch.crates-io] +palette = {git = "https://github.com/Ogeon/palette", features = ["serializing"] } diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index b53e338..ecf4415 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -12,17 +12,15 @@ rustdoc-args = ["--cfg", "docsrs"] [features] default = [] no-default = [] -contrast-derivation = ["float-cmp"] -theme-from-image = ["kmeans_colors", "contrast-derivation", "float-cmp", "image"] -hex-color = ["hex"] +theme-from-image = ["kmeans_colors", "image"] [dependencies] -palette = {version = "0.7", features = ["serializing"] } +# palette = {version = "0.7", features = ["serializing"] } +almost = "0.2" +palette = {git = "https://github.com/Ogeon/palette", features = ["serializing"] } anyhow = "1.0" -hex = {version = "0.4.3", optional = true} kmeans_colors = { version = "0.5", features = ["palette_color"], default-features = false, optional = true } image = {version = "0.24.1", optional = true } -float-cmp = { version = "0.9.0", optional = true } serde = { version = "1.0.129", features = ["derive"] } ron = "0.8" lazy_static = "1.4.0" diff --git a/cosmic-theme/src/color_picker/exact.rs b/cosmic-theme/src/color_picker/exact.rs deleted file mode 100644 index 2e29c26..0000000 --- a/cosmic-theme/src/color_picker/exact.rs +++ /dev/null @@ -1,170 +0,0 @@ -use super::ColorPicker; -use crate::{Selection, ThemeConstraints}; -use anyhow::{anyhow, bail, Result}; -use float_cmp::approx_eq; -use palette::{Clamp, IntoColor, Lch, RelativeContrast, Srgba}; -use serde::{de::DeserializeOwned, Serialize}; -use std::fmt; - -/// Implementation of a Cosmic color chooser which exactly meets constraints -#[derive(Debug, Default, Clone)] -pub struct Exact { - selection: Selection, - constraints: ThemeConstraints, -} - -impl Exact -where - C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, -{ - /// create a new Exact color picker - pub fn new(selection: Selection, constraints: ThemeConstraints) -> Self { - Self { - selection, - constraints, - } - } -} - -impl ColorPicker for Exact -where - C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, -{ - fn get_constraints(&self) -> ThemeConstraints { - self.constraints - } - - fn get_selection(&self) -> Selection { - self.selection.clone() - } - - fn pick_color_graphic( - &self, - color: C, - contrast: f32, - grayscale: bool, - lighten: Option, - ) -> (C, Option) { - let mut err = None; - - let res = self.pick_color(color.clone(), Some(contrast), grayscale, lighten); - if let Ok(c) = res { - return (c, err); - } else if let Err(e) = res { - err = Some(anyhow!("Graphic contrast {} failed: {}", contrast, e)); - } - - let res = self.pick_color(color.clone(), None, grayscale, lighten); - if let Ok(c) = res { - return (c, err); - } else if let Err(e) = res { - err = Some(e); - } - - // return same color if no other color possible - (color, err) - } - - fn pick_color_text( - &self, - color: C, - grayscale: bool, - lighten: Option, - ) -> (C, Option) { - let mut err = None; - - // AAA - let res = self.pick_color(color.clone(), Some(7.0), grayscale, lighten); - if let Ok(c) = res { - return (c, err); - } else if let Err(e) = res { - err = Some(anyhow!("AAA text contrast failed: {}", e)); - } - - // AA - let res = self.pick_color(color.clone(), Some(4.5), grayscale, lighten); - if let Ok(c) = res { - return (c, err); - } else if let Err(e) = res { - err = Some(anyhow!("AA text contrast failed: {}", e)); - } - - let res = self.pick_color(color.clone(), None, grayscale, lighten); - if let Ok(c) = res { - return (c, err); - } else if let Err(e) = res { - err = Some(e); - } - - (color, err) - } - - fn pick_color( - &self, - color: C, - contrast: Option, - grayscale: bool, - lighten: Option, - ) -> Result { - let srgba: Srgba = color.clone().into(); - let mut lch_color: Lch = srgba.into_color(); - - // set to grayscale - if grayscale { - lch_color.chroma = 0.0; - } - - // lighten or darken - // TODO closed form solution using Lch color space contrast formula? - // for now do binary search... - - if let Some(contrast) = contrast { - let (min, max) = match lighten { - Some(b) if b => (lch_color.l, 100.0), - Some(_) => (0.0, lch_color.l), - None => (0.0, 100.0), - }; - let (mut l, mut r) = (min, max); - - for _ in 0..100 { - let cur_guess_lightness = (l + r) / 2.0; - let mut cur_guess = lch_color; - cur_guess.l = cur_guess_lightness; - let cur_contrast = srgba.get_contrast_ratio(&cur_guess.into_color()); - let contrast_dir = contrast > cur_contrast; - let lightness_dir = lch_color.l < cur_guess.l; - if approx_eq!(f32, contrast, cur_contrast, ulps = 4) { - lch_color = cur_guess; - break; - // TODO fix - } else if lightness_dir && contrast_dir || !lightness_dir && !contrast_dir { - l = cur_guess_lightness; - } else { - r = cur_guess_lightness; - } - } - - // clamp to valid value in range - lch_color.clamp_self(); - - // verify contrast - let actual_contrast = srgba.get_contrast_ratio(&lch_color.into_color()); - if !approx_eq!(f32, contrast, actual_contrast, ulps = 4) { - bail!( - "Failed to derive color with contrast {} from {:?}", - contrast, - color - ); - } - - Ok(C::from(lch_color.into_color())) - } else { - // maximize contrast if no constraint is given - if lch_color.l > 50.0 { - Ok(C::from(palette::named::BLACK.into_format().into_color())) - } else { - Ok(C::from(palette::named::WHITE.into_format().into_color())) - } - } - } -} diff --git a/cosmic-theme/src/color_picker/mod.rs b/cosmic-theme/src/color_picker/mod.rs deleted file mode 100644 index b5bf4ee..0000000 --- a/cosmic-theme/src/color_picker/mod.rs +++ /dev/null @@ -1,280 +0,0 @@ -use crate::{Component, Container, ContainerType, Derivation, Selection, Theme, ThemeConstraints}; -use anyhow::{anyhow, Result}; -use palette::{IntoColor, Lcha, Shade, Srgba}; -use serde::{de::DeserializeOwned, Serialize}; -use std::fmt; - -pub use exact::*; -mod exact; - -// TODO derive palette from Selection? -/// Color picker derives colors and theme elements -pub trait ColorPicker< - C: Into + From + Clone + fmt::Debug + Default + Serialize + DeserializeOwned, -> -{ - /// try to derive a color with a given contrast, grayscale setting, and lightness direction - fn pick_color( - &self, - color: C, - contrast: Option, - grayscale: bool, - lighten: Option, - ) -> Result; - - /// try to derive a text color with a given grayscale setting, and lightness direction - fn pick_color_text( - &self, - color: C, - grayscale: bool, - lighten: Option, - ) -> (C, Option); - - /// try to derive a graphic color with a given contrast, grayscale setting, and lightness direction - fn pick_color_graphic( - &self, - color: C, - contrast: f32, - grayscale: bool, - lighten: Option, - ) -> (C, Option); - - /// get the selection for this color picker - fn get_selection(&self) -> Selection; - - /// get the constraints for this color picker - fn get_constraints(&self) -> ThemeConstraints; - - /// derive a theme from the selection and constraints - fn theme_derivation(&self) -> Derivation> { - let mut theme_errors = Vec::new(); - - let Derivation { - derived: background, - errors: mut errs, - } = self.container_derivation(ContainerType::Background); - theme_errors.append(&mut errs); - - let Derivation { - derived: primary, - errors: mut errs, - } = self.container_derivation(ContainerType::Primary); - theme_errors.append(&mut errs); - - let Derivation { - derived: secondary, - mut errors, - } = self.container_derivation(ContainerType::Secondary); - theme_errors.append(&mut errors); - - let Derivation { - derived: accent, - mut errors, - } = self.widget_derivation(self.get_selection().accent); - theme_errors.append(&mut errors); - - let Derivation { - derived: destructive, - mut errors, - } = self.widget_derivation(self.get_selection().destructive); - theme_errors.append(&mut errors); - - let Derivation { - derived: warning, - mut errors, - } = self.widget_derivation(self.get_selection().warning); - theme_errors.append(&mut errors); - - let Derivation { - derived: success, - mut errors, - } = self.widget_derivation(self.get_selection().success); - theme_errors.append(&mut errors); - - Derivation { - derived: Theme::new( - background, - primary, - secondary, - accent, - destructive, - warning, - success, - ), - errors: theme_errors, - } - } - - /// derive a container element - fn container_derivation(&self, container_type: ContainerType) -> Derivation> { - let selection = self.get_selection(); - let constraints = self.get_constraints(); - - let mut errors = Vec::new(); - - let Selection { - background, - primary_container, - secondary_container, - .. - } = selection; - - let ThemeConstraints { - elevated_contrast_ratio, - divider_contrast_ratio, - divider_gray_scale, - lighten, - .. - } = constraints; - - let container = match container_type { - ContainerType::Background => background, - ContainerType::Primary => primary_container, - ContainerType::Secondary => secondary_container, - }; - let (container_divider, err) = self.pick_color_graphic( - container.clone(), - divider_contrast_ratio, - divider_gray_scale, - Some(lighten), - ); - if let Some(e) = err { - errors.push(e); - }; - - let (container_fg, err) = self.pick_color_text(container.clone(), true, None); - if let Some(err) = err { - let err = anyhow!("{} => \"container text\" failed: {}", container_type, err); - errors.push(err); - }; - - // TODO revisit this and adjust constraints for transparency - let mut container_fg_opacity_80: Srgba = container_fg.clone().into(); - container_fg_opacity_80.alpha *= 0.8; - - let (component_default, err) = self.pick_color_graphic( - container.clone(), - elevated_contrast_ratio, - false, - Some(lighten), - ); - if let Some(e) = err { - let err = anyhow!( - "{} => \"container component\" failed: {}", - container_type, - e - ); - errors.push(err); - }; - - let Derivation { - derived: container_component, - errors: errs, - } = self.widget_derivation(component_default); - for e in errs { - let err = anyhow!( - "{} => \"container component derivation\" failed: {}", - container_type, - e - ); - errors.push(err); - } - - Derivation { - derived: Container { - base: container, - divider: container_divider, - on: container_fg, - component: container_component, - }, - errors, - } - } - - /// derive a widget - fn widget_derivation(&self, default: C) -> Derivation> { - let ThemeConstraints { - divider_contrast_ratio, - divider_gray_scale, - lighten, - .. - } = self.get_constraints(); - - let mut errors = Vec::new(); - - let rgba: Srgba = default.clone().into(); - let lch = Lcha { - color: rgba.color.into_color(), - alpha: rgba.alpha, - }; - - // TODO define constraints for different states... - // & add color self methods and errors if these fail - let hover = if lighten { - lch.lighten(0.1) - } else { - lch.darken(0.1) - }; - - let pressed = if lighten { - hover.lighten(0.1) - } else { - hover.darken(0.1) - }; - let pressed = C::from(Srgba { - color: pressed.color.into_color(), - alpha: pressed.alpha, - }); - - // TODO is this actually a different color? or just outlined? - let selected = default.clone(); - - let mut disabled: Srgba = default.clone().into(); - disabled.alpha = 0.5; - - let (divider, error) = self.pick_color_graphic( - pressed.clone(), - divider_contrast_ratio, - divider_gray_scale, - Some(lighten), - ); - if let Some(error) = error { - errors.push(error); - } - - let (text, error) = self.pick_color_text(pressed.clone(), true, None); - if let Some(error) = error { - errors.push(error); - } - - let (selected_text, error) = self.pick_color_text(selected.clone(), true, None); - if let Some(error) = error { - errors.push(error); - } - - let mut text_opacity_80: Srgba = text.clone().into(); - text_opacity_80.alpha = 0.8; - - let mut disabled_fg = text.clone().into(); - disabled_fg.alpha = 0.5; - - Derivation { - derived: Component { - base: default, - hover: C::from(Srgba { - color: hover.color.into_color(), - alpha: hover.alpha, - }), - pressed, - selected: selected.clone(), - selected_text: selected_text, - focus: selected.clone(), // FIXME - divider, - on: text, - disabled: disabled.into(), - on_disabled: disabled_fg.into(), - }, - errors, - } - } -} diff --git a/cosmic-theme/src/composite.rs b/cosmic-theme/src/composite.rs new file mode 100644 index 0000000..c30469b --- /dev/null +++ b/cosmic-theme/src/composite.rs @@ -0,0 +1,27 @@ +use palette::Srgba; + +/// straight alpha "A over B" operator on non-linear srgba +pub fn over, B: Into>(a: A, b: B) -> Srgba { + let a = a.into(); + let b = b.into(); + let o_a = (alpha_over(a.alpha, b.alpha)).max(0.0).min(1.0); + let o_r = (c_over(a.red, b.red, a.alpha, b.alpha, o_a)) + .max(0.0) + .min(1.0); + let o_g = (c_over(a.green, b.green, a.alpha, b.alpha, o_a)) + .max(0.0) + .min(1.0); + let o_b = (c_over(a.blue, b.blue, a.alpha, b.alpha, o_a)) + .max(0.0) + .min(1.0); + + Srgba::new(o_r, o_g, o_b, o_a) +} + +fn alpha_over(a: f32, b: f32) -> f32 { + a + b * (1.0 - a) +} + +fn c_over(a: f32, b: f32, a_alpha: f32, b_alpha: f32, o_alpha: f32) -> f32 { + a * a_alpha + b * b_alpha * (1.0 - a_alpha) / o_alpha +} diff --git a/cosmic-theme/src/config/mod.rs b/cosmic-theme/src/config/mod.rs deleted file mode 100644 index a558fcf..0000000 --- a/cosmic-theme/src/config/mod.rs +++ /dev/null @@ -1,196 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0-only - -use crate::{util::CssColor, Theme, NAME, THEME_DIR}; -use anyhow::{bail, Context, Result}; -use directories::{BaseDirsExt, ProjectDirsExt}; -use palette::Srgba; -use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use std::{ - fmt, - fs::File, - io::{prelude::*, BufReader}, - path::PathBuf, -}; - -/// Cosmic Theme config -#[derive(Debug, Deserialize, Serialize, Clone)] -#[serde(deny_unknown_fields)] -pub struct Config { - /// whether high contrast mode is activated - pub is_high_contrast: bool, - /// active - pub is_dark: bool, - /// Selected light theme name - pub light: String, - /// Selected dark theme name - pub dark: String, -} - -impl Default for Config { - fn default() -> Self { - Self { - is_dark: true, - light: "cosmic-light".to_string(), - dark: "cosmic-dark".to_string(), - is_high_contrast: false, - } - } -} - -/// name of the config file -pub const CONFIG_NAME: &str = "config"; - -impl Config { - /// create a new cosmic theme config - pub fn new(is_dark: bool, high_contrast: bool, light: String, dark: String) -> Self { - Self { - is_dark, - light, - dark, - is_high_contrast: high_contrast, - } - } - - /// save the cosmic theme config - pub fn save(&self) -> Result<()> { - let xdg_dirs = directories::ProjectDirs::from_path(PathBuf::from(NAME)) - .context("Failed to find project directory.")?; - if let Ok(path) = xdg_dirs.place_config_file(PathBuf::from(format!("{CONFIG_NAME}.ron"))) { - let mut f = File::create(path)?; - let ron = ron::ser::to_string_pretty(&self, Default::default())?; - f.write_all(ron.as_bytes())?; - Ok(()) - } else { - bail!("failed to save theme config") - } - } - - /// init the config directory - pub fn init() -> anyhow::Result { - let base_dirs = directories::BaseDirs::new().context("Failed to get base directories.")?; - let res = Ok(base_dirs.create_config_directory(NAME)?); - Theme::::init()?; - - if Self::load().is_ok() { - res - } else { - Self::default().save()?; - Theme::dark_default().save()?; - Theme::light_default().save()?; - res - } - } - - /// load the cosmic theme config - pub fn load() -> Result { - let xdg_dirs = directories::ProjectDirs::from_path(PathBuf::from(NAME)) - .context("Failed to find project directory.")?; - let path = xdg_dirs.config_dir(); - std::fs::create_dir_all(&path)?; - let path = xdg_dirs.find_config_file(PathBuf::from(format!("{CONFIG_NAME}.ron"))); - if path.is_none() { - let s = Self::default(); - s.save()?; - } - if let Some(path) = xdg_dirs.find_config_file(PathBuf::from(format!("{CONFIG_NAME}.ron"))) { - let mut f = File::open(&path)?; - let mut s = String::new(); - f.read_to_string(&mut s)?; - Ok(ron::from_str(s.as_str())?) - } else { - anyhow::bail!("Failed to load config") - } - } - - /// get the name of the active theme - pub fn active_name(&self) -> Option { - if self.is_dark && self.dark.is_empty() { - Some(self.dark.clone()) - } else if !self.is_dark && !self.light.is_empty() { - Some(self.light.clone()) - } else { - None - } - // if *high_contrast { - // if let Some(palette) = palette.take() { - // // TODO enforce high contrast constraints - // *palette = palette.to_high_contrast(); - // todo!() - // } - // } - } - - /// get the active theme - pub fn get_active(&self) -> anyhow::Result> { - let active = match self.active_name() { - Some(n) => n, - _ => anyhow::bail!("No configured active overrides"), - }; - let css_path: PathBuf = [NAME, THEME_DIR].iter().collect(); - let css_dirs = directories::ProjectDirs::from_path(PathBuf::from(css_path)) - .context("Failed to find project directory.")?; - let active_theme_path = match css_dirs.find_data_file(format!("{active}.ron")) { - Some(p) => p, - _ => anyhow::bail!("Could not find theme"), - }; - match File::open(active_theme_path) { - Ok(active_theme_file) => { - let reader = BufReader::new(active_theme_file); - Ok(ron::de::from_reader::<_, Theme>(reader)?) - } - Err(_) => { - if self.is_dark { - Ok(Theme::dark_default()) - } else { - Ok(Theme::light_default()) - } - } - } - } - - /// set the name of the active light theme - pub fn set_active_light(new: &str) -> Result<()> { - let mut self_ = Self::load()?; - - self_.light = new.to_string(); - - self_.save() - } - - /// set the name of the active dark theme - pub fn set_active_dark(new: &str) -> Result<()> { - let mut self_ = Self::load()?; - - self_.dark = new.to_string(); - - self_.save() - } -} - -impl From<(Theme, Theme)> for Config -where - C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, -{ - fn from((light, dark): (Theme, Theme)) -> Self { - Self { - light: light.name, - dark: dark.name, - is_dark: true, - is_high_contrast: false, - } - } -} - -impl From> for Config -where - C: Clone + fmt::Debug + Default + Into + From + Serialize + DeserializeOwned, -{ - fn from(t: Theme) -> Self { - Self { - light: t.clone().name, - dark: t.name, - is_dark: true, - is_high_contrast: true, - } - } -} diff --git a/cosmic-theme/src/hex_color.rs b/cosmic-theme/src/hex_color.rs deleted file mode 100644 index bf04f21..0000000 --- a/cosmic-theme/src/hex_color.rs +++ /dev/null @@ -1,35 +0,0 @@ -use hex::encode; -use palette::{Pixel, Srgba}; -use std::fmt; - -/// Wrapper type for Hex color strings -#[derive(Debug, Clone)] -pub struct Hex { - hex_string: String, -} - -impl> From for Hex { - fn from(c: C) -> Self { - let srgba: Srgba = c.into(); - let hex_string = encode::<[u8; 4]>(Srgba::into_raw(srgba.into_format())); - Hex { hex_string } - } -} - -impl Into for Hex { - fn into(self) -> String { - self.hex_string - } -} - -impl fmt::Display for Hex { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "#{}", self) - } -} - -/// Create a hex String from an Srgba -pub fn hex_from_rgba(rgba: &Srgba) -> String { - let hex = encode::<[u8; 4]>(Srgba::into_raw(rgba.into_format())); - format!("#{hex}") -} diff --git a/cosmic-theme/src/image.rs b/cosmic-theme/src/image.rs new file mode 100644 index 0000000..52a319e --- /dev/null +++ b/cosmic-theme/src/image.rs @@ -0,0 +1 @@ +// TODO theme from image diff --git a/cosmic-theme/src/lib.rs b/cosmic-theme/src/lib.rs index efa4025..28e6e8a 100644 --- a/cosmic-theme/src/lib.rs +++ b/cosmic-theme/src/lib.rs @@ -6,22 +6,16 @@ //! Provides utilities for creating custom cosmic themes. //! -#[cfg(feature = "contrast-derivation")] -pub use color_picker::*; -pub use config::*; -#[cfg(feature = "hex-color")] -pub use hex_color::*; pub use model::*; pub use output::*; -pub use theme_provider::*; -#[cfg(feature = "contrast-derivation")] -mod color_picker; -mod config; -#[cfg(feature = "hex-color")] -mod hex_color; + mod model; mod output; -mod theme_provider; + +/// composite colors in srgb +pub mod composite; +/// get color steps +pub mod steps; /// utilities pub mod util; @@ -33,47 +27,3 @@ pub const THEME_DIR: &str = "themes"; pub const PALETTE_DIR: &str = "palettes"; pub use palette; - -/// theme derivation from an image -#[cfg(feature = "theme-from-image")] -pub mod theme_from_image { - use image::EncodableLayout; - use kmeans_colors::{get_kmeans_hamerly, Kmeans, Sort}; - use palette::{rgb::Srgba, Pixel}; - use palette::{IntoColor, Lab}; - use std::path::Path; - - /// Create a palette from an image - /// The palette is sorted by how often a color occurs in the image, most often first - pub fn theme_from_image>(path: P) -> Option> { - // calculate kmeans colors from file - // let pixbuf = Pixbuf::from_file(path); - let img = image::open(path); - match img { - Ok(img) => { - let lab: Vec = Srgba::from_raw_slice(img.to_rgba8().into_raw().as_bytes()) - .iter() - .map(|x| x.color.into_format().into_color()) - .collect(); - - let mut result = Kmeans::new(); - - // TODO random seed - for i in 0..2 { - let run_result = get_kmeans_hamerly(5, 20, 5.0, false, &lab, i as u64); - if run_result.score < result.score { - result = run_result; - } - } - let mut res = Lab::sort_indexed_colors(&result.centroids, &result.indices); - res.sort_unstable_by(|a, b| (b.percentage).partial_cmp(&a.percentage).unwrap()); - let colors: Vec = res.iter().map(|x| x.centroid.into_color()).collect(); - Some(colors) - } - Err(err) => { - eprintln!("{}", err); - None - } - } - } -} diff --git a/cosmic-theme/src/model/constraint.rs b/cosmic-theme/src/model/constraint.rs deleted file mode 100644 index 4513249..0000000 --- a/cosmic-theme/src/model/constraint.rs +++ /dev/null @@ -1,26 +0,0 @@ -/// Cosmic theme custom constraints which are used to pick colors -#[derive(Copy, Clone, Debug)] -pub struct ThemeConstraints { - /// requested contrast ratio for elevated surfaces - pub elevated_contrast_ratio: f32, - /// requested contrast ratio for dividers - pub divider_contrast_ratio: f32, - /// requested contrast ratio for text - pub text_contrast_ratio: f32, - /// gray scale or color for dividers - pub divider_gray_scale: bool, - /// elevated surfaces are lightened or darkened - pub lighten: bool, -} - -impl Default for ThemeConstraints { - fn default() -> Self { - Self { - elevated_contrast_ratio: 1.1, - divider_contrast_ratio: 1.51, - text_contrast_ratio: 7.0, - divider_gray_scale: true, - lighten: true, - } - } -} diff --git a/cosmic-theme/src/model/derivation.rs b/cosmic-theme/src/model/derivation.rs index 9c186c9..e626500 100644 --- a/cosmic-theme/src/model/derivation.rs +++ b/cosmic-theme/src/model/derivation.rs @@ -2,7 +2,7 @@ use palette::Srgba; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::fmt; -use crate::{util::over, CosmicPalette}; +use crate::{composite::over, CosmicPalette}; /// Theme Container colors of a theme, can be a theme background container, primary container, or secondary container #[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] diff --git a/cosmic-theme/src/model/mod.rs b/cosmic-theme/src/model/mod.rs index 684df0b..5e82c76 100644 --- a/cosmic-theme/src/model/mod.rs +++ b/cosmic-theme/src/model/mod.rs @@ -1,14 +1,7 @@ -#[cfg(feature = "contrast-derivation")] -pub use constraint::*; pub use cosmic_palette::*; pub use derivation::*; -#[cfg(feature = "contrast-derivation")] -pub use selection::*; pub use theme::*; -#[cfg(feature = "contrast-derivation")] -mod constraint; + mod cosmic_palette; mod derivation; -#[cfg(feature = "contrast-derivation")] -mod selection; mod theme; diff --git a/cosmic-theme/src/model/selection.rs b/cosmic-theme/src/model/selection.rs deleted file mode 100644 index a4120c4..0000000 --- a/cosmic-theme/src/model/selection.rs +++ /dev/null @@ -1,99 +0,0 @@ -use palette::{named, IntoColor, Lch, Srgba}; -use std::convert::TryFrom; - -/// A Selection is a group of colors from which a cosmic palette can be derived -#[derive(Copy, Clone, Debug, Default)] -pub struct Selection { - /// base background container color - pub background: C, - /// base primary container color - pub primary_container: C, - /// base secondary container color - pub secondary_container: C, - /// base accent color - pub accent: C, - /// custom accent color (overrides base) - pub accent_fg: Option, - /// custom accent nav handle text color (overrides base) - pub accent_nav_handle_fg: Option, - /// base destructive element color - pub destructive: C, - /// base destructive element color - pub warning: C, - /// base destructive element color - pub success: C, -} - -// vector should be in order of most common -impl TryFrom> for Selection -where - C: Clone + From, -{ - type Error = anyhow::Error; - - fn try_from(mut colors: Vec) -> Result { - if colors.len() < 8 { - anyhow::bail!("length of inputted vector must be at least 8.") - } else { - let lch_colors: Vec = colors - .iter() - .map(|x| { - let srgba: Srgba = x.clone().into(); - srgba.color.into_format().into_color() - }) - .collect(); - - let red_lch: Lch = named::CRIMSON.into_format().into_color(); - let mut reddest_i = 1; - for (i, c) in lch_colors[1..].iter().enumerate() { - let d_cur = (c.hue.to_degrees() - red_lch.hue.to_degrees()).abs(); - let reddest_d = (lch_colors[reddest_i].hue.to_degrees().abs() - - red_lch.hue.to_degrees().abs()) - .abs(); - if d_cur < reddest_d { - reddest_i = i; - } - } - - let yellow_lch: Lch = named::YELLOW.into_format().into_color(); - let mut yellow_i = 1; - for (i, c) in lch_colors[1..].iter().enumerate() { - let d_cur = (c.hue.to_degrees() - yellow_lch.hue.to_degrees()).abs(); - let reddest_d = (lch_colors[yellow_i].hue.to_degrees().abs() - - yellow_lch.hue.to_degrees().abs()) - .abs(); - if d_cur < reddest_d { - yellow_i = i; - } - } - - let green_lch: Lch = named::GREEN.into_format().into_color(); - let mut green_i = 1; - for (i, c) in lch_colors[1..].iter().enumerate() { - let d_cur = (c.hue.to_degrees() - green_lch.hue.to_degrees()).abs(); - let reddest_d = (lch_colors[green_i].hue.to_degrees().abs() - - green_lch.hue.to_degrees().abs()) - .abs(); - if d_cur < reddest_d { - green_i = i; - } - } - - let red = colors.remove(reddest_i); - let green = colors.remove(green_i); - let yellow = colors.remove(yellow_i); - - Ok(Self { - background: colors[0].into(), - primary_container: colors[1].into(), - secondary_container: colors[3].into(), - accent: colors[2].into(), - accent_fg: Some(colors[2].into()), - accent_nav_handle_fg: Some(colors[2].into()), - destructive: red.into(), - warning: yellow.into(), - success: green.into(), - }) - } - } -} diff --git a/cosmic-theme/src/output/gtk4_output.rs b/cosmic-theme/src/output/gtk4_output.rs index 43fb498..4472f6e 100644 --- a/cosmic-theme/src/output/gtk4_output.rs +++ b/cosmic-theme/src/output/gtk4_output.rs @@ -10,7 +10,7 @@ use std::{fmt, fs::File, io::prelude::*, path::PathBuf}; pub(crate) const CSS_DIR: &'static str = "css"; pub(crate) const THEME_DIR: &'static str = "themes"; -/// Trait for outputting the Theme as Gtk4CSS +/// Trait for outputting the Theme variables as Gtk4CSS pub trait Gtk4Output { /// turn the theme into css fn as_css(&self) -> String; diff --git a/cosmic-theme/src/steps.rs b/cosmic-theme/src/steps.rs new file mode 100644 index 0000000..5e2f635 --- /dev/null +++ b/cosmic-theme/src/steps.rs @@ -0,0 +1,137 @@ +use almost::equal; +use palette::{convert::FromColorUnclamped, ClampAssign, Oklch, Srgb}; + +/// Get an array of 100 colors with a specific hue and chroma +/// over the full range of lightness. +/// Colors which are not valid Srgb will fallback to a color with the nearest valid chroma. +pub fn steps(mut c: Oklch) -> [Srgb; 100] { + let mut steps = [Srgb::new(0.0, 0.0, 0.0); 100]; + + for i in 0..steps.len() { + let lightness = i as f32 / 100.0; + c.l = lightness; + steps[i] = oklch_to_srgba_nearest_chroma(c) + } + + steps +} + +/// find the nearest chroma which makes our color a valid color in Srgb +pub fn oklch_to_srgba_nearest_chroma(mut c: Oklch) -> Srgb { + let mut r_chroma = c.chroma; + let mut l_chroma = 0.0; + // exit early if we found it right away + let mut new_c = Srgb::from_color_unclamped(c); + + if is_valid_srgb(new_c) { + new_c.clamp_assign(); + return new_c; + } + + // is this an excessive depth to search? + for _ in 0..64 { + let new_c = Srgb::from_color_unclamped(c); + if is_valid_srgb(new_c) { + l_chroma = c.chroma; + c.chroma = (c.chroma + r_chroma) / 2.0; + } else { + r_chroma = c.chroma; + c.chroma = (c.chroma + l_chroma) / 2.0; + } + } + Srgb::from_color_unclamped(c) +} + +/// checks that the color is valid srgb +pub fn is_valid_srgb(c: Srgb) -> bool { + (equal(c.red, Srgb::max_red()) || (c.red >= Srgb::min_red() && c.red <= Srgb::max_red())) + && (equal(c.blue, Srgb::max_blue()) + || (c.blue >= Srgb::min_blue() && c.blue <= Srgb::max_blue())) + && (equal(c.green, Srgb::max_green()) + || (c.green >= Srgb::min_green() && c.green <= Srgb::max_green())) +} + +#[cfg(test)] +mod tests { + use almost::equal; + use palette::{OklabHue, Srgb}; + + use super::{is_valid_srgb, oklch_to_srgba_nearest_chroma}; + + #[test] + fn test_valid_check() { + assert!(is_valid_srgb(Srgb::new(1.0, 1.0, 1.0))); + assert!(is_valid_srgb(Srgb::new(0.0, 0.0, 0.0))); + assert!(is_valid_srgb(Srgb::new(0.5, 0.5, 0.5))); + assert!(!is_valid_srgb(Srgb::new(-0.1, 0.0, 0.0))); + assert!(!is_valid_srgb(Srgb::new(0.0, -0.1, 0.0))); + assert!(!is_valid_srgb(Srgb::new(-0.0, 0.0, -0.1))); + assert!(!is_valid_srgb(Srgb::new(-100.1, 0.0, 0.0))); + assert!(!is_valid_srgb(Srgb::new(0.0, -100.1, 0.0))); + assert!(!is_valid_srgb(Srgb::new(-0.0, 0.0, -100.1))); + assert!(!is_valid_srgb(Srgb::new(1.1, 0.0, 0.0))); + assert!(!is_valid_srgb(Srgb::new(0.0, 1.1, 0.0))); + assert!(!is_valid_srgb(Srgb::new(-0.0, 0.0, 1.1))); + assert!(!is_valid_srgb(Srgb::new(100.1, 0.0, 0.0))); + assert!(!is_valid_srgb(Srgb::new(0.0, 100.1, 0.0))); + assert!(!is_valid_srgb(Srgb::new(-0.0, 0.0, 100.1))); + } + + #[test] + fn test_conversion_boundaries() { + let c1 = palette::Oklch::new(0.0, 0.288, OklabHue::from_degrees(0.0)); + let srgb = oklch_to_srgba_nearest_chroma(c1); + equal(srgb.red, 0.0); + equal(srgb.blue, 0.0); + equal(srgb.green, 0.0); + + let c1 = palette::Oklch::new(1.0, 0.288, OklabHue::from_degrees(0.0)); + let srgb = oklch_to_srgba_nearest_chroma(c1); + + equal(srgb.red, 1.0); + equal(srgb.blue, 1.0); + equal(srgb.green, 1.0); + } + + #[test] + fn test_conversion_colors() { + let c1 = palette::Oklch::new(0.4608, 0.11111, OklabHue::new(57.31)); + let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); + assert!(srgb.red == 133); + assert!(srgb.green == 69); + assert!(srgb.blue == 0); + + let c1 = palette::Oklch::new(0.30, 0.08, OklabHue::new(35.0)); + let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); + assert!(srgb.red == 78); + assert!(srgb.green == 27); + assert!(srgb.blue == 15); + + let c1 = palette::Oklch::new(0.757, 0.146, OklabHue::new(301.2)); + let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); + assert!(srgb.red == 192); + assert!(srgb.green == 153); + assert!(srgb.blue == 253); + } + + #[test] + fn test_conversion_fallback_colors() { + let c1 = palette::Oklch::new(0.70, 0.284, OklabHue::new(35.0)); + let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); + assert!(srgb.red == 255); + assert!(srgb.green == 103); + assert!(srgb.blue == 65); + + let c1 = palette::Oklch::new(0.757, 0.239, OklabHue::new(301.2)); + let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); + assert!(srgb.red == 193); + assert!(srgb.green == 152); + assert!(srgb.blue == 255); + + let c1 = palette::Oklch::new(0.163, 0.333, OklabHue::new(141.0)); + let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); + assert!(srgb.red == 1); + assert!(srgb.green == 19); + assert!(srgb.blue == 0); + } +} diff --git a/cosmic-theme/src/theme_provider/mod.rs b/cosmic-theme/src/theme_provider/mod.rs deleted file mode 100644 index 8b13789..0000000 --- a/cosmic-theme/src/theme_provider/mod.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/cosmic-theme/src/util.rs b/cosmic-theme/src/util.rs index bb264c8..762017d 100644 --- a/cosmic-theme/src/util.rs +++ b/cosmic-theme/src/util.rs @@ -31,29 +31,3 @@ impl Into for CssColor { ) } } - -/// straight alpha "A over B" operator on non-linear srgba -pub fn over, B: Into>(a: A, b: B) -> Srgba { - let a = a.into(); - let b = b.into(); - let o_a = (alpha_over(a.alpha, b.alpha)).max(0.0).min(1.0); - let o_r = (c_over(a.red, b.red, a.alpha, b.alpha, o_a)) - .max(0.0) - .min(1.0); - let o_g = (c_over(a.green, b.green, a.alpha, b.alpha, o_a)) - .max(0.0) - .min(1.0); - let o_b = (c_over(a.blue, b.blue, a.alpha, b.alpha, o_a)) - .max(0.0) - .min(1.0); - - Srgba::new(o_r, o_g, o_b, o_a) -} - -fn alpha_over(a: f32, b: f32) -> f32 { - a + b * (1.0 - a) -} - -fn c_over(a: f32, b: f32, a_alpha: f32, b_alpha: f32, o_alpha: f32) -> f32 { - a * a_alpha + b * b_alpha * (1.0 - a_alpha) / o_alpha -}