1//! # Color
2//!
3//! The `color` crate provides a set utilities for working with colors. It is a wrapper around the [`palette`](https://docs.rs/palette) crate with some additional functionality.
4//!
5//! It is used to create a manipulate colors when building themes.
6//!
7//! === In development note ===
8//!
9//! This crate is meant to sit between gpui and the theme/ui for all the color related stuff.
10//!
11//! It could be folded into gpui, ui or theme potentially but for now we'll continue
12//! to develop it in isolation.
13//!
14//! Once we have a good idea of the needs of the theme system and color in gpui in general I see 3 paths:
15//! 1. Use `palette` (or another color library) directly in gpui and everywhere else, rather than rolling our own color system.
16//! 2. Keep this crate as a thin wrapper around `palette` and use it everywhere except gpui, and convert to gpui's color system when needed.
17//! 3. Build the needed functionality into gpui and keep using its color system everywhere.
18//!
19//! I'm leaning towards 2 in the short term and 1 in the long term, but we'll need to discuss it more.
20//!
21//! === End development note ===
22use palette::{
23 blend::Blend, convert::FromColorUnclamped, encoding, rgb::Rgb, Clamp, Mix, Srgb, WithAlpha,
24};
25
26/// The types of blend modes supported
27#[derive(Debug, Copy, Clone, PartialEq, Eq)]
28pub enum BlendMode {
29 /// Multiplies the colors, resulting in a darker color. This mode is useful for creating shadows.
30 Multiply,
31 /// Lightens the color by adding the source and destination colors. It results in a lighter color.
32 Screen,
33 /// Combines Multiply and Screen blend modes. Parts of the image that are lighter than 50% gray are lightened, and parts that are darker are darkened.
34 Overlay,
35 /// Selects the darker of the base or blend color as the resulting color. Useful for darkening images without affecting the overall contrast.
36 Darken,
37 /// Selects the lighter of the base or blend color as the resulting color. Useful for lightening images without affecting the overall contrast.
38 Lighten,
39 /// Brightens the base color to reflect the blend color. The result is a lightened image.
40 Dodge,
41 /// Darkens the base color to reflect the blend color. The result is a darkened image.
42 Burn,
43 /// Similar to Overlay, but with a stronger effect. Hard Light can either multiply or screen colors, depending on the blend color.
44 HardLight,
45 /// A softer version of Hard Light. Soft Light either darkens or lightens colors, depending on the blend color.
46 SoftLight,
47 /// Subtracts the darker of the two constituent colors from the lighter color. Difference mode is useful for creating more vivid colors.
48 Difference,
49 /// Similar to Difference, but with a lower contrast. Exclusion mode produces an effect similar to Difference but with less intensity.
50 Exclusion,
51}
52
53/// Converts a hexadecimal color string to a `palette::Hsla` color.
54///
55/// This function supports the following hex formats:
56/// `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`.
57pub fn hex_to_hsla(s: &str) -> Result<RGBAColor, String> {
58 let hex = s.trim_start_matches('#');
59
60 // Expand shorthand formats #RGB and #RGBA to #RRGGBB and #RRGGBBAA
61 let hex = match hex.len() {
62 3 => hex.chars().map(|c| c.to_string().repeat(2)).collect(),
63 4 => {
64 let (rgb, alpha) = hex.split_at(3);
65 let rgb = rgb
66 .chars()
67 .map(|c| c.to_string().repeat(2))
68 .collect::<String>();
69 let alpha = alpha.chars().next().unwrap().to_string().repeat(2);
70 format!("{}{}", rgb, alpha)
71 }
72 6 => format!("{}ff", hex), // Add alpha if missing
73 8 => hex.to_string(), // Already in full format
74 _ => return Err("Invalid hexadecimal string length".to_string()),
75 };
76
77 let hex_val =
78 u32::from_str_radix(&hex, 16).map_err(|_| format!("Invalid hexadecimal string: {}", s))?;
79
80 Ok(RGBAColor {
81 r: ((hex_val >> 24) & 0xFF) as f32 / 255.0,
82 g: ((hex_val >> 16) & 0xFF) as f32 / 255.0,
83 b: ((hex_val >> 8) & 0xFF) as f32 / 255.0,
84 a: (hex_val & 0xFF) as f32 / 255.0,
85 })
86}
87
88// These derives implement to and from palette's color types.
89#[derive(FromColorUnclamped, WithAlpha, Debug, Clone)]
90#[palette(skip_derives(Rgb), rgb_standard = "encoding::Srgb")]
91pub struct RGBAColor {
92 r: f32,
93 g: f32,
94 b: f32,
95 // Let Palette know this is our alpha channel.
96 #[palette(alpha)]
97 a: f32,
98}
99
100impl FromColorUnclamped<RGBAColor> for RGBAColor {
101 fn from_color_unclamped(color: RGBAColor) -> RGBAColor {
102 color
103 }
104}
105
106impl<S> FromColorUnclamped<Rgb<S, f32>> for RGBAColor
107where
108 Srgb: FromColorUnclamped<Rgb<S, f32>>,
109{
110 fn from_color_unclamped(color: Rgb<S, f32>) -> RGBAColor {
111 let srgb = Srgb::from_color_unclamped(color);
112 RGBAColor {
113 r: srgb.red,
114 g: srgb.green,
115 b: srgb.blue,
116 a: 1.0,
117 }
118 }
119}
120
121impl<S> FromColorUnclamped<RGBAColor> for Rgb<S, f32>
122where
123 Rgb<S, f32>: FromColorUnclamped<Srgb>,
124{
125 fn from_color_unclamped(color: RGBAColor) -> Self {
126 Self::from_color_unclamped(Srgb::new(color.r, color.g, color.b))
127 }
128}
129
130impl Clamp for RGBAColor {
131 fn clamp(self) -> Self {
132 RGBAColor {
133 r: self.r.min(1.0).max(0.0),
134 g: self.g.min(1.0).max(0.0),
135 b: self.b.min(1.0).max(0.0),
136 a: self.a.min(1.0).max(0.0),
137 }
138 }
139}
140
141impl RGBAColor {
142 /// Creates a new color from the given RGBA values.
143 ///
144 /// This color can be used to convert to any [`palette::Color`] type.
145 pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
146 RGBAColor { r, g, b, a }
147 }
148
149 /// Returns a set of states for this color.
150 pub fn states(self, is_light: bool) -> ColorStates {
151 states_for_color(self, is_light)
152 }
153
154 /// Mixes this color with another [`palette::Hsl`] color at the given `mix_ratio`.
155 pub fn mixed(&self, other: RGBAColor, mix_ratio: f32) -> Self {
156 let srgb_self = Srgb::new(self.r, self.g, self.b);
157 let srgb_other = Srgb::new(other.r, other.g, other.b);
158
159 // Directly mix the colors as sRGB values
160 let mixed = srgb_self.mix(srgb_other, mix_ratio);
161 RGBAColor::from_color_unclamped(mixed)
162 }
163
164 pub fn blend(&self, other: RGBAColor, blend_mode: BlendMode) -> Self {
165 let srgb_self = Srgb::new(self.r, self.g, self.b);
166 let srgb_other = Srgb::new(other.r, other.g, other.b);
167
168 let blended = match blend_mode {
169 // replace hsl methods with the respective sRGB methods
170 BlendMode::Multiply => srgb_self.multiply(srgb_other),
171 _ => unimplemented!(),
172 };
173
174 Self {
175 r: blended.red,
176 g: blended.green,
177 b: blended.blue,
178 a: self.a,
179 }
180 }
181}
182
183/// A set of colors for different states of an element.
184#[derive(Debug, Clone)]
185pub struct ColorStates {
186 /// The default color.
187 pub default: RGBAColor,
188 /// The color when the mouse is hovering over the element.
189 pub hover: RGBAColor,
190 /// The color when the mouse button is held down on the element.
191 pub active: RGBAColor,
192 /// The color when the element is focused with the keyboard.
193 pub focused: RGBAColor,
194 /// The color when the element is disabled.
195 pub disabled: RGBAColor,
196}
197
198/// Returns a set of colors for different states of an element.
199///
200/// todo("This should take a theme and use appropriate colors from it")
201pub fn states_for_color(color: RGBAColor, is_light: bool) -> ColorStates {
202 let adjustment_factor = if is_light { 0.1 } else { -0.1 };
203 let hover_adjustment = 1.0 - adjustment_factor;
204 let active_adjustment = 1.0 - 2.0 * adjustment_factor;
205 let focused_adjustment = 1.0 - 3.0 * adjustment_factor;
206 let disabled_adjustment = 1.0 - 4.0 * adjustment_factor;
207
208 let make_adjustment = |color: RGBAColor, adjustment: f32| -> RGBAColor {
209 // Adjust lightness for each state
210 // Note: Adjustment logic may differ; simplify as needed for sRGB
211 RGBAColor::new(
212 color.r * adjustment,
213 color.g * adjustment,
214 color.b * adjustment,
215 color.a,
216 )
217 };
218
219 let color = color.clamp();
220
221 ColorStates {
222 default: color.clone(),
223 hover: make_adjustment(color.clone(), hover_adjustment),
224 active: make_adjustment(color.clone(), active_adjustment),
225 focused: make_adjustment(color.clone(), focused_adjustment),
226 disabled: make_adjustment(color.clone(), disabled_adjustment),
227 }
228}