use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Rgb {
pub r: f32,
pub g: f32,
pub b: f32,
}
impl Rgb {
pub fn new(r: f32, g: f32, b: f32) -> Self {
Self { r, g, b }
}
pub fn from_u8(r: u8, g: u8, b: u8) -> Self {
Self {
r: r as f32 / 255.0,
g: g as f32 / 255.0,
b: b as f32 / 255.0,
}
}
pub fn luminance(&self) -> f32 {
0.299 * self.r + 0.587 * self.g + 0.114 * self.b
}
pub fn saturation(&self) -> f32 {
let max_c = self.r.max(self.g).max(self.b);
let min_c = self.r.min(self.g).min(self.b);
if max_c > 0.0 { (max_c - min_c) / max_c } else { 0.0 }
}
pub fn hue(&self) -> f32 {
let max_c = self.r.max(self.g).max(self.b);
let min_c = self.r.min(self.g).min(self.b);
let delta = max_c - min_c;
if delta <= 0.0 {
return 0.0;
}
let mut h = if max_c == self.r {
(self.g - self.b) / delta
} else if max_c == self.g {
2.0 + (self.b - self.r) / delta
} else {
4.0 + (self.r - self.g) / delta
};
h *= 60.0;
if h < 0.0 {
h += 360.0;
}
h
}
pub fn lightened(&self, amount: f32) -> Self {
Self {
r: (self.r + (1.0 - self.r) * amount).min(1.0),
g: (self.g + (1.0 - self.g) * amount).min(1.0),
b: (self.b + (1.0 - self.b) * amount).min(1.0),
}
}
pub fn darkened(&self, amount: f32) -> Self {
Self {
r: (self.r * (1.0 - amount)).max(0.0),
g: (self.g * (1.0 - amount)).max(0.0),
b: (self.b * (1.0 - amount)).max(0.0),
}
}
pub fn saturated(&self, factor: f32) -> Self {
let gray = self.luminance();
Self {
r: (gray + (self.r - gray) * factor).clamp(0.0, 1.0),
g: (gray + (self.g - gray) * factor).clamp(0.0, 1.0),
b: (gray + (self.b - gray) * factor).clamp(0.0, 1.0),
}
}
pub fn hex(&self) -> String {
format!(
"#{:02X}{:02X}{:02X}",
(self.r * 255.0) as u8,
(self.g * 255.0) as u8,
(self.b * 255.0) as u8
)
}
pub fn hex_strip(&self) -> String {
format!(
"{:02X}{:02X}{:02X}",
(self.r * 255.0) as u8,
(self.g * 255.0) as u8,
(self.b * 255.0) as u8
)
}
pub fn rgb_string(&self) -> String {
format!("{}, {}, {}", (self.r * 255.0) as u8, (self.g * 255.0) as u8, (self.b * 255.0) as u8)
}
pub fn rgba_string(&self, alpha: f32) -> String {
format!("{:.6} {:.6} {:.6} {:.2}", self.r, self.g, self.b, alpha)
}
pub fn xrgba_string(&self) -> String {
format!(
"{:02x}/{:02x}/{:02x}/ff",
(self.r * 255.0) as u8,
(self.g * 255.0) as u8,
(self.b * 255.0) as u8
)
}
pub fn distance_squared(&self, other: &Rgb) -> f32 {
let dr = self.r - other.r;
let dg = self.g - other.g;
let db = self.b - other.b;
dr * dr + dg * dg + db * db
}
}
impl Default for Rgb {
fn default() -> Self {
Self::new(0.0, 0.0, 0.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColorScheme {
pub wallpaper: String,
pub is_dark: bool,
pub alpha: u8,
pub background: Rgb,
pub foreground: Rgb,
pub cursor: Rgb,
pub colors: Vec<Rgb>,
}
impl ColorScheme {
pub fn new(wallpaper: String, is_dark: bool, background: Rgb, foreground: Rgb, cursor: Rgb, colors: Vec<Rgb>) -> Self {
Self {
wallpaper,
is_dark,
alpha: 100,
background,
foreground,
cursor,
colors,
}
}
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(self)
}
pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
serde_json::from_str(json)
}
pub fn to_shell_format(&self) -> String {
let mut lines = Vec::new();
lines.push(format!("wallpaper='{}'", self.wallpaper));
lines.push(format!("background='{}'", self.background.hex()));
lines.push(format!("foreground='{}'", self.foreground.hex()));
lines.push(format!("cursor='{}'", self.cursor.hex()));
for (i, color) in self.colors.iter().enumerate() {
lines.push(format!("color{}='{}'", i, color.hex()));
}
lines.join("\n")
}
pub fn to_css_format(&self) -> String {
let mut lines = Vec::new();
lines.push(":root {".to_string());
lines.push(format!(" --background: {};", self.background.hex()));
lines.push(format!(" --foreground: {};", self.foreground.hex()));
lines.push(format!(" --cursor: {};", self.cursor.hex()));
for (i, color) in self.colors.iter().enumerate() {
lines.push(format!(" --color{}: {};", i, color.hex()));
}
lines.push("}".to_string());
lines.join("\n")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rgb_hex() {
let color = Rgb::new(1.0, 0.5, 0.0);
assert_eq!(color.hex(), "#FF7F00");
}
#[test]
fn test_rgb_luminance() {
let white = Rgb::new(1.0, 1.0, 1.0);
let black = Rgb::new(0.0, 0.0, 0.0);
assert!((white.luminance() - 1.0).abs() < 0.001);
assert!(black.luminance().abs() < 0.001);
}
#[test]
fn test_rgb_lightened() {
let color = Rgb::new(0.5, 0.5, 0.5);
let lighter = color.lightened(0.5);
assert!(lighter.r > color.r);
assert!(lighter.g > color.g);
assert!(lighter.b > color.b);
}
#[test]
fn test_color_scheme_json() {
let scheme = ColorScheme::new(
"/path/to/wallpaper.jpg".to_string(),
true,
Rgb::new(0.1, 0.1, 0.1),
Rgb::new(0.9, 0.9, 0.9),
Rgb::new(0.8, 0.8, 0.8),
vec![Rgb::new(0.0, 0.0, 0.0); 16],
);
let json = scheme.to_json().unwrap();
let parsed = ColorScheme::from_json(&json).unwrap();
assert_eq!(parsed.wallpaper, scheme.wallpaper);
assert_eq!(parsed.is_dark, scheme.is_dark);
}
}