Skip to repository content1071 lines · 30.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:53:21.427Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
color.rs
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