feat: support for custom & system themes + move cosmic-theme to libcosmic

This commit is contained in:
Ashley Wulber 2023-05-22 13:05:33 -04:00
parent 259bba4d19
commit bf1c474d08
No known key found for this signature in database
GPG key ID: 5216D4F46A90A820
26 changed files with 2639 additions and 44 deletions

View file

@ -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 = [

View file

@ -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
View 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
View file

@ -0,0 +1 @@
# WIP

View 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()))
}
}
}
}

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

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

View 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
View 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
}
}
}
}

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

View 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)?)
}
}

View 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",
),
)
)

View 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())
}
}
}
}

View 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",
),
)
)

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

View 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(),
})
}
}
}

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

View 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}}};
"#
)
}

View 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::*;

View file

@ -0,0 +1 @@

59
cosmic-theme/src/util.rs Normal file
View 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
}

View file

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

View file

@ -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()
}
}

View file

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

View file

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

View file

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