wip: theme update & some cleanup

This commit is contained in:
Ashley Wulber 2023-08-03 15:03:23 -04:00 committed by Ashley Wulber
parent 54d47a1b38
commit 620c1adb74
17 changed files with 181 additions and 905 deletions

View file

@ -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"] }

View file

@ -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"

View file

@ -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<C> {
selection: Selection<C>,
constraints: ThemeConstraints,
}
impl<C> Exact<C>
where
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
{
/// create a new Exact color picker
pub fn new(selection: Selection<C>, constraints: ThemeConstraints) -> Self {
Self {
selection,
constraints,
}
}
}
impl<C> ColorPicker<C> for Exact<C>
where
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
{
fn get_constraints(&self) -> ThemeConstraints {
self.constraints
}
fn get_selection(&self) -> Selection<C> {
self.selection.clone()
}
fn pick_color_graphic(
&self,
color: C,
contrast: f32,
grayscale: bool,
lighten: Option<bool>,
) -> (C, Option<anyhow::Error>) {
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<bool>,
) -> (C, Option<anyhow::Error>) {
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<f32>,
grayscale: bool,
lighten: Option<bool>,
) -> Result<C> {
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()))
}
}
}
}

View file

@ -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<Srgba> + From<Srgba> + 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<f32>,
grayscale: bool,
lighten: Option<bool>,
) -> Result<C>;
/// 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<bool>,
) -> (C, Option<anyhow::Error>);
/// 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<bool>,
) -> (C, Option<anyhow::Error>);
/// get the selection for this color picker
fn get_selection(&self) -> Selection<C>;
/// 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<Theme<C>> {
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<Container<C>> {
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<Component<C>> {
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,
}
}
}

View file

@ -0,0 +1,27 @@
use palette::Srgba;
/// straight alpha "A over B" operator on non-linear srgba
pub fn over<A: Into<Srgba>, B: Into<Srgba>>(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
}

View file

@ -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<PathBuf> {
let base_dirs = directories::BaseDirs::new().context("Failed to get base directories.")?;
let res = Ok(base_dirs.create_config_directory(NAME)?);
Theme::<Srgba>::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<Self> {
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<String> {
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<Theme<CssColor>> {
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<CssColor>>(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<C> From<(Theme<C>, Theme<C>)> for Config
where
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
{
fn from((light, dark): (Theme<C>, Theme<C>)) -> Self {
Self {
light: light.name,
dark: dark.name,
is_dark: true,
is_high_contrast: false,
}
}
}
impl<C> From<Theme<C>> for Config
where
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
{
fn from(t: Theme<C>) -> Self {
Self {
light: t.clone().name,
dark: t.name,
is_dark: true,
is_high_contrast: true,
}
}
}

View file

@ -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<C: Into<Srgba>> From<C> 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<String> 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}")
}

View file

@ -0,0 +1 @@
// TODO theme from image

View file

@ -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<P: AsRef<Path>>(path: P) -> Option<Vec<Srgba>> {
// calculate kmeans colors from file
// let pixbuf = Pixbuf::from_file(path);
let img = image::open(path);
match img {
Ok(img) => {
let lab: Vec<Lab> = 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<Srgba> = res.iter().map(|x| x.centroid.into_color()).collect();
Some(colors)
}
Err(err) => {
eprintln!("{}", err);
None
}
}
}
}

View file

@ -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,
}
}
}

View file

@ -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)]

View file

@ -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;

View file

@ -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<C> {
/// 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<C>,
/// custom accent nav handle text color (overrides base)
pub accent_nav_handle_fg: Option<C>,
/// 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<C> TryFrom<Vec<Srgba>> for Selection<C>
where
C: Clone + From<Srgba>,
{
type Error = anyhow::Error;
fn try_from(mut colors: Vec<Srgba>) -> Result<Self, Self::Error> {
if colors.len() < 8 {
anyhow::bail!("length of inputted vector must be at least 8.")
} else {
let lch_colors: Vec<Lch> = 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(),
})
}
}
}

View file

@ -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;

137
cosmic-theme/src/steps.rs Normal file
View file

@ -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::<u8>();
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::<u8>();
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::<u8>();
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::<u8>();
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::<u8>();
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::<u8>();
assert!(srgb.red == 1);
assert!(srgb.green == 19);
assert!(srgb.blue == 0);
}
}

View file

@ -1 +0,0 @@

View file

@ -31,29 +31,3 @@ impl Into<Srgba> for CssColor {
)
}
}
/// straight alpha "A over B" operator on non-linear srgba
pub fn over<A: Into<Srgba>, B: Into<Srgba>>(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
}