color.rs

  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 it's 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
 63            .chars()
 64            .map(|c| c.to_string().repeat(2))
 65            .collect::<String>(),
 66        4 => {
 67            let (rgb, alpha) = hex.split_at(3);
 68            let rgb = rgb
 69                .chars()
 70                .map(|c| c.to_string().repeat(2))
 71                .collect::<String>();
 72            let alpha = alpha.chars().next().unwrap().to_string().repeat(2);
 73            format!("{}{}", rgb, alpha)
 74        }
 75        6 => format!("{}ff", hex), // Add alpha if missing
 76        8 => hex.to_string(),      // Already in full format
 77        _ => return Err("Invalid hexadecimal string length".to_string()),
 78    };
 79
 80    let hex_val =
 81        u32::from_str_radix(&hex, 16).map_err(|_| format!("Invalid hexadecimal string: {}", s))?;
 82
 83    let r = ((hex_val >> 24) & 0xFF) as f32 / 255.0;
 84    let g = ((hex_val >> 16) & 0xFF) as f32 / 255.0;
 85    let b = ((hex_val >> 8) & 0xFF) as f32 / 255.0;
 86    let a = (hex_val & 0xFF) as f32 / 255.0;
 87
 88    let color = RGBAColor { r, g, b, a };
 89
 90    Ok(color)
 91}
 92
 93// These derives implement to and from palette's color types.
 94#[derive(FromColorUnclamped, WithAlpha, Debug, Clone)]
 95#[palette(skip_derives(Rgb), rgb_standard = "encoding::Srgb")]
 96pub struct RGBAColor {
 97    r: f32,
 98    g: f32,
 99    b: f32,
100    // Let Palette know this is our alpha channel.
101    #[palette(alpha)]
102    a: f32,
103}
104
105impl FromColorUnclamped<RGBAColor> for RGBAColor {
106    fn from_color_unclamped(color: RGBAColor) -> RGBAColor {
107        color
108    }
109}
110
111impl<S> FromColorUnclamped<Rgb<S, f32>> for RGBAColor
112where
113    Srgb: FromColorUnclamped<Rgb<S, f32>>,
114{
115    fn from_color_unclamped(color: Rgb<S, f32>) -> RGBAColor {
116        let srgb = Srgb::from_color_unclamped(color);
117        RGBAColor {
118            r: srgb.red,
119            g: srgb.green,
120            b: srgb.blue,
121            a: 1.0,
122        }
123    }
124}
125
126impl<S> FromColorUnclamped<RGBAColor> for Rgb<S, f32>
127where
128    Rgb<S, f32>: FromColorUnclamped<Srgb>,
129{
130    fn from_color_unclamped(color: RGBAColor) -> Self {
131        let srgb = Srgb::new(color.r, color.g, color.b);
132        Self::from_color_unclamped(srgb)
133    }
134}
135
136impl Clamp for RGBAColor {
137    fn clamp(self) -> Self {
138        RGBAColor {
139            r: self.r.min(1.0).max(0.0),
140            g: self.g.min(1.0).max(0.0),
141            b: self.b.min(1.0).max(0.0),
142            a: self.a.min(1.0).max(0.0),
143        }
144    }
145}
146
147impl RGBAColor {
148    /// Creates a new color from the given RGBA values.
149    ///
150    /// This color can be used to convert to any [`palette::Color`] type.
151    pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
152        RGBAColor { r, g, b, a }
153    }
154
155    /// Returns a set of states for this color.
156    pub fn states(self, is_light: bool) -> ColorStates {
157        states_for_color(self, is_light)
158    }
159
160    /// Mixes this color with another [`palette::Hsl`] color at the given `mix_ratio`.
161    pub fn mixed(&self, other: RGBAColor, mix_ratio: f32) -> Self {
162        let srgb_self = Srgb::new(self.r, self.g, self.b);
163        let srgb_other = Srgb::new(other.r, other.g, other.b);
164
165        // Directly mix the colors as sRGB values
166        let mixed = srgb_self.mix(srgb_other, mix_ratio);
167        RGBAColor::from_color_unclamped(mixed)
168    }
169
170    pub fn blend(&self, other: RGBAColor, blend_mode: BlendMode) -> Self {
171        let srgb_self = Srgb::new(self.r, self.g, self.b);
172        let srgb_other = Srgb::new(other.r, other.g, other.b);
173
174        let blended = match blend_mode {
175            // replace hsl methods with the respective sRGB methods
176            BlendMode::Multiply => srgb_self.multiply(srgb_other),
177            _ => unimplemented!(),
178        };
179
180        Self {
181            r: blended.red,
182            g: blended.green,
183            b: blended.blue,
184            a: self.a,
185        }
186    }
187}
188
189/// A set of colors for different states of an element.
190#[derive(Debug, Clone)]
191pub struct ColorStates {
192    /// The default color.
193    pub default: RGBAColor,
194    /// The color when the mouse is hovering over the element.
195    pub hover: RGBAColor,
196    /// The color when the mouse button is held down on the element.
197    pub active: RGBAColor,
198    /// The color when the element is focused with the keyboard.
199    pub focused: RGBAColor,
200    /// The color when the element is disabled.
201    pub disabled: RGBAColor,
202}
203
204/// Returns a set of colors for different states of an element.
205///
206/// todo!("This should take a theme and use appropriate colors from it")
207pub fn states_for_color(color: RGBAColor, is_light: bool) -> ColorStates {
208    let adjustment_factor = if is_light { 0.1 } else { -0.1 };
209    let hover_adjustment = 1.0 - adjustment_factor;
210    let active_adjustment = 1.0 - 2.0 * adjustment_factor;
211    let focused_adjustment = 1.0 - 3.0 * adjustment_factor;
212    let disabled_adjustment = 1.0 - 4.0 * adjustment_factor;
213
214    let make_adjustment = |color: RGBAColor, adjustment: f32| -> RGBAColor {
215        // Adjust lightness for each state
216        // Note: Adjustment logic may differ; simplify as needed for sRGB
217        RGBAColor::new(
218            color.r * adjustment,
219            color.g * adjustment,
220            color.b * adjustment,
221            color.a,
222        )
223    };
224
225    let color = color.clamp();
226
227    ColorStates {
228        default: color.clone(),
229        hover: make_adjustment(color.clone(), hover_adjustment),
230        active: make_adjustment(color.clone(), active_adjustment),
231        focused: make_adjustment(color.clone(), focused_adjustment),
232        disabled: make_adjustment(color.clone(), disabled_adjustment),
233    }
234}