Cosmic advanced text (#103)

* wip: update to use cosmic-advanced-text

* use cosmic-advanced-text branch of iced

* fix: line height and spacing for segmented button and update to get svg fix

* fix: spin button styling & spacing

* update iced to fix segmented button border radius

* feat: example improvements

* feat: helper for loading fonts

* feat: add focus style to button

* fix: slider height and iced fixed

* feat: hash icon width and height

* cleanup

* update ci

* refactor: always use lazy feature of iced

* update iced

* update iced

* cleanup & update iced

* update iced: new slider & tiny-skia quad updates

* update iced: fixes for tiny-skia quad rendering with edge case border radius

* re-export iced_runtime & iced_widget

* merge master

* udpate iced

* update iced

* update iced

* update iced

* fix: make rectangle_tracker subscription only return update if there is some

* feat: derive macro for loading a cosmic-config

* feat (cosmic-config): iced subscription

* fix (example): update to rectangle tracker subscription

* fix (cosmic-config)

* refactor(cosmic-config-derive): add support for types with generic parameters

* fix (cosmic-config): feature gate updates for subscription helpers

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

* feat: sorta hacky way of creating header bars for libcosmic + update iced to get support for resizable windows in iced-sctk

* update iced

* update and reexport sctk

* fix: applet border radius

* feat (cosmic-theme): add id and name methods

* fix(cosmic-theme): reexport palette from cosmic-theme

* fix(cosmic-config-derive): allow use with reexported cosmic-config

* feat: update iced with fix and refactor applet env vars

* update iced
This commit is contained in:
Ashley Wulber 2023-05-30 12:03:15 -04:00 committed by GitHub
parent a173794bed
commit e056e8c830
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 3431 additions and 405 deletions

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,431 @@
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> {
/// version of the theme
pub fn version() -> u32 {
1
}
/// id of the theme
pub fn id() -> &'static str {
NAME
}
}
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,
}
}
}