Skip to main content

gpui_kit_material_color/dynamic/
palette.rs

1// Copyright 2021 Google LLC
2// SPDX-License-Identifier: Apache-2.0
3// Ported to Rust from material-color-utilities
4// (typescript/palettes/tonal_palette.ts), modified for this crate: tones are
5// computed on request rather than cached in the palette, which keeps it `Copy`.
6// See licenses/material-color-utilities.txt.
7
8//! Tonal palettes: colors of constant hue and chroma at varying tone.
9use super::hct::Hct;
10
11/// A hue and chroma, from which any tone can be requested.
12#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct TonalPalette {
14    hue: f64,
15    chroma: f64,
16    key_color: Hct,
17}
18
19impl TonalPalette {
20    /// The hue and chroma of an ARGB color.
21    pub fn from_argb(argb: u32) -> Self {
22        Self::from_hct(Hct::from_argb(argb))
23    }
24
25    /// The hue and chroma of an HCT color, which is also the key color.
26    pub fn from_hct(hct: Hct) -> Self {
27        Self {
28            hue: hct.hue(),
29            chroma: hct.chroma(),
30            key_color: hct,
31        }
32    }
33
34    /// A hue and chroma; the key color is searched for.
35    pub fn from_hue_and_chroma(hue: f64, chroma: f64) -> Self {
36        Self {
37            hue,
38            chroma,
39            key_color: key_color(hue, chroma),
40        }
41    }
42
43    /// The palette's hue.
44    pub fn hue(&self) -> f64 {
45        self.hue
46    }
47
48    /// The palette's chroma.
49    pub fn chroma(&self) -> f64 {
50        self.chroma
51    }
52
53    /// The color that represents the palette: the tone closest to T50 that
54    /// reaches its chroma.
55    pub fn key_color(&self) -> Hct {
56        self.key_color
57    }
58
59    /// The ARGB color at `tone`, 0 to 100.
60    pub fn tone(&self, tone: f64) -> u32 {
61        // Upstream: T99 on yellow hues is the average of T98 and T100.
62        if tone == 99. && Hct::is_yellow(self.hue) {
63            average_argb(self.tone(98.), self.tone(100.))
64        } else {
65            Hct::from_hct(self.hue, self.chroma, tone).argb()
66        }
67    }
68
69    /// The HCT color at `tone`.
70    pub fn hct(&self, tone: f64) -> Hct {
71        Hct::from_argb(self.tone(tone))
72    }
73}
74
75fn average_argb(a: u32, b: u32) -> u32 {
76    let channel = |shift: u32| {
77        let sum = ((a >> shift) & 0xff) + ((b >> shift) & 0xff);
78        // Math.round((x + y) / 2): halves round up.
79        sum.div_ceil(2) << shift
80    };
81    0xff00_0000 | channel(16) | channel(8) | channel(0)
82}
83
84/// The first tone, starting from T50 where the most chroma is available on
85/// average, that matches the hue and chroma.
86fn key_color(hue: f64, requested_chroma: f64) -> Hct {
87    const MAX_CHROMA: f64 = 200.;
88    const EPSILON: f64 = 0.01;
89    const PIVOT_TONE: i32 = 50;
90    // Each tone's maximum chroma is needed up to three times per step, and a
91    // solve is the expensive part; 101 tones fit on the stack.
92    let mut cache = [f64::NAN; 102];
93    let mut max_chroma = |tone: i32| {
94        let slot = &mut cache[tone.clamp(0, 101) as usize];
95        if slot.is_nan() {
96            *slot = Hct::from_hct(hue, MAX_CHROMA, f64::from(tone)).chroma();
97        }
98        *slot
99    };
100    let (mut lower, mut upper) = (0, 100);
101    while lower < upper {
102        let mid = (lower + upper) / 2;
103        let is_ascending = max_chroma(mid) < max_chroma(mid + 1);
104        if max_chroma(mid) >= requested_chroma - EPSILON {
105            // Either side holds an answer: search the one nearer the pivot.
106            if (lower - PIVOT_TONE).abs() < (upper - PIVOT_TONE).abs() {
107                upper = mid;
108            } else {
109                if lower == mid {
110                    return Hct::from_hct(hue, requested_chroma, f64::from(lower));
111                }
112                lower = mid;
113            }
114        } else if is_ascending {
115            // Not enough chroma here: follow the direction to the peak.
116            lower = mid + 1;
117        } else {
118            // Keep mid for a potential peak.
119            upper = mid;
120        }
121    }
122    Hct::from_hct(hue, requested_chroma, f64::from(lower))
123}