use std::collections::HashMap; use gpui::Hsla; use serde::{de::Error, Deserialize, Deserializer}; use crate::theme::hsl; use anyhow::Result; pub(crate) trait ColorExt { fn to_hex_string(&self) -> String; fn parse_hex_string(hex: &str) -> Result; } impl ColorExt for Hsla { fn to_hex_string(&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_string(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_hsa_channel", alias = "hslChannel")] pub(crate) hsla: Hsla, } /// Deserialize Hsla from a string in the format "210 40% 98%" fn from_hsa_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! { #[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) => { 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_string(), "#F8FAFC"); let color: Hsla = rgb(0xfef2f2).into(); assert_eq!(color.to_hex_string(), "#FEF2F2"); let color: Hsla = rgba(0x0413fcaa).into(); assert_eq!(color.to_hex_string(), "#0413FCAA"); } #[test] fn test_from_hex_string() { let color: Hsla = Hsla::parse_hex_string("#F8FAFC").unwrap(); assert_eq!(color, rgb(0xf8fafc).into()); let color: Hsla = Hsla::parse_hex_string("#FEF2F2").unwrap(); assert_eq!(color, rgb(0xfef2f2).into()); let color: Hsla = Hsla::parse_hex_string("#0413FCAA").unwrap(); assert_eq!(color, rgba(0x0413fcaa).into()); } }