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
13pub 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
19pub 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
25pub 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#[derive(PartialEq, Clone, Copy, Default)]
38#[repr(C)]
39pub struct Rgba {
40 pub r: f32,
42 pub g: f32,
44 pub b: f32,
46 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 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
74impl From<Rgba> for u32 {
75 fn from(rgba: Rgba) -> Self {
76 let r = (rgba.r * 255.0) as u32;
77 let g = (rgba.g * 255.0) as u32;
78 let b = (rgba.b * 255.0) as u32;
79 let a = (rgba.a * 255.0) as u32;
80 (r << 24) | (g << 16) | (b << 8) | a
81 }
82}
83
84struct RgbaVisitor;
85
86impl Visitor<'_> for RgbaVisitor {
87 type Value = Rgba;
88
89 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
90 formatter.write_str("a string in the format #rrggbb or #rrggbbaa")
91 }
92
93 fn visit_str<E: de::Error>(self, value: &str) -> Result<Rgba, E> {
94 Rgba::try_from(value).map_err(E::custom)
95 }
96}
97
98impl JsonSchema for Rgba {
99 fn schema_name() -> Cow<'static, str> {
100 "Rgba".into()
101 }
102
103 fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
104 json_schema!({
105 "type": "string",
106 "pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$"
107 })
108 }
109}
110
111impl<'de> Deserialize<'de> for Rgba {
112 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
113 deserializer.deserialize_str(RgbaVisitor)
114 }
115}
116
117impl Serialize for Rgba {
118 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
119 where
120 S: Serializer,
121 {
122 let r = (self.r * 255.0).round() as u8;
123 let g = (self.g * 255.0).round() as u8;
124 let b = (self.b * 255.0).round() as u8;
125 let a = (self.a * 255.0).round() as u8;
126
127 let s = format!("#{r:02x}{g:02x}{b:02x}{a:02x}");
128 serializer.serialize_str(&s)
129 }
130}
131
132impl From<Hsla> for Rgba {
133 fn from(color: Hsla) -> Self {
134 let h = color.h;
135 let s = color.s;
136 let l = color.l;
137
138 let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
139 let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs());
140 let m = l - c / 2.0;
141 let cm = c + m;
142 let xm = x + m;
143
144 let (r, g, b) = match (h * 6.0).floor() as i32 {
145 0 | 6 => (cm, xm, m),
146 1 => (xm, cm, m),
147 2 => (m, cm, xm),
148 3 => (m, xm, cm),
149 4 => (xm, m, cm),
150 _ => (cm, m, xm),
151 };
152
153 Rgba {
154 r: r.clamp(0., 1.),
155 g: g.clamp(0., 1.),
156 b: b.clamp(0., 1.),
157 a: color.a,
158 }
159 }
160}
161
162impl TryFrom<&'_ str> for Rgba {
163 type Error = anyhow::Error;
164
165 fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
166 const RGB: usize = "rgb".len();
167 const RGBA: usize = "rgba".len();
168 const RRGGBB: usize = "rrggbb".len();
169 const RRGGBBAA: usize = "rrggbbaa".len();
170
171 const EXPECTED_FORMATS: &str = "Expected #rgb, #rgba, #rrggbb, or #rrggbbaa";
172 const INVALID_UNICODE: &str = "invalid unicode characters in color";
173
174 let Some(("", hex)) = value.trim().split_once('#') else {
175 bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}");
176 };
177
178 let (r, g, b, a) = match hex.len() {
179 RGB | RGBA => {
180 let r = u8::from_str_radix(
181 hex.get(0..1).with_context(|| {
182 format!("{INVALID_UNICODE}: r component of #rgb/#rgba for value: '{value}'")
183 })?,
184 16,
185 )?;
186 let g = u8::from_str_radix(
187 hex.get(1..2).with_context(|| {
188 format!("{INVALID_UNICODE}: g component of #rgb/#rgba for value: '{value}'")
189 })?,
190 16,
191 )?;
192 let b = u8::from_str_radix(
193 hex.get(2..3).with_context(|| {
194 format!("{INVALID_UNICODE}: b component of #rgb/#rgba for value: '{value}'")
195 })?,
196 16,
197 )?;
198 let a = if hex.len() == RGBA {
199 u8::from_str_radix(
200 hex.get(3..4).with_context(|| {
201 format!("{INVALID_UNICODE}: a component of #rgba for value: '{value}'")
202 })?,
203 16,
204 )?
205 } else {
206 0xf
207 };
208
209 const fn duplicate(value: u8) -> u8 {
212 (value << 4) | value
213 }
214
215 (duplicate(r), duplicate(g), duplicate(b), duplicate(a))
216 }
217 RRGGBB | RRGGBBAA => {
218 let r = u8::from_str_radix(
219 hex.get(0..2).with_context(|| {
220 format!(
221 "{}: r component of #rrggbb/#rrggbbaa for value: '{}'",
222 INVALID_UNICODE, value
223 )
224 })?,
225 16,
226 )?;
227 let g = u8::from_str_radix(
228 hex.get(2..4).with_context(|| {
229 format!(
230 "{INVALID_UNICODE}: g component of #rrggbb/#rrggbbaa for value: '{value}'"
231 )
232 })?,
233 16,
234 )?;
235 let b = u8::from_str_radix(
236 hex.get(4..6).with_context(|| {
237 format!(
238 "{INVALID_UNICODE}: b component of #rrggbb/#rrggbbaa for value: '{value}'"
239 )
240 })?,
241 16,
242 )?;
243 let a = if hex.len() == RRGGBBAA {
244 u8::from_str_radix(
245 hex.get(6..8).with_context(|| {
246 format!(
247 "{INVALID_UNICODE}: a component of #rrggbbaa for value: '{value}'"
248 )
249 })?,
250 16,
251 )?
252 } else {
253 0xff
254 };
255 (r, g, b, a)
256 }
257 _ => bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}"),
258 };
259
260 Ok(Rgba {
261 r: r as f32 / 255.,
262 g: g as f32 / 255.,
263 b: b as f32 / 255.,
264 a: a as f32 / 255.,
265 })
266 }
267}
268
269#[derive(Default, Copy, Clone, Debug)]
271#[repr(C)]
272pub struct Hsla {
273 pub h: f32,
275
276 pub s: f32,
278
279 pub l: f32,
281
282 pub a: f32,
284}
285
286#[cfg(feature = "proptest")]
287mod property {
288 use super::Hsla;
289 use proptest::prelude::*;
290
291 impl Hsla {
292 pub fn opaque_strategy() -> impl Strategy<Value = Self> {
296 (0.0f32..=1.0, 0.0f32..=1.0, 0.0f32..=1.0).prop_map(|(h, s, l)| Hsla { h, s, l, a: 1. })
297 }
298 }
299
300 impl Arbitrary for Hsla {
301 type Strategy = BoxedStrategy<Self>;
302 type Parameters = ();
303
304 fn arbitrary_with((): Self::Parameters) -> Self::Strategy {
305 (0.0f32..=1.0, 0.0f32..=1.0, 0.0f32..=1.0, 0.0f32..=1.0)
306 .prop_map(|(h, s, l, a)| Hsla { h, s, l, a })
307 .boxed()
308 }
309 }
310}
311
312impl PartialEq for Hsla {
313 fn eq(&self, other: &Self) -> bool {
314 self.h
315 .total_cmp(&other.h)
316 .then(self.s.total_cmp(&other.s))
317 .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a)))
318 .is_eq()
319 }
320}
321
322impl PartialOrd for Hsla {
323 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
324 Some(self.cmp(other))
325 }
326}
327
328impl Ord for Hsla {
329 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
330 self.h
331 .total_cmp(&other.h)
332 .then(self.s.total_cmp(&other.s))
333 .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a)))
334 }
335}
336
337impl Eq for Hsla {}
338
339impl Hash for Hsla {
340 fn hash<H: Hasher>(&self, state: &mut H) {
341 state.write_u32(u32::from_be_bytes(self.h.to_be_bytes()));
342 state.write_u32(u32::from_be_bytes(self.s.to_be_bytes()));
343 state.write_u32(u32::from_be_bytes(self.l.to_be_bytes()));
344 state.write_u32(u32::from_be_bytes(self.a.to_be_bytes()));
345 }
346}
347
348impl Display for Hsla {
349 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
350 write!(
351 f,
352 "hsla({:.2}, {:.2}%, {:.2}%, {:.2})",
353 self.h * 360.,
354 self.s * 100.,
355 self.l * 100.,
356 self.a
357 )
358 }
359}
360
361pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
363 Hsla {
364 h: h.clamp(0., 1.),
365 s: s.clamp(0., 1.),
366 l: l.clamp(0., 1.),
367 a: a.clamp(0., 1.),
368 }
369}
370
371pub const fn black() -> Hsla {
373 Hsla {
374 h: 0.,
375 s: 0.,
376 l: 0.,
377 a: 1.,
378 }
379}
380
381pub const fn transparent_black() -> Hsla {
383 Hsla {
384 h: 0.,
385 s: 0.,
386 l: 0.,
387 a: 0.,
388 }
389}
390
391pub const fn transparent_white() -> Hsla {
393 Hsla {
394 h: 0.,
395 s: 0.,
396 l: 1.,
397 a: 0.,
398 }
399}
400
401pub fn opaque_grey(lightness: f32, opacity: f32) -> Hsla {
403 Hsla {
404 h: 0.,
405 s: 0.,
406 l: lightness.clamp(0., 1.),
407 a: opacity.clamp(0., 1.),
408 }
409}
410
411pub const fn white() -> Hsla {
413 Hsla {
414 h: 0.,
415 s: 0.,
416 l: 1.,
417 a: 1.,
418 }
419}
420
421pub const fn red() -> Hsla {
423 Hsla {
424 h: 0.,
425 s: 1.,
426 l: 0.5,
427 a: 1.,
428 }
429}
430
431pub const fn blue() -> Hsla {
433 Hsla {
434 h: 0.6666666667,
435 s: 1.,
436 l: 0.5,
437 a: 1.,
438 }
439}
440
441pub const fn green() -> Hsla {
443 Hsla {
444 h: 0.3333333333,
445 s: 1.,
446 l: 0.25,
447 a: 1.,
448 }
449}
450
451pub const fn yellow() -> Hsla {
453 Hsla {
454 h: 0.1666666667,
455 s: 1.,
456 l: 0.5,
457 a: 1.,
458 }
459}
460
461impl Hsla {
462 pub fn to_rgb(self) -> Rgba {
464 self.into()
465 }
466
467 pub const fn red() -> Self {
469 red()
470 }
471
472 pub const fn green() -> Self {
474 green()
475 }
476
477 pub const fn blue() -> Self {
479 blue()
480 }
481
482 pub const fn black() -> Self {
484 black()
485 }
486
487 pub const fn white() -> Self {
489 white()
490 }
491
492 pub const fn transparent_black() -> Self {
494 transparent_black()
495 }
496
497 pub fn is_transparent(&self) -> bool {
499 self.a == 0.0
500 }
501
502 pub fn is_opaque(&self) -> bool {
504 self.a == 1.0
505 }
506
507 pub fn blend(self, other: Hsla) -> Hsla {
519 let alpha = other.a;
520
521 if alpha >= 1.0 {
522 other
523 } else if alpha <= 0.0 {
524 self
525 } else {
526 let converted_self = Rgba::from(self);
527 let converted_other = Rgba::from(other);
528 let blended_rgb = converted_self.blend(converted_other);
529 Hsla::from(blended_rgb)
530 }
531 }
532
533 pub fn grayscale(&self) -> Self {
535 Hsla {
536 h: self.h,
537 s: 0.,
538 l: self.l,
539 a: self.a,
540 }
541 }
542
543 pub fn fade_out(&mut self, factor: f32) {
546 self.a *= 1.0 - factor.clamp(0., 1.);
547 }
548
549 pub fn opacity(&self, factor: f32) -> Self {
576 Hsla {
577 h: self.h,
578 s: self.s,
579 l: self.l,
580 a: self.a * factor.clamp(0., 1.),
581 }
582 }
583
584 pub fn alpha(&self, a: f32) -> Self {
606 Hsla {
607 h: self.h,
608 s: self.s,
609 l: self.l,
610 a: a.clamp(0., 1.),
611 }
612 }
613}
614
615impl From<Rgba> for Hsla {
616 fn from(color: Rgba) -> Self {
617 let r = color.r;
618 let g = color.g;
619 let b = color.b;
620
621 let max = r.max(g.max(b));
622 let min = r.min(g.min(b));
623 let delta = max - min;
624
625 let l = (max + min) / 2.0;
626 let s = if l == 0.0 || l == 1.0 {
627 0.0
628 } else if l < 0.5 {
629 delta / (2.0 * l)
630 } else {
631 delta / (2.0 - 2.0 * l)
632 };
633
634 let h = if delta == 0.0 {
635 0.0
636 } else if max == r {
637 ((g - b) / delta).rem_euclid(6.0) / 6.0
638 } else if max == g {
639 ((b - r) / delta + 2.0) / 6.0
640 } else {
641 ((r - g) / delta + 4.0) / 6.0
642 };
643
644 Hsla {
645 h,
646 s,
647 l,
648 a: color.a,
649 }
650 }
651}
652
653impl JsonSchema for Hsla {
654 fn schema_name() -> Cow<'static, str> {
655 Rgba::schema_name()
656 }
657
658 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
659 Rgba::json_schema(generator)
660 }
661}
662
663impl Serialize for Hsla {
664 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
665 where
666 S: Serializer,
667 {
668 Rgba::from(*self).serialize(serializer)
669 }
670}
671
672impl<'de> Deserialize<'de> for Hsla {
673 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
674 where
675 D: Deserializer<'de>,
676 {
677 Ok(Rgba::deserialize(deserializer)?.into())
678 }
679}
680
681#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
682#[repr(C)]
683pub(crate) enum BackgroundTag {
684 Solid = 0,
685 LinearGradient = 1,
686 PatternSlash = 2,
687 Checkerboard = 3,
688}
689
690#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
696#[repr(C)]
697pub enum ColorSpace {
698 #[default]
699 Srgb = 0,
701 Oklab = 1,
703}
704
705impl Display for ColorSpace {
706 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
707 match self {
708 ColorSpace::Srgb => write!(f, "sRGB"),
709 ColorSpace::Oklab => write!(f, "Oklab"),
710 }
711 }
712}
713
714#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
716#[repr(C)]
717pub struct Background {
718 pub(crate) tag: BackgroundTag,
719 pub(crate) color_space: ColorSpace,
720 pub(crate) solid: Hsla,
721 pub(crate) gradient_angle_or_pattern_height: f32,
722 pub(crate) colors: [LinearColorStop; 2],
723 pad: u32,
725}
726
727impl std::fmt::Debug for Background {
728 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
729 match self.tag {
730 BackgroundTag::Solid => write!(f, "Solid({:?})", self.solid),
731 BackgroundTag::LinearGradient => write!(
732 f,
733 "LinearGradient({}, {:?}, {:?})",
734 self.gradient_angle_or_pattern_height, self.colors[0], self.colors[1]
735 ),
736 BackgroundTag::PatternSlash => write!(
737 f,
738 "PatternSlash({:?}, {})",
739 self.solid, self.gradient_angle_or_pattern_height
740 ),
741 BackgroundTag::Checkerboard => write!(
742 f,
743 "Checkerboard({:?}, {})",
744 self.solid, self.gradient_angle_or_pattern_height
745 ),
746 }
747 }
748}
749
750impl Eq for Background {}
751impl Default for Background {
752 fn default() -> Self {
753 Self {
754 tag: BackgroundTag::Solid,
755 solid: Hsla::default(),
756 color_space: ColorSpace::default(),
757 gradient_angle_or_pattern_height: 0.0,
758 colors: [LinearColorStop::default(), LinearColorStop::default()],
759 pad: 0,
760 }
761 }
762}
763
764pub fn pattern_slash(color: impl Into<Hsla>, width: f32, interval: f32) -> Background {
766 let width_scaled = (width * 255.0) as u32;
767 let interval_scaled = (interval * 255.0) as u32;
768 let height = ((width_scaled * 0xFFFF) + interval_scaled) as f32;
769
770 Background {
771 tag: BackgroundTag::PatternSlash,
772 solid: color.into(),
773 gradient_angle_or_pattern_height: height,
774 ..Default::default()
775 }
776}
777
778pub fn checkerboard(color: impl Into<Hsla>, size: f32) -> Background {
780 Background {
781 tag: BackgroundTag::Checkerboard,
782 solid: color.into(),
783 gradient_angle_or_pattern_height: size,
784 ..Default::default()
785 }
786}
787
788pub fn solid_background(color: impl Into<Hsla>) -> Background {
790 Background {
791 solid: color.into(),
792 ..Default::default()
793 }
794}
795
796pub fn linear_gradient(
804 angle: f32,
805 from: impl Into<LinearColorStop>,
806 to: impl Into<LinearColorStop>,
807) -> Background {
808 Background {
809 tag: BackgroundTag::LinearGradient,
810 gradient_angle_or_pattern_height: angle,
811 colors: [from.into(), to.into()],
812 ..Default::default()
813 }
814}
815
816#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
820#[repr(C)]
821pub struct LinearColorStop {
822 pub color: Hsla,
824 pub percentage: f32,
826}
827
828pub fn linear_color_stop(color: impl Into<Hsla>, percentage: f32) -> LinearColorStop {
832 LinearColorStop {
833 color: color.into(),
834 percentage,
835 }
836}
837
838impl LinearColorStop {
839 pub fn opacity(&self, factor: f32) -> Self {
841 Self {
842 percentage: self.percentage,
843 color: self.color.opacity(factor),
844 }
845 }
846}
847
848impl Background {
849 pub fn as_solid(&self) -> Option<Hsla> {
851 if self.tag == BackgroundTag::Solid {
852 Some(self.solid)
853 } else {
854 None
855 }
856 }
857
858 pub fn color_space(mut self, color_space: ColorSpace) -> Self {
862 self.color_space = color_space;
863 self
864 }
865
866 pub fn opacity(&self, factor: f32) -> Self {
868 let mut background = *self;
869 background.solid = background.solid.opacity(factor);
870 background.colors = [
871 self.colors[0].opacity(factor),
872 self.colors[1].opacity(factor),
873 ];
874 background
875 }
876
877 pub fn is_transparent(&self) -> bool {
879 match self.tag {
880 BackgroundTag::Solid => self.solid.is_transparent(),
881 BackgroundTag::LinearGradient => self.colors.iter().all(|c| c.color.is_transparent()),
882 BackgroundTag::PatternSlash => self.solid.is_transparent(),
883 BackgroundTag::Checkerboard => self.solid.is_transparent(),
884 }
885 }
886}
887
888impl From<Hsla> for Background {
889 fn from(value: Hsla) -> Self {
890 Background {
891 tag: BackgroundTag::Solid,
892 solid: value,
893 ..Default::default()
894 }
895 }
896}
897impl From<Rgba> for Background {
898 fn from(value: Rgba) -> Self {
899 Background {
900 tag: BackgroundTag::Solid,
901 solid: Hsla::from(value),
902 ..Default::default()
903 }
904 }
905}
906
907#[cfg(test)]
908mod tests {
909 use serde_json::json;
910
911 use super::*;
912
913 #[test]
914 fn test_deserialize_three_value_hex_to_rgba() {
915 let actual: Rgba = serde_json::from_value(json!("#f09")).unwrap();
916
917 assert_eq!(actual, rgba(0xff0099ff))
918 }
919
920 #[test]
921 fn test_deserialize_four_value_hex_to_rgba() {
922 let actual: Rgba = serde_json::from_value(json!("#f09f")).unwrap();
923
924 assert_eq!(actual, rgba(0xff0099ff))
925 }
926
927 #[test]
928 fn test_deserialize_six_value_hex_to_rgba() {
929 let actual: Rgba = serde_json::from_value(json!("#ff0099")).unwrap();
930
931 assert_eq!(actual, rgba(0xff0099ff))
932 }
933
934 #[test]
935 fn test_deserialize_eight_value_hex_to_rgba() {
936 let actual: Rgba = serde_json::from_value(json!("#ff0099ff")).unwrap();
937
938 assert_eq!(actual, rgba(0xff0099ff))
939 }
940
941 #[test]
942 fn test_deserialize_eight_value_hex_with_padding_to_rgba() {
943 let actual: Rgba = serde_json::from_value(json!(" #f5f5f5ff ")).unwrap();
944
945 assert_eq!(actual, rgba(0xf5f5f5ff))
946 }
947
948 #[test]
949 fn test_deserialize_eight_value_hex_with_mixed_case_to_rgba() {
950 let actual: Rgba = serde_json::from_value(json!("#DeAdbEeF")).unwrap();
951
952 assert_eq!(actual, rgba(0xdeadbeef))
953 }
954
955 #[test]
956 fn test_background_solid() {
957 let color = Hsla::from(rgba(0xff0099ff));
958 let mut background = Background::from(color);
959 assert_eq!(background.tag, BackgroundTag::Solid);
960 assert_eq!(background.solid, color);
961
962 assert_eq!(background.opacity(0.5).solid, color.opacity(0.5));
963 assert!(!background.is_transparent());
964 background.solid = hsla(0.0, 0.0, 0.0, 0.0);
965 assert!(background.is_transparent());
966 }
967
968 #[test]
969 fn test_background_linear_gradient() {
970 let from = linear_color_stop(rgba(0xff0099ff), 0.0);
971 let to = linear_color_stop(rgba(0x00ff99ff), 1.0);
972 let background = linear_gradient(90.0, from, to);
973 assert_eq!(background.tag, BackgroundTag::LinearGradient);
974 assert_eq!(background.colors[0], from);
975 assert_eq!(background.colors[1], to);
976
977 assert_eq!(background.opacity(0.5).colors[0], from.opacity(0.5));
978 assert_eq!(background.opacity(0.5).colors[1], to.opacity(0.5));
979 assert!(!background.is_transparent());
980 assert!(background.opacity(0.0).is_transparent());
981 }
982}