Skip to main content

gpui/
color.rs

1use palette::{IntoColor, OklabHue, Oklcha, RgbHue};
2use schemars::{JsonSchema, json_schema};
3use serde::{Deserialize, Serialize};
4use std::fmt::{self, Display, Formatter};
5
6// Re-exported for api maintenance
7pub use palette::{Hsla, rgb::Rgba};
8
9/// Convert an RGB hex color code number to a color type
10pub fn rgb(hex: u32) -> Rgba {
11    let [_, r, g, b] = hex.to_be_bytes().map(|b| (b as f32) / 255.0);
12    Rgba {
13        color: palette::rgb::Rgb::new(r, g, b),
14        alpha: 1.0,
15    }
16}
17
18/// Convert an RGBA hex color code number to [`Rgba`]
19pub fn rgba(hex: u32) -> Rgba {
20    let [r, g, b, a] = hex.to_be_bytes().map(|b| (b as f32) / 255.0);
21    Rgba::new(r, g, b, a)
22}
23
24/// Convert an sRGB color to GPUI's HSL-with-alpha representation.
25///
26/// This is the explicit conversion boundary for APIs that store [`Hsla`]. It
27/// avoids exposing Palette's conversion traits to applications and examples.
28pub fn rgb_to_hsla(color: Rgba) -> Hsla {
29    color.into_color()
30}
31
32/// Convert GPUI's HSL-with-alpha representation to sRGB.
33pub fn hsla_to_rgba(color: Hsla) -> Rgba {
34    color.into_color()
35}
36
37/// Swap from RGBA with premultiplied alpha to BGRA
38pub fn swap_rgba_pa_to_bgra(color: &mut [u8]) {
39    color.swap(0, 2);
40    if color[3] > 0 {
41        let a = color[3] as f32 / 255.;
42        color[0] = (color[0] as f32 / a) as u8;
43        color[1] = (color[1] as f32 / a) as u8;
44        color[2] = (color[2] as f32 / a) as u8;
45    }
46}
47
48/// Construct an [`Hsla`] object from plain values
49pub const fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
50    Hsla {
51        color: palette::Hsl::new_const(
52            // `RgbHue` stores degrees, so the 0..1 fraction of the circle needs
53            // scaling to 0..360 before it's wrapped.
54            RgbHue::new(h.clamp(0., 1.) * 360.),
55            s.clamp(0., 1.),
56            l.clamp(0., 1.),
57        ),
58        alpha: a.clamp(0., 1.),
59    }
60}
61
62/// Constructs a ['Oklcha'](palette::Oklcha) object from plain values.
63pub fn oklcha<T>(lightness: T, chroma: T, hue: impl Into<OklabHue<T>>, alpha: T) -> Oklcha<T> {
64    Oklcha::new(lightness, chroma, hue, alpha)
65}
66
67/// Pure black in [`Hsla`]
68pub const fn black() -> Hsla {
69    Hsla::new_const(RgbHue::new(0.), 0., 0., 1.)
70}
71
72/// Transparent black in [`Hsla`]
73pub const fn transparent_black() -> Hsla {
74    Hsla::new_const(RgbHue::new(0.), 0., 0., 0.)
75}
76
77/// Transparent white in [`Hsla`]
78pub const fn transparent_white() -> Hsla {
79    Hsla::new_const(RgbHue::new(0.), 0., 1., 0.)
80}
81
82/// Opaque grey in [`Hsla`], values must be provided in the range [0, 1]
83pub const fn opaque_grey(lightness: f32, opacity: f32) -> Hsla {
84    Hsla::new_const(RgbHue::new(0.), 0., lightness, opacity)
85}
86
87/// Pure white in [`Hsla`]
88pub const fn white() -> Hsla {
89    Hsla::new_const(RgbHue::new(0.), 0., 1., 1.)
90}
91
92/// The color red in [`Hsla`]
93pub const fn red() -> Hsla {
94    Hsla::new_const(RgbHue::new(0.), 1., 0.5, 1.)
95}
96
97/// The color blue in [`Hsla`]
98pub const fn blue() -> Hsla {
99    Hsla::new_const(RgbHue::new(240.), 1., 0.5, 1.)
100}
101
102/// The color green in [`Hsla`]
103pub const fn green() -> Hsla {
104    Hsla::new_const(RgbHue::new(120.), 1., 0.25, 1.)
105}
106
107/// The color yellow in [`Hsla`]
108pub const fn yellow() -> Hsla {
109    Hsla::new_const(RgbHue::new(60.), 1., 0.5, 1.)
110}
111
112/// Generates the JsonSchema for palette::Hsla
113pub fn hsla_schemar(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
114    // https://github.com/Ogeon/palette/blob/9aa1ac21a7da60db398e8c044a43dbf3fdaf4855/palette/src/hsl.rs#L633-L636
115    // https://github.com/Ogeon/palette/blob/9aa1ac21a7da60db398e8c044a43dbf3fdaf4855/palette/src/alpha/alpha.rs#L1197
116    json_schema!({
117        "type": "object",
118        "properties": {
119          "hue": {
120              "type": "number",
121              "format": "float"
122          },
123          "saturation": {
124              "type": "number",
125              "format": "float"
126          },
127          "lightness": {
128              "type": "number",
129              "format": "float"
130          },
131          "alpha": {
132              "type": "number",
133              "format": "float"
134          },
135        }
136    })
137}
138
139/// Generates the JsonSchema for palette::Rgba
140pub fn rgba_schemar(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
141    // https://github.com/Ogeon/palette/blob/9aa1ac21a7da60db398e8c044a43dbf3fdaf4855/palette/src/rgb/rgb.rs#L1690
142    // https://github.com/Ogeon/palette/blob/9aa1ac21a7da60db398e8c044a43dbf3fdaf4855/palette/src/alpha/alpha.rs#L1197
143    json_schema!({
144        "type": "object",
145        "properties": {
146          "red": {
147              "type": "number",
148              "format": "float"
149          },
150          "green": {
151              "type": "number",
152              "format": "float"
153          },
154          "blue": {
155              "type": "number",
156              "format": "float"
157          },
158          "alpha": {
159              "type": "number",
160              "format": "float"
161          },
162        }
163    })
164}
165
166/// Wrapper methods to make alpha operations more convenient
167pub trait ColorExt {
168    /// Performs a SrcAlpha x (1 - SrcAlpha) blend
169    fn blend(&self, other: &Self) -> Self
170    where
171        Self: Sized;
172
173    /// Fade out the color by a given factor. This factor should be between 0.0 and 1.0.
174    /// Where 0.0 will leave the color unchanged, and 1.0 will completely fade out the color.
175    fn fade_out(&mut self, factor: f32);
176
177    /// Multiplies the alpha value of the color by a given factor and returns a new color.
178    /// If the color was previously opaque, then this is equivalent to
179    /// [`with_alpha`](palette::WithAlpha::with_alpha).
180    ///
181    /// Useful for transforming colors with dynamic opacity,
182    /// like a color from an external source.
183    ///
184    /// Example:
185    /// ```
186    /// use gpui::ColorExt;
187    /// let color = gpui::red();
188    /// let faded_color = color.opacity(0.5);
189    /// assert_eq!(faded_color.alpha, 0.5);
190    /// ```
191    ///
192    /// This will return a red color with half the opacity.
193    ///
194    /// Example:
195    /// ```
196    /// use gpui::{hsla, ColorExt};
197    /// let color = hsla(0.7, 1.0, 0.5, 0.7); // A saturated blue
198    /// let faded_color = color.opacity(0.16);
199    /// assert!((faded_color.alpha - 0.112).abs() < 1e-6);
200    /// ```
201    ///
202    /// This will return a blue color with around ~10% opacity,
203    /// suitable for an element's hover or selected state.
204    ///
205    fn opacity(&self, factor: f32) -> Self
206    where
207        Self: Sized;
208}
209impl ColorExt for Rgba {
210    fn blend(&self, other: &Self) -> Self {
211        use palette::blend::{BlendWith, Equations, Parameter};
212        let blend_mode =
213            Equations::from_parameters(Parameter::OneMinusSourceAlpha, Parameter::SourceAlpha);
214        self.blend_with(*other, blend_mode)
215    }
216
217    fn fade_out(&mut self, factor: f32) {
218        self.alpha *= 1.0 - factor.clamp(0., 1.);
219    }
220
221    fn opacity(&self, factor: f32) -> Self {
222        let mut color = *self;
223        color.alpha *= factor.clamp(0., 1.);
224        color
225    }
226}
227impl ColorExt for Hsla {
228    fn blend(&self, other: &Self) -> Self {
229        let this: Rgba = (*self).into_color();
230        let other: Rgba = (*other).into_color();
231        this.blend(&other).into_color()
232    }
233
234    fn fade_out(&mut self, factor: f32) {
235        self.alpha *= 1.0 - factor.clamp(0., 1.);
236    }
237
238    fn opacity(&self, factor: f32) -> Self {
239        let mut color = *self;
240        color.alpha *= factor.clamp(0., 1.);
241        color
242    }
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
246#[repr(C)]
247pub(crate) enum BackgroundTag {
248    Solid = 0,
249    LinearGradient = 1,
250    PatternSlash = 2,
251    Checkerboard = 3,
252}
253
254/// A color space for color interpolation.
255///
256/// References:
257/// - <https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method>
258/// - <https://www.w3.org/TR/css-color-4/#typedef-color-space>
259#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
260#[repr(C)]
261pub enum ColorSpace {
262    #[default]
263    /// The sRGB color space.
264    Srgb = 0,
265    /// The Oklab color space.
266    Oklab = 1,
267}
268
269impl Display for ColorSpace {
270    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
271        match self {
272            ColorSpace::Srgb => write!(f, "sRGB"),
273            ColorSpace::Oklab => write!(f, "Oklab"),
274        }
275    }
276}
277
278/// A background color, which can be either a solid color or a linear gradient.
279#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
280#[repr(C)]
281pub struct Background {
282    pub(crate) tag: BackgroundTag,
283    pub(crate) color_space: ColorSpace,
284    pub(crate) solid: crate::SceneHsla,
285    pub(crate) gradient_angle_or_pattern_height: f32,
286    pub(crate) colors: [LinearColorStop; 2],
287    /// Padding for alignment for repr(C) layout.
288    pad: u32,
289}
290
291impl std::fmt::Debug for Background {
292    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
293        match self.tag {
294            BackgroundTag::Solid => write!(f, "Solid({:?})", self.solid),
295            BackgroundTag::LinearGradient => write!(
296                f,
297                "LinearGradient({}, {:?}, {:?})",
298                self.gradient_angle_or_pattern_height, self.colors[0], self.colors[1]
299            ),
300            BackgroundTag::PatternSlash => write!(
301                f,
302                "PatternSlash({:?}, {})",
303                self.solid, self.gradient_angle_or_pattern_height
304            ),
305            BackgroundTag::Checkerboard => write!(
306                f,
307                "Checkerboard({:?}, {})",
308                self.solid, self.gradient_angle_or_pattern_height
309            ),
310        }
311    }
312}
313
314impl Eq for Background {}
315impl Default for Background {
316    fn default() -> Self {
317        Self {
318            tag: BackgroundTag::Solid,
319            solid: Hsla::default().into(),
320            color_space: ColorSpace::default(),
321            gradient_angle_or_pattern_height: 0.0,
322            colors: [LinearColorStop::default(), LinearColorStop::default()],
323            pad: 0,
324        }
325    }
326}
327
328/// Creates a hash pattern background
329pub fn pattern_slash(color: impl IntoColor<Hsla>, width: f32, interval: f32) -> Background {
330    let width_scaled = (width * 255.0) as u32;
331    let interval_scaled = (interval * 255.0) as u32;
332    let height = ((width_scaled * 0xFFFF) + interval_scaled) as f32;
333
334    Background {
335        tag: BackgroundTag::PatternSlash,
336        solid: color.into_color().into(),
337        gradient_angle_or_pattern_height: height,
338        ..Default::default()
339    }
340}
341
342/// Creates a checkerboard pattern background
343pub fn checkerboard(color: impl IntoColor<Hsla>, size: f32) -> Background {
344    Background {
345        tag: BackgroundTag::Checkerboard,
346        solid: color.into_color().into(),
347        gradient_angle_or_pattern_height: size,
348        ..Default::default()
349    }
350}
351
352/// Creates a solid background color.
353pub fn solid_background(color: impl IntoColor<Hsla>) -> Background {
354    Background {
355        solid: color.into_color().into(),
356        ..Default::default()
357    }
358}
359
360/// Creates a LinearGradient background color.
361///
362/// The gradient line's angle of direction. A value of `0.` is equivalent to top; increasing values rotate clockwise from there.
363///
364/// The `angle` is in degrees value in the range 0.0 to 360.0.
365///
366/// <https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient>
367pub fn linear_gradient(
368    angle: f32,
369    from: impl Into<LinearColorStop>,
370    to: impl Into<LinearColorStop>,
371) -> Background {
372    Background {
373        tag: BackgroundTag::LinearGradient,
374        gradient_angle_or_pattern_height: angle,
375        colors: [from.into(), to.into()],
376        color_space: ColorSpace::Oklab,
377        ..Default::default()
378    }
379}
380
381/// A color stop in a linear gradient.
382///
383/// <https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient#linear-color-stop>
384#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
385#[repr(C)]
386pub struct LinearColorStop {
387    /// The color of the color stop.
388    pub color: crate::SceneHsla,
389    /// The percentage of the gradient, in the range 0.0 to 1.0.
390    pub percentage: f32,
391}
392
393/// Creates a new linear color stop.
394///
395/// The percentage of the gradient, in the range 0.0 to 1.0.
396pub fn linear_color_stop(color: impl IntoColor<Hsla>, percentage: f32) -> LinearColorStop {
397    LinearColorStop {
398        color: color.into_color().into(),
399        percentage,
400    }
401}
402
403impl LinearColorStop {
404    /// Returns a new color stop with the same color, but with a modified alpha value.
405    pub fn opacity(&self, factor: f32) -> Self {
406        let color: Hsla = self.color.into();
407        Self {
408            percentage: self.percentage,
409            color: color.opacity(factor).into(),
410        }
411    }
412}
413
414/// What a [`Background`] paints, decoded from its packed representation.
415#[derive(Debug, Clone, Copy, PartialEq)]
416pub enum BackgroundKind {
417    /// A flat color.
418    Solid(Hsla),
419    /// A linear gradient between two color stops.
420    LinearGradient {
421        /// The gradient line's angle in degrees, `0.0` pointing up, increasing clockwise.
422        angle: f32,
423        /// The two ends of the gradient.
424        stops: [LinearColorStop; 2],
425    },
426    /// A diagonal stripe pattern.
427    PatternSlash {
428        /// The stripe color.
429        color: Hsla,
430        /// The stripe width, in logical pixels.
431        width: f32,
432        /// The gap between stripes, in logical pixels.
433        interval: f32,
434    },
435    /// Alternating squares of one color and full transparency.
436    Checkerboard {
437        /// The color of one set of squares. The other set is fully transparent.
438        color: Hsla,
439        /// The width and height of each square, in logical pixels.
440        size: f32,
441    },
442}
443
444impl Background {
445    /// Returns the solid color if this is a solid background, None otherwise.
446    pub fn as_solid(&self) -> Option<Hsla> {
447        if self.tag == BackgroundTag::Solid {
448            Some(self.solid.into())
449        } else {
450            None
451        }
452    }
453
454    /// Returns the decoded form of this background.
455    pub fn kind(&self) -> BackgroundKind {
456        match self.tag {
457            BackgroundTag::Solid => BackgroundKind::Solid(self.solid.into()),
458            BackgroundTag::LinearGradient => BackgroundKind::LinearGradient {
459                angle: self.gradient_angle_or_pattern_height,
460                stops: self.colors,
461            },
462            BackgroundTag::PatternSlash => {
463                // `pattern_slash` packs both values into one f32 as `(width * 255) * 0xFFFF + (interval * 255)`.
464                // floor + rem_euclid to invert it since that's the pairing that stays correct for negative inputs.
465                // truncation and `%` give the wrong entry.
466                let packed = self.gradient_angle_or_pattern_height;
467                BackgroundKind::PatternSlash {
468                    color: self.solid.into(),
469                    width: (packed / 0xFFFF as f32).floor() / 255.0,
470                    interval: (packed.rem_euclid(0xFFFF as f32)) / 255.0,
471                }
472            }
473            BackgroundTag::Checkerboard => BackgroundKind::Checkerboard {
474                color: self.solid.into(),
475                size: self.gradient_angle_or_pattern_height,
476            },
477        }
478    }
479
480    /// Use specified color space for color interpolation.
481    ///
482    /// <https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method>
483    pub fn color_space(mut self, color_space: ColorSpace) -> Self {
484        self.color_space = color_space;
485        self
486    }
487
488    /// The color space used to interpolate this background, set by [`Background::color_space`].
489    pub fn interpolation_space(&self) -> ColorSpace {
490        self.color_space
491    }
492
493    /// Returns a new background color with the same hue, saturation, and lightness, but with a modified alpha value.
494    pub fn opacity(&self, factor: f32) -> Self {
495        let mut background = *self;
496        let solid: Hsla = background.solid.into();
497        background.solid = solid.opacity(factor).into();
498        background.colors = [
499            self.colors[0].opacity(factor),
500            self.colors[1].opacity(factor),
501        ];
502        background
503    }
504
505    /// Returns whether the background color is transparent.
506    pub fn is_transparent(&self) -> bool {
507        match self.tag {
508            BackgroundTag::Solid => self.solid.a == 0.,
509            BackgroundTag::LinearGradient => self.colors.iter().all(|c| c.color.a == 0.),
510            BackgroundTag::PatternSlash => self.solid.a == 0.,
511            BackgroundTag::Checkerboard => self.solid.a == 0.,
512        }
513    }
514}
515
516impl<T: IntoColor<Hsla>> From<T> for Background {
517    fn from(value: T) -> Self {
518        Self {
519            tag: BackgroundTag::Solid,
520            solid: value.into_color().into(),
521            ..Default::default()
522        }
523    }
524}
525
526#[cfg(test)]
527mod tests {
528    use super::*;
529
530    #[test]
531    fn test_background_solid() {
532        let color: Hsla = rgba(0xff0099ff).into_color();
533        let mut background = Background::from(color);
534        assert_eq!(background.tag, BackgroundTag::Solid);
535        assert_eq!(background.solid, color.into());
536
537        assert_eq!(background.opacity(0.5).solid, color.opacity(0.5).into());
538        assert!(!background.is_transparent());
539        background.solid = hsla(0.0, 0.0, 0.0, 0.0).into();
540        assert!(background.is_transparent());
541    }
542
543    #[test]
544    fn test_background_linear_gradient() {
545        let from = linear_color_stop(rgba(0xff0099ff), 0.0);
546        let to = linear_color_stop(rgba(0x00ff99ff), 1.0);
547        let background = linear_gradient(90.0, from, to);
548        assert_eq!(background.tag, BackgroundTag::LinearGradient);
549        assert_eq!(background.colors[0], from);
550        assert_eq!(background.colors[1], to);
551
552        assert_eq!(background.opacity(0.5).colors[0], from.opacity(0.5));
553        assert_eq!(background.opacity(0.5).colors[1], to.opacity(0.5));
554        assert!(!background.is_transparent());
555        assert!(background.opacity(0.0).is_transparent());
556    }
557
558    #[test]
559    fn test_background_kind() {
560        let color: Hsla = rgba(0xff0099ff).into_color();
561        assert_eq!(Background::from(color).kind(), BackgroundKind::Solid(color));
562
563        let from = linear_color_stop(rgba(0xff0099ff), 0.0);
564        let to = linear_color_stop(rgba(0x00ff99ff), 1.0);
565        assert_eq!(
566            linear_gradient(90.0, from, to).kind(),
567            BackgroundKind::LinearGradient {
568                angle: 90.0,
569                stops: [from, to],
570            }
571        );
572
573        assert_eq!(
574            checkerboard(color, 12.0).kind(),
575            BackgroundKind::Checkerboard { color, size: 12.0 }
576        );
577    }
578
579    #[test]
580    fn test_background_kind_unpacks_pattern_slash() {
581        let color: Hsla = rgba(0xff0099ff).into_color();
582        // Both values survive to the 1/255 the constructor quantizes them to.
583        for (width, interval) in [(1.0, 3.0), (0.5, 0.25), (2.0, 10.0)] {
584            let BackgroundKind::PatternSlash {
585                width: got_width,
586                interval: got_interval,
587                ..
588            } = pattern_slash(color, width, interval).kind()
589            else {
590                panic!("pattern_slash did not produce a PatternSlash");
591            };
592            assert!((got_width - width).abs() <= 1.0 / 255.0);
593            assert!((got_interval - interval).abs() <= 1.0 / 255.0);
594        }
595    }
596
597    #[test]
598    fn test_rgba_alpha() {
599        use palette::WithAlpha;
600        let color = Rgba::<palette::Srgb>::new(0.2, 0.6, 1.0, 0.8);
601        assert_eq!(color.with_alpha(0.25).alpha, 0.25);
602        // NOTE: diverging from upstream, where Rgba::alpha clamps. palette does not clamp alpha
603        assert_eq!(color.with_alpha(1.5).alpha, 1.5);
604    }
605
606    #[test]
607    fn test_rgba_opacity() {
608        use super::ColorExt;
609        let color = Rgba::new(0.2, 0.6, 1.0, 0.8);
610        assert!((color.opacity(0.5).alpha - 0.4).abs() < 1e-6);
611        assert_eq!(color.opacity(2.0).alpha, 0.8);
612    }
613}