1use egui::{Color32, FontData, FontDefinitions, FontFamily};
77use serde::{Deserialize, Serialize};
78use std::collections::HashMap;
79use std::sync::{Arc, Mutex};
80
81#[cfg(feature = "ondemand")]
82use std::io::Read;
83
84#[derive(Debug, Clone)]
90pub struct PreparedFont {
91 pub name: String,
92 pub data: Arc<FontData>,
93 pub families: Vec<FontFamily>,
94}
95
96static PREPARED_FONTS: Mutex<Vec<PreparedFont>> = Mutex::new(Vec::new());
97
98#[derive(Debug, Clone)]
114pub struct PreparedTheme {
115 pub name: String,
116 pub theme_data: MaterialThemeFile,
117}
118
119static PREPARED_THEMES: Mutex<Vec<PreparedTheme>> = Mutex::new(Vec::new());
120
121#[derive(Clone, Debug, Deserialize, Serialize)]
123pub struct MaterialScheme {
124 pub primary: String,
125 #[serde(rename = "surfaceTint")]
126 pub surface_tint: String,
127 #[serde(rename = "onPrimary")]
128 pub on_primary: String,
129 #[serde(rename = "primaryContainer")]
130 pub primary_container: String,
131 #[serde(rename = "onPrimaryContainer")]
132 pub on_primary_container: String,
133 pub secondary: String,
134 #[serde(rename = "onSecondary")]
135 pub on_secondary: String,
136 #[serde(rename = "secondaryContainer")]
137 pub secondary_container: String,
138 #[serde(rename = "onSecondaryContainer")]
139 pub on_secondary_container: String,
140 pub tertiary: String,
141 #[serde(rename = "onTertiary")]
142 pub on_tertiary: String,
143 #[serde(rename = "tertiaryContainer")]
144 pub tertiary_container: String,
145 #[serde(rename = "onTertiaryContainer")]
146 pub on_tertiary_container: String,
147 pub error: String,
148 #[serde(rename = "onError")]
149 pub on_error: String,
150 #[serde(rename = "errorContainer")]
151 pub error_container: String,
152 #[serde(rename = "onErrorContainer")]
153 pub on_error_container: String,
154 pub background: String,
155 #[serde(rename = "onBackground")]
156 pub on_background: String,
157 pub surface: String,
158 #[serde(rename = "onSurface")]
159 pub on_surface: String,
160 #[serde(rename = "surfaceVariant")]
161 pub surface_variant: String,
162 #[serde(rename = "onSurfaceVariant")]
163 pub on_surface_variant: String,
164 pub outline: String,
165 #[serde(rename = "outlineVariant")]
166 pub outline_variant: String,
167 pub shadow: String,
168 pub scrim: String,
169 #[serde(rename = "inverseSurface")]
170 pub inverse_surface: String,
171 #[serde(rename = "inverseOnSurface")]
172 pub inverse_on_surface: String,
173 #[serde(rename = "inversePrimary")]
174 pub inverse_primary: String,
175 #[serde(rename = "primaryFixed")]
176 pub primary_fixed: String,
177 #[serde(rename = "onPrimaryFixed")]
178 pub on_primary_fixed: String,
179 #[serde(rename = "primaryFixedDim")]
180 pub primary_fixed_dim: String,
181 #[serde(rename = "onPrimaryFixedVariant")]
182 pub on_primary_fixed_variant: String,
183 #[serde(rename = "secondaryFixed")]
184 pub secondary_fixed: String,
185 #[serde(rename = "onSecondaryFixed")]
186 pub on_secondary_fixed: String,
187 #[serde(rename = "secondaryFixedDim")]
188 pub secondary_fixed_dim: String,
189 #[serde(rename = "onSecondaryFixedVariant")]
190 pub on_secondary_fixed_variant: String,
191 #[serde(rename = "tertiaryFixed")]
192 pub tertiary_fixed: String,
193 #[serde(rename = "onTertiaryFixed")]
194 pub on_tertiary_fixed: String,
195 #[serde(rename = "tertiaryFixedDim")]
196 pub tertiary_fixed_dim: String,
197 #[serde(rename = "onTertiaryFixedVariant")]
198 pub on_tertiary_fixed_variant: String,
199 #[serde(rename = "surfaceDim")]
200 pub surface_dim: String,
201 #[serde(rename = "surfaceBright")]
202 pub surface_bright: String,
203 #[serde(rename = "surfaceContainerLowest")]
204 pub surface_container_lowest: String,
205 #[serde(rename = "surfaceContainerLow")]
206 pub surface_container_low: String,
207 #[serde(rename = "surfaceContainer")]
208 pub surface_container: String,
209 #[serde(rename = "surfaceContainerHigh")]
210 pub surface_container_high: String,
211 #[serde(rename = "surfaceContainerHighest")]
212 pub surface_container_highest: String,
213}
214
215#[derive(Clone, Debug, Deserialize, Serialize)]
216pub struct MaterialThemeFile {
217 pub description: String,
218 pub seed: String,
219 #[serde(rename = "coreColors")]
220 pub core_colors: HashMap<String, String>,
221 #[serde(rename = "extendedColors")]
222 pub extended_colors: Vec<serde_json::Value>,
223 pub schemes: HashMap<String, MaterialScheme>,
224 pub palettes: HashMap<String, HashMap<String, String>>,
225}
226
227#[derive(Clone, Debug, Copy, PartialEq)]
228pub enum ContrastLevel {
229 Normal,
230 Medium,
231 High,
232}
233
234#[derive(Debug, Clone, Copy, PartialEq)]
235pub enum ThemeMode {
236 Light,
237 Dark,
238 Auto,
239}
240
241impl Default for ThemeMode {
242 fn default() -> Self {
243 ThemeMode::Auto
244 }
245}
246
247#[derive(Clone, Debug)]
249pub struct MaterialThemeContext {
250 pub theme_mode: ThemeMode,
251 pub contrast_level: ContrastLevel,
252 pub material_theme: Option<MaterialThemeFile>,
253 pub selected_colors: HashMap<String, Color32>,
254}
255
256impl Default for MaterialThemeContext {
257 fn default() -> Self {
258 Self {
259 theme_mode: ThemeMode::Auto,
260 contrast_level: ContrastLevel::Normal,
261 material_theme: Some(get_default_material_theme()),
262 selected_colors: HashMap::new(),
263 }
264 }
265}
266
267fn get_default_material_theme() -> MaterialThemeFile {
268 let light_scheme = MaterialScheme {
270 primary: "#48672F".to_string(),
271 surface_tint: "#48672F".to_string(),
272 on_primary: "#FFFFFF".to_string(),
273 primary_container: "#C8EEA8".to_string(),
274 on_primary_container: "#314F19".to_string(),
275 secondary: "#56624B".to_string(),
276 on_secondary: "#FFFFFF".to_string(),
277 secondary_container: "#DAE7C9".to_string(),
278 on_secondary_container: "#3F4A34".to_string(),
279 tertiary: "#386665".to_string(),
280 on_tertiary: "#FFFFFF".to_string(),
281 tertiary_container: "#BBECEA".to_string(),
282 on_tertiary_container: "#1E4E4D".to_string(),
283 error: "#BA1A1A".to_string(),
284 on_error: "#FFFFFF".to_string(),
285 error_container: "#FFDAD6".to_string(),
286 on_error_container: "#93000A".to_string(),
287 background: "#F9FAEF".to_string(),
288 on_background: "#191D16".to_string(),
289 surface: "#F9FAEF".to_string(),
290 on_surface: "#191D16".to_string(),
291 surface_variant: "#E0E4D6".to_string(),
292 on_surface_variant: "#44483E".to_string(),
293 outline: "#74796D".to_string(),
294 outline_variant: "#C4C8BA".to_string(),
295 shadow: "#000000".to_string(),
296 scrim: "#000000".to_string(),
297 inverse_surface: "#2E312A".to_string(),
298 inverse_on_surface: "#F0F2E7".to_string(),
299 inverse_primary: "#ADD28E".to_string(),
300 primary_fixed: "#C8EEA8".to_string(),
301 on_primary_fixed: "#0B2000".to_string(),
302 primary_fixed_dim: "#ADD28E".to_string(),
303 on_primary_fixed_variant: "#314F19".to_string(),
304 secondary_fixed: "#DAE7C9".to_string(),
305 on_secondary_fixed: "#141E0C".to_string(),
306 secondary_fixed_dim: "#BECBAE".to_string(),
307 on_secondary_fixed_variant: "#3F4A34".to_string(),
308 tertiary_fixed: "#BBECEA".to_string(),
309 on_tertiary_fixed: "#00201F".to_string(),
310 tertiary_fixed_dim: "#A0CFCE".to_string(),
311 on_tertiary_fixed_variant: "#1E4E4D".to_string(),
312 surface_dim: "#D9DBD1".to_string(),
313 surface_bright: "#F9FAEF".to_string(),
314 surface_container_lowest: "#FFFFFF".to_string(),
315 surface_container_low: "#F3F5EA".to_string(),
316 surface_container: "#EDEFE4".to_string(),
317 surface_container_high: "#E7E9DE".to_string(),
318 surface_container_highest: "#E2E3D9".to_string(),
319 };
320
321 let dark_scheme = MaterialScheme {
322 primary: "#ADD28E".to_string(),
323 surface_tint: "#ADD28E".to_string(),
324 on_primary: "#1B3704".to_string(),
325 primary_container: "#314F19".to_string(),
326 on_primary_container: "#C8EEA8".to_string(),
327 secondary: "#BECBAE".to_string(),
328 on_secondary: "#29341F".to_string(),
329 secondary_container: "#3F4A34".to_string(),
330 on_secondary_container: "#DAE7C9".to_string(),
331 tertiary: "#A0CFCE".to_string(),
332 on_tertiary: "#003736".to_string(),
333 tertiary_container: "#1E4E4D".to_string(),
334 on_tertiary_container: "#BBECEA".to_string(),
335 error: "#FFB4AB".to_string(),
336 on_error: "#690005".to_string(),
337 error_container: "#93000A".to_string(),
338 on_error_container: "#FFDAD6".to_string(),
339 background: "#11140E".to_string(),
340 on_background: "#E2E3D9".to_string(),
341 surface: "#11140E".to_string(),
342 on_surface: "#E2E3D9".to_string(),
343 surface_variant: "#44483E".to_string(),
344 on_surface_variant: "#C4C8BA".to_string(),
345 outline: "#8E9286".to_string(),
346 outline_variant: "#44483E".to_string(),
347 shadow: "#000000".to_string(),
348 scrim: "#000000".to_string(),
349 inverse_surface: "#E2E3D9".to_string(),
350 inverse_on_surface: "#2E312A".to_string(),
351 inverse_primary: "#48672F".to_string(),
352 primary_fixed: "#C8EEA8".to_string(),
353 on_primary_fixed: "#0B2000".to_string(),
354 primary_fixed_dim: "#ADD28E".to_string(),
355 on_primary_fixed_variant: "#314F19".to_string(),
356 secondary_fixed: "#DAE7C9".to_string(),
357 on_secondary_fixed: "#141E0C".to_string(),
358 secondary_fixed_dim: "#BECBAE".to_string(),
359 on_secondary_fixed_variant: "#3F4A34".to_string(),
360 tertiary_fixed: "#BBECEA".to_string(),
361 on_tertiary_fixed: "#00201F".to_string(),
362 tertiary_fixed_dim: "#A0CFCE".to_string(),
363 on_tertiary_fixed_variant: "#1E4E4D".to_string(),
364 surface_dim: "#11140E".to_string(),
365 surface_bright: "#373A33".to_string(),
366 surface_container_lowest: "#0C0F09".to_string(),
367 surface_container_low: "#191D16".to_string(),
368 surface_container: "#1E211A".to_string(),
369 surface_container_high: "#282B24".to_string(),
370 surface_container_highest: "#33362F".to_string(),
371 };
372
373 let 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 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 Self::load_local_font(&font_file_path)
624 } else {
625 #[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 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 #[cfg(feature = "ondemand")]
663 fn download_google_font(font_name: &str) -> Option<Vec<u8>> {
664 let font_url_name = font_name.replace(" ", "+");
666
667 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 let font_url = Self::extract_font_url_from_css(&css_content)?;
685
686 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 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 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; 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 if let Some(path) = font_path {
750 if std::path::Path::new(path).exists() {
751 if let Ok(data) = std::fs::read(path) {
752 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 }
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 fn get_embedded_material_symbols() -> Option<Vec<u8>> {
820 None
823 }
824
825 pub fn setup_local_theme(theme_path: Option<&str>) {
840 let theme_data = if let Some(path) = theme_path {
841 if std::path::Path::new(path).exists() {
843 std::fs::read_to_string(path).ok()
844 } else {
845 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 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 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 themes.retain(|t| t.name != theme_name);
876 themes.push(prepared_theme);
877 }
878 }
879 }
880 }
881
882 fn get_embedded_theme_data(theme_path: &str) -> Option<String> {
884 std::fs::read_to_string(theme_path).ok()
887 }
888
889 pub fn load_themes() {
901 if let Ok(prepared_themes) = PREPARED_THEMES.lock() {
902 if let Some(theme) = prepared_themes.first() {
903 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 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 fonts
921 .font_data
922 .insert(prepared_font.name.clone(), prepared_font.data.clone());
923
924 for family in &prepared_font.families {
926 match family {
927 FontFamily::Proportional => {
928 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 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, };
1058
1059 Self::hex_to_color32(hex).unwrap_or(Color32::GRAY)
1060 } else {
1061 match name {
1063 "primary" => Color32::from_rgb(72, 103, 47), "surfaceTint" => Color32::from_rgb(72, 103, 47), "onPrimary" => Color32::WHITE, "primaryContainer" => Color32::from_rgb(200, 238, 168), "onPrimaryContainer" => Color32::from_rgb(49, 79, 25), "secondary" => Color32::from_rgb(86, 98, 75), "onSecondary" => Color32::WHITE, "secondaryContainer" => Color32::from_rgb(218, 231, 201), "onSecondaryContainer" => Color32::from_rgb(63, 74, 52), "tertiary" => Color32::from_rgb(56, 102, 101), "onTertiary" => Color32::WHITE, "tertiaryContainer" => Color32::from_rgb(187, 236, 234), "onTertiaryContainer" => Color32::from_rgb(30, 78, 77), "error" => Color32::from_rgb(186, 26, 26), "onError" => Color32::WHITE, "errorContainer" => Color32::from_rgb(255, 218, 214), "onErrorContainer" => Color32::from_rgb(147, 0, 10), "background" => Color32::from_rgb(249, 250, 239), "onBackground" => Color32::from_rgb(25, 29, 22), "surface" => Color32::from_rgb(249, 250, 239), "onSurface" => Color32::from_rgb(25, 29, 22), "surfaceVariant" => Color32::from_rgb(224, 228, 214), "onSurfaceVariant" => Color32::from_rgb(68, 72, 62), "outline" => Color32::from_rgb(116, 121, 109), "outlineVariant" => Color32::from_rgb(196, 200, 186), "shadow" => Color32::BLACK, "scrim" => Color32::BLACK, "inverseSurface" => Color32::from_rgb(46, 49, 42), "inverseOnSurface" => Color32::from_rgb(240, 242, 231), "inversePrimary" => Color32::from_rgb(173, 210, 142), "primaryFixed" => Color32::from_rgb(200, 238, 168), "onPrimaryFixed" => Color32::from_rgb(11, 32, 0), "primaryFixedDim" => Color32::from_rgb(173, 210, 142), "onPrimaryFixedVariant" => Color32::from_rgb(49, 79, 25), "secondaryFixed" => Color32::from_rgb(218, 231, 201), "onSecondaryFixed" => Color32::from_rgb(20, 30, 12), "secondaryFixedDim" => Color32::from_rgb(190, 203, 174), "onSecondaryFixedVariant" => Color32::from_rgb(63, 74, 52), "tertiaryFixed" => Color32::from_rgb(187, 236, 234), "onTertiaryFixed" => Color32::from_rgb(0, 32, 31), "tertiaryFixedDim" => Color32::from_rgb(160, 207, 206), "onTertiaryFixedVariant" => Color32::from_rgb(30, 78, 77), "surfaceDim" => Color32::from_rgb(217, 219, 209), "surfaceBright" => Color32::from_rgb(249, 250, 239), "surfaceContainerLowest" => Color32::WHITE, "surfaceContainerLow" => Color32::from_rgb(243, 245, 234), "surfaceContainer" => Color32::from_rgb(237, 239, 228), "surfaceContainerHigh" => Color32::from_rgb(231, 233, 222), "surfaceContainerHighest" => Color32::from_rgb(226, 227, 217), _ => Color32::GRAY,
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
1138static 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
1146pub fn update_global_theme(theme: MaterialThemeContext) {
1172 if let Ok(mut global_theme) = GLOBAL_THEME.lock() {
1173 *global_theme = theme;
1174 }
1175}
1176
1177pub fn setup_google_fonts(font_name: Option<&str>) {
1181 MaterialThemeContext::setup_fonts(font_name);
1182}
1183
1184pub fn setup_local_fonts(font_path: Option<&str>) {
1190 MaterialThemeContext::setup_local_fonts(font_path);
1191}
1192
1193pub 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
1204pub fn setup_local_theme(theme_path: Option<&str>) {
1236 MaterialThemeContext::setup_local_theme(theme_path);
1237}
1238
1239pub fn load_themes() {
1261 MaterialThemeContext::load_themes();
1262}
1263
1264pub 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
1281pub fn load_fonts<C: ContextRef>(ctx: C) {
1284 MaterialThemeContext::load_fonts(ctx.context_ref());
1285}
1286
1287pub fn update_window_background<C: ContextRef>(ctx: C) {
1336 let ctx = ctx.context_ref();
1337 if let Ok(theme) = GLOBAL_THEME.lock() {
1338 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 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"), };
1366
1367 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
1379pub 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 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}