wip: theme update & some cleanup
This commit is contained in:
parent
54d47a1b38
commit
620c1adb74
17 changed files with 181 additions and 905 deletions
|
|
@ -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"] }
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
27
cosmic-theme/src/composite.rs
Normal file
27
cosmic-theme/src/composite.rs
Normal 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
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}")
|
||||
}
|
||||
1
cosmic-theme/src/image.rs
Normal file
1
cosmic-theme/src/image.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
// TODO theme from image
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
137
cosmic-theme/src/steps.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue