Skip to main content

gpui_component/theme/
color.rs

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