From 141bbd23ec24518930f884cb653678eedc2ab8bf Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 26 Mar 2026 16:17:36 -0400 Subject: [PATCH] feat: hex_color serialization for the theme can also can deserialize the previous version of the theme, so existing themes should not be affected --- cosmic-theme/Cargo.toml | 1 + cosmic-theme/src/model/color.rs | 173 +++++++++++++++++++++++ cosmic-theme/src/model/cosmic_palette.rs | 33 +++++ cosmic-theme/src/model/derivation.rs | 17 +++ cosmic-theme/src/model/mod.rs | 1 + cosmic-theme/src/model/theme.rs | 63 +++++---- src/command.rs | 1 - 7 files changed, 264 insertions(+), 25 deletions(-) create mode 100644 cosmic-theme/src/model/color.rs diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index 7e408d8d..faec2fd5 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -15,6 +15,7 @@ export = ["serde_json"] no-default = [] [dependencies] +hex_color = { version = "3", features = ["serde"] } palette = { version = "0.7.6", features = ["serializing"] } almost = "0.2" serde = { version = "1.0.228", features = ["derive"] } diff --git a/cosmic-theme/src/model/color.rs b/cosmic-theme/src/model/color.rs new file mode 100644 index 00000000..cd64ec7f --- /dev/null +++ b/cosmic-theme/src/model/color.rs @@ -0,0 +1,173 @@ +//! Color representation and serde helpers for the Cosmic theme + +use hex_color::HexColor; +use palette::{Srgb, Srgba}; +use serde::{Deserialize, Serialize}; + +/// A color in the Cosmic theme for serialization and deserialization +#[derive(Debug, Copy, Clone, PartialEq, Deserialize, Serialize)] +#[serde(untagged)] +pub enum ColorRepr { + /// A color represented as a hex string + #[serde(with = "hex_color::rgba")] + Hex(HexColor), + /// A color represented as an RGBA value + Rgba(Srgba), + /// A color represented as an RGB value + Rgb(Srgb), +} + +/// An optional color in the Cosmic theme for serialization and deserialization +#[repr(transparent)] +#[derive(Debug, Copy, Clone, PartialEq, Deserialize, Serialize)] +#[serde(transparent)] +pub struct ColorReprOption(Option); + +impl From for ColorRepr { + fn from(color: Srgb) -> Self { + let rgb_u8: Srgb = color.into_format(); + ColorRepr::Hex(HexColor { + r: rgb_u8.red, + g: rgb_u8.green, + b: rgb_u8.blue, + a: 255, + }) + } +} + +impl From for ColorRepr { + fn from(color: Srgba) -> Self { + let rgba_u8: Srgba = color.into_format(); + ColorRepr::Hex(HexColor { + r: rgba_u8.red, + g: rgba_u8.green, + b: rgba_u8.blue, + a: rgba_u8.alpha, + }) + } +} + +impl From for Srgb { + fn from(value: ColorRepr) -> Self { + match value { + ColorRepr::Hex(hex) => Srgb::::new(hex.r, hex.g, hex.b).into_format(), + ColorRepr::Rgb(rgb) => rgb, + ColorRepr::Rgba(rgba) => Srgb::new(rgba.red, rgba.green, rgba.blue), + } + } +} + +impl From for Srgba { + fn from(value: ColorRepr) -> Self { + match value { + ColorRepr::Hex(hex) => Srgba::::new(hex.r, hex.g, hex.b, hex.a).into_format(), + ColorRepr::Rgb(rgb) => Srgba::new(rgb.red, rgb.green, rgb.blue, 1.0), + ColorRepr::Rgba(rgba) => rgba, + } + } +} + +impl From for Option { + fn from(value: ColorReprOption) -> Self { + value.0.map(std::convert::Into::into) + } +} + +impl From for Option { + fn from(value: ColorReprOption) -> Self { + value.0.map(std::convert::Into::into) + } +} + +impl From> for ColorReprOption { + fn from(value: Option) -> Self { + ColorReprOption(value.map(std::convert::Into::into)) + } +} + +impl From> for ColorReprOption { + fn from(value: Option) -> Self { + ColorReprOption(value.map(std::convert::Into::into)) + } +} + +/// A trait for converting between a color type and its representation for serialization and deserialization +pub trait ConvColorRepr: Sized { + /// Convert from a color representation to the color type + fn from_repr(repr: ColorRepr) -> Self; + /// Convert from the color type to its representation for serialization + fn to_repr(&self) -> ColorRepr; +} + +impl ConvColorRepr for Srgba { + fn from_repr(repr: ColorRepr) -> Self { + repr.into() + } + + fn to_repr(&self) -> ColorRepr { + (*self).into() + } +} + +impl ConvColorRepr for Srgb { + fn from_repr(repr: ColorRepr) -> Self { + repr.into() + } + + fn to_repr(&self) -> ColorRepr { + (*self).into() + } +} + +/// Serde helpers for serializing and deserializing colors in the Cosmic theme +pub mod color_serde { + use super::*; + use serde::{Deserialize, Deserializer, Serializer}; + + /// Serialize a color to a hex string + pub fn serialize(color: &T, serializer: S) -> Result + where + T: ConvColorRepr, + S: Serializer, + { + let repr = color.to_repr(); + repr.serialize(serializer) + } + + /// Deserialize a color from a hex string or RGB/RGBA + pub fn deserialize<'de, T, D>(deserializer: D) -> Result + where + T: ConvColorRepr, + D: Deserializer<'de>, + { + let repr = ColorRepr::deserialize(deserializer)?; + Ok(T::from_repr(repr)) + } + + /// Serde helpers for serializing and deserializing optional colors in the Cosmic theme + pub mod option { + use super::*; + + /// Serialize an optional color + pub fn serialize(value: &Option, serializer: S) -> Result + where + T: ConvColorRepr, + S: Serializer, + { + match value { + Some(v) => super::serialize(v, serializer), + None => serializer.serialize_none(), + } + } + + /// Deserialize an optional color + pub fn deserialize<'de, T, D>(deserializer: D) -> Result, D::Error> + where + T: ConvColorRepr, + D: Deserializer<'de>, + { + let opt = Option::::deserialize(deserializer)?; + Ok(opt.map(T::from_repr)) + } + } +} diff --git a/cosmic-theme/src/model/cosmic_palette.rs b/cosmic-theme/src/model/cosmic_palette.rs index 3852742b..4360f8f4 100644 --- a/cosmic-theme/src/model/cosmic_palette.rs +++ b/cosmic-theme/src/model/cosmic_palette.rs @@ -1,3 +1,4 @@ +use crate::color::color_serde; use palette::Srgba; use serde::{Deserialize, Serialize}; use std::sync::LazyLock; @@ -95,75 +96,107 @@ pub struct CosmicPaletteInner { /// Utility Colors /// Colors used for various points of emphasis in the UI. + #[serde(with = "color_serde")] pub bright_red: Srgba, /// Colors used for various points of emphasis in the UI. + #[serde(with = "color_serde")] pub bright_green: Srgba, /// Colors used for various points of emphasis in the UI. + #[serde(with = "color_serde")] pub bright_orange: Srgba, /// Surface Grays /// Colors used for three levels of surfaces in the UI. + #[serde(with = "color_serde")] pub gray_1: Srgba, /// Colors used for three levels of surfaces in the UI. + #[serde(with = "color_serde")] pub gray_2: Srgba, /// System Neutrals /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_0: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_1: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_2: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_3: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_4: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_5: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_6: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_7: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_8: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_9: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_10: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_blue: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_indigo: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_purple: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_pink: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_red: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_orange: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_yellow: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_green: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_warm_grey: Srgba, /// Extended Color Palette /// Colors used for themes, app icons, illustrations, and other brand purposes. + #[serde(with = "color_serde")] pub ext_warm_grey: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. + #[serde(with = "color_serde")] pub ext_orange: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. + #[serde(with = "color_serde")] pub ext_yellow: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. + #[serde(with = "color_serde")] pub ext_blue: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. + #[serde(with = "color_serde")] pub ext_purple: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. + #[serde(with = "color_serde")] pub ext_pink: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. + #[serde(with = "color_serde")] pub ext_indigo: Srgba, } diff --git a/cosmic-theme/src/model/derivation.rs b/cosmic-theme/src/model/derivation.rs index dce653e5..796ddab3 100644 --- a/cosmic-theme/src/model/derivation.rs +++ b/cosmic-theme/src/model/derivation.rs @@ -1,3 +1,4 @@ +use crate::color::color_serde; use palette::{Srgba, WithAlpha}; use serde::{Deserialize, Serialize}; @@ -8,14 +9,18 @@ use crate::composite::over; #[must_use] pub struct Container { /// the color of the container + #[serde(with = "color_serde")] pub base: Srgba, /// the color of components in the container pub component: Component, /// the color of dividers in the container + #[serde(with = "color_serde")] pub divider: Srgba, /// the color of text in the container + #[serde(with = "color_serde")] pub on: Srgba, /// the color of @small_widget_container + #[serde(with = "color_serde")] pub small_widget: Srgba, } @@ -45,30 +50,42 @@ impl Container { #[must_use] pub struct Component { /// The base color of the widget + #[serde(with = "color_serde")] pub base: Srgba, /// The color of the widget when it is hovered + #[serde(with = "color_serde")] pub hover: Srgba, /// the color of the widget when it is pressed + #[serde(with = "color_serde")] pub pressed: Srgba, /// the color of the widget when it is selected + #[serde(with = "color_serde")] pub selected: Srgba, /// the color of the widget when it is selected + #[serde(with = "color_serde")] pub selected_text: Srgba, /// the color of the widget when it is focused + #[serde(with = "color_serde")] pub focus: Srgba, /// the color of dividers for this widget + #[serde(with = "color_serde")] pub divider: Srgba, /// the color of text for this widget + #[serde(with = "color_serde")] pub on: Srgba, // the color of text with opacity 80 for this widget // pub text_opacity_80: Srgba, /// the color of the widget when it is disabled + #[serde(with = "color_serde")] pub disabled: Srgba, /// the color of text in the widget when it is disabled + #[serde(with = "color_serde")] pub on_disabled: Srgba, /// the color of the border for the widget + #[serde(with = "color_serde")] pub border: Srgba, /// the color of the border for the widget when it is disabled + #[serde(with = "color_serde")] pub disabled_border: Srgba, } diff --git a/cosmic-theme/src/model/mod.rs b/cosmic-theme/src/model/mod.rs index f48d1a8d..ff8eed3e 100644 --- a/cosmic-theme/src/model/mod.rs +++ b/cosmic-theme/src/model/mod.rs @@ -6,6 +6,7 @@ pub use mode::*; pub use spacing::*; pub use theme::*; +pub mod color; mod corner; mod cosmic_palette; mod density; diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 5db0f32c..53ea95b7 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -1,10 +1,11 @@ use crate::{ Component, Container, CornerRadii, CosmicPalette, CosmicPaletteInner, DARK_PALETTE, LIGHT_PALETTE, NAME, Spacing, ThemeMode, + color::{ColorRepr, ColorReprOption, color_serde, color_serde::option as color_serde_option}, composite::over, steps::{color_index, get_small_widget_color, get_surface_color, get_text, steps}, }; -use cosmic_config::{Config, CosmicConfigEntry}; +use cosmic_config::{Config, CosmicConfigEntry, cosmic_config_derive::CosmicConfigEntry}; use palette::{ IntoColor, Oklcha, Srgb, Srgba, WithAlpha, color_difference::Wcag21RelativeContrast, rgb::Rgb, }; @@ -37,15 +38,8 @@ pub enum Layer { #[must_use] /// Cosmic Theme data structure with all colors and its name -#[derive( - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - cosmic_config::cosmic_config_derive::CosmicConfigEntry, -)] -#[version = 1] +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, CosmicConfigEntry)] +#[version = 2] pub struct Theme { /// name of the theme pub name: String, @@ -98,13 +92,21 @@ pub struct Theme { /// enables blurred transparency pub is_frosted: bool, /// shade color for dialogs + #[serde(with = "color_serde")] + #[cosmic_config_entry(with = ColorRepr)] pub shade: Srgba, /// accent text colors /// If None, accent base color is the accent text color. + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub accent_text: Option, /// control tint color + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub control_tint: Option, /// text tint color + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub text_tint: Option, } @@ -739,7 +741,7 @@ impl Theme { if color_scheme.trim().contains("default") || color_scheme.trim().contains("light") { return Self::light_default(); } - }; + } Self::dark_default() } @@ -748,10 +750,10 @@ impl Theme { pub fn preferred_theme() -> Self { let current_desktop = std::env::var("XDG_CURRENT_DESKTOP"); - if let Ok(desktop) = current_desktop { - if desktop.trim().to_lowercase().contains("gnome") { - return Self::gtk_prefer_colorscheme(); - } + if let Ok(desktop) = current_desktop + && desktop.trim().to_lowercase().contains("gnome") + { + return Self::gtk_prefer_colorscheme(); } Self::dark_default() @@ -766,15 +768,8 @@ impl From for Theme { #[must_use] /// Helper for building customized themes -#[derive( - Clone, - Debug, - Serialize, - Deserialize, - cosmic_config::cosmic_config_derive::CosmicConfigEntry, - PartialEq, -)] -#[version = 1] +#[derive(Clone, Debug, Serialize, Deserialize, CosmicConfigEntry, PartialEq)] +#[version = 2] pub struct ThemeBuilder { /// override the palette for the builder pub palette: CosmicPalette, @@ -783,22 +778,40 @@ pub struct ThemeBuilder { /// override corner radii for the builder pub corner_radii: CornerRadii, /// override neutral_tint for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub neutral_tint: Option, /// override bg_color for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub bg_color: Option, /// override the primary container bg color for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub primary_container_bg: Option, /// override the secontary container bg color for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub secondary_container_bg: Option, /// override the text tint for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub text_tint: Option, /// override the accent color for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub accent: Option, /// override the success color for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub success: Option, /// override the warning color for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub warning: Option, /// override the destructive color for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub destructive: Option, /// enabled blurred transparency pub is_frosted: bool, // TODO handle @@ -807,6 +820,8 @@ pub struct ThemeBuilder { /// cosmic-comp active hint window outline width pub active_hint: u32, /// cosmic-comp custom window hint color + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub window_hint: Option, } diff --git a/src/command.rs b/src/command.rs index 1d6f635c..6bb16e8d 100644 --- a/src/command.rs +++ b/src/command.rs @@ -65,7 +65,6 @@ pub fn file_transfer_send( /// Returns a list of file paths. #[cfg(feature = "xdg-portal")] pub fn file_transfer_receive(key: String) -> iced::Task>> { - dbg!(&key); iced::Task::future(async move { let file_transfer = ashpd::documents::FileTransfer::new().await?; file_transfer.retrieve_files(&key).await