1use std::{fmt::Debug, str::FromStr};
2
3use crate::{
4 color::Gamut,
5 math::{normalize, FloatNumber},
6 Error,
7 Swatch,
8};
9
10#[derive(Debug, Clone, Copy, PartialEq)]
24pub enum Theme {
25 #[deprecated(since = "0.8.0", note = "Use Palette::find_swatches() instead.")]
28 Basic,
29 Colorful,
32 Vivid,
35 Muted,
38 Light,
41 Dark,
44}
45
46impl Theme {
47 const MAX_LIGHTNESS: u8 = 100;
49
50 #[inline]
61 #[must_use]
62 pub(crate) fn score<T>(&self, swatch: &Swatch<T>) -> T
63 where
64 T: FloatNumber,
65 {
66 let params = match self {
67 #[allow(deprecated)]
68 Theme::Basic => {
69 return swatch.ratio();
70 }
71 Theme::Colorful => ThemeParams {
72 mean_chroma: T::from_f64(0.75),
73 sigma_chroma: T::from_f64(0.18),
74 mean_lightness: T::from_f64(0.60),
75 sigma_lightness: T::from_f64(0.20),
76 },
77 Theme::Vivid => ThemeParams {
78 mean_chroma: T::from_f64(0.90),
79 sigma_chroma: T::from_f64(0.10),
80 mean_lightness: T::from_f64(0.70),
81 sigma_lightness: T::from_f64(0.20),
82 },
83 Theme::Muted => ThemeParams {
84 mean_chroma: T::from_f64(0.20),
85 sigma_chroma: T::from_f64(0.15),
86 mean_lightness: T::from_f64(0.40),
87 sigma_lightness: T::from_f64(0.15),
88 },
89 Theme::Light => ThemeParams {
90 mean_chroma: T::from_f64(0.60),
91 sigma_chroma: T::from_f64(0.15),
92 mean_lightness: T::from_f64(0.85),
93 sigma_lightness: T::from_f64(0.15),
94 },
95 Theme::Dark => ThemeParams {
96 mean_chroma: T::from_f64(0.25),
97 sigma_chroma: T::from_f64(0.15),
98 mean_lightness: T::from_f64(0.15),
99 sigma_lightness: T::from_f64(0.15),
100 },
101 };
102
103 let color = swatch.color();
104 let max_chroma = Gamut::default().max_chroma(color.hue());
105 let chroma = normalize(color.chroma(), T::zero(), max_chroma);
106 let lightness = normalize(
107 color.lightness(),
108 T::zero(),
109 T::from_u8(Self::MAX_LIGHTNESS),
110 );
111
112 let dc = (chroma - params.mean_chroma) / params.sigma_chroma;
113 let dl = (lightness - params.mean_lightness) / params.sigma_lightness;
114 (T::from_f64(-0.5) * (dc * dc + dl * dl)).exp()
115 }
116}
117
118impl FromStr for Theme {
119 type Err = Error;
120
121 fn from_str(s: &str) -> Result<Self, Self::Err> {
122 match s.to_lowercase().as_str() {
123 #[allow(deprecated)]
124 "basic" => Ok(Theme::Basic),
125 "colorful" => Ok(Theme::Colorful),
126 "vivid" => Ok(Theme::Vivid),
127 "muted" => Ok(Theme::Muted),
128 "light" => Ok(Theme::Light),
129 "dark" => Ok(Theme::Dark),
130 _ => Err(Error::UnsupportedTheme {
131 name: s.to_string(),
132 }),
133 }
134 }
135}
136
137#[derive(Debug, PartialEq)]
142struct ThemeParams<T>
143where
144 T: FloatNumber,
145{
146 mean_chroma: T,
148
149 sigma_chroma: T,
151
152 mean_lightness: T,
154
155 sigma_lightness: T,
157}
158
159#[cfg(test)]
160mod tests {
161 use std::str::FromStr;
162
163 use rstest::rstest;
164
165 use super::*;
166 use crate::{assert_approx_eq, color::Color};
167
168 #[test]
169 fn test_score_basic() {
170 let color: Color<f64> = Color::from_str("#ff0080").unwrap();
172 let swatch = Swatch::new(color, (32, 64), 256, 0.25);
173 let actual = Theme::Basic.score(&swatch);
174
175 assert_approx_eq!(actual, 0.25);
177 }
178
179 #[rstest]
180 #[case::black("#000000", 0.000001)]
181 #[case::gray("#808080", 0.000162)]
182 #[case::white("#ffffff", 0.000023)]
183 #[case::red("#ff0000", 0.359991)]
184 #[case::green("#00ff00", 0.145718)]
185 #[case::blue("#0000ff", 0.146085)]
186 #[case::yellow("#ffff00", 0.067976)]
187 #[case::cyan("#00ffff", 0.113646)]
188 #[case::magenta("#ff00ff", 0.381121)]
189 #[case::orange("#ff8000", 0.358194)]
190 #[case::purple("#8000ff", 0.241719)]
191 #[case::lime("#80ff00", 0.124594)]
192 fn test_score_colorful(#[case] hex: &str, #[case] expected: f64) {
193 let color: Color<f64> = Color::from_str(hex).unwrap();
195 let swatch = Swatch::new(color, (32, 64), 256, 0.5);
196 let actual = Theme::Colorful.score(&swatch);
197
198 assert_approx_eq!(actual, expected);
200 }
201
202 #[rstest]
203 #[case::black("#000000", 0.0)]
204 #[case::gray("#808080", 0.0)]
205 #[case::white("#ffffff", 0.0)]
206 #[case::red("#ff0000", 0.426884)]
207 #[case::green("#00ff00", 0.409349)]
208 #[case::blue("#0000ff", 0.102639)]
209 #[case::yellow("#ffff00", 0.241562)]
210 #[case::cyan("#00ffff", 0.347395)]
211 #[case::magenta("#ff00ff", 0.539526)]
212 #[case::orange("#ff8000", 0.599979)]
213 #[case::purple("#8000ff", 0.210622)]
214 #[case::lime("#80ff00", 0.369553)]
215 fn test_score_vivid(#[case] hex: &str, #[case] expected: f64) {
216 let color: Color<f64> = Color::from_str(hex).unwrap();
218 let swatch = Swatch::new(color, (32, 64), 256, 0.5);
219 let actual = Theme::Vivid.score(&swatch);
220
221 assert_approx_eq!(actual, expected);
223 }
224
225 #[rstest]
226 #[case::black("#000000", 0.011743)]
227 #[case::gray("#808080", 0.273211)]
228 #[case::white("#ffffff", 0.000138)]
229 #[case::red("#ff0000", 0.0)]
230 #[case::green("#00ff00", 0.0)]
231 #[case::blue("#0000ff", 0.0)]
232 #[case::yellow("#ffff00", 0.0)]
233 #[case::cyan("#00ffff", 0.0)]
234 #[case::magenta("#ff00ff", 0.0)]
235 #[case::orange("#ff8000", 0.0)]
236 #[case::purple("#8000ff", 0.0)]
237 #[case::lime("#80ff00", 0.0)]
238 fn test_score_muted(#[case] hex: &str, #[case] expected: f64) {
239 let color: Color<f64> = Color::from_str(hex).unwrap();
241 let swatch = Swatch::new(color, (32, 64), 256, 0.5);
242 let actual = Theme::Muted.score(&swatch);
243
244 assert_approx_eq!(actual, expected);
246 }
247
248 #[rstest]
249 #[case::black("#000000", 0.0)]
250 #[case::gray("#808080", 0.000037)]
251 #[case::white("#ffffff", 0.000204)]
252 #[case::red("#ff0000", 0.003035)]
253 #[case::green("#00ff00", 0.028094)]
254 #[case::blue("#0000ff", 0.000059)]
255 #[case::yellow("#ffff00", 0.020589)]
256 #[case::cyan("#00ffff", 0.026287)]
257 #[case::magenta("#ff00ff", 0.007381)]
258 #[case::orange("#ff8000", 0.013962)]
259 #[case::purple("#8000ff", 0.000380)]
260 #[case::lime("#80ff00", 0.027076)]
261 fn test_score_light(#[case] hex: &str, #[case] expected: f64) {
262 let color: Color<f64> = Color::from_str(hex).unwrap();
264 let swatch = Swatch::new(color, (32, 64), 256, 0.5);
265 let actual = Theme::Light.score(&swatch);
266
267 assert_approx_eq!(actual, expected);
269 }
270
271 #[rstest]
272 #[case::black("#000000", 0.151239)]
273 #[case::gray("#808080", 0.009136)]
274 #[case::white("#ffffff", 0.0)]
275 #[case::red("#ff0000", 0.0)]
276 #[case::green("#00ff00", 0.0)]
277 #[case::blue("#0000ff", 0.000001)]
278 #[case::yellow("#ffff00", 0.0)]
279 #[case::cyan("#00ffff", 0.0)]
280 #[case::magenta("#ff00ff", 0.0)]
281 #[case::orange("#ff8000", 0.0)]
282 #[case::purple("#8000ff", 0.0)]
283 #[case::lime("#80ff00", 0.0)]
284 fn test_score_dark(#[case] hex: &str, #[case] expected: f64) {
285 let color: Color<f64> = Color::from_str(hex).unwrap();
287 let swatch = Swatch::new(color, (32, 64), 256, 0.5);
288 let actual = Theme::Dark.score(&swatch);
289
290 assert_approx_eq!(actual, expected);
292 }
293
294 #[rstest]
295 #[case::basic("basic", Theme::Basic)]
296 #[case::colorful("colorful", Theme::Colorful)]
297 #[case::vivid("vivid", Theme::Vivid)]
298 #[case::muted("muted", Theme::Muted)]
299 #[case::light("light", Theme::Light)]
300 #[case::dark("dark", Theme::Dark)]
301 #[case::basic_upper("BASIC", Theme::Basic)]
302 #[case::colorful_upper("COLORFUL", Theme::Colorful)]
303 #[case::vivid_upper("VIVID", Theme::Vivid)]
304 #[case::muted_upper("MUTED", Theme::Muted)]
305 #[case::light_upper("LIGHT", Theme::Light)]
306 #[case::dark_upper("DARK", Theme::Dark)]
307 #[case::basic_capitalized("Basic", Theme::Basic)]
308 #[case::colorful_capitalized("Colorful", Theme::Colorful)]
309 #[case::vivid_capitalized("Vivid", Theme::Vivid)]
310 #[case::muted_capitalized("Muted", Theme::Muted)]
311 #[case::light_capitalized("Light", Theme::Light)]
312 #[case::dark_capitalized("Dark", Theme::Dark)]
313 fn test_from_str(#[case] str: &str, #[case] expected: Theme) {
314 let actual = Theme::from_str(str);
316
317 assert!(actual.is_ok());
319 assert_eq!(actual.unwrap(), expected);
320 }
321
322 #[rstest]
323 #[case::empty("")]
324 #[case::invalid("unknown")]
325 fn test_from_str_error(#[case] str: &str) {
326 let actual = Theme::from_str(str);
328
329 assert!(actual.is_err());
331 assert_eq!(
332 actual.unwrap_err().to_string(),
333 format!("Unsupported theme specified: '{}'", str),
334 );
335 }
336}