use gpui::{Hsla, Rgba, rgb, rgba};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ColorToken {
pub base: Rgba,
pub hover: Rgba,
pub active: Rgba,
pub muted: Rgba,
pub subtle: Rgba,
}
impl ColorToken {
pub fn from_base(base: Rgba) -> Self {
let hsla = Hsla::from(base);
let hover_hsla = Hsla {
h: hsla.h,
s: (hsla.s * 1.1).min(1.0), l: (hsla.l * 1.1).min(0.95), a: hsla.a,
};
let active_hsla = Hsla {
h: hsla.h,
s: hsla.s,
l: hsla.l * 0.85, a: hsla.a,
};
let muted_hsla = Hsla {
h: hsla.h,
s: hsla.s,
l: hsla.l,
a: 0.2,
};
let subtle_hsla = Hsla {
h: hsla.h,
s: hsla.s,
l: hsla.l,
a: 0.1,
};
Self {
base,
hover: hover_hsla.into(),
active: active_hsla.into(),
muted: muted_hsla.into(),
subtle: subtle_hsla.into(),
}
}
pub fn from_hex(hex: u32) -> Self {
Self::from_base(rgb(hex))
}
pub fn from_hex_alpha(hex: u32) -> Self {
Self::from_base(rgba(hex))
}
pub fn from_base_with_alpha(base: Rgba, alpha: f32) -> Self {
let base_with_alpha = Rgba {
r: base.r,
g: base.g,
b: base.b,
a: alpha,
};
Self::from_base(base_with_alpha)
}
pub fn with_alpha(self, alpha: f32) -> Self {
let base = Rgba {
r: self.base.r,
g: self.base.g,
b: self.base.b,
a: alpha,
};
Self::from_base(base)
}
pub fn lighter(self, amount: f32) -> Self {
let hsla = Hsla::from(self.base);
let lighter = Hsla {
h: hsla.h,
s: hsla.s,
l: (hsla.l + amount).min(1.0),
a: hsla.a,
};
Self::from_base(lighter.into())
}
pub fn darker(self, amount: f32) -> Self {
let hsla = Hsla::from(self.base);
let darker = Hsla {
h: hsla.h,
s: hsla.s,
l: (hsla.l - amount).max(0.0),
a: hsla.a,
};
Self::from_base(darker.into())
}
}
impl Default for ColorToken {
fn default() -> Self {
Self::from_hex(0x007acc) }
}
impl From<Rgba> for ColorToken {
fn from(color: Rgba) -> Self {
Self::from_base(color)
}
}
impl From<u32> for ColorToken {
fn from(hex: u32) -> Self {
Self::from_hex(hex)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SemanticColors {
pub primary: ColorToken,
pub secondary: ColorToken,
pub success: ColorToken,
pub warning: ColorToken,
pub error: ColorToken,
pub info: ColorToken,
}
impl SemanticColors {
pub fn new(
primary: impl Into<ColorToken>,
secondary: impl Into<ColorToken>,
success: impl Into<ColorToken>,
warning: impl Into<ColorToken>,
error: impl Into<ColorToken>,
info: impl Into<ColorToken>,
) -> Self {
Self {
primary: primary.into(),
secondary: secondary.into(),
success: success.into(),
warning: warning.into(),
error: error.into(),
info: info.into(),
}
}
pub fn dark() -> Self {
Self {
primary: ColorToken::from_hex(0x007acc),
secondary: ColorToken::from_hex(0x6c757d),
success: ColorToken::from_hex(0x22c55e),
warning: ColorToken::from_hex(0xf59e0b),
error: ColorToken::from_hex(0xef4444),
info: ColorToken::from_hex(0x3b82f6),
}
}
pub fn light() -> Self {
Self {
primary: ColorToken::from_hex(0x0066cc),
secondary: ColorToken::from_hex(0x6c757d),
success: ColorToken::from_hex(0x16a34a),
warning: ColorToken::from_hex(0xd97706),
error: ColorToken::from_hex(0xdc2626),
info: ColorToken::from_hex(0x2563eb),
}
}
}
impl Default for SemanticColors {
fn default() -> Self {
Self::dark()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct BackgroundColors {
pub page: ColorToken,
pub surface: ColorToken,
pub overlay: ColorToken,
}
impl BackgroundColors {
pub fn dark() -> Self {
Self {
page: ColorToken::from_hex(0x1e1e1e),
surface: ColorToken::from_hex(0x2a2a2a),
overlay: ColorToken::from_base_with_alpha(rgb(0x000000), 0.5),
}
}
pub fn light() -> Self {
Self {
page: ColorToken::from_hex(0xf5f5f5),
surface: ColorToken::from_hex(0xffffff),
overlay: ColorToken::from_base_with_alpha(rgb(0x000000), 0.5),
}
}
}
impl Default for BackgroundColors {
fn default() -> Self {
Self::dark()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct TextColors {
pub primary: ColorToken,
pub secondary: ColorToken,
pub muted: ColorToken,
pub inverted: ColorToken,
}
impl TextColors {
pub fn dark() -> Self {
Self {
primary: ColorToken::from_hex(0xffffff),
secondary: ColorToken::from_hex(0xcccccc),
muted: ColorToken::from_hex(0x888888),
inverted: ColorToken::from_hex(0x1a1a1a),
}
}
pub fn light() -> Self {
Self {
primary: ColorToken::from_hex(0x1a1a1a),
secondary: ColorToken::from_hex(0x4a4a4a),
muted: ColorToken::from_hex(0x888888),
inverted: ColorToken::from_hex(0xffffff),
}
}
}
impl Default for TextColors {
fn default() -> Self {
Self::dark()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct BorderColors {
pub default: ColorToken,
pub focus: ColorToken,
pub error: ColorToken,
}
impl BorderColors {
pub fn dark() -> Self {
Self {
default: ColorToken::from_hex(0x3a3a3a),
focus: ColorToken::from_hex(0x007acc),
error: ColorToken::from_hex(0xef4444),
}
}
pub fn light() -> Self {
Self {
default: ColorToken::from_hex(0xd4d4d4),
focus: ColorToken::from_hex(0x0066cc),
error: ColorToken::from_hex(0xdc2626),
}
}
}
impl Default for BorderColors {
fn default() -> Self {
Self::dark()
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ColorPalette {
pub semantic: SemanticColors,
pub backgrounds: BackgroundColors,
pub text: TextColors,
pub borders: BorderColors,
}
impl ColorPalette {
pub fn dark() -> Self {
Self {
semantic: SemanticColors::dark(),
backgrounds: BackgroundColors::dark(),
text: TextColors::dark(),
borders: BorderColors::dark(),
}
}
pub fn light() -> Self {
Self {
semantic: SemanticColors::light(),
backgrounds: BackgroundColors::light(),
text: TextColors::light(),
borders: BorderColors::light(),
}
}
}
pub fn with_alpha(color: Rgba, alpha: f32) -> Rgba {
Rgba {
r: color.r,
g: color.g,
b: color.b,
a: alpha,
}
}
pub fn lighten(color: Rgba, amount: f32) -> Rgba {
let hsla = Hsla::from(color);
Hsla {
h: hsla.h,
s: hsla.s,
l: (hsla.l + amount).min(1.0),
a: hsla.a,
}
.into()
}
pub fn darken(color: Rgba, amount: f32) -> Rgba {
let hsla = Hsla::from(color);
Hsla {
h: hsla.h,
s: hsla.s,
l: (hsla.l - amount).max(0.0),
a: hsla.a,
}
.into()
}
pub fn saturate(color: Rgba, amount: f32) -> Rgba {
let hsla = Hsla::from(color);
Hsla {
h: hsla.h,
s: (hsla.s + amount).clamp(0.0, 1.0),
l: hsla.l,
a: hsla.a,
}
.into()
}
pub fn desaturate(color: Rgba, amount: f32) -> Rgba {
saturate(color, -amount)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_token_from_hex() {
let token = ColorToken::from_hex(0x007acc);
assert_eq!(token.base.r, 0.0);
assert!(token.base.g > 0.4 && token.base.g < 0.5); assert!(token.base.b > 0.75 && token.base.b < 0.85); }
#[test]
fn test_color_token_variants_different() {
let token = ColorToken::from_hex(0x007acc);
assert_ne!(token.base, token.hover);
assert_ne!(token.base, token.active);
assert_ne!(token.base, token.muted);
assert_ne!(token.base, token.subtle);
}
#[test]
fn test_muted_has_low_alpha() {
let token = ColorToken::from_hex(0x007acc);
assert!(token.muted.a < 0.3); assert!(token.subtle.a < 0.15); }
#[test]
fn test_semantic_colors_default() {
let colors = SemanticColors::default();
assert!(colors.primary.base.b > colors.primary.base.r);
assert!(colors.success.base.g > colors.success.base.r);
assert!(colors.error.base.r > colors.error.base.g);
}
#[test]
fn test_color_palette_dark_and_light_differ() {
let dark = ColorPalette::dark();
let light = ColorPalette::light();
assert_ne!(dark.text.primary.base, light.text.primary.base);
assert_ne!(dark.backgrounds.page.base, light.backgrounds.page.base);
}
#[test]
fn test_lighter_darker() {
let token = ColorToken::from_hex(0x808080); let lighter = token.lighter(0.2);
let darker = token.darker(0.2);
let base_hsla = Hsla::from(token.base);
let lighter_hsla = Hsla::from(lighter.base);
let darker_hsla = Hsla::from(darker.base);
assert!(lighter_hsla.l > base_hsla.l);
assert!(darker_hsla.l < base_hsla.l);
}
#[test]
fn test_helper_functions() {
let color = rgb(0x808080);
let alpha_color = with_alpha(color, 0.5);
assert!((alpha_color.a - 0.5).abs() < 0.01);
let lighter_color = lighten(color, 0.1);
let hsla = Hsla::from(lighter_color);
assert!(hsla.l > 0.5);
let darker_color = darken(color, 0.1);
let hsla = Hsla::from(darker_color);
assert!(hsla.l < 0.5);
}
}