egui_material3/
theme.rs

1//! Material Design 3 theming system
2//! 
3//! This module provides a comprehensive theming system for Material Design 3 components,
4//! including support for build-time theme inclusion, runtime theme loading, and dynamic
5//! theme switching with multiple modes and contrast levels.
6//!
7//! # Overview
8//!
9//! The theme system consists of several key components:
10//!
11//! - **Theme Preparation**: Load and parse Material Design theme JSON files
12//! - **Theme Loading**: Apply prepared themes to the global context
13//! - **Font Management**: Handle Google Fonts and local font loading
14//! - **Background Updates**: Apply theme-appropriate background colors
15//! - **Runtime Switching**: Change themes, modes, and contrast levels dynamically
16//!
17//! # Basic Usage
18//!
19//! ```rust,no_run
20//! use egui_material3::theme::{
21//!     setup_google_fonts, setup_local_fonts, setup_local_theme,
22//!     load_fonts, load_themes, update_window_background
23//! };
24//!
25//! // Setup fonts and themes (typically during app initialization)
26//! setup_google_fonts(Some("Roboto"));
27//! setup_local_fonts(Some("path/to/MaterialSymbols.ttf"));
28//! setup_local_theme(None); // Use build-time included themes
29//!
30//! // Load prepared fonts and themes (accepts both &egui::Context and egui::Context)
31//! load_fonts(&egui_ctx);  // With reference
32//! load_fonts(egui_ctx);   // With owned context  
33//! load_themes();
34//!
35//! // Apply theme background (also flexible with context types)
36//! update_window_background(&egui_ctx);
37//! ```
38//!
39//! # Build-time Theme Inclusion
40//!
41//! The build script automatically scans for theme JSON files in:
42//! - `resources/` directory  
43//! - `examples/` directory
44//!
45//! Files matching patterns like `*theme*.json` or `*material-theme*.json` are
46//! included as string constants for optimal performance.
47//!
48//! # Theme JSON Format
49//!
50//! Themes should follow the Material Design Theme Builder export format:
51//!
52//! ```json
53//! {
54//!   "description": "My Custom Theme",
55//!   "seed": "#6750A4",
56//!   "coreColors": {
57//!     "primary": "#6750A4"
58//!   },
59//!   "schemes": {
60//!     "light": {
61//!       "primary": "#6750A4",
62//!       "onPrimary": "#FFFFFF",
63//!       "surface": "#FEF7FF",
64//!       // ... more colors
65//!     },
66//!     "dark": {
67//!       "primary": "#D0BCFF", 
68//!       "onPrimary": "#381E72",
69//!       "surface": "#141218",
70//!       // ... more colors
71//!     }
72//!   }
73//! }
74//! ```
75
76use 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// Font runtime management system - replaced build-time font inclusion with runtime loading for better flexibility
85
86// Runtime font management system with support for both local and on-demand font loading
87
88/// Global collection of prepared fonts before loading to context
89#[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/// A prepared Material Design theme ready for loading
99/// 
100/// This struct represents a Material Design theme that has been loaded and parsed
101/// from a JSON file, but not yet applied to the global theme context. Themes are
102/// stored in this prepared state to allow multiple themes to be loaded and then
103/// selectively applied.
104/// 
105/// # Fields
106/// * `name` - Human-readable name for the theme (derived from filename or "default")
107/// * `theme_data` - The complete Material Design theme specification parsed from JSON
108/// 
109/// # Usage
110/// This struct is primarily used internally by the theme system. Themes are prepared
111/// by `setup_local_theme()` and stored in the static `PREPARED_THEMES` collection,
112/// then activated by `load_themes()`.
113#[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/// Material Design color scheme structure from JSON
122#[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/// Global theme context that can be shared across all Material components
248#[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    // Create default Material theme programmatically using colors from material-theme4.json
269    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        // Check if font exists in resources directory first
395        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            // Use local font file with include_bytes!
399            Self::load_local_font(&font_file_path)
400        } else {
401            // Download font from Google Fonts at runtime (only if ondemand feature is enabled)
402            #[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                // Remove any existing font with the same name
424                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    // On-demand font downloading feature - downloads Google Fonts at runtime when ondemand feature is enabled
435    #[cfg(feature = "ondemand")]
436    fn download_google_font(font_name: &str) -> Option<Vec<u8>> {
437        // Convert font name to Google Fonts URL format
438        let font_url_name = font_name.replace(" ", "+");
439        
440        // First, get the CSS file to find the actual font URL
441        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                // Parse CSS to find TTF URL
451                let font_url = Self::extract_font_url_from_css(&css_content)?;
452                
453                // Download the actual font file
454                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                            // Save font to resources directory for future use
459                            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        // Look for TTF URLs in the CSS content
485        // Google Fonts CSS contains lines like: src: url(https://fonts.gstatic.com/...) format('truetype');
486        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; // Skip "url("
490                    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        // Determine which font to use
504        let font_data = if let Some(path) = font_path {
505            // Try to load custom font from path
506            if std::path::Path::new(path).exists() {
507                std::fs::read(path).ok()
508            } else {
509                // Fall back to default font if custom font doesn't exist
510                if std::path::Path::new(default_material_symbols_path).exists() {
511                    std::fs::read(default_material_symbols_path).ok()
512                } else {
513                    // Use include_bytes! as fallback if file exists in resources
514                    Self::get_embedded_material_symbols()
515                }
516            }
517        } else {
518            // Use default Material Symbols Outlined font
519            if std::path::Path::new(default_material_symbols_path).exists() {
520                std::fs::read(default_material_symbols_path).ok()
521            } else {
522                // Use include_bytes! as fallback
523                Self::get_embedded_material_symbols()
524            }
525        };
526        
527        // Prepare font if available
528        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                // Remove any existing font with the same name
537                fonts.retain(|f| f.name != "MaterialSymbolsOutlined");
538                fonts.push(prepared_font);
539            }
540        }
541    }
542    
543    // Fallback font embedding system - includes Material Symbols font at build-time if available
544    fn get_embedded_material_symbols() -> Option<Vec<u8>> {
545        // Font files are excluded from package distribution, so this will always return None in published packages
546        // Users should provide their own font files or use the ondemand feature
547        None
548    }
549    
550    /// Internal implementation for preparing local themes from JSON files
551    /// 
552    /// This function handles the loading and parsing of Material Design theme JSON files.
553    /// It supports both runtime file loading and build-time constant inclusion.
554    /// 
555    /// # Arguments
556    /// * `theme_path` - Optional path to theme JSON file. If None, uses build-time constants.
557    /// 
558    /// # Implementation Details
559    /// - First attempts to load from specified file path (if provided)
560    /// - Falls back to build-time included theme constants
561    /// - Finally falls back to default built-in theme
562    /// - Parses JSON and stores in static PREPARED_THEMES collection
563    /// - Replaces any existing theme with the same name
564    pub fn setup_local_theme(theme_path: Option<&str>) {
565        let theme_data = if let Some(path) = theme_path {
566            // Try to load custom theme from path
567            if std::path::Path::new(path).exists() {
568                std::fs::read_to_string(path).ok()
569            } else {
570                // Fall back to embedded theme files or default theme
571                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            // Use embedded theme data first, then fall back to default
577            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        // Parse and prepare theme if available
583        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                    // Remove any existing theme with the same name
596                    themes.retain(|t| t.name != theme_name);
597                    themes.push(prepared_theme);
598                }
599            }
600        }
601    }
602    
603    // Build-time theme embedding system - includes theme JSON files as string constants for optimal performance
604    fn get_embedded_theme_data(theme_path: &str) -> Option<String> {
605        // For published packages, theme files are not included so we fallback to runtime loading
606        // Users should provide their own theme files or use the default programmatic theme
607        std::fs::read_to_string(theme_path).ok()
608    }
609    
610
611    /// Internal implementation for loading prepared themes to the global theme context
612    /// 
613    /// This function applies the first prepared theme from the PREPARED_THEMES collection
614    /// as the active global theme. It creates a new MaterialThemeContext with the theme
615    /// data and updates the global theme state.
616    /// 
617    /// # Behavior
618    /// - Takes the first theme from prepared themes collection
619    /// - Creates a MaterialThemeContext with default settings (Light mode, Normal contrast)
620    /// - Updates the global GLOBAL_THEME with the new context
621    /// - If no themes were prepared, the global theme remains unchanged
622    pub fn load_themes() {
623        if let Ok(prepared_themes) = PREPARED_THEMES.lock() {
624            if let Some(theme) = prepared_themes.first() {
625                // Load the first prepared theme as the active theme
626                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    /// Load all prepared fonts to the egui context
636    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                // Add font data
642                fonts.font_data.insert(
643                    prepared_font.name.clone(),
644                    prepared_font.data.clone(),
645                );
646                
647                // Add to font families
648                for family in &prepared_font.families {
649                    match family {
650                        FontFamily::Proportional => {
651                            // Google fonts go to the front, icon fonts go to the back
652                            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                    // For auto mode, we'll default to light for now
693                    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, // fallback
780            };
781            
782            Self::hex_to_color32(hex).unwrap_or(Color32::GRAY)
783        } else {
784            // Fallback colors when no theme is loaded (using material-theme4.json light values)
785            match name {
786                "primary" => Color32::from_rgb(72, 103, 47), // #48672F
787                "surfaceTint" => Color32::from_rgb(72, 103, 47), // #48672F
788                "onPrimary" => Color32::WHITE, // #FFFFFF
789                "primaryContainer" => Color32::from_rgb(200, 238, 168), // #C8EEA8
790                "onPrimaryContainer" => Color32::from_rgb(49, 79, 25), // #314F19
791                "secondary" => Color32::from_rgb(86, 98, 75), // #56624B
792                "onSecondary" => Color32::WHITE, // #FFFFFF
793                "secondaryContainer" => Color32::from_rgb(218, 231, 201), // #DAE7C9
794                "onSecondaryContainer" => Color32::from_rgb(63, 74, 52), // #3F4A34
795                "tertiary" => Color32::from_rgb(56, 102, 101), // #386665
796                "onTertiary" => Color32::WHITE, // #FFFFFF
797                "tertiaryContainer" => Color32::from_rgb(187, 236, 234), // #BBECEA
798                "onTertiaryContainer" => Color32::from_rgb(30, 78, 77), // #1E4E4D
799                "error" => Color32::from_rgb(186, 26, 26), // #BA1A1A
800                "onError" => Color32::WHITE, // #FFFFFF
801                "errorContainer" => Color32::from_rgb(255, 218, 214), // #FFDAD6
802                "onErrorContainer" => Color32::from_rgb(147, 0, 10), // #93000A
803                "background" => Color32::from_rgb(249, 250, 239), // #F9FAEF
804                "onBackground" => Color32::from_rgb(25, 29, 22), // #191D16
805                "surface" => Color32::from_rgb(249, 250, 239), // #F9FAEF
806                "onSurface" => Color32::from_rgb(25, 29, 22), // #191D16
807                "surfaceVariant" => Color32::from_rgb(224, 228, 214), // #E0E4D6
808                "onSurfaceVariant" => Color32::from_rgb(68, 72, 62), // #44483E
809                "outline" => Color32::from_rgb(116, 121, 109), // #74796D
810                "outlineVariant" => Color32::from_rgb(196, 200, 186), // #C4C8BA
811                "shadow" => Color32::BLACK, // #000000
812                "scrim" => Color32::BLACK, // #000000
813                "inverseSurface" => Color32::from_rgb(46, 49, 42), // #2E312A
814                "inverseOnSurface" => Color32::from_rgb(240, 242, 231), // #F0F2E7
815                "inversePrimary" => Color32::from_rgb(173, 210, 142), // #ADD28E
816                "primaryFixed" => Color32::from_rgb(200, 238, 168), // #C8EEA8
817                "onPrimaryFixed" => Color32::from_rgb(11, 32, 0), // #0B2000
818                "primaryFixedDim" => Color32::from_rgb(173, 210, 142), // #ADD28E
819                "onPrimaryFixedVariant" => Color32::from_rgb(49, 79, 25), // #314F19
820                "secondaryFixed" => Color32::from_rgb(218, 231, 201), // #DAE7C9
821                "onSecondaryFixed" => Color32::from_rgb(20, 30, 12), // #141E0C
822                "secondaryFixedDim" => Color32::from_rgb(190, 203, 174), // #BECBAE
823                "onSecondaryFixedVariant" => Color32::from_rgb(63, 74, 52), // #3F4A34
824                "tertiaryFixed" => Color32::from_rgb(187, 236, 234), // #BBECEA
825                "onTertiaryFixed" => Color32::from_rgb(0, 32, 31), // #00201F
826                "tertiaryFixedDim" => Color32::from_rgb(160, 207, 206), // #A0CFCE
827                "onTertiaryFixedVariant" => Color32::from_rgb(30, 78, 77), // #1E4E4D
828                "surfaceDim" => Color32::from_rgb(217, 219, 209), // #D9DBD1
829                "surfaceBright" => Color32::from_rgb(249, 250, 239), // #F9FAEF
830                "surfaceContainerLowest" => Color32::WHITE, // #FFFFFF
831                "surfaceContainerLow" => Color32::from_rgb(243, 245, 234), // #F3F5EA
832                "surfaceContainer" => Color32::from_rgb(237, 239, 228), // #EDEFE4
833                "surfaceContainerHigh" => Color32::from_rgb(231, 233, 222), // #E7E9DE
834                "surfaceContainerHighest" => Color32::from_rgb(226, 227, 217), // #E2E3D9
835                _ => 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
861// Global theme context accessible by all components
862static 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
869/// Update the global theme context with a new theme configuration
870/// 
871/// This function replaces the current global theme context with a new one.
872/// It's used internally by the theme system and can be used by applications
873/// to programmatically change theme settings at runtime.
874/// 
875/// # Arguments
876/// * `theme` - The new MaterialThemeContext to set as the global theme
877/// 
878/// # Usage
879/// This function is typically called by:
880/// - `load_themes()` - To apply a loaded theme as the global theme
881/// - Application code - To change theme mode, contrast level, or selected colors at runtime
882/// 
883/// # Thread Safety
884/// This function is thread-safe and uses a mutex to ensure exclusive access
885/// to the global theme state.
886/// 
887/// # Example
888/// ```rust
889/// let mut theme_context = MaterialThemeContext::default();
890/// theme_context.theme_mode = ThemeMode::Dark;
891/// theme_context.contrast_level = ContrastLevel::High;
892/// update_global_theme(theme_context);
893/// ```
894pub fn update_global_theme(theme: MaterialThemeContext) {
895    if let Ok(mut global_theme) = GLOBAL_THEME.lock() {
896        *global_theme = theme;
897    }
898}
899
900/// Helper function to prepare Material Design fonts for the application
901/// Default font is "Google Sans Code" if not specified
902/// Note: Fonts are only prepared, call load_fonts() to actually load them
903pub fn setup_google_fonts(font_name: Option<&str>) {
904    MaterialThemeContext::setup_fonts(font_name);
905}
906
907/// Helper function to prepare local fonts from the resources directory
908/// 
909/// # Arguments
910/// * `font_path` - Optional path to a TTF font file. If None, uses the default MaterialSymbolsOutlined font
911/// Note: Fonts are only prepared, call load_fonts() to actually load them
912pub fn setup_local_fonts(font_path: Option<&str>) {
913    MaterialThemeContext::setup_local_fonts(font_path);
914}
915
916/// Prepare local Material Design themes for the application from JSON files
917/// 
918/// This function loads Material Design theme data from JSON files and prepares them for use.
919/// Theme data is included at build-time when using the default behavior (None path), or loaded
920/// at runtime when a specific path is provided.
921/// 
922/// # Arguments
923/// * `theme_path` - Optional path to a Material Design theme JSON file:
924///   - `Some(path)` - Load theme from the specified file path at runtime
925///   - `None` - Use themes that were included at build-time from the build script scan
926/// 
927/// # Build-time Theme Inclusion
928/// When `theme_path` is `None`, the build script automatically scans for JSON files in:
929/// - `resources/` directory
930/// - `examples/` directory
931/// 
932/// Files matching `*theme*.json` or `*material-theme*.json` patterns are included as constants.
933/// 
934/// # Example
935/// ```rust
936/// // Use build-time included themes (recommended for production)
937/// setup_local_theme(None);
938/// 
939/// // Load specific theme file at runtime (useful for development/testing)
940/// setup_local_theme(Some("resources/my-custom-theme.json"));
941/// setup_local_theme(Some("examples/material-theme6.json"));
942/// ```
943/// 
944/// # Note
945/// Themes are only prepared by this function. Call `load_themes()` after this to actually
946/// apply the prepared themes to the global theme context.
947pub fn setup_local_theme(theme_path: Option<&str>) {
948    MaterialThemeContext::setup_local_theme(theme_path);
949}
950
951/// Load all prepared themes to the global theme context
952/// 
953/// This function takes themes that were prepared by `setup_local_theme()` and applies
954/// the first prepared theme as the active global theme. This makes the theme available
955/// to all Material Design components throughout the application.
956/// 
957/// # Usage
958/// This should be called after all `setup_local_theme()` calls and typically during
959/// application initialization.
960/// 
961/// # Example
962/// ```rust
963/// // Setup and load themes during app initialization
964/// setup_local_theme(Some("resources/my-theme.json"));
965/// load_themes();  // Apply the prepared theme globally
966/// ```
967/// 
968/// # Behavior
969/// - If multiple themes were prepared, only the first one becomes active
970/// - If no themes were prepared, the default built-in theme is used
971/// - The active theme becomes available via `get_global_color()` and other theme functions
972pub fn load_themes() {
973    MaterialThemeContext::load_themes();
974}
975
976/// Trait to provide a unified interface for accessing egui Context
977pub 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
993/// Load all prepared fonts to the egui context
994/// Call this after all setup_*_fonts functions to actually load the fonts
995pub fn load_fonts<C: ContextRef>(ctx: C) {
996    MaterialThemeContext::load_fonts(ctx.context_ref());
997}
998
999/// Update the window/panel background colors based on the current theme
1000/// 
1001/// This function automatically applies the appropriate background colors from the current
1002/// Material Design theme to the egui context. The background color is selected based on
1003/// the current theme mode (Light/Dark/Auto) and contrast level (Normal/Medium/High).
1004/// 
1005/// # Arguments
1006/// * `ctx` - The egui context to update with new background colors
1007/// 
1008/// # Background Color Selection
1009/// The function selects background colors according to Material Design guidelines:
1010/// 
1011/// **Dark Theme:**
1012/// - High contrast: `surfaceContainerHighest`
1013/// - Medium contrast: `surfaceContainerHigh` 
1014/// - Normal contrast: `surface`
1015/// 
1016/// **Light Theme:**
1017/// - High contrast: `surfaceContainerLowest`
1018/// - Medium contrast: `surfaceContainerLow`
1019/// - Normal contrast: `surface`
1020/// 
1021/// **Auto Theme:** Uses `surface` as default
1022/// 
1023/// # Usage
1024/// This function should be called:
1025/// - Once during application initialization (after `load_themes()`)
1026/// - Whenever theme settings change (mode or contrast level)
1027/// 
1028/// # Example
1029/// ```rust
1030/// // During app initialization in eframe::run_native
1031/// setup_local_theme(Some("my-theme.json"));
1032/// load_themes();
1033/// update_window_background(&cc.egui_ctx);  // Apply initial background
1034/// 
1035/// // When theme settings change at runtime
1036/// fn change_theme_mode(&mut self, ctx: &egui::Context) {
1037///     // Update theme mode in global context...
1038///     update_window_background(ctx);  // Apply new background
1039/// }
1040/// ```
1041/// 
1042/// # Effects
1043/// This function updates the following egui visual properties:
1044/// - `window_fill` - Background color for floating windows
1045/// - `panel_fill` - Background color for side panels and central panel  
1046/// - `extreme_bg_color` - Background color for extreme contrast areas
1047pub fn update_window_background<C: ContextRef>(ctx: C) {
1048    let ctx = ctx.context_ref();
1049    if let Ok(theme) = GLOBAL_THEME.lock() {
1050        // Get the appropriate background color from the material theme
1051        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"), // Default to surface for auto mode
1059        };
1060        
1061        // Apply the background color to the context
1062        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
1073/// Helper function to get a color by name from the global theme
1074pub 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        // Fallback colors when theme is not accessible
1079        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}