Skip to main content

rgpui_component/theme/
color.rs

1use std::{collections::HashMap, fmt::Display};
2
3use rgpui::{Hsla, SharedString, hsla};
4use serde::{Deserialize, Deserializer, de::Error as _};
5
6use anyhow::{Error, Result, anyhow};
7
8/// Create a [`gpui::Hsla`] color.
9///
10/// - h: 0..360.0
11/// - s: 0.0..100.0
12/// - l: 0.0..100.0
13#[inline]
14pub fn hsl(h: f32, s: f32, l: f32) -> Hsla {
15    hsla(h / 360., s / 100.0, l / 100.0, 1.0)
16}
17
18pub trait Colorize: Sized {
19    /// Returns a new color with the given opacity.
20    ///
21    /// The opacity is a value between 0.0 and 1.0, where 0.0 is fully transparent and 1.0 is fully opaque.
22    fn opacity(&self, opacity: f32) -> Self;
23    /// Returns a new color with each channel divided by the given divisor.
24    ///
25    /// The divisor in range of 0.0 .. 1.0
26    fn divide(&self, divisor: f32) -> Self;
27    /// Return inverted color
28    fn invert(&self) -> Self;
29    /// Return inverted lightness
30    fn invert_l(&self) -> Self;
31    /// Return a new color with the lightness increased by the given factor.
32    ///
33    /// factor range: 0.0 .. 1.0
34    fn lighten(&self, amount: f32) -> Self;
35    /// Return a new color with the darkness increased by the given factor.
36    ///
37    /// factor range: 0.0 .. 1.0
38    fn darken(&self, amount: f32) -> Self;
39    /// Return a new color with the same lightness and alpha but different hue and saturation.
40    fn apply(&self, base_color: Self) -> Self;
41
42    /// Mix two colors together, the `factor` is a value between 0.0 and 1.0 for first color.
43    fn mix(&self, other: Self, factor: f32) -> Self;
44    /// Mix two colors together in Oklab color space, the `factor` is a value between 0.0 and 1.0 for first color.
45    ///
46    /// This is similar to CSS `color-mix(in oklab, color1 factor%, color2)`.
47    fn mix_oklab(&self, other: Self, factor: f32) -> Self;
48    /// Change the `Hue` of the color by the given in range: 0.0 .. 1.0
49    fn hue(&self, hue: f32) -> Self;
50    /// Change the `Saturation` of the color by the given value in range: 0.0 .. 1.0
51    fn saturation(&self, saturation: f32) -> Self;
52    /// Change the `Lightness` of the color by the given value in range: 0.0 .. 1.0
53    fn lightness(&self, lightness: f32) -> Self;
54
55    /// Convert the color to a hex string. For example, "#F8FAFC".
56    fn to_hex(&self) -> String;
57    /// Parse a hex string to a color.
58    fn parse_hex(hex: &str) -> Result<Self>;
59}
60
61/// Helper functions for Oklab color space conversions
62mod oklab {
63    use rgpui::Rgba;
64
65    /// Convert sRGB component to linear RGB
66    #[inline]
67    fn to_linear(c: f32) -> f32 {
68        if c <= 0.04045 {
69            c / 12.92
70        } else {
71            ((c + 0.055) / 1.055).powf(2.4)
72        }
73    }
74
75    /// Convert linear RGB component to sRGB
76    #[inline]
77    fn from_linear(c: f32) -> f32 {
78        if c <= 0.0031308 {
79            c * 12.92
80        } else {
81            1.055 * c.powf(1.0 / 2.4) - 0.055
82        }
83    }
84
85    /// Convert RGB to Oklab color space
86    #[allow(non_snake_case)]
87    pub fn rgb_to_oklab(rgb: Rgba) -> (f32, f32, f32) {
88        // sRGB to linear RGB
89        let lr = to_linear(rgb.r);
90        let lg = to_linear(rgb.g);
91        let lb = to_linear(rgb.b);
92
93        // Linear RGB to LMS
94        let l = 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb;
95        let m = 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb;
96        let s = 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb;
97
98        // LMS to Oklab (using cube root)
99        let l_ = l.cbrt();
100        let m_ = m.cbrt();
101        let s_ = s.cbrt();
102
103        let L = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_;
104        let a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_;
105        let b = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_;
106
107        (L, a, b)
108    }
109
110    /// Convert Oklab to RGB color space
111    #[allow(non_snake_case)]
112    pub fn oklab_to_rgb(L: f32, a: f32, b: f32) -> Rgba {
113        // Oklab to LMS
114        let l_ = L + 0.3963377774 * a + 0.2158037573 * b;
115        let m_ = L - 0.1055613458 * a - 0.0638541728 * b;
116        let s_ = L - 0.0894841775 * a - 1.2914855480 * b;
117
118        let l = l_ * l_ * l_;
119        let m = m_ * m_ * m_;
120        let s = s_ * s_ * s_;
121
122        // LMS to Linear RGB
123        let lr = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
124        let lg = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
125        let lb = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
126
127        // Linear RGB to sRGB
128        Rgba {
129            r: from_linear(lr).clamp(0.0, 1.0),
130            g: from_linear(lg).clamp(0.0, 1.0),
131            b: from_linear(lb).clamp(0.0, 1.0),
132            a: 1.0,
133        }
134    }
135}
136
137impl Colorize for Hsla {
138    fn opacity(&self, factor: f32) -> Self {
139        Self {
140            a: self.a * factor.clamp(0.0, 1.0),
141            ..*self
142        }
143    }
144
145    fn divide(&self, divisor: f32) -> Self {
146        Self {
147            a: divisor,
148            ..*self
149        }
150    }
151
152    fn invert(&self) -> Self {
153        Self {
154            h: 1.0 - self.h,
155            s: 1.0 - self.s,
156            l: 1.0 - self.l,
157            a: self.a,
158        }
159    }
160
161    fn invert_l(&self) -> Self {
162        Self {
163            l: 1.0 - self.l,
164            ..*self
165        }
166    }
167
168    fn lighten(&self, factor: f32) -> Self {
169        let l = self.l * (1.0 + factor.clamp(0.0, 1.0));
170
171        Hsla { l, ..*self }
172    }
173
174    fn darken(&self, factor: f32) -> Self {
175        let l = self.l * (1.0 - factor.clamp(0.0, 1.0));
176
177        Self { l, ..*self }
178    }
179
180    fn apply(&self, new_color: Self) -> Self {
181        Hsla {
182            h: new_color.h,
183            s: new_color.s,
184            l: self.l,
185            a: self.a,
186        }
187    }
188
189    /// Reference:
190    /// https://github.com/bevyengine/bevy/blob/85eceb022da0326b47ac2b0d9202c9c9f01835bb/crates/bevy_color/src/hsla.rs#L112
191    fn mix(&self, other: Self, factor: f32) -> Self {
192        let factor = factor.clamp(0.0, 1.0);
193        let inv = 1.0 - factor;
194
195        #[inline]
196        fn lerp_hue(a: f32, b: f32, t: f32) -> f32 {
197            let diff = (b - a + 180.0).rem_euclid(360.) - 180.;
198            (a + diff * t).rem_euclid(360.0)
199        }
200
201        Hsla {
202            h: lerp_hue(self.h * 360., other.h * 360., factor) / 360.,
203            s: self.s * factor + other.s * inv,
204            l: self.l * factor + other.l * inv,
205            a: self.a * factor + other.a * inv,
206        }
207    }
208
209    #[allow(non_snake_case)]
210    fn mix_oklab(&self, other: Self, factor: f32) -> Self {
211        let factor = factor.clamp(0.0, 1.0);
212        let inv = 1.0 - factor;
213
214        // Interpolate alpha first
215        let result_alpha = self.a * factor + other.a * inv;
216
217        // Handle the case where result alpha is zero
218        if result_alpha == 0.0 {
219            return Self {
220                h: 0.0,
221                s: 0.0,
222                l: 0.0,
223                a: 0.0,
224            };
225        }
226
227        // Convert both colors to RGB
228        let rgb1 = self.to_rgb();
229        let rgb2 = other.to_rgb();
230
231        // Convert to Oklab color space
232        let (l1, a1, b1) = oklab::rgb_to_oklab(rgb1);
233        let (l2, a2, b2) = oklab::rgb_to_oklab(rgb2);
234
235        // Premultiply alpha in Oklab space (using alpha-premultiplied interpolation)
236        // This matches CSS color-mix behavior
237        let alpha1 = self.a;
238        let alpha2 = other.a;
239
240        // Premultiply
241        let l1_pm = l1 * alpha1;
242        let a1_pm = a1 * alpha1;
243        let b1_pm = b1 * alpha1;
244
245        let l2_pm = l2 * alpha2;
246        let a2_pm = a2 * alpha2;
247        let b2_pm = b2 * alpha2;
248
249        // Interpolate premultiplied values
250        let L_pm = l1_pm * factor + l2_pm * inv;
251        let a_pm = a1_pm * factor + a2_pm * inv;
252        let b_pm = b1_pm * factor + b2_pm * inv;
253
254        // Unpremultiply
255        let L = L_pm / result_alpha;
256        let a = a_pm / result_alpha;
257        let b = b_pm / result_alpha;
258
259        // Convert back to RGB
260        let mut rgb = oklab::oklab_to_rgb(L, a, b);
261        rgb.a = result_alpha;
262
263        // Convert RGB to HSLA
264        rgb.into()
265    }
266
267    fn to_hex(&self) -> String {
268        let rgb = self.to_rgb();
269
270        if rgb.a < 1. {
271            return format!(
272                "#{:02X}{:02X}{:02X}{:02X}",
273                ((rgb.r * 255.) as u32),
274                ((rgb.g * 255.) as u32),
275                ((rgb.b * 255.) as u32),
276                ((self.a * 255.) as u32)
277            );
278        }
279
280        format!(
281            "#{:02X}{:02X}{:02X}",
282            ((rgb.r * 255.) as u32),
283            ((rgb.g * 255.) as u32),
284            ((rgb.b * 255.) as u32)
285        )
286    }
287
288    fn parse_hex(hex: &str) -> Result<Self> {
289        let hex = hex.trim_start_matches('#');
290        let len = hex.len();
291        if len != 6 && len != 8 {
292            return Err(anyhow::anyhow!("invalid hex color"));
293        }
294
295        let r = u8::from_str_radix(&hex[0..2], 16)? as f32 / 255.;
296        let g = u8::from_str_radix(&hex[2..4], 16)? as f32 / 255.;
297        let b = u8::from_str_radix(&hex[4..6], 16)? as f32 / 255.;
298        let a = if len == 8 {
299            u8::from_str_radix(&hex[6..8], 16)? as f32 / 255.
300        } else {
301            1.
302        };
303
304        let v = rgpui::Rgba { r, g, b, a };
305        let color: Hsla = v.into();
306        Ok(color)
307    }
308
309    fn hue(&self, hue: f32) -> Self {
310        let mut color = *self;
311        color.h = hue.clamp(0., 1.);
312        color
313    }
314
315    fn saturation(&self, saturation: f32) -> Self {
316        let mut color = *self;
317        color.s = saturation.clamp(0., 1.);
318        color
319    }
320
321    fn lightness(&self, lightness: f32) -> Self {
322        let mut color = *self;
323        color.l = lightness.clamp(0., 1.);
324        color
325    }
326}
327
328pub(crate) static DEFAULT_COLORS: once_cell::sync::Lazy<ShadcnColors> =
329    once_cell::sync::Lazy::new(|| {
330        serde_json::from_str(include_str!("./default-colors.json"))
331            .expect("failed to parse default-colors.json")
332    });
333
334type ColorScales = HashMap<usize, ShadcnColor>;
335
336mod color_scales {
337    use std::collections::HashMap;
338
339    use super::{ColorScales, ShadcnColor};
340
341    use serde::de::{Deserialize, Deserializer};
342
343    pub fn deserialize<'de, D>(deserializer: D) -> Result<ColorScales, D::Error>
344    where
345        D: Deserializer<'de>,
346    {
347        let mut map = HashMap::new();
348        for color in Vec::<ShadcnColor>::deserialize(deserializer)? {
349            map.insert(color.scale, color);
350        }
351        Ok(map)
352    }
353}
354
355/// Enum representing the available color names.
356#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
357pub enum ColorName {
358    White,
359    Black,
360    Neutral,
361    Gray,
362    Red,
363    Orange,
364    Amber,
365    Yellow,
366    Lime,
367    Green,
368    Emerald,
369    Teal,
370    Cyan,
371    Sky,
372    Blue,
373    Indigo,
374    Violet,
375    Purple,
376    Fuchsia,
377    Pink,
378    Rose,
379}
380
381impl Display for ColorName {
382    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
383        write!(f, "{:?}", self)
384    }
385}
386
387// Strict color name parser.
388impl TryFrom<&str> for ColorName {
389    type Error = anyhow::Error;
390    fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
391        match value.to_lowercase().as_str() {
392            "white" => Ok(ColorName::White),
393            "black" => Ok(ColorName::Black),
394            "neutral" => Ok(ColorName::Neutral),
395            "gray" => Ok(ColorName::Gray),
396            "red" => Ok(ColorName::Red),
397            "orange" => Ok(ColorName::Orange),
398            "amber" => Ok(ColorName::Amber),
399            "yellow" => Ok(ColorName::Yellow),
400            "lime" => Ok(ColorName::Lime),
401            "green" => Ok(ColorName::Green),
402            "emerald" => Ok(ColorName::Emerald),
403            "teal" => Ok(ColorName::Teal),
404            "cyan" => Ok(ColorName::Cyan),
405            "sky" => Ok(ColorName::Sky),
406            "blue" => Ok(ColorName::Blue),
407            "indigo" => Ok(ColorName::Indigo),
408            "violet" => Ok(ColorName::Violet),
409            "purple" => Ok(ColorName::Purple),
410            "fuchsia" => Ok(ColorName::Fuchsia),
411            "pink" => Ok(ColorName::Pink),
412            "rose" => Ok(ColorName::Rose),
413            _ => Err(anyhow::anyhow!("Invalid color name")),
414        }
415    }
416}
417
418impl TryFrom<SharedString> for ColorName {
419    type Error = anyhow::Error;
420    fn try_from(value: SharedString) -> std::result::Result<Self, Self::Error> {
421        value.as_ref().try_into()
422    }
423}
424
425impl ColorName {
426    /// Returns all available color names.
427    pub fn all() -> [Self; 19] {
428        [
429            ColorName::Neutral,
430            ColorName::Gray,
431            ColorName::Red,
432            ColorName::Orange,
433            ColorName::Amber,
434            ColorName::Yellow,
435            ColorName::Lime,
436            ColorName::Green,
437            ColorName::Emerald,
438            ColorName::Teal,
439            ColorName::Cyan,
440            ColorName::Sky,
441            ColorName::Blue,
442            ColorName::Indigo,
443            ColorName::Violet,
444            ColorName::Purple,
445            ColorName::Fuchsia,
446            ColorName::Pink,
447            ColorName::Rose,
448        ]
449    }
450
451    /// Returns the color for the given scale.
452    ///
453    /// The `scale` is any of `[50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]`
454    /// falls back to 500 if out of range.
455    pub fn scale(&self, scale: usize) -> Hsla {
456        if self == &ColorName::White {
457            return DEFAULT_COLORS.white.hsla;
458        }
459        if self == &ColorName::Black {
460            return DEFAULT_COLORS.black.hsla;
461        }
462
463        let colors = match self {
464            ColorName::Neutral => &DEFAULT_COLORS.neutral,
465            ColorName::Gray => &DEFAULT_COLORS.gray,
466            ColorName::Red => &DEFAULT_COLORS.red,
467            ColorName::Orange => &DEFAULT_COLORS.orange,
468            ColorName::Amber => &DEFAULT_COLORS.amber,
469            ColorName::Yellow => &DEFAULT_COLORS.yellow,
470            ColorName::Lime => &DEFAULT_COLORS.lime,
471            ColorName::Green => &DEFAULT_COLORS.green,
472            ColorName::Emerald => &DEFAULT_COLORS.emerald,
473            ColorName::Teal => &DEFAULT_COLORS.teal,
474            ColorName::Cyan => &DEFAULT_COLORS.cyan,
475            ColorName::Sky => &DEFAULT_COLORS.sky,
476            ColorName::Blue => &DEFAULT_COLORS.blue,
477            ColorName::Indigo => &DEFAULT_COLORS.indigo,
478            ColorName::Violet => &DEFAULT_COLORS.violet,
479            ColorName::Purple => &DEFAULT_COLORS.purple,
480            ColorName::Fuchsia => &DEFAULT_COLORS.fuchsia,
481            ColorName::Pink => &DEFAULT_COLORS.pink,
482            ColorName::Rose => &DEFAULT_COLORS.rose,
483            _ => unreachable!(),
484        };
485
486        if let Some(color) = colors.get(&scale) {
487            color.hsla
488        } else {
489            colors.get(&500).unwrap().hsla
490        }
491    }
492}
493
494#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
495pub(crate) struct ShadcnColors {
496    pub(crate) black: ShadcnColor,
497    pub(crate) white: ShadcnColor,
498    #[serde(with = "color_scales")]
499    pub(crate) slate: ColorScales,
500    #[serde(with = "color_scales")]
501    pub(crate) gray: ColorScales,
502    #[serde(with = "color_scales")]
503    pub(crate) zinc: ColorScales,
504    #[serde(with = "color_scales")]
505    pub(crate) neutral: ColorScales,
506    #[serde(with = "color_scales")]
507    pub(crate) stone: ColorScales,
508    #[serde(with = "color_scales")]
509    pub(crate) red: ColorScales,
510    #[serde(with = "color_scales")]
511    pub(crate) orange: ColorScales,
512    #[serde(with = "color_scales")]
513    pub(crate) amber: ColorScales,
514    #[serde(with = "color_scales")]
515    pub(crate) yellow: ColorScales,
516    #[serde(with = "color_scales")]
517    pub(crate) lime: ColorScales,
518    #[serde(with = "color_scales")]
519    pub(crate) green: ColorScales,
520    #[serde(with = "color_scales")]
521    pub(crate) emerald: ColorScales,
522    #[serde(with = "color_scales")]
523    pub(crate) teal: ColorScales,
524    #[serde(with = "color_scales")]
525    pub(crate) cyan: ColorScales,
526    #[serde(with = "color_scales")]
527    pub(crate) sky: ColorScales,
528    #[serde(with = "color_scales")]
529    pub(crate) blue: ColorScales,
530    #[serde(with = "color_scales")]
531    pub(crate) indigo: ColorScales,
532    #[serde(with = "color_scales")]
533    pub(crate) violet: ColorScales,
534    #[serde(with = "color_scales")]
535    pub(crate) purple: ColorScales,
536    #[serde(with = "color_scales")]
537    pub(crate) fuchsia: ColorScales,
538    #[serde(with = "color_scales")]
539    pub(crate) pink: ColorScales,
540    #[serde(with = "color_scales")]
541    pub(crate) rose: ColorScales,
542}
543
544#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize)]
545pub(crate) struct ShadcnColor {
546    #[serde(default)]
547    pub(crate) scale: usize,
548    #[serde(deserialize_with = "from_hsl_channel", alias = "hslChannel")]
549    pub(crate) hsla: Hsla,
550}
551
552/// Deserialize Hsla from a string in the format "210 40% 98%"
553fn from_hsl_channel<'de, D>(deserializer: D) -> Result<Hsla, D::Error>
554where
555    D: Deserializer<'de>,
556{
557    let s: String = Deserialize::deserialize(deserializer).unwrap();
558
559    let mut parts = s.split_whitespace();
560    if parts.clone().count() != 3 {
561        return Err(D::Error::custom(
562            "expected hslChannel has 3 parts, e.g: '210 40% 98%'",
563        ));
564    }
565
566    fn parse_number(s: &str) -> f32 {
567        s.trim_end_matches('%')
568            .parse()
569            .expect("failed to parse number")
570    }
571
572    let (h, s, l) = (
573        parse_number(parts.next().unwrap()),
574        parse_number(parts.next().unwrap()),
575        parse_number(parts.next().unwrap()),
576    );
577
578    Ok(hsl(h, s, l))
579}
580
581macro_rules! color_method {
582    ($color:tt, $scale:tt) => {
583        paste::paste! {
584            #[inline]
585            #[allow(unused)]
586            pub fn [<$color _ $scale>]() -> Hsla {
587                if let Some(color) = DEFAULT_COLORS.$color.get(&($scale as usize)) {
588                    return color.hsla;
589                }
590
591                black()
592            }
593        }
594    };
595}
596
597macro_rules! color_methods {
598    ($color:tt) => {
599        paste::paste! {
600            /// Get color by scale number.
601            ///
602            /// The possible scale numbers are:
603            /// 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950
604            ///
605            /// If the scale number is not found, it will return black color.
606            #[inline]
607            pub fn [<$color>](scale: usize) -> Hsla {
608                if let Some(color) = DEFAULT_COLORS.$color.get(&scale) {
609                    return color.hsla;
610                }
611
612                black()
613            }
614        }
615
616        color_method!($color, 50);
617        color_method!($color, 100);
618        color_method!($color, 200);
619        color_method!($color, 300);
620        color_method!($color, 400);
621        color_method!($color, 500);
622        color_method!($color, 600);
623        color_method!($color, 700);
624        color_method!($color, 800);
625        color_method!($color, 900);
626        color_method!($color, 950);
627    };
628}
629
630pub fn black() -> Hsla {
631    DEFAULT_COLORS.black.hsla
632}
633
634pub fn white() -> Hsla {
635    DEFAULT_COLORS.white.hsla
636}
637
638color_methods!(slate);
639color_methods!(gray);
640color_methods!(zinc);
641color_methods!(neutral);
642color_methods!(stone);
643color_methods!(red);
644color_methods!(orange);
645color_methods!(amber);
646color_methods!(yellow);
647color_methods!(lime);
648color_methods!(green);
649color_methods!(emerald);
650color_methods!(teal);
651color_methods!(cyan);
652color_methods!(sky);
653color_methods!(blue);
654color_methods!(indigo);
655color_methods!(violet);
656color_methods!(purple);
657color_methods!(fuchsia);
658color_methods!(pink);
659color_methods!(rose);
660
661/// Try to parse the color, HEX or [Tailwind Color](https://tailwindcss.com/docs/colors) expression.
662///
663/// # Parameter `color` should be one string value listed below:
664///
665/// - `#RRGGBB` - The HEX color string.
666/// - `#RRGGBBAA` - The HEX color string with alpha.
667///
668/// Or the Tailwind Color format:
669///
670/// - `name` - The color name `black`, `white`, or any other defined in `crate::color`.
671/// - `name-scale` - The color name with scale.
672/// - `name/opacity` - The color name with opacity, `opacity` should be an integer between 0 and 100.
673/// - `name-scale/opacity` - The color name with scale and opacity.
674///
675pub fn try_parse_color(color: &str) -> Result<Hsla> {
676    if color.starts_with("#") {
677        let rgba = rgpui::Rgba::try_from(color)?;
678        return Ok(rgba.into());
679    }
680
681    let mut name = String::new();
682    let mut scale = None;
683    let mut opacity = None;
684    // 0: name, 1: scale, 2: opacity
685    let mut state = 0;
686    let mut part = String::new();
687
688    for c in color.chars() {
689        match c {
690            '-' if state == 0 => {
691                name = std::mem::take(&mut part);
692                state = 1;
693            }
694            '/' if state <= 1 => {
695                if state == 0 {
696                    name = std::mem::take(&mut part);
697                } else if state == 1 {
698                    scale = part.parse::<usize>().ok();
699                    part.clear();
700                }
701                state = 2;
702            }
703            _ => part.push(c),
704        }
705    }
706
707    match state {
708        0 => name = part,
709        1 => scale = part.parse::<usize>().ok(),
710        2 => opacity = part.parse::<f32>().ok(),
711        _ => {}
712    }
713
714    if name.is_empty() {
715        return Err(anyhow!("Empty color name"));
716    }
717
718    let mut hsla = match name.as_str() {
719        "black" => Ok::<Hsla, Error>(crate::black()),
720        "white" => Ok(crate::white()),
721        _ => {
722            let color_name = ColorName::try_from(name.as_str())?;
723            if let Some(scale) = scale {
724                Ok(color_name.scale(scale))
725            } else {
726                Ok(color_name.scale(500))
727            }
728        }
729    }?;
730
731    if let Some(opacity) = opacity {
732        if opacity > 100. {
733            return Err(anyhow!("Invalid color opacity"));
734        }
735        hsla = hsla.opacity(opacity / 100.);
736    }
737
738    Ok(hsla)
739}
740
741#[cfg(test)]
742mod tests {
743    use rgpui::{rgb, rgba};
744
745    use super::*;
746
747    #[test]
748    fn test_default_colors() {
749        assert_eq!(white(), hsl(0.0, 0.0, 100.0));
750        assert_eq!(black(), hsl(0.0, 0.0, 0.0));
751
752        assert_eq!(slate_50(), hsl(210.0, 40.0, 98.0));
753        assert_eq!(slate_100(), hsl(210.0, 40.0, 96.1));
754        assert_eq!(slate_900(), hsl(222.2, 47.4, 11.2));
755
756        assert_eq!(red_50(), hsl(0.0, 85.7, 97.3));
757        assert_eq!(yellow_100(), hsl(54.9, 96.7, 88.0));
758        assert_eq!(green_200(), hsl(141.0, 78.9, 85.1));
759        assert_eq!(cyan_300(), hsl(187.0, 92.4, 69.0));
760        assert_eq!(blue_400(), hsl(213.1, 93.9, 67.8));
761        assert_eq!(indigo_500(), hsl(238.7, 83.5, 66.7));
762    }
763
764    #[test]
765    fn test_to_hex_string() {
766        let color: Hsla = rgb(0xf8fafc).into();
767        assert_eq!(color.to_hex(), "#F8FAFC");
768
769        let color: Hsla = rgb(0xfef2f2).into();
770        assert_eq!(color.to_hex(), "#FEF2F2");
771
772        let color: Hsla = rgba(0x0413fcaa).into();
773        assert_eq!(color.to_hex(), "#0413FCAA");
774    }
775
776    #[test]
777    fn test_from_hex_string() {
778        let color: Hsla = Hsla::parse_hex("#F8FAFC").unwrap();
779        assert_eq!(color, rgb(0xf8fafc).into());
780
781        let color: Hsla = Hsla::parse_hex("#FEF2F2").unwrap();
782        assert_eq!(color, rgb(0xfef2f2).into());
783
784        let color: Hsla = Hsla::parse_hex("#0413FCAA").unwrap();
785        assert_eq!(color, rgba(0x0413fcaa).into());
786    }
787
788    #[test]
789    fn test_lighten() {
790        let color = super::hsl(240.0, 5.0, 30.0);
791        let color = color.lighten(0.5);
792        assert_eq!(color.l, 0.45000002);
793        let color = color.lighten(0.5);
794        assert_eq!(color.l, 0.675);
795        let color = color.lighten(0.1);
796        assert_eq!(color.l, 0.7425);
797    }
798
799    #[test]
800    fn test_darken() {
801        let color = super::hsl(240.0, 5.0, 96.0);
802        let color = color.darken(0.5);
803        assert_eq!(color.l, 0.48);
804        let color = color.darken(0.5);
805        assert_eq!(color.l, 0.24);
806    }
807
808    #[test]
809    fn test_mix() {
810        let red = Hsla::parse_hex("#FF0000").unwrap();
811        let blue = Hsla::parse_hex("#0000FF").unwrap();
812        let green = Hsla::parse_hex("#00FF00").unwrap();
813        let yellow = Hsla::parse_hex("#FFFF00").unwrap();
814
815        assert_eq!(red.mix(blue, 0.5).to_hex(), "#FF00FF");
816        assert_eq!(green.mix(red, 0.5).to_hex(), "#FFFF00");
817        assert_eq!(blue.mix(yellow, 0.2).to_hex(), "#0098FF");
818    }
819
820    #[test]
821    fn test_mix_oklab() {
822        let red = Hsla::parse_hex("#FF0000").unwrap();
823        let blue = Hsla::parse_hex("#0000FF").unwrap();
824        let transparent = rgpui::Hsla {
825            h: 0.0,
826            s: 0.0,
827            l: 0.0,
828            a: 0.0,
829        };
830
831        // Test mixing red with transparent (similar to CSS color-mix example)
832        // color-mix(in oklab, red 20%, transparent) should give red with 20% opacity
833        let result = red.mix_oklab(transparent, 0.2);
834        assert!((result.a - 0.2).abs() < 0.01); // Alpha should be 20%
835
836        // The color should remain red (hue should be preserved)
837        let rgb_result = result.to_rgb();
838        let rgb_red = red.to_rgb();
839        // Allow some tolerance due to color space conversions
840        assert!(
841            (rgb_result.r - rgb_red.r).abs() < 0.05,
842            "Red channel should be preserved"
843        );
844        assert!(rgb_result.g < 0.05, "Green channel should be near 0");
845        assert!(rgb_result.b < 0.05, "Blue channel should be near 0");
846
847        // Test basic color mixing in Oklab space
848        let purple = red.mix_oklab(blue, 0.5);
849        // Oklab mixing should produce different results than HSL mixing
850        let purple_hsl = red.mix(blue, 0.5);
851        assert_ne!(purple.to_hex(), purple_hsl.to_hex());
852
853        // Test factor boundaries (allowing small floating point errors)
854        let result_0 = red.mix_oklab(blue, 0.0);
855        let result_1 = red.mix_oklab(blue, 1.0);
856
857        // Check that result is close to expected (within 1 color unit per channel)
858        let rgb_0 = result_0.to_rgb();
859        let rgb_blue = blue.to_rgb();
860        assert!((rgb_0.r - rgb_blue.r).abs() < 0.01);
861        assert!((rgb_0.g - rgb_blue.g).abs() < 0.01);
862        assert!((rgb_0.b - rgb_blue.b).abs() < 0.01);
863
864        let rgb_1 = result_1.to_rgb();
865        let rgb_red = red.to_rgb();
866        assert!((rgb_1.r - rgb_red.r).abs() < 0.01);
867        assert!((rgb_1.g - rgb_red.g).abs() < 0.01);
868        assert!((rgb_1.b - rgb_red.b).abs() < 0.01);
869    }
870
871    #[test]
872    fn test_color_name() {
873        assert_eq!(ColorName::Purple.to_string(), "Purple");
874        assert_eq!(format!("{}", ColorName::Green), "Green");
875        assert_eq!(format!("{:?}", ColorName::Yellow), "Yellow");
876
877        let color = ColorName::Green;
878        assert_eq!(color.scale(500).to_hex(), "#21C55E");
879        assert_eq!(color.scale(1500).to_hex(), "#21C55E");
880
881        for name in ColorName::all().iter() {
882            let name1: ColorName = name.to_string().as_str().try_into().unwrap();
883            assert_eq!(name1, *name);
884        }
885    }
886
887    #[test]
888    fn test_h_s_l() {
889        let color = hsl(260., 94., 80.);
890        assert_eq!(color.hue(200. / 360.), hsl(200., 94., 80.));
891        assert_eq!(color.saturation(74. / 100.), hsl(260., 74., 80.));
892        assert_eq!(color.lightness(74. / 100.), hsl(260., 94., 74.));
893    }
894
895    #[test]
896    fn test_try_parse_color() {
897        assert_eq!(
898            try_parse_color("#F2F200").ok(),
899            Some(hsla(0.16666667, 1., 0.4745098, 1.0))
900        );
901        assert_eq!(
902            try_parse_color("#00f21888").ok(),
903            Some(hsla(0.34986225, 1.0, 0.4745098, 0.53333336))
904        );
905        assert_eq!(try_parse_color("black").ok(), Some(crate::black()));
906        assert_eq!(try_parse_color("white-800").ok(), Some(crate::white()));
907        assert_eq!(try_parse_color("red").ok(), Some(crate::red_500()));
908        assert_eq!(try_parse_color("blue-600").ok(), Some(crate::blue_600()));
909        assert_eq!(
910            try_parse_color("pink/33").ok(),
911            Some(crate::pink_500().opacity(0.33))
912        );
913        assert_eq!(
914            try_parse_color("orange-300/66").ok(),
915            Some(crate::orange_300().opacity(0.66))
916        );
917    }
918}