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