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/// Creates an opaque [`Rgba`] color from a `0xRRGGBB` hex number.
10///
11/// The highest byte of `hex` is ignored, and the alpha is `1.0`. Use [`rgba`] for a color
12/// with alpha.
13///
14/// # Examples
15///
16/// ```
17/// use gpui::rgb;
18///
19/// assert_eq!(rgb(0xffffff).alpha, 1.0);
20/// ```
21pub fn rgb(hex: u32) -> Rgba {
22    let [_, r, g, b] = hex.to_be_bytes().map(|b| (b as f32) / 255.0);
23    Rgba {
24        color: palette::rgb::Rgb::new(r, g, b),
25        alpha: 1.0,
26    }
27}
28
29/// Creates an [`Rgba`] color from a `0xRRGGBBAA` hex number.
30///
31/// # Examples
32///
33/// ```
34/// use gpui::rgba;
35///
36/// let half_red = rgba(0xff000080);
37/// assert!((half_red.alpha - 128.0 / 255.0).abs() < 1e-6);
38/// ```
39pub fn rgba(hex: u32) -> Rgba {
40    let [r, g, b, a] = hex.to_be_bytes().map(|b| (b as f32) / 255.0);
41    Rgba::new(r, g, b, a)
42}
43
44/// Converts an sRGB [`Rgba`] color to [`Hsla`], keeping the alpha.
45///
46/// Use it at an API boundary that stores [`Hsla`], such as [`Styled::bg`](crate::Styled::bg),
47/// when the value you have is [`Rgba`].
48pub fn rgb_to_hsla(color: Rgba) -> Hsla {
49    color.into_color()
50}
51
52/// Converts an [`Hsla`] color to sRGB [`Rgba`], keeping the alpha.
53pub fn hsla_to_rgba(color: Hsla) -> Rgba {
54    color.into_color()
55}
56
57/// Converts one premultiplied RGBA pixel to straight-alpha BGRA, in place.
58///
59/// The function reads and writes only the first four bytes of `color`. Bytes after the
60/// first pixel are left unchanged.
61///
62/// # Panics
63///
64/// Panics if `color` has fewer than four bytes.
65pub fn swap_rgba_pa_to_bgra(color: &mut [u8]) {
66    color.swap(0, 2);
67    if color[3] > 0 {
68        let a = color[3] as f32 / 255.;
69        color[0] = (color[0] as f32 / a) as u8;
70        color[1] = (color[1] as f32 / a) as u8;
71        color[2] = (color[2] as f32 / a) as u8;
72    }
73}
74
75/// Creates an [`Hsla`] color from hue, saturation, lightness and alpha.
76///
77/// Every argument is a fraction from `0.0` to `1.0`; values outside that range are clamped.
78/// The hue is a fraction of one turn, not degrees: `0.0` is red and `1.0 / 3.0` is green.
79/// If you have a hue in degrees, use [`oklcha`] or convert it before calling this function.
80///
81/// # Examples
82///
83/// ```
84/// use gpui::{hsla, red};
85///
86/// assert_eq!(hsla(0.0, 1.0, 0.5, 1.0), red());
87/// ```
88pub const fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
89    Hsla {
90        color: palette::Hsl::new_const(
91            // `RgbHue` stores degrees, so the 0..1 fraction of the circle needs
92            // scaling to 0..360 before it's wrapped.
93            RgbHue::new(h.clamp(0., 1.) * 360.),
94            s.clamp(0., 1.),
95            l.clamp(0., 1.),
96        ),
97        alpha: a.clamp(0., 1.),
98    }
99}
100
101/// Creates an [`Oklcha`](palette::Oklcha) color from lightness, chroma, hue and alpha.
102///
103/// Unlike [`hsla`], the arguments are not clamped. The hue is in degrees. Use this for
104/// perceptually even colors, such as palettes that keep the same lightness across hues.
105pub fn oklcha<T>(lightness: T, chroma: T, hue: impl Into<OklabHue<T>>, alpha: T) -> Oklcha<T> {
106    Oklcha::new(lightness, chroma, hue, alpha)
107}
108
109/// Opaque black in [`Hsla`].
110pub const fn black() -> Hsla {
111    Hsla::new_const(RgbHue::new(0.), 0., 0., 1.)
112}
113
114/// Black with zero alpha in [`Hsla`].
115pub const fn transparent_black() -> Hsla {
116    Hsla::new_const(RgbHue::new(0.), 0., 0., 0.)
117}
118
119/// White with zero alpha in [`Hsla`].
120pub const fn transparent_white() -> Hsla {
121    Hsla::new_const(RgbHue::new(0.), 0., 1., 0.)
122}
123
124/// A grey in [`Hsla`] with the given lightness and opacity.
125///
126/// Both arguments are fractions from `0.0` to `1.0`. This constructor does not clamp them, so
127/// callers must keep them in range.
128pub const fn opaque_grey(lightness: f32, opacity: f32) -> Hsla {
129    Hsla::new_const(RgbHue::new(0.), 0., lightness, opacity)
130}
131
132/// Opaque white in [`Hsla`].
133pub const fn white() -> Hsla {
134    Hsla::new_const(RgbHue::new(0.), 0., 1., 1.)
135}
136
137/// Opaque red in [`Hsla`], at hue `0.0`.
138pub const fn red() -> Hsla {
139    Hsla::new_const(RgbHue::new(0.), 1., 0.5, 1.)
140}
141
142/// Opaque blue in [`Hsla`], at hue 240 degrees.
143pub const fn blue() -> Hsla {
144    Hsla::new_const(RgbHue::new(240.), 1., 0.5, 1.)
145}
146
147/// A dark, opaque green in [`Hsla`], at hue 120 degrees and lightness 0.25.
148pub const fn green() -> Hsla {
149    Hsla::new_const(RgbHue::new(120.), 1., 0.25, 1.)
150}
151
152/// Opaque yellow in [`Hsla`], at hue 60 degrees.
153pub const fn yellow() -> Hsla {
154    Hsla::new_const(RgbHue::new(60.), 1., 0.5, 1.)
155}
156
157/// Generates the JsonSchema for palette::Hsla
158pub fn hsla_schemar(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
159    // https://github.com/Ogeon/palette/blob/9aa1ac21a7da60db398e8c044a43dbf3fdaf4855/palette/src/hsl.rs#L633-L636
160    // https://github.com/Ogeon/palette/blob/9aa1ac21a7da60db398e8c044a43dbf3fdaf4855/palette/src/alpha/alpha.rs#L1197
161    json_schema!({
162        "type": "object",
163        "properties": {
164          "hue": {
165              "type": "number",
166              "format": "float"
167          },
168          "saturation": {
169              "type": "number",
170              "format": "float"
171          },
172          "lightness": {
173              "type": "number",
174              "format": "float"
175          },
176          "alpha": {
177              "type": "number",
178              "format": "float"
179          },
180        }
181    })
182}
183
184/// Generates the JsonSchema for palette::Rgba
185pub fn rgba_schemar(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
186    // https://github.com/Ogeon/palette/blob/9aa1ac21a7da60db398e8c044a43dbf3fdaf4855/palette/src/rgb/rgb.rs#L1690
187    // https://github.com/Ogeon/palette/blob/9aa1ac21a7da60db398e8c044a43dbf3fdaf4855/palette/src/alpha/alpha.rs#L1197
188    json_schema!({
189        "type": "object",
190        "properties": {
191          "red": {
192              "type": "number",
193              "format": "float"
194          },
195          "green": {
196              "type": "number",
197              "format": "float"
198          },
199          "blue": {
200              "type": "number",
201              "format": "float"
202          },
203          "alpha": {
204              "type": "number",
205              "format": "float"
206          },
207        }
208    })
209}
210
211/// Helpers for changing the alpha of a color.
212///
213/// Implemented for [`Rgba`] and [`Hsla`]. The factors in these methods are clamped to
214/// `0.0..=1.0`.
215pub trait ColorExt {
216    /// Composites `other` over this color with Porter–Duff source-over.
217    fn blend(&self, other: &Self) -> Self
218    where
219        Self: Sized;
220    /// Reduces the alpha of this color in place by `factor`.
221    ///
222    /// A factor of `0.0` leaves the color unchanged, and `1.0` makes it fully transparent.
223    /// Values outside `0.0..=1.0` are clamped.
224    fn fade_out(&mut self, factor: f32);
225    /// Returns a copy of this color with its alpha multiplied by `factor`.
226    ///
227    /// If the color is opaque, the result has alpha `factor`. Use this for colors whose
228    /// opacity comes from data, such as a color chosen at runtime. Values outside
229    /// `0.0..=1.0` are clamped.
230    ///
231    /// # Examples
232    ///
233    /// ```
234    /// use gpui::ColorExt;
235    ///
236    /// let faded = gpui::red().opacity(0.5);
237    /// assert_eq!(faded.alpha, 0.5);
238    /// ```
239    ///
240    /// Multiplying a color that already has alpha gives a result that is less opaque:
241    ///
242    /// ```
243    /// use gpui::{ColorExt, hsla};
244    ///
245    /// let color = hsla(0.7, 1.0, 0.5, 0.7);
246    /// let faded = color.opacity(0.16);
247    /// assert!((faded.alpha - 0.112).abs() < 1e-6);
248    /// ```
249    fn opacity(&self, factor: f32) -> Self
250    where
251        Self: Sized;
252}
253impl ColorExt for Rgba {
254    fn blend(&self, other: &Self) -> Self {
255        use palette::blend::Compose;
256        other.over(*self)
257    }
258
259    fn fade_out(&mut self, factor: f32) {
260        self.alpha *= 1.0 - factor.clamp(0., 1.);
261    }
262
263    fn opacity(&self, factor: f32) -> Self {
264        let mut color = *self;
265        color.alpha *= factor.clamp(0., 1.);
266        color
267    }
268}
269impl ColorExt for Hsla {
270    fn blend(&self, other: &Self) -> Self {
271        let this: Rgba = (*self).into_color();
272        let other: Rgba = (*other).into_color();
273        this.blend(&other).into_color()
274    }
275
276    fn fade_out(&mut self, factor: f32) {
277        self.alpha *= 1.0 - factor.clamp(0., 1.);
278    }
279
280    fn opacity(&self, factor: f32) -> Self {
281        let mut color = *self;
282        color.alpha *= factor.clamp(0., 1.);
283        color
284    }
285}
286
287/// The storage-buffer discriminant for a [`Background`].
288#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
289#[repr(u32)]
290pub enum BackgroundTag {
291    /// A flat color.
292    Solid = 0,
293    /// A two-stop linear gradient.
294    LinearGradient = 1,
295    /// Diagonal color stripes over transparency.
296    PatternSlash = 2,
297    /// Alternating colored and transparent squares.
298    Checkerboard = 3,
299}
300
301/// A color space for color interpolation.
302///
303/// References:
304/// - <https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method>
305/// - <https://www.w3.org/TR/css-color-4/#typedef-color-space>
306#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
307#[repr(u32)]
308pub enum ColorSpace {
309    #[default]
310    /// The sRGB color space.
311    Srgb = 0,
312    /// The Oklab color space.
313    Oklab = 1,
314}
315
316impl Display for ColorSpace {
317    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
318        match self {
319            ColorSpace::Srgb => write!(f, "sRGB"),
320            ColorSpace::Oklab => write!(f, "Oklab"),
321        }
322    }
323}
324
325/// A fill for a shape: a solid color, a linear gradient, or a pattern.
326///
327/// Build a background with [`solid_background`], [`linear_gradient`], [`pattern_slash`] or
328/// [`checkerboard`], or convert a color with `.into()`. Elements take a background through
329/// [`Styled::bg`](crate::Styled::bg). Use [`Background::kind`] to inspect one.
330///
331/// # Examples
332///
333/// ```
334/// use gpui::{Background, blue, linear_color_stop, linear_gradient, red};
335///
336/// let solid: Background = red().into();
337/// assert!(solid.as_solid().is_some());
338///
339/// let gradient = linear_gradient(
340///     90.,
341///     linear_color_stop(red(), 0.),
342///     linear_color_stop(blue(), 1.),
343/// );
344/// assert!(gradient.as_solid().is_none());
345/// ```
346#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
347#[repr(C)]
348pub struct Background {
349    pub(crate) tag: BackgroundTag,
350    pub(crate) color_space: ColorSpace,
351    pub(crate) solid: crate::SceneHsla,
352    pub(crate) gradient_angle_or_pattern_height: f32,
353    pub(crate) colors: [LinearColorStop; 2],
354    /// Padding for alignment for repr(C) layout.
355    pub(crate) padding: u32,
356}
357
358impl std::fmt::Debug for Background {
359    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
360        match self.tag {
361            BackgroundTag::Solid => write!(f, "Solid({:?})", self.solid),
362            BackgroundTag::LinearGradient => write!(
363                f,
364                "LinearGradient({}, {:?}, {:?})",
365                self.gradient_angle_or_pattern_height, self.colors[0], self.colors[1]
366            ),
367            BackgroundTag::PatternSlash => write!(
368                f,
369                "PatternSlash({:?}, {})",
370                self.solid, self.gradient_angle_or_pattern_height
371            ),
372            BackgroundTag::Checkerboard => write!(
373                f,
374                "Checkerboard({:?}, {})",
375                self.solid, self.gradient_angle_or_pattern_height
376            ),
377        }
378    }
379}
380
381impl Eq for Background {}
382impl Default for Background {
383    /// Returns a solid background with zero alpha, which paints nothing.
384    fn default() -> Self {
385        Self {
386            tag: BackgroundTag::Solid,
387            solid: Hsla::default().into(),
388            color_space: ColorSpace::default(),
389            gradient_angle_or_pattern_height: 0.0,
390            colors: [LinearColorStop::default(), LinearColorStop::default()],
391            padding: 0,
392        }
393    }
394}
395
396/// Creates a background of diagonal stripes in `color`, over transparency.
397///
398/// `width` is the width of each stripe and `interval` is the gap between stripes, both in
399/// logical pixels. Both are stored in steps of 1/255 pixel and packed into one `f32`. The
400/// packing is exact only for widths up to about 1px and intervals up to 257px. Larger
401/// values lose precision or overflow into the width.
402pub fn pattern_slash(color: impl IntoColor<Hsla>, width: f32, interval: f32) -> Background {
403    const PATTERN_COMPONENT_SCALE: f32 = u8::MAX as f32;
404    const PATTERN_PACKING_RADIX: u32 = u16::MAX as u32;
405
406    let width_scaled = (width * PATTERN_COMPONENT_SCALE) as u32;
407    let interval_scaled = (interval * PATTERN_COMPONENT_SCALE) as u32;
408    let height = (width_scaled * PATTERN_PACKING_RADIX + interval_scaled) as f32;
409
410    Background {
411        tag: BackgroundTag::PatternSlash,
412        solid: color.into_color().into(),
413        gradient_angle_or_pattern_height: height,
414        ..Default::default()
415    }
416}
417
418/// Creates a checkerboard of squares in `color`, alternating with transparency.
419///
420/// `size` is the width and height of each square in logical pixels.
421pub fn checkerboard(color: impl IntoColor<Hsla>, size: f32) -> Background {
422    Background {
423        tag: BackgroundTag::Checkerboard,
424        solid: color.into_color().into(),
425        gradient_angle_or_pattern_height: size,
426        ..Default::default()
427    }
428}
429
430/// Creates a solid background of `color`.
431///
432/// This is the same as converting `color` with `.into()`.
433pub fn solid_background(color: impl IntoColor<Hsla>) -> Background {
434    Background {
435        solid: color.into_color().into(),
436        ..Default::default()
437    }
438}
439
440/// Creates a linear gradient between two color stops.
441///
442/// `angle` is the direction of the gradient line in degrees, from `0.` (up) increasing
443/// clockwise. The colors are interpolated in [`ColorSpace::Oklab`]. Call
444/// [`Background::color_space`] to interpolate in sRGB instead.
445///
446/// # Examples
447///
448/// ```
449/// use gpui::{ColorSpace, blue, linear_color_stop, linear_gradient, red};
450///
451/// let gradient = linear_gradient(
452///     180.,
453///     linear_color_stop(red(), 0.),
454///     linear_color_stop(blue(), 1.),
455/// );
456/// assert_eq!(gradient.interpolation_space(), ColorSpace::Oklab);
457/// ```
458///
459/// <https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient>
460pub fn linear_gradient(
461    angle: f32,
462    from: impl Into<LinearColorStop>,
463    to: impl Into<LinearColorStop>,
464) -> Background {
465    Background {
466        tag: BackgroundTag::LinearGradient,
467        gradient_angle_or_pattern_height: angle,
468        colors: [from.into(), to.into()],
469        color_space: ColorSpace::Oklab,
470        ..Default::default()
471    }
472}
473
474/// A color stop in a linear gradient.
475///
476/// <https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient#linear-color-stop>
477#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
478#[repr(C)]
479pub struct LinearColorStop {
480    /// The color of the color stop.
481    pub color: crate::SceneHsla,
482    /// The percentage of the gradient, in the range 0.0 to 1.0.
483    pub percentage: f32,
484}
485
486/// Creates a color stop of `color` at `percentage` along a gradient line.
487///
488/// `percentage` is in the range `0.0` (the start of the gradient) to `1.0` (the end).
489pub fn linear_color_stop(color: impl IntoColor<Hsla>, percentage: f32) -> LinearColorStop {
490    LinearColorStop {
491        color: color.into_color().into(),
492        percentage,
493    }
494}
495
496impl LinearColorStop {
497    /// Returns a copy of this stop with its alpha multiplied by `factor`.
498    ///
499    /// The factor is clamped to `0.0..=1.0`, as with [`ColorExt::opacity`].
500    pub fn opacity(&self, factor: f32) -> Self {
501        let color: Hsla = self.color.into();
502        Self {
503            percentage: self.percentage,
504            color: color.opacity(factor).into(),
505        }
506    }
507}
508
509/// What a [`Background`] paints, decoded from its packed representation.
510#[derive(Debug, Clone, Copy, PartialEq)]
511pub enum BackgroundKind {
512    /// A flat color.
513    Solid(Hsla),
514    /// A linear gradient between two color stops.
515    LinearGradient {
516        /// The gradient line's angle in degrees, `0.0` pointing up, increasing clockwise.
517        angle: f32,
518        /// The two ends of the gradient.
519        stops: [LinearColorStop; 2],
520    },
521    /// A diagonal stripe pattern.
522    PatternSlash {
523        /// The stripe color.
524        color: Hsla,
525        /// The stripe width, in logical pixels.
526        width: f32,
527        /// The gap between stripes, in logical pixels.
528        interval: f32,
529    },
530    /// Alternating squares of one color and full transparency.
531    Checkerboard {
532        /// The color of one set of squares. The other set is fully transparent.
533        color: Hsla,
534        /// The width and height of each square, in logical pixels.
535        size: f32,
536    },
537}
538
539impl Background {
540    /// Returns the color if this is a solid background, and `None` otherwise.
541    pub fn as_solid(&self) -> Option<Hsla> {
542        if self.tag == BackgroundTag::Solid {
543            Some(self.solid.into())
544        } else {
545            None
546        }
547    }
548
549    /// Returns the decoded form of this background.
550    pub fn kind(&self) -> BackgroundKind {
551        match self.tag {
552            BackgroundTag::Solid => BackgroundKind::Solid(self.solid.into()),
553            BackgroundTag::LinearGradient => BackgroundKind::LinearGradient {
554                angle: self.gradient_angle_or_pattern_height,
555                stops: self.colors,
556            },
557            BackgroundTag::PatternSlash => {
558                const PATTERN_COMPONENT_SCALE: f32 = u8::MAX as f32;
559                const PATTERN_PACKING_RADIX: f32 = u16::MAX as f32;
560
561                // `pattern_slash` packs both values into one exactly representable f32;
562                // floor + rem_euclid inverts it, since truncation and `%` give the wrong entry.
563                let packed = self.gradient_angle_or_pattern_height;
564                BackgroundKind::PatternSlash {
565                    color: self.solid.into(),
566                    width: (packed / PATTERN_PACKING_RADIX).floor() / PATTERN_COMPONENT_SCALE,
567                    interval: (packed.rem_euclid(PATTERN_PACKING_RADIX)) / PATTERN_COMPONENT_SCALE,
568                }
569            }
570            BackgroundTag::Checkerboard => BackgroundKind::Checkerboard {
571                color: self.solid.into(),
572                size: self.gradient_angle_or_pattern_height,
573            },
574        }
575    }
576
577    /// Sets the color space that a gradient's colors are interpolated in.
578    ///
579    /// The default for [`linear_gradient`] is [`ColorSpace::Oklab`]. Solid colors and patterns
580    /// don't interpolate, so this has no effect on them.
581    ///
582    /// <https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method>
583    pub fn color_space(mut self, color_space: ColorSpace) -> Self {
584        self.color_space = color_space;
585        self
586    }
587
588    /// The color space used to interpolate this background, set by [`Background::color_space`].
589    pub fn interpolation_space(&self) -> ColorSpace {
590        self.color_space
591    }
592
593    /// Returns a copy of this background with the alpha of every color multiplied by `factor`.
594    ///
595    /// The factor is clamped to `0.0..=1.0`. Gradient stops are scaled too.
596    pub fn opacity(&self, factor: f32) -> Self {
597        let mut background = *self;
598        let solid: Hsla = background.solid.into();
599        background.solid = solid.opacity(factor).into();
600        background.colors = [
601            self.colors[0].opacity(factor),
602            self.colors[1].opacity(factor),
603        ];
604        background
605    }
606
607    /// Returns whether this background paints nothing.
608    ///
609    /// A solid, pattern or checkerboard background is transparent when its color has zero
610    /// alpha. A gradient is transparent only when both of its stops have zero alpha.
611    pub fn is_transparent(&self) -> bool {
612        match self.tag {
613            BackgroundTag::Solid => self.solid.a == 0.,
614            BackgroundTag::LinearGradient => self.colors.iter().all(|c| c.color.a == 0.),
615            BackgroundTag::PatternSlash => self.solid.a == 0.,
616            BackgroundTag::Checkerboard => self.solid.a == 0.,
617        }
618    }
619}
620
621impl<T: IntoColor<Hsla>> From<T> for Background {
622    /// Converts a color into a solid [`Background`].
623    fn from(value: T) -> Self {
624        Self {
625            tag: BackgroundTag::Solid,
626            solid: value.into_color().into(),
627            ..Default::default()
628        }
629    }
630}
631
632#[cfg(test)]
633mod tests {
634    use super::*;
635
636    #[test]
637    fn color_blend_composites_the_overlay_over_the_base() {
638        let assert_close = |actual: Rgba, expected: Rgba| {
639            for (actual, expected) in [
640                (actual.red, expected.red),
641                (actual.green, expected.green),
642                (actual.blue, expected.blue),
643                (actual.alpha, expected.alpha),
644            ] {
645                assert!(
646                    (actual - expected).abs() < 1e-6,
647                    "{actual:?} != {expected:?}"
648                );
649            }
650        };
651
652        let base = Rgba::new(1.0, 0.0, 0.0, 1.0);
653        assert_close(base.blend(&Rgba::new(0.0, 0.0, 1.0, 0.0)), base);
654        assert_close(
655            base.blend(&Rgba::new(0.0, 0.0, 1.0, 1.0)),
656            Rgba::new(0.0, 0.0, 1.0, 1.0),
657        );
658        assert_close(
659            base.blend(&Rgba::new(0.0, 0.0, 1.0, 0.25)),
660            Rgba::new(0.75, 0.0, 0.25, 1.0),
661        );
662    }
663
664    #[test]
665    fn highlight_keeps_the_base_color_under_a_transparent_overlay() {
666        let base = crate::TextStyle {
667            color: red(),
668            ..Default::default()
669        };
670        let transparent_blue = Hsla::new_const(RgbHue::new(240.), 1., 0.5, 0.);
671        let highlighted = base
672            .clone()
673            .highlight(crate::HighlightStyle::color(transparent_blue));
674        assert_eq!(highlighted.color, base.color);
675    }
676
677    #[test]
678    fn test_background_solid() {
679        let color: Hsla = rgba(0xff0099ff).into_color();
680        let mut background = Background::from(color);
681        assert_eq!(background.tag, BackgroundTag::Solid);
682        assert_eq!(background.solid, color.into());
683
684        assert_eq!(background.opacity(0.5).solid, color.opacity(0.5).into());
685        assert!(!background.is_transparent());
686        background.solid = hsla(0.0, 0.0, 0.0, 0.0).into();
687        assert!(background.is_transparent());
688    }
689
690    #[test]
691    fn test_background_linear_gradient() {
692        let from = linear_color_stop(rgba(0xff0099ff), 0.0);
693        let to = linear_color_stop(rgba(0x00ff99ff), 1.0);
694        let background = linear_gradient(90.0, from, to);
695        assert_eq!(background.tag, BackgroundTag::LinearGradient);
696        assert_eq!(background.colors[0], from);
697        assert_eq!(background.colors[1], to);
698
699        assert_eq!(background.opacity(0.5).colors[0], from.opacity(0.5));
700        assert_eq!(background.opacity(0.5).colors[1], to.opacity(0.5));
701        assert!(!background.is_transparent());
702        assert!(background.opacity(0.0).is_transparent());
703    }
704
705    #[test]
706    fn test_background_kind() {
707        let color: Hsla = rgba(0xff0099ff).into_color();
708        assert_eq!(Background::from(color).kind(), BackgroundKind::Solid(color));
709
710        let from = linear_color_stop(rgba(0xff0099ff), 0.0);
711        let to = linear_color_stop(rgba(0x00ff99ff), 1.0);
712        assert_eq!(
713            linear_gradient(90.0, from, to).kind(),
714            BackgroundKind::LinearGradient {
715                angle: 90.0,
716                stops: [from, to],
717            }
718        );
719
720        assert_eq!(
721            checkerboard(color, 12.0).kind(),
722            BackgroundKind::Checkerboard { color, size: 12.0 }
723        );
724    }
725
726    #[test]
727    fn test_background_kind_unpacks_pattern_slash() {
728        let color: Hsla = rgba(0xff0099ff).into_color();
729        // Both values survive to the 1/255 the constructor quantizes them to.
730        for (width, interval) in [(1.0, 3.0), (0.5, 0.25), (2.0, 10.0)] {
731            let BackgroundKind::PatternSlash {
732                width: got_width,
733                interval: got_interval,
734                ..
735            } = pattern_slash(color, width, interval).kind()
736            else {
737                panic!("pattern_slash did not produce a PatternSlash");
738            };
739            assert!((got_width - width).abs() <= 1.0 / 255.0);
740            assert!((got_interval - interval).abs() <= 1.0 / 255.0);
741        }
742    }
743
744    #[test]
745    fn test_rgba_alpha() {
746        use palette::WithAlpha;
747        let color = Rgba::<palette::Srgb>::new(0.2, 0.6, 1.0, 0.8);
748        assert_eq!(color.with_alpha(0.25).alpha, 0.25);
749        // NOTE: diverging from upstream, where Rgba::alpha clamps. palette does not clamp alpha
750        assert_eq!(color.with_alpha(1.5).alpha, 1.5);
751    }
752
753    #[test]
754    fn test_rgba_opacity() {
755        use super::ColorExt;
756        let color = Rgba::new(0.2, 0.6, 1.0, 0.8);
757        assert!((color.opacity(0.5).alpha - 0.4).abs() < 1e-6);
758        assert_eq!(color.opacity(2.0).alpha, 0.8);
759    }
760}