Skip to main content

guise/theme/
color.rs

1//! 24-bit RGB color with hex parsing and conversion into gpui's `Hsla`.
2//!
3//! Colors are stored as plain RGB triples (the open-color palette is
4//! authored as hex). `hsla`/`alpha` bridge into the gpui rendering types at the
5//! point of use.
6
7use gpui::{Hsla, Rgba};
8
9/// An opaque 24-bit color.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
11pub struct Color {
12  pub r: u8,
13  pub g: u8,
14  pub b: u8,
15}
16
17impl Color {
18  pub const fn new(r: u8, g: u8, b: u8) -> Self {
19    Color { r, g, b }
20  }
21
22  /// Parse a `#rrggbb` / `rrggbb` / `#rgb` / `rgb` hex string.
23  ///
24  /// Panics on malformed input — callers pass static palette literals, so a
25  /// bad value is a programming error that should fail loudly at startup.
26  pub const fn hex(s: &str) -> Self {
27    let bytes = s.as_bytes();
28    let hex = if !bytes.is_empty() && bytes[0] == b'#' {
29      split_at(bytes, 1)
30    } else {
31      bytes
32    };
33    match hex.len() {
34      3 => Color::new(
35        nibble(hex[0]) * 17,
36        nibble(hex[1]) * 17,
37        nibble(hex[2]) * 17,
38      ),
39      6 => Color::new(
40        nibble(hex[0]) * 16 + nibble(hex[1]),
41        nibble(hex[2]) * 16 + nibble(hex[3]),
42        nibble(hex[4]) * 16 + nibble(hex[5]),
43      ),
44      _ => panic!("guise: color hex must be 3 or 6 digits"),
45    }
46  }
47
48  /// Build an opaque `Color` from a gpui `Hsla`, dropping any alpha. Used so a
49  /// CSS color (which parses to `Hsla`) can populate the opaque theme fields.
50  pub fn from_hsla(c: Hsla) -> Color {
51    let (r, g, b) = hsl_to_rgb(c.h, c.s, c.l);
52    Color::new(r, g, b)
53  }
54
55  /// As an opaque gpui `Rgba`.
56  pub fn rgba(self) -> Rgba {
57    Rgba {
58      r: self.r as f32 / 255.0,
59      g: self.g as f32 / 255.0,
60      b: self.b as f32 / 255.0,
61      a: 1.0,
62    }
63  }
64
65  /// As an opaque gpui `Hsla`.
66  pub fn hsla(self) -> Hsla {
67    self.rgba().into()
68  }
69
70  /// As a gpui `Hsla` with the given alpha in `0.0..=1.0`.
71  pub fn alpha(self, a: f32) -> Hsla {
72    let mut hsla: Hsla = self.rgba().into();
73    hsla.a = a.clamp(0.0, 1.0);
74    hsla
75  }
76
77  /// Relative luminance in `0.0..=1.0` (Rec. 709 weights). Used to pick a
78  /// readable foreground (black vs. white) over a filled color.
79  pub fn luminance(self) -> f32 {
80    (0.2126 * self.r as f32 + 0.7152 * self.g as f32 + 0.0722 * self.b as f32) / 255.0
81  }
82
83  /// A foreground color (black or white) with adequate contrast over `self`.
84  pub fn contrasting(self) -> Color {
85    if self.luminance() > 0.55 {
86      Color::new(0, 0, 0)
87    } else {
88      Color::new(255, 255, 255)
89    }
90  }
91}
92
93impl From<Color> for Hsla {
94  fn from(c: Color) -> Hsla {
95    c.hsla()
96  }
97}
98
99/// HSL (h,s,l all in `0.0..=1.0`, the gpui convention) to 8-bit RGB.
100fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (u8, u8, u8) {
101  let to_u8 = |v: f32| (v.clamp(0.0, 1.0) * 255.0).round() as u8;
102  if s <= 0.0 {
103    let v = to_u8(l);
104    return (v, v, v);
105  }
106  let q = if l < 0.5 {
107    l * (1.0 + s)
108  } else {
109    l + s - l * s
110  };
111  let p = 2.0 * l - q;
112  let hue = |mut t: f32| {
113    if t < 0.0 {
114      t += 1.0;
115    }
116    if t > 1.0 {
117      t -= 1.0;
118    }
119    if t < 1.0 / 6.0 {
120      p + (q - p) * 6.0 * t
121    } else if t < 1.0 / 2.0 {
122      q
123    } else if t < 2.0 / 3.0 {
124      p + (q - p) * (2.0 / 3.0 - t) * 6.0
125    } else {
126      p
127    }
128  };
129  (
130    to_u8(hue(h + 1.0 / 3.0)),
131    to_u8(hue(h)),
132    to_u8(hue(h - 1.0 / 3.0)),
133  )
134}
135
136const fn split_at(bytes: &[u8], at: usize) -> &[u8] {
137  // const-friendly slice from `at` to end.
138  bytes.split_at(at).1
139}
140
141const fn nibble(b: u8) -> u8 {
142  match b {
143    b'0'..=b'9' => b - b'0',
144    b'a'..=b'f' => b - b'a' + 10,
145    b'A'..=b'F' => b - b'A' + 10,
146    _ => panic!("guise: invalid hex digit in color"),
147  }
148}
149
150#[cfg(test)]
151mod tests {
152  use super::*;
153
154  #[test]
155  fn parses_long_and_short_forms() {
156    assert_eq!(Color::hex("#228be6"), Color::new(0x22, 0x8b, 0xe6));
157    assert_eq!(Color::hex("228be6"), Color::new(0x22, 0x8b, 0xe6));
158    assert_eq!(Color::hex("#fff"), Color::new(255, 255, 255));
159    assert_eq!(Color::hex("000"), Color::new(0, 0, 0));
160  }
161
162  #[test]
163  fn contrasting_picks_readable_foreground() {
164    assert_eq!(Color::hex("#ffffff").contrasting(), Color::new(0, 0, 0));
165    assert_eq!(
166      Color::hex("#1864ab").contrasting(),
167      Color::new(255, 255, 255)
168    );
169  }
170}