1use std::sync::Arc;
2
3use error::ThemeError;
4
5use crate::{color::RGB, map_values::map_values};
6
7pub mod error;
11
12pub trait Theme {
14 fn main_color(&self, hash: &[u8]) -> Result<RGB, ThemeError>;
16
17 fn background_color(&self, hash: &[u8]) -> Result<RGB, ThemeError>;
19}
20
21pub struct Selection {
29 main: Vec<RGB>,
32
33 background: Vec<RGB>,
36}
37
38impl Selection {
39 pub fn new(main: Vec<RGB>, background: Vec<RGB>) -> Result<Selection, ThemeError> {
44 let theme = Selection { main, background };
45 theme.validate().map(|_| theme)
46 }
47
48 fn validate(&self) -> Result<(), ThemeError> {
49 if self.main.is_empty() {
50 Err(ThemeError::ThemeValidationError(
51 "main color selection is empty".to_string(),
52 ))
53 } else if self.background.is_empty() {
54 Err(ThemeError::ThemeValidationError(
55 "background color selection is empty".to_string(),
56 ))
57 } else {
58 Ok(())
59 }
60 }
61}
62
63impl Theme for Selection {
64 fn main_color(&self, hash: &[u8]) -> Result<RGB, ThemeError> {
65 if self.main.is_empty() {
66 Err(ThemeError::ThemeValidationError(
67 "main color selection is empty".to_string(),
68 ))
69 } else {
70 let index = hash[0 % hash.len()] as usize % self.main.len();
71 Ok(self.main[index])
72 }
73 }
74
75 fn background_color(&self, hash: &[u8]) -> Result<RGB, ThemeError> {
76 if self.background.is_empty() {
77 Err(ThemeError::ThemeValidationError(
78 "background color selection is empty".to_string(),
79 ))
80 } else {
81 let index = hash[2 % hash.len()] as usize % self.background.len();
82 Ok(self.background[index])
83 }
84 }
85}
86
87pub struct HSLRange {
95 hue_min: f32,
98
99 hue_max: f32,
102
103 saturation_min: f32,
107
108 saturation_max: f32,
112
113 lightness_min: f32,
117
118 lightness_max: f32,
122
123 background: Vec<RGB>,
127}
128
129impl HSLRange {
130 pub fn new(
143 hue_min: f32,
144 hue_max: f32,
145 saturation_min: f32,
146 saturation_max: f32,
147 lightness_min: f32,
148 lightness_max: f32,
149 background: Vec<RGB>,
150 ) -> Result<HSLRange, ThemeError> {
151 let theme = HSLRange {
152 hue_min,
153 hue_max,
154 saturation_min,
155 saturation_max,
156 lightness_min,
157 lightness_max,
158 background,
159 };
160
161 theme.validate().map(|_| theme)
162 }
163
164 fn validate(&self) -> Result<(), ThemeError> {
165 if self.hue_max < self.hue_min {
166 Err(ThemeError::ThemeValidationError(
167 "hue_max must be larger than hue_min".to_string(),
168 ))
169 } else if self.saturation_max < self.saturation_min {
170 Err(ThemeError::ThemeValidationError(
171 "saturation_max must be larger than saturation_min".to_string(),
172 ))
173 } else if self.lightness_max < self.lightness_min {
174 Err(ThemeError::ThemeValidationError(
175 "lightness_max must be larger than lightness_min".to_string(),
176 ))
177 } else {
178 Ok(())
179 }
180 }
181}
182
183impl Theme for HSLRange {
184 fn main_color(&self, hash: &[u8]) -> Result<RGB, ThemeError> {
185 self.validate()?;
187
188 let hue_hash = ((hash[0 % hash.len()] as u16) << 8) | hash[1 % hash.len()] as u16;
190
191 let hash_hue = map_values(
193 hue_hash as f32,
194 u16::MIN as f32,
195 u16::MAX as f32,
196 self.hue_min,
197 self.hue_max,
198 );
199
200 let hue = hash_hue % 360.0;
202
203 let saturation = map_values(
205 hash[2 % hash.len()] as f32,
206 u8::MIN as f32,
207 u8::MAX as f32,
208 self.saturation_min,
209 self.saturation_max,
210 ) / 100.0;
211
212 let lightness = map_values(
214 hash[3 % hash.len()] as f32,
215 u8::MIN as f32,
216 u8::MAX as f32,
217 self.lightness_min,
218 self.lightness_max,
219 ) / 100.0;
220
221 let chroma = (1.0 - ((2.0 * lightness) - 1.0).abs()) * saturation;
223 let hue_prime = hue / 60.0;
224 let x = chroma * (1.0 - ((hue_prime % 2.0) - 1.0).abs());
225
226 let (r_prime, g_prime, b_prime) = match hue_prime {
228 0.0..1.0 => (chroma, x, 0.0),
229 1.0..2.0 => (x, chroma, 0.0),
230 2.0..3.0 => (0.0, chroma, x),
231 3.0..4.0 => (0.0, x, chroma),
232 4.0..5.0 => (x, 0.0, chroma),
233 5.0..=6.0 => (chroma, 0.0, x),
234 _ => (0.0, 0.0, 0.0),
236 };
237
238 let m = lightness - chroma * 0.5;
240
241 let red = (r_prime + m) * 255.0;
242 let green = (g_prime + m) * 255.0;
243 let blue = (b_prime + m) * 255.0;
244
245 Ok(RGB {
246 red: red as u8,
247 green: green as u8,
248 blue: blue as u8,
249 })
250 }
251
252 fn background_color(&self, hash: &[u8]) -> Result<RGB, ThemeError> {
253 if self.background.is_empty() {
254 Err(ThemeError::ThemeValidationError(
255 "background color selection is empty".to_string(),
256 ))
257 } else {
258 let index = hash[2 % hash.len()] as usize % self.background.len();
259 Ok(self.background[index])
260 }
261 }
262}
263
264pub fn default_theme() -> Arc<dyn Theme + Send + Sync> {
269 Arc::new(HSLRange {
270 hue_min: 0.0,
271 hue_max: 360.0,
272 saturation_min: 50.0,
273 saturation_max: 75.0,
274 lightness_min: 60.0,
275 lightness_max: 70.0,
276 background: vec![RGB {
277 red: 240,
278 green: 240,
279 blue: 240,
280 }],
281 })
282}
283
284pub fn pastel_selection_theme() -> Arc<dyn Theme + Send + Sync> {
289 let main = vec![
290 RGB {
291 red: 255,
292 green: 173,
293 blue: 173,
294 },
295 RGB {
296 red: 255,
297 green: 214,
298 blue: 165,
299 },
300 RGB {
301 red: 253,
302 green: 255,
303 blue: 182,
304 },
305 RGB {
306 red: 202,
307 green: 255,
308 blue: 191,
309 },
310 RGB {
311 red: 155,
312 green: 246,
313 blue: 255,
314 },
315 RGB {
316 red: 160,
317 green: 196,
318 blue: 255,
319 },
320 RGB {
321 red: 189,
322 green: 178,
323 blue: 255,
324 },
325 RGB {
326 red: 255,
327 green: 198,
328 blue: 255,
329 },
330 ];
331 let background = vec![RGB {
332 red: 240,
333 green: 240,
334 blue: 240,
335 }];
336
337 Arc::new(Selection { main, background })
338}
339
340#[cfg(test)]
341mod tests {
342 use std::sync::Arc;
343
344 use crate::{color::RGB, hash};
345
346 use super::{HSLRange, Selection, Theme, default_theme, pastel_selection_theme};
347 const CONSISTENCY_STRING_1: &str = "TEST CONSISTENCY";
348 const CONSISTENCY_STRING_2: &str = "TEST CONSISTENCY ALTERNATE";
349 const CONSISTENCY_STRING_3: &str = "CONSISTENCY TEST INPUT";
350
351 fn test_theme_consistency(
352 input: &str,
353 theme: Arc<dyn Theme>,
354 expected_main_color: RGB,
355 expected_background_color: RGB,
356 ) {
357 let hash = hash::hash_value(input);
358
359 let main_color = theme
360 .main_color(&hash)
361 .expect("could not generate main color");
362 let background_color = theme
363 .background_color(&hash)
364 .expect("could not generate background color");
365
366 assert_eq!(expected_main_color, main_color);
367 assert_eq!(expected_background_color, background_color);
368 }
369
370 #[test]
371 fn hsl_range_theme_consistency() {
372 let expected_main_color: RGB = (116, 93, 222).into();
373 let expected_background_color: RGB = (240, 240, 240).into();
374
375 test_theme_consistency(
376 CONSISTENCY_STRING_1,
377 default_theme(),
378 expected_main_color,
379 expected_background_color,
380 );
381
382 let expected_main_color: RGB = (94, 225, 227).into();
383 let expected_background_color: RGB = (240, 240, 240).into();
384
385 test_theme_consistency(
386 CONSISTENCY_STRING_2,
387 default_theme(),
388 expected_main_color,
389 expected_background_color,
390 );
391 }
392
393 #[test]
394 fn hsl_range_theme_multiple_background_consistency() {
395 let theme = Arc::new(HSLRange {
396 hue_min: 0.0,
397 hue_max: 100.0,
398 saturation_min: 0.0,
399 saturation_max: 100.0,
400 lightness_min: 0.0,
401 lightness_max: 100.0,
402 background: vec![(0, 0, 0).into(), (255, 255, 255).into()],
403 });
404
405 let expected_main_color: RGB = (67, 77, 16).into();
406 let expected_background_color: RGB = (0, 0, 0).into();
407
408 test_theme_consistency(
409 CONSISTENCY_STRING_1,
410 theme.clone(),
411 expected_main_color,
412 expected_background_color,
413 );
414
415 let expected_main_color: RGB = (232, 253, 218).into();
416 let expected_background_color: RGB = (255, 255, 255).into();
417
418 test_theme_consistency(
419 CONSISTENCY_STRING_3,
420 theme,
421 expected_main_color,
422 expected_background_color,
423 );
424 }
425
426 #[test]
427 fn hsl_theme_validation() {
428 let theme = HSLRange::new(
429 0.0,
430 360.0,
431 0.0,
432 100.0,
433 0.0,
434 100.0,
435 vec![RGB {
436 red: 255,
437 green: 255,
438 blue: 255,
439 }],
440 );
441 assert!(theme.is_ok());
442
443 let theme = HSLRange::new(
444 360.0,
445 0.0,
446 0.0,
447 100.0,
448 0.0,
449 100.0,
450 vec![RGB {
451 red: 255,
452 green: 255,
453 blue: 255,
454 }],
455 );
456 assert!(theme.is_err());
457
458 let theme = HSLRange::new(
459 0.0,
460 360.0,
461 100.0,
462 50.0,
463 0.0,
464 100.0,
465 vec![RGB {
466 red: 255,
467 green: 255,
468 blue: 255,
469 }],
470 );
471 assert!(theme.is_err());
472
473 let theme = HSLRange::new(
474 0.0,
475 360.0,
476 0.0,
477 100.0,
478 100.0,
479 50.0,
480 vec![RGB {
481 red: 255,
482 green: 255,
483 blue: 255,
484 }],
485 );
486 assert!(theme.is_err());
487 }
488
489 #[test]
490 fn selection_theme_consistency() {
491 let expected_main_color: RGB = (253, 255, 182).into();
492 let expected_background_color: RGB = (240, 240, 240).into();
493
494 test_theme_consistency(
495 CONSISTENCY_STRING_1,
496 pastel_selection_theme(),
497 expected_main_color,
498 expected_background_color,
499 );
500
501 let expected_main_color: RGB = (255, 173, 173).into();
502 let expected_background_color: RGB = (240, 240, 240).into();
503
504 test_theme_consistency(
505 CONSISTENCY_STRING_2,
506 pastel_selection_theme(),
507 expected_main_color,
508 expected_background_color,
509 );
510 }
511
512 #[test]
513 fn selection_theme_multiple_background_consistency() {
514 let theme = Arc::new(Selection {
515 main: vec![(0, 0, 0).into()],
516 background: vec![(0, 0, 0).into(), (255, 255, 255).into()],
517 });
518
519 let expected_main_color: RGB = (0, 0, 0).into();
520 let expected_background_color: RGB = (0, 0, 0).into();
521
522 test_theme_consistency(
523 CONSISTENCY_STRING_1,
524 theme.clone(),
525 expected_main_color,
526 expected_background_color,
527 );
528
529 let expected_main_color: RGB = (0, 0, 0).into();
530 let expected_background_color: RGB = (255, 255, 255).into();
531
532 test_theme_consistency(
533 CONSISTENCY_STRING_3,
534 theme,
535 expected_main_color,
536 expected_background_color,
537 );
538 }
539
540 #[test]
541 fn selection_theme_validation() {
542 let theme = Selection::new(
543 vec![],
544 vec![RGB {
545 red: 255,
546 green: 255,
547 blue: 255,
548 }],
549 );
550 assert!(theme.is_err());
551
552 let theme = Selection::new(
553 vec![RGB {
554 red: 255,
555 green: 255,
556 blue: 255,
557 }],
558 vec![],
559 );
560 assert!(theme.is_err());
561
562 let theme = Selection::new(
563 vec![RGB {
564 red: 255,
565 green: 255,
566 blue: 255,
567 }],
568 vec![RGB {
569 red: 0,
570 green: 0,
571 blue: 0,
572 }],
573 );
574 assert!(theme.is_ok());
575 }
576}