init color crate

Nate Butler created

Change summary

Cargo.lock                |  76 +++++++++++++++++++++++++
crates/color/Cargo.toml   |  32 +++++++++++
crates/color/src/color.rs | 118 +++++++++++++++++++++++++++++++++++++++++
crates/zed/Cargo.toml     |   1 
4 files changed, 225 insertions(+), 2 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -1580,6 +1580,28 @@ dependencies = [
  "rustc-hash",
 ]
 
+[[package]]
+name = "color"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "fs",
+ "indexmap 1.9.3",
+ "itertools 0.11.0",
+ "palette",
+ "parking_lot 0.11.2",
+ "refineable",
+ "schemars",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "settings",
+ "story",
+ "toml 0.5.11",
+ "util",
+ "uuid 1.4.1",
+]
+
 [[package]]
 name = "color_quant"
 version = "1.1.0"
@@ -4976,6 +4998,7 @@ dependencies = [
  "approx",
  "fast-srgb8",
  "palette_derive",
+ "phf",
 ]
 
 [[package]]
@@ -5164,6 +5187,48 @@ dependencies = [
  "indexmap 2.0.0",
 ]
 
+[[package]]
+name = "phf"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
+dependencies = [
+ "phf_shared",
+ "rand 0.8.5",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3444646e286606587e49f3bcf1679b8cef1dc2c5ecc29ddacaffc305180d464b"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.37",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
+dependencies = [
+ "siphasher 0.3.11",
+]
+
 [[package]]
 name = "picker"
 version = "0.1.0"
@@ -7073,6 +7138,12 @@ version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0b8de496cf83d4ed58b6be86c3a275b8602f6ffe98d3024a869e124147a9a3ac"
 
+[[package]]
+name = "siphasher"
+version = "0.3.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
+
 [[package]]
 name = "slab"
 version = "0.4.9"
@@ -7643,7 +7714,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9c536faaff1a10837cfe373142583f6e27d81e96beba339147e77b67c9f260ff"
 dependencies = [
  "float-cmp",
- "siphasher",
+ "siphasher 0.2.3",
 ]
 
 [[package]]
@@ -8857,7 +8928,7 @@ dependencies = [
  "roxmltree",
  "rustybuzz",
  "simplecss",
- "siphasher",
+ "siphasher 0.2.3",
  "svgtypes",
  "ttf-parser 0.12.3",
  "unicode-bidi",
@@ -9649,6 +9720,7 @@ dependencies = [
  "client",
  "collab_ui",
  "collections",
+ "color",
  "command_palette",
  "copilot",
  "copilot_ui",

crates/color/Cargo.toml 🔗

@@ -0,0 +1,32 @@
+[package]
+name = "color"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[features]
+default = []
+stories = ["dep:itertools", "dep:story"]
+
+[lib]
+path = "src/color.rs"
+doctest = true
+
+[dependencies]
+# TODO: Clean up dependencies
+anyhow.workspace = true
+fs = { path = "../fs" }
+indexmap = "1.6.2"
+parking_lot.workspace = true
+refineable.workspace = true
+schemars.workspace = true
+serde.workspace = true
+serde_derive.workspace = true
+serde_json.workspace = true
+settings = { path = "../settings" }
+story = { path = "../story", optional = true }
+toml.workspace = true
+uuid.workspace = true
+util = { path = "../util" }
+itertools = { version = "0.11.0", optional = true }
+palette = "0.7.3"

crates/color/src/color.rs 🔗

@@ -0,0 +1,118 @@
+//! # Color
+//!
+//! 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.
+//!
+//! It is used to create a manipulate colors when building themes.
+//!
+//! **Note:** This crate does not depend on `gpui`, so it does not provide any
+//! interfaces for converting to `gpui` style colors.
+
+use palette::{FromColor, Hsl, Hsla, Mix, Srgba, WithAlpha};
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq)]
+pub enum BlendMode {
+    Multiply,
+    Screen,
+    Overlay,
+    Darken,
+    Lighten,
+    Dodge,
+    Burn,
+    HardLight,
+    SoftLight,
+    Difference,
+    Exclusion,
+}
+
+/// Creates a new [`palette::Hsl`] color.
+pub fn hsl(h: f32, s: f32, l: f32) -> Hsl {
+    Hsl::new_srgb(h, s, l)
+}
+
+/// Converts a hexadecimal color string to a `palette::Hsla` color.
+///
+/// This function supports the following hex formats:
+/// `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`.
+pub fn hex_to_hsla(s: &str) -> Result<Hsla, String> {
+    let hex = s.trim_start_matches('#');
+
+    // Expand shorthand formats #RGB and #RGBA to #RRGGBB and #RRGGBBAA
+    let hex = match hex.len() {
+        3 => hex
+            .chars()
+            .map(|c| c.to_string().repeat(2))
+            .collect::<String>(),
+        4 => {
+            let (rgb, alpha) = hex.split_at(3);
+            let rgb = rgb
+                .chars()
+                .map(|c| c.to_string().repeat(2))
+                .collect::<String>();
+            let alpha = alpha.chars().next().unwrap().to_string().repeat(2);
+            format!("{}{}", rgb, alpha)
+        }
+        6 => format!("{}ff", hex), // Add alpha if missing
+        8 => hex.to_string(),      // Already in full format
+        _ => return Err("Invalid hexadecimal string length".to_string()),
+    };
+
+    let hex_val =
+        u32::from_str_radix(&hex, 16).map_err(|_| format!("Invalid hexadecimal string: {}", s))?;
+
+    let r = ((hex_val >> 24) & 0xFF) as f32 / 255.0;
+    let g = ((hex_val >> 16) & 0xFF) as f32 / 255.0;
+    let b = ((hex_val >> 8) & 0xFF) as f32 / 255.0;
+    let a = (hex_val & 0xFF) as f32 / 255.0;
+
+    let srgba = Srgba::new(r, g, b, a);
+    let hsl = Hsl::from_color(srgba);
+    let hsla = Hsla::from(hsl).with_alpha(a);
+
+    Ok(hsla)
+}
+
+/// Mixes two [`palette::Hsl`] colors at the given `mix_ratio`.
+pub fn hsl_mix(hsla_1: Hsl, hsla_2: Hsl, mix_ratio: f32) -> Hsl {
+    hsla_1.mix(hsla_2, mix_ratio).into()
+}
+
+/// Represents a color
+/// An interstitial state used to provide a consistent API for colors
+/// with additional functionality like color mixing, blending, etc.
+///
+/// Does not return [gpui] colors as the `color` crate does not
+/// depend on [gpui].
+#[derive(Debug, Copy, Clone)]
+pub struct Color {
+    value: Hsla,
+}
+
+impl Color {
+    /// Creates a new [`Color`]
+    pub fn new(hue: f32, saturation: f32, lightness: f32) -> Self {
+        let hsl = hsl(hue, saturation, lightness);
+
+        Self { value: hsl.into() }
+    }
+
+    /// Creates a new [`Color`] with an alpha value.
+    pub fn from_hsla(hue: f32, saturation: f32, lightness: f32, alpha: f32) -> Self {
+        Self {
+            value: Hsla::new(hue, saturation, lightness, alpha),
+        }
+    }
+
+    /// Returns the [`palette::Hsla`] value of this color.
+    pub fn value(&self) -> Hsla {
+        self.value
+    }
+
+    /// Mixes this color with another [`palette::Hsl`] color at the given `mix_ratio`.
+    pub fn mix(&self, other: Hsl, mix_ratio: f32) -> Self {
+        let mixed = self.value.mix(other.into(), mix_ratio);
+
+        Self {
+            value: mixed.into(),
+        }
+    }
+}

crates/zed/Cargo.toml 🔗

@@ -29,6 +29,7 @@ command_palette = { path = "../command_palette" }
 # component_test = { path = "../component_test" }
 client = { path = "../client" }
 # clock = { path = "../clock" }
+color = { path = "../color" }
 copilot = { path = "../copilot" }
 copilot_ui = { path = "../copilot_ui" }
 diagnostics = { path = "../diagnostics" }