gpui_component/theme/
color.rs

1use std::{collections::HashMap, fmt::Display};
2
3use gpui::{hsla, Hsla, SharedString};
4use serde::{de::Error, Deserialize, Deserializer};
5
6use anyhow::Result;
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    /// Change the `Hue` of the color by the given in range: 0.0 .. 1.0
45    fn hue(&self, hue: f32) -> Self;
46    /// Change the `Saturation` of the color by the given value in range: 0.0 .. 1.0
47    fn saturation(&self, saturation: f32) -> Self;
48    /// Change the `Lightness` of the color by the given value in range: 0.0 .. 1.0
49    fn lightness(&self, lightness: f32) -> Self;
50
51    /// Convert the color to a hex string. For example, "#F8FAFC".
52    fn to_hex(&self) -> String;
53    /// Parse a hex string to a color.
54    fn parse_hex(hex: &str) -> Result<Self>;
55}
56
57impl Colorize for Hsla {
58    fn opacity(&self, factor: f32) -> Self {
59        Self {
60            a: self.a * factor.clamp(0.0, 1.0),
61            ..*self
62        }
63    }
64
65    fn divide(&self, divisor: f32) -> Self {
66        Self {
67            a: divisor,
68            ..*self
69        }
70    }
71
72    fn invert(&self) -> Self {
73        Self {
74            h: 1.0 - self.h,
75            s: 1.0 - self.s,
76            l: 1.0 - self.l,
77            a: self.a,
78        }
79    }
80
81    fn invert_l(&self) -> Self {
82        Self {
83            l: 1.0 - self.l,
84            ..*self
85        }
86    }
87
88    fn lighten(&self, factor: f32) -> Self {
89        let l = self.l * (1.0 + factor.clamp(0.0, 1.0));
90
91        Hsla { l, ..*self }
92    }
93
94    fn darken(&self, factor: f32) -> Self {
95        let l = self.l * (1.0 - factor.clamp(0.0, 1.0));
96
97        Self { l, ..*self }
98    }
99
100    fn apply(&self, new_color: Self) -> Self {
101        Hsla {
102            h: new_color.h,
103            s: new_color.s,
104            l: self.l,
105            a: self.a,
106        }
107    }
108
109    /// Reference:
110    /// https://github.com/bevyengine/bevy/blob/85eceb022da0326b47ac2b0d9202c9c9f01835bb/crates/bevy_color/src/hsla.rs#L112
111    fn mix(&self, other: Self, factor: f32) -> Self {
112        let factor = factor.clamp(0.0, 1.0);
113        let inv = 1.0 - factor;
114
115        #[inline]
116        fn lerp_hue(a: f32, b: f32, t: f32) -> f32 {
117            let diff = (b - a + 180.0).rem_euclid(360.) - 180.;
118            (a + diff * t).rem_euclid(360.0)
119        }
120
121        Hsla {
122            h: lerp_hue(self.h * 360., other.h * 360., factor) / 360.,
123            s: self.s * factor + other.s * inv,
124            l: self.l * factor + other.l * inv,
125            a: self.a * factor + other.a * inv,
126        }
127    }
128
129    fn to_hex(&self) -> String {
130        let rgb = self.to_rgb();
131
132        if rgb.a < 1. {
133            return format!(
134                "#{:02X}{:02X}{:02X}{:02X}",
135                ((rgb.r * 255.) as u32),
136                ((rgb.g * 255.) as u32),
137                ((rgb.b * 255.) as u32),
138                ((self.a * 255.) as u32)
139            );
140        }
141
142        format!(
143            "#{:02X}{:02X}{:02X}",
144            ((rgb.r * 255.) as u32),
145            ((rgb.g * 255.) as u32),
146            ((rgb.b * 255.) as u32)
147        )
148    }
149
150    fn parse_hex(hex: &str) -> Result<Self> {
151        let hex = hex.trim_start_matches('#');
152        let len = hex.len();
153        if len != 6 && len != 8 {
154            return Err(anyhow::anyhow!("invalid hex color"));
155        }
156
157        let r = u8::from_str_radix(&hex[0..2], 16)? as f32 / 255.;
158        let g = u8::from_str_radix(&hex[2..4], 16)? as f32 / 255.;
159        let b = u8::from_str_radix(&hex[4..6], 16)? as f32 / 255.;
160        let a = if len == 8 {
161            u8::from_str_radix(&hex[6..8], 16)? as f32 / 255.
162        } else {
163            1.
164        };
165
166        let v = gpui::Rgba { r, g, b, a };
167        let color: Hsla = v.into();
168        Ok(color)
169    }
170
171    fn hue(&self, hue: f32) -> Self {
172        let mut color = *self;
173        color.h = hue.clamp(0., 1.);
174        color
175    }
176
177    fn saturation(&self, saturation: f32) -> Self {
178        let mut color = *self;
179        color.s = saturation.clamp(0., 1.);
180        color
181    }
182
183    fn lightness(&self, lightness: f32) -> Self {
184        let mut color = *self;
185        color.l = lightness.clamp(0., 1.);
186        color
187    }
188}
189
190pub(crate) static DEFAULT_COLORS: once_cell::sync::Lazy<ShadcnColors> =
191    once_cell::sync::Lazy::new(|| {
192        serde_json::from_str(include_str!("./default-colors.json"))
193            .expect("failed to parse default-colors.json")
194    });
195
196type ColorScales = HashMap<usize, ShadcnColor>;
197
198mod color_scales {
199    use std::collections::HashMap;
200
201    use super::{ColorScales, ShadcnColor};
202
203    use serde::de::{Deserialize, Deserializer};
204
205    pub fn deserialize<'de, D>(deserializer: D) -> Result<ColorScales, D::Error>
206    where
207        D: Deserializer<'de>,
208    {
209        let mut map = HashMap::new();
210        for color in Vec::<ShadcnColor>::deserialize(deserializer)? {
211            map.insert(color.scale, color);
212        }
213        Ok(map)
214    }
215}
216
217/// Enum representing the available color names.
218#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
219pub enum ColorName {
220    Gray,
221    Red,
222    Orange,
223    Amber,
224    Yellow,
225    Lime,
226    Green,
227    Emerald,
228    Teal,
229    Cyan,
230    Sky,
231    Blue,
232    Indigo,
233    Violet,
234    Purple,
235    Fuchsia,
236    Pink,
237    Rose,
238}
239
240impl Display for ColorName {
241    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242        write!(f, "{:?}", self)
243    }
244}
245
246impl From<&str> for ColorName {
247    fn from(value: &str) -> Self {
248        match value.to_lowercase().as_str() {
249            "gray" => ColorName::Gray,
250            "red" => ColorName::Red,
251            "orange" => ColorName::Orange,
252            "amber" => ColorName::Amber,
253            "yellow" => ColorName::Yellow,
254            "lime" => ColorName::Lime,
255            "green" => ColorName::Green,
256            "emerald" => ColorName::Emerald,
257            "teal" => ColorName::Teal,
258            "cyan" => ColorName::Cyan,
259            "sky" => ColorName::Sky,
260            "blue" => ColorName::Blue,
261            "indigo" => ColorName::Indigo,
262            "violet" => ColorName::Violet,
263            "purple" => ColorName::Purple,
264            "fuchsia" => ColorName::Fuchsia,
265            "pink" => ColorName::Pink,
266            "rose" => ColorName::Rose,
267            _ => ColorName::Gray,
268        }
269    }
270}
271
272impl From<SharedString> for ColorName {
273    fn from(value: SharedString) -> Self {
274        value.as_ref().into()
275    }
276}
277
278impl ColorName {
279    /// Returns all available color names.
280    pub fn all() -> [Self; 18] {
281        [
282            ColorName::Gray,
283            ColorName::Red,
284            ColorName::Orange,
285            ColorName::Amber,
286            ColorName::Yellow,
287            ColorName::Lime,
288            ColorName::Green,
289            ColorName::Emerald,
290            ColorName::Teal,
291            ColorName::Cyan,
292            ColorName::Sky,
293            ColorName::Blue,
294            ColorName::Indigo,
295            ColorName::Violet,
296            ColorName::Purple,
297            ColorName::Fuchsia,
298            ColorName::Pink,
299            ColorName::Rose,
300        ]
301    }
302
303    /// Returns the color for the given scale.
304    ///
305    /// The `scale` is any of `[50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950]`
306    /// falls back to 500 if out of range.
307    pub fn scale(&self, scale: usize) -> Hsla {
308        let colors = match self {
309            ColorName::Gray => &DEFAULT_COLORS.gray,
310            ColorName::Red => &DEFAULT_COLORS.red,
311            ColorName::Orange => &DEFAULT_COLORS.orange,
312            ColorName::Amber => &DEFAULT_COLORS.amber,
313            ColorName::Yellow => &DEFAULT_COLORS.yellow,
314            ColorName::Lime => &DEFAULT_COLORS.lime,
315            ColorName::Green => &DEFAULT_COLORS.green,
316            ColorName::Emerald => &DEFAULT_COLORS.emerald,
317            ColorName::Teal => &DEFAULT_COLORS.teal,
318            ColorName::Cyan => &DEFAULT_COLORS.cyan,
319            ColorName::Sky => &DEFAULT_COLORS.sky,
320            ColorName::Blue => &DEFAULT_COLORS.blue,
321            ColorName::Indigo => &DEFAULT_COLORS.indigo,
322            ColorName::Violet => &DEFAULT_COLORS.violet,
323            ColorName::Purple => &DEFAULT_COLORS.purple,
324            ColorName::Fuchsia => &DEFAULT_COLORS.fuchsia,
325            ColorName::Pink => &DEFAULT_COLORS.pink,
326            ColorName::Rose => &DEFAULT_COLORS.rose,
327        };
328
329        if let Some(color) = colors.get(&scale) {
330            color.hsla
331        } else {
332            colors.get(&500).unwrap().hsla
333        }
334    }
335}
336
337#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
338pub(crate) struct ShadcnColors {
339    pub(crate) black: ShadcnColor,
340    pub(crate) white: ShadcnColor,
341    #[serde(with = "color_scales")]
342    pub(crate) slate: ColorScales,
343    #[serde(with = "color_scales")]
344    pub(crate) gray: ColorScales,
345    #[serde(with = "color_scales")]
346    pub(crate) zinc: ColorScales,
347    #[serde(with = "color_scales")]
348    pub(crate) neutral: ColorScales,
349    #[serde(with = "color_scales")]
350    pub(crate) stone: ColorScales,
351    #[serde(with = "color_scales")]
352    pub(crate) red: ColorScales,
353    #[serde(with = "color_scales")]
354    pub(crate) orange: ColorScales,
355    #[serde(with = "color_scales")]
356    pub(crate) amber: ColorScales,
357    #[serde(with = "color_scales")]
358    pub(crate) yellow: ColorScales,
359    #[serde(with = "color_scales")]
360    pub(crate) lime: ColorScales,
361    #[serde(with = "color_scales")]
362    pub(crate) green: ColorScales,
363    #[serde(with = "color_scales")]
364    pub(crate) emerald: ColorScales,
365    #[serde(with = "color_scales")]
366    pub(crate) teal: ColorScales,
367    #[serde(with = "color_scales")]
368    pub(crate) cyan: ColorScales,
369    #[serde(with = "color_scales")]
370    pub(crate) sky: ColorScales,
371    #[serde(with = "color_scales")]
372    pub(crate) blue: ColorScales,
373    #[serde(with = "color_scales")]
374    pub(crate) indigo: ColorScales,
375    #[serde(with = "color_scales")]
376    pub(crate) violet: ColorScales,
377    #[serde(with = "color_scales")]
378    pub(crate) purple: ColorScales,
379    #[serde(with = "color_scales")]
380    pub(crate) fuchsia: ColorScales,
381    #[serde(with = "color_scales")]
382    pub(crate) pink: ColorScales,
383    #[serde(with = "color_scales")]
384    pub(crate) rose: ColorScales,
385}
386
387#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize)]
388pub(crate) struct ShadcnColor {
389    #[serde(default)]
390    pub(crate) scale: usize,
391    #[serde(deserialize_with = "from_hsl_channel", alias = "hslChannel")]
392    pub(crate) hsla: Hsla,
393}
394
395/// Deserialize Hsla from a string in the format "210 40% 98%"
396fn from_hsl_channel<'de, D>(deserializer: D) -> Result<Hsla, D::Error>
397where
398    D: Deserializer<'de>,
399{
400    let s: String = Deserialize::deserialize(deserializer).unwrap();
401
402    let mut parts = s.split_whitespace();
403    if parts.clone().count() != 3 {
404        return Err(D::Error::custom(
405            "expected hslChannel has 3 parts, e.g: '210 40% 98%'",
406        ));
407    }
408
409    fn parse_number(s: &str) -> f32 {
410        s.trim_end_matches('%')
411            .parse()
412            .expect("failed to parse number")
413    }
414
415    let (h, s, l) = (
416        parse_number(parts.next().unwrap()),
417        parse_number(parts.next().unwrap()),
418        parse_number(parts.next().unwrap()),
419    );
420
421    Ok(hsl(h, s, l))
422}
423
424macro_rules! color_method {
425    ($color:tt, $scale:tt) => {
426        paste::paste! {
427            #[inline]
428            #[allow(unused)]
429            pub fn [<$color _ $scale>]() -> Hsla {
430                if let Some(color) = DEFAULT_COLORS.$color.get(&($scale as usize)) {
431                    return color.hsla;
432                }
433
434                black()
435            }
436        }
437    };
438}
439
440macro_rules! color_methods {
441    ($color:tt) => {
442        paste::paste! {
443            /// Get color by scale number.
444            ///
445            /// The possible scale numbers are:
446            /// 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950
447            ///
448            /// If the scale number is not found, it will return black color.
449            #[inline]
450            pub fn [<$color>](scale: usize) -> Hsla {
451                if let Some(color) = DEFAULT_COLORS.$color.get(&scale) {
452                    return color.hsla;
453                }
454
455                black()
456            }
457        }
458
459        color_method!($color, 50);
460        color_method!($color, 100);
461        color_method!($color, 200);
462        color_method!($color, 300);
463        color_method!($color, 400);
464        color_method!($color, 500);
465        color_method!($color, 600);
466        color_method!($color, 700);
467        color_method!($color, 800);
468        color_method!($color, 900);
469        color_method!($color, 950);
470    };
471}
472
473pub fn black() -> Hsla {
474    DEFAULT_COLORS.black.hsla
475}
476
477pub fn white() -> Hsla {
478    DEFAULT_COLORS.white.hsla
479}
480
481color_methods!(slate);
482color_methods!(gray);
483color_methods!(zinc);
484color_methods!(neutral);
485color_methods!(stone);
486color_methods!(red);
487color_methods!(orange);
488color_methods!(amber);
489color_methods!(yellow);
490color_methods!(lime);
491color_methods!(green);
492color_methods!(emerald);
493color_methods!(teal);
494color_methods!(cyan);
495color_methods!(sky);
496color_methods!(blue);
497color_methods!(indigo);
498color_methods!(violet);
499color_methods!(purple);
500color_methods!(fuchsia);
501color_methods!(pink);
502color_methods!(rose);
503
504#[cfg(test)]
505mod tests {
506    use gpui::{rgb, rgba};
507
508    use super::*;
509
510    #[test]
511    fn test_default_colors() {
512        assert_eq!(white(), hsl(0.0, 0.0, 100.0));
513        assert_eq!(black(), hsl(0.0, 0.0, 0.0));
514
515        assert_eq!(slate_50(), hsl(210.0, 40.0, 98.0));
516        assert_eq!(slate_100(), hsl(210.0, 40.0, 96.1));
517        assert_eq!(slate_900(), hsl(222.2, 47.4, 11.2));
518
519        assert_eq!(red_50(), hsl(0.0, 85.7, 97.3));
520        assert_eq!(yellow_100(), hsl(54.9, 96.7, 88.0));
521        assert_eq!(green_200(), hsl(141.0, 78.9, 85.1));
522        assert_eq!(cyan_300(), hsl(187.0, 92.4, 69.0));
523        assert_eq!(blue_400(), hsl(213.1, 93.9, 67.8));
524        assert_eq!(indigo_500(), hsl(238.7, 83.5, 66.7));
525    }
526
527    #[test]
528    fn test_to_hex_string() {
529        let color: Hsla = rgb(0xf8fafc).into();
530        assert_eq!(color.to_hex(), "#F8FAFC");
531
532        let color: Hsla = rgb(0xfef2f2).into();
533        assert_eq!(color.to_hex(), "#FEF2F2");
534
535        let color: Hsla = rgba(0x0413fcaa).into();
536        assert_eq!(color.to_hex(), "#0413FCAA");
537    }
538
539    #[test]
540    fn test_from_hex_string() {
541        let color: Hsla = Hsla::parse_hex("#F8FAFC").unwrap();
542        assert_eq!(color, rgb(0xf8fafc).into());
543
544        let color: Hsla = Hsla::parse_hex("#FEF2F2").unwrap();
545        assert_eq!(color, rgb(0xfef2f2).into());
546
547        let color: Hsla = Hsla::parse_hex("#0413FCAA").unwrap();
548        assert_eq!(color, rgba(0x0413fcaa).into());
549    }
550
551    #[test]
552    fn test_lighten() {
553        let color = super::hsl(240.0, 5.0, 30.0);
554        let color = color.lighten(0.5);
555        assert_eq!(color.l, 0.45000002);
556        let color = color.lighten(0.5);
557        assert_eq!(color.l, 0.675);
558        let color = color.lighten(0.1);
559        assert_eq!(color.l, 0.7425);
560    }
561
562    #[test]
563    fn test_darken() {
564        let color = super::hsl(240.0, 5.0, 96.0);
565        let color = color.darken(0.5);
566        assert_eq!(color.l, 0.48);
567        let color = color.darken(0.5);
568        assert_eq!(color.l, 0.24);
569    }
570
571    #[test]
572    fn test_mix() {
573        let red = Hsla::parse_hex("#FF0000").unwrap();
574        let blue = Hsla::parse_hex("#0000FF").unwrap();
575        let green = Hsla::parse_hex("#00FF00").unwrap();
576        let yellow = Hsla::parse_hex("#FFFF00").unwrap();
577
578        assert_eq!(red.mix(blue, 0.5).to_hex(), "#FF00FF");
579        assert_eq!(green.mix(red, 0.5).to_hex(), "#FFFF00");
580        assert_eq!(blue.mix(yellow, 0.2).to_hex(), "#0098FF");
581    }
582
583    #[test]
584    fn test_color_name() {
585        assert_eq!(ColorName::Purple.to_string(), "Purple");
586        assert_eq!(format!("{}", ColorName::Green), "Green");
587        assert_eq!(format!("{:?}", ColorName::Yellow), "Yellow");
588
589        let color = ColorName::Green;
590        assert_eq!(color.scale(500).to_hex(), "#21C55E");
591        assert_eq!(color.scale(1500).to_hex(), "#21C55E");
592
593        for name in ColorName::all().iter() {
594            let name1: ColorName = name.to_string().as_str().into();
595            assert_eq!(name1, *name);
596        }
597    }
598
599    #[test]
600    fn test_h_s_l() {
601        let color = hsl(260., 94., 80.);
602        assert_eq!(color.hue(200. / 360.), hsl(200., 94., 80.));
603        assert_eq!(color.saturation(74. / 100.), hsl(260., 74., 80.));
604        assert_eq!(color.lightness(74. / 100.), hsl(260., 94., 74.));
605    }
606}