1pub mod contrast;
32pub mod dynamiccolor;
33pub mod hct;
34pub mod palettes;
35pub mod scheme;
36pub mod utils;
37
38use serde::{Deserialize, Serialize};
39use std::collections::HashMap;
40
41pub use hct::Hct;
43pub use palettes::TonalPalette;
44pub use scheme::{DynamicScheme, Variant};
45
46pub fn hex_to_argb(hex: &str) -> Result<u32, String> {
50 let hex = hex.trim_start_matches('#');
51
52 let rgb = u32::from_str_radix(hex, 16).map_err(|e| format!("Invalid hex color: {}", e))?;
53
54 Ok(0xFF000000 | rgb)
56}
57
58pub fn argb_to_hex(argb: u32) -> String {
62 format!("#{:06X}", argb & 0x00FFFFFF)
63}
64
65#[inline]
67pub fn argb_to_rgb(argb: u32) -> (u8, u8, u8) {
68 (
69 ((argb >> 16) & 0xFF) as u8,
70 ((argb >> 8) & 0xFF) as u8,
71 (argb & 0xFF) as u8,
72 )
73}
74
75#[inline]
77pub fn argb_to_rgba(argb: u32) -> [u8; 4] {
78 [
79 ((argb >> 16) & 0xFF) as u8, ((argb >> 8) & 0xFF) as u8, (argb & 0xFF) as u8, ((argb >> 24) & 0xFF) as u8, ]
84}
85
86#[inline]
88pub fn argb_to_rgba_f32(argb: u32) -> [f32; 4] {
89 [
90 ((argb >> 16) & 0xFF) as f32 / 255.0,
91 ((argb >> 8) & 0xFF) as f32 / 255.0,
92 (argb & 0xFF) as f32 / 255.0,
93 ((argb >> 24) & 0xFF) as f32 / 255.0,
94 ]
95}
96
97#[derive(Debug, Clone)]
107pub struct SchemeColors {
108 colors: HashMap<String, u32>,
109}
110
111impl SchemeColors {
112 pub fn new(colors: HashMap<String, u32>) -> Self {
114 Self { colors }
115 }
116
117 pub fn get_argb(&self, role: &str) -> Option<u32> {
119 self.colors.get(role).copied()
120 }
121
122 pub fn get_rgb(&self, role: &str) -> Option<(u8, u8, u8)> {
124 self.get_argb(role).map(argb_to_rgb)
125 }
126
127 pub fn get_rgba(&self, role: &str) -> Option<[u8; 4]> {
129 self.get_argb(role).map(argb_to_rgba)
130 }
131
132 pub fn get_rgba_f32(&self, role: &str) -> Option<[f32; 4]> {
134 self.get_argb(role).map(argb_to_rgba_f32)
135 }
136
137 pub fn roles(&self) -> impl Iterator<Item = &String> {
141 self.colors.keys()
142 }
143}
144
145#[cfg(feature = "iced")]
148impl SchemeColors {
149 pub fn get_iced(&self, role: &str) -> Option<iced::Color> {
153 self.get_rgba_f32(role)
154 .map(|[r, g, b, a]| iced::Color::from_rgba(r, g, b, a))
155 }
156}
157
158#[derive(Debug, Clone)]
163pub struct MaterialTheme {
164 pub description: String,
166 pub seed_color: u32,
168 pub core_colors: HashMap<String, u32>,
170 pub schemes: HashMap<String, SchemeColors>,
172 pub palettes: HashMap<String, TonalPalette>,
174}
175
176#[derive(Debug, Serialize, Deserialize)]
182pub struct MaterialThemeJson {
183 pub description: String,
184 pub seed: String,
185 #[serde(rename = "coreColors")]
186 pub core_colors: HashMap<String, String>,
187 #[serde(rename = "extendedColors")]
188 pub extended_colors: Vec<serde_json::Value>,
189 pub schemes: HashMap<String, HashMap<String, String>>,
190 pub palettes: HashMap<String, HashMap<String, String>>,
191}
192
193impl MaterialTheme {
196 pub fn to_json(&self) -> MaterialThemeJson {
202 MaterialThemeJson {
203 description: self.description.clone(),
204 seed: argb_to_hex(self.seed_color),
205 core_colors: self
206 .core_colors
207 .iter()
208 .map(|(k, &v)| (k.clone(), argb_to_hex(v)))
209 .collect(),
210 extended_colors: vec![],
211 schemes: self
212 .schemes
213 .iter()
214 .map(|(name, scheme)| {
215 let colors = scheme
216 .colors
217 .iter()
218 .map(|(k, &v)| (k.clone(), argb_to_hex(v)))
219 .collect();
220 (name.clone(), colors)
221 })
222 .collect(),
223 palettes: self
224 .palettes
225 .iter()
226 .map(|(name, palette)| {
227 let tones = [
228 0, 5, 10, 15, 20, 25, 30, 35, 40, 50, 60, 70, 80, 90, 95, 98, 99, 100,
229 ];
230 let tone_map = tones
231 .iter()
232 .map(|&t| (t.to_string(), argb_to_hex(palette.get(t))))
233 .collect();
234 (name.clone(), tone_map)
235 })
236 .collect(),
237 }
238 }
239
240 pub fn from_json(json: MaterialThemeJson) -> Result<Self, String> {
245 let seed_color = hex_to_argb(&json.seed)?;
246
247 let core_colors = json
248 .core_colors
249 .iter()
250 .map(|(k, v)| hex_to_argb(v).map(|argb| (k.clone(), argb)))
251 .collect::<Result<HashMap<_, _>, _>>()?;
252
253 let schemes = json
254 .schemes
255 .iter()
256 .map(|(name, colors)| -> Result<(String, SchemeColors), String> {
257 let argb_colors = colors
258 .iter()
259 .map(|(k, v)| hex_to_argb(v).map(|argb| (k.clone(), argb)))
260 .collect::<Result<HashMap<_, _>, _>>()?;
261 Ok((name.clone(), SchemeColors::new(argb_colors)))
262 })
263 .collect::<Result<HashMap<_, _>, _>>()?;
264
265 let palettes = json
267 .palettes
268 .iter()
269 .map(
270 |(name, tone_map)| -> Result<(String, TonalPalette), String> {
271 let tone_50_hex = tone_map
273 .get("50")
274 .ok_or_else(|| format!("Missing tone 50 in palette {}", name))?;
275 let argb_50 = hex_to_argb(tone_50_hex)?;
276 let hct = Hct::from_int(argb_50);
277 let palette = TonalPalette::of(hct.hue(), hct.chroma());
278 Ok((name.clone(), palette))
279 },
280 )
281 .collect::<Result<HashMap<_, _>, _>>()?;
282
283 Ok(MaterialTheme {
284 description: json.description,
285 seed_color,
286 core_colors,
287 schemes,
288 palettes,
289 })
290 }
291}
292
293pub fn generate_theme_from_color(hex_color: &str) -> Result<MaterialTheme, String> {
300 let argb = hex_to_argb(hex_color)?;
301 let source = Hct::from_int(argb);
302
303 let light_scheme = DynamicScheme::tonal_spot(source.clone(), false, 0.0);
307 let light_medium_scheme = DynamicScheme::tonal_spot(source.clone(), false, 0.5);
308 let light_high_scheme = DynamicScheme::tonal_spot(source.clone(), false, 1.0);
309
310 let dark_scheme = DynamicScheme::tonal_spot(source.clone(), true, 0.0);
311 let dark_medium_scheme = DynamicScheme::tonal_spot(source.clone(), true, 0.5);
312 let dark_high_scheme = DynamicScheme::tonal_spot(source.clone(), true, 1.0);
313
314 let mut schemes = HashMap::new();
316 schemes.insert("light".to_string(), build_scheme_colors(&light_scheme));
317 schemes.insert(
318 "light-medium-contrast".to_string(),
319 build_scheme_colors(&light_medium_scheme),
320 );
321 schemes.insert(
322 "light-high-contrast".to_string(),
323 build_scheme_colors(&light_high_scheme),
324 );
325 schemes.insert("dark".to_string(), build_scheme_colors(&dark_scheme));
326 schemes.insert(
327 "dark-medium-contrast".to_string(),
328 build_scheme_colors(&dark_medium_scheme),
329 );
330 schemes.insert(
331 "dark-high-contrast".to_string(),
332 build_scheme_colors(&dark_high_scheme),
333 );
334
335 let palettes = build_palettes(&light_scheme);
337
338 let mut core_colors = HashMap::new();
339 core_colors.insert("primary".to_string(), argb);
340
341 Ok(MaterialTheme {
342 description: "TYPE: CUSTOM\nMaterial Theme export".to_string(),
343 seed_color: argb,
344 core_colors,
345 schemes,
346 palettes,
347 })
348}
349
350fn build_scheme_colors(scheme: &DynamicScheme) -> SchemeColors {
351 use dynamiccolor::MaterialDynamicColors;
352
353 let mut colors = HashMap::new();
354
355 colors.insert(
357 "primary".to_string(),
358 MaterialDynamicColors::primary().get_argb(scheme),
359 );
360 colors.insert(
361 "onPrimary".to_string(),
362 MaterialDynamicColors::on_primary().get_argb(scheme),
363 );
364 colors.insert(
365 "primaryContainer".to_string(),
366 MaterialDynamicColors::primary_container().get_argb(scheme),
367 );
368 colors.insert(
369 "onPrimaryContainer".to_string(),
370 MaterialDynamicColors::on_primary_container().get_argb(scheme),
371 );
372
373 colors.insert(
374 "secondary".to_string(),
375 MaterialDynamicColors::secondary().get_argb(scheme),
376 );
377 colors.insert(
378 "onSecondary".to_string(),
379 MaterialDynamicColors::on_secondary().get_argb(scheme),
380 );
381 colors.insert(
382 "secondaryContainer".to_string(),
383 MaterialDynamicColors::secondary_container().get_argb(scheme),
384 );
385 colors.insert(
386 "onSecondaryContainer".to_string(),
387 MaterialDynamicColors::on_secondary_container().get_argb(scheme),
388 );
389
390 colors.insert(
391 "tertiary".to_string(),
392 MaterialDynamicColors::tertiary().get_argb(scheme),
393 );
394 colors.insert(
395 "onTertiary".to_string(),
396 MaterialDynamicColors::on_tertiary().get_argb(scheme),
397 );
398 colors.insert(
399 "tertiaryContainer".to_string(),
400 MaterialDynamicColors::tertiary_container().get_argb(scheme),
401 );
402 colors.insert(
403 "onTertiaryContainer".to_string(),
404 MaterialDynamicColors::on_tertiary_container().get_argb(scheme),
405 );
406
407 colors.insert(
408 "error".to_string(),
409 MaterialDynamicColors::error().get_argb(scheme),
410 );
411 colors.insert(
412 "onError".to_string(),
413 MaterialDynamicColors::on_error().get_argb(scheme),
414 );
415 colors.insert(
416 "errorContainer".to_string(),
417 MaterialDynamicColors::error_container().get_argb(scheme),
418 );
419 colors.insert(
420 "onErrorContainer".to_string(),
421 MaterialDynamicColors::on_error_container().get_argb(scheme),
422 );
423
424 colors.insert(
425 "background".to_string(),
426 MaterialDynamicColors::background().get_argb(scheme),
427 );
428 colors.insert(
429 "onBackground".to_string(),
430 MaterialDynamicColors::on_background().get_argb(scheme),
431 );
432
433 colors.insert(
434 "surface".to_string(),
435 MaterialDynamicColors::surface().get_argb(scheme),
436 );
437 colors.insert(
438 "onSurface".to_string(),
439 MaterialDynamicColors::on_surface().get_argb(scheme),
440 );
441 colors.insert(
442 "surfaceVariant".to_string(),
443 MaterialDynamicColors::surface_variant().get_argb(scheme),
444 );
445 colors.insert(
446 "onSurfaceVariant".to_string(),
447 MaterialDynamicColors::on_surface_variant().get_argb(scheme),
448 );
449
450 colors.insert(
451 "outline".to_string(),
452 MaterialDynamicColors::outline().get_argb(scheme),
453 );
454 colors.insert(
455 "outlineVariant".to_string(),
456 MaterialDynamicColors::outline_variant().get_argb(scheme),
457 );
458 colors.insert(
459 "shadow".to_string(),
460 MaterialDynamicColors::shadow().get_argb(scheme),
461 );
462 colors.insert(
463 "scrim".to_string(),
464 MaterialDynamicColors::scrim().get_argb(scheme),
465 );
466
467 colors.insert(
468 "inverseSurface".to_string(),
469 MaterialDynamicColors::inverse_surface().get_argb(scheme),
470 );
471 colors.insert(
472 "inverseOnSurface".to_string(),
473 MaterialDynamicColors::inverse_on_surface().get_argb(scheme),
474 );
475 colors.insert(
476 "inversePrimary".to_string(),
477 MaterialDynamicColors::inverse_primary().get_argb(scheme),
478 );
479
480 colors.insert(
482 "primaryFixed".to_string(),
483 MaterialDynamicColors::primary_fixed().get_argb(scheme),
484 );
485 colors.insert(
486 "onPrimaryFixed".to_string(),
487 MaterialDynamicColors::on_primary_fixed().get_argb(scheme),
488 );
489 colors.insert(
490 "primaryFixedDim".to_string(),
491 MaterialDynamicColors::primary_fixed_dim().get_argb(scheme),
492 );
493 colors.insert(
494 "onPrimaryFixedVariant".to_string(),
495 MaterialDynamicColors::on_primary_fixed_variant().get_argb(scheme),
496 );
497
498 colors.insert(
499 "secondaryFixed".to_string(),
500 MaterialDynamicColors::secondary_fixed().get_argb(scheme),
501 );
502 colors.insert(
503 "onSecondaryFixed".to_string(),
504 MaterialDynamicColors::on_secondary_fixed().get_argb(scheme),
505 );
506 colors.insert(
507 "secondaryFixedDim".to_string(),
508 MaterialDynamicColors::secondary_fixed_dim().get_argb(scheme),
509 );
510 colors.insert(
511 "onSecondaryFixedVariant".to_string(),
512 MaterialDynamicColors::on_secondary_fixed_variant().get_argb(scheme),
513 );
514
515 colors.insert(
516 "tertiaryFixed".to_string(),
517 MaterialDynamicColors::tertiary_fixed().get_argb(scheme),
518 );
519 colors.insert(
520 "onTertiaryFixed".to_string(),
521 MaterialDynamicColors::on_tertiary_fixed().get_argb(scheme),
522 );
523 colors.insert(
524 "tertiaryFixedDim".to_string(),
525 MaterialDynamicColors::tertiary_fixed_dim().get_argb(scheme),
526 );
527 colors.insert(
528 "onTertiaryFixedVariant".to_string(),
529 MaterialDynamicColors::on_tertiary_fixed_variant().get_argb(scheme),
530 );
531
532 colors.insert(
534 "surfaceDim".to_string(),
535 MaterialDynamicColors::surface_dim().get_argb(scheme),
536 );
537 colors.insert(
538 "surfaceBright".to_string(),
539 MaterialDynamicColors::surface_bright().get_argb(scheme),
540 );
541 colors.insert(
542 "surfaceContainerLowest".to_string(),
543 MaterialDynamicColors::surface_container_lowest().get_argb(scheme),
544 );
545 colors.insert(
546 "surfaceContainerLow".to_string(),
547 MaterialDynamicColors::surface_container_low().get_argb(scheme),
548 );
549 colors.insert(
550 "surfaceContainer".to_string(),
551 MaterialDynamicColors::surface_container().get_argb(scheme),
552 );
553 colors.insert(
554 "surfaceContainerHigh".to_string(),
555 MaterialDynamicColors::surface_container_high().get_argb(scheme),
556 );
557 colors.insert(
558 "surfaceContainerHighest".to_string(),
559 MaterialDynamicColors::surface_container_highest().get_argb(scheme),
560 );
561 colors.insert(
562 "surfaceTint".to_string(),
563 MaterialDynamicColors::surface_tint().get_argb(scheme),
564 );
565
566 SchemeColors::new(colors)
567}
568
569fn build_palettes(scheme: &DynamicScheme) -> HashMap<String, TonalPalette> {
570 let mut palettes = HashMap::new();
571
572 let source_hct = &scheme.source_color_hct;
575 let source_hue = source_hct.hue();
576 let source_chroma = source_hct.chroma();
577
578 let primary_palette = TonalPalette::from_hct(source_hct);
580 palettes.insert("primary".to_string(), primary_palette);
581
582 let secondary_palette = TonalPalette::of(source_hue, source_chroma / 3.0);
584 palettes.insert("secondary".to_string(), secondary_palette);
585
586 let tertiary_hue = (source_hue + 60.0) % 360.0;
588 let tertiary_palette = TonalPalette::of(tertiary_hue, source_chroma / 2.0);
589 palettes.insert("tertiary".to_string(), tertiary_palette);
590
591 let neutral_palette = TonalPalette::of(source_hue, (source_chroma / 12.0).min(4.0));
593 palettes.insert("neutral".to_string(), neutral_palette);
594
595 let neutral_variant_palette = TonalPalette::of(source_hue, (source_chroma / 6.0).min(8.0));
597 palettes.insert("neutral-variant".to_string(), neutral_variant_palette);
598
599 palettes
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605
606 #[test]
607 fn test_hex_conversion() {
608 let argb = hex_to_argb("#39C5BB").unwrap();
609 assert_eq!(argb, 0xFF39C5BB);
610 assert_eq!(argb_to_hex(argb), "#39C5BB");
611 }
612
613 #[test]
614 fn test_generate_theme() {
615 let theme = generate_theme_from_color("#39C5BB").unwrap();
616 assert!(theme.schemes.contains_key("light"));
617 assert!(theme.schemes.contains_key("dark"));
618 assert!(theme.palettes.contains_key("primary"));
619 }
620}