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