Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:53:21.427Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

color.rs

1071 lines · 30.4 KB · rust
1use anyhow::{Context as _, bail};
2use schemars::{JsonSchema, json_schema};
3use serde::{
4    Deserialize, Deserializer, Serialize, Serializer,
5    de::{self, Visitor},
6};
7use std::borrow::Cow;
8use std::{
9    fmt::{self, Display, Formatter},
10    hash::{Hash, Hasher},
11};
12
13/// Convert an RGB hex color code number to a color type
14pub fn rgb(hex: u32) -> Rgba {
15    let [_, r, g, b] = hex.to_be_bytes().map(|b| (b as f32) / 255.0);
16    Rgba { r, g, b, a: 1.0 }
17}
18
19/// Convert an RGBA hex color code number to [`Rgba`]
20pub fn rgba(hex: u32) -> Rgba {
21    let [r, g, b, a] = hex.to_be_bytes().map(|b| (b as f32) / 255.0);
22    Rgba { r, g, b, a }
23}
24
25/// Swap from RGBA with premultiplied alpha to BGRA
26pub fn swap_rgba_pa_to_bgra(color: &mut [u8]) {
27    color.swap(0, 2);
28    if color[3] > 0 {
29        let a = color[3] as f32 / 255.;
30        color[0] = (color[0] as f32 / a) as u8;
31        color[1] = (color[1] as f32 / a) as u8;
32        color[2] = (color[2] as f32 / a) as u8;
33    }
34}
35
36/// An RGBA color
37#[derive(PartialEq, Clone, Copy, Default)]
38#[repr(C)]
39pub struct Rgba {
40    /// The red component of the color, in the range 0.0 to 1.0
41    pub r: f32,
42    /// The green component of the color, in the range 0.0 to 1.0
43    pub g: f32,
44    /// The blue component of the color, in the range 0.0 to 1.0
45    pub b: f32,
46    /// The alpha component of the color, in the range 0.0 to 1.0
47    pub a: f32,
48}
49
50impl fmt::Debug for Rgba {
51    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
52        write!(f, "rgba({:#010x})", u32::from(*self))
53    }
54}
55
56impl Rgba {
57    /// Create a new [`Rgba`] color by blending this and another color together
58    pub fn blend(&self, other: Rgba) -> Self {
59        if other.a >= 1.0 {
60            other
61        } else if other.a <= 0.0 {
62            *self
63        } else {
64            Rgba {
65                r: (self.r * (1.0 - other.a)) + (other.r * other.a),
66                g: (self.g * (1.0 - other.a)) + (other.g * other.a),
67                b: (self.b * (1.0 - other.a)) + (other.b * other.a),
68                a: self.a,
69            }
70        }
71    }
72
73    /// Returns a new RGBA color with the same red, green and blue channels, but
74    /// with a new alpha value.
75    ///
76    /// Example:
77    /// ```
78    /// use gpui::rgba;
79    /// let color = rgba(0xFF0000FF);
80    /// let faded = color.alpha(0.25);
81    /// assert_eq!(faded.a, 0.25);
82    /// ```
83    ///
84    /// This will return a red color with 25% opacity.
85    ///
86    /// Example:
87    /// ```
88    /// use gpui::rgba;
89    /// let color = rgba(0x3399FFCC);
90    /// let transparent = color.alpha(0.0);
91    /// assert_eq!(transparent.a, 0.0);
92    /// ```
93    ///
94    /// This will return the same blue color, fully transparent.
95    pub fn alpha(&self, a: f32) -> Self {
96        Rgba {
97            r: self.r,
98            g: self.g,
99            b: self.b,
100            a: a.clamp(0., 1.),
101        }
102    }
103
104    /// Returns a new RGBA color with the same red, green, and blue channels,
105    /// but with the alpha channel multiplied by the given factor.
106    ///
107    /// Example:
108    /// ```
109    /// use gpui::rgba;
110    /// let color = rgba(0xFF0000FF); // Fully opaque red
111    /// let faded = color.opacity(0.5);
112    /// assert_eq!(faded.a, 0.5);
113    /// ```
114    ///
115    /// This will return a red color with 50% opacity.
116    ///
117    /// Example:
118    /// ```
119    /// use gpui::rgba;
120    /// let color = rgba(0x3399FFCC); // A light blue with 80% opacity
121    /// let faded = color.opacity(0.5);
122    /// assert!((faded.a - 0.4).abs() < 1e-6);
123    /// ```
124    ///
125    /// This will return the same blue color scaled down to 40% opacity.
126    pub fn opacity(&self, factor: f32) -> Self {
127        Rgba {
128            r: self.r,
129            g: self.g,
130            b: self.b,
131            a: self.a * factor.clamp(0., 1.),
132        }
133    }
134}
135
136impl From<Rgba> for u32 {
137    fn from(rgba: Rgba) -> Self {
138        let r = (rgba.r * 255.0) as u32;
139        let g = (rgba.g * 255.0) as u32;
140        let b = (rgba.b * 255.0) as u32;
141        let a = (rgba.a * 255.0) as u32;
142        (r << 24) | (g << 16) | (b << 8) | a
143    }
144}
145
146struct RgbaVisitor;
147
148impl Visitor<'_> for RgbaVisitor {
149    type Value = Rgba;
150
151    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
152        formatter.write_str("a string in the format #rrggbb or #rrggbbaa")
153    }
154
155    fn visit_str<E: de::Error>(self, value: &str) -> Result<Rgba, E> {
156        Rgba::try_from(value).map_err(E::custom)
157    }
158}
159
160impl JsonSchema for Rgba {
161    fn schema_name() -> Cow<'static, str> {
162        "Rgba".into()
163    }
164
165    fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
166        json_schema!({
167            "type": "string",
168            "pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$"
169        })
170    }
171}
172
173impl<'de> Deserialize<'de> for Rgba {
174    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
175        deserializer.deserialize_str(RgbaVisitor)
176    }
177}
178
179impl Serialize for Rgba {
180    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
181    where
182        S: Serializer,
183    {
184        let r = (self.r * 255.0).round() as u8;
185        let g = (self.g * 255.0).round() as u8;
186        let b = (self.b * 255.0).round() as u8;
187        let a = (self.a * 255.0).round() as u8;
188
189        let s = format!("#{r:02x}{g:02x}{b:02x}{a:02x}");
190        serializer.serialize_str(&s)
191    }
192}
193
194impl From<Hsla> for Rgba {
195    fn from(color: Hsla) -> Self {
196        let h = color.h;
197        let s = color.s;
198        let l = color.l;
199
200        let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
201        let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs());
202        let m = l - c / 2.0;
203        let cm = c + m;
204        let xm = x + m;
205
206        let (r, g, b) = match (h * 6.0).floor() as i32 {
207            0 | 6 => (cm, xm, m),
208            1 => (xm, cm, m),
209            2 => (m, cm, xm),
210            3 => (m, xm, cm),
211            4 => (xm, m, cm),
212            _ => (cm, m, xm),
213        };
214
215        Rgba {
216            r: r.clamp(0., 1.),
217            g: g.clamp(0., 1.),
218            b: b.clamp(0., 1.),
219            a: color.a,
220        }
221    }
222}
223
224impl TryFrom<&'_ str> for Rgba {
225    type Error = anyhow::Error;
226
227    fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
228        const RGB: usize = "rgb".len();
229        const RGBA: usize = "rgba".len();
230        const RRGGBB: usize = "rrggbb".len();
231        const RRGGBBAA: usize = "rrggbbaa".len();
232
233        const EXPECTED_FORMATS: &str = "Expected #rgb, #rgba, #rrggbb, or #rrggbbaa";
234        const INVALID_UNICODE: &str = "invalid unicode characters in color";
235
236        let Some(("", hex)) = value.trim().split_once('#') else {
237            bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}");
238        };
239
240        let (r, g, b, a) = match hex.len() {
241            RGB | RGBA => {
242                let r = u8::from_str_radix(
243                    hex.get(0..1).with_context(|| {
244                        format!("{INVALID_UNICODE}: r component of #rgb/#rgba for value: '{value}'")
245                    })?,
246                    16,
247                )?;
248                let g = u8::from_str_radix(
249                    hex.get(1..2).with_context(|| {
250                        format!("{INVALID_UNICODE}: g component of #rgb/#rgba for value: '{value}'")
251                    })?,
252                    16,
253                )?;
254                let b = u8::from_str_radix(
255                    hex.get(2..3).with_context(|| {
256                        format!("{INVALID_UNICODE}: b component of #rgb/#rgba for value: '{value}'")
257                    })?,
258                    16,
259                )?;
260                let a = if hex.len() == RGBA {
261                    u8::from_str_radix(
262                        hex.get(3..4).with_context(|| {
263                            format!("{INVALID_UNICODE}: a component of #rgba for value: '{value}'")
264                        })?,
265                        16,
266                    )?
267                } else {
268                    0xf
269                };
270
271                /// Duplicates a given hex digit.
272                /// E.g., `0xf` -> `0xff`.
273                const fn duplicate(value: u8) -> u8 {
274                    (value << 4) | value
275                }
276
277                (duplicate(r), duplicate(g), duplicate(b), duplicate(a))
278            }
279            RRGGBB | RRGGBBAA => {
280                let r = u8::from_str_radix(
281                    hex.get(0..2).with_context(|| {
282                        format!(
283                            "{}: r component of #rrggbb/#rrggbbaa for value: '{}'",
284                            INVALID_UNICODE, value
285                        )
286                    })?,
287                    16,
288                )?;
289                let g = u8::from_str_radix(
290                    hex.get(2..4).with_context(|| {
291                        format!(
292                            "{INVALID_UNICODE}: g component of #rrggbb/#rrggbbaa for value: '{value}'"
293                        )
294                    })?,
295                    16,
296                )?;
297                let b = u8::from_str_radix(
298                    hex.get(4..6).with_context(|| {
299                        format!(
300                            "{INVALID_UNICODE}: b component of #rrggbb/#rrggbbaa for value: '{value}'"
301                        )
302                    })?,
303                    16,
304                )?;
305                let a = if hex.len() == RRGGBBAA {
306                    u8::from_str_radix(
307                        hex.get(6..8).with_context(|| {
308                            format!(
309                                "{INVALID_UNICODE}: a component of #rrggbbaa for value: '{value}'"
310                            )
311                        })?,
312                        16,
313                    )?
314                } else {
315                    0xff
316                };
317                (r, g, b, a)
318            }
319            _ => bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}"),
320        };
321
322        Ok(Rgba {
323            r: r as f32 / 255.,
324            g: g as f32 / 255.,
325            b: b as f32 / 255.,
326            a: a as f32 / 255.,
327        })
328    }
329}
330
331/// An HSLA color
332#[derive(Default, Copy, Clone, Debug)]
333#[repr(C)]
334pub struct Hsla {
335    /// Hue, in a range from 0 to 1
336    pub h: f32,
337
338    /// Saturation, in a range from 0 to 1
339    pub s: f32,
340
341    /// Lightness, in a range from 0 to 1
342    pub l: f32,
343
344    /// Alpha, in a range from 0 to 1
345    pub a: f32,
346}
347
348#[cfg(feature = "proptest")]
349mod property {
350    use super::Hsla;
351    use proptest::prelude::*;
352
353    impl Hsla {
354        /// Proptest [`Strategy`] that produces opaque colors (i.e. alpha = 1).
355        ///
356        /// For truly arbitrary colors, use the [`Arbitrary`] implementation.
357        pub fn opaque_strategy() -> impl Strategy<Value = Self> {
358            (0.0f32..=1.0, 0.0f32..=1.0, 0.0f32..=1.0).prop_map(|(h, s, l)| Hsla { h, s, l, a: 1. })
359        }
360    }
361
362    impl Arbitrary for Hsla {
363        type Strategy = BoxedStrategy<Self>;
364        type Parameters = ();
365
366        fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
367            (0.0f32..=1.0, 0.0f32..=1.0, 0.0f32..=1.0, 0.0f32..=1.0)
368                .prop_map(|(h, s, l, a)| Hsla { h, s, l, a })
369                .boxed()
370        }
371    }
372}
373
374impl PartialEq for Hsla {
375    fn eq(&self, other: &Self) -> bool {
376        self.h
377            .total_cmp(&other.h)
378            .then(self.s.total_cmp(&other.s))
379            .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a)))
380            .is_eq()
381    }
382}
383
384impl PartialOrd for Hsla {
385    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
386        Some(self.cmp(other))
387    }
388}
389
390impl Ord for Hsla {
391    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
392        self.h
393            .total_cmp(&other.h)
394            .then(self.s.total_cmp(&other.s))
395            .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a)))
396    }
397}
398
399impl Eq for Hsla {}
400
401impl Hash for Hsla {
402    fn hash<H: Hasher>(&self, state: &mut H) {
403        state.write_u32(u32::from_be_bytes(self.h.to_be_bytes()));
404        state.write_u32(u32::from_be_bytes(self.s.to_be_bytes()));
405        state.write_u32(u32::from_be_bytes(self.l.to_be_bytes()));
406        state.write_u32(u32::from_be_bytes(self.a.to_be_bytes()));
407    }
408}
409
410impl Display for Hsla {
411    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
412        write!(
413            f,
414            "hsla({:.2}, {:.2}%, {:.2}%, {:.2})",
415            self.h * 360.,
416            self.s * 100.,
417            self.l * 100.,
418            self.a
419        )
420    }
421}
422
423/// Construct an [`Hsla`] object from plain values
424pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
425    Hsla {
426        h: h.clamp(0., 1.),
427        s: s.clamp(0., 1.),
428        l: l.clamp(0., 1.),
429        a: a.clamp(0., 1.),
430    }
431}
432
433/// Pure black in [`Hsla`]
434pub const fn black() -> Hsla {
435    Hsla {
436        h: 0.,
437        s: 0.,
438        l: 0.,
439        a: 1.,
440    }
441}
442
443/// Transparent black in [`Hsla`]
444pub const fn transparent_black() -> Hsla {
445    Hsla {
446        h: 0.,
447        s: 0.,
448        l: 0.,
449        a: 0.,
450    }
451}
452
453/// Transparent white in [`Hsla`]
454pub const fn transparent_white() -> Hsla {
455    Hsla {
456        h: 0.,
457        s: 0.,
458        l: 1.,
459        a: 0.,
460    }
461}
462
463/// Opaque grey in [`Hsla`], values will be clamped to the range [0, 1]
464pub fn opaque_grey(lightness: f32, opacity: f32) -> Hsla {
465    Hsla {
466        h: 0.,
467        s: 0.,
468        l: lightness.clamp(0., 1.),
469        a: opacity.clamp(0., 1.),
470    }
471}
472
473/// Pure white in [`Hsla`]
474pub const fn white() -> Hsla {
475    Hsla {
476        h: 0.,
477        s: 0.,
478        l: 1.,
479        a: 1.,
480    }
481}
482
483/// The color red in [`Hsla`]
484pub const fn red() -> Hsla {
485    Hsla {
486        h: 0.,
487        s: 1.,
488        l: 0.5,
489        a: 1.,
490    }
491}
492
493/// The color blue in [`Hsla`]
494pub const fn blue() -> Hsla {
495    Hsla {
496        h: 0.6666666667,
497        s: 1.,
498        l: 0.5,
499        a: 1.,
500    }
501}
502
503/// The color green in [`Hsla`]
504pub const fn green() -> Hsla {
505    Hsla {
506        h: 0.3333333333,
507        s: 1.,
508        l: 0.25,
509        a: 1.,
510    }
511}
512
513/// The color yellow in [`Hsla`]
514pub const fn yellow() -> Hsla {
515    Hsla {
516        h: 0.1666666667,
517        s: 1.,
518        l: 0.5,
519        a: 1.,
520    }
521}
522
523impl Hsla {
524    /// Converts this HSLA color to an RGBA color.
525    pub fn to_rgb(self) -> Rgba {
526        self.into()
527    }
528
529    /// The color red
530    pub const fn red() -> Self {
531        red()
532    }
533
534    /// The color green
535    pub const fn green() -> Self {
536        green()
537    }
538
539    /// The color blue
540    pub const fn blue() -> Self {
541        blue()
542    }
543
544    /// The color black
545    pub const fn black() -> Self {
546        black()
547    }
548
549    /// The color white
550    pub const fn white() -> Self {
551        white()
552    }
553
554    /// The color transparent black
555    pub const fn transparent_black() -> Self {
556        transparent_black()
557    }
558
559    /// Returns true if the HSLA color is fully transparent, false otherwise.
560    pub fn is_transparent(&self) -> bool {
561        self.a == 0.0
562    }
563
564    /// Returns true if the HSLA color is fully opaque, false otherwise.
565    pub fn is_opaque(&self) -> bool {
566        self.a == 1.0
567    }
568
569    /// Blends `other` on top of `self` based on `other`'s alpha value. The resulting color is a combination of `self`'s and `other`'s colors.
570    ///
571    /// If `other`'s alpha value is 1.0 or greater, `other` color is fully opaque, thus `other` is returned as the output color.
572    /// If `other`'s alpha value is 0.0 or less, `other` color is fully transparent, thus `self` is returned as the output color.
573    /// Else, the output color is calculated as a blend of `self` and `other` based on their weighted alpha values.
574    ///
575    /// Assumptions:
576    /// - Alpha values are contained in the range [0, 1], with 1 as fully opaque and 0 as fully transparent.
577    /// - The relative contributions of `self` and `other` is based on `self`'s alpha value (`self.a`) and `other`'s  alpha value (`other.a`), `self` contributing `self.a * (1.0 - other.a)` and `other` contributing its own alpha value.
578    /// - RGB color components are contained in the range [0, 1].
579    /// - If `self` and `other` colors are out of the valid range, the blend operation's output and behavior is undefined.
580    pub fn blend(self, other: Hsla) -> Hsla {
581        let alpha = other.a;
582
583        if alpha >= 1.0 {
584            other
585        } else if alpha <= 0.0 {
586            self
587        } else {
588            let converted_self = Rgba::from(self);
589            let converted_other = Rgba::from(other);
590            let blended_rgb = converted_self.blend(converted_other);
591            Hsla::from(blended_rgb)
592        }
593    }
594
595    /// Returns a new HSLA color with the same hue, and lightness, but with no saturation.
596    pub fn grayscale(&self) -> Self {
597        Hsla {
598            h: self.h,
599            s: 0.,
600            l: self.l,
601            a: self.a,
602        }
603    }
604
605    /// Fade out the color by a given factor. This factor should be between 0.0 and 1.0.
606    /// Where 0.0 will leave the color unchanged, and 1.0 will completely fade out the color.
607    pub fn fade_out(&mut self, factor: f32) {
608        self.a *= 1.0 - factor.clamp(0., 1.);
609    }
610
611    /// Multiplies the alpha value of the color by a given factor
612    /// and returns a new HSLA color.
613    ///
614    /// Useful for transforming colors with dynamic opacity,
615    /// like a color from an external source.
616    ///
617    /// Example:
618    /// ```
619    /// let color = gpui::red();
620    /// let faded_color = color.opacity(0.5);
621    /// assert_eq!(faded_color.a, 0.5);
622    /// ```
623    ///
624    /// This will return a red color with half the opacity.
625    ///
626    /// Example:
627    /// ```
628    /// use gpui::hsla;
629    /// let color = hsla(0.7, 1.0, 0.5, 0.7); // A saturated blue
630    /// let faded_color = color.opacity(0.16);
631    /// assert!((faded_color.a - 0.112).abs() < 1e-6);
632    /// ```
633    ///
634    /// This will return a blue color with around ~10% opacity,
635    /// suitable for an element's hover or selected state.
636    ///
637    pub fn opacity(&self, factor: f32) -> Self {
638        Hsla {
639            h: self.h,
640            s: self.s,
641            l: self.l,
642            a: self.a * factor.clamp(0., 1.),
643        }
644    }
645
646    /// Returns a new HSLA color with the same hue, saturation,
647    /// and lightness, but with a new alpha value.
648    ///
649    /// Example:
650    /// ```
651    /// let color = gpui::red();
652    /// let red_color = color.alpha(0.25);
653    /// assert_eq!(red_color.a, 0.25);
654    /// ```
655    ///
656    /// This will return a red color with 25% opacity.
657    ///
658    /// Example:
659    /// ```
660    /// use gpui::hsla;
661    /// let color = hsla(0.7, 1.0, 0.5, 0.7); // A saturated blue
662    /// let faded_color = color.alpha(0.25);
663    /// assert_eq!(faded_color.a, 0.25);
664    /// ```
665    ///
666    /// This will return a blue color with 25% opacity.
667    pub fn alpha(&self, a: f32) -> Self {
668        Hsla {
669            h: self.h,
670            s: self.s,
671            l: self.l,
672            a: a.clamp(0., 1.),
673        }
674    }
675}
676
677impl From<Rgba> for Hsla {
678    fn from(color: Rgba) -> Self {
679        let r = color.r;
680        let g = color.g;
681        let b = color.b;
682
683        let max = r.max(g.max(b));
684        let min = r.min(g.min(b));
685        let delta = max - min;
686
687        let l = (max + min) / 2.0;
688        let s = if l == 0.0 || l == 1.0 {
689            0.0
690        } else if l < 0.5 {
691            delta / (2.0 * l)
692        } else {
693            delta / (2.0 - 2.0 * l)
694        };
695
696        let h = if delta == 0.0 {
697            0.0
698        } else if max == r {
699            ((g - b) / delta).rem_euclid(6.0) / 6.0
700        } else if max == g {
701            ((b - r) / delta + 2.0) / 6.0
702        } else {
703            ((r - g) / delta + 4.0) / 6.0
704        };
705
706        Hsla {
707            h,
708            s,
709            l,
710            a: color.a,
711        }
712    }
713}
714
715impl JsonSchema for Hsla {
716    fn schema_name() -> Cow<'static, str> {
717        Rgba::schema_name()
718    }
719
720    fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
721        Rgba::json_schema(generator)
722    }
723}
724
725impl Serialize for Hsla {
726    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
727    where
728        S: Serializer,
729    {
730        Rgba::from(*self).serialize(serializer)
731    }
732}
733
734impl<'de> Deserialize<'de> for Hsla {
735    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
736    where
737        D: Deserializer<'de>,
738    {
739        Ok(Rgba::deserialize(deserializer)?.into())
740    }
741}
742
743#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
744#[repr(C)]
745pub(crate) enum BackgroundTag {
746    Solid = 0,
747    LinearGradient = 1,
748    PatternSlash = 2,
749    Checkerboard = 3,
750}
751
752/// A color space for color interpolation.
753///
754/// References:
755/// - <https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method>
756/// - <https://www.w3.org/TR/css-color-4/#typedef-color-space>
757#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
758#[repr(C)]
759pub enum ColorSpace {
760    #[default]
761    /// The sRGB color space.
762    Srgb = 0,
763    /// The Oklab color space.
764    Oklab = 1,
765}
766
767impl Display for ColorSpace {
768    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
769        match self {
770            ColorSpace::Srgb => write!(f, "sRGB"),
771            ColorSpace::Oklab => write!(f, "Oklab"),
772        }
773    }
774}
775
776/// A background color, which can be either a solid color or a linear gradient.
777#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
778#[repr(C)]
779pub struct Background {
780    pub(crate) tag: BackgroundTag,
781    pub(crate) color_space: ColorSpace,
782    pub(crate) solid: Hsla,
783    pub(crate) gradient_angle_or_pattern_height: f32,
784    pub(crate) colors: [LinearColorStop; 2],
785    /// Padding for alignment for repr(C) layout.
786    pad: u32,
787}
788
789impl std::fmt::Debug for Background {
790    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
791        match self.tag {
792            BackgroundTag::Solid => write!(f, "Solid({:?})", self.solid),
793            BackgroundTag::LinearGradient => write!(
794                f,
795                "LinearGradient({}, {:?}, {:?})",
796                self.gradient_angle_or_pattern_height, self.colors[0], self.colors[1]
797            ),
798            BackgroundTag::PatternSlash => write!(
799                f,
800                "PatternSlash({:?}, {})",
801                self.solid, self.gradient_angle_or_pattern_height
802            ),
803            BackgroundTag::Checkerboard => write!(
804                f,
805                "Checkerboard({:?}, {})",
806                self.solid, self.gradient_angle_or_pattern_height
807            ),
808        }
809    }
810}
811
812impl Eq for Background {}
813impl Default for Background {
814    fn default() -> Self {
815        Self {
816            tag: BackgroundTag::Solid,
817            solid: Hsla::default(),
818            color_space: ColorSpace::default(),
819            gradient_angle_or_pattern_height: 0.0,
820            colors: [LinearColorStop::default(), LinearColorStop::default()],
821            pad: 0,
822        }
823    }
824}
825
826/// Creates a hash pattern background
827pub fn pattern_slash(color: impl Into<Hsla>, width: f32, interval: f32) -> Background {
828    let width_scaled = (width * 255.0) as u32;
829    let interval_scaled = (interval * 255.0) as u32;
830    let height = ((width_scaled * 0xFFFF) + interval_scaled) as f32;
831
832    Background {
833        tag: BackgroundTag::PatternSlash,
834        solid: color.into(),
835        gradient_angle_or_pattern_height: height,
836        ..Default::default()
837    }
838}
839
840/// Creates a checkerboard pattern background
841pub fn checkerboard(color: impl Into<Hsla>, size: f32) -> Background {
842    Background {
843        tag: BackgroundTag::Checkerboard,
844        solid: color.into(),
845        gradient_angle_or_pattern_height: size,
846        ..Default::default()
847    }
848}
849
850/// Creates a solid background color.
851pub fn solid_background(color: impl Into<Hsla>) -> Background {
852    Background {
853        solid: color.into(),
854        ..Default::default()
855    }
856}
857
858/// Creates a LinearGradient background color.
859///
860/// The gradient line's angle of direction. A value of `0.` is equivalent to top; increasing values rotate clockwise from there.
861///
862/// The `angle` is in degrees value in the range 0.0 to 360.0.
863///
864/// <https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient>
865pub fn linear_gradient(
866    angle: f32,
867    from: impl Into<LinearColorStop>,
868    to: impl Into<LinearColorStop>,
869) -> Background {
870    Background {
871        tag: BackgroundTag::LinearGradient,
872        gradient_angle_or_pattern_height: angle,
873        colors: [from.into(), to.into()],
874        ..Default::default()
875    }
876}
877
878/// A color stop in a linear gradient.
879///
880/// <https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient#linear-color-stop>
881#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
882#[repr(C)]
883pub struct LinearColorStop {
884    /// The color of the color stop.
885    pub color: Hsla,
886    /// The percentage of the gradient, in the range 0.0 to 1.0.
887    pub percentage: f32,
888}
889
890/// Creates a new linear color stop.
891///
892/// The percentage of the gradient, in the range 0.0 to 1.0.
893pub fn linear_color_stop(color: impl Into<Hsla>, percentage: f32) -> LinearColorStop {
894    LinearColorStop {
895        color: color.into(),
896        percentage,
897    }
898}
899
900impl LinearColorStop {
901    /// Returns a new color stop with the same color, but with a modified alpha value.
902    pub fn opacity(&self, factor: f32) -> Self {
903        Self {
904            percentage: self.percentage,
905            color: self.color.opacity(factor),
906        }
907    }
908}
909
910impl Background {
911    /// Returns the solid color if this is a solid background, None otherwise.
912    pub fn as_solid(&self) -> Option<Hsla> {
913        if self.tag == BackgroundTag::Solid {
914            Some(self.solid)
915        } else {
916            None
917        }
918    }
919
920    /// Use specified color space for color interpolation.
921    ///
922    /// <https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method>
923    pub fn color_space(mut self, color_space: ColorSpace) -> Self {
924        self.color_space = color_space;
925        self
926    }
927
928    /// Returns a new background color with the same hue, saturation, and lightness, but with a modified alpha value.
929    pub fn opacity(&self, factor: f32) -> Self {
930        let mut background = *self;
931        background.solid = background.solid.opacity(factor);
932        background.colors = [
933            self.colors[0].opacity(factor),
934            self.colors[1].opacity(factor),
935        ];
936        background
937    }
938
939    /// Returns whether the background color is transparent.
940    pub fn is_transparent(&self) -> bool {
941        match self.tag {
942            BackgroundTag::Solid => self.solid.is_transparent(),
943            BackgroundTag::LinearGradient => self.colors.iter().all(|c| c.color.is_transparent()),
944            BackgroundTag::PatternSlash => self.solid.is_transparent(),
945            BackgroundTag::Checkerboard => self.solid.is_transparent(),
946        }
947    }
948}
949
950impl From<Hsla> for Background {
951    fn from(value: Hsla) -> Self {
952        Background {
953            tag: BackgroundTag::Solid,
954            solid: value,
955            ..Default::default()
956        }
957    }
958}
959
960impl From<Rgba> for Background {
961    fn from(value: Rgba) -> Self {
962        Background {
963            tag: BackgroundTag::Solid,
964            solid: Hsla::from(value),
965            ..Default::default()
966        }
967    }
968}
969
970#[cfg(test)]
971mod tests {
972    use serde_json::json;
973
974    use super::*;
975
976    #[test]
977    fn test_deserialize_three_value_hex_to_rgba() {
978        let actual: Rgba = serde_json::from_value(json!("#f09")).unwrap();
979
980        assert_eq!(actual, rgba(0xff0099ff))
981    }
982
983    #[test]
984    fn test_deserialize_four_value_hex_to_rgba() {
985        let actual: Rgba = serde_json::from_value(json!("#f09f")).unwrap();
986
987        assert_eq!(actual, rgba(0xff0099ff))
988    }
989
990    #[test]
991    fn test_deserialize_six_value_hex_to_rgba() {
992        let actual: Rgba = serde_json::from_value(json!("#ff0099")).unwrap();
993
994        assert_eq!(actual, rgba(0xff0099ff))
995    }
996
997    #[test]
998    fn test_deserialize_eight_value_hex_to_rgba() {
999        let actual: Rgba = serde_json::from_value(json!("#ff0099ff")).unwrap();
1000
1001        assert_eq!(actual, rgba(0xff0099ff))
1002    }
1003
1004    #[test]
1005    fn test_deserialize_eight_value_hex_with_padding_to_rgba() {
1006        let actual: Rgba = serde_json::from_value(json!(" #f5f5f5ff   ")).unwrap();
1007
1008        assert_eq!(actual, rgba(0xf5f5f5ff))
1009    }
1010
1011    #[test]
1012    fn test_deserialize_eight_value_hex_with_mixed_case_to_rgba() {
1013        let actual: Rgba = serde_json::from_value(json!("#DeAdbEeF")).unwrap();
1014
1015        assert_eq!(actual, rgba(0xdeadbeef))
1016    }
1017
1018    #[test]
1019    fn test_background_solid() {
1020        let color = Hsla::from(rgba(0xff0099ff));
1021        let mut background = Background::from(color);
1022        assert_eq!(background.tag, BackgroundTag::Solid);
1023        assert_eq!(background.solid, color);
1024
1025        assert_eq!(background.opacity(0.5).solid, color.opacity(0.5));
1026        assert!(!background.is_transparent());
1027        background.solid = hsla(0.0, 0.0, 0.0, 0.0);
1028        assert!(background.is_transparent());
1029    }
1030
1031    #[test]
1032    fn test_background_linear_gradient() {
1033        let from = linear_color_stop(rgba(0xff0099ff), 0.0);
1034        let to = linear_color_stop(rgba(0x00ff99ff), 1.0);
1035        let background = linear_gradient(90.0, from, to);
1036        assert_eq!(background.tag, BackgroundTag::LinearGradient);
1037        assert_eq!(background.colors[0], from);
1038        assert_eq!(background.colors[1], to);
1039
1040        assert_eq!(background.opacity(0.5).colors[0], from.opacity(0.5));
1041        assert_eq!(background.opacity(0.5).colors[1], to.opacity(0.5));
1042        assert!(!background.is_transparent());
1043        assert!(background.opacity(0.0).is_transparent());
1044    }
1045
1046    #[test]
1047    fn test_rgba_alpha() {
1048        let color = Rgba {
1049            r: 0.2,
1050            g: 0.6,
1051            b: 1.0,
1052            a: 0.8,
1053        };
1054
1055        assert_eq!(color.alpha(0.25).a, 0.25);
1056        assert_eq!(color.alpha(1.5).a, 1.0);
1057    }
1058
1059    #[test]
1060    fn test_rgba_opacity() {
1061        let color = Rgba {
1062            r: 0.2,
1063            g: 0.6,
1064            b: 1.0,
1065            a: 0.8,
1066        };
1067        assert!((color.opacity(0.5).a - 0.4).abs() < 1e-6);
1068        assert_eq!(color.opacity(2.0).a, 0.8);
1069    }
1070}
1071
Served at tenant.openagents/omega Member data and write actions are omitted.