1use egui::{Color32, FontData, FontDefinitions, FontFamily};
77use serde::{Deserialize, Serialize};
78use std::collections::HashMap;
79use std::sync::{Arc, Mutex};
80
81#[cfg(feature = "ondemand")]
82use std::io::Read;
83
84#[derive(Debug, Clone)]
90pub struct PreparedFont {
91 pub name: String,
92 pub data: Arc<FontData>,
93 pub families: Vec<FontFamily>,
94}
95
96static PREPARED_FONTS: Mutex<Vec<PreparedFont>> = Mutex::new(Vec::new());
97
98#[derive(Debug, Clone)]
114pub struct PreparedTheme {
115 pub name: String,
116 pub theme_data: MaterialThemeFile,
117}
118
119static PREPARED_THEMES: Mutex<Vec<PreparedTheme>> = Mutex::new(Vec::new());
120
121#[derive(Clone, Debug, Deserialize, Serialize)]
123pub struct MaterialScheme {
124 pub primary: String,
125 #[serde(rename = "surfaceTint")]
126 pub surface_tint: String,
127 #[serde(rename = "onPrimary")]
128 pub on_primary: String,
129 #[serde(rename = "primaryContainer")]
130 pub primary_container: String,
131 #[serde(rename = "onPrimaryContainer")]
132 pub on_primary_container: String,
133 pub secondary: String,
134 #[serde(rename = "onSecondary")]
135 pub on_secondary: String,
136 #[serde(rename = "secondaryContainer")]
137 pub secondary_container: String,
138 #[serde(rename = "onSecondaryContainer")]
139 pub on_secondary_container: String,
140 pub tertiary: String,
141 #[serde(rename = "onTertiary")]
142 pub on_tertiary: String,
143 #[serde(rename = "tertiaryContainer")]
144 pub tertiary_container: String,
145 #[serde(rename = "onTertiaryContainer")]
146 pub on_tertiary_container: String,
147 pub error: String,
148 #[serde(rename = "onError")]
149 pub on_error: String,
150 #[serde(rename = "errorContainer")]
151 pub error_container: String,
152 #[serde(rename = "onErrorContainer")]
153 pub on_error_container: String,
154 pub background: String,
155 #[serde(rename = "onBackground")]
156 pub on_background: String,
157 pub surface: String,
158 #[serde(rename = "onSurface")]
159 pub on_surface: String,
160 #[serde(rename = "surfaceVariant")]
161 pub surface_variant: String,
162 #[serde(rename = "onSurfaceVariant")]
163 pub on_surface_variant: String,
164 pub outline: String,
165 #[serde(rename = "outlineVariant")]
166 pub outline_variant: String,
167 pub shadow: String,
168 pub scrim: String,
169 #[serde(rename = "inverseSurface")]
170 pub inverse_surface: String,
171 #[serde(rename = "inverseOnSurface")]
172 pub inverse_on_surface: String,
173 #[serde(rename = "inversePrimary")]
174 pub inverse_primary: String,
175 #[serde(rename = "primaryFixed")]
176 pub primary_fixed: String,
177 #[serde(rename = "onPrimaryFixed")]
178 pub on_primary_fixed: String,
179 #[serde(rename = "primaryFixedDim")]
180 pub primary_fixed_dim: String,
181 #[serde(rename = "onPrimaryFixedVariant")]
182 pub on_primary_fixed_variant: String,
183 #[serde(rename = "secondaryFixed")]
184 pub secondary_fixed: String,
185 #[serde(rename = "onSecondaryFixed")]
186 pub on_secondary_fixed: String,
187 #[serde(rename = "secondaryFixedDim")]
188 pub secondary_fixed_dim: String,
189 #[serde(rename = "onSecondaryFixedVariant")]
190 pub on_secondary_fixed_variant: String,
191 #[serde(rename = "tertiaryFixed")]
192 pub tertiary_fixed: String,
193 #[serde(rename = "onTertiaryFixed")]
194 pub on_tertiary_fixed: String,
195 #[serde(rename = "tertiaryFixedDim")]
196 pub tertiary_fixed_dim: String,
197 #[serde(rename = "onTertiaryFixedVariant")]
198 pub on_tertiary_fixed_variant: String,
199 #[serde(rename = "surfaceDim")]
200 pub surface_dim: String,
201 #[serde(rename = "surfaceBright")]
202 pub surface_bright: String,
203 #[serde(rename = "surfaceContainerLowest")]
204 pub surface_container_lowest: String,
205 #[serde(rename = "surfaceContainerLow")]
206 pub surface_container_low: String,
207 #[serde(rename = "surfaceContainer")]
208 pub surface_container: String,
209 #[serde(rename = "surfaceContainerHigh")]
210 pub surface_container_high: String,
211 #[serde(rename = "surfaceContainerHighest")]
212 pub surface_container_highest: String,
213}
214
215#[derive(Clone, Debug, Deserialize, Serialize)]
216pub struct MaterialThemeFile {
217 pub description: String,
218 pub seed: String,
219 #[serde(rename = "coreColors")]
220 pub core_colors: HashMap<String, String>,
221 #[serde(rename = "extendedColors")]
222 pub extended_colors: Vec<serde_json::Value>,
223 pub schemes: HashMap<String, MaterialScheme>,
224 pub palettes: HashMap<String, HashMap<String, String>>,
225}
226
227#[derive(Clone, Debug, Copy, PartialEq)]
228pub enum ContrastLevel {
229 Normal,
230 Medium,
231 High,
232}
233
234#[derive(Debug, Clone, Copy, PartialEq)]
235pub enum ThemeMode {
236 Light,
237 Dark,
238 Auto,
239}
240
241impl Default for ThemeMode {
242 fn default() -> Self {
243 ThemeMode::Auto
244 }
245}
246
247#[derive(Clone, Debug)]
249pub struct MaterialThemeContext {
250 pub theme_mode: ThemeMode,
251 pub contrast_level: ContrastLevel,
252 pub material_theme: Option<MaterialThemeFile>,
253 pub selected_colors: HashMap<String, Color32>,
254}
255
256impl Default for MaterialThemeContext {
257 fn default() -> Self {
258 Self {
259 theme_mode: ThemeMode::Auto,
260 contrast_level: ContrastLevel::Normal,
261 material_theme: Some(get_default_material_theme()),
262 selected_colors: HashMap::new(),
263 }
264 }
265}
266
267fn get_default_material_theme() -> MaterialThemeFile {
268 let light_scheme = MaterialScheme {
270 primary: "#48672F".to_string(),
271 surface_tint: "#48672F".to_string(),
272 on_primary: "#FFFFFF".to_string(),
273 primary_container: "#C8EEA8".to_string(),
274 on_primary_container: "#314F19".to_string(),
275 secondary: "#56624B".to_string(),
276 on_secondary: "#FFFFFF".to_string(),
277 secondary_container: "#DAE7C9".to_string(),
278 on_secondary_container: "#3F4A34".to_string(),
279 tertiary: "#386665".to_string(),
280 on_tertiary: "#FFFFFF".to_string(),
281 tertiary_container: "#BBECEA".to_string(),
282 on_tertiary_container: "#1E4E4D".to_string(),
283 error: "#BA1A1A".to_string(),
284 on_error: "#FFFFFF".to_string(),
285 error_container: "#FFDAD6".to_string(),
286 on_error_container: "#93000A".to_string(),
287 background: "#F9FAEF".to_string(),
288 on_background: "#191D16".to_string(),
289 surface: "#F9FAEF".to_string(),
290 on_surface: "#191D16".to_string(),
291 surface_variant: "#E0E4D6".to_string(),
292 on_surface_variant: "#44483E".to_string(),
293 outline: "#74796D".to_string(),
294 outline_variant: "#C4C8BA".to_string(),
295 shadow: "#000000".to_string(),
296 scrim: "#000000".to_string(),
297 inverse_surface: "#2E312A".to_string(),
298 inverse_on_surface: "#F0F2E7".to_string(),
299 inverse_primary: "#ADD28E".to_string(),
300 primary_fixed: "#C8EEA8".to_string(),
301 on_primary_fixed: "#0B2000".to_string(),
302 primary_fixed_dim: "#ADD28E".to_string(),
303 on_primary_fixed_variant: "#314F19".to_string(),
304 secondary_fixed: "#DAE7C9".to_string(),
305 on_secondary_fixed: "#141E0C".to_string(),
306 secondary_fixed_dim: "#BECBAE".to_string(),
307 on_secondary_fixed_variant: "#3F4A34".to_string(),
308 tertiary_fixed: "#BBECEA".to_string(),
309 on_tertiary_fixed: "#00201F".to_string(),
310 tertiary_fixed_dim: "#A0CFCE".to_string(),
311 on_tertiary_fixed_variant: "#1E4E4D".to_string(),
312 surface_dim: "#D9DBD1".to_string(),
313 surface_bright: "#F9FAEF".to_string(),
314 surface_container_lowest: "#FFFFFF".to_string(),
315 surface_container_low: "#F3F5EA".to_string(),
316 surface_container: "#EDEFE4".to_string(),
317 surface_container_high: "#E7E9DE".to_string(),
318 surface_container_highest: "#E2E3D9".to_string(),
319 };
320
321 let dark_scheme = MaterialScheme {
322 primary: "#ADD28E".to_string(),
323 surface_tint: "#ADD28E".to_string(),
324 on_primary: "#1B3704".to_string(),
325 primary_container: "#314F19".to_string(),
326 on_primary_container: "#C8EEA8".to_string(),
327 secondary: "#BECBAE".to_string(),
328 on_secondary: "#29341F".to_string(),
329 secondary_container: "#3F4A34".to_string(),
330 on_secondary_container: "#DAE7C9".to_string(),
331 tertiary: "#A0CFCE".to_string(),
332 on_tertiary: "#003736".to_string(),
333 tertiary_container: "#1E4E4D".to_string(),
334 on_tertiary_container: "#BBECEA".to_string(),
335 error: "#FFB4AB".to_string(),
336 on_error: "#690005".to_string(),
337 error_container: "#93000A".to_string(),
338 on_error_container: "#FFDAD6".to_string(),
339 background: "#11140E".to_string(),
340 on_background: "#E2E3D9".to_string(),
341 surface: "#11140E".to_string(),
342 on_surface: "#E2E3D9".to_string(),
343 surface_variant: "#44483E".to_string(),
344 on_surface_variant: "#C4C8BA".to_string(),
345 outline: "#8E9286".to_string(),
346 outline_variant: "#44483E".to_string(),
347 shadow: "#000000".to_string(),
348 scrim: "#000000".to_string(),
349 inverse_surface: "#E2E3D9".to_string(),
350 inverse_on_surface: "#2E312A".to_string(),
351 inverse_primary: "#48672F".to_string(),
352 primary_fixed: "#C8EEA8".to_string(),
353 on_primary_fixed: "#0B2000".to_string(),
354 primary_fixed_dim: "#ADD28E".to_string(),
355 on_primary_fixed_variant: "#314F19".to_string(),
356 secondary_fixed: "#DAE7C9".to_string(),
357 on_secondary_fixed: "#141E0C".to_string(),
358 secondary_fixed_dim: "#BECBAE".to_string(),
359 on_secondary_fixed_variant: "#3F4A34".to_string(),
360 tertiary_fixed: "#BBECEA".to_string(),
361 on_tertiary_fixed: "#00201F".to_string(),
362 tertiary_fixed_dim: "#A0CFCE".to_string(),
363 on_tertiary_fixed_variant: "#1E4E4D".to_string(),
364 surface_dim: "#11140E".to_string(),
365 surface_bright: "#373A33".to_string(),
366 surface_container_lowest: "#0C0F09".to_string(),
367 surface_container_low: "#191D16".to_string(),
368 surface_container: "#1E211A".to_string(),
369 surface_container_high: "#282B24".to_string(),
370 surface_container_highest: "#33362F".to_string(),
371 };
372
373 let mut schemes = HashMap::new();
374 schemes.insert("light".to_string(), light_scheme);
375 schemes.insert("dark".to_string(), dark_scheme);
376
377 let mut core_colors = HashMap::new();
378 core_colors.insert("primary".to_string(), "#5C883A".to_string());
379
380 MaterialThemeFile {
381 description: "TYPE: CUSTOM Material Theme Builder export 2025-08-21 11:51:45".to_string(),
382 seed: "#5C883A".to_string(),
383 core_colors,
384 extended_colors: Vec::new(),
385 schemes,
386 palettes: HashMap::new(),
387 }
388}
389
390impl MaterialThemeContext {
391 pub fn setup_fonts(font_name: Option<&str>) {
392 let font_name = font_name.unwrap_or("Google Sans Code");
393
394 let font_file_path = format!("resources/{}.ttf", font_name.replace(" ", "-").to_lowercase());
396
397 let font_data = if std::path::Path::new(&font_file_path).exists() {
398 Self::load_local_font(&font_file_path)
400 } else {
401 #[cfg(feature = "ondemand")]
403 {
404 Self::download_google_font(font_name)
405 }
406 #[cfg(not(feature = "ondemand"))]
407 {
408 eprintln!("Font '{}' not found locally and ondemand feature is not enabled", font_name);
409 None
410 }
411 };
412
413 if let Some(data) = font_data {
414 let font_family_name = font_name.replace(" ", "");
415
416 let prepared_font = PreparedFont {
417 name: font_family_name.clone(),
418 data: Arc::new(FontData::from_owned(data)),
419 families: vec![FontFamily::Proportional, FontFamily::Monospace],
420 };
421
422 if let Ok(mut fonts) = PREPARED_FONTS.lock() {
423 fonts.retain(|f| f.name != font_family_name);
425 fonts.push(prepared_font);
426 }
427 }
428 }
429
430 fn load_local_font(font_path: &str) -> Option<Vec<u8>> {
431 std::fs::read(font_path).ok()
432 }
433
434 #[cfg(feature = "ondemand")]
436 fn download_google_font(font_name: &str) -> Option<Vec<u8>> {
437 let font_url_name = font_name.replace(" ", "+");
439
440 let css_url = format!("https://fonts.googleapis.com/css2?family={}:wght@400&display=swap", font_url_name);
442
443 match ureq::get(&css_url)
444 .set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
445 .call()
446 {
447 Ok(response) => {
448 let css_content = response.into_string().ok()?;
449
450 let font_url = Self::extract_font_url_from_css(&css_content)?;
452
453 match ureq::get(&font_url).call() {
455 Ok(font_response) => {
456 let mut font_data = Vec::new();
457 if font_response.into_reader().read_to_end(&mut font_data).is_ok() {
458 let target_path = format!("resources/{}.ttf", font_name.replace(" ", "-").to_lowercase());
460 if let Ok(()) = std::fs::write(&target_path, &font_data) {
461 eprintln!("Font '{}' downloaded and saved to {}", font_name, target_path);
462 }
463 Some(font_data)
464 } else {
465 eprintln!("Failed to read font data for '{}'", font_name);
466 None
467 }
468 },
469 Err(e) => {
470 eprintln!("Failed to download font '{}': {}", font_name, e);
471 None
472 }
473 }
474 },
475 Err(e) => {
476 eprintln!("Failed to fetch CSS for font '{}': {}", font_name, e);
477 None
478 }
479 }
480 }
481
482 #[cfg(feature = "ondemand")]
483 fn extract_font_url_from_css(css_content: &str) -> Option<String> {
484 for line in css_content.lines() {
487 if line.contains("src:") && line.contains("url(") && line.contains("format('truetype')") {
488 if let Some(start) = line.find("url(") {
489 let start = start + 4; if let Some(end) = line[start..].find(")") {
491 let url = &line[start..start + end];
492 return Some(url.to_string());
493 }
494 }
495 }
496 }
497 None
498 }
499
500 pub fn setup_local_fonts(font_path: Option<&str>) {
501 let default_material_symbols_path = "resources/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf";
502
503 let font_data = if let Some(path) = font_path {
505 if std::path::Path::new(path).exists() {
507 std::fs::read(path).ok()
508 } else {
509 if std::path::Path::new(default_material_symbols_path).exists() {
511 std::fs::read(default_material_symbols_path).ok()
512 } else {
513 Self::get_embedded_material_symbols()
515 }
516 }
517 } else {
518 if std::path::Path::new(default_material_symbols_path).exists() {
520 std::fs::read(default_material_symbols_path).ok()
521 } else {
522 Self::get_embedded_material_symbols()
524 }
525 };
526
527 if let Some(data) = font_data {
529 let prepared_font = PreparedFont {
530 name: "MaterialSymbolsOutlined".to_owned(),
531 data: Arc::new(FontData::from_owned(data)),
532 families: vec![FontFamily::Proportional, FontFamily::Monospace],
533 };
534
535 if let Ok(mut fonts) = PREPARED_FONTS.lock() {
536 fonts.retain(|f| f.name != "MaterialSymbolsOutlined");
538 fonts.push(prepared_font);
539 }
540 }
541 }
542
543 fn get_embedded_material_symbols() -> Option<Vec<u8>> {
545 None
548 }
549
550 pub fn setup_local_theme(theme_path: Option<&str>) {
565 let theme_data = if let Some(path) = theme_path {
566 if std::path::Path::new(path).exists() {
568 std::fs::read_to_string(path).ok()
569 } else {
570 Self::get_embedded_theme_data(path).or_else(|| {
572 Some(serde_json::to_string(&get_default_material_theme()).unwrap_or_default())
573 })
574 }
575 } else {
576 Self::get_embedded_theme_data("resources/material-theme1.json").or_else(|| {
578 Some(serde_json::to_string(&get_default_material_theme()).unwrap_or_default())
579 })
580 };
581
582 if let Some(data) = theme_data {
584 if let Ok(theme_file) = serde_json::from_str::<MaterialThemeFile>(&data) {
585 let theme_name = theme_path.and_then(|p| {
586 std::path::Path::new(p).file_stem().map(|s| s.to_string_lossy().to_string())
587 }).unwrap_or_else(|| "default".to_string());
588
589 let prepared_theme = PreparedTheme {
590 name: theme_name.clone(),
591 theme_data: theme_file,
592 };
593
594 if let Ok(mut themes) = PREPARED_THEMES.lock() {
595 themes.retain(|t| t.name != theme_name);
597 themes.push(prepared_theme);
598 }
599 }
600 }
601 }
602
603 fn get_embedded_theme_data(theme_path: &str) -> Option<String> {
605 std::fs::read_to_string(theme_path).ok()
608 }
609
610
611 pub fn load_themes() {
623 if let Ok(prepared_themes) = PREPARED_THEMES.lock() {
624 if let Some(theme) = prepared_themes.first() {
625 let theme_context = MaterialThemeContext {
627 material_theme: Some(theme.theme_data.clone()),
628 ..Default::default()
629 };
630 update_global_theme(theme_context);
631 }
632 }
633 }
634
635 pub fn load_fonts(ctx: &egui::Context) {
637 let mut fonts = FontDefinitions::default();
638
639 if let Ok(prepared_fonts) = PREPARED_FONTS.lock() {
640 for prepared_font in prepared_fonts.iter() {
641 fonts.font_data.insert(
643 prepared_font.name.clone(),
644 prepared_font.data.clone(),
645 );
646
647 for family in &prepared_font.families {
649 match family {
650 FontFamily::Proportional => {
651 if prepared_font.name.contains("MaterialSymbols") {
653 fonts
654 .families
655 .entry(FontFamily::Proportional)
656 .or_default()
657 .push(prepared_font.name.clone());
658 } else {
659 fonts
660 .families
661 .entry(FontFamily::Proportional)
662 .or_default()
663 .insert(0, prepared_font.name.clone());
664 }
665 }
666 FontFamily::Monospace => {
667 fonts
668 .families
669 .entry(FontFamily::Monospace)
670 .or_default()
671 .push(prepared_font.name.clone());
672 }
673 _ => {}
674 }
675 }
676 }
677 }
678
679 ctx.set_fonts(fonts);
680 }
681
682 pub fn get_current_scheme(&self) -> Option<&MaterialScheme> {
683 if let Some(ref theme) = self.material_theme {
684 let scheme_key = match (self.theme_mode, self.contrast_level) {
685 (ThemeMode::Light, ContrastLevel::Normal) => "light",
686 (ThemeMode::Light, ContrastLevel::Medium) => "light-medium-contrast",
687 (ThemeMode::Light, ContrastLevel::High) => "light-high-contrast",
688 (ThemeMode::Dark, ContrastLevel::Normal) => "dark",
689 (ThemeMode::Dark, ContrastLevel::Medium) => "dark-medium-contrast",
690 (ThemeMode::Dark, ContrastLevel::High) => "dark-high-contrast",
691 (ThemeMode::Auto, contrast) => {
692 match contrast {
694 ContrastLevel::Normal => "light",
695 ContrastLevel::Medium => "light-medium-contrast",
696 ContrastLevel::High => "light-high-contrast",
697 }
698 }
699 };
700 theme.schemes.get(scheme_key)
701 } else {
702 None
703 }
704 }
705
706 pub fn hex_to_color32(hex: &str) -> Option<Color32> {
707 if hex.starts_with('#') && hex.len() == 7 {
708 if let Ok(r) = u8::from_str_radix(&hex[1..3], 16) {
709 if let Ok(g) = u8::from_str_radix(&hex[3..5], 16) {
710 if let Ok(b) = u8::from_str_radix(&hex[5..7], 16) {
711 return Some(Color32::from_rgb(r, g, b));
712 }
713 }
714 }
715 }
716 None
717 }
718
719 pub fn color32_to_hex(color: Color32) -> String {
720 format!("#{:02X}{:02X}{:02X}", color.r(), color.g(), color.b())
721 }
722
723 pub fn get_color_by_name(&self, name: &str) -> Color32 {
724 if let Some(color) = self.selected_colors.get(name) {
725 return *color;
726 }
727
728 if let Some(scheme) = self.get_current_scheme() {
729 let hex = match name {
730 "primary" => &scheme.primary,
731 "surfaceTint" => &scheme.surface_tint,
732 "onPrimary" => &scheme.on_primary,
733 "primaryContainer" => &scheme.primary_container,
734 "onPrimaryContainer" => &scheme.on_primary_container,
735 "secondary" => &scheme.secondary,
736 "onSecondary" => &scheme.on_secondary,
737 "secondaryContainer" => &scheme.secondary_container,
738 "onSecondaryContainer" => &scheme.on_secondary_container,
739 "tertiary" => &scheme.tertiary,
740 "onTertiary" => &scheme.on_tertiary,
741 "tertiaryContainer" => &scheme.tertiary_container,
742 "onTertiaryContainer" => &scheme.on_tertiary_container,
743 "error" => &scheme.error,
744 "onError" => &scheme.on_error,
745 "errorContainer" => &scheme.error_container,
746 "onErrorContainer" => &scheme.on_error_container,
747 "background" => &scheme.background,
748 "onBackground" => &scheme.on_background,
749 "surface" => &scheme.surface,
750 "onSurface" => &scheme.on_surface,
751 "surfaceVariant" => &scheme.surface_variant,
752 "onSurfaceVariant" => &scheme.on_surface_variant,
753 "outline" => &scheme.outline,
754 "outlineVariant" => &scheme.outline_variant,
755 "shadow" => &scheme.shadow,
756 "scrim" => &scheme.scrim,
757 "inverseSurface" => &scheme.inverse_surface,
758 "inverseOnSurface" => &scheme.inverse_on_surface,
759 "inversePrimary" => &scheme.inverse_primary,
760 "primaryFixed" => &scheme.primary_fixed,
761 "onPrimaryFixed" => &scheme.on_primary_fixed,
762 "primaryFixedDim" => &scheme.primary_fixed_dim,
763 "onPrimaryFixedVariant" => &scheme.on_primary_fixed_variant,
764 "secondaryFixed" => &scheme.secondary_fixed,
765 "onSecondaryFixed" => &scheme.on_secondary_fixed,
766 "secondaryFixedDim" => &scheme.secondary_fixed_dim,
767 "onSecondaryFixedVariant" => &scheme.on_secondary_fixed_variant,
768 "tertiaryFixed" => &scheme.tertiary_fixed,
769 "onTertiaryFixed" => &scheme.on_tertiary_fixed,
770 "tertiaryFixedDim" => &scheme.tertiary_fixed_dim,
771 "onTertiaryFixedVariant" => &scheme.on_tertiary_fixed_variant,
772 "surfaceDim" => &scheme.surface_dim,
773 "surfaceBright" => &scheme.surface_bright,
774 "surfaceContainerLowest" => &scheme.surface_container_lowest,
775 "surfaceContainerLow" => &scheme.surface_container_low,
776 "surfaceContainer" => &scheme.surface_container,
777 "surfaceContainerHigh" => &scheme.surface_container_high,
778 "surfaceContainerHighest" => &scheme.surface_container_highest,
779 _ => return Color32::GRAY, };
781
782 Self::hex_to_color32(hex).unwrap_or(Color32::GRAY)
783 } else {
784 match name {
786 "primary" => Color32::from_rgb(72, 103, 47), "surfaceTint" => Color32::from_rgb(72, 103, 47), "onPrimary" => Color32::WHITE, "primaryContainer" => Color32::from_rgb(200, 238, 168), "onPrimaryContainer" => Color32::from_rgb(49, 79, 25), "secondary" => Color32::from_rgb(86, 98, 75), "onSecondary" => Color32::WHITE, "secondaryContainer" => Color32::from_rgb(218, 231, 201), "onSecondaryContainer" => Color32::from_rgb(63, 74, 52), "tertiary" => Color32::from_rgb(56, 102, 101), "onTertiary" => Color32::WHITE, "tertiaryContainer" => Color32::from_rgb(187, 236, 234), "onTertiaryContainer" => Color32::from_rgb(30, 78, 77), "error" => Color32::from_rgb(186, 26, 26), "onError" => Color32::WHITE, "errorContainer" => Color32::from_rgb(255, 218, 214), "onErrorContainer" => Color32::from_rgb(147, 0, 10), "background" => Color32::from_rgb(249, 250, 239), "onBackground" => Color32::from_rgb(25, 29, 22), "surface" => Color32::from_rgb(249, 250, 239), "onSurface" => Color32::from_rgb(25, 29, 22), "surfaceVariant" => Color32::from_rgb(224, 228, 214), "onSurfaceVariant" => Color32::from_rgb(68, 72, 62), "outline" => Color32::from_rgb(116, 121, 109), "outlineVariant" => Color32::from_rgb(196, 200, 186), "shadow" => Color32::BLACK, "scrim" => Color32::BLACK, "inverseSurface" => Color32::from_rgb(46, 49, 42), "inverseOnSurface" => Color32::from_rgb(240, 242, 231), "inversePrimary" => Color32::from_rgb(173, 210, 142), "primaryFixed" => Color32::from_rgb(200, 238, 168), "onPrimaryFixed" => Color32::from_rgb(11, 32, 0), "primaryFixedDim" => Color32::from_rgb(173, 210, 142), "onPrimaryFixedVariant" => Color32::from_rgb(49, 79, 25), "secondaryFixed" => Color32::from_rgb(218, 231, 201), "onSecondaryFixed" => Color32::from_rgb(20, 30, 12), "secondaryFixedDim" => Color32::from_rgb(190, 203, 174), "onSecondaryFixedVariant" => Color32::from_rgb(63, 74, 52), "tertiaryFixed" => Color32::from_rgb(187, 236, 234), "onTertiaryFixed" => Color32::from_rgb(0, 32, 31), "tertiaryFixedDim" => Color32::from_rgb(160, 207, 206), "onTertiaryFixedVariant" => Color32::from_rgb(30, 78, 77), "surfaceDim" => Color32::from_rgb(217, 219, 209), "surfaceBright" => Color32::from_rgb(249, 250, 239), "surfaceContainerLowest" => Color32::WHITE, "surfaceContainerLow" => Color32::from_rgb(243, 245, 234), "surfaceContainer" => Color32::from_rgb(237, 239, 228), "surfaceContainerHigh" => Color32::from_rgb(231, 233, 222), "surfaceContainerHighest" => Color32::from_rgb(226, 227, 217), _ => Color32::GRAY,
836 }
837 }
838 }
839
840 pub fn get_primary_color(&self) -> Color32 {
841 self.get_color_by_name("primary")
842 }
843
844 pub fn get_secondary_color(&self) -> Color32 {
845 self.get_color_by_name("secondary")
846 }
847
848 pub fn get_tertiary_color(&self) -> Color32 {
849 self.get_color_by_name("tertiary")
850 }
851
852 pub fn get_surface_color(&self, _dark_mode: bool) -> Color32 {
853 self.get_color_by_name("surface")
854 }
855
856 pub fn get_on_primary_color(&self) -> Color32 {
857 self.get_color_by_name("onPrimary")
858 }
859}
860
861static GLOBAL_THEME: std::sync::LazyLock<Arc<Mutex<MaterialThemeContext>>> =
863 std::sync::LazyLock::new(|| Arc::new(Mutex::new(MaterialThemeContext::default())));
864
865pub fn get_global_theme() -> Arc<Mutex<MaterialThemeContext>> {
866 GLOBAL_THEME.clone()
867}
868
869pub fn update_global_theme(theme: MaterialThemeContext) {
895 if let Ok(mut global_theme) = GLOBAL_THEME.lock() {
896 *global_theme = theme;
897 }
898}
899
900pub fn setup_google_fonts(font_name: Option<&str>) {
904 MaterialThemeContext::setup_fonts(font_name);
905}
906
907pub fn setup_local_fonts(font_path: Option<&str>) {
913 MaterialThemeContext::setup_local_fonts(font_path);
914}
915
916pub fn setup_local_theme(theme_path: Option<&str>) {
948 MaterialThemeContext::setup_local_theme(theme_path);
949}
950
951pub fn load_themes() {
973 MaterialThemeContext::load_themes();
974}
975
976pub trait ContextRef {
978 fn context_ref(&self) -> &egui::Context;
979}
980
981impl ContextRef for egui::Context {
982 fn context_ref(&self) -> &egui::Context {
983 self
984 }
985}
986
987impl ContextRef for &egui::Context {
988 fn context_ref(&self) -> &egui::Context {
989 self
990 }
991}
992
993pub fn load_fonts<C: ContextRef>(ctx: C) {
996 MaterialThemeContext::load_fonts(ctx.context_ref());
997}
998
999pub fn update_window_background<C: ContextRef>(ctx: C) {
1048 let ctx = ctx.context_ref();
1049 if let Ok(theme) = GLOBAL_THEME.lock() {
1050 let background_color = match (theme.theme_mode, theme.contrast_level) {
1052 (ThemeMode::Dark, ContrastLevel::High) => theme.get_color_by_name("surfaceContainerHighest"),
1053 (ThemeMode::Dark, ContrastLevel::Medium) => theme.get_color_by_name("surfaceContainerHigh"),
1054 (ThemeMode::Dark, _) => theme.get_color_by_name("surface"),
1055 (ThemeMode::Light, ContrastLevel::High) => theme.get_color_by_name("surfaceContainerLowest"),
1056 (ThemeMode::Light, ContrastLevel::Medium) => theme.get_color_by_name("surfaceContainerLow"),
1057 (ThemeMode::Light, _) => theme.get_color_by_name("surface"),
1058 (ThemeMode::Auto, _) => theme.get_color_by_name("surface"), };
1060
1061 let mut visuals = ctx.style().visuals.clone();
1063 visuals.window_fill = background_color;
1064 visuals.panel_fill = background_color;
1065 visuals.extreme_bg_color = background_color;
1066
1067 let mut style = (*ctx.style()).clone();
1068 style.visuals = visuals;
1069 ctx.set_style(style);
1070 }
1071}
1072
1073pub fn get_global_color(name: &str) -> Color32 {
1075 if let Ok(theme) = GLOBAL_THEME.lock() {
1076 theme.get_color_by_name(name)
1077 } else {
1078 match name {
1080 "primary" => Color32::from_rgb(103, 80, 164),
1081 "onPrimary" => Color32::WHITE,
1082 "surface" => Color32::from_rgb(254, 247, 255),
1083 "onSurface" => Color32::from_rgb(28, 27, 31),
1084 "surfaceContainer" => Color32::from_rgb(247, 243, 249),
1085 "surfaceContainerHigh" => Color32::from_rgb(237, 231, 246),
1086 "surfaceContainerHighest" => Color32::from_rgb(230, 224, 233),
1087 "surfaceContainerLow" => Color32::from_rgb(247, 243, 249),
1088 "surfaceContainerLowest" => Color32::from_rgb(255, 255, 255),
1089 "outline" => Color32::from_rgb(121, 116, 126),
1090 "outlineVariant" => Color32::from_rgb(196, 199, 197),
1091 "surfaceVariant" => Color32::from_rgb(232, 222, 248),
1092 "secondary" => Color32::from_rgb(125, 82, 96),
1093 "tertiary" => Color32::from_rgb(125, 82, 96),
1094 "error" => Color32::from_rgb(186, 26, 26),
1095 "background" => Color32::from_rgb(255, 251, 254),
1096 "onBackground" => Color32::from_rgb(28, 27, 31),
1097 _ => Color32::GRAY,
1098 }
1099 }
1100}