use std::collections::HashMap; use gpui::Hsla; use serde::{de::Error, Deserialize, Deserializer}; use crate::hsl; use anyhow::Result; pub trait Colorize: Sized { /// Returns a new color with the given opacity. /// /// The opacity is a value between 0.0 and 1.0, where 0.0 is fully transparent and 1.0 is fully opaque. fn opacity(&self, opacity: f32) -> Self; /// Returns a new color with each channel divided by the given divisor. /// /// The divisor in range of 0.0 .. 1.0 fn divide(&self, divisor: f32) -> Self; /// Return inverted color fn invert(&self) -> Self; /// Return inverted lightness fn invert_l(&self) -> Self; /// Return a new color with the lightness increased by the given factor. /// /// factor range: 0.0 .. 1.0 fn lighten(&self, amount: f32) -> Self; /// Return a new color with the darkness increased by the given factor. /// /// factor range: 0.0 .. 1.0 fn darken(&self, amount: f32) -> Self; /// Return a new color with the same lightness and alpha but different hue and saturation. fn apply(&self, base_color: Self) -> Self; /// Mix two colors together, the `factor` is a value between 0.0 and 1.0 for first color. fn mix(&self, other: Self, factor: f32) -> Self; /// Convert the color to a hex string. For example, "#F8FAFC". fn to_hex(&self) -> String; /// Parse a hex string to a color. fn parse_hex(hex: &str) -> Result; } impl Colorize for Hsla { fn opacity(&self, factor: f32) -> Self { Self { a: self.a * factor.clamp(0.0, 1.0), ..*self } } fn divide(&self, divisor: f32) -> Self { Self { a: divisor, ..*self } } fn invert(&self) -> Self { Self { h: 1.0 - self.h, s: 1.0 - self.s, l: 1.0 - self.l, a: self.a, } } fn invert_l(&self) -> Self { Self { l: 1.0 - self.l, ..*self } } fn lighten(&self, factor: f32) -> Self { let l = self.l * (1.0 + factor.clamp(0.0, 1.0)); Hsla { l, ..*self } } fn darken(&self, factor: f32) -> Self { let l = self.l * (1.0 - factor.clamp(0.0, 1.0)); Self { l, ..*self } } fn apply(&self, new_color: Self) -> Self { Hsla { h: new_color.h, s: new_color.s, l: self.l, a: self.a, } } /// Reference: /// https://github.com/bevyengine/bevy/blob/85eceb022da0326b47ac2b0d9202c9c9f01835bb/crates/bevy_color/src/hsla.rs#L112 fn mix(&self, other: Self, factor: f32) -> Self { let factor = factor.clamp(0.0, 1.0); let inv = 1.0 - factor; #[inline] fn lerp_hue(a: f32, b: f32, t: f32) -> f32 { let diff = (b - a + 180.0).rem_euclid(360.) - 180.; (a + diff * t).rem_euclid(360.0) } Hsla { h: lerp_hue(self.h * 360., other.h * 360., factor) / 360., s: self.s * factor + other.s * inv, l: self.l * factor + other.l * inv, a: self.a * factor + other.a * inv, } } fn to_hex(&self) -> String { let rgb = self.to_rgb(); if rgb.a < 1. { return format!( "#{:02X}{:02X}{:02X}{:02X}", ((rgb.r * 255.) as u32), ((rgb.g * 255.) as u32), ((rgb.b * 255.) as u32), ((self.a * 255.) as u32) ); } format!( "#{:02X}{:02X}{:02X}", ((rgb.r * 255.) as u32), ((rgb.g * 255.) as u32), ((rgb.b * 255.) as u32) ) } fn parse_hex(hex: &str) -> Result { let hex = hex.trim_start_matches('#'); let len = hex.len(); if len != 6 && len != 8 { return Err(anyhow::anyhow!("invalid hex color")); } let r = u8::from_str_radix(&hex[0..2], 16)? as f32 / 255.; let g = u8::from_str_radix(&hex[2..4], 16)? as f32 / 255.; let b = u8::from_str_radix(&hex[4..6], 16)? as f32 / 255.; let a = if len == 8 { u8::from_str_radix(&hex[6..8], 16)? as f32 / 255. } else { 1. }; let v = gpui::Rgba { r, g, b, a }; let color: Hsla = v.into(); Ok(color) } } pub(crate) static DEFAULT_COLOR: once_cell::sync::Lazy = once_cell::sync::Lazy::new(|| { serde_json::from_str(include_str!("../default-colors.json")) .expect("failed to parse default-json") }); type ColorScales = HashMap; mod color_scales { use std::collections::HashMap; use super::{ColorScales, ShadcnColor}; use serde::de::{Deserialize, Deserializer}; pub fn deserialize<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { let mut map = HashMap::new(); for color in Vec::::deserialize(deserializer)? { map.insert(color.scale, color); } Ok(map) } } #[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)] pub(crate) struct ShadcnColors { pub(crate) black: ShadcnColor, pub(crate) white: ShadcnColor, #[serde(with = "color_scales")] pub(crate) slate: ColorScales, #[serde(with = "color_scales")] pub(crate) gray: ColorScales, #[serde(with = "color_scales")] pub(crate) zinc: ColorScales, #[serde(with = "color_scales")] pub(crate) neutral: ColorScales, #[serde(with = "color_scales")] pub(crate) stone: ColorScales, #[serde(with = "color_scales")] pub(crate) red: ColorScales, #[serde(with = "color_scales")] pub(crate) orange: ColorScales, #[serde(with = "color_scales")] pub(crate) amber: ColorScales, #[serde(with = "color_scales")] pub(crate) yellow: ColorScales, #[serde(with = "color_scales")] pub(crate) lime: ColorScales, #[serde(with = "color_scales")] pub(crate) green: ColorScales, #[serde(with = "color_scales")] pub(crate) emerald: ColorScales, #[serde(with = "color_scales")] pub(crate) teal: ColorScales, #[serde(with = "color_scales")] pub(crate) cyan: ColorScales, #[serde(with = "color_scales")] pub(crate) sky: ColorScales, #[serde(with = "color_scales")] pub(crate) blue: ColorScales, #[serde(with = "color_scales")] pub(crate) indigo: ColorScales, #[serde(with = "color_scales")] pub(crate) violet: ColorScales, #[serde(with = "color_scales")] pub(crate) purple: ColorScales, #[serde(with = "color_scales")] pub(crate) fuchsia: ColorScales, #[serde(with = "color_scales")] pub(crate) pink: ColorScales, #[serde(with = "color_scales")] pub(crate) rose: ColorScales, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize)] pub(crate) struct ShadcnColor { #[serde(default)] pub(crate) scale: usize, #[serde(deserialize_with = "from_hsl_channel", alias = "hslChannel")] pub(crate) hsla: Hsla, } /// Deserialize Hsla from a string in the format "210 40% 98%" fn from_hsl_channel<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { let s: String = Deserialize::deserialize(deserializer).unwrap(); let mut parts = s.split_whitespace(); if parts.clone().count() != 3 { return Err(D::Error::custom( "expected hslChannel has 3 parts, e.g: '210 40% 98%'", )); } fn parse_number(s: &str) -> f32 { s.trim_end_matches('%') .parse() .expect("failed to parse number") } let (h, s, l) = ( parse_number(parts.next().unwrap()), parse_number(parts.next().unwrap()), parse_number(parts.next().unwrap()), ); Ok(hsl(h, s, l)) } macro_rules! color_method { ($color:tt, $scale:tt) => { paste::paste! { #[inline] #[allow(unused)] pub fn [<$color _ $scale>]() -> Hsla { if let Some(color) = DEFAULT_COLOR.$color.get(&($scale as usize)) { return color.hsla; } black() } } }; } macro_rules! color_methods { ($color:tt) => { paste::paste! { /// Get color by scale number. /// /// The possible scale numbers are: /// 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950 /// /// If the scale number is not found, it will return black color. #[inline] pub fn [<$color>](scale: usize) -> Hsla { if let Some(color) = DEFAULT_COLOR.$color.get(&scale) { return color.hsla; } black() } } color_method!($color, 50); color_method!($color, 100); color_method!($color, 200); color_method!($color, 300); color_method!($color, 400); color_method!($color, 500); color_method!($color, 600); color_method!($color, 700); color_method!($color, 800); color_method!($color, 900); color_method!($color, 950); }; } pub fn black() -> Hsla { DEFAULT_COLOR.black.hsla } pub fn white() -> Hsla { DEFAULT_COLOR.white.hsla } color_methods!(slate); color_methods!(gray); color_methods!(zinc); color_methods!(neutral); color_methods!(stone); color_methods!(red); color_methods!(orange); color_methods!(amber); color_methods!(yellow); color_methods!(lime); color_methods!(green); color_methods!(emerald); color_methods!(teal); color_methods!(cyan); color_methods!(sky); color_methods!(blue); color_methods!(indigo); color_methods!(violet); color_methods!(purple); color_methods!(fuchsia); color_methods!(pink); color_methods!(rose); #[cfg(test)] mod tests { use gpui::{rgb, rgba}; use super::*; #[test] fn test_default_colors() { assert_eq!(white(), hsl(0.0, 0.0, 100.0)); assert_eq!(black(), hsl(0.0, 0.0, 0.0)); assert_eq!(slate_50(), hsl(210.0, 40.0, 98.0)); assert_eq!(slate_100(), hsl(210.0, 40.0, 96.1)); assert_eq!(slate_900(), hsl(222.2, 47.4, 11.2)); assert_eq!(red_50(), hsl(0.0, 85.7, 97.3)); assert_eq!(yellow_100(), hsl(54.9, 96.7, 88.0)); assert_eq!(green_200(), hsl(141.0, 78.9, 85.1)); assert_eq!(cyan_300(), hsl(187.0, 92.4, 69.0)); assert_eq!(blue_400(), hsl(213.1, 93.9, 67.8)); assert_eq!(indigo_500(), hsl(238.7, 83.5, 66.7)); } #[test] fn test_to_hex_string() { let color: Hsla = rgb(0xf8fafc).into(); assert_eq!(color.to_hex(), "#F8FAFC"); let color: Hsla = rgb(0xfef2f2).into(); assert_eq!(color.to_hex(), "#FEF2F2"); let color: Hsla = rgba(0x0413fcaa).into(); assert_eq!(color.to_hex(), "#0413FCAA"); } #[test] fn test_from_hex_string() { let color: Hsla = Hsla::parse_hex("#F8FAFC").unwrap(); assert_eq!(color, rgb(0xf8fafc).into()); let color: Hsla = Hsla::parse_hex("#FEF2F2").unwrap(); assert_eq!(color, rgb(0xfef2f2).into()); let color: Hsla = Hsla::parse_hex("#0413FCAA").unwrap(); assert_eq!(color, rgba(0x0413fcaa).into()); } #[test] fn test_lighten() { let color = super::hsl(240.0, 5.0, 30.0); let color = color.lighten(0.5); assert_eq!(color.l, 0.45000002); let color = color.lighten(0.5); assert_eq!(color.l, 0.675); let color = color.lighten(0.1); assert_eq!(color.l, 0.7425); } #[test] fn test_darken() { let color = super::hsl(240.0, 5.0, 96.0); let color = color.darken(0.5); assert_eq!(color.l, 0.48); let color = color.darken(0.5); assert_eq!(color.l, 0.24); } #[test] fn test_mix() { let red = Hsla::parse_hex("#FF0000").unwrap(); let blue = Hsla::parse_hex("#0000FF").unwrap(); let green = Hsla::parse_hex("#00FF00").unwrap(); let yellow = Hsla::parse_hex("#FFFF00").unwrap(); assert_eq!(red.mix(blue, 0.5).to_hex(), "#FF00FF"); assert_eq!(green.mix(red, 0.5).to_hex(), "#FFFF00"); assert_eq!(blue.mix(yellow, 0.2).to_hex(), "#0098FF"); } }