Skip to main content

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 light_medium_contrast_scheme = MaterialScheme {
374        primary: "#253D05".to_string(),
375        surface_tint: "#4C662B".to_string(),
376        on_primary: "#FFFFFF".to_string(),
377        primary_container: "#5A7539".to_string(),
378        on_primary_container: "#FFFFFF".to_string(),
379        secondary: "#303924".to_string(),
380        on_secondary: "#FFFFFF".to_string(),
381        secondary_container: "#667157".to_string(),
382        on_secondary_container: "#FFFFFF".to_string(),
383        tertiary: "#083D3A".to_string(),
384        on_tertiary: "#FFFFFF".to_string(),
385        tertiary_container: "#477572".to_string(),
386        on_tertiary_container: "#FFFFFF".to_string(),
387        error: "#740006".to_string(),
388        on_error: "#FFFFFF".to_string(),
389        error_container: "#CF2C27".to_string(),
390        on_error_container: "#FFFFFF".to_string(),
391        background: "#F9FAEF".to_string(),
392        on_background: "#1A1C16".to_string(),
393        surface: "#F9FAEF".to_string(),
394        on_surface: "#0F120C".to_string(),
395        surface_variant: "#E1E4D5".to_string(),
396        on_surface_variant: "#34382D".to_string(),
397        outline: "#505449".to_string(),
398        outline_variant: "#6B6F62".to_string(),
399        shadow: "#000000".to_string(),
400        scrim: "#000000".to_string(),
401        inverse_surface: "#2F312A".to_string(),
402        inverse_on_surface: "#F1F2E6".to_string(),
403        inverse_primary: "#B1D18A".to_string(),
404        primary_fixed: "#5A7539".to_string(),
405        on_primary_fixed: "#FFFFFF".to_string(),
406        primary_fixed_dim: "#425C23".to_string(),
407        on_primary_fixed_variant: "#FFFFFF".to_string(),
408        secondary_fixed: "#667157".to_string(),
409        on_secondary_fixed: "#FFFFFF".to_string(),
410        secondary_fixed_dim: "#4E5840".to_string(),
411        on_secondary_fixed_variant: "#FFFFFF".to_string(),
412        tertiary_fixed: "#477572".to_string(),
413        on_tertiary_fixed: "#FFFFFF".to_string(),
414        tertiary_fixed_dim: "#2E5C59".to_string(),
415        on_tertiary_fixed_variant: "#FFFFFF".to_string(),
416        surface_dim: "#C6C7BD".to_string(),
417        surface_bright: "#F9FAEF".to_string(),
418        surface_container_lowest: "#FFFFFF".to_string(),
419        surface_container_low: "#F3F4E9".to_string(),
420        surface_container: "#E8E9DE".to_string(),
421        surface_container_high: "#DCDED3".to_string(),
422        surface_container_highest: "#D1D3C8".to_string(),
423    };
424
425    let light_high_contrast_scheme = MaterialScheme {
426        primary: "#1C3200".to_string(),
427        surface_tint: "#4C662B".to_string(),
428        on_primary: "#FFFFFF".to_string(),
429        primary_container: "#375018".to_string(),
430        on_primary_container: "#FFFFFF".to_string(),
431        secondary: "#262F1A".to_string(),
432        on_secondary: "#FFFFFF".to_string(),
433        secondary_container: "#434C35".to_string(),
434        on_secondary_container: "#FFFFFF".to_string(),
435        tertiary: "#003230".to_string(),
436        on_tertiary: "#FFFFFF".to_string(),
437        tertiary_container: "#21504E".to_string(),
438        on_tertiary_container: "#FFFFFF".to_string(),
439        error: "#600004".to_string(),
440        on_error: "#FFFFFF".to_string(),
441        error_container: "#98000A".to_string(),
442        on_error_container: "#FFFFFF".to_string(),
443        background: "#F9FAEF".to_string(),
444        on_background: "#1A1C16".to_string(),
445        surface: "#F9FAEF".to_string(),
446        on_surface: "#000000".to_string(),
447        surface_variant: "#E1E4D5".to_string(),
448        on_surface_variant: "#000000".to_string(),
449        outline: "#2A2D24".to_string(),
450        outline_variant: "#474B40".to_string(),
451        shadow: "#000000".to_string(),
452        scrim: "#000000".to_string(),
453        inverse_surface: "#2F312A".to_string(),
454        inverse_on_surface: "#FFFFFF".to_string(),
455        inverse_primary: "#B1D18A".to_string(),
456        primary_fixed: "#375018".to_string(),
457        on_primary_fixed: "#FFFFFF".to_string(),
458        primary_fixed_dim: "#213903".to_string(),
459        on_primary_fixed_variant: "#FFFFFF".to_string(),
460        secondary_fixed: "#434C35".to_string(),
461        on_secondary_fixed: "#FFFFFF".to_string(),
462        secondary_fixed_dim: "#2C3620".to_string(),
463        on_secondary_fixed_variant: "#FFFFFF".to_string(),
464        tertiary_fixed: "#21504E".to_string(),
465        on_tertiary_fixed: "#FFFFFF".to_string(),
466        tertiary_fixed_dim: "#033937".to_string(),
467        on_tertiary_fixed_variant: "#FFFFFF".to_string(),
468        surface_dim: "#B8BAAF".to_string(),
469        surface_bright: "#F9FAEF".to_string(),
470        surface_container_lowest: "#FFFFFF".to_string(),
471        surface_container_low: "#F1F2E6".to_string(),
472        surface_container: "#E2E3D8".to_string(),
473        surface_container_high: "#D4D5CA".to_string(),
474        surface_container_highest: "#C6C7BD".to_string(),
475    };
476
477    let dark_medium_contrast_scheme = MaterialScheme {
478        primary: "#C7E79E".to_string(),
479        surface_tint: "#B1D18A".to_string(),
480        on_primary: "#172B00".to_string(),
481        primary_container: "#7D9A59".to_string(),
482        on_primary_container: "#000000".to_string(),
483        secondary: "#D5E1C2".to_string(),
484        on_secondary: "#1F2814".to_string(),
485        secondary_container: "#8A9579".to_string(),
486        on_secondary_container: "#000000".to_string(),
487        tertiary: "#B5E6E1".to_string(),
488        on_tertiary: "#002B29".to_string(),
489        tertiary_container: "#6B9995".to_string(),
490        on_tertiary_container: "#000000".to_string(),
491        error: "#FFD2CC".to_string(),
492        on_error: "#540003".to_string(),
493        error_container: "#FF5449".to_string(),
494        on_error_container: "#000000".to_string(),
495        background: "#12140E".to_string(),
496        on_background: "#E2E3D8".to_string(),
497        surface: "#12140E".to_string(),
498        on_surface: "#FFFFFF".to_string(),
499        surface_variant: "#44483D".to_string(),
500        on_surface_variant: "#DBDECF".to_string(),
501        outline: "#B0B3A6".to_string(),
502        outline_variant: "#8E9285".to_string(),
503        shadow: "#000000".to_string(),
504        scrim: "#000000".to_string(),
505        inverse_surface: "#E2E3D8".to_string(),
506        inverse_on_surface: "#282B24".to_string(),
507        inverse_primary: "#364F17".to_string(),
508        primary_fixed: "#CDEDA3".to_string(),
509        on_primary_fixed: "#081400".to_string(),
510        primary_fixed_dim: "#B1D18A".to_string(),
511        on_primary_fixed_variant: "#253D05".to_string(),
512        secondary_fixed: "#DCE7C8".to_string(),
513        on_secondary_fixed: "#0B1403".to_string(),
514        secondary_fixed_dim: "#BFCBAD".to_string(),
515        on_secondary_fixed_variant: "#303924".to_string(),
516        tertiary_fixed: "#BCECE7".to_string(),
517        on_tertiary_fixed: "#001413".to_string(),
518        tertiary_fixed_dim: "#A0D0CB".to_string(),
519        on_tertiary_fixed_variant: "#083D3A".to_string(),
520        surface_dim: "#12140E".to_string(),
521        surface_bright: "#43453D".to_string(),
522        surface_container_lowest: "#060804".to_string(),
523        surface_container_low: "#1C1E18".to_string(),
524        surface_container: "#262922".to_string(),
525        surface_container_high: "#31342C".to_string(),
526        surface_container_highest: "#3C3F37".to_string(),
527    };
528
529    let dark_high_contrast_scheme = MaterialScheme {
530        primary: "#DAFBB0".to_string(),
531        surface_tint: "#B1D18A".to_string(),
532        on_primary: "#000000".to_string(),
533        primary_container: "#ADCD86".to_string(),
534        on_primary_container: "#050E00".to_string(),
535        secondary: "#E9F4D5".to_string(),
536        on_secondary: "#000000".to_string(),
537        secondary_container: "#BCC7A9".to_string(),
538        on_secondary_container: "#060D01".to_string(),
539        tertiary: "#C9F9F5".to_string(),
540        on_tertiary: "#000000".to_string(),
541        tertiary_container: "#9CCCC7".to_string(),
542        on_tertiary_container: "#000E0D".to_string(),
543        error: "#FFECE9".to_string(),
544        on_error: "#000000".to_string(),
545        error_container: "#FFAEA4".to_string(),
546        on_error_container: "#220001".to_string(),
547        background: "#12140E".to_string(),
548        on_background: "#E2E3D8".to_string(),
549        surface: "#12140E".to_string(),
550        on_surface: "#FFFFFF".to_string(),
551        surface_variant: "#44483D".to_string(),
552        on_surface_variant: "#FFFFFF".to_string(),
553        outline: "#EEF2E2".to_string(),
554        outline_variant: "#C1C4B6".to_string(),
555        shadow: "#000000".to_string(),
556        scrim: "#000000".to_string(),
557        inverse_surface: "#E2E3D8".to_string(),
558        inverse_on_surface: "#000000".to_string(),
559        inverse_primary: "#364F17".to_string(),
560        primary_fixed: "#CDEDA3".to_string(),
561        on_primary_fixed: "#000000".to_string(),
562        primary_fixed_dim: "#B1D18A".to_string(),
563        on_primary_fixed_variant: "#081400".to_string(),
564        secondary_fixed: "#DCE7C8".to_string(),
565        on_secondary_fixed: "#000000".to_string(),
566        secondary_fixed_dim: "#BFCBAD".to_string(),
567        on_secondary_fixed_variant: "#0B1403".to_string(),
568        tertiary_fixed: "#BCECE7".to_string(),
569        on_tertiary_fixed: "#000000".to_string(),
570        tertiary_fixed_dim: "#A0D0CB".to_string(),
571        on_tertiary_fixed_variant: "#001413".to_string(),
572        surface_dim: "#12140E".to_string(),
573        surface_bright: "#4F5149".to_string(),
574        surface_container_lowest: "#000000".to_string(),
575        surface_container_low: "#1E201A".to_string(),
576        surface_container: "#2F312A".to_string(),
577        surface_container_high: "#3A3C35".to_string(),
578        surface_container_highest: "#454840".to_string(),
579    };
580
581    let mut schemes = HashMap::new();
582    schemes.insert("light".to_string(), light_scheme);
583    schemes.insert(
584        "light-medium-contrast".to_string(),
585        light_medium_contrast_scheme,
586    );
587    schemes.insert(
588        "light-high-contrast".to_string(),
589        light_high_contrast_scheme,
590    );
591    schemes.insert("dark".to_string(), dark_scheme);
592    schemes.insert(
593        "dark-medium-contrast".to_string(),
594        dark_medium_contrast_scheme,
595    );
596    schemes.insert("dark-high-contrast".to_string(), dark_high_contrast_scheme);
597
598    let mut core_colors = HashMap::new();
599    core_colors.insert("primary".to_string(), "#5C883A".to_string());
600
601    MaterialThemeFile {
602        description: "TYPE: CUSTOM Material Theme Builder export 2025-08-21 11:51:45".to_string(),
603        seed: "#5C883A".to_string(),
604        core_colors,
605        extended_colors: Vec::new(),
606        schemes,
607        palettes: HashMap::new(),
608    }
609}
610
611impl MaterialThemeContext {
612    pub fn setup_fonts(font_name: Option<&str>) {
613        let font_name = font_name.unwrap_or("Google Sans Code");
614
615        // Check if font exists in resources directory first
616        let font_file_path = format!(
617            "resources/{}.ttf",
618            font_name.replace(" ", "-").to_lowercase()
619        );
620
621        let font_data = if std::path::Path::new(&font_file_path).exists() {
622            // Use local font file with include_bytes!
623            Self::load_local_font(&font_file_path)
624        } else {
625            // Download font from Google Fonts at runtime (only if ondemand feature is enabled)
626            #[cfg(feature = "ondemand")]
627            {
628                Self::download_google_font(font_name)
629            }
630            #[cfg(not(feature = "ondemand"))]
631            {
632                eprintln!(
633                    "Font '{}' not found locally and ondemand feature is not enabled",
634                    font_name
635                );
636                None
637            }
638        };
639
640        if let Some(data) = font_data {
641            let font_family_name = font_name.replace(" ", "");
642
643            let prepared_font = PreparedFont {
644                name: font_family_name.clone(),
645                data: Arc::new(FontData::from_owned(data)),
646                families: vec![FontFamily::Proportional, FontFamily::Monospace],
647            };
648
649            if let Ok(mut fonts) = PREPARED_FONTS.lock() {
650                // Remove any existing font with the same name
651                fonts.retain(|f| f.name != font_family_name);
652                fonts.push(prepared_font);
653            }
654        }
655    }
656
657    fn load_local_font(font_path: &str) -> Option<Vec<u8>> {
658        std::fs::read(font_path).ok()
659    }
660
661    // On-demand font downloading feature - downloads Google Fonts at runtime when ondemand feature is enabled
662    #[cfg(feature = "ondemand")]
663    fn download_google_font(font_name: &str) -> Option<Vec<u8>> {
664        // Convert font name to Google Fonts URL format
665        let font_url_name = font_name.replace(" ", "+");
666
667        // First, get the CSS file to find the actual font URL
668        let css_url = format!(
669            "https://fonts.googleapis.com/css2?family={}:wght@400&display=swap",
670            font_url_name
671        );
672
673        match ureq::get(&css_url)
674            .set(
675                "User-Agent",
676                "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
677            )
678            .call()
679        {
680            Ok(response) => {
681                let css_content = response.into_string().ok()?;
682
683                // Parse CSS to find TTF URL
684                let font_url = Self::extract_font_url_from_css(&css_content)?;
685
686                // Download the actual font file
687                match ureq::get(&font_url).call() {
688                    Ok(font_response) => {
689                        let mut font_data = Vec::new();
690                        if font_response
691                            .into_reader()
692                            .read_to_end(&mut font_data)
693                            .is_ok()
694                        {
695                            // Save font to resources directory for future use
696                            let target_path = format!(
697                                "resources/{}.ttf",
698                                font_name.replace(" ", "-").to_lowercase()
699                            );
700                            if let Ok(()) = std::fs::write(&target_path, &font_data) {
701                                eprintln!(
702                                    "Font '{}' downloaded and saved to {}",
703                                    font_name, target_path
704                                );
705                            }
706                            Some(font_data)
707                        } else {
708                            eprintln!("Failed to read font data for '{}'", font_name);
709                            None
710                        }
711                    }
712                    Err(e) => {
713                        eprintln!("Failed to download font '{}': {}", font_name, e);
714                        None
715                    }
716                }
717            }
718            Err(e) => {
719                eprintln!("Failed to fetch CSS for font '{}': {}", font_name, e);
720                None
721            }
722        }
723    }
724
725    #[cfg(feature = "ondemand")]
726    fn extract_font_url_from_css(css_content: &str) -> Option<String> {
727        // Look for TTF URLs in the CSS content
728        // Google Fonts CSS contains lines like: src: url(https://fonts.gstatic.com/...) format('truetype');
729        for line in css_content.lines() {
730            if line.contains("src:") && line.contains("url(") && line.contains("format('truetype')")
731            {
732                if let Some(start) = line.find("url(") {
733                    let start = start + 4; // Skip "url("
734                    if let Some(end) = line[start..].find(")") {
735                        let url = &line[start..start + end];
736                        return Some(url.to_string());
737                    }
738                }
739            }
740        }
741        None
742    }
743
744    pub fn setup_local_fonts(font_path: Option<&str>) {
745        // let default_material_symbols_path =
746        //     "resources/MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf";
747
748        // Load custom text font if provided
749        if let Some(path) = font_path {
750            if std::path::Path::new(path).exists() {
751                if let Ok(data) = std::fs::read(path) {
752                    // Derive font name from filename (e.g. "noto-sans-kr.ttf" -> "NotoSansKr")
753                    let font_name = std::path::Path::new(path)
754                        .file_stem()
755                        .and_then(|s| s.to_str())
756                        .unwrap_or("CustomFont")
757                        .split(|c: char| c == '-' || c == '_')
758                        .map(|part| {
759                            let mut chars = part.chars();
760                            match chars.next() {
761                                Some(first) => {
762                                    let upper: String = first.to_uppercase().collect();
763                                    format!("{}{}", upper, chars.as_str())
764                                }
765                                None => String::new(),
766                            }
767                        })
768                        .collect::<String>();
769
770                    let prepared_font = PreparedFont {
771                        name: font_name.clone(),
772                        data: Arc::new(FontData::from_owned(data)),
773                        families: vec![FontFamily::Proportional, FontFamily::Monospace],
774                    };
775
776                    if let Ok(mut fonts) = PREPARED_FONTS.lock() {
777                        fonts.retain(|f| f.name != font_name);
778                        fonts.push(prepared_font);
779                    }
780                }
781            }
782        }
783
784        // Always load Material Symbols icon font
785        // let icon_font_data = if std::path::Path::new(default_material_symbols_path).exists() {
786        //     std::fs::read(default_material_symbols_path).ok()
787        // } else {
788        //     Self::get_embedded_material_symbols()
789        // };
790
791        // if let Some(data) = icon_font_data {
792        //     let prepared_font = PreparedFont {
793        //         name: "MaterialSymbolsOutlined".to_owned(),
794        //         data: Arc::new(FontData::from_owned(data)),
795        //         families: vec![FontFamily::Proportional, FontFamily::Monospace],
796        //     };
797
798        //     if let Ok(mut fonts) = PREPARED_FONTS.lock() {
799        //         fonts.retain(|f| f.name != "MaterialSymbolsOutlined");
800        //         fonts.push(prepared_font);
801        //     }
802        // }
803    }
804
805    pub fn setup_local_fonts_from_bytes(font_name: &str, font_data: &[u8]) {
806        let prepared_font = PreparedFont {
807            name: font_name.to_owned(),
808            data: Arc::new(FontData::from_owned(font_data.to_vec())),
809            families: vec![FontFamily::Proportional, FontFamily::Monospace],
810        };
811
812        if let Ok(mut fonts) = PREPARED_FONTS.lock() {
813            fonts.retain(|f| f.name != font_name);
814            fonts.push(prepared_font);
815        }
816    }
817
818    // Fallback font embedding system - includes Material Symbols font at build-time if available
819    fn get_embedded_material_symbols() -> Option<Vec<u8>> {
820        // Font files are excluded from package distribution, so this will always return None in published packages
821        // Users should provide their own font files or use the ondemand feature
822        None
823    }
824
825    /// Internal implementation for preparing local themes from JSON files
826    ///
827    /// This function handles the loading and parsing of Material Design theme JSON files.
828    /// It supports both runtime file loading and build-time constant inclusion.
829    ///
830    /// # Arguments
831    /// * `theme_path` - Optional path to theme JSON file. If None, uses build-time constants.
832    ///
833    /// # Implementation Details
834    /// - First attempts to load from specified file path (if provided)
835    /// - Falls back to build-time included theme constants
836    /// - Finally falls back to default built-in theme
837    /// - Parses JSON and stores in static PREPARED_THEMES collection
838    /// - Replaces any existing theme with the same name
839    pub fn setup_local_theme(theme_path: Option<&str>) {
840        let theme_data = if let Some(path) = theme_path {
841            // Try to load custom theme from path
842            if std::path::Path::new(path).exists() {
843                std::fs::read_to_string(path).ok()
844            } else {
845                // Fall back to embedded theme files or default theme
846                Self::get_embedded_theme_data(path).or_else(|| {
847                    Some(serde_json::to_string(&get_default_material_theme()).unwrap_or_default())
848                })
849            }
850        } else {
851            // Use embedded theme data first, then fall back to default
852            Self::get_embedded_theme_data("resources/material-theme1.json").or_else(|| {
853                Some(serde_json::to_string(&get_default_material_theme()).unwrap_or_default())
854            })
855        };
856
857        // Parse and prepare theme if available
858        if let Some(data) = theme_data {
859            if let Ok(theme_file) = serde_json::from_str::<MaterialThemeFile>(&data) {
860                let theme_name = theme_path
861                    .and_then(|p| {
862                        std::path::Path::new(p)
863                            .file_stem()
864                            .map(|s| s.to_string_lossy().to_string())
865                    })
866                    .unwrap_or_else(|| "default".to_string());
867
868                let prepared_theme = PreparedTheme {
869                    name: theme_name.clone(),
870                    theme_data: theme_file,
871                };
872
873                if let Ok(mut themes) = PREPARED_THEMES.lock() {
874                    // Remove any existing theme with the same name
875                    themes.retain(|t| t.name != theme_name);
876                    themes.push(prepared_theme);
877                }
878            }
879        }
880    }
881
882    // Build-time theme embedding system - includes theme JSON files as string constants for optimal performance
883    fn get_embedded_theme_data(theme_path: &str) -> Option<String> {
884        // For published packages, theme files are not included so we fallback to runtime loading
885        // Users should provide their own theme files or use the default programmatic theme
886        std::fs::read_to_string(theme_path).ok()
887    }
888
889    /// Internal implementation for loading prepared themes to the global theme context
890    ///
891    /// This function applies the first prepared theme from the PREPARED_THEMES collection
892    /// as the active global theme. It creates a new MaterialThemeContext with the theme
893    /// data and updates the global theme state.
894    ///
895    /// # Behavior
896    /// - Takes the first theme from prepared themes collection
897    /// - Creates a MaterialThemeContext with default settings (Light mode, Normal contrast)
898    /// - Updates the global GLOBAL_THEME with the new context
899    /// - If no themes were prepared, the global theme remains unchanged
900    pub fn load_themes() {
901        if let Ok(prepared_themes) = PREPARED_THEMES.lock() {
902            if let Some(theme) = prepared_themes.first() {
903                // Load the first prepared theme as the active theme
904                let theme_context = MaterialThemeContext {
905                    material_theme: Some(theme.theme_data.clone()),
906                    ..Default::default()
907                };
908                update_global_theme(theme_context);
909            }
910        }
911    }
912
913    /// Load all prepared fonts to the egui context
914    pub fn load_fonts(ctx: &egui::Context) {
915        let mut fonts = FontDefinitions::default();
916
917        if let Ok(prepared_fonts) = PREPARED_FONTS.lock() {
918            for prepared_font in prepared_fonts.iter() {
919                // Add font data
920                fonts
921                    .font_data
922                    .insert(prepared_font.name.clone(), prepared_font.data.clone());
923
924                // Add to font families
925                for family in &prepared_font.families {
926                    match family {
927                        FontFamily::Proportional => {
928                            // Google fonts go to the front, icon fonts go to the back
929                            if prepared_font.name.contains("MaterialSymbols") {
930                                fonts
931                                    .families
932                                    .entry(FontFamily::Proportional)
933                                    .or_default()
934                                    .push(prepared_font.name.clone());
935                            } else {
936                                fonts
937                                    .families
938                                    .entry(FontFamily::Proportional)
939                                    .or_default()
940                                    .insert(0, prepared_font.name.clone());
941                            }
942                        }
943                        FontFamily::Monospace => {
944                            fonts
945                                .families
946                                .entry(FontFamily::Monospace)
947                                .or_default()
948                                .push(prepared_font.name.clone());
949                        }
950                        _ => {}
951                    }
952                }
953            }
954        }
955
956        ctx.set_fonts(fonts);
957    }
958
959    pub fn get_current_scheme(&self) -> Option<&MaterialScheme> {
960        if let Some(ref theme) = self.material_theme {
961            let scheme_key = match (self.theme_mode, self.contrast_level) {
962                (ThemeMode::Light, ContrastLevel::Normal) => "light",
963                (ThemeMode::Light, ContrastLevel::Medium) => "light-medium-contrast",
964                (ThemeMode::Light, ContrastLevel::High) => "light-high-contrast",
965                (ThemeMode::Dark, ContrastLevel::Normal) => "dark",
966                (ThemeMode::Dark, ContrastLevel::Medium) => "dark-medium-contrast",
967                (ThemeMode::Dark, ContrastLevel::High) => "dark-high-contrast",
968                (ThemeMode::Auto, contrast) => {
969                    // For auto mode, we'll default to light for now
970                    match contrast {
971                        ContrastLevel::Normal => "light",
972                        ContrastLevel::Medium => "light-medium-contrast",
973                        ContrastLevel::High => "light-high-contrast",
974                    }
975                }
976            };
977            theme.schemes.get(scheme_key)
978        } else {
979            None
980        }
981    }
982
983    pub fn hex_to_color32(hex: &str) -> Option<Color32> {
984        if hex.starts_with('#') && hex.len() == 7 {
985            if let Ok(r) = u8::from_str_radix(&hex[1..3], 16) {
986                if let Ok(g) = u8::from_str_radix(&hex[3..5], 16) {
987                    if let Ok(b) = u8::from_str_radix(&hex[5..7], 16) {
988                        return Some(Color32::from_rgb(r, g, b));
989                    }
990                }
991            }
992        }
993        None
994    }
995
996    pub fn color32_to_hex(color: Color32) -> String {
997        format!("#{:02X}{:02X}{:02X}", color.r(), color.g(), color.b())
998    }
999
1000    pub fn get_color_by_name(&self, name: &str) -> Color32 {
1001        if let Some(color) = self.selected_colors.get(name) {
1002            return *color;
1003        }
1004
1005        if let Some(scheme) = self.get_current_scheme() {
1006            let hex = match name {
1007                "primary" => &scheme.primary,
1008                "surfaceTint" => &scheme.surface_tint,
1009                "onPrimary" => &scheme.on_primary,
1010                "primaryContainer" => &scheme.primary_container,
1011                "onPrimaryContainer" => &scheme.on_primary_container,
1012                "secondary" => &scheme.secondary,
1013                "onSecondary" => &scheme.on_secondary,
1014                "secondaryContainer" => &scheme.secondary_container,
1015                "onSecondaryContainer" => &scheme.on_secondary_container,
1016                "tertiary" => &scheme.tertiary,
1017                "onTertiary" => &scheme.on_tertiary,
1018                "tertiaryContainer" => &scheme.tertiary_container,
1019                "onTertiaryContainer" => &scheme.on_tertiary_container,
1020                "error" => &scheme.error,
1021                "onError" => &scheme.on_error,
1022                "errorContainer" => &scheme.error_container,
1023                "onErrorContainer" => &scheme.on_error_container,
1024                "background" => &scheme.background,
1025                "onBackground" => &scheme.on_background,
1026                "surface" => &scheme.surface,
1027                "onSurface" => &scheme.on_surface,
1028                "surfaceVariant" => &scheme.surface_variant,
1029                "onSurfaceVariant" => &scheme.on_surface_variant,
1030                "outline" => &scheme.outline,
1031                "outlineVariant" => &scheme.outline_variant,
1032                "shadow" => &scheme.shadow,
1033                "scrim" => &scheme.scrim,
1034                "inverseSurface" => &scheme.inverse_surface,
1035                "inverseOnSurface" => &scheme.inverse_on_surface,
1036                "inversePrimary" => &scheme.inverse_primary,
1037                "primaryFixed" => &scheme.primary_fixed,
1038                "onPrimaryFixed" => &scheme.on_primary_fixed,
1039                "primaryFixedDim" => &scheme.primary_fixed_dim,
1040                "onPrimaryFixedVariant" => &scheme.on_primary_fixed_variant,
1041                "secondaryFixed" => &scheme.secondary_fixed,
1042                "onSecondaryFixed" => &scheme.on_secondary_fixed,
1043                "secondaryFixedDim" => &scheme.secondary_fixed_dim,
1044                "onSecondaryFixedVariant" => &scheme.on_secondary_fixed_variant,
1045                "tertiaryFixed" => &scheme.tertiary_fixed,
1046                "onTertiaryFixed" => &scheme.on_tertiary_fixed,
1047                "tertiaryFixedDim" => &scheme.tertiary_fixed_dim,
1048                "onTertiaryFixedVariant" => &scheme.on_tertiary_fixed_variant,
1049                "surfaceDim" => &scheme.surface_dim,
1050                "surfaceBright" => &scheme.surface_bright,
1051                "surfaceContainerLowest" => &scheme.surface_container_lowest,
1052                "surfaceContainerLow" => &scheme.surface_container_low,
1053                "surfaceContainer" => &scheme.surface_container,
1054                "surfaceContainerHigh" => &scheme.surface_container_high,
1055                "surfaceContainerHighest" => &scheme.surface_container_highest,
1056                _ => return Color32::GRAY, // fallback
1057            };
1058
1059            Self::hex_to_color32(hex).unwrap_or(Color32::GRAY)
1060        } else {
1061            // Fallback colors when no theme is loaded (using material-theme4.json light values)
1062            match name {
1063                "primary" => Color32::from_rgb(72, 103, 47), // #48672F
1064                "surfaceTint" => Color32::from_rgb(72, 103, 47), // #48672F
1065                "onPrimary" => Color32::WHITE,               // #FFFFFF
1066                "primaryContainer" => Color32::from_rgb(200, 238, 168), // #C8EEA8
1067                "onPrimaryContainer" => Color32::from_rgb(49, 79, 25), // #314F19
1068                "secondary" => Color32::from_rgb(86, 98, 75), // #56624B
1069                "onSecondary" => Color32::WHITE,             // #FFFFFF
1070                "secondaryContainer" => Color32::from_rgb(218, 231, 201), // #DAE7C9
1071                "onSecondaryContainer" => Color32::from_rgb(63, 74, 52), // #3F4A34
1072                "tertiary" => Color32::from_rgb(56, 102, 101), // #386665
1073                "onTertiary" => Color32::WHITE,              // #FFFFFF
1074                "tertiaryContainer" => Color32::from_rgb(187, 236, 234), // #BBECEA
1075                "onTertiaryContainer" => Color32::from_rgb(30, 78, 77), // #1E4E4D
1076                "error" => Color32::from_rgb(186, 26, 26),   // #BA1A1A
1077                "onError" => Color32::WHITE,                 // #FFFFFF
1078                "errorContainer" => Color32::from_rgb(255, 218, 214), // #FFDAD6
1079                "onErrorContainer" => Color32::from_rgb(147, 0, 10), // #93000A
1080                "background" => Color32::from_rgb(249, 250, 239), // #F9FAEF
1081                "onBackground" => Color32::from_rgb(25, 29, 22), // #191D16
1082                "surface" => Color32::from_rgb(249, 250, 239), // #F9FAEF
1083                "onSurface" => Color32::from_rgb(25, 29, 22), // #191D16
1084                "surfaceVariant" => Color32::from_rgb(224, 228, 214), // #E0E4D6
1085                "onSurfaceVariant" => Color32::from_rgb(68, 72, 62), // #44483E
1086                "outline" => Color32::from_rgb(116, 121, 109), // #74796D
1087                "outlineVariant" => Color32::from_rgb(196, 200, 186), // #C4C8BA
1088                "shadow" => Color32::BLACK,                  // #000000
1089                "scrim" => Color32::BLACK,                   // #000000
1090                "inverseSurface" => Color32::from_rgb(46, 49, 42), // #2E312A
1091                "inverseOnSurface" => Color32::from_rgb(240, 242, 231), // #F0F2E7
1092                "inversePrimary" => Color32::from_rgb(173, 210, 142), // #ADD28E
1093                "primaryFixed" => Color32::from_rgb(200, 238, 168), // #C8EEA8
1094                "onPrimaryFixed" => Color32::from_rgb(11, 32, 0), // #0B2000
1095                "primaryFixedDim" => Color32::from_rgb(173, 210, 142), // #ADD28E
1096                "onPrimaryFixedVariant" => Color32::from_rgb(49, 79, 25), // #314F19
1097                "secondaryFixed" => Color32::from_rgb(218, 231, 201), // #DAE7C9
1098                "onSecondaryFixed" => Color32::from_rgb(20, 30, 12), // #141E0C
1099                "secondaryFixedDim" => Color32::from_rgb(190, 203, 174), // #BECBAE
1100                "onSecondaryFixedVariant" => Color32::from_rgb(63, 74, 52), // #3F4A34
1101                "tertiaryFixed" => Color32::from_rgb(187, 236, 234), // #BBECEA
1102                "onTertiaryFixed" => Color32::from_rgb(0, 32, 31), // #00201F
1103                "tertiaryFixedDim" => Color32::from_rgb(160, 207, 206), // #A0CFCE
1104                "onTertiaryFixedVariant" => Color32::from_rgb(30, 78, 77), // #1E4E4D
1105                "surfaceDim" => Color32::from_rgb(217, 219, 209), // #D9DBD1
1106                "surfaceBright" => Color32::from_rgb(249, 250, 239), // #F9FAEF
1107                "surfaceContainerLowest" => Color32::WHITE,  // #FFFFFF
1108                "surfaceContainerLow" => Color32::from_rgb(243, 245, 234), // #F3F5EA
1109                "surfaceContainer" => Color32::from_rgb(237, 239, 228), // #EDEFE4
1110                "surfaceContainerHigh" => Color32::from_rgb(231, 233, 222), // #E7E9DE
1111                "surfaceContainerHighest" => Color32::from_rgb(226, 227, 217), // #E2E3D9
1112                _ => Color32::GRAY,
1113            }
1114        }
1115    }
1116
1117    pub fn get_primary_color(&self) -> Color32 {
1118        self.get_color_by_name("primary")
1119    }
1120
1121    pub fn get_secondary_color(&self) -> Color32 {
1122        self.get_color_by_name("secondary")
1123    }
1124
1125    pub fn get_tertiary_color(&self) -> Color32 {
1126        self.get_color_by_name("tertiary")
1127    }
1128
1129    pub fn get_surface_color(&self, _dark_mode: bool) -> Color32 {
1130        self.get_color_by_name("surface")
1131    }
1132
1133    pub fn get_on_primary_color(&self) -> Color32 {
1134        self.get_color_by_name("onPrimary")
1135    }
1136}
1137
1138// Global theme context accessible by all components
1139static GLOBAL_THEME: std::sync::LazyLock<Arc<Mutex<MaterialThemeContext>>> =
1140    std::sync::LazyLock::new(|| Arc::new(Mutex::new(MaterialThemeContext::default())));
1141
1142pub fn get_global_theme() -> Arc<Mutex<MaterialThemeContext>> {
1143    GLOBAL_THEME.clone()
1144}
1145
1146/// Update the global theme context with a new theme configuration
1147///
1148/// This function replaces the current global theme context with a new one.
1149/// It's used internally by the theme system and can be used by applications
1150/// to programmatically change theme settings at runtime.
1151///
1152/// # Arguments
1153/// * `theme` - The new MaterialThemeContext to set as the global theme
1154///
1155/// # Usage
1156/// This function is typically called by:
1157/// - `load_themes()` - To apply a loaded theme as the global theme
1158/// - Application code - To change theme mode, contrast level, or selected colors at runtime
1159///
1160/// # Thread Safety
1161/// This function is thread-safe and uses a mutex to ensure exclusive access
1162/// to the global theme state.
1163///
1164/// # Example
1165/// ```rust
1166/// let mut theme_context = MaterialThemeContext::default();
1167/// theme_context.theme_mode = ThemeMode::Dark;
1168/// theme_context.contrast_level = ContrastLevel::High;
1169/// update_global_theme(theme_context);
1170/// ```
1171pub fn update_global_theme(theme: MaterialThemeContext) {
1172    if let Ok(mut global_theme) = GLOBAL_THEME.lock() {
1173        *global_theme = theme;
1174    }
1175}
1176
1177/// Helper function to prepare Material Design fonts for the application
1178/// Default font is "Google Sans Code" if not specified
1179/// Note: Fonts are only prepared, call load_fonts() to actually load them
1180pub fn setup_google_fonts(font_name: Option<&str>) {
1181    MaterialThemeContext::setup_fonts(font_name);
1182}
1183
1184/// Helper function to prepare local fonts from the resources directory
1185///
1186/// # Arguments
1187/// * `font_path` - Optional path to a TTF font file. If None, uses the default MaterialSymbolsOutlined font
1188/// Note: Fonts are only prepared, call load_fonts() to actually load them
1189pub fn setup_local_fonts(font_path: Option<&str>) {
1190    MaterialThemeContext::setup_local_fonts(font_path);
1191}
1192
1193/// Prepare a local font from pre-loaded byte data
1194///
1195/// # Arguments
1196/// * `font_name` - Name to register the font under (must not contain "MaterialSymbols" for text fonts)
1197/// * `font_data` - Raw TTF/OTF font bytes (e.g. from `include_bytes!`)
1198///
1199/// Note: Fonts are only prepared, call load_fonts() to actually load them
1200pub fn setup_local_fonts_from_bytes(font_name: &str, font_data: &[u8]) {
1201    MaterialThemeContext::setup_local_fonts_from_bytes(font_name, font_data);
1202}
1203
1204/// Prepare local Material Design themes for the application from JSON files
1205///
1206/// This function loads Material Design theme data from JSON files and prepares them for use.
1207/// Theme data is included at build-time when using the default behavior (None path), or loaded
1208/// at runtime when a specific path is provided.
1209///
1210/// # Arguments
1211/// * `theme_path` - Optional path to a Material Design theme JSON file:
1212///   - `Some(path)` - Load theme from the specified file path at runtime
1213///   - `None` - Use themes that were included at build-time from the build script scan
1214///
1215/// # Build-time Theme Inclusion
1216/// When `theme_path` is `None`, the build script automatically scans for JSON files in:
1217/// - `resources/` directory
1218/// - `examples/` directory
1219///
1220/// Files matching `*theme*.json` or `*material-theme*.json` patterns are included as constants.
1221///
1222/// # Example
1223/// ```rust
1224/// // Use build-time included themes (recommended for production)
1225/// setup_local_theme(None);
1226///
1227/// // Load specific theme file at runtime (useful for development/testing)
1228/// setup_local_theme(Some("resources/my-custom-theme.json"));
1229/// setup_local_theme(Some("examples/material-theme6.json"));
1230/// ```
1231///
1232/// # Note
1233/// Themes are only prepared by this function. Call `load_themes()` after this to actually
1234/// apply the prepared themes to the global theme context.
1235pub fn setup_local_theme(theme_path: Option<&str>) {
1236    MaterialThemeContext::setup_local_theme(theme_path);
1237}
1238
1239/// Load all prepared themes to the global theme context
1240///
1241/// This function takes themes that were prepared by `setup_local_theme()` and applies
1242/// the first prepared theme as the active global theme. This makes the theme available
1243/// to all Material Design components throughout the application.
1244///
1245/// # Usage
1246/// This should be called after all `setup_local_theme()` calls and typically during
1247/// application initialization.
1248///
1249/// # Example
1250/// ```rust
1251/// // Setup and load themes during app initialization
1252/// setup_local_theme(Some("resources/my-theme.json"));
1253/// load_themes();  // Apply the prepared theme globally
1254/// ```
1255///
1256/// # Behavior
1257/// - If multiple themes were prepared, only the first one becomes active
1258/// - If no themes were prepared, the default built-in theme is used
1259/// - The active theme becomes available via `get_global_color()` and other theme functions
1260pub fn load_themes() {
1261    MaterialThemeContext::load_themes();
1262}
1263
1264/// Trait to provide a unified interface for accessing egui Context
1265pub trait ContextRef {
1266    fn context_ref(&self) -> &egui::Context;
1267}
1268
1269impl ContextRef for egui::Context {
1270    fn context_ref(&self) -> &egui::Context {
1271        self
1272    }
1273}
1274
1275impl ContextRef for &egui::Context {
1276    fn context_ref(&self) -> &egui::Context {
1277        self
1278    }
1279}
1280
1281/// Load all prepared fonts to the egui context
1282/// Call this after all setup_*_fonts functions to actually load the fonts
1283pub fn load_fonts<C: ContextRef>(ctx: C) {
1284    MaterialThemeContext::load_fonts(ctx.context_ref());
1285}
1286
1287/// Update the window/panel background colors based on the current theme
1288///
1289/// This function automatically applies the appropriate background colors from the current
1290/// Material Design theme to the egui context. The background color is selected based on
1291/// the current theme mode (Light/Dark/Auto) and contrast level (Normal/Medium/High).
1292///
1293/// # Arguments
1294/// * `ctx` - The egui context to update with new background colors
1295///
1296/// # Background Color Selection
1297/// The function selects background colors according to Material Design guidelines:
1298///
1299/// **Dark Theme:**
1300/// - High contrast: `surfaceContainerHighest`
1301/// - Medium contrast: `surfaceContainerHigh`
1302/// - Normal contrast: `surface`
1303///
1304/// **Light Theme:**
1305/// - High contrast: `surfaceContainerLowest`
1306/// - Medium contrast: `surfaceContainerLow`
1307/// - Normal contrast: `surface`
1308///
1309/// **Auto Theme:** Uses `surface` as default
1310///
1311/// # Usage
1312/// This function should be called:
1313/// - Once during application initialization (after `load_themes()`)
1314/// - Whenever theme settings change (mode or contrast level)
1315///
1316/// # Example
1317/// ```rust
1318/// // During app initialization in eframe::run_native
1319/// setup_local_theme(Some("my-theme.json"));
1320/// load_themes();
1321/// update_window_background(&cc.egui_ctx);  // Apply initial background
1322///
1323/// // When theme settings change at runtime
1324/// fn change_theme_mode(&mut self, ctx: &egui::Context) {
1325///     // Update theme mode in global context...
1326///     update_window_background(ctx);  // Apply new background
1327/// }
1328/// ```
1329///
1330/// # Effects
1331/// This function updates the following egui visual properties:
1332/// - `window_fill` - Background color for floating windows
1333/// - `panel_fill` - Background color for side panels and central panel  
1334/// - `extreme_bg_color` - Background color for extreme contrast areas
1335pub fn update_window_background<C: ContextRef>(ctx: C) {
1336    let ctx = ctx.context_ref();
1337    if let Ok(theme) = GLOBAL_THEME.lock() {
1338        // Resolve Auto mode to actual mode based on context
1339        let mut resolved_theme = theme.clone();
1340        if resolved_theme.theme_mode == ThemeMode::Auto {
1341            if ctx.style().visuals.dark_mode {
1342                resolved_theme.theme_mode = ThemeMode::Dark;
1343            } else {
1344                resolved_theme.theme_mode = ThemeMode::Light;
1345            }
1346        }
1347
1348        // Get the appropriate background color from the material theme
1349        let background_color = match (resolved_theme.theme_mode, resolved_theme.contrast_level) {
1350            (ThemeMode::Dark, ContrastLevel::High) => {
1351                resolved_theme.get_color_by_name("surfaceContainerHighest")
1352            }
1353            (ThemeMode::Dark, ContrastLevel::Medium) => {
1354                resolved_theme.get_color_by_name("surfaceContainerHigh")
1355            }
1356            (ThemeMode::Dark, _) => resolved_theme.get_color_by_name("surface"),
1357            (ThemeMode::Light, ContrastLevel::High) => {
1358                resolved_theme.get_color_by_name("surfaceContainerLowest")
1359            }
1360            (ThemeMode::Light, ContrastLevel::Medium) => {
1361                resolved_theme.get_color_by_name("surfaceContainerLow")
1362            }
1363            (ThemeMode::Light, _) => resolved_theme.get_color_by_name("surface"),
1364            (ThemeMode::Auto, _) => resolved_theme.get_color_by_name("surface"), // Should be unreachable
1365        };
1366
1367        // Apply the background color to the context
1368        let mut visuals = ctx.style().visuals.clone();
1369        visuals.window_fill = background_color;
1370        visuals.panel_fill = background_color;
1371        visuals.extreme_bg_color = background_color;
1372
1373        let mut style = (*ctx.style()).clone();
1374        style.visuals = visuals;
1375        ctx.set_style(style);
1376    }
1377}
1378
1379/// Helper function to get a color by name from the global theme
1380pub fn get_global_color(name: &str) -> Color32 {
1381    if let Ok(theme) = GLOBAL_THEME.lock() {
1382        theme.get_color_by_name(name)
1383    } else {
1384        // Fallback colors when theme is not accessible
1385        match name {
1386            "primary" => Color32::from_rgb(103, 80, 164),
1387            "onPrimary" => Color32::WHITE,
1388            "surface" => Color32::from_rgb(254, 247, 255),
1389            "onSurface" => Color32::from_rgb(28, 27, 31),
1390            "surfaceContainer" => Color32::from_rgb(247, 243, 249),
1391            "surfaceContainerHigh" => Color32::from_rgb(237, 231, 246),
1392            "surfaceContainerHighest" => Color32::from_rgb(230, 224, 233),
1393            "surfaceContainerLow" => Color32::from_rgb(247, 243, 249),
1394            "surfaceContainerLowest" => Color32::from_rgb(255, 255, 255),
1395            "outline" => Color32::from_rgb(121, 116, 126),
1396            "outlineVariant" => Color32::from_rgb(196, 199, 197),
1397            "surfaceVariant" => Color32::from_rgb(232, 222, 248),
1398            "secondary" => Color32::from_rgb(125, 82, 96),
1399            "tertiary" => Color32::from_rgb(125, 82, 96),
1400            "error" => Color32::from_rgb(186, 26, 26),
1401            "background" => Color32::from_rgb(255, 251, 254),
1402            "onBackground" => Color32::from_rgb(28, 27, 31),
1403            _ => Color32::GRAY,
1404        }
1405    }
1406}