use gpui::{Hsla, Rgba, rgb, rgba}; pub trait Mix { fn mix(&self, to: &Self, t: f32) -> Self; } impl Mix for Hsla { fn mix(&self, to: &Self, t: f32) -> Self { if t <= 0.0 { return *self; } if t >= 1.0 { return *to; } let (a, b) = (self.to_rgb(), to.to_rgb()); let lerp = |x: f32, y: f32| x + (y - x) * t; Rgba { r: lerp(a.r, b.r), g: lerp(a.g, b.g), b: lerp(a.b, b.b), a: lerp(a.a, b.a), } .into() } } impl Mix for [Hsla; N] { fn mix(&self, to: &Self, t: f32) -> Self { std::array::from_fn(|i| self[i].mix(&to[i], t)) } } macro_rules! mixable { ($(#[$meta:meta])* $name:ident { $($field:ident: $ty:ty),* $(,)? }) => { $(#[$meta])* #[derive(Clone, Debug, PartialEq)] pub struct $name { $(pub $field: $ty,)* } impl Mix for $name { fn mix(&self, to: &Self, t: f32) -> Self { Self { $($field: self.$field.mix(&to.$field, t),)* } } } }; } mixable!( /// Code token colors. Syntax { keyword: Hsla, string: Hsla, number: Hsla, comment: Hsla, function: Hsla, type_name: Hsla, constant: Hsla, property: Hsla, tag: Hsla, attribute: Hsla, operator: Hsla, punctuation: Hsla, variable: Hsla, } ); mixable!( /// Semantic colors. Components read these, never literals. Palette { bg: Hsla, surface: Hsla, sunken: Hsla, overlay: Hsla, hover: Hsla, active: Hsla, border: Hsla, border_strong: Hsla, fg: Hsla, fg_muted: Hsla, fg_subtle: Hsla, fg_disabled: Hsla, accent: Hsla, accent_hover: Hsla, on_accent: Hsla, on_media: Hsla, focus: Hsla, link: Hsla, selection: Hsla, success: Hsla, warning: Hsla, danger: Hsla, info: Hsla, success_subtle: Hsla, warning_subtle: Hsla, danger_subtle: Hsla, info_subtle: Hsla, backdrop: Hsla, media_backdrop: Hsla, shimmer: Hsla, shadow: Hsla, tooltip_bg: Hsla, tooltip_fg: Hsla, paper: Hsla, ink: Hsla, chart: [Hsla; 8], ansi: [Hsla; 16], syntax: Syntax, } ); fn c(hex: u32) -> Hsla { rgb(hex).into() } fn c8(hex: [u32; N]) -> [Hsla; N] { hex.map(c) } impl Palette { /// The chart color at `hue`; a hue past the chart fails, naming `owner`. pub fn hue(&self, hue: usize, owner: impl std::fmt::Display) -> Hsla { *self .chart .get(hue) .unwrap_or_else(|| panic!("{owner}: no chart hue {hue}")) } pub fn light(high_contrast: bool) -> Self { let mut palette = Self { bg: c(0xfcfaf7), surface: c(0xffffff), sunken: c(0xf6f4f1), overlay: c(0xffffff), hover: c(0xf2f0ed), active: c(0xe9e7e5), border: c(0xe1dfdd), border_strong: c(0xcccac8), fg: c(0x181613), fg_muted: c(0x605d5a), fg_subtle: c(0x82807d), fg_disabled: c(0xacaaa8), accent: c(0x181613), accent_hover: c(0x302d2b), on_accent: c(0xfcfaf7), on_media: c(0xffffff), focus: c(0x3772bb), link: c(0x2863ab), selection: rgba(0x3772bb33).into(), success: c(0x267b4c), warning: c(0xa25f12), danger: c(0xba3e38), info: c(0x2e69b2), success_subtle: c(0xe7f9ec), warning_subtle: c(0xfef2dd), danger_subtle: c(0xffefec), info_subtle: c(0xebf4ff), backdrop: rgba(0x18161352).into(), media_backdrop: rgba(0x0e0d0bf8).into(), shimmer: rgba(0xffffffb3).into(), shadow: c(0x181613), tooltip_bg: c(0x181613), tooltip_fg: c(0xfcfaf7), paper: c(0xffffff), ink: c(0x181613), chart: c8([ 0x5182c1, 0x009589, 0xaa732b, 0xb8636b, 0x8572bb, 0x539156, 0xb66946, 0x138db1, ]), ansi: c8([ 0x181613, 0xa34942, 0x287c42, 0x8c5f00, 0x346aac, 0x875093, 0x007c84, 0xacaaa8, 0x605d5a, 0xbd6158, 0x439458, 0xa57710, 0x4c82c6, 0xa067ac, 0x00959c, 0xcccac8, ]), syntax: Syntax { keyword: c(0x714ca6), string: c(0x317a45), number: c(0xb3621e), comment: c(0x82807d), function: c(0x2863ab), type_name: c(0x1e7777), constant: c(0xb1582b), property: c(0xa14759), tag: c(0xa14759), attribute: c(0xa25f12), operator: c(0x605d5a), punctuation: c(0x605d5a), variable: c(0x181613), }, }; if high_contrast { palette.fg_muted = c(0x44423f); palette.fg_subtle = c(0x575552); palette.border = c(0xa6a4a2); palette.border_strong = c(0x82807d); } palette } pub fn dark(high_contrast: bool) -> Self { let mut palette = Self { bg: c(0x131110), surface: c(0x1b1917), sunken: c(0x0e0d0b), overlay: c(0x201f1d), hover: c(0x252422), active: c(0x2c2b29), border: c(0x2c2b29), border_strong: c(0x413f3d), fg: c(0xf3f1f0), fg_muted: c(0xafadab), fg_subtle: c(0x858380), fg_disabled: c(0x5c5a58), accent: c(0xf3f1f0), accent_hover: c(0xd9d7d5), on_accent: c(0x131110), on_media: c(0xffffff), focus: c(0x69a1e8), link: c(0x7eb1f3), selection: rgba(0x69a1e84d).into(), success: c(0x6bbc89), warning: c(0xe8b45e), danger: c(0xe5756e), info: c(0x77abec), success_subtle: c(0x182c1f), warning_subtle: c(0x342611), danger_subtle: c(0x3a1d1b), info_subtle: c(0x19273a), backdrop: rgba(0x00000080).into(), media_backdrop: rgba(0x000000f8).into(), shimmer: rgba(0xffffff10).into(), shadow: c(0x000000), tooltip_bg: c(0x373533), tooltip_fg: c(0xf3f1f0), paper: c(0xf3f1f0), ink: c(0x181613), chart: c8([ 0x79a7e2, 0x4eb9ad, 0xcd995c, 0xdc8a90, 0xa998dd, 0x7cb57d, 0xd9906f, 0x56b2d4, ]), ansi: c8([ 0x2c2b29, 0xe1897f, 0x6fb880, 0xc99d4e, 0x74a7e8, 0xc38ecf, 0x35b9c0, 0xafadab, 0x5c5a58, 0xfda297, 0x89d298, 0xe3b667, 0x8dc1ff, 0xdda7ea, 0x56d3da, 0xf3f1f0, ]), syntax: Syntax { keyword: c(0xbba3e8), string: c(0x8ac596), number: c(0xeaab78), comment: c(0x858380), function: c(0x85b6e9), type_name: c(0x84c9c8), constant: c(0xeda382), property: c(0xe29fa9), tag: c(0xe29fa9), attribute: c(0xe1b671), operator: c(0xafadab), punctuation: c(0xafadab), variable: c(0xf3f1f0), }, }; if high_contrast { palette.fg_muted = c(0xcfcdcb); palette.fg_subtle = c(0xafadab); palette.border = c(0x6a6966); palette.border_strong = c(0x858380); } palette } } #[cfg(test)] mod tests { use super::*; #[test] fn mix_hits_both_ends_and_the_middle() { let (black, white) = (c(0x000000), c(0xffffff)); assert_eq!(black.mix(&white, 0.0).to_rgb().r, 0.0); assert_eq!(black.mix(&white, 1.0).to_rgb().r, 1.0); assert!((black.mix(&white, 0.5).to_rgb().g - 0.5).abs() < 1e-4); } #[test] fn palette_mix_moves_every_field() { let (light, dark) = (Palette::light(false), Palette::dark(false)); assert_eq!(light.mix(&dark, 1.0), dark); assert_eq!(light.mix(&dark, 0.0), light); } #[test] fn high_contrast_strengthens_borders() { let (base, hc) = (Palette::light(false), Palette::light(true)); assert!(hc.border.l < base.border.l); let (base, hc) = (Palette::dark(false), Palette::dark(true)); assert!(hc.border.l > base.border.l); } }