use std::{collections::HashMap, fmt::Display};
use gpui::{hsla, Hsla, SharedString};
use serde::{de::Error, Deserialize, Deserializer};
use anyhow::Result;
#[inline]
pub fn hsl(h: f32, s: f32, l: f32) -> Hsla {
hsla(h / 360., s / 100.0, l / 100.0, 1.0)
}
pub trait Colorize: Sized {
fn opacity(&self, opacity: f32) -> Self;
fn divide(&self, divisor: f32) -> Self;
fn invert(&self) -> Self;
fn invert_l(&self) -> Self;
fn lighten(&self, amount: f32) -> Self;
fn darken(&self, amount: f32) -> Self;
fn apply(&self, base_color: Self) -> Self;
fn mix(&self, other: Self, factor: f32) -> Self;
fn hue(&self, hue: f32) -> Self;
fn saturation(&self, saturation: f32) -> Self;
fn lightness(&self, lightness: f32) -> Self;
fn to_hex(&self) -> String;
fn parse_hex(hex: &str) -> Result<Self>;
}
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,
}
}
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<Self> {
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)
}
fn hue(&self, hue: f32) -> Self {
let mut color = *self;
color.h = hue.clamp(0., 1.);
color
}
fn saturation(&self, saturation: f32) -> Self {
let mut color = *self;
color.s = saturation.clamp(0., 1.);
color
}
fn lightness(&self, lightness: f32) -> Self {
let mut color = *self;
color.l = lightness.clamp(0., 1.);
color
}
}
pub(crate) static DEFAULT_COLORS: once_cell::sync::Lazy<ShadcnColors> =
once_cell::sync::Lazy::new(|| {
serde_json::from_str(include_str!("./default-colors.json"))
.expect("failed to parse default-colors.json")
});
type ColorScales = HashMap<usize, ShadcnColor>;
mod color_scales {
use std::collections::HashMap;
use super::{ColorScales, ShadcnColor};
use serde::de::{Deserialize, Deserializer};
pub fn deserialize<'de, D>(deserializer: D) -> Result<ColorScales, D::Error>
where
D: Deserializer<'de>,
{
let mut map = HashMap::new();
for color in Vec::<ShadcnColor>::deserialize(deserializer)? {
map.insert(color.scale, color);
}
Ok(map)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ColorName {
Gray,
Red,
Orange,
Amber,
Yellow,
Lime,
Green,
Emerald,
Teal,
Cyan,
Sky,
Blue,
Indigo,
Violet,
Purple,
Fuchsia,
Pink,
Rose,
}
impl Display for ColorName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self)
}
}
impl From<&str> for ColorName {
fn from(value: &str) -> Self {
match value.to_lowercase().as_str() {
"gray" => ColorName::Gray,
"red" => ColorName::Red,
"orange" => ColorName::Orange,
"amber" => ColorName::Amber,
"yellow" => ColorName::Yellow,
"lime" => ColorName::Lime,
"green" => ColorName::Green,
"emerald" => ColorName::Emerald,
"teal" => ColorName::Teal,
"cyan" => ColorName::Cyan,
"sky" => ColorName::Sky,
"blue" => ColorName::Blue,
"indigo" => ColorName::Indigo,
"violet" => ColorName::Violet,
"purple" => ColorName::Purple,
"fuchsia" => ColorName::Fuchsia,
"pink" => ColorName::Pink,
"rose" => ColorName::Rose,
_ => ColorName::Gray,
}
}
}
impl From<SharedString> for ColorName {
fn from(value: SharedString) -> Self {
value.as_ref().into()
}
}
impl ColorName {
pub fn all() -> [Self; 18] {
[
ColorName::Gray,
ColorName::Red,
ColorName::Orange,
ColorName::Amber,
ColorName::Yellow,
ColorName::Lime,
ColorName::Green,
ColorName::Emerald,
ColorName::Teal,
ColorName::Cyan,
ColorName::Sky,
ColorName::Blue,
ColorName::Indigo,
ColorName::Violet,
ColorName::Purple,
ColorName::Fuchsia,
ColorName::Pink,
ColorName::Rose,
]
}
pub fn scale(&self, scale: usize) -> Hsla {
let colors = match self {
ColorName::Gray => &DEFAULT_COLORS.gray,
ColorName::Red => &DEFAULT_COLORS.red,
ColorName::Orange => &DEFAULT_COLORS.orange,
ColorName::Amber => &DEFAULT_COLORS.amber,
ColorName::Yellow => &DEFAULT_COLORS.yellow,
ColorName::Lime => &DEFAULT_COLORS.lime,
ColorName::Green => &DEFAULT_COLORS.green,
ColorName::Emerald => &DEFAULT_COLORS.emerald,
ColorName::Teal => &DEFAULT_COLORS.teal,
ColorName::Cyan => &DEFAULT_COLORS.cyan,
ColorName::Sky => &DEFAULT_COLORS.sky,
ColorName::Blue => &DEFAULT_COLORS.blue,
ColorName::Indigo => &DEFAULT_COLORS.indigo,
ColorName::Violet => &DEFAULT_COLORS.violet,
ColorName::Purple => &DEFAULT_COLORS.purple,
ColorName::Fuchsia => &DEFAULT_COLORS.fuchsia,
ColorName::Pink => &DEFAULT_COLORS.pink,
ColorName::Rose => &DEFAULT_COLORS.rose,
};
if let Some(color) = colors.get(&scale) {
color.hsla
} else {
colors.get(&500).unwrap().hsla
}
}
}
#[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,
}
fn from_hsl_channel<'de, D>(deserializer: D) -> Result<Hsla, D::Error>
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_COLORS.$color.get(&($scale as usize)) {
return color.hsla;
}
black()
}
}
};
}
macro_rules! color_methods {
($color:tt) => {
paste::paste! {
#[inline]
pub fn [<$color>](scale: usize) -> Hsla {
if let Some(color) = DEFAULT_COLORS.$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_COLORS.black.hsla
}
pub fn white() -> Hsla {
DEFAULT_COLORS.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");
}
#[test]
fn test_color_name() {
assert_eq!(ColorName::Purple.to_string(), "Purple");
assert_eq!(format!("{}", ColorName::Green), "Green");
assert_eq!(format!("{:?}", ColorName::Yellow), "Yellow");
let color = ColorName::Green;
assert_eq!(color.scale(500).to_hex(), "#21C55E");
assert_eq!(color.scale(1500).to_hex(), "#21C55E");
for name in ColorName::all().iter() {
let name1: ColorName = name.to_string().as_str().into();
assert_eq!(name1, *name);
}
}
#[test]
fn test_h_s_l() {
let color = hsl(260., 94., 80.);
assert_eq!(color.hue(200. / 360.), hsl(200., 94., 80.));
assert_eq!(color.saturation(74. / 100.), hsl(260., 74., 80.));
assert_eq!(color.lightness(74. / 100.), hsl(260., 94., 74.));
}
}