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(crate) 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
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,
155 g,
156 b,
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 /// Duplicates a given hex digit.
210 /// E.g., `0xf` -> `0xff`.
211 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/// An HSLA color
270#[derive(Default, Copy, Clone, Debug)]
271#[repr(C)]
272pub struct Hsla {
273 /// Hue, in a range from 0 to 1
274 pub h: f32,
275
276 /// Saturation, in a range from 0 to 1
277 pub s: f32,
278
279 /// Lightness, in a range from 0 to 1
280 pub l: f32,
281
282 /// Alpha, in a range from 0 to 1
283 pub a: f32,
284}
285
286impl PartialEq for Hsla {
287 fn eq(&self, other: &Self) -> bool {
288 self.h
289 .total_cmp(&other.h)
290 .then(self.s.total_cmp(&other.s))
291 .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a)))
292 .is_eq()
293 }
294}
295
296impl PartialOrd for Hsla {
297 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
298 Some(self.cmp(other))
299 }
300}
301
302impl Ord for Hsla {
303 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
304 self.h
305 .total_cmp(&other.h)
306 .then(self.s.total_cmp(&other.s))
307 .then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a)))
308 }
309}
310
311impl Eq for Hsla {}
312
313impl Hash for Hsla {
314 fn hash<H: Hasher>(&self, state: &mut H) {
315 state.write_u32(u32::from_be_bytes(self.h.to_be_bytes()));
316 state.write_u32(u32::from_be_bytes(self.s.to_be_bytes()));
317 state.write_u32(u32::from_be_bytes(self.l.to_be_bytes()));
318 state.write_u32(u32::from_be_bytes(self.a.to_be_bytes()));
319 }
320}
321
322impl Display for Hsla {
323 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
324 write!(
325 f,
326 "hsla({:.2}, {:.2}%, {:.2}%, {:.2})",
327 self.h * 360.,
328 self.s * 100.,
329 self.l * 100.,
330 self.a
331 )
332 }
333}
334
335/// Construct an [`Hsla`] object from plain values
336pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
337 Hsla {
338 h: h.clamp(0., 1.),
339 s: s.clamp(0., 1.),
340 l: l.clamp(0., 1.),
341 a: a.clamp(0., 1.),
342 }
343}
344
345/// Pure black in [`Hsla`]
346pub const fn black() -> Hsla {
347 Hsla {
348 h: 0.,
349 s: 0.,
350 l: 0.,
351 a: 1.,
352 }
353}
354
355/// Transparent black in [`Hsla`]
356pub const fn transparent_black() -> Hsla {
357 Hsla {
358 h: 0.,
359 s: 0.,
360 l: 0.,
361 a: 0.,
362 }
363}
364
365/// Transparent black in [`Hsla`]
366pub const fn transparent_white() -> Hsla {
367 Hsla {
368 h: 0.,
369 s: 0.,
370 l: 1.,
371 a: 0.,
372 }
373}
374
375/// Opaque grey in [`Hsla`], values will be clamped to the range [0, 1]
376pub fn opaque_grey(lightness: f32, opacity: f32) -> Hsla {
377 Hsla {
378 h: 0.,
379 s: 0.,
380 l: lightness.clamp(0., 1.),
381 a: opacity.clamp(0., 1.),
382 }
383}
384
385/// Pure white in [`Hsla`]
386pub const fn white() -> Hsla {
387 Hsla {
388 h: 0.,
389 s: 0.,
390 l: 1.,
391 a: 1.,
392 }
393}
394
395/// The color red in [`Hsla`]
396pub const fn red() -> Hsla {
397 Hsla {
398 h: 0.,
399 s: 1.,
400 l: 0.5,
401 a: 1.,
402 }
403}
404
405/// The color blue in [`Hsla`]
406pub const fn blue() -> Hsla {
407 Hsla {
408 h: 0.6666666667,
409 s: 1.,
410 l: 0.5,
411 a: 1.,
412 }
413}
414
415/// The color green in [`Hsla`]
416pub const fn green() -> Hsla {
417 Hsla {
418 h: 0.3333333333,
419 s: 1.,
420 l: 0.25,
421 a: 1.,
422 }
423}
424
425/// The color yellow in [`Hsla`]
426pub const fn yellow() -> Hsla {
427 Hsla {
428 h: 0.1666666667,
429 s: 1.,
430 l: 0.5,
431 a: 1.,
432 }
433}
434
435impl Hsla {
436 /// Converts this HSLA color to an RGBA color.
437 pub fn to_rgb(self) -> Rgba {
438 self.into()
439 }
440
441 /// The color red
442 pub const fn red() -> Self {
443 red()
444 }
445
446 /// The color green
447 pub const fn green() -> Self {
448 green()
449 }
450
451 /// The color blue
452 pub const fn blue() -> Self {
453 blue()
454 }
455
456 /// The color black
457 pub const fn black() -> Self {
458 black()
459 }
460
461 /// The color white
462 pub const fn white() -> Self {
463 white()
464 }
465
466 /// The color transparent black
467 pub const fn transparent_black() -> Self {
468 transparent_black()
469 }
470
471 /// Returns true if the HSLA color is fully transparent, false otherwise.
472 pub fn is_transparent(&self) -> bool {
473 self.a == 0.0
474 }
475
476 /// 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.
477 ///
478 /// If `other`'s alpha value is 1.0 or greater, `other` color is fully opaque, thus `other` is returned as the output color.
479 /// If `other`'s alpha value is 0.0 or less, `other` color is fully transparent, thus `self` is returned as the output color.
480 /// Else, the output color is calculated as a blend of `self` and `other` based on their weighted alpha values.
481 ///
482 /// Assumptions:
483 /// - Alpha values are contained in the range [0, 1], with 1 as fully opaque and 0 as fully transparent.
484 /// - 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.
485 /// - RGB color components are contained in the range [0, 1].
486 /// - If `self` and `other` colors are out of the valid range, the blend operation's output and behavior is undefined.
487 pub fn blend(self, other: Hsla) -> Hsla {
488 let alpha = other.a;
489
490 if alpha >= 1.0 {
491 other
492 } else if alpha <= 0.0 {
493 self
494 } else {
495 let converted_self = Rgba::from(self);
496 let converted_other = Rgba::from(other);
497 let blended_rgb = converted_self.blend(converted_other);
498 Hsla::from(blended_rgb)
499 }
500 }
501
502 /// Returns a new HSLA color with the same hue, and lightness, but with no saturation.
503 pub fn grayscale(&self) -> Self {
504 Hsla {
505 h: self.h,
506 s: 0.,
507 l: self.l,
508 a: self.a,
509 }
510 }
511
512 /// Fade out the color by a given factor. This factor should be between 0.0 and 1.0.
513 /// Where 0.0 will leave the color unchanged, and 1.0 will completely fade out the color.
514 pub fn fade_out(&mut self, factor: f32) {
515 self.a *= 1.0 - factor.clamp(0., 1.);
516 }
517
518 /// Multiplies the alpha value of the color by a given factor
519 /// and returns a new HSLA color.
520 ///
521 /// Useful for transforming colors with dynamic opacity,
522 /// like a color from an external source.
523 ///
524 /// Example:
525 /// ```
526 /// let color = gpui::red();
527 /// let faded_color = color.opacity(0.5);
528 /// assert_eq!(faded_color.a, 0.5);
529 /// ```
530 ///
531 /// This will return a red color with half the opacity.
532 ///
533 /// Example:
534 /// ```
535 /// let color = hlsa(0.7, 1.0, 0.5, 0.7); // A saturated blue
536 /// let faded_color = color.opacity(0.16);
537 /// assert_eq!(faded_color.a, 0.112);
538 /// ```
539 ///
540 /// This will return a blue color with around ~10% opacity,
541 /// suitable for an element's hover or selected state.
542 ///
543 pub fn opacity(&self, factor: f32) -> Self {
544 Hsla {
545 h: self.h,
546 s: self.s,
547 l: self.l,
548 a: self.a * factor.clamp(0., 1.),
549 }
550 }
551
552 /// Returns a new HSLA color with the same hue, saturation,
553 /// and lightness, but with a new alpha value.
554 ///
555 /// Example:
556 /// ```
557 /// let color = gpui::red();
558 /// let red_color = color.alpha(0.25);
559 /// assert_eq!(red_color.a, 0.25);
560 /// ```
561 ///
562 /// This will return a red color with half the opacity.
563 ///
564 /// Example:
565 /// ```
566 /// let color = hsla(0.7, 1.0, 0.5, 0.7); // A saturated blue
567 /// let faded_color = color.alpha(0.25);
568 /// assert_eq!(faded_color.a, 0.25);
569 /// ```
570 ///
571 /// This will return a blue color with 25% opacity.
572 pub fn alpha(&self, a: f32) -> Self {
573 Hsla {
574 h: self.h,
575 s: self.s,
576 l: self.l,
577 a: a.clamp(0., 1.),
578 }
579 }
580}
581
582impl From<Rgba> for Hsla {
583 fn from(color: Rgba) -> Self {
584 let r = color.r;
585 let g = color.g;
586 let b = color.b;
587
588 let max = r.max(g.max(b));
589 let min = r.min(g.min(b));
590 let delta = max - min;
591
592 let l = (max + min) / 2.0;
593 let s = if l == 0.0 || l == 1.0 {
594 0.0
595 } else if l < 0.5 {
596 delta / (2.0 * l)
597 } else {
598 delta / (2.0 - 2.0 * l)
599 };
600
601 let h = if delta == 0.0 {
602 0.0
603 } else if max == r {
604 ((g - b) / delta).rem_euclid(6.0) / 6.0
605 } else if max == g {
606 ((b - r) / delta + 2.0) / 6.0
607 } else {
608 ((r - g) / delta + 4.0) / 6.0
609 };
610
611 Hsla {
612 h,
613 s,
614 l,
615 a: color.a,
616 }
617 }
618}
619
620impl JsonSchema for Hsla {
621 fn schema_name() -> Cow<'static, str> {
622 Rgba::schema_name()
623 }
624
625 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
626 Rgba::json_schema(generator)
627 }
628}
629
630impl Serialize for Hsla {
631 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
632 where
633 S: Serializer,
634 {
635 Rgba::from(*self).serialize(serializer)
636 }
637}
638
639impl<'de> Deserialize<'de> for Hsla {
640 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
641 where
642 D: Deserializer<'de>,
643 {
644 Ok(Rgba::deserialize(deserializer)?.into())
645 }
646}
647
648#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
649#[repr(C)]
650pub(crate) enum BackgroundTag {
651 Solid = 0,
652 LinearGradient = 1,
653 PatternSlash = 2,
654}
655
656/// A color space for color interpolation.
657///
658/// References:
659/// - <https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method>
660/// - <https://www.w3.org/TR/css-color-4/#typedef-color-space>
661#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
662#[repr(C)]
663pub enum ColorSpace {
664 #[default]
665 /// The sRGB color space.
666 Srgb = 0,
667 /// The Oklab color space.
668 Oklab = 1,
669}
670
671impl Display for ColorSpace {
672 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
673 match self {
674 ColorSpace::Srgb => write!(f, "sRGB"),
675 ColorSpace::Oklab => write!(f, "Oklab"),
676 }
677 }
678}
679
680/// A background color, which can be either a solid color or a linear gradient.
681#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
682#[repr(C)]
683pub struct Background {
684 pub(crate) tag: BackgroundTag,
685 pub(crate) color_space: ColorSpace,
686 pub(crate) solid: Hsla,
687 pub(crate) gradient_angle_or_pattern_height: f32,
688 pub(crate) colors: [LinearColorStop; 2],
689 /// Padding for alignment for repr(C) layout.
690 pad: u32,
691}
692
693impl std::fmt::Debug for Background {
694 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
695 match self.tag {
696 BackgroundTag::Solid => write!(f, "Solid({:?})", self.solid),
697 BackgroundTag::LinearGradient => {
698 write!(
699 f,
700 "LinearGradient({}, {:?}, {:?})",
701 self.gradient_angle_or_pattern_height, self.colors[0], self.colors[1]
702 )
703 }
704 BackgroundTag::PatternSlash => {
705 write!(
706 f,
707 "PatternSlash({:?}, {})",
708 self.solid, self.gradient_angle_or_pattern_height
709 )
710 }
711 }
712 }
713}
714
715impl Eq for Background {}
716impl Default for Background {
717 fn default() -> Self {
718 Self {
719 tag: BackgroundTag::Solid,
720 solid: Hsla::default(),
721 color_space: ColorSpace::default(),
722 gradient_angle_or_pattern_height: 0.0,
723 colors: [LinearColorStop::default(), LinearColorStop::default()],
724 pad: 0,
725 }
726 }
727}
728
729/// Creates a hash pattern background
730pub fn pattern_slash(color: Hsla, width: f32, interval: f32) -> Background {
731 let width_scaled = (width * 255.0) as u32;
732 let interval_scaled = (interval * 255.0) as u32;
733 let height = ((width_scaled * 0xFFFF) + interval_scaled) as f32;
734
735 Background {
736 tag: BackgroundTag::PatternSlash,
737 solid: color,
738 gradient_angle_or_pattern_height: height,
739 ..Default::default()
740 }
741}
742
743/// Creates a solid background color.
744pub fn solid_background(color: impl Into<Hsla>) -> Background {
745 Background {
746 solid: color.into(),
747 ..Default::default()
748 }
749}
750
751/// Creates a LinearGradient background color.
752///
753/// The gradient line's angle of direction. A value of `0.` is equivalent to top; increasing values rotate clockwise from there.
754///
755/// The `angle` is in degrees value in the range 0.0 to 360.0.
756///
757/// <https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient>
758pub fn linear_gradient(
759 angle: f32,
760 from: impl Into<LinearColorStop>,
761 to: impl Into<LinearColorStop>,
762) -> Background {
763 Background {
764 tag: BackgroundTag::LinearGradient,
765 gradient_angle_or_pattern_height: angle,
766 colors: [from.into(), to.into()],
767 ..Default::default()
768 }
769}
770
771/// A color stop in a linear gradient.
772///
773/// <https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient#linear-color-stop>
774#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
775#[repr(C)]
776pub struct LinearColorStop {
777 /// The color of the color stop.
778 pub color: Hsla,
779 /// The percentage of the gradient, in the range 0.0 to 1.0.
780 pub percentage: f32,
781}
782
783/// Creates a new linear color stop.
784///
785/// The percentage of the gradient, in the range 0.0 to 1.0.
786pub fn linear_color_stop(color: impl Into<Hsla>, percentage: f32) -> LinearColorStop {
787 LinearColorStop {
788 color: color.into(),
789 percentage,
790 }
791}
792
793impl LinearColorStop {
794 /// Returns a new color stop with the same color, but with a modified alpha value.
795 pub fn opacity(&self, factor: f32) -> Self {
796 Self {
797 percentage: self.percentage,
798 color: self.color.opacity(factor),
799 }
800 }
801}
802
803impl Background {
804 /// Use specified color space for color interpolation.
805 ///
806 /// <https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method>
807 pub fn color_space(mut self, color_space: ColorSpace) -> Self {
808 self.color_space = color_space;
809 self
810 }
811
812 /// Returns a new background color with the same hue, saturation, and lightness, but with a modified alpha value.
813 pub fn opacity(&self, factor: f32) -> Self {
814 let mut background = *self;
815 background.solid = background.solid.opacity(factor);
816 background.colors = [
817 self.colors[0].opacity(factor),
818 self.colors[1].opacity(factor),
819 ];
820 background
821 }
822
823 /// Returns whether the background color is transparent.
824 pub fn is_transparent(&self) -> bool {
825 match self.tag {
826 BackgroundTag::Solid => self.solid.is_transparent(),
827 BackgroundTag::LinearGradient => self.colors.iter().all(|c| c.color.is_transparent()),
828 BackgroundTag::PatternSlash => self.solid.is_transparent(),
829 }
830 }
831}
832
833impl From<Hsla> for Background {
834 fn from(value: Hsla) -> Self {
835 Background {
836 tag: BackgroundTag::Solid,
837 solid: value,
838 ..Default::default()
839 }
840 }
841}
842impl From<Rgba> for Background {
843 fn from(value: Rgba) -> Self {
844 Background {
845 tag: BackgroundTag::Solid,
846 solid: Hsla::from(value),
847 ..Default::default()
848 }
849 }
850}
851
852#[cfg(test)]
853mod tests {
854 use serde_json::json;
855
856 use super::*;
857
858 #[test]
859 fn test_deserialize_three_value_hex_to_rgba() {
860 let actual: Rgba = serde_json::from_value(json!("#f09")).unwrap();
861
862 assert_eq!(actual, rgba(0xff0099ff))
863 }
864
865 #[test]
866 fn test_deserialize_four_value_hex_to_rgba() {
867 let actual: Rgba = serde_json::from_value(json!("#f09f")).unwrap();
868
869 assert_eq!(actual, rgba(0xff0099ff))
870 }
871
872 #[test]
873 fn test_deserialize_six_value_hex_to_rgba() {
874 let actual: Rgba = serde_json::from_value(json!("#ff0099")).unwrap();
875
876 assert_eq!(actual, rgba(0xff0099ff))
877 }
878
879 #[test]
880 fn test_deserialize_eight_value_hex_to_rgba() {
881 let actual: Rgba = serde_json::from_value(json!("#ff0099ff")).unwrap();
882
883 assert_eq!(actual, rgba(0xff0099ff))
884 }
885
886 #[test]
887 fn test_deserialize_eight_value_hex_with_padding_to_rgba() {
888 let actual: Rgba = serde_json::from_value(json!(" #f5f5f5ff ")).unwrap();
889
890 assert_eq!(actual, rgba(0xf5f5f5ff))
891 }
892
893 #[test]
894 fn test_deserialize_eight_value_hex_with_mixed_case_to_rgba() {
895 let actual: Rgba = serde_json::from_value(json!("#DeAdbEeF")).unwrap();
896
897 assert_eq!(actual, rgba(0xdeadbeef))
898 }
899
900 #[test]
901 fn test_background_solid() {
902 let color = Hsla::from(rgba(0xff0099ff));
903 let mut background = Background::from(color);
904 assert_eq!(background.tag, BackgroundTag::Solid);
905 assert_eq!(background.solid, color);
906
907 assert_eq!(background.opacity(0.5).solid, color.opacity(0.5));
908 assert!(!background.is_transparent());
909 background.solid = hsla(0.0, 0.0, 0.0, 0.0);
910 assert!(background.is_transparent());
911 }
912
913 #[test]
914 fn test_background_linear_gradient() {
915 let from = linear_color_stop(rgba(0xff0099ff), 0.0);
916 let to = linear_color_stop(rgba(0x00ff99ff), 1.0);
917 let background = linear_gradient(90.0, from, to);
918 assert_eq!(background.tag, BackgroundTag::LinearGradient);
919 assert_eq!(background.colors[0], from);
920 assert_eq!(background.colors[1], to);
921
922 assert_eq!(background.opacity(0.5).colors[0], from.opacity(0.5));
923 assert_eq!(background.opacity(0.5).colors[1], to.opacity(0.5));
924 assert!(!background.is_transparent());
925 assert!(background.opacity(0.0).is_transparent());
926 }
927}