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