feat: support for custom & system themes + move cosmic-theme to libcosmic
This commit is contained in:
parent
259bba4d19
commit
bf1c474d08
26 changed files with 2639 additions and 44 deletions
|
|
@ -35,7 +35,7 @@ cosmic-config = { path = "cosmic-config" }
|
|||
freedesktop-icons = "0.2.2"
|
||||
|
||||
[dependencies.cosmic-theme]
|
||||
git = "https://github.com/pop-os/cosmic-theme.git"
|
||||
path = "cosmic-theme"
|
||||
|
||||
[dependencies.iced]
|
||||
path = "iced"
|
||||
|
|
@ -78,6 +78,7 @@ optional = true
|
|||
members = [
|
||||
"cosmic-config",
|
||||
"cosmic-config-derive",
|
||||
"cosmic-theme",
|
||||
"examples/*",
|
||||
]
|
||||
exclude = [
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{self, parse_quote};
|
||||
use syn::{self};
|
||||
|
||||
#[proc_macro_derive(CosmicConfigEntry)]
|
||||
pub fn cosmic_config_entry_derive(input: TokenStream) -> TokenStream {
|
||||
|
|
@ -14,7 +14,7 @@ pub fn cosmic_config_entry_derive(input: TokenStream) -> TokenStream {
|
|||
|
||||
fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream {
|
||||
let name = &ast.ident;
|
||||
let generics = &ast.generics;
|
||||
// let generics = &ast.generics;
|
||||
|
||||
// Get the fields of the struct
|
||||
let fields = match ast.data {
|
||||
|
|
@ -43,29 +43,30 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream {
|
|||
}
|
||||
});
|
||||
|
||||
// Get the existing where clause or create a new one if it doesn't exist
|
||||
let mut where_clause = ast
|
||||
.generics
|
||||
.where_clause
|
||||
.clone()
|
||||
.unwrap_or_else(|| parse_quote!(where));
|
||||
// // Get the existing where clause or create a new one if it doesn't exist
|
||||
// let mut where_clause = ast
|
||||
// .generics
|
||||
// .where_clause
|
||||
// .clone()
|
||||
// .unwrap_or_else(|| parse_quote!(where));
|
||||
|
||||
// Add your additional constraints to the where clause
|
||||
// Here, we add the constraint 'T: Debug' to all generic parameters
|
||||
for param in ast.generics.params.iter() {
|
||||
where_clause
|
||||
.predicates
|
||||
.push(parse_quote!(#param: ::std::default::Default + ::serde::Serialize + ::serde::de::DeserializeOwned));
|
||||
}
|
||||
// // Add your additional constraints to the where clause
|
||||
// // Here, we add the constraint 'T: Debug' to all generic parameters
|
||||
// for param in ast.generics.params.iter() {
|
||||
// where_clause
|
||||
// .predicates
|
||||
// .push(parse_quote!(#param: ::std::default::Default + ::serde::Serialize + ::serde::de::DeserializeOwned));
|
||||
// }
|
||||
|
||||
let gen = quote! {
|
||||
impl #generics CosmicConfigEntry for #name #generics #where_clause {
|
||||
impl CosmicConfigEntry for #name {
|
||||
fn write_entry(&self, config: &Config) -> Result<(), cosmic_config::Error> {
|
||||
let tx = config.transaction();
|
||||
#(#write_each_config_field)*
|
||||
Ok(())
|
||||
tx.commit()
|
||||
}
|
||||
|
||||
fn get_entry(config: &Config) -> Result<Self, (Vec<cosmic_config::Error>, Self)> {
|
||||
fn get_entry(config: &Config) -> Result<Self, (Vec<::cosmic_config::Error>, Self)> {
|
||||
let mut default = Self::default();
|
||||
let mut errors = Vec::new();
|
||||
|
||||
|
|
|
|||
32
cosmic-theme/Cargo.toml
Normal file
32
cosmic-theme/Cargo.toml
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
[package]
|
||||
name = "cosmic-theme"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["test_all_features"]
|
||||
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"]
|
||||
|
||||
[dependencies]
|
||||
palette = {version = "0.6", 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"
|
||||
csscolorparser = {version = "0.6.2", features = ["serde"]}
|
||||
directories = { git = "https://github.com/edfloreshz/directories-rs", version = "4.0.1" }
|
||||
cosmic-config = { path = "../cosmic-config/", default-features = false, features = ["subscription"] }
|
||||
|
||||
1
cosmic-theme/README.md
Normal file
1
cosmic-theme/README.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
# WIP
|
||||
170
cosmic-theme/src/color_picker/exact.rs
Normal file
170
cosmic-theme/src/color_picker/exact.rs
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
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()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
280
cosmic-theme/src/color_picker/mod.rs
Normal file
280
cosmic-theme/src/color_picker/mod.rs
Normal file
|
|
@ -0,0 +1,280 @@
|
|||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
196
cosmic-theme/src/config/mod.rs
Normal file
196
cosmic-theme/src/config/mod.rs
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
35
cosmic-theme/src/hex_color.rs
Normal file
35
cosmic-theme/src/hex_color.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
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}")
|
||||
}
|
||||
77
cosmic-theme/src/lib.rs
Normal file
77
cosmic-theme/src/lib.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
|
||||
#![warn(missing_docs, missing_debug_implementations, rust_2018_idioms)]
|
||||
|
||||
//! Cosmic theme library.
|
||||
//!
|
||||
//! 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;
|
||||
/// utilities
|
||||
pub mod util;
|
||||
|
||||
/// name of cosmic theme
|
||||
pub const NAME: &'static str = "com.system76.CosmicTheme";
|
||||
/// Name of the theme directory
|
||||
pub const THEME_DIR: &str = "themes";
|
||||
/// name of the palette directory
|
||||
pub const PALETTE_DIR: &str = "palettes";
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
26
cosmic-theme/src/model/constraint.rs
Normal file
26
cosmic-theme/src/model/constraint.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
252
cosmic-theme/src/model/cosmic_palette.rs
Normal file
252
cosmic-theme/src/model/cosmic_palette.rs
Normal file
|
|
@ -0,0 +1,252 @@
|
|||
use std::{
|
||||
fmt,
|
||||
fs::File,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use directories::{BaseDirsExt, ProjectDirsExt};
|
||||
use lazy_static::lazy_static;
|
||||
use palette::Srgba;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
|
||||
use crate::{util::CssColor, NAME, PALETTE_DIR};
|
||||
|
||||
lazy_static! {
|
||||
/// built in light palette
|
||||
pub static ref LIGHT_PALETTE: CosmicPalette<CssColor> =
|
||||
ron::from_str(include_str!("light.ron")).unwrap();
|
||||
/// built in dark palette
|
||||
pub static ref DARK_PALETTE: CosmicPalette<CssColor> =
|
||||
ron::from_str(include_str!("dark.ron")).unwrap();
|
||||
}
|
||||
|
||||
/// Palette type
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub enum CosmicPalette<C> {
|
||||
/// Dark mode
|
||||
Dark(CosmicPaletteInner<C>),
|
||||
/// Light mode
|
||||
Light(CosmicPaletteInner<C>),
|
||||
/// High contrast light mode
|
||||
HighContrastLight(CosmicPaletteInner<C>),
|
||||
/// High contrast dark mode
|
||||
HighContrastDark(CosmicPaletteInner<C>),
|
||||
}
|
||||
|
||||
impl<C> AsRef<CosmicPaletteInner<C>> for CosmicPalette<C>
|
||||
where
|
||||
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
|
||||
{
|
||||
fn as_ref(&self) -> &CosmicPaletteInner<C> {
|
||||
match self {
|
||||
CosmicPalette::Dark(p) => p,
|
||||
CosmicPalette::Light(p) => p,
|
||||
CosmicPalette::HighContrastLight(p) => p,
|
||||
CosmicPalette::HighContrastDark(p) => p,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> CosmicPalette<C>
|
||||
where
|
||||
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
|
||||
{
|
||||
/// check if the palette is dark
|
||||
pub fn is_dark(&self) -> bool {
|
||||
match self {
|
||||
CosmicPalette::Dark(_) | CosmicPalette::HighContrastDark(_) => true,
|
||||
CosmicPalette::Light(_) | CosmicPalette::HighContrastLight(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// check if the palette is high_contrast
|
||||
pub fn is_high_contrast(&self) -> bool {
|
||||
match self {
|
||||
CosmicPalette::HighContrastLight(_) | CosmicPalette::HighContrastDark(_) => true,
|
||||
CosmicPalette::Light(_) | CosmicPalette::Dark(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Default for CosmicPalette<C>
|
||||
where
|
||||
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
|
||||
{
|
||||
fn default() -> Self {
|
||||
CosmicPalette::Dark(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
/// The palette for Cosmic Theme, from which all color properties are derived
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct CosmicPaletteInner<C> {
|
||||
/// name of the palette
|
||||
pub name: String,
|
||||
|
||||
/// basic palette
|
||||
/// blue: colors used for various points of emphasis in the UI
|
||||
pub blue: C,
|
||||
/// red: colors used for various points of emphasis in the UI
|
||||
pub red: C,
|
||||
/// green: colors used for various points of emphasis in the UI
|
||||
pub green: C,
|
||||
/// yellow: colors used for various points of emphasis in the UI
|
||||
pub yellow: C,
|
||||
|
||||
/// surface grays
|
||||
/// colors used for three levels of surfaces in the UI
|
||||
pub gray_1: C,
|
||||
/// colors used for three levels of surfaces in the UI
|
||||
pub gray_2: C,
|
||||
/// colors used for three levels of surfaces in the UI
|
||||
pub gray_3: C,
|
||||
|
||||
/// System Neutrals
|
||||
/// A wider spread of dark colors for more general use.
|
||||
pub neutral_1: C,
|
||||
/// A wider spread of dark colors for more general use.
|
||||
pub neutral_2: C,
|
||||
/// A wider spread of dark colors for more general use.
|
||||
pub neutral_3: C,
|
||||
/// A wider spread of dark colors for more general use.
|
||||
pub neutral_4: C,
|
||||
/// A wider spread of dark colors for more general use.
|
||||
pub neutral_5: C,
|
||||
/// A wider spread of dark colors for more general use.
|
||||
pub neutral_6: C,
|
||||
/// A wider spread of dark colors for more general use.
|
||||
pub neutral_7: C,
|
||||
/// A wider spread of dark colors for more general use.
|
||||
pub neutral_8: C,
|
||||
/// A wider spread of dark colors for more general use.
|
||||
pub neutral_9: C,
|
||||
/// A wider spread of dark colors for more general use.
|
||||
pub neutral_10: C,
|
||||
|
||||
/// Extended Color Palette
|
||||
/// Colors used for themes, app icons, illustrations, and other brand purposes.
|
||||
pub ext_warm_grey: C,
|
||||
/// Colors used for themes, app icons, illustrations, and other brand purposes.
|
||||
pub ext_orange: C,
|
||||
/// Colors used for themes, app icons, illustrations, and other brand purposes.
|
||||
pub ext_yellow: C,
|
||||
/// Colors used for themes, app icons, illustrations, and other brand purposes.
|
||||
pub ext_blue: C,
|
||||
/// Colors used for themes, app icons, illustrations, and other brand purposes.
|
||||
pub ext_purple: C,
|
||||
/// Colors used for themes, app icons, illustrations, and other brand purposes.
|
||||
pub ext_pink: C,
|
||||
/// Colors used for themes, app icons, illustrations, and other brand purposes.
|
||||
pub ext_indigo: C,
|
||||
|
||||
/// Potential Accent Color Combos
|
||||
pub accent_warm_grey: C,
|
||||
/// Potential Accent Color Combos
|
||||
pub accent_orange: C,
|
||||
/// Potential Accent Color Combos
|
||||
pub accent_yellow: C,
|
||||
/// Potential Accent Color Combos
|
||||
pub accent_purple: C,
|
||||
/// Potential Accent Color Combos
|
||||
pub accent_pink: C,
|
||||
/// Potential Accent Color Combos
|
||||
pub accent_indigo: C,
|
||||
}
|
||||
|
||||
impl From<CosmicPaletteInner<CssColor>> for CosmicPaletteInner<Srgba> {
|
||||
fn from(p: CosmicPaletteInner<CssColor>) -> Self {
|
||||
CosmicPaletteInner {
|
||||
name: p.name,
|
||||
blue: p.blue.into(),
|
||||
red: p.red.into(),
|
||||
green: p.green.into(),
|
||||
yellow: p.yellow.into(),
|
||||
gray_1: p.gray_1.into(),
|
||||
gray_2: p.gray_2.into(),
|
||||
gray_3: p.gray_3.into(),
|
||||
neutral_1: p.neutral_1.into(),
|
||||
neutral_2: p.neutral_2.into(),
|
||||
neutral_3: p.neutral_3.into(),
|
||||
neutral_4: p.neutral_4.into(),
|
||||
neutral_5: p.neutral_5.into(),
|
||||
neutral_6: p.neutral_6.into(),
|
||||
neutral_7: p.neutral_7.into(),
|
||||
neutral_8: p.neutral_8.into(),
|
||||
neutral_9: p.neutral_9.into(),
|
||||
neutral_10: p.neutral_10.into(),
|
||||
ext_warm_grey: p.ext_warm_grey.into(),
|
||||
ext_orange: p.ext_orange.into(),
|
||||
ext_yellow: p.ext_yellow.into(),
|
||||
ext_blue: p.ext_blue.into(),
|
||||
ext_purple: p.ext_purple.into(),
|
||||
ext_pink: p.ext_pink.into(),
|
||||
ext_indigo: p.ext_indigo.into(),
|
||||
accent_warm_grey: p.accent_warm_grey.into(),
|
||||
accent_orange: p.accent_orange.into(),
|
||||
accent_yellow: p.accent_yellow.into(),
|
||||
accent_purple: p.accent_purple.into(),
|
||||
accent_pink: p.accent_pink.into(),
|
||||
accent_indigo: p.accent_indigo.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> CosmicPalette<C>
|
||||
where
|
||||
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
|
||||
{
|
||||
/// name of the palette
|
||||
pub fn name(&self) -> &str {
|
||||
match &self {
|
||||
CosmicPalette::Dark(p) => &p.name,
|
||||
CosmicPalette::Light(p) => &p.name,
|
||||
CosmicPalette::HighContrastLight(p) => &p.name,
|
||||
CosmicPalette::HighContrastDark(p) => &p.name,
|
||||
}
|
||||
}
|
||||
/// save the theme to the theme directory
|
||||
pub fn save(&self) -> anyhow::Result<()> {
|
||||
let ron_path: PathBuf = [NAME, PALETTE_DIR].iter().collect();
|
||||
let ron_dirs = directories::ProjectDirs::from_path(ron_path)
|
||||
.context("Failed to get project directories.")?;
|
||||
let ron_name = format!("{}.ron", self.name());
|
||||
|
||||
if let Ok(p) = ron_dirs.place_config_file(ron_name) {
|
||||
let mut f = File::create(p)?;
|
||||
f.write_all(ron::ser::to_string_pretty(self, Default::default())?.as_bytes())?;
|
||||
} else {
|
||||
anyhow::bail!("Failed to write RON theme.");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// init the theme directory
|
||||
pub fn init() -> anyhow::Result<PathBuf> {
|
||||
let ron_path: PathBuf = [NAME, PALETTE_DIR].iter().collect();
|
||||
let base_dirs = directories::BaseDirs::new().context("Failed to get base directories.")?;
|
||||
Ok(base_dirs.create_config_directory(ron_path)?)
|
||||
}
|
||||
|
||||
/// load a theme by name
|
||||
pub fn load_from_name(name: &str) -> anyhow::Result<Self> {
|
||||
let ron_path: PathBuf = [NAME, PALETTE_DIR].iter().collect();
|
||||
let ron_dirs = directories::ProjectDirs::from_path(ron_path)
|
||||
.context("Failed to get project directories.")?;
|
||||
|
||||
let ron_name = format!("{}.ron", name);
|
||||
if let Some(p) = ron_dirs.find_config_file(ron_name) {
|
||||
let f = File::open(p)?;
|
||||
Ok(ron::de::from_reader(f)?)
|
||||
} else {
|
||||
anyhow::bail!("Failed to write RON theme.");
|
||||
}
|
||||
}
|
||||
|
||||
/// load a theme by path
|
||||
pub fn load(p: &dyn AsRef<Path>) -> anyhow::Result<Self> {
|
||||
let f = File::open(p)?;
|
||||
Ok(ron::de::from_reader(f)?)
|
||||
}
|
||||
}
|
||||
95
cosmic-theme/src/model/dark.ron
Normal file
95
cosmic-theme/src/model/dark.ron
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
Dark (
|
||||
(
|
||||
name: "cosmic-dark",
|
||||
blue: (
|
||||
c: "#94EBEB",
|
||||
),
|
||||
red: (
|
||||
c: "#FFB5B5",
|
||||
),
|
||||
green: (
|
||||
c: "#ACF7D2",
|
||||
),
|
||||
yellow: (
|
||||
c: "#FFF19E",
|
||||
),
|
||||
gray_1: (
|
||||
c: "#1E1E1E",
|
||||
),
|
||||
gray_2: (
|
||||
c: "#292929",
|
||||
),
|
||||
gray_3: (
|
||||
c: "#2E2E2E",
|
||||
),
|
||||
neutral_1: (
|
||||
c: "#000000",
|
||||
),
|
||||
neutral_2: (
|
||||
c: "#272727",
|
||||
),
|
||||
neutral_3: (
|
||||
c: "#424242",
|
||||
),
|
||||
neutral_4: (
|
||||
c: "#5D5D5D",
|
||||
),
|
||||
neutral_5: (
|
||||
c: "#787878",
|
||||
),
|
||||
neutral_6: (
|
||||
c: "#939393",
|
||||
),
|
||||
neutral_7: (
|
||||
c: "#AEAEAE",
|
||||
),
|
||||
neutral_8: (
|
||||
c: "#C9C9C9",
|
||||
),
|
||||
neutral_9: (
|
||||
c: "#E4E4E4",
|
||||
),
|
||||
neutral_10: (
|
||||
c: "#FFFFFF",
|
||||
),
|
||||
ext_warm_grey: (
|
||||
c: "#9B8E8A",
|
||||
),
|
||||
ext_orange: (
|
||||
c: "#FFAD00",
|
||||
),
|
||||
ext_yellow: (
|
||||
c: "#FEDB40",
|
||||
),
|
||||
ext_blue: (
|
||||
c: "#48B9C7",
|
||||
),
|
||||
ext_purple: (
|
||||
c: "#CF7DFF",
|
||||
),
|
||||
ext_pink: (
|
||||
c: "#F93A83",
|
||||
),
|
||||
ext_indigo: (
|
||||
c: "#3E88FF",
|
||||
),
|
||||
accent_warm_grey: (
|
||||
c: "#554742",
|
||||
),
|
||||
accent_orange: (
|
||||
c: "#AF5C02",
|
||||
),
|
||||
accent_yellow: (
|
||||
c: "#966800",
|
||||
),
|
||||
accent_purple: (
|
||||
c: "#813FFF",
|
||||
),
|
||||
accent_pink: (
|
||||
c: "#F93A83",
|
||||
),
|
||||
accent_indigo: (
|
||||
c: "#3E88FF",
|
||||
),
|
||||
)
|
||||
)
|
||||
480
cosmic-theme/src/model/derivation.rs
Normal file
480
cosmic-theme/src/model/derivation.rs
Normal file
|
|
@ -0,0 +1,480 @@
|
|||
use palette::Srgba;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
use crate::{util::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)]
|
||||
pub struct Container<C> {
|
||||
/// the color of the container
|
||||
pub base: C,
|
||||
/// the color of components in the container
|
||||
pub component: Component<C>,
|
||||
/// the color of dividers in the container
|
||||
pub divider: C,
|
||||
/// the color of text in the container
|
||||
pub on: C,
|
||||
}
|
||||
|
||||
impl<C> Container<C>
|
||||
where
|
||||
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
|
||||
{
|
||||
/// convert to srgba
|
||||
pub fn into_srgba(self) -> Container<Srgba> {
|
||||
Container {
|
||||
base: self.base.into(),
|
||||
component: self.component.into_srgba(),
|
||||
divider: self.divider.into(),
|
||||
on: self.on.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new(
|
||||
palette: CosmicPalette<C>,
|
||||
container_type: ComponentType,
|
||||
bg: C,
|
||||
on_bg: C,
|
||||
) -> Self {
|
||||
let mut divider_c: Srgba = on_bg.clone().into();
|
||||
divider_c.alpha = 0.2;
|
||||
|
||||
let divider = over(divider_c.clone(), bg.clone());
|
||||
Self {
|
||||
base: bg,
|
||||
component: (palette, container_type).into(),
|
||||
divider: divider.into(),
|
||||
on: on_bg,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> From<(CosmicPalette<C>, ContainerType)> for Container<C>
|
||||
where
|
||||
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
|
||||
{
|
||||
fn from((p, t): (CosmicPalette<C>, ContainerType)) -> Self {
|
||||
match (p, t) {
|
||||
(CosmicPalette::Dark(p), ContainerType::Background) => Self::new(
|
||||
CosmicPalette::Dark(p.clone()),
|
||||
ComponentType::Background,
|
||||
p.gray_1.clone(),
|
||||
p.neutral_7.clone(),
|
||||
),
|
||||
(CosmicPalette::Dark(p), ContainerType::Primary) => Self::new(
|
||||
CosmicPalette::Dark(p.clone()),
|
||||
ComponentType::Primary,
|
||||
p.gray_2.clone(),
|
||||
p.neutral_8.clone(),
|
||||
),
|
||||
(CosmicPalette::Dark(p), ContainerType::Secondary) => Self::new(
|
||||
CosmicPalette::Dark(p.clone()),
|
||||
ComponentType::Secondary,
|
||||
p.gray_3.clone(),
|
||||
p.neutral_8.clone(),
|
||||
),
|
||||
(CosmicPalette::HighContrastDark(p), ContainerType::Background) => Self::new(
|
||||
CosmicPalette::HighContrastDark(p.clone()),
|
||||
ComponentType::Background,
|
||||
p.gray_1.clone(),
|
||||
p.neutral_8.clone(),
|
||||
),
|
||||
(CosmicPalette::HighContrastDark(p), ContainerType::Primary) => Self::new(
|
||||
CosmicPalette::HighContrastDark(p.clone()),
|
||||
ComponentType::Primary,
|
||||
p.gray_2.clone(),
|
||||
p.neutral_9.clone(),
|
||||
),
|
||||
(CosmicPalette::HighContrastDark(p), ContainerType::Secondary) => Self::new(
|
||||
CosmicPalette::HighContrastDark(p.clone()),
|
||||
ComponentType::Secondary,
|
||||
p.gray_3.clone(),
|
||||
p.neutral_9.clone(),
|
||||
),
|
||||
(CosmicPalette::Light(p), ContainerType::Background) => Self::new(
|
||||
CosmicPalette::Light(p.clone()),
|
||||
ComponentType::Background,
|
||||
p.gray_1.clone(),
|
||||
p.neutral_9.clone(),
|
||||
),
|
||||
(CosmicPalette::Light(p), ContainerType::Primary) => Self::new(
|
||||
CosmicPalette::Light(p.clone()),
|
||||
ComponentType::Primary,
|
||||
p.gray_2.clone(),
|
||||
p.neutral_8.clone(),
|
||||
),
|
||||
(CosmicPalette::Light(p), ContainerType::Secondary) => Self::new(
|
||||
CosmicPalette::Light(p.clone()),
|
||||
ComponentType::Secondary,
|
||||
p.gray_3.clone(),
|
||||
p.neutral_8.clone(),
|
||||
),
|
||||
(CosmicPalette::HighContrastLight(p), ContainerType::Background) => Self::new(
|
||||
CosmicPalette::HighContrastLight(p.clone()),
|
||||
ComponentType::Background,
|
||||
p.gray_1.clone(),
|
||||
p.neutral_10.clone(),
|
||||
),
|
||||
(CosmicPalette::HighContrastLight(p), ContainerType::Primary) => Self::new(
|
||||
CosmicPalette::HighContrastLight(p.clone()),
|
||||
ComponentType::Primary,
|
||||
p.gray_2.clone(),
|
||||
p.neutral_9.clone(),
|
||||
),
|
||||
(CosmicPalette::HighContrastLight(p), ContainerType::Secondary) => Self::new(
|
||||
CosmicPalette::HighContrastLight(p.clone()),
|
||||
ComponentType::Secondary,
|
||||
p.gray_3.clone(),
|
||||
p.neutral_9.clone(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The type of the container
|
||||
#[derive(Copy, Clone, PartialEq, Debug, Deserialize, Serialize)]
|
||||
pub enum ContainerType {
|
||||
/// Background type
|
||||
Background,
|
||||
/// Primary type
|
||||
Primary,
|
||||
/// Secondary type
|
||||
Secondary,
|
||||
}
|
||||
|
||||
impl Default for ContainerType {
|
||||
fn default() -> Self {
|
||||
Self::Background
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ContainerType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
ContainerType::Background => write!(f, "Background"),
|
||||
ContainerType::Primary => write!(f, "Primary Container"),
|
||||
ContainerType::Secondary => write!(f, "Secondary Container"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The colors for a widget of the Cosmic theme
|
||||
#[derive(Clone, PartialEq, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct Component<C> {
|
||||
/// The base color of the widget
|
||||
pub base: C,
|
||||
/// The color of the widget when it is hovered
|
||||
pub hover: C,
|
||||
/// the color of the widget when it is pressed
|
||||
pub pressed: C,
|
||||
/// the color of the widget when it is selected
|
||||
pub selected: C,
|
||||
/// the color of the widget when it is selected
|
||||
pub selected_text: C,
|
||||
/// the color of the widget when it is focused
|
||||
pub focus: C,
|
||||
/// the color of dividers for this widget
|
||||
pub divider: C,
|
||||
/// the color of text for this widget
|
||||
pub on: C,
|
||||
// the color of text with opacity 80 for this widget
|
||||
// pub text_opacity_80: C,
|
||||
/// the color of the widget when it is disabled
|
||||
pub disabled: C,
|
||||
/// the color of text in the widget when it is disabled
|
||||
pub on_disabled: C,
|
||||
}
|
||||
|
||||
impl<C> Component<C>
|
||||
where
|
||||
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
|
||||
{
|
||||
/// get @hover_state_color
|
||||
pub fn hover_state_color(&self) -> Srgba {
|
||||
self.hover.clone().into()
|
||||
}
|
||||
/// get @pressed_state_color
|
||||
pub fn pressed_state_color(&self) -> Srgba {
|
||||
self.pressed.clone().into()
|
||||
}
|
||||
/// get @selected_state_color
|
||||
pub fn selected_state_color(&self) -> Srgba {
|
||||
self.selected.clone().into()
|
||||
}
|
||||
/// get @selected_state_text_color
|
||||
pub fn selected_state_text_color(&self) -> Srgba {
|
||||
self.selected_text.clone().into()
|
||||
}
|
||||
/// get @focus_color
|
||||
pub fn focus_color(&self) -> Srgba {
|
||||
self.focus.clone().into()
|
||||
}
|
||||
/// convert to srgba
|
||||
pub fn into_srgba(self) -> Component<Srgba> {
|
||||
Component {
|
||||
base: self.base.into(),
|
||||
hover: self.hover.into(),
|
||||
pressed: self.pressed.into(),
|
||||
selected: self.selected.into(),
|
||||
selected_text: self.selected_text.into(),
|
||||
focus: self.focus.into(),
|
||||
divider: self.divider.into(),
|
||||
on: self.on.into(),
|
||||
disabled: self.disabled.into(),
|
||||
on_disabled: self.on_disabled.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// helper for producing a component from a base color a neutral and an accent
|
||||
pub fn colored_component(base: C, neutral: C, accent: C) -> Self {
|
||||
let neutral = neutral.clone().into();
|
||||
let mut neutral_05 = neutral.clone();
|
||||
let mut neutral_10 = neutral.clone();
|
||||
let mut neutral_20 = neutral.clone();
|
||||
neutral_05.alpha = 0.05;
|
||||
neutral_10.alpha = 0.1;
|
||||
neutral_20.alpha = 0.2;
|
||||
|
||||
let base: Srgba = base.into();
|
||||
let mut base_50 = base.clone();
|
||||
base_50.alpha = 0.5;
|
||||
|
||||
let on_20 = neutral.clone();
|
||||
let mut on_50 = on_20.clone();
|
||||
|
||||
on_50.alpha = 0.5;
|
||||
|
||||
Component {
|
||||
base: base.clone().into(),
|
||||
hover: over(neutral_10, base).into(),
|
||||
pressed: over(neutral_20, base).into(),
|
||||
selected: over(neutral_10, base).into(),
|
||||
selected_text: accent.clone(),
|
||||
divider: on_20.into(),
|
||||
on: neutral.into(),
|
||||
disabled: base_50.into(),
|
||||
on_disabled: on_50.into(),
|
||||
focus: accent,
|
||||
}
|
||||
}
|
||||
|
||||
/// helper for producing a component color theme
|
||||
pub fn component(
|
||||
base: C,
|
||||
component_state_overlay: C,
|
||||
base_overlay: C,
|
||||
base_overlay_alpha: f32,
|
||||
accent: C,
|
||||
on_component: C,
|
||||
is_high_contrast: bool,
|
||||
) -> Self {
|
||||
let component_state_overlay = component_state_overlay.clone().into();
|
||||
let mut component_state_overlay_10 = component_state_overlay.clone();
|
||||
let mut component_state_overlay_20 = component_state_overlay.clone();
|
||||
component_state_overlay_10.alpha = 0.1;
|
||||
component_state_overlay_20.alpha = 0.2;
|
||||
|
||||
let base = base.into();
|
||||
let mut base_overlay = base_overlay.into();
|
||||
base_overlay.alpha = base_overlay_alpha;
|
||||
let base = over(base_overlay, base);
|
||||
let mut base_50 = base.clone();
|
||||
base_50.alpha = 0.5;
|
||||
|
||||
let mut on_20 = on_component.clone().into();
|
||||
let mut on_50 = on_20.clone();
|
||||
|
||||
on_20.alpha = 0.2;
|
||||
on_50.alpha = 0.5;
|
||||
|
||||
Component {
|
||||
base: base.clone().into(),
|
||||
hover: over(component_state_overlay_10, base).into(),
|
||||
pressed: over(component_state_overlay_20, base).into(),
|
||||
selected: over(component_state_overlay_10, base).into(),
|
||||
selected_text: accent.clone(),
|
||||
focus: accent.clone(),
|
||||
divider: if is_high_contrast {
|
||||
on_50.clone().into()
|
||||
} else {
|
||||
on_20.into()
|
||||
},
|
||||
on: on_component.clone(),
|
||||
disabled: base_50.into(),
|
||||
on_disabled: on_50.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Derived theme element from a palette and constraints
|
||||
#[derive(Debug)]
|
||||
pub struct Derivation<E> {
|
||||
/// Derived theme element
|
||||
pub derived: E,
|
||||
/// Derivation errors (Failed constraints)
|
||||
pub errors: Vec<anyhow::Error>,
|
||||
}
|
||||
|
||||
pub(crate) enum ComponentType {
|
||||
Background,
|
||||
Primary,
|
||||
Secondary,
|
||||
Destructive,
|
||||
Warning,
|
||||
Success,
|
||||
Accent,
|
||||
}
|
||||
|
||||
impl<C> From<(CosmicPalette<C>, ComponentType)> for Component<C>
|
||||
where
|
||||
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
|
||||
{
|
||||
fn from((p, t): (CosmicPalette<C>, ComponentType)) -> Self {
|
||||
match (p, t) {
|
||||
(CosmicPalette::Dark(p), ComponentType::Background) => Self::component(
|
||||
p.gray_1,
|
||||
p.neutral_1,
|
||||
p.neutral_10,
|
||||
0.08,
|
||||
p.blue,
|
||||
p.neutral_8,
|
||||
false,
|
||||
),
|
||||
|
||||
(CosmicPalette::Dark(p), ComponentType::Primary) => Self::component(
|
||||
p.gray_2,
|
||||
p.neutral_1,
|
||||
p.neutral_10,
|
||||
0.08,
|
||||
p.blue,
|
||||
p.neutral_8,
|
||||
false,
|
||||
),
|
||||
|
||||
(CosmicPalette::Dark(p), ComponentType::Secondary) => Self::component(
|
||||
p.gray_3,
|
||||
p.neutral_1,
|
||||
p.neutral_10,
|
||||
0.08,
|
||||
p.blue,
|
||||
p.neutral_9,
|
||||
false,
|
||||
),
|
||||
(CosmicPalette::HighContrastDark(p), ComponentType::Background) => Self::component(
|
||||
p.gray_1,
|
||||
p.neutral_1,
|
||||
p.neutral_10,
|
||||
0.08,
|
||||
p.blue,
|
||||
p.neutral_9,
|
||||
true,
|
||||
),
|
||||
(CosmicPalette::HighContrastDark(p), ComponentType::Primary) => Self::component(
|
||||
p.gray_2,
|
||||
p.neutral_1,
|
||||
p.neutral_10,
|
||||
0.08,
|
||||
p.blue,
|
||||
p.neutral_9,
|
||||
true,
|
||||
),
|
||||
(CosmicPalette::HighContrastDark(p), ComponentType::Secondary) => Self::component(
|
||||
p.gray_3,
|
||||
p.neutral_1,
|
||||
p.neutral_10.clone(),
|
||||
0.08,
|
||||
p.blue,
|
||||
p.neutral_10,
|
||||
true,
|
||||
),
|
||||
|
||||
(CosmicPalette::Light(p), ComponentType::Background) => Component::component(
|
||||
p.gray_1.clone(),
|
||||
p.neutral_1.clone(),
|
||||
p.neutral_1,
|
||||
0.75,
|
||||
p.blue.clone(),
|
||||
p.neutral_8,
|
||||
false,
|
||||
),
|
||||
(CosmicPalette::Light(p), ComponentType::Primary) => Component::component(
|
||||
p.gray_2.clone(),
|
||||
p.neutral_1.clone(),
|
||||
p.neutral_1,
|
||||
0.9,
|
||||
p.blue.clone(),
|
||||
p.neutral_8,
|
||||
false,
|
||||
),
|
||||
(CosmicPalette::Light(p), ComponentType::Secondary) => Component::component(
|
||||
p.gray_3.clone(),
|
||||
p.neutral_1.clone(),
|
||||
p.neutral_1,
|
||||
1.0,
|
||||
p.blue.clone(),
|
||||
p.neutral_8,
|
||||
false,
|
||||
),
|
||||
(CosmicPalette::HighContrastLight(p), ComponentType::Background) => {
|
||||
Component::component(
|
||||
p.gray_1.clone(),
|
||||
p.neutral_1.clone(),
|
||||
p.neutral_1,
|
||||
0.75,
|
||||
p.blue.clone(),
|
||||
p.neutral_9,
|
||||
true,
|
||||
)
|
||||
}
|
||||
(CosmicPalette::HighContrastLight(p), ComponentType::Primary) => Component::component(
|
||||
p.gray_2.clone(),
|
||||
p.neutral_1.clone(),
|
||||
p.neutral_1,
|
||||
0.9,
|
||||
p.blue.clone(),
|
||||
p.neutral_9,
|
||||
true,
|
||||
),
|
||||
(CosmicPalette::HighContrastLight(p), ComponentType::Secondary) => {
|
||||
Component::component(
|
||||
p.gray_3.clone(),
|
||||
p.neutral_1.clone(),
|
||||
p.neutral_1,
|
||||
1.0,
|
||||
p.blue.clone(),
|
||||
p.neutral_9,
|
||||
true,
|
||||
)
|
||||
}
|
||||
|
||||
(CosmicPalette::Dark(p), ComponentType::Destructive)
|
||||
| (CosmicPalette::Light(p), ComponentType::Destructive)
|
||||
| (CosmicPalette::HighContrastLight(p), ComponentType::Destructive)
|
||||
| (CosmicPalette::HighContrastDark(p), ComponentType::Destructive) => {
|
||||
Component::colored_component(p.red.clone(), p.neutral_1.clone(), p.blue.clone())
|
||||
}
|
||||
|
||||
(CosmicPalette::Dark(p), ComponentType::Warning)
|
||||
| (CosmicPalette::Light(p), ComponentType::Warning)
|
||||
| (CosmicPalette::HighContrastLight(p), ComponentType::Warning)
|
||||
| (CosmicPalette::HighContrastDark(p), ComponentType::Warning) => {
|
||||
Component::colored_component(p.yellow.clone(), p.neutral_1, p.blue.clone())
|
||||
}
|
||||
|
||||
(CosmicPalette::Dark(p), ComponentType::Success)
|
||||
| (CosmicPalette::Light(p), ComponentType::Success)
|
||||
| (CosmicPalette::HighContrastLight(p), ComponentType::Success)
|
||||
| (CosmicPalette::HighContrastDark(p), ComponentType::Success) => {
|
||||
Component::colored_component(p.green.clone(), p.neutral_1, p.blue.clone())
|
||||
}
|
||||
|
||||
(CosmicPalette::Dark(p), ComponentType::Accent)
|
||||
| (CosmicPalette::Light(p), ComponentType::Accent)
|
||||
| (CosmicPalette::HighContrastDark(p), ComponentType::Accent)
|
||||
| (CosmicPalette::HighContrastLight(p), ComponentType::Accent) => {
|
||||
Component::colored_component(p.blue.clone(), p.neutral_1, p.blue.clone())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
95
cosmic-theme/src/model/light.ron
Normal file
95
cosmic-theme/src/model/light.ron
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
Light (
|
||||
(
|
||||
name: "cosmic-light",
|
||||
blue: (
|
||||
c: "#00496D",
|
||||
),
|
||||
red: (
|
||||
c: "#A0252B",
|
||||
),
|
||||
green: (
|
||||
c: "#3B6E43",
|
||||
),
|
||||
yellow: (
|
||||
c: "#966800",
|
||||
),
|
||||
gray_1: (
|
||||
c: "#DEDEDE",
|
||||
),
|
||||
gray_2: (
|
||||
c: "#E9E9E9",
|
||||
),
|
||||
gray_3: (
|
||||
c: "#F4F4F4",
|
||||
),
|
||||
neutral_1: (
|
||||
c: "#FFFFFF",
|
||||
),
|
||||
neutral_2: (
|
||||
c: "#E4E4E4",
|
||||
),
|
||||
neutral_3: (
|
||||
c: "#C9C9C9",
|
||||
),
|
||||
neutral_4: (
|
||||
c: "#AEAEAE",
|
||||
),
|
||||
neutral_5: (
|
||||
c: "#939393",
|
||||
),
|
||||
neutral_6: (
|
||||
c: "#787878",
|
||||
),
|
||||
neutral_7: (
|
||||
c: "#5D5D5D",
|
||||
),
|
||||
neutral_8: (
|
||||
c: "#424242",
|
||||
),
|
||||
neutral_9: (
|
||||
c: "#272727",
|
||||
),
|
||||
neutral_10: (
|
||||
c: "#000000",
|
||||
),
|
||||
ext_warm_grey: (
|
||||
c: "#9B8E8A",
|
||||
),
|
||||
ext_orange: (
|
||||
c: "#FBB86C",
|
||||
),
|
||||
ext_yellow: (
|
||||
c: "#F7E062",
|
||||
),
|
||||
ext_blue: (
|
||||
c: "#6ACAD8",
|
||||
),
|
||||
ext_purple: (
|
||||
c: "#D58CFF",
|
||||
),
|
||||
ext_pink: (
|
||||
c: "#FF9CDD",
|
||||
),
|
||||
ext_indigo: (
|
||||
c: "#95C4FC",
|
||||
),
|
||||
accent_warm_grey: (
|
||||
c: "#ADA29E",
|
||||
),
|
||||
accent_orange: (
|
||||
c: "#FFD7A1",
|
||||
),
|
||||
accent_yellow: (
|
||||
c: "#FFF19E",
|
||||
),
|
||||
accent_purple: (
|
||||
c: "#D58CFF",
|
||||
),
|
||||
accent_pink: (
|
||||
c: "#FF9CDD",
|
||||
),
|
||||
accent_indigo: (
|
||||
c: "#95C4FC",
|
||||
),
|
||||
)
|
||||
)
|
||||
14
cosmic-theme/src/model/mod.rs
Normal file
14
cosmic-theme/src/model/mod.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
#[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;
|
||||
99
cosmic-theme/src/model/selection.rs
Normal file
99
cosmic-theme/src/model/selection.rs
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
419
cosmic-theme/src/model/theme.rs
Normal file
419
cosmic-theme/src/model/theme.rs
Normal file
|
|
@ -0,0 +1,419 @@
|
|||
use crate::{
|
||||
util::CssColor, Component, ComponentType, Container, ContainerType, CosmicPalette,
|
||||
CosmicPaletteInner, DARK_PALETTE, LIGHT_PALETTE, NAME, THEME_DIR,
|
||||
};
|
||||
use anyhow::Context;
|
||||
use cosmic_config::{Config, ConfigGet, ConfigSet, CosmicConfigEntry};
|
||||
use directories::{BaseDirsExt, ProjectDirsExt};
|
||||
use palette::Srgba;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
use std::{
|
||||
fmt,
|
||||
fs::File,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize, PartialEq, Eq)]
|
||||
/// Theme layer type
|
||||
pub enum Layer {
|
||||
/// Background layer
|
||||
#[default]
|
||||
Background,
|
||||
/// Primary Layer
|
||||
Primary,
|
||||
/// Secondary Layer
|
||||
Secondary,
|
||||
}
|
||||
|
||||
/// Cosmic Theme data structure with all colors and its name
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Theme<C> {
|
||||
/// name of the theme
|
||||
pub name: String,
|
||||
/// background element colors
|
||||
pub background: Container<C>,
|
||||
/// primary element colors
|
||||
pub primary: Container<C>,
|
||||
/// secondary element colors
|
||||
pub secondary: Container<C>,
|
||||
/// accent element colors
|
||||
pub accent: Component<C>,
|
||||
/// suggested element colors
|
||||
pub success: Component<C>,
|
||||
/// destructive element colors
|
||||
pub destructive: Component<C>,
|
||||
/// warning element colors
|
||||
pub warning: Component<C>,
|
||||
/// palette
|
||||
pub palette: CosmicPaletteInner<C>,
|
||||
/// is dark
|
||||
pub is_dark: bool,
|
||||
/// is high contrast
|
||||
pub is_high_contrast: bool,
|
||||
}
|
||||
|
||||
impl CosmicConfigEntry for Theme<CssColor> {
|
||||
fn write_entry(&self, config: &Config) -> Result<(), cosmic_config::Error> {
|
||||
let self_ = self.clone();
|
||||
// TODO do as transaction
|
||||
let tx = config.transaction();
|
||||
|
||||
tx.set("name", self_.name)?;
|
||||
tx.set("background", self_.background)?;
|
||||
tx.set("primary", self_.primary)?;
|
||||
tx.set("secondary", self_.secondary)?;
|
||||
tx.set("accent", self_.accent)?;
|
||||
tx.set("success", self_.success)?;
|
||||
tx.set("destructive", self_.destructive)?;
|
||||
tx.set("warning", self_.warning)?;
|
||||
tx.set("palette", self_.palette)?;
|
||||
tx.set("is_dark", self_.is_dark)?;
|
||||
tx.set("is_high_contrast", self_.is_high_contrast)?;
|
||||
|
||||
tx.commit()
|
||||
}
|
||||
|
||||
fn get_entry(config: &Config) -> Result<Self, (Vec<cosmic_config::Error>, Self)> {
|
||||
let mut default = Self::default();
|
||||
let mut errors = Vec::new();
|
||||
|
||||
match config.get::<String>("name") {
|
||||
Ok(name) => default.name = name,
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
match config.get::<Container<CssColor>>("background") {
|
||||
Ok(background) => default.background = background,
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
match config.get::<Container<CssColor>>("primary") {
|
||||
Ok(primary) => default.primary = primary,
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
match config.get::<Container<CssColor>>("secondary") {
|
||||
Ok(secondary) => default.secondary = secondary,
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
match config.get::<Component<CssColor>>("accent") {
|
||||
Ok(accent) => default.accent = accent,
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
match config.get::<Component<CssColor>>("success") {
|
||||
Ok(success) => default.success = success,
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
match config.get::<Component<CssColor>>("destructive") {
|
||||
Ok(destructive) => default.destructive = destructive,
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
match config.get::<Component<CssColor>>("warning") {
|
||||
Ok(warning) => default.warning = warning,
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
match config.get::<CosmicPaletteInner<CssColor>>("palette") {
|
||||
Ok(palette) => default.palette = palette,
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
match config.get::<bool>("is_dark") {
|
||||
Ok(is_dark) => default.is_dark = is_dark,
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
match config.get::<bool>("is_high_contrast") {
|
||||
Ok(is_high_contrast) => default.is_high_contrast = is_high_contrast,
|
||||
Err(e) => errors.push(e),
|
||||
}
|
||||
|
||||
if errors.is_empty() {
|
||||
Ok(default)
|
||||
} else {
|
||||
Err((errors, default))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Theme<Srgba> {
|
||||
fn default() -> Self {
|
||||
Theme::<CssColor>::dark_default().into_srgba()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Theme<CssColor> {
|
||||
fn default() -> Self {
|
||||
Self::dark_default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for layered themes
|
||||
pub trait LayeredTheme {
|
||||
/// Set the layer of the theme
|
||||
fn set_layer(&mut self, layer: Layer);
|
||||
}
|
||||
|
||||
// TODO better eq check
|
||||
impl<C> PartialEq for Theme<C>
|
||||
where
|
||||
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
|
||||
{
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.name == other.name
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Eq for Theme<C> where
|
||||
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned
|
||||
{
|
||||
}
|
||||
|
||||
impl<C> Theme<C>
|
||||
where
|
||||
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
|
||||
{
|
||||
/// Convert the theme to a high-contrast variant
|
||||
pub fn to_high_contrast(&self) -> Self {
|
||||
todo!();
|
||||
}
|
||||
|
||||
/// save the theme to the theme directory
|
||||
pub fn save(&self) -> anyhow::Result<()> {
|
||||
let ron_path: PathBuf = [NAME, THEME_DIR].iter().collect();
|
||||
let ron_dirs = directories::ProjectDirs::from_path(ron_path)
|
||||
.context("Failed to get project directories.")?;
|
||||
let ron_name = format!("{}.ron", &self.name);
|
||||
|
||||
if let Ok(p) = ron_dirs.place_config_file(ron_name) {
|
||||
let mut f = File::create(p)?;
|
||||
f.write_all(ron::ser::to_string_pretty(self, Default::default())?.as_bytes())?;
|
||||
} else {
|
||||
anyhow::bail!("Failed to write RON theme.");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// init the theme directory
|
||||
pub fn init() -> anyhow::Result<PathBuf> {
|
||||
let ron_path: PathBuf = [NAME, THEME_DIR].iter().collect();
|
||||
let base_dirs = directories::BaseDirs::new().context("Failed to get base directories.")?;
|
||||
Ok(base_dirs.create_config_directory(ron_path)?)
|
||||
}
|
||||
|
||||
/// load a theme by name
|
||||
pub fn load_from_name(name: &str) -> anyhow::Result<Self> {
|
||||
let ron_path: PathBuf = [NAME, THEME_DIR].iter().collect();
|
||||
let ron_dirs = directories::ProjectDirs::from_path(ron_path)
|
||||
.context("Failed to get project directories.")?;
|
||||
|
||||
let ron_name = format!("{}.ron", name);
|
||||
if let Some(p) = ron_dirs.find_config_file(ron_name) {
|
||||
let f = File::open(p)?;
|
||||
Ok(ron::de::from_reader(f)?)
|
||||
} else {
|
||||
anyhow::bail!("Failed to write RON theme.");
|
||||
}
|
||||
}
|
||||
|
||||
/// load a theme by path
|
||||
pub fn load(p: &dyn AsRef<Path>) -> anyhow::Result<Self> {
|
||||
let f = File::open(p)?;
|
||||
Ok(ron::de::from_reader(f)?)
|
||||
}
|
||||
|
||||
// TODO convenient getter functions for each named color variable
|
||||
/// get @accent_color
|
||||
pub fn accent_color(&self) -> Srgba {
|
||||
self.accent.base.clone().into()
|
||||
}
|
||||
/// get @success_color
|
||||
pub fn success_color(&self) -> Srgba {
|
||||
self.success.base.clone().into()
|
||||
}
|
||||
/// get @destructive_color
|
||||
pub fn destructive_color(&self) -> Srgba {
|
||||
self.destructive.base.clone().into()
|
||||
}
|
||||
/// get @warning_color
|
||||
pub fn warning_color(&self) -> Srgba {
|
||||
self.warning.base.clone().into()
|
||||
}
|
||||
|
||||
// Containers
|
||||
/// get @bg_color
|
||||
pub fn bg_color(&self) -> Srgba {
|
||||
self.background.base.clone().into()
|
||||
}
|
||||
/// get @bg_component_color
|
||||
pub fn bg_component_color(&self) -> Srgba {
|
||||
self.background.component.base.clone().into()
|
||||
}
|
||||
/// get @primary_container_color
|
||||
pub fn primary_container_color(&self) -> Srgba {
|
||||
self.primary.base.clone().into()
|
||||
}
|
||||
/// get @primary_component_color
|
||||
pub fn primary_component_color(&self) -> Srgba {
|
||||
self.primary.component.base.clone().into()
|
||||
}
|
||||
/// get @secondary_container_color
|
||||
pub fn secondary_container_color(&self) -> Srgba {
|
||||
self.secondary.base.clone().into()
|
||||
}
|
||||
/// get @secondary_component_color
|
||||
pub fn secondary_component_color(&self) -> Srgba {
|
||||
self.secondary.component.base.clone().into()
|
||||
}
|
||||
|
||||
// Text
|
||||
/// get @on_bg_color
|
||||
pub fn on_bg_color(&self) -> Srgba {
|
||||
self.background.on.clone().into()
|
||||
}
|
||||
/// get @on_bg_component_color
|
||||
pub fn on_bg_component_color(&self) -> Srgba {
|
||||
self.background.component.on.clone().into()
|
||||
}
|
||||
/// get @on_primary_color
|
||||
pub fn on_primary_container_color(&self) -> Srgba {
|
||||
self.primary.on.clone().into()
|
||||
}
|
||||
/// get @on_primary_component_color
|
||||
pub fn on_primary_component_color(&self) -> Srgba {
|
||||
self.primary.component.on.clone().into()
|
||||
}
|
||||
/// get @on_secondary_color
|
||||
pub fn on_secondary_container_color(&self) -> Srgba {
|
||||
self.secondary.on.clone().into()
|
||||
}
|
||||
/// get @on_secondary_component_color
|
||||
pub fn on_secondary_component_color(&self) -> Srgba {
|
||||
self.secondary.component.on.clone().into()
|
||||
}
|
||||
/// get @accent_text_color
|
||||
pub fn accent_text_color(&self) -> Srgba {
|
||||
self.accent.base.clone().into()
|
||||
}
|
||||
/// get @success_text_color
|
||||
pub fn success_text_color(&self) -> Srgba {
|
||||
self.success.base.clone().into()
|
||||
}
|
||||
/// get @warning_text_color
|
||||
pub fn warning_text_color(&self) -> Srgba {
|
||||
self.warning.base.clone().into()
|
||||
}
|
||||
/// get @destructive_text_color
|
||||
pub fn destructive_text_color(&self) -> Srgba {
|
||||
self.destructive.base.clone().into()
|
||||
}
|
||||
/// get @on_accent_color
|
||||
pub fn on_accent_color(&self) -> Srgba {
|
||||
self.accent.on.clone().into()
|
||||
}
|
||||
/// get @on_success_color
|
||||
pub fn on_success_color(&self) -> Srgba {
|
||||
self.success.on.clone().into()
|
||||
}
|
||||
/// get @oon_warning_color
|
||||
pub fn on_warning_color(&self) -> Srgba {
|
||||
self.warning.on.clone().into()
|
||||
}
|
||||
/// get @on_destructive_color
|
||||
pub fn on_destructive_color(&self) -> Srgba {
|
||||
self.destructive.on.clone().into()
|
||||
}
|
||||
|
||||
// Borders and Dividers
|
||||
/// get @bg_divider
|
||||
pub fn bg_divider(&self) -> Srgba {
|
||||
self.background.divider.clone().into()
|
||||
}
|
||||
/// get @bg_component_divider
|
||||
pub fn bg_component_divider(&self) -> Srgba {
|
||||
self.background.component.divider.clone().into()
|
||||
}
|
||||
/// get @primary_container_divider
|
||||
pub fn primary_container_divider(&self) -> Srgba {
|
||||
self.primary.divider.clone().into()
|
||||
}
|
||||
/// get @primary_component_divider
|
||||
pub fn primary_component_divider(&self) -> Srgba {
|
||||
self.primary.component.divider.clone().into()
|
||||
}
|
||||
/// get @secondary_container_divider
|
||||
pub fn secondary_container_divider(&self) -> Srgba {
|
||||
self.secondary.divider.clone().into()
|
||||
}
|
||||
/// get @secondary_component_divider
|
||||
pub fn secondary_component_divider(&self) -> Srgba {
|
||||
self.secondary.component.divider.clone().into()
|
||||
}
|
||||
|
||||
/// get @window_header_bg
|
||||
pub fn window_header_bg(&self) -> Srgba {
|
||||
self.background.base.clone().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Theme<CssColor> {
|
||||
/// get the built in light theme
|
||||
pub fn light_default() -> Self {
|
||||
LIGHT_PALETTE.clone().into()
|
||||
}
|
||||
|
||||
/// get the built in dark theme
|
||||
pub fn dark_default() -> Self {
|
||||
DARK_PALETTE.clone().into()
|
||||
}
|
||||
|
||||
/// get the built in high contrast dark theme
|
||||
pub fn high_contrast_dark_default() -> Self {
|
||||
CosmicPalette::HighContrastDark(DARK_PALETTE.as_ref().clone()).into()
|
||||
}
|
||||
|
||||
/// get the built in high contrast light theme
|
||||
pub fn high_contrast_light_default() -> Self {
|
||||
CosmicPalette::HighContrastLight(LIGHT_PALETTE.as_ref().clone()).into()
|
||||
}
|
||||
|
||||
/// convert to srgba
|
||||
pub fn into_srgba(self) -> Theme<Srgba> {
|
||||
Theme {
|
||||
name: self.name,
|
||||
background: self.background.into_srgba(),
|
||||
primary: self.primary.into_srgba(),
|
||||
secondary: self.secondary.into_srgba(),
|
||||
accent: self.accent.into_srgba(),
|
||||
success: self.success.into_srgba(),
|
||||
destructive: self.destructive.into_srgba(),
|
||||
warning: self.warning.into_srgba(),
|
||||
palette: self.palette.into(),
|
||||
is_dark: self.is_dark,
|
||||
is_high_contrast: self.is_high_contrast,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> From<CosmicPalette<C>> for Theme<C>
|
||||
where
|
||||
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
|
||||
{
|
||||
fn from(p: CosmicPalette<C>) -> Self {
|
||||
let is_dark = p.is_dark();
|
||||
let is_high_contrast = p.is_high_contrast();
|
||||
Self {
|
||||
name: p.name().to_string(),
|
||||
background: (p.clone(), ContainerType::Background).into(),
|
||||
primary: (p.clone(), ContainerType::Primary).into(),
|
||||
secondary: (p.clone(), ContainerType::Secondary).into(),
|
||||
accent: (p.clone(), ComponentType::Accent).into(),
|
||||
success: (p.clone(), ComponentType::Success).into(),
|
||||
destructive: (p.clone(), ComponentType::Destructive).into(),
|
||||
warning: (p.clone(), ComponentType::Warning).into(),
|
||||
palette: match p {
|
||||
CosmicPalette::Dark(p) => p.into(),
|
||||
CosmicPalette::Light(p) => p.into(),
|
||||
CosmicPalette::HighContrastLight(p) => p.into(),
|
||||
CosmicPalette::HighContrastDark(p) => p.into(),
|
||||
},
|
||||
is_dark,
|
||||
is_high_contrast,
|
||||
}
|
||||
}
|
||||
}
|
||||
187
cosmic-theme/src/output/gtk4_output.rs
Normal file
187
cosmic-theme/src/output/gtk4_output.rs
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
use crate::{
|
||||
model::{Accent, Container, ContainerType, Destructive, Widget},
|
||||
Hex, Theme, NAME,
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
use palette::Srgba;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
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
|
||||
pub trait Gtk4Output {
|
||||
/// turn the theme into css
|
||||
fn as_css(&self) -> String;
|
||||
/// Serialize the theme as RON and write the CSS to the appropriate directories
|
||||
/// Should be written in the XDG data directory for cosmic-theme
|
||||
fn write(&self) -> Result<()>;
|
||||
}
|
||||
|
||||
impl<C> Gtk4Output for Theme<C>
|
||||
where
|
||||
C: Clone
|
||||
+ fmt::Debug
|
||||
+ Default
|
||||
+ Into<Hex>
|
||||
+ Into<Srgba>
|
||||
+ From<Srgba>
|
||||
+ Serialize
|
||||
+ DeserializeOwned,
|
||||
{
|
||||
fn as_css(&self) -> String {
|
||||
let Self {
|
||||
background,
|
||||
primary,
|
||||
secondary,
|
||||
accent,
|
||||
destructive,
|
||||
..
|
||||
} = self;
|
||||
let mut css = String::new();
|
||||
|
||||
css.push_str(&background.as_css());
|
||||
css.push_str(&primary.as_css());
|
||||
css.push_str(&secondary.as_css());
|
||||
css.push_str(&accent.as_css());
|
||||
css.push_str(&destructive.as_css());
|
||||
|
||||
css
|
||||
}
|
||||
|
||||
fn write(&self) -> Result<()> {
|
||||
// TODO sass -> css
|
||||
let ron_str = ron::ser::to_string_pretty(self, Default::default())?;
|
||||
let css_str = self.as_css();
|
||||
|
||||
let ron_path: PathBuf = [NAME, THEME_DIR].iter().collect();
|
||||
let css_path: PathBuf = [NAME, CSS_DIR].iter().collect();
|
||||
|
||||
let ron_dirs = xdg::BaseDirectories::with_prefix(ron_path)?;
|
||||
let css_dirs = xdg::BaseDirectories::with_prefix(css_path)?;
|
||||
|
||||
let ron_name = format!("{}.ron", &self.name);
|
||||
let css_name = format!("{}.css", &self.name);
|
||||
|
||||
if let Ok(p) = ron_dirs.place_data_file(ron_name) {
|
||||
let mut f = File::create(p)?;
|
||||
f.write_all(ron_str.as_bytes())?;
|
||||
} else {
|
||||
bail!("Failed to write RON theme.")
|
||||
}
|
||||
|
||||
if let Ok(p) = css_dirs.place_data_file(css_name) {
|
||||
let mut f = File::create(p)?;
|
||||
f.write_all(css_str.as_bytes())?;
|
||||
} else {
|
||||
bail!("Failed to write RON theme.")
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for converting theme data into gtk4 CSS
|
||||
pub trait AsGtk4Css<C>
|
||||
where
|
||||
C: Copy + Into<Srgba> + From<Srgba>,
|
||||
{
|
||||
/// function for converting theme data into gtk4 CSS
|
||||
fn as_css(&self) -> String;
|
||||
}
|
||||
|
||||
impl<C> AsGtk4Css<C> for Container<C>
|
||||
where
|
||||
C: Copy + Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + fmt::Display,
|
||||
{
|
||||
fn as_css(&self) -> String {
|
||||
let Self {
|
||||
prefix,
|
||||
container,
|
||||
container_component,
|
||||
container_divider,
|
||||
container_fg,
|
||||
..
|
||||
} = self;
|
||||
|
||||
let prefix_lower = match prefix {
|
||||
ContainerType::Background => "background",
|
||||
ContainerType::Primary => "primary",
|
||||
ContainerType::Secondary => "secondary",
|
||||
};
|
||||
let component = widget_gtk4_css(prefix_lower, container_component);
|
||||
|
||||
format!(
|
||||
r#"
|
||||
@define-color {prefix_lower}_container #{{{container}}};
|
||||
@define-color {prefix_lower}_container_divider #{{{container_divider}}};
|
||||
@define-color {prefix_lower}_container_fg #{{{container_fg}}};
|
||||
{component}
|
||||
"#
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> AsGtk4Css<C> for Accent<C>
|
||||
where
|
||||
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
|
||||
{
|
||||
fn as_css(&self) -> String {
|
||||
let Accent {
|
||||
accent,
|
||||
accent_fg,
|
||||
accent_nav_handle_fg,
|
||||
suggested,
|
||||
} = self;
|
||||
let suggested = widget_gtk4_css("suggested", suggested);
|
||||
|
||||
format!(
|
||||
r#"
|
||||
@define-color accent #{{{accent}}};
|
||||
@define-color accent_fg #{{{accent_fg}}};
|
||||
@define-color accent_nav_handle_fg #{{{accent_nav_handle_fg}}};
|
||||
{suggested}
|
||||
"#
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> AsGtk4Css<C> for Destructive<C>
|
||||
where
|
||||
C: Clone + fmt::Debug + Default + Into<Srgba> + From<Srgba> + Serialize + DeserializeOwned,
|
||||
{
|
||||
fn as_css(&self) -> String {
|
||||
let Destructive { destructive } = &self;
|
||||
widget_gtk4_css("destructive", destructive)
|
||||
}
|
||||
}
|
||||
|
||||
fn widget_gtk4_css<C: fmt::Display>(
|
||||
prefix: &str,
|
||||
Widget {
|
||||
base,
|
||||
hover,
|
||||
pressed,
|
||||
focused,
|
||||
divider,
|
||||
text,
|
||||
text_opacity_80,
|
||||
disabled,
|
||||
disabled_fg,
|
||||
}: &Widget<C>,
|
||||
) -> String {
|
||||
format!(
|
||||
r#"
|
||||
@define-color {prefix}_widget_base #{{{base}}};
|
||||
@define-color {prefix}_widget_hover #{{{hover}}};
|
||||
@define-color {prefix}_widget_pressed #{{{pressed}}};
|
||||
@define-color {prefix}_widget_focused #{{{focused}}};
|
||||
@define-color {prefix}_widget_divider #{{{divider}}};
|
||||
@define-color {prefix}_widget_fg #{{{text}}};
|
||||
@define-color {prefix}_widget_fg_opacity_80 #{{{text_opacity_80}}};
|
||||
@define-color {prefix}_widget_disabled #{{{disabled}}};
|
||||
@define-color {prefix}_widget_disabled_fg #{{{disabled_fg}}};
|
||||
"#
|
||||
)
|
||||
}
|
||||
8
cosmic-theme/src/output/mod.rs
Normal file
8
cosmic-theme/src/output/mod.rs
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#[cfg(feature = "gtk4-theme")]
|
||||
/// Module for outputting the Cosmic gtk4 theme type as CSS
|
||||
pub mod gtk4_output;
|
||||
#[cfg(feature = "gtk4-theme")]
|
||||
pub use gtk4_output::*;
|
||||
|
||||
#[cfg(feature = "ron-serialization")]
|
||||
pub use ron::*;
|
||||
1
cosmic-theme/src/theme_provider/mod.rs
Normal file
1
cosmic-theme/src/theme_provider/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
59
cosmic-theme/src/util.rs
Normal file
59
cosmic-theme/src/util.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
use csscolorparser::Color;
|
||||
use palette::Srgba;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// utility wrapper for serializing and deserializing colors with arbitrary CSS
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct CssColor {
|
||||
c: Color,
|
||||
}
|
||||
|
||||
impl From<Srgba> for CssColor {
|
||||
fn from(c: Srgba) -> Self {
|
||||
Self {
|
||||
c: Color {
|
||||
r: c.red as f64,
|
||||
g: c.green as f64,
|
||||
b: c.blue as f64,
|
||||
a: c.alpha as f64,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Srgba> for CssColor {
|
||||
fn into(self) -> Srgba {
|
||||
Srgba::new(
|
||||
self.c.r as f32,
|
||||
self.c.g as f32,
|
||||
self.c.b as f32,
|
||||
self.c.a as f32,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
|
@ -12,3 +12,4 @@ libcosmic = { path = "../..", default-features = false, features = ["debug", "wi
|
|||
once_cell = "1.15"
|
||||
slotmap = "1.0.6"
|
||||
env_logger = "0.10"
|
||||
log = "0.4.17"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
/// Copyright 2022 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
use cosmic::{
|
||||
cosmic_config::config_subscription,
|
||||
font::load_fonts,
|
||||
iced::{self, Application, Command, Length, Subscription},
|
||||
iced::{
|
||||
|
|
@ -9,15 +10,20 @@ use cosmic::{
|
|||
window::{self, close, drag, minimize, toggle_maximize},
|
||||
},
|
||||
keyboard_nav,
|
||||
theme::{self, Theme},
|
||||
theme::{self, CosmicTheme, CosmicThemeCss, Theme},
|
||||
widget::{
|
||||
header_bar, icon, list, nav_bar, nav_bar_toggle, scrollable, segmented_button, settings,
|
||||
warning, IconSource,
|
||||
},
|
||||
Element, ElementExt,
|
||||
};
|
||||
use log::error;
|
||||
use std::{
|
||||
sync::atomic::{AtomicU32, Ordering},
|
||||
borrow::Cow,
|
||||
sync::{
|
||||
atomic::{AtomicU32, Ordering},
|
||||
Arc,
|
||||
},
|
||||
vec,
|
||||
};
|
||||
|
||||
|
|
@ -154,6 +160,7 @@ pub struct Window {
|
|||
warning_message: String,
|
||||
scale_factor: f64,
|
||||
scale_factor_string: String,
|
||||
system_theme: Arc<CosmicTheme>,
|
||||
}
|
||||
|
||||
impl Window {
|
||||
|
|
@ -198,6 +205,7 @@ pub enum Message {
|
|||
ToggleNavBarCondensed,
|
||||
ToggleWarning,
|
||||
FontsLoaded,
|
||||
SystemTheme(CosmicTheme),
|
||||
}
|
||||
|
||||
impl From<Page> for Message {
|
||||
|
|
@ -375,6 +383,16 @@ impl Application for Window {
|
|||
Subscription::batch(vec![
|
||||
window_break.map(|_| Message::CondensedViewToggle),
|
||||
keyboard_nav::subscription().map(Message::KeyboardNav),
|
||||
config_subscription::<_, CosmicThemeCss>(0, Cow::from("com.system76.CosmicTheme"), 1)
|
||||
.map(|(_, update)| match update {
|
||||
Ok(t) => Message::SystemTheme(t.into_srgba()),
|
||||
Err((errors, t)) => {
|
||||
for error in errors {
|
||||
error!("{:?}", error);
|
||||
}
|
||||
Message::SystemTheme(t.into_srgba())
|
||||
}
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
|
|
@ -395,7 +413,13 @@ impl Application for Window {
|
|||
Some(demo::Output::Debug(debug)) => self.debug = debug,
|
||||
Some(demo::Output::ScalingFactor(factor)) => self.set_scale_factor(factor),
|
||||
Some(demo::Output::ThemeChanged(theme)) => {
|
||||
self.theme = theme;
|
||||
self.theme = match theme {
|
||||
demo::ThemeVariant::Light => Theme::light(),
|
||||
demo::ThemeVariant::Dark => Theme::dark(),
|
||||
demo::ThemeVariant::HighContrastDark => Theme::dark_hc(),
|
||||
demo::ThemeVariant::HighContrastLight => Theme::light_hc(),
|
||||
demo::ThemeVariant::Custom => Theme::custom(self.system_theme.clone()),
|
||||
};
|
||||
}
|
||||
Some(demo::Output::ToggleWarning) => self.toggle_warning(),
|
||||
None => (),
|
||||
|
|
@ -425,6 +449,9 @@ impl Application for Window {
|
|||
},
|
||||
Message::ToggleWarning => self.toggle_warning(),
|
||||
Message::FontsLoaded => {}
|
||||
Message::SystemTheme(t) => {
|
||||
self.system_theme = Arc::new(t);
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
|
@ -572,6 +599,6 @@ impl Application for Window {
|
|||
}
|
||||
|
||||
fn theme(&self) -> Theme {
|
||||
self.theme
|
||||
self.theme.clone()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
use apply::Apply;
|
||||
use cosmic::{
|
||||
cosmic_theme,
|
||||
iced::widget::{checkbox, pick_list, progress_bar, radio, row, slider, text, text_input},
|
||||
iced::widget::{checkbox, column, pick_list, progress_bar, radio, slider, text, text_input},
|
||||
iced::{id, Alignment, Length},
|
||||
theme::{self, Button as ButtonTheme, Theme},
|
||||
theme::{self, Button as ButtonTheme, Theme, ThemeType},
|
||||
widget::{
|
||||
button, container, icon, segmented_button, segmented_selection, settings, spin_button,
|
||||
toggler, view_switcher,
|
||||
|
|
@ -15,6 +15,33 @@ use once_cell::sync::Lazy;
|
|||
|
||||
use super::{Page, Window};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq)]
|
||||
pub enum ThemeVariant {
|
||||
Light,
|
||||
Dark,
|
||||
HighContrastDark,
|
||||
HighContrastLight,
|
||||
Custom,
|
||||
}
|
||||
|
||||
impl From<&ThemeType> for ThemeVariant {
|
||||
fn from(theme: &ThemeType) -> Self {
|
||||
match theme {
|
||||
ThemeType::Light => ThemeVariant::Light,
|
||||
ThemeType::Dark => ThemeVariant::Dark,
|
||||
ThemeType::HighContrastDark => ThemeVariant::HighContrastDark,
|
||||
ThemeType::HighContrastLight => ThemeVariant::HighContrastLight,
|
||||
ThemeType::Custom(_) => ThemeVariant::Custom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ThemeType> for ThemeVariant {
|
||||
fn from(theme: ThemeType) -> Self {
|
||||
ThemeVariant::from(&theme)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum DemoView {
|
||||
TabA,
|
||||
TabB,
|
||||
|
|
@ -44,7 +71,7 @@ pub enum Message {
|
|||
Selection(segmented_button::Entity),
|
||||
SliderChanged(f32),
|
||||
SpinButton(spin_button::Message),
|
||||
ThemeChanged(Theme),
|
||||
ThemeChanged(ThemeVariant),
|
||||
ToggleWarning,
|
||||
TogglerToggled(bool),
|
||||
ViewSwitcher(segmented_button::Entity),
|
||||
|
|
@ -54,7 +81,7 @@ pub enum Message {
|
|||
pub enum Output {
|
||||
Debug(bool),
|
||||
ScalingFactor(f32),
|
||||
ThemeChanged(Theme),
|
||||
ThemeChanged(ThemeVariant),
|
||||
ToggleWarning,
|
||||
}
|
||||
|
||||
|
|
@ -151,20 +178,21 @@ impl State {
|
|||
|
||||
pub(super) fn view<'a>(&'a self, window: &'a Window) -> Element<'a, Message> {
|
||||
let choose_theme = [
|
||||
Theme::light(),
|
||||
Theme::dark(),
|
||||
Theme::light_hc(),
|
||||
Theme::dark_hc(),
|
||||
ThemeVariant::Light,
|
||||
ThemeVariant::Dark,
|
||||
ThemeVariant::HighContrastLight,
|
||||
ThemeVariant::HighContrastLight,
|
||||
ThemeVariant::Custom,
|
||||
]
|
||||
.iter()
|
||||
.into_iter()
|
||||
.fold(
|
||||
row![].spacing(10).align_items(Alignment::Center),
|
||||
column![].spacing(10).align_items(Alignment::Center),
|
||||
|row, theme| {
|
||||
row.push(radio(
|
||||
format!("{:?}", theme.theme_type),
|
||||
*theme,
|
||||
if window.theme == *theme {
|
||||
Some(*theme)
|
||||
format!("{:?}", theme),
|
||||
theme,
|
||||
if ThemeVariant::from(&window.theme.theme_type) == theme {
|
||||
Some(theme)
|
||||
} else {
|
||||
None
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
#![allow(clippy::module_name_repetitions)]
|
||||
|
||||
pub use cosmic_config;
|
||||
pub use cosmic_theme;
|
||||
pub use iced;
|
||||
pub use iced_runtime;
|
||||
|
|
@ -12,7 +13,6 @@ pub use iced_style;
|
|||
pub use iced_widget;
|
||||
#[cfg(feature = "winit")]
|
||||
pub use iced_winit;
|
||||
|
||||
#[cfg(feature = "applet")]
|
||||
pub mod applet;
|
||||
pub mod executor;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ mod segmented_button;
|
|||
use std::hash::Hash;
|
||||
use std::hash::Hasher;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use self::segmented_button::SegmentedButton;
|
||||
|
||||
|
|
@ -32,10 +33,10 @@ use iced_style::toggler;
|
|||
use iced_core::{Background, Color};
|
||||
use palette::Srgba;
|
||||
|
||||
type CosmicColor = ::palette::rgb::Srgba;
|
||||
type CosmicComponent = cosmic_theme::Component<CosmicColor>;
|
||||
type CosmicTheme = cosmic_theme::Theme<CosmicColor>;
|
||||
type CosmicThemeCss = cosmic_theme::Theme<cosmic_theme::util::CssColor>;
|
||||
pub type CosmicColor = ::palette::rgb::Srgba;
|
||||
pub type CosmicComponent = cosmic_theme::Component<CosmicColor>;
|
||||
pub type CosmicTheme = cosmic_theme::Theme<CosmicColor>;
|
||||
pub type CosmicThemeCss = cosmic_theme::Theme<cosmic_theme::util::CssColor>;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref COSMIC_DARK: CosmicTheme = CosmicThemeCss::dark_default().into_srgba();
|
||||
|
|
@ -56,16 +57,17 @@ lazy_static::lazy_static! {
|
|||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Default)]
|
||||
pub enum ThemeType {
|
||||
#[default]
|
||||
Dark,
|
||||
Light,
|
||||
HighContrastDark,
|
||||
HighContrastLight,
|
||||
Custom(Arc<CosmicTheme>),
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Default)]
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Default)]
|
||||
pub struct Theme {
|
||||
pub theme_type: ThemeType,
|
||||
pub layer: cosmic_theme::Layer,
|
||||
|
|
@ -79,6 +81,7 @@ impl Theme {
|
|||
ThemeType::Light => &COSMIC_LIGHT,
|
||||
ThemeType::HighContrastDark => &COSMIC_HC_DARK,
|
||||
ThemeType::HighContrastLight => &COSMIC_HC_LIGHT,
|
||||
ThemeType::Custom(ref t) => t.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -114,6 +117,14 @@ impl Theme {
|
|||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn custom(theme: Arc<CosmicTheme>) -> Self {
|
||||
Self {
|
||||
theme_type: ThemeType::Custom(theme),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// get current container
|
||||
/// can be used in a component that is intended to be a child of a `CosmicContainer`
|
||||
#[must_use]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue