- Add `mix` method to mix two color. - Export `ui::theme::*` to crate root, before `use ui:theme::ActiveTheme` to `use ui::ActiveTheme`. ## Break Changes - Moved `ui::theme::Colorize` to `ui::Colorize`. - Merged `ColorExt` to `Colorize`. - Renamed `to_hex_string` to `to_hex`, `parse_hex_string` to `parse_hex`.
429 lines
12 KiB
Rust
429 lines
12 KiB
Rust
use std::collections::HashMap;
|
|
|
|
use gpui::Hsla;
|
|
use serde::{de::Error, Deserialize, Deserializer};
|
|
|
|
use crate::hsl;
|
|
use anyhow::Result;
|
|
|
|
pub trait Colorize: Sized {
|
|
/// Returns a new color with the given opacity.
|
|
///
|
|
/// The opacity is a value between 0.0 and 1.0, where 0.0 is fully transparent and 1.0 is fully opaque.
|
|
fn opacity(&self, opacity: f32) -> Self;
|
|
/// Returns a new color with each channel divided by the given divisor.
|
|
///
|
|
/// The divisor in range of 0.0 .. 1.0
|
|
fn divide(&self, divisor: f32) -> Self;
|
|
/// Return inverted color
|
|
fn invert(&self) -> Self;
|
|
/// Return inverted lightness
|
|
fn invert_l(&self) -> Self;
|
|
/// Return a new color with the lightness increased by the given factor.
|
|
///
|
|
/// factor range: 0.0 .. 1.0
|
|
fn lighten(&self, amount: f32) -> Self;
|
|
/// Return a new color with the darkness increased by the given factor.
|
|
///
|
|
/// factor range: 0.0 .. 1.0
|
|
fn darken(&self, amount: f32) -> Self;
|
|
/// Return a new color with the same lightness and alpha but different hue and saturation.
|
|
fn apply(&self, base_color: Self) -> Self;
|
|
|
|
/// Mix two colors together, the `factor` is a value between 0.0 and 1.0 for first color.
|
|
fn mix(&self, other: Self, factor: f32) -> Self;
|
|
|
|
/// Convert the color to a hex string. For example, "#F8FAFC".
|
|
fn to_hex(&self) -> String;
|
|
/// Parse a hex string to a color.
|
|
fn parse_hex(hex: &str) -> Result<Self>;
|
|
}
|
|
|
|
impl Colorize for Hsla {
|
|
fn opacity(&self, factor: f32) -> Self {
|
|
Self {
|
|
a: self.a * factor.clamp(0.0, 1.0),
|
|
..*self
|
|
}
|
|
}
|
|
|
|
fn divide(&self, divisor: f32) -> Self {
|
|
Self {
|
|
a: divisor,
|
|
..*self
|
|
}
|
|
}
|
|
|
|
fn invert(&self) -> Self {
|
|
Self {
|
|
h: 1.0 - self.h,
|
|
s: 1.0 - self.s,
|
|
l: 1.0 - self.l,
|
|
a: self.a,
|
|
}
|
|
}
|
|
|
|
fn invert_l(&self) -> Self {
|
|
Self {
|
|
l: 1.0 - self.l,
|
|
..*self
|
|
}
|
|
}
|
|
|
|
fn lighten(&self, factor: f32) -> Self {
|
|
let l = self.l * (1.0 + factor.clamp(0.0, 1.0));
|
|
|
|
Hsla { l, ..*self }
|
|
}
|
|
|
|
fn darken(&self, factor: f32) -> Self {
|
|
let l = self.l * (1.0 - factor.clamp(0.0, 1.0));
|
|
|
|
Self { l, ..*self }
|
|
}
|
|
|
|
fn apply(&self, new_color: Self) -> Self {
|
|
Hsla {
|
|
h: new_color.h,
|
|
s: new_color.s,
|
|
l: self.l,
|
|
a: self.a,
|
|
}
|
|
}
|
|
|
|
/// Reference:
|
|
/// https://github.com/bevyengine/bevy/blob/85eceb022da0326b47ac2b0d9202c9c9f01835bb/crates/bevy_color/src/hsla.rs#L112
|
|
fn mix(&self, other: Self, factor: f32) -> Self {
|
|
let factor = factor.clamp(0.0, 1.0);
|
|
let inv = 1.0 - factor;
|
|
|
|
#[inline]
|
|
fn lerp_hue(a: f32, b: f32, t: f32) -> f32 {
|
|
let diff = (b - a + 180.0).rem_euclid(360.) - 180.;
|
|
(a + diff * t).rem_euclid(360.0)
|
|
}
|
|
|
|
Hsla {
|
|
h: lerp_hue(self.h * 360., other.h * 360., factor) / 360.,
|
|
s: self.s * factor + other.s * inv,
|
|
l: self.l * factor + other.l * inv,
|
|
a: self.a * factor + other.a * inv,
|
|
}
|
|
}
|
|
|
|
fn to_hex(&self) -> String {
|
|
let rgb = self.to_rgb();
|
|
|
|
if rgb.a < 1. {
|
|
return format!(
|
|
"#{:02X}{:02X}{:02X}{:02X}",
|
|
((rgb.r * 255.) as u32),
|
|
((rgb.g * 255.) as u32),
|
|
((rgb.b * 255.) as u32),
|
|
((self.a * 255.) as u32)
|
|
);
|
|
}
|
|
|
|
format!(
|
|
"#{:02X}{:02X}{:02X}",
|
|
((rgb.r * 255.) as u32),
|
|
((rgb.g * 255.) as u32),
|
|
((rgb.b * 255.) as u32)
|
|
)
|
|
}
|
|
|
|
fn parse_hex(hex: &str) -> Result<Self> {
|
|
let hex = hex.trim_start_matches('#');
|
|
let len = hex.len();
|
|
if len != 6 && len != 8 {
|
|
return Err(anyhow::anyhow!("invalid hex color"));
|
|
}
|
|
|
|
let r = u8::from_str_radix(&hex[0..2], 16)? as f32 / 255.;
|
|
let g = u8::from_str_radix(&hex[2..4], 16)? as f32 / 255.;
|
|
let b = u8::from_str_radix(&hex[4..6], 16)? as f32 / 255.;
|
|
let a = if len == 8 {
|
|
u8::from_str_radix(&hex[6..8], 16)? as f32 / 255.
|
|
} else {
|
|
1.
|
|
};
|
|
|
|
let v = gpui::Rgba { r, g, b, a };
|
|
let color: Hsla = v.into();
|
|
Ok(color)
|
|
}
|
|
}
|
|
|
|
pub(crate) static DEFAULT_COLOR: once_cell::sync::Lazy<ShadcnColors> =
|
|
once_cell::sync::Lazy::new(|| {
|
|
serde_json::from_str(include_str!("../default-colors.json"))
|
|
.expect("failed to parse default-json")
|
|
});
|
|
|
|
type ColorScales = HashMap<usize, ShadcnColor>;
|
|
|
|
mod color_scales {
|
|
use std::collections::HashMap;
|
|
|
|
use super::{ColorScales, ShadcnColor};
|
|
|
|
use serde::de::{Deserialize, Deserializer};
|
|
|
|
pub fn deserialize<'de, D>(deserializer: D) -> Result<ColorScales, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let mut map = HashMap::new();
|
|
for color in Vec::<ShadcnColor>::deserialize(deserializer)? {
|
|
map.insert(color.scale, color);
|
|
}
|
|
Ok(map)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize)]
|
|
pub(crate) struct ShadcnColors {
|
|
pub(crate) black: ShadcnColor,
|
|
pub(crate) white: ShadcnColor,
|
|
#[serde(with = "color_scales")]
|
|
pub(crate) slate: ColorScales,
|
|
#[serde(with = "color_scales")]
|
|
pub(crate) gray: ColorScales,
|
|
#[serde(with = "color_scales")]
|
|
pub(crate) zinc: ColorScales,
|
|
#[serde(with = "color_scales")]
|
|
pub(crate) neutral: ColorScales,
|
|
#[serde(with = "color_scales")]
|
|
pub(crate) stone: ColorScales,
|
|
#[serde(with = "color_scales")]
|
|
pub(crate) red: ColorScales,
|
|
#[serde(with = "color_scales")]
|
|
pub(crate) orange: ColorScales,
|
|
#[serde(with = "color_scales")]
|
|
pub(crate) amber: ColorScales,
|
|
#[serde(with = "color_scales")]
|
|
pub(crate) yellow: ColorScales,
|
|
#[serde(with = "color_scales")]
|
|
pub(crate) lime: ColorScales,
|
|
#[serde(with = "color_scales")]
|
|
pub(crate) green: ColorScales,
|
|
#[serde(with = "color_scales")]
|
|
pub(crate) emerald: ColorScales,
|
|
#[serde(with = "color_scales")]
|
|
pub(crate) teal: ColorScales,
|
|
#[serde(with = "color_scales")]
|
|
pub(crate) cyan: ColorScales,
|
|
#[serde(with = "color_scales")]
|
|
pub(crate) sky: ColorScales,
|
|
#[serde(with = "color_scales")]
|
|
pub(crate) blue: ColorScales,
|
|
#[serde(with = "color_scales")]
|
|
pub(crate) indigo: ColorScales,
|
|
#[serde(with = "color_scales")]
|
|
pub(crate) violet: ColorScales,
|
|
#[serde(with = "color_scales")]
|
|
pub(crate) purple: ColorScales,
|
|
#[serde(with = "color_scales")]
|
|
pub(crate) fuchsia: ColorScales,
|
|
#[serde(with = "color_scales")]
|
|
pub(crate) pink: ColorScales,
|
|
#[serde(with = "color_scales")]
|
|
pub(crate) rose: ColorScales,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Deserialize)]
|
|
pub(crate) struct ShadcnColor {
|
|
#[serde(default)]
|
|
pub(crate) scale: usize,
|
|
#[serde(deserialize_with = "from_hsl_channel", alias = "hslChannel")]
|
|
pub(crate) hsla: Hsla,
|
|
}
|
|
|
|
/// Deserialize Hsla from a string in the format "210 40% 98%"
|
|
fn from_hsl_channel<'de, D>(deserializer: D) -> Result<Hsla, D::Error>
|
|
where
|
|
D: Deserializer<'de>,
|
|
{
|
|
let s: String = Deserialize::deserialize(deserializer).unwrap();
|
|
|
|
let mut parts = s.split_whitespace();
|
|
if parts.clone().count() != 3 {
|
|
return Err(D::Error::custom(
|
|
"expected hslChannel has 3 parts, e.g: '210 40% 98%'",
|
|
));
|
|
}
|
|
|
|
fn parse_number(s: &str) -> f32 {
|
|
s.trim_end_matches('%')
|
|
.parse()
|
|
.expect("failed to parse number")
|
|
}
|
|
|
|
let (h, s, l) = (
|
|
parse_number(parts.next().unwrap()),
|
|
parse_number(parts.next().unwrap()),
|
|
parse_number(parts.next().unwrap()),
|
|
);
|
|
|
|
Ok(hsl(h, s, l))
|
|
}
|
|
|
|
macro_rules! color_method {
|
|
($color:tt, $scale:tt) => {
|
|
paste::paste! {
|
|
#[inline]
|
|
#[allow(unused)]
|
|
pub fn [<$color _ $scale>]() -> Hsla {
|
|
if let Some(color) = DEFAULT_COLOR.$color.get(&($scale as usize)) {
|
|
return color.hsla;
|
|
}
|
|
|
|
black()
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
macro_rules! color_methods {
|
|
($color:tt) => {
|
|
paste::paste! {
|
|
/// Get color by scale number.
|
|
///
|
|
/// The possible scale numbers are:
|
|
/// 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950
|
|
///
|
|
/// If the scale number is not found, it will return black color.
|
|
#[inline]
|
|
pub fn [<$color>](scale: usize) -> Hsla {
|
|
if let Some(color) = DEFAULT_COLOR.$color.get(&scale) {
|
|
return color.hsla;
|
|
}
|
|
|
|
black()
|
|
}
|
|
}
|
|
|
|
color_method!($color, 50);
|
|
color_method!($color, 100);
|
|
color_method!($color, 200);
|
|
color_method!($color, 300);
|
|
color_method!($color, 400);
|
|
color_method!($color, 500);
|
|
color_method!($color, 600);
|
|
color_method!($color, 700);
|
|
color_method!($color, 800);
|
|
color_method!($color, 900);
|
|
color_method!($color, 950);
|
|
};
|
|
}
|
|
|
|
pub fn black() -> Hsla {
|
|
DEFAULT_COLOR.black.hsla
|
|
}
|
|
|
|
pub fn white() -> Hsla {
|
|
DEFAULT_COLOR.white.hsla
|
|
}
|
|
|
|
color_methods!(slate);
|
|
color_methods!(gray);
|
|
color_methods!(zinc);
|
|
color_methods!(neutral);
|
|
color_methods!(stone);
|
|
color_methods!(red);
|
|
color_methods!(orange);
|
|
color_methods!(amber);
|
|
color_methods!(yellow);
|
|
color_methods!(lime);
|
|
color_methods!(green);
|
|
color_methods!(emerald);
|
|
color_methods!(teal);
|
|
color_methods!(cyan);
|
|
color_methods!(sky);
|
|
color_methods!(blue);
|
|
color_methods!(indigo);
|
|
color_methods!(violet);
|
|
color_methods!(purple);
|
|
color_methods!(fuchsia);
|
|
color_methods!(pink);
|
|
color_methods!(rose);
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use gpui::{rgb, rgba};
|
|
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_default_colors() {
|
|
assert_eq!(white(), hsl(0.0, 0.0, 100.0));
|
|
assert_eq!(black(), hsl(0.0, 0.0, 0.0));
|
|
|
|
assert_eq!(slate_50(), hsl(210.0, 40.0, 98.0));
|
|
assert_eq!(slate_100(), hsl(210.0, 40.0, 96.1));
|
|
assert_eq!(slate_900(), hsl(222.2, 47.4, 11.2));
|
|
|
|
assert_eq!(red_50(), hsl(0.0, 85.7, 97.3));
|
|
assert_eq!(yellow_100(), hsl(54.9, 96.7, 88.0));
|
|
assert_eq!(green_200(), hsl(141.0, 78.9, 85.1));
|
|
assert_eq!(cyan_300(), hsl(187.0, 92.4, 69.0));
|
|
assert_eq!(blue_400(), hsl(213.1, 93.9, 67.8));
|
|
assert_eq!(indigo_500(), hsl(238.7, 83.5, 66.7));
|
|
}
|
|
|
|
#[test]
|
|
fn test_to_hex_string() {
|
|
let color: Hsla = rgb(0xf8fafc).into();
|
|
assert_eq!(color.to_hex(), "#F8FAFC");
|
|
|
|
let color: Hsla = rgb(0xfef2f2).into();
|
|
assert_eq!(color.to_hex(), "#FEF2F2");
|
|
|
|
let color: Hsla = rgba(0x0413fcaa).into();
|
|
assert_eq!(color.to_hex(), "#0413FCAA");
|
|
}
|
|
|
|
#[test]
|
|
fn test_from_hex_string() {
|
|
let color: Hsla = Hsla::parse_hex("#F8FAFC").unwrap();
|
|
assert_eq!(color, rgb(0xf8fafc).into());
|
|
|
|
let color: Hsla = Hsla::parse_hex("#FEF2F2").unwrap();
|
|
assert_eq!(color, rgb(0xfef2f2).into());
|
|
|
|
let color: Hsla = Hsla::parse_hex("#0413FCAA").unwrap();
|
|
assert_eq!(color, rgba(0x0413fcaa).into());
|
|
}
|
|
|
|
#[test]
|
|
fn test_lighten() {
|
|
let color = super::hsl(240.0, 5.0, 30.0);
|
|
let color = color.lighten(0.5);
|
|
assert_eq!(color.l, 0.45000002);
|
|
let color = color.lighten(0.5);
|
|
assert_eq!(color.l, 0.675);
|
|
let color = color.lighten(0.1);
|
|
assert_eq!(color.l, 0.7425);
|
|
}
|
|
|
|
#[test]
|
|
fn test_darken() {
|
|
let color = super::hsl(240.0, 5.0, 96.0);
|
|
let color = color.darken(0.5);
|
|
assert_eq!(color.l, 0.48);
|
|
let color = color.darken(0.5);
|
|
assert_eq!(color.l, 0.24);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mix() {
|
|
let red = Hsla::parse_hex("#FF0000").unwrap();
|
|
let blue = Hsla::parse_hex("#0000FF").unwrap();
|
|
let green = Hsla::parse_hex("#00FF00").unwrap();
|
|
let yellow = Hsla::parse_hex("#FFFF00").unwrap();
|
|
|
|
assert_eq!(red.mix(blue, 0.5).to_hex(), "#FF00FF");
|
|
assert_eq!(green.mix(red, 0.5).to_hex(), "#FFFF00");
|
|
assert_eq!(blue.mix(yellow, 0.2).to_hex(), "#0098FF");
|
|
}
|
|
}
|