diff --git a/Cargo.lock b/Cargo.lock index 42416c2f24..4c3a4e8592 100644 --- a/Cargo.lock +++ b/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" @@ -4977,6 +4999,7 @@ dependencies = [ "approx", "fast-srgb8", "palette_derive", + "phf", ] [[package]] @@ -5165,6 +5188,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" @@ -7074,6 +7139,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" @@ -7644,7 +7715,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c536faaff1a10837cfe373142583f6e27d81e96beba339147e77b67c9f260ff" dependencies = [ "float-cmp", - "siphasher", + "siphasher 0.2.3", ] [[package]] @@ -7855,6 +7926,7 @@ name = "theme" version = "0.1.0" dependencies = [ "anyhow", + "color", "fs", "gpui", "indexmap 1.9.3", @@ -8858,7 +8930,7 @@ dependencies = [ "roxmltree", "rustybuzz", "simplecss", - "siphasher", + "siphasher 0.2.3", "svgtypes", "ttf-parser 0.12.3", "unicode-bidi", @@ -9650,6 +9722,7 @@ dependencies = [ "client", "collab_ui", "collections", + "color", "command_palette", "copilot", "copilot_ui", diff --git a/crates/color/Cargo.toml b/crates/color/Cargo.toml new file mode 100644 index 0000000000..c6416f9691 --- /dev/null +++ b/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" diff --git a/crates/color/src/color.rs b/crates/color/src/color.rs new file mode 100644 index 0000000000..8529f3bc5f --- /dev/null +++ b/crates/color/src/color.rs @@ -0,0 +1,234 @@ +//! # 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. +//! +//! === In development note === +//! +//! This crate is meant to sit between gpui and the theme/ui for all the color related stuff. +//! +//! It could be folded into gpui, ui or theme potentially but for now we'll continue +//! to develop it in isolation. +//! +//! Once we have a good idea of the needs of the theme system and color in gpui in general I see 3 paths: +//! 1. Use `palette` (or another color library) directly in gpui and everywhere else, rather than rolling our own color system. +//! 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. +//! 3. Build the needed functionality into gpui and keep using it's color system everywhere. +//! +//! I'm leaning towards 2 in the short term and 1 in the long term, but we'll need to discuss it more. +//! +//! === End development note === +use palette::{ + blend::Blend, convert::FromColorUnclamped, encoding, rgb::Rgb, Clamp, Mix, Srgb, WithAlpha, +}; + +/// The types of blend modes supported +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum BlendMode { + /// Multiplies the colors, resulting in a darker color. This mode is useful for creating shadows. + Multiply, + /// Lightens the color by adding the source and destination colors. It results in a lighter color. + Screen, + /// 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. + Overlay, + /// Selects the darker of the base or blend color as the resulting color. Useful for darkening images without affecting the overall contrast. + Darken, + /// Selects the lighter of the base or blend color as the resulting color. Useful for lightening images without affecting the overall contrast. + Lighten, + /// Brightens the base color to reflect the blend color. The result is a lightened image. + Dodge, + /// Darkens the base color to reflect the blend color. The result is a darkened image. + Burn, + /// Similar to Overlay, but with a stronger effect. Hard Light can either multiply or screen colors, depending on the blend color. + HardLight, + /// A softer version of Hard Light. Soft Light either darkens or lightens colors, depending on the blend color. + SoftLight, + /// Subtracts the darker of the two constituent colors from the lighter color. Difference mode is useful for creating more vivid colors. + Difference, + /// Similar to Difference, but with a lower contrast. Exclusion mode produces an effect similar to Difference but with less intensity. + Exclusion, +} + +/// 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 { + 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::(), + 4 => { + let (rgb, alpha) = hex.split_at(3); + let rgb = rgb + .chars() + .map(|c| c.to_string().repeat(2)) + .collect::(); + 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 color = RGBAColor { r, g, b, a }; + + Ok(color) +} + +// These derives implement to and from palette's color types. +#[derive(FromColorUnclamped, WithAlpha, Debug, Clone)] +#[palette(skip_derives(Rgb), rgb_standard = "encoding::Srgb")] +pub struct RGBAColor { + r: f32, + g: f32, + b: f32, + // Let Palette know this is our alpha channel. + #[palette(alpha)] + a: f32, +} + +impl FromColorUnclamped for RGBAColor { + fn from_color_unclamped(color: RGBAColor) -> RGBAColor { + color + } +} + +impl FromColorUnclamped> for RGBAColor +where + Srgb: FromColorUnclamped>, +{ + fn from_color_unclamped(color: Rgb) -> RGBAColor { + let srgb = Srgb::from_color_unclamped(color); + RGBAColor { + r: srgb.red, + g: srgb.green, + b: srgb.blue, + a: 1.0, + } + } +} + +impl FromColorUnclamped for Rgb +where + Rgb: FromColorUnclamped, +{ + fn from_color_unclamped(color: RGBAColor) -> Self { + let srgb = Srgb::new(color.r, color.g, color.b); + Self::from_color_unclamped(srgb) + } +} + +impl Clamp for RGBAColor { + fn clamp(self) -> Self { + RGBAColor { + r: self.r.min(1.0).max(0.0), + g: self.g.min(1.0).max(0.0), + b: self.b.min(1.0).max(0.0), + a: self.a.min(1.0).max(0.0), + } + } +} + +impl RGBAColor { + /// Creates a new color from the given RGBA values. + /// + /// This color can be used to convert to any [`palette::Color`] type. + pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self { + RGBAColor { r, g, b, a } + } + + /// Returns a set of states for this color. + pub fn states(self, is_light: bool) -> ColorStates { + states_for_color(self, is_light) + } + + /// Mixes this color with another [`palette::Hsl`] color at the given `mix_ratio`. + pub fn mixed(&self, other: RGBAColor, mix_ratio: f32) -> Self { + let srgb_self = Srgb::new(self.r, self.g, self.b); + let srgb_other = Srgb::new(other.r, other.g, other.b); + + // Directly mix the colors as sRGB values + let mixed = srgb_self.mix(srgb_other, mix_ratio); + RGBAColor::from_color_unclamped(mixed) + } + + pub fn blend(&self, other: RGBAColor, blend_mode: BlendMode) -> Self { + let srgb_self = Srgb::new(self.r, self.g, self.b); + let srgb_other = Srgb::new(other.r, other.g, other.b); + + let blended = match blend_mode { + // replace hsl methods with the respective sRGB methods + BlendMode::Multiply => srgb_self.multiply(srgb_other), + _ => unimplemented!(), + }; + + Self { + r: blended.red, + g: blended.green, + b: blended.blue, + a: self.a, + } + } +} + +/// A set of colors for different states of an element. +#[derive(Debug, Clone)] +pub struct ColorStates { + /// The default color. + pub default: RGBAColor, + /// The color when the mouse is hovering over the element. + pub hover: RGBAColor, + /// The color when the mouse button is held down on the element. + pub active: RGBAColor, + /// The color when the element is focused with the keyboard. + pub focused: RGBAColor, + /// The color when the element is disabled. + pub disabled: RGBAColor, +} + +/// Returns a set of colors for different states of an element. +/// +/// todo!("This should take a theme and use appropriate colors from it") +pub fn states_for_color(color: RGBAColor, is_light: bool) -> ColorStates { + let adjustment_factor = if is_light { 0.1 } else { -0.1 }; + let hover_adjustment = 1.0 - adjustment_factor; + let active_adjustment = 1.0 - 2.0 * adjustment_factor; + let focused_adjustment = 1.0 - 3.0 * adjustment_factor; + let disabled_adjustment = 1.0 - 4.0 * adjustment_factor; + + let make_adjustment = |color: RGBAColor, adjustment: f32| -> RGBAColor { + // Adjust lightness for each state + // Note: Adjustment logic may differ; simplify as needed for sRGB + RGBAColor::new( + color.r * adjustment, + color.g * adjustment, + color.b * adjustment, + color.a, + ) + }; + + let color = color.clamp(); + + ColorStates { + default: color.clone(), + hover: make_adjustment(color.clone(), hover_adjustment), + active: make_adjustment(color.clone(), active_adjustment), + focused: make_adjustment(color.clone(), focused_adjustment), + disabled: make_adjustment(color.clone(), disabled_adjustment), + } +} diff --git a/crates/theme/Cargo.toml b/crates/theme/Cargo.toml index 1c30176b25..428bcaac10 100644 --- a/crates/theme/Cargo.toml +++ b/crates/theme/Cargo.toml @@ -34,6 +34,7 @@ story = { path = "../story", optional = true } toml.workspace = true uuid.workspace = true util = { path = "../util" } +color = {path = "../color"} itertools = { version = "0.11.0", optional = true } [dev-dependencies] diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 734c225cb1..1e21648408 100644 --- a/crates/zed/Cargo.toml +++ b/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" }