Skip to main content

gpui_kit_material_color/dynamic/
seed.rs

1//! A seed color and style as a complete [`Scheme`] for the whole application.
2//!
3//! Every role material-color-utilities defines comes from it unchanged. The
4//! status roles it does not define (`SUCCESS`, `WARNING`, `INFO` and their
5//! on-roles) follow Material's custom-color recipe: the baseline theme's status
6//! color harmonized toward the seed, at the tone of an accent, held to the same
7//! contrast curves as the accents. The kit's application extensions are derived
8//! as for reference themes.
9use super::blend::harmonize;
10use super::color::{ContrastCurve, DynamicRole, foreground_tone, with_tone_memo};
11use super::contrast::ratio_of_tones;
12use super::hct::Hct;
13use super::palette::TonalPalette;
14use super::scheme::{DynamicScheme, SpecVersion, Variant};
15use crate::scheme::Scheme;
16use crate::themes::{Contrast, REFERENCE_ROLES, ThemeId, ThemeMode};
17use crate::{Color, ColorRole};
18
19/// What a generated theme is made from.
20///
21/// ```
22/// use gpui_kit_material_color::dynamic::{SeedScheme, Variant};
23/// use gpui_kit_material_color::{Color, ColorRole};
24///
25/// let scheme = SeedScheme::new(0x6750a4).variant(Variant::TonalSpot).scheme();
26/// assert_eq!(scheme.get(ColorRole::PRIMARY), Color::hex(0x65558f));
27/// assert!(!scheme.is_dark());
28/// ```
29#[derive(Clone, Copy, Debug, PartialEq)]
30pub struct SeedScheme {
31    /// The seed as `0xRRGGBB`.
32    pub seed: u32,
33    /// The style.
34    pub variant: Variant,
35    /// Dark mode.
36    pub dark: bool,
37    /// -1 is minimum contrast, 0 standard, 0.5 medium, 1 high.
38    pub contrast: f64,
39    /// The color spec. Variants without a 2025 spec resolve as 2021.
40    pub spec: SpecVersion,
41}
42
43impl SeedScheme {
44    /// A light tonal-spot scheme at standard contrast on the 2021 spec.
45    pub const fn new(seed: u32) -> Self {
46        Self {
47            seed: seed & 0xff_ffff,
48            variant: Variant::TonalSpot,
49            dark: false,
50            contrast: 0.,
51            spec: SpecVersion::Spec2021,
52        }
53    }
54
55    /// Set the style.
56    pub const fn variant(mut self, variant: Variant) -> Self {
57        self.variant = variant;
58        self
59    }
60
61    /// Set the mode.
62    pub const fn dark(mut self, dark: bool) -> Self {
63        self.dark = dark;
64        self
65    }
66
67    /// Set the contrast level, from -1 to 1.
68    pub const fn contrast(mut self, contrast: f64) -> Self {
69        self.contrast = contrast;
70        self
71    }
72
73    /// Set the color spec.
74    pub const fn spec(mut self, spec: SpecVersion) -> Self {
75        self.spec = spec;
76        self
77    }
78
79    /// Parse `#RRGGBB`, `RRGGBB`, `#RGB` or `RGB`.
80    pub fn parse_hex(text: &str) -> Option<u32> {
81        let digits = text.trim();
82        let digits = digits.strip_prefix('#').unwrap_or(digits);
83        if !digits.chars().all(|digit| digit.is_ascii_hexdigit()) {
84            return None;
85        }
86        let value = u32::from_str_radix(digits, 16).ok()?;
87        match digits.len() {
88            6 => Some(value),
89            3 => {
90                let double = |nibble: u32| (nibble & 0xf) * 0x11;
91                Some(double(value >> 8) << 16 | double(value >> 4) << 8 | double(value))
92            }
93            _ => None,
94        }
95    }
96
97    /// The resolved palettes and roles.
98    pub fn dynamic(&self) -> DynamicScheme {
99        DynamicScheme::new(
100            Hct::from_argb(0xff00_0000 | self.seed),
101            self.variant,
102            self.dark,
103            self.contrast,
104            self.spec,
105        )
106    }
107
108    /// Every [`ColorRole`] of the generated theme.
109    pub fn scheme(&self) -> Scheme {
110        let dynamic = self.dynamic();
111        let colors = with_tone_memo(|| {
112            REFERENCE_ROLES.map(|role| {
113                let argb = match upstream_role(role) {
114                    Some(upstream) => dynamic.argb(upstream),
115                    None => self.status(&dynamic, role),
116                };
117                argb & 0xff_ffff
118            })
119        });
120        Scheme::from_reference(&colors, self.dark)
121    }
122
123    fn status(&self, dynamic: &DynamicScheme, role: ColorRole) -> u32 {
124        let (source_role, foreground) = match role {
125            ColorRole::SUCCESS => (ColorRole::SUCCESS, false),
126            ColorRole::ON_SUCCESS => (ColorRole::SUCCESS, true),
127            ColorRole::WARNING => (ColorRole::WARNING, false),
128            ColorRole::ON_WARNING => (ColorRole::WARNING, true),
129            ColorRole::ON_INFO => (ColorRole::INFO, true),
130            _ => (ColorRole::INFO, false),
131        };
132        let baseline = ThemeId::Baseline.scheme(ThemeMode::Light, Contrast::Standard);
133        let palette = TonalPalette::from_argb(harmonize(
134            argb(baseline.get(source_role)),
135            0xff00_0000 | self.seed,
136        ));
137        // The accents' tones and contrast curves in the 2021 spec.
138        let surface = if self.dark {
139            DynamicRole::SurfaceBright
140        } else {
141            DynamicRole::SurfaceDim
142        };
143        let mut tone = if self.dark { 80. } else { 40. };
144        let needed = ContrastCurve::new(3., 4.5, 7., 7.).get(dynamic.contrast_level);
145        let surface_tone = dynamic.tone(surface);
146        if ratio_of_tones(surface_tone, tone) < needed {
147            tone = foreground_tone(surface_tone, needed);
148        }
149        if !foreground {
150            return palette.tone(tone);
151        }
152        let mut on_tone = if self.dark { 20. } else { 100. };
153        let needed = ContrastCurve::new(4.5, 7., 11., 21.).get(dynamic.contrast_level);
154        if ratio_of_tones(tone, on_tone) < needed {
155            on_tone = foreground_tone(tone, needed);
156        }
157        palette.tone(on_tone)
158    }
159}
160
161fn argb(color: Color) -> u32 {
162    let channel = |value: f32| (value.clamp(0., 1.) * 255.).round() as u32;
163    0xff00_0000 | channel(color.r) << 16 | channel(color.g) << 8 | channel(color.b)
164}
165
166/// The upstream role of a reference role; `None` for the status roles.
167fn upstream_role(role: ColorRole) -> Option<DynamicRole> {
168    use DynamicRole as D;
169    Some(match role {
170        ColorRole::ERROR => D::Error,
171        ColorRole::ERROR_CONTAINER => D::ErrorContainer,
172        ColorRole::INVERSE_ON_SURFACE => D::InverseOnSurface,
173        ColorRole::INVERSE_PRIMARY => D::InversePrimary,
174        ColorRole::INVERSE_SURFACE => D::InverseSurface,
175        ColorRole::ON_ERROR => D::OnError,
176        ColorRole::ON_ERROR_CONTAINER => D::OnErrorContainer,
177        ColorRole::ON_PRIMARY => D::OnPrimary,
178        ColorRole::ON_PRIMARY_CONTAINER => D::OnPrimaryContainer,
179        ColorRole::ON_PRIMARY_FIXED => D::OnPrimaryFixed,
180        ColorRole::ON_PRIMARY_FIXED_VARIANT => D::OnPrimaryFixedVariant,
181        ColorRole::ON_SECONDARY => D::OnSecondary,
182        ColorRole::ON_SECONDARY_CONTAINER => D::OnSecondaryContainer,
183        ColorRole::ON_SECONDARY_FIXED => D::OnSecondaryFixed,
184        ColorRole::ON_SECONDARY_FIXED_VARIANT => D::OnSecondaryFixedVariant,
185        ColorRole::ON_SURFACE => D::OnSurface,
186        ColorRole::ON_SURFACE_VARIANT => D::OnSurfaceVariant,
187        ColorRole::ON_TERTIARY => D::OnTertiary,
188        ColorRole::ON_TERTIARY_CONTAINER => D::OnTertiaryContainer,
189        ColorRole::ON_TERTIARY_FIXED => D::OnTertiaryFixed,
190        ColorRole::ON_TERTIARY_FIXED_VARIANT => D::OnTertiaryFixedVariant,
191        ColorRole::OUTLINE => D::Outline,
192        ColorRole::OUTLINE_VARIANT => D::OutlineVariant,
193        ColorRole::PRIMARY => D::Primary,
194        ColorRole::PRIMARY_CONTAINER => D::PrimaryContainer,
195        ColorRole::PRIMARY_FIXED => D::PrimaryFixed,
196        ColorRole::PRIMARY_FIXED_DIM => D::PrimaryFixedDim,
197        ColorRole::SCRIM => D::Scrim,
198        ColorRole::SECONDARY => D::Secondary,
199        ColorRole::SECONDARY_CONTAINER => D::SecondaryContainer,
200        ColorRole::SECONDARY_FIXED => D::SecondaryFixed,
201        ColorRole::SECONDARY_FIXED_DIM => D::SecondaryFixedDim,
202        ColorRole::SHADOW => D::Shadow,
203        ColorRole::SURFACE => D::Surface,
204        ColorRole::SURFACE_BRIGHT => D::SurfaceBright,
205        ColorRole::SURFACE_CONTAINER => D::SurfaceContainer,
206        ColorRole::SURFACE_CONTAINER_HIGH => D::SurfaceContainerHigh,
207        ColorRole::SURFACE_CONTAINER_HIGHEST => D::SurfaceContainerHighest,
208        ColorRole::SURFACE_CONTAINER_LOW => D::SurfaceContainerLow,
209        ColorRole::SURFACE_CONTAINER_LOWEST => D::SurfaceContainerLowest,
210        ColorRole::SURFACE_DIM => D::SurfaceDim,
211        ColorRole::SURFACE_VARIANT => D::SurfaceVariant,
212        ColorRole::TERTIARY => D::Tertiary,
213        ColorRole::TERTIARY_CONTAINER => D::TertiaryContainer,
214        ColorRole::TERTIARY_FIXED => D::TertiaryFixed,
215        ColorRole::TERTIARY_FIXED_DIM => D::TertiaryFixedDim,
216        _ => return None,
217    })
218}