1use std::{collections::HashMap, fmt::Display};
2
3use gpui::{
4 Background, Hsla, LinearColorStop, SharedString, hsla, linear_color_stop, linear_gradient,
5};
6use serde::{Deserialize, Deserializer, de::Error as _};
7
8use anyhow::{Error, Result, anyhow};
9
10#[inline]
16pub fn hsl(h: f32, s: f32, l: f32) -> Hsla {
17 hsla(h / 360., s / 100.0, l / 100.0, 1.0)
18}
19
20pub trait Colorize: Sized {
21 fn opacity(&self, opacity: f32) -> Self;
25 fn divide(&self, divisor: f32) -> Self;
29 fn invert(&self) -> Self;
31 fn invert_l(&self) -> Self;
33 fn lighten(&self, amount: f32) -> Self;
37 fn darken(&self, amount: f32) -> Self;
41 fn apply(&self, base_color: Self) -> Self;
43
44 fn mix(&self, other: Self, factor: f32) -> Self;
46 fn mix_oklab(&self, other: Self, factor: f32) -> Self;
50 fn hue(&self, hue: f32) -> Self;
52 fn saturation(&self, saturation: f32) -> Self;
54 fn lightness(&self, lightness: f32) -> Self;
56
57 fn to_hex(&self) -> String;
59 fn parse_hex(hex: &str) -> Result<Self>;
61}
62
63mod oklab {
65 use gpui::Rgba;
66
67 #[inline]
69 fn to_linear(c: f32) -> f32 {
70 if c <= 0.04045 {
71 c / 12.92
72 } else {
73 ((c + 0.055) / 1.055).powf(2.4)
74 }
75 }
76
77 #[inline]
79 fn from_linear(c: f32) -> f32 {
80 if c <= 0.0031308 {
81 c * 12.92
82 } else {
83 1.055 * c.powf(1.0 / 2.4) - 0.055
84 }
85 }
86
87 #[allow(non_snake_case)]
89 pub fn rgb_to_oklab(rgb: Rgba) -> (f32, f32, f32) {
90 let lr = to_linear(rgb.r);
92 let lg = to_linear(rgb.g);
93 let lb = to_linear(rgb.b);
94
95 let l = 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb;
97 let m = 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb;
98 let s = 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb;
99
100 let l_ = l.cbrt();
102 let m_ = m.cbrt();
103 let s_ = s.cbrt();
104
105 let L = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_;
106 let a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_;
107 let b = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_;
108
109 (L, a, b)
110 }
111
112 #[allow(non_snake_case)]
114 pub fn oklab_to_rgb(L: f32, a: f32, b: f32) -> Rgba {
115 let l_ = L + 0.3963377774 * a + 0.2158037573 * b;
117 let m_ = L - 0.1055613458 * a - 0.0638541728 * b;
118 let s_ = L - 0.0894841775 * a - 1.2914855480 * b;
119
120 let l = l_ * l_ * l_;
121 let m = m_ * m_ * m_;
122 let s = s_ * s_ * s_;
123
124 let lr = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;
126 let lg = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;
127 let lb = -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s;
128
129 Rgba {
131 r: from_linear(lr).clamp(0.0, 1.0),
132 g: from_linear(lg).clamp(0.0, 1.0),
133 b: from_linear(lb).clamp(0.0, 1.0),
134 a: 1.0,
135 }
136 }
137}
138
139impl Colorize for Hsla {
140 fn opacity(&self, factor: f32) -> Self {
141 Self {
142 a: self.a * factor.clamp(0.0, 1.0),
143 ..*self
144 }
145 }
146
147 fn divide(&self, divisor: f32) -> Self {
148 Self {
149 a: divisor,
150 ..*self
151 }
152 }
153
154 fn invert(&self) -> Self {
155 Self {
156 h: 1.0 - self.h,
157 s: 1.0 - self.s,
158 l: 1.0 - self.l,
159 a: self.a,
160 }
161 }
162
163 fn invert_l(&self) -> Self {
164 Self {
165 l: 1.0 - self.l,
166 ..*self
167 }
168 }
169
170 fn lighten(&self, factor: f32) -> Self {
171 let l = self.l * (1.0 + factor.clamp(0.0, 1.0));
172
173 Hsla { l, ..*self }
174 }
175
176 fn darken(&self, factor: f32) -> Self {
177 let l = self.l * (1.0 - factor.clamp(0.0, 1.0));
178
179 Self { l, ..*self }
180 }
181
182 fn apply(&self, new_color: Self) -> Self {
183 Hsla {
184 h: new_color.h,
185 s: new_color.s,
186 l: self.l,
187 a: self.a,
188 }
189 }
190
191 fn mix(&self, other: Self, factor: f32) -> Self {
194 let factor = factor.clamp(0.0, 1.0);
195 let inv = 1.0 - factor;
196
197 #[inline]
198 fn lerp_hue(a: f32, b: f32, t: f32) -> f32 {
199 let diff = (b - a + 180.0).rem_euclid(360.) - 180.;
200 (a + diff * t).rem_euclid(360.0)
201 }
202
203 Hsla {
204 h: lerp_hue(self.h * 360., other.h * 360., factor) / 360.,
205 s: self.s * factor + other.s * inv,
206 l: self.l * factor + other.l * inv,
207 a: self.a * factor + other.a * inv,
208 }
209 }
210
211 #[allow(non_snake_case)]
212 fn mix_oklab(&self, other: Self, factor: f32) -> Self {
213 let factor = factor.clamp(0.0, 1.0);
214 let inv = 1.0 - factor;
215
216 let result_alpha = self.a * factor + other.a * inv;
218
219 if result_alpha == 0.0 {
221 return Self {
222 h: 0.0,
223 s: 0.0,
224 l: 0.0,
225 a: 0.0,
226 };
227 }
228
229 let rgb1 = self.to_rgb();
231 let rgb2 = other.to_rgb();
232
233 let (l1, a1, b1) = oklab::rgb_to_oklab(rgb1);
235 let (l2, a2, b2) = oklab::rgb_to_oklab(rgb2);
236
237 let alpha1 = self.a;
240 let alpha2 = other.a;
241
242 let l1_pm = l1 * alpha1;
244 let a1_pm = a1 * alpha1;
245 let b1_pm = b1 * alpha1;
246
247 let l2_pm = l2 * alpha2;
248 let a2_pm = a2 * alpha2;
249 let b2_pm = b2 * alpha2;
250
251 let L_pm = l1_pm * factor + l2_pm * inv;
253 let a_pm = a1_pm * factor + a2_pm * inv;
254 let b_pm = b1_pm * factor + b2_pm * inv;
255
256 let L = L_pm / result_alpha;
258 let a = a_pm / result_alpha;
259 let b = b_pm / result_alpha;
260
261 let mut rgb = oklab::oklab_to_rgb(L, a, b);
263 rgb.a = result_alpha;
264
265 rgb.into()
267 }
268
269 fn to_hex(&self) -> String {
270 let rgb = self.to_rgb();
271
272 if rgb.a < 1. {
273 return format!(
274 "#{:02X}{:02X}{:02X}{:02X}",
275 ((rgb.r * 255.) as u32),
276 ((rgb.g * 255.) as u32),
277 ((rgb.b * 255.) as u32),
278 ((self.a * 255.) as u32)
279 );
280 }
281
282 format!(
283 "#{:02X}{:02X}{:02X}",
284 ((rgb.r * 255.) as u32),
285 ((rgb.g * 255.) as u32),
286 ((rgb.b * 255.) as u32)
287 )
288 }
289
290 fn parse_hex(hex: &str) -> Result<Self> {
291 let hex = hex.trim_start_matches('#');
292 let len = hex.len();
293 if len != 6 && len != 8 {
294 return Err(anyhow::anyhow!("invalid hex color"));
295 }
296
297 let r = u8::from_str_radix(&hex[0..2], 16)? as f32 / 255.;
298 let g = u8::from_str_radix(&hex[2..4], 16)? as f32 / 255.;
299 let b = u8::from_str_radix(&hex[4..6], 16)? as f32 / 255.;
300 let a = if len == 8 {
301 u8::from_str_radix(&hex[6..8], 16)? as f32 / 255.
302 } else {
303 1.
304 };
305
306 let v = gpui::Rgba { r, g, b, a };
307 let color: Hsla = v.into();
308 Ok(color)
309 }
310
311 fn hue(&self, hue: f32) -> Self {
312 let mut color = *self;
313 color.h = hue.clamp(0., 1.);
314 color
315 }
316
317 fn saturation(&self, saturation: f32) -> Self {
318 let mut color = *self;
319 color.s = saturation.clamp(0., 1.);
320 color
321 }
322
323 fn lightness(&self, lightness: f32) -> Self {
324 let mut color = *self;
325 color.l = lightness.clamp(0., 1.);
326 color
327 }
328}
329
330pub(crate) static DEFAULT_COLORS: once_cell::sync::Lazy<ShadcnColors> =
331 once_cell::sync::Lazy::new(|| {
332 serde_json::from_str(include_str!("./default-colors.json"))
333 .expect("failed to parse default-colors.json")
334 });
335
336type ColorScales = HashMap<usize, ShadcnColor>;
337
338mod color_scales {
339 use std::collections::HashMap;
340
341 use super::{ColorScales, ShadcnColor};
342
343 use serde::de::{Deserialize, Deserializer};
344
345 pub fn deserialize<'de, D>(deserializer: D) -> Result<ColorScales, D::Error>
346 where
347 D: Deserializer<'de>,
348 {
349 let mut map = HashMap::new();
350 for color in Vec::<ShadcnColor>::deserialize(deserializer)? {
351 map.insert(color.scale, color);
352 }
353 Ok(map)
354 }
355}
356
357#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
359pub enum ColorName {
360 White,
361 Black,
362 Neutral,
363 Gray,
364 Red,
365 Orange,
366 Amber,
367 Yellow,
368 Lime,
369 Green,
370 Emerald,
371 Teal,
372 Cyan,
373 Sky,
374 Blue,
375 Indigo,
376 Violet,
377 Purple,
378 Fuchsia,
379 Pink,
380 Rose,
381}
382
383impl Display for ColorName {
384 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
385 write!(f, "{:?}", self)
386 }
387}
388
389impl TryFrom<&str> for ColorName {
391 type Error = anyhow::Error;
392 fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
393 match value.to_lowercase().as_str() {
394 "white" => Ok(ColorName::White),
395 "black" => Ok(ColorName::Black),
396 "neutral" => Ok(ColorName::Neutral),
397 "gray" => Ok(ColorName::Gray),
398 "red" => Ok(ColorName::Red),
399 "orange" => Ok(ColorName::Orange),
400 "amber" => Ok(ColorName::Amber),
401 "yellow" => Ok(ColorName::Yellow),
402 "lime" => Ok(ColorName::Lime),
403 "green" => Ok(ColorName::Green),
404 "emerald" => Ok(ColorName::Emerald),
405 "teal" => Ok(ColorName::Teal),
406 "cyan" => Ok(ColorName::Cyan),
407 "sky" => Ok(ColorName::Sky),
408 "blue" => Ok(ColorName::Blue),
409 "indigo" => Ok(ColorName::Indigo),
410 "violet" => Ok(ColorName::Violet),
411 "purple" => Ok(ColorName::Purple),
412 "fuchsia" => Ok(ColorName::Fuchsia),
413 "pink" => Ok(ColorName::Pink),
414 "rose" => Ok(ColorName::Rose),
415 _ => Err(anyhow::anyhow!("Invalid color name")),
416 }
417 }
418}
419
420impl TryFrom<SharedString> for ColorName {
421 type Error = anyhow::Error;
422 fn try_from(value: SharedString) -> std::result::Result<Self, Self::Error> {
423 value.as_ref().try_into()
424 }
425}
426
427impl ColorName {
428 pub fn all() -> [Self; 19] {
430 [
431 ColorName::Neutral,
432 ColorName::Gray,
433 ColorName::Red,
434 ColorName::Orange,
435 ColorName::Amber,
436 ColorName::Yellow,
437 ColorName::Lime,
438 ColorName::Green,
439 ColorName::Emerald,
440 ColorName::Teal,
441 ColorName::Cyan,
442 ColorName::Sky,
443 ColorName::Blue,
444 ColorName::Indigo,
445 ColorName::Violet,
446 ColorName::Purple,
447 ColorName::Fuchsia,
448 ColorName::Pink,
449 ColorName::Rose,
450 ]
451 }
452
453 pub fn scale(&self, scale: usize) -> Hsla {
458 if self == &ColorName::White {
459 return DEFAULT_COLORS.white.hsla;
460 }
461 if self == &ColorName::Black {
462 return DEFAULT_COLORS.black.hsla;
463 }
464
465 let colors = match self {
466 ColorName::Neutral => &DEFAULT_COLORS.neutral,
467 ColorName::Gray => &DEFAULT_COLORS.gray,
468 ColorName::Red => &DEFAULT_COLORS.red,
469 ColorName::Orange => &DEFAULT_COLORS.orange,
470 ColorName::Amber => &DEFAULT_COLORS.amber,
471 ColorName::Yellow => &DEFAULT_COLORS.yellow,
472 ColorName::Lime => &DEFAULT_COLORS.lime,
473 ColorName::Green => &DEFAULT_COLORS.green,
474 ColorName::Emerald => &DEFAULT_COLORS.emerald,
475 ColorName::Teal => &DEFAULT_COLORS.teal,
476 ColorName::Cyan => &DEFAULT_COLORS.cyan,
477 ColorName::Sky => &DEFAULT_COLORS.sky,
478 ColorName::Blue => &DEFAULT_COLORS.blue,
479 ColorName::Indigo => &DEFAULT_COLORS.indigo,
480 ColorName::Violet => &DEFAULT_COLORS.violet,
481 ColorName::Purple => &DEFAULT_COLORS.purple,
482 ColorName::Fuchsia => &DEFAULT_COLORS.fuchsia,
483 ColorName::Pink => &DEFAULT_COLORS.pink,
484 ColorName::Rose => &DEFAULT_COLORS.rose,
485 _ => unreachable!(),
486 };
487
488 if let Some(color) = colors.get(&scale) {
489 color.hsla
490 } else {
491 colors.get(&500).unwrap().hsla
492 }
493 }
494}
495
496#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
497pub(crate) struct ShadcnColors {
498 pub(crate) black: ShadcnColor,
499 pub(crate) white: ShadcnColor,
500 #[serde(with = "color_scales")]
501 pub(crate) slate: ColorScales,
502 #[serde(with = "color_scales")]
503 pub(crate) gray: ColorScales,
504 #[serde(with = "color_scales")]
505 pub(crate) zinc: ColorScales,
506 #[serde(with = "color_scales")]
507 pub(crate) neutral: ColorScales,
508 #[serde(with = "color_scales")]
509 pub(crate) stone: ColorScales,
510 #[serde(with = "color_scales")]
511 pub(crate) red: ColorScales,
512 #[serde(with = "color_scales")]
513 pub(crate) orange: ColorScales,
514 #[serde(with = "color_scales")]
515 pub(crate) amber: ColorScales,
516 #[serde(with = "color_scales")]
517 pub(crate) yellow: ColorScales,
518 #[serde(with = "color_scales")]
519 pub(crate) lime: ColorScales,
520 #[serde(with = "color_scales")]
521 pub(crate) green: ColorScales,
522 #[serde(with = "color_scales")]
523 pub(crate) emerald: ColorScales,
524 #[serde(with = "color_scales")]
525 pub(crate) teal: ColorScales,
526 #[serde(with = "color_scales")]
527 pub(crate) cyan: ColorScales,
528 #[serde(with = "color_scales")]
529 pub(crate) sky: ColorScales,
530 #[serde(with = "color_scales")]
531 pub(crate) blue: ColorScales,
532 #[serde(with = "color_scales")]
533 pub(crate) indigo: ColorScales,
534 #[serde(with = "color_scales")]
535 pub(crate) violet: ColorScales,
536 #[serde(with = "color_scales")]
537 pub(crate) purple: ColorScales,
538 #[serde(with = "color_scales")]
539 pub(crate) fuchsia: ColorScales,
540 #[serde(with = "color_scales")]
541 pub(crate) pink: ColorScales,
542 #[serde(with = "color_scales")]
543 pub(crate) rose: ColorScales,
544}
545
546#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize)]
547pub(crate) struct ShadcnColor {
548 #[serde(default)]
549 pub(crate) scale: usize,
550 #[serde(deserialize_with = "from_hsl_channel", alias = "hslChannel")]
551 pub(crate) hsla: Hsla,
552}
553
554fn from_hsl_channel<'de, D>(deserializer: D) -> Result<Hsla, D::Error>
556where
557 D: Deserializer<'de>,
558{
559 let s: String = Deserialize::deserialize(deserializer).unwrap();
560
561 let mut parts = s.split_whitespace();
562 if parts.clone().count() != 3 {
563 return Err(D::Error::custom(
564 "expected hslChannel has 3 parts, e.g: '210 40% 98%'",
565 ));
566 }
567
568 fn parse_number(s: &str) -> f32 {
569 s.trim_end_matches('%')
570 .parse()
571 .expect("failed to parse number")
572 }
573
574 let (h, s, l) = (
575 parse_number(parts.next().unwrap()),
576 parse_number(parts.next().unwrap()),
577 parse_number(parts.next().unwrap()),
578 );
579
580 Ok(hsl(h, s, l))
581}
582
583macro_rules! color_method {
584 ($color:tt, $scale:tt) => {
585 paste::paste! {
586 #[inline]
587 #[allow(unused)]
588 pub fn [<$color _ $scale>]() -> Hsla {
589 if let Some(color) = DEFAULT_COLORS.$color.get(&($scale as usize)) {
590 return color.hsla;
591 }
592
593 black()
594 }
595 }
596 };
597}
598
599macro_rules! color_methods {
600 ($color:tt) => {
601 paste::paste! {
602 #[inline]
609 pub fn [<$color>](scale: usize) -> Hsla {
610 if let Some(color) = DEFAULT_COLORS.$color.get(&scale) {
611 return color.hsla;
612 }
613
614 black()
615 }
616 }
617
618 color_method!($color, 50);
619 color_method!($color, 100);
620 color_method!($color, 200);
621 color_method!($color, 300);
622 color_method!($color, 400);
623 color_method!($color, 500);
624 color_method!($color, 600);
625 color_method!($color, 700);
626 color_method!($color, 800);
627 color_method!($color, 900);
628 color_method!($color, 950);
629 };
630}
631
632pub fn black() -> Hsla {
633 DEFAULT_COLORS.black.hsla
634}
635
636pub fn white() -> Hsla {
637 DEFAULT_COLORS.white.hsla
638}
639
640color_methods!(slate);
641color_methods!(gray);
642color_methods!(zinc);
643color_methods!(neutral);
644color_methods!(stone);
645color_methods!(red);
646color_methods!(orange);
647color_methods!(amber);
648color_methods!(yellow);
649color_methods!(lime);
650color_methods!(green);
651color_methods!(emerald);
652color_methods!(teal);
653color_methods!(cyan);
654color_methods!(sky);
655color_methods!(blue);
656color_methods!(indigo);
657color_methods!(violet);
658color_methods!(purple);
659color_methods!(fuchsia);
660color_methods!(pink);
661color_methods!(rose);
662
663pub fn try_parse_color(color: &str) -> Result<Hsla> {
678 if color.starts_with("#") {
679 let rgba = gpui::Rgba::try_from(color)?;
680 return Ok(rgba.into());
681 }
682
683 let mut name = String::new();
684 let mut scale = None;
685 let mut opacity = None;
686 let mut state = 0;
688 let mut part = String::new();
689
690 for c in color.chars() {
691 match c {
692 '-' if state == 0 => {
693 name = std::mem::take(&mut part);
694 state = 1;
695 }
696 '/' if state <= 1 => {
697 if state == 0 {
698 name = std::mem::take(&mut part);
699 } else if state == 1 {
700 scale = part.parse::<usize>().ok();
701 part.clear();
702 }
703 state = 2;
704 }
705 _ => part.push(c),
706 }
707 }
708
709 match state {
710 0 => name = part,
711 1 => scale = part.parse::<usize>().ok(),
712 2 => opacity = part.parse::<f32>().ok(),
713 _ => {}
714 }
715
716 if name.is_empty() {
717 return Err(anyhow!("Empty color name"));
718 }
719
720 let mut hsla = match name.as_str() {
721 "black" => Ok::<Hsla, Error>(crate::black()),
722 "white" => Ok(crate::white()),
723 _ => {
724 let color_name = ColorName::try_from(name.as_str())?;
725 if let Some(scale) = scale {
726 Ok(color_name.scale(scale))
727 } else {
728 Ok(color_name.scale(500))
729 }
730 }
731 }?;
732
733 if let Some(opacity) = opacity {
734 if opacity > 100. {
735 return Err(anyhow!("Invalid color opacity"));
736 }
737 hsla = hsla.opacity(opacity / 100.);
738 }
739
740 Ok(hsla)
741}
742
743pub fn try_parse_background(background: &str) -> Result<Background> {
748 if let Ok(color) = try_parse_color(background) {
749 return Ok(color.into());
750 }
751
752 let gradient = parse_linear_gradient(background)?;
753 Ok(linear_gradient(gradient.angle, gradient.from, gradient.to))
754}
755
756pub(crate) fn try_parse_background_clamped(background: &str, max: f32) -> Result<Background> {
762 if let Ok(color) = try_parse_color(background) {
763 return Ok(color.alpha(color.a.min(max)).into());
764 }
765
766 let gradient = parse_linear_gradient(background)?;
767 let clamp = |stop: LinearColorStop| {
768 linear_color_stop(stop.color.alpha(stop.color.a.min(max)), stop.percentage)
769 };
770 Ok(linear_gradient(
771 gradient.angle,
772 clamp(gradient.from),
773 clamp(gradient.to),
774 ))
775}
776
777pub(crate) fn try_parse_theme_color(color: &str) -> Result<Hsla> {
778 if let Ok(color) = try_parse_color(color) {
779 return Ok(color);
780 }
781
782 Ok(parse_linear_gradient(color)?.from.color)
783}
784
785struct ParsedLinearGradient {
786 angle: f32,
787 from: LinearColorStop,
788 to: LinearColorStop,
789}
790
791fn parse_linear_gradient(background: &str) -> Result<ParsedLinearGradient> {
792 const PREFIX: &str = "linear-gradient(";
793
794 let background = background.trim();
795 if !background.to_ascii_lowercase().starts_with(PREFIX) || !background.ends_with(')') {
796 return Err(anyhow!("Unsupported background value"));
797 }
798
799 let inner = &background[PREFIX.len()..background.len() - 1];
800 let parts = split_top_level_commas(inner);
801 let (angle, from, to) = match parts.as_slice() {
802 [from, to] => (
803 180.,
804 parse_linear_color_stop(from, 0.)?,
805 parse_linear_color_stop(to, 1.)?,
806 ),
807 [angle, from, to] => (
808 parse_linear_gradient_angle(angle)?,
809 parse_linear_color_stop(from, 0.)?,
810 parse_linear_color_stop(to, 1.)?,
811 ),
812 _ => {
813 return Err(anyhow!(
814 "Expected linear-gradient with two color stops, e.g. linear-gradient(135deg, #000, #fff)"
815 ));
816 }
817 };
818
819 Ok(ParsedLinearGradient { angle, from, to })
820}
821
822fn split_top_level_commas(value: &str) -> Vec<String> {
823 let mut parts = Vec::new();
824 let mut depth = 0usize;
825 let mut start = 0usize;
826
827 for (ix, ch) in value.char_indices() {
828 match ch {
829 '(' => depth += 1,
830 ')' => depth = depth.saturating_sub(1),
831 ',' if depth == 0 => {
832 parts.push(value[start..ix].trim().to_string());
833 start = ix + ch.len_utf8();
834 }
835 _ => {}
836 }
837 }
838
839 parts.push(value[start..].trim().to_string());
840 parts
841}
842
843fn parse_linear_gradient_angle(angle: &str) -> Result<f32> {
844 let angle = angle.trim().to_ascii_lowercase();
845
846 if let Some(degrees) = angle.strip_suffix("deg") {
847 return Ok(degrees.trim().parse::<f32>()?.rem_euclid(360.));
848 }
849
850 if let Some(direction) = angle.strip_prefix("to ") {
851 return parse_linear_gradient_direction(direction);
852 }
853
854 Err(anyhow!("Unsupported linear-gradient angle: {angle}"))
855}
856
857fn parse_linear_gradient_direction(direction: &str) -> Result<f32> {
858 let mut top = false;
859 let mut right = false;
860 let mut bottom = false;
861 let mut left = false;
862
863 for part in direction.split_whitespace() {
864 match part {
865 "top" => top = true,
866 "right" => right = true,
867 "bottom" => bottom = true,
868 "left" => left = true,
869 _ => {
870 return Err(anyhow!(
871 "Unsupported linear-gradient direction: {direction}"
872 ));
873 }
874 }
875 }
876
877 match (top, right, bottom, left) {
878 (true, false, false, false) => Ok(0.),
879 (false, true, false, false) => Ok(90.),
880 (false, false, true, false) => Ok(180.),
881 (false, false, false, true) => Ok(270.),
882 (true, true, false, false) => Ok(45.),
883 (false, true, true, false) => Ok(135.),
884 (false, false, true, true) => Ok(225.),
885 (true, false, false, true) => Ok(315.),
886 _ => Err(anyhow!(
887 "Unsupported linear-gradient direction: {direction}"
888 )),
889 }
890}
891
892fn parse_linear_color_stop(stop: &str, default_percentage: f32) -> Result<LinearColorStop> {
893 let stop = stop.trim();
894 let mut parts = stop.split_whitespace().collect::<Vec<_>>();
895 let percentage = parts
896 .last()
897 .and_then(|part| part.strip_suffix('%'))
898 .map(|part| part.parse::<f32>().map(|value| value / 100.))
899 .transpose()?
900 .unwrap_or(default_percentage);
901
902 if stop.ends_with('%') {
903 parts.pop();
904 }
905
906 let color = parts.join(" ");
907 if color.is_empty() {
908 return Err(anyhow!("Expected color in linear-gradient color stop"));
909 }
910
911 Ok(linear_color_stop(
912 try_parse_color(&color)?,
913 percentage.clamp(0., 1.),
914 ))
915}
916
917#[cfg(test)]
918mod tests {
919 use gpui::{rgb, rgba};
920
921 use super::*;
922
923 #[test]
924 fn test_default_colors() {
925 assert_eq!(white(), hsl(0.0, 0.0, 100.0));
926 assert_eq!(black(), hsl(0.0, 0.0, 0.0));
927
928 assert_eq!(slate_50(), hsl(210.0, 40.0, 98.0));
929 assert_eq!(slate_100(), hsl(210.0, 40.0, 96.1));
930 assert_eq!(slate_900(), hsl(222.2, 47.4, 11.2));
931
932 assert_eq!(red_50(), hsl(0.0, 85.7, 97.3));
933 assert_eq!(yellow_100(), hsl(54.9, 96.7, 88.0));
934 assert_eq!(green_200(), hsl(141.0, 78.9, 85.1));
935 assert_eq!(cyan_300(), hsl(187.0, 92.4, 69.0));
936 assert_eq!(blue_400(), hsl(213.1, 93.9, 67.8));
937 assert_eq!(indigo_500(), hsl(238.7, 83.5, 66.7));
938 }
939
940 #[test]
941 fn test_to_hex_string() {
942 let color: Hsla = rgb(0xf8fafc).into();
943 assert_eq!(color.to_hex(), "#F8FAFC");
944
945 let color: Hsla = rgb(0xfef2f2).into();
946 assert_eq!(color.to_hex(), "#FEF2F2");
947
948 let color: Hsla = rgba(0x0413fcaa).into();
949 assert_eq!(color.to_hex(), "#0413FCAA");
950 }
951
952 #[test]
953 fn test_from_hex_string() {
954 let color: Hsla = Hsla::parse_hex("#F8FAFC").unwrap();
955 assert_eq!(color, rgb(0xf8fafc).into());
956
957 let color: Hsla = Hsla::parse_hex("#FEF2F2").unwrap();
958 assert_eq!(color, rgb(0xfef2f2).into());
959
960 let color: Hsla = Hsla::parse_hex("#0413FCAA").unwrap();
961 assert_eq!(color, rgba(0x0413fcaa).into());
962 }
963
964 #[test]
965 fn test_lighten() {
966 let color = super::hsl(240.0, 5.0, 30.0);
967 let color = color.lighten(0.5);
968 assert_eq!(color.l, 0.45000002);
969 let color = color.lighten(0.5);
970 assert_eq!(color.l, 0.675);
971 let color = color.lighten(0.1);
972 assert_eq!(color.l, 0.7425);
973 }
974
975 #[test]
976 fn test_darken() {
977 let color = super::hsl(240.0, 5.0, 96.0);
978 let color = color.darken(0.5);
979 assert_eq!(color.l, 0.48);
980 let color = color.darken(0.5);
981 assert_eq!(color.l, 0.24);
982 }
983
984 #[test]
985 fn test_mix() {
986 let red = Hsla::parse_hex("#FF0000").unwrap();
987 let blue = Hsla::parse_hex("#0000FF").unwrap();
988 let green = Hsla::parse_hex("#00FF00").unwrap();
989 let yellow = Hsla::parse_hex("#FFFF00").unwrap();
990
991 assert_eq!(red.mix(blue, 0.5).to_hex(), "#FF00FF");
992 assert_eq!(green.mix(red, 0.5).to_hex(), "#FFFF00");
993 assert_eq!(blue.mix(yellow, 0.2).to_hex(), "#0098FF");
994 }
995
996 #[test]
997 fn test_mix_oklab() {
998 let red = Hsla::parse_hex("#FF0000").unwrap();
999 let blue = Hsla::parse_hex("#0000FF").unwrap();
1000 let transparent = gpui::Hsla {
1001 h: 0.0,
1002 s: 0.0,
1003 l: 0.0,
1004 a: 0.0,
1005 };
1006
1007 let result = red.mix_oklab(transparent, 0.2);
1010 assert!((result.a - 0.2).abs() < 0.01); let rgb_result = result.to_rgb();
1014 let rgb_red = red.to_rgb();
1015 assert!(
1017 (rgb_result.r - rgb_red.r).abs() < 0.05,
1018 "Red channel should be preserved"
1019 );
1020 assert!(rgb_result.g < 0.05, "Green channel should be near 0");
1021 assert!(rgb_result.b < 0.05, "Blue channel should be near 0");
1022
1023 let purple = red.mix_oklab(blue, 0.5);
1025 let purple_hsl = red.mix(blue, 0.5);
1027 assert_ne!(purple.to_hex(), purple_hsl.to_hex());
1028
1029 let result_0 = red.mix_oklab(blue, 0.0);
1031 let result_1 = red.mix_oklab(blue, 1.0);
1032
1033 let rgb_0 = result_0.to_rgb();
1035 let rgb_blue = blue.to_rgb();
1036 assert!((rgb_0.r - rgb_blue.r).abs() < 0.01);
1037 assert!((rgb_0.g - rgb_blue.g).abs() < 0.01);
1038 assert!((rgb_0.b - rgb_blue.b).abs() < 0.01);
1039
1040 let rgb_1 = result_1.to_rgb();
1041 let rgb_red = red.to_rgb();
1042 assert!((rgb_1.r - rgb_red.r).abs() < 0.01);
1043 assert!((rgb_1.g - rgb_red.g).abs() < 0.01);
1044 assert!((rgb_1.b - rgb_red.b).abs() < 0.01);
1045 }
1046
1047 #[test]
1048 fn test_color_name() {
1049 assert_eq!(ColorName::Purple.to_string(), "Purple");
1050 assert_eq!(format!("{}", ColorName::Green), "Green");
1051 assert_eq!(format!("{:?}", ColorName::Yellow), "Yellow");
1052
1053 let color = ColorName::Green;
1054 assert_eq!(color.scale(500).to_hex(), "#21C55E");
1055 assert_eq!(color.scale(1500).to_hex(), "#21C55E");
1056
1057 for name in ColorName::all().iter() {
1058 let name1: ColorName = name.to_string().as_str().try_into().unwrap();
1059 assert_eq!(name1, *name);
1060 }
1061 }
1062
1063 #[test]
1064 fn test_h_s_l() {
1065 let color = hsl(260., 94., 80.);
1066 assert_eq!(color.hue(200. / 360.), hsl(200., 94., 80.));
1067 assert_eq!(color.saturation(74. / 100.), hsl(260., 74., 80.));
1068 assert_eq!(color.lightness(74. / 100.), hsl(260., 94., 74.));
1069 }
1070
1071 #[test]
1072 fn test_try_parse_color() {
1073 assert_eq!(
1074 try_parse_color("#F2F200").ok(),
1075 Some(hsla(0.16666667, 1., 0.4745098, 1.0))
1076 );
1077 assert_eq!(
1078 try_parse_color("#00f21888").ok(),
1079 Some(hsla(0.34986225, 1.0, 0.4745098, 0.53333336))
1080 );
1081 assert_eq!(try_parse_color("black").ok(), Some(crate::black()));
1082 assert_eq!(try_parse_color("white-800").ok(), Some(crate::white()));
1083 assert_eq!(try_parse_color("red").ok(), Some(crate::red_500()));
1084 assert_eq!(try_parse_color("blue-600").ok(), Some(crate::blue_600()));
1085 assert_eq!(
1086 try_parse_color("pink/33").ok(),
1087 Some(crate::pink_500().opacity(0.33))
1088 );
1089 assert_eq!(
1090 try_parse_color("orange-300/66").ok(),
1091 Some(crate::orange_300().opacity(0.66))
1092 );
1093 }
1094
1095 #[test]
1096 fn test_try_parse_background_linear_gradient() {
1097 let from = try_parse_color("#4F46E5").unwrap();
1098 let to = try_parse_color("#06B6D4").unwrap();
1099
1100 assert_eq!(
1101 try_parse_background("linear-gradient(135deg, #4F46E5, #06B6D4)").unwrap(),
1102 gpui::linear_gradient(
1103 135.,
1104 gpui::linear_color_stop(from, 0.),
1105 gpui::linear_color_stop(to, 1.)
1106 )
1107 );
1108 }
1109
1110 #[test]
1111 fn test_try_parse_background_linear_gradient_direction_and_stops() {
1112 assert_eq!(
1113 try_parse_background("linear-gradient(to right, red-500 25%, blue-600 75%)").unwrap(),
1114 gpui::linear_gradient(
1115 90.,
1116 gpui::linear_color_stop(crate::red_500(), 0.25),
1117 gpui::linear_color_stop(crate::blue_600(), 0.75)
1118 )
1119 );
1120 }
1121}