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: 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 /// 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 white 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 /// Returns true if the HSLA color is fully opaque, false otherwise.
477 pub fn is_opaque(&self) -> bool {
478 self.a == 1.0
479 }
480
481 /// 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.
482 ///
483 /// If `other`'s alpha value is 1.0 or greater, `other` color is fully opaque, thus `other` is returned as the output color.
484 /// If `other`'s alpha value is 0.0 or less, `other` color is fully transparent, thus `self` is returned as the output color.
485 /// Else, the output color is calculated as a blend of `self` and `other` based on their weighted alpha values.
486 ///
487 /// Assumptions:
488 /// - Alpha values are contained in the range [0, 1], with 1 as fully opaque and 0 as fully transparent.
489 /// - 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.
490 /// - RGB color components are contained in the range [0, 1].
491 /// - If `self` and `other` colors are out of the valid range, the blend operation's output and behavior is undefined.
492 pub fn blend(self, other: Hsla) -> Hsla {
493 let alpha = other.a;
494
495 if alpha >= 1.0 {
496 other
497 } else if alpha <= 0.0 {
498 self
499 } else {
500 let converted_self = Rgba::from(self);
501 let converted_other = Rgba::from(other);
502 let blended_rgb = converted_self.blend(converted_other);
503 Hsla::from(blended_rgb)
504 }
505 }
506
507 /// Returns a new HSLA color with the same hue, and lightness, but with no saturation.
508 pub fn grayscale(&self) -> Self {
509 Hsla {
510 h: self.h,
511 s: 0.,
512 l: self.l,
513 a: self.a,
514 }
515 }
516
517 /// Fade out the color by a given factor. This factor should be between 0.0 and 1.0.
518 /// Where 0.0 will leave the color unchanged, and 1.0 will completely fade out the color.
519 pub fn fade_out(&mut self, factor: f32) {
520 self.a *= 1.0 - factor.clamp(0., 1.);
521 }
522
523 /// Multiplies the alpha value of the color by a given factor
524 /// and returns a new HSLA color.
525 ///
526 /// Useful for transforming colors with dynamic opacity,
527 /// like a color from an external source.
528 ///
529 /// Example:
530 /// ```
531 /// let color = gpui::red();
532 /// let faded_color = color.opacity(0.5);
533 /// assert_eq!(faded_color.a, 0.5);
534 /// ```
535 ///
536 /// This will return a red color with half the opacity.
537 ///
538 /// Example:
539 /// ```
540 /// use gpui::hsla;
541 /// let color = hsla(0.7, 1.0, 0.5, 0.7); // A saturated blue
542 /// let faded_color = color.opacity(0.16);
543 /// assert!((faded_color.a - 0.112).abs() < 1e-6);
544 /// ```
545 ///
546 /// This will return a blue color with around ~10% opacity,
547 /// suitable for an element's hover or selected state.
548 ///
549 pub fn opacity(&self, factor: f32) -> Self {
550 Hsla {
551 h: self.h,
552 s: self.s,
553 l: self.l,
554 a: self.a * factor.clamp(0., 1.),
555 }
556 }
557
558 /// Returns a new HSLA color with the same hue, saturation,
559 /// and lightness, but with a new alpha value.
560 ///
561 /// Example:
562 /// ```
563 /// let color = gpui::red();
564 /// let red_color = color.alpha(0.25);
565 /// assert_eq!(red_color.a, 0.25);
566 /// ```
567 ///
568 /// This will return a red color with half the opacity.
569 ///
570 /// Example:
571 /// ```
572 /// use gpui::hsla;
573 /// let color = hsla(0.7, 1.0, 0.5, 0.7); // A saturated blue
574 /// let faded_color = color.alpha(0.25);
575 /// assert_eq!(faded_color.a, 0.25);
576 /// ```
577 ///
578 /// This will return a blue color with 25% opacity.
579 pub fn alpha(&self, a: f32) -> Self {
580 Hsla {
581 h: self.h,
582 s: self.s,
583 l: self.l,
584 a: a.clamp(0., 1.),
585 }
586 }
587}
588
589impl From<Rgba> for Hsla {
590 fn from(color: Rgba) -> Self {
591 let r = color.r;
592 let g = color.g;
593 let b = color.b;
594
595 let max = r.max(g.max(b));
596 let min = r.min(g.min(b));
597 let delta = max - min;
598
599 let l = (max + min) / 2.0;
600 let s = if l == 0.0 || l == 1.0 {
601 0.0
602 } else if l < 0.5 {
603 delta / (2.0 * l)
604 } else {
605 delta / (2.0 - 2.0 * l)
606 };
607
608 let h = if delta == 0.0 {
609 0.0
610 } else if max == r {
611 ((g - b) / delta).rem_euclid(6.0) / 6.0
612 } else if max == g {
613 ((b - r) / delta + 2.0) / 6.0
614 } else {
615 ((r - g) / delta + 4.0) / 6.0
616 };
617
618 Hsla {
619 h,
620 s,
621 l,
622 a: color.a,
623 }
624 }
625}
626
627impl JsonSchema for Hsla {
628 fn schema_name() -> Cow<'static, str> {
629 Rgba::schema_name()
630 }
631
632 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
633 Rgba::json_schema(generator)
634 }
635}
636
637impl Serialize for Hsla {
638 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
639 where
640 S: Serializer,
641 {
642 Rgba::from(*self).serialize(serializer)
643 }
644}
645
646impl<'de> Deserialize<'de> for Hsla {
647 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
648 where
649 D: Deserializer<'de>,
650 {
651 Ok(Rgba::deserialize(deserializer)?.into())
652 }
653}
654
655#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
656#[repr(C)]
657pub(crate) enum BackgroundTag {
658 Solid = 0,
659 LinearGradient = 1,
660 PatternSlash = 2,
661}
662
663/// A color space for color interpolation.
664///
665/// References:
666/// - <https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method>
667/// - <https://www.w3.org/TR/css-color-4/#typedef-color-space>
668#[derive(Debug, Clone, Copy, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
669#[repr(C)]
670pub enum ColorSpace {
671 #[default]
672 /// The sRGB color space.
673 Srgb = 0,
674 /// The Oklab color space.
675 Oklab = 1,
676}
677
678impl Display for ColorSpace {
679 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
680 match self {
681 ColorSpace::Srgb => write!(f, "sRGB"),
682 ColorSpace::Oklab => write!(f, "Oklab"),
683 }
684 }
685}
686
687/// A background color, which can be either a solid color or a linear gradient.
688#[derive(Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
689#[repr(C)]
690pub struct Background {
691 pub(crate) tag: BackgroundTag,
692 pub(crate) color_space: ColorSpace,
693 pub(crate) solid: Hsla,
694 pub(crate) gradient_angle_or_pattern_height: f32,
695 pub(crate) colors: [LinearColorStop; 2],
696 /// Padding for alignment for repr(C) layout.
697 pad: u32,
698}
699
700impl std::fmt::Debug for Background {
701 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
702 match self.tag {
703 BackgroundTag::Solid => write!(f, "Solid({:?})", self.solid),
704 BackgroundTag::LinearGradient => {
705 write!(
706 f,
707 "LinearGradient({}, {:?}, {:?})",
708 self.gradient_angle_or_pattern_height, self.colors[0], self.colors[1]
709 )
710 }
711 BackgroundTag::PatternSlash => {
712 write!(
713 f,
714 "PatternSlash({:?}, {})",
715 self.solid, self.gradient_angle_or_pattern_height
716 )
717 }
718 }
719 }
720}
721
722impl Eq for Background {}
723impl Default for Background {
724 fn default() -> Self {
725 Self {
726 tag: BackgroundTag::Solid,
727 solid: Hsla::default(),
728 color_space: ColorSpace::default(),
729 gradient_angle_or_pattern_height: 0.0,
730 colors: [LinearColorStop::default(), LinearColorStop::default()],
731 pad: 0,
732 }
733 }
734}
735
736/// Creates a hash pattern background
737pub fn pattern_slash(color: Hsla, width: f32, interval: f32) -> Background {
738 let width_scaled = (width * 255.0) as u32;
739 let interval_scaled = (interval * 255.0) as u32;
740 let height = ((width_scaled * 0xFFFF) + interval_scaled) as f32;
741
742 Background {
743 tag: BackgroundTag::PatternSlash,
744 solid: color,
745 gradient_angle_or_pattern_height: height,
746 ..Default::default()
747 }
748}
749
750/// Creates a solid background color.
751pub fn solid_background(color: impl Into<Hsla>) -> Background {
752 Background {
753 solid: color.into(),
754 ..Default::default()
755 }
756}
757
758/// Creates a LinearGradient background color.
759///
760/// The gradient line's angle of direction. A value of `0.` is equivalent to top; increasing values rotate clockwise from there.
761///
762/// The `angle` is in degrees value in the range 0.0 to 360.0.
763///
764/// <https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient>
765pub fn linear_gradient(
766 angle: f32,
767 from: impl Into<LinearColorStop>,
768 to: impl Into<LinearColorStop>,
769) -> Background {
770 Background {
771 tag: BackgroundTag::LinearGradient,
772 gradient_angle_or_pattern_height: angle,
773 colors: [from.into(), to.into()],
774 ..Default::default()
775 }
776}
777
778/// A color stop in a linear gradient.
779///
780/// <https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient#linear-color-stop>
781#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
782#[repr(C)]
783pub struct LinearColorStop {
784 /// The color of the color stop.
785 pub color: Hsla,
786 /// The percentage of the gradient, in the range 0.0 to 1.0.
787 pub percentage: f32,
788}
789
790/// Creates a new linear color stop.
791///
792/// The percentage of the gradient, in the range 0.0 to 1.0.
793pub fn linear_color_stop(color: impl Into<Hsla>, percentage: f32) -> LinearColorStop {
794 LinearColorStop {
795 color: color.into(),
796 percentage,
797 }
798}
799
800impl LinearColorStop {
801 /// Returns a new color stop with the same color, but with a modified alpha value.
802 pub fn opacity(&self, factor: f32) -> Self {
803 Self {
804 percentage: self.percentage,
805 color: self.color.opacity(factor),
806 }
807 }
808}
809
810impl Background {
811 /// Use specified color space for color interpolation.
812 ///
813 /// <https://developer.mozilla.org/en-US/docs/Web/CSS/color-interpolation-method>
814 pub fn color_space(mut self, color_space: ColorSpace) -> Self {
815 self.color_space = color_space;
816 self
817 }
818
819 /// Returns a new background color with the same hue, saturation, and lightness, but with a modified alpha value.
820 pub fn opacity(&self, factor: f32) -> Self {
821 let mut background = *self;
822 background.solid = background.solid.opacity(factor);
823 background.colors = [
824 self.colors[0].opacity(factor),
825 self.colors[1].opacity(factor),
826 ];
827 background
828 }
829
830 /// Returns whether the background color is transparent.
831 pub fn is_transparent(&self) -> bool {
832 match self.tag {
833 BackgroundTag::Solid => self.solid.is_transparent(),
834 BackgroundTag::LinearGradient => self.colors.iter().all(|c| c.color.is_transparent()),
835 BackgroundTag::PatternSlash => self.solid.is_transparent(),
836 }
837 }
838}
839
840impl From<Hsla> for Background {
841 fn from(value: Hsla) -> Self {
842 Background {
843 tag: BackgroundTag::Solid,
844 solid: value,
845 ..Default::default()
846 }
847 }
848}
849impl From<Rgba> for Background {
850 fn from(value: Rgba) -> Self {
851 Background {
852 tag: BackgroundTag::Solid,
853 solid: Hsla::from(value),
854 ..Default::default()
855 }
856 }
857}
858
859#[cfg(test)]
860mod tests {
861 use serde_json::json;
862
863 use super::*;
864
865 #[test]
866 fn test_deserialize_three_value_hex_to_rgba() {
867 let actual: Rgba = serde_json::from_value(json!("#f09")).unwrap();
868
869 assert_eq!(actual, rgba(0xff0099ff))
870 }
871
872 #[test]
873 fn test_deserialize_four_value_hex_to_rgba() {
874 let actual: Rgba = serde_json::from_value(json!("#f09f")).unwrap();
875
876 assert_eq!(actual, rgba(0xff0099ff))
877 }
878
879 #[test]
880 fn test_deserialize_six_value_hex_to_rgba() {
881 let actual: Rgba = serde_json::from_value(json!("#ff0099")).unwrap();
882
883 assert_eq!(actual, rgba(0xff0099ff))
884 }
885
886 #[test]
887 fn test_deserialize_eight_value_hex_to_rgba() {
888 let actual: Rgba = serde_json::from_value(json!("#ff0099ff")).unwrap();
889
890 assert_eq!(actual, rgba(0xff0099ff))
891 }
892
893 #[test]
894 fn test_deserialize_eight_value_hex_with_padding_to_rgba() {
895 let actual: Rgba = serde_json::from_value(json!(" #f5f5f5ff ")).unwrap();
896
897 assert_eq!(actual, rgba(0xf5f5f5ff))
898 }
899
900 #[test]
901 fn test_deserialize_eight_value_hex_with_mixed_case_to_rgba() {
902 let actual: Rgba = serde_json::from_value(json!("#DeAdbEeF")).unwrap();
903
904 assert_eq!(actual, rgba(0xdeadbeef))
905 }
906
907 #[test]
908 fn test_background_solid() {
909 let color = Hsla::from(rgba(0xff0099ff));
910 let mut background = Background::from(color);
911 assert_eq!(background.tag, BackgroundTag::Solid);
912 assert_eq!(background.solid, color);
913
914 assert_eq!(background.opacity(0.5).solid, color.opacity(0.5));
915 assert!(!background.is_transparent());
916 background.solid = hsla(0.0, 0.0, 0.0, 0.0);
917 assert!(background.is_transparent());
918 }
919
920 #[test]
921 fn test_background_linear_gradient() {
922 let from = linear_color_stop(rgba(0xff0099ff), 0.0);
923 let to = linear_color_stop(rgba(0x00ff99ff), 1.0);
924 let background = linear_gradient(90.0, from, to);
925 assert_eq!(background.tag, BackgroundTag::LinearGradient);
926 assert_eq!(background.colors[0], from);
927 assert_eq!(background.colors[1], to);
928
929 assert_eq!(background.opacity(0.5).colors[0], from.opacity(0.5));
930 assert_eq!(background.opacity(0.5).colors[1], to.opacity(0.5));
931 assert!(!background.is_transparent());
932 assert!(background.opacity(0.0).is_transparent());
933 }
934}