use egui::{Color32, FontData, FontDefinitions, FontFamily};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
#[cfg(feature = "ondemand")]
use std::io::Read;
#[derive(Debug, Clone)]
pub struct PreparedFont {
pub name: String,
pub data: Arc<FontData>,
pub families: Vec<FontFamily>,
}
static PREPARED_FONTS: Mutex<Vec<PreparedFont>> = Mutex::new(Vec::new());
#[derive(Debug, Clone)]
pub struct PreparedTheme {
pub name: String,
pub theme_data: MaterialThemeFile,
}
static PREPARED_THEMES: Mutex<Vec<PreparedTheme>> = Mutex::new(Vec::new());
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct MaterialScheme {
pub primary: String,
#[serde(rename = "surfaceTint")]
pub surface_tint: String,
#[serde(rename = "onPrimary")]
pub on_primary: String,
#[serde(rename = "primaryContainer")]
pub primary_container: String,
#[serde(rename = "onPrimaryContainer")]
pub on_primary_container: String,
pub secondary: String,
#[serde(rename = "onSecondary")]
pub on_secondary: String,
#[serde(rename = "secondaryContainer")]
pub secondary_container: String,
#[serde(rename = "onSecondaryContainer")]
pub on_secondary_container: String,
pub tertiary: String,
#[serde(rename = "onTertiary")]
pub on_tertiary: String,
#[serde(rename = "tertiaryContainer")]
pub tertiary_container: String,
#[serde(rename = "onTertiaryContainer")]
pub on_tertiary_container: String,
pub error: String,
#[serde(rename = "onError")]
pub on_error: String,
#[serde(rename = "errorContainer")]
pub error_container: String,
#[serde(rename = "onErrorContainer")]
pub on_error_container: String,
pub background: String,
#[serde(rename = "onBackground")]
pub on_background: String,
pub surface: String,
#[serde(rename = "onSurface")]
pub on_surface: String,
#[serde(rename = "surfaceVariant")]
pub surface_variant: String,
#[serde(rename = "onSurfaceVariant")]
pub on_surface_variant: String,
pub outline: String,
#[serde(rename = "outlineVariant")]
pub outline_variant: String,
pub shadow: String,
pub scrim: String,
#[serde(rename = "inverseSurface")]
pub inverse_surface: String,
#[serde(rename = "inverseOnSurface")]
pub inverse_on_surface: String,
#[serde(rename = "inversePrimary")]
pub inverse_primary: String,
#[serde(rename = "primaryFixed")]
pub primary_fixed: String,
#[serde(rename = "onPrimaryFixed")]
pub on_primary_fixed: String,
#[serde(rename = "primaryFixedDim")]
pub primary_fixed_dim: String,
#[serde(rename = "onPrimaryFixedVariant")]
pub on_primary_fixed_variant: String,
#[serde(rename = "secondaryFixed")]
pub secondary_fixed: String,
#[serde(rename = "onSecondaryFixed")]
pub on_secondary_fixed: String,
#[serde(rename = "secondaryFixedDim")]
pub secondary_fixed_dim: String,
#[serde(rename = "onSecondaryFixedVariant")]
pub on_secondary_fixed_variant: String,
#[serde(rename = "tertiaryFixed")]
pub tertiary_fixed: String,
#[serde(rename = "onTertiaryFixed")]
pub on_tertiary_fixed: String,
#[serde(rename = "tertiaryFixedDim")]
pub tertiary_fixed_dim: String,
#[serde(rename = "onTertiaryFixedVariant")]
pub on_tertiary_fixed_variant: String,
#[serde(rename = "surfaceDim")]
pub surface_dim: String,
#[serde(rename = "surfaceBright")]
pub surface_bright: String,
#[serde(rename = "surfaceContainerLowest")]
pub surface_container_lowest: String,
#[serde(rename = "surfaceContainerLow")]
pub surface_container_low: String,
#[serde(rename = "surfaceContainer")]
pub surface_container: String,
#[serde(rename = "surfaceContainerHigh")]
pub surface_container_high: String,
#[serde(rename = "surfaceContainerHighest")]
pub surface_container_highest: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct MaterialThemeFile {
pub description: String,
pub seed: String,
#[serde(rename = "coreColors")]
pub core_colors: HashMap<String, String>,
#[serde(rename = "extendedColors")]
pub extended_colors: Vec<serde_json::Value>,
pub schemes: HashMap<String, MaterialScheme>,
pub palettes: HashMap<String, HashMap<String, String>>,
}
#[derive(Clone, Debug, Copy, PartialEq)]
pub enum ContrastLevel {
Normal,
Medium,
High,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[derive(Default)]
pub enum ThemeMode {
Light,
Dark,
#[default]
Auto,
}
#[derive(Clone, Debug)]
pub struct MaterialThemeContext {
pub theme_mode: ThemeMode,
pub contrast_level: ContrastLevel,
pub material_theme: Option<MaterialThemeFile>,
pub selected_colors: HashMap<String, Color32>,
}
impl Default for MaterialThemeContext {
fn default() -> Self {
Self {
theme_mode: ThemeMode::Auto,
contrast_level: ContrastLevel::Normal,
material_theme: Some(get_default_material_theme()),
selected_colors: HashMap::new(),
}
}
}
fn get_default_material_theme() -> MaterialThemeFile {
let light_scheme = MaterialScheme {
primary: "#48672F".to_string(),
surface_tint: "#48672F".to_string(),
on_primary: "#FFFFFF".to_string(),
primary_container: "#C8EEA8".to_string(),
on_primary_container: "#314F19".to_string(),
secondary: "#56624B".to_string(),
on_secondary: "#FFFFFF".to_string(),
secondary_container: "#DAE7C9".to_string(),
on_secondary_container: "#3F4A34".to_string(),
tertiary: "#386665".to_string(),
on_tertiary: "#FFFFFF".to_string(),
tertiary_container: "#BBECEA".to_string(),
on_tertiary_container: "#1E4E4D".to_string(),
error: "#BA1A1A".to_string(),
on_error: "#FFFFFF".to_string(),
error_container: "#FFDAD6".to_string(),
on_error_container: "#93000A".to_string(),
background: "#F9FAEF".to_string(),
on_background: "#191D16".to_string(),
surface: "#F9FAEF".to_string(),
on_surface: "#191D16".to_string(),
surface_variant: "#E0E4D6".to_string(),
on_surface_variant: "#44483E".to_string(),
outline: "#74796D".to_string(),
outline_variant: "#C4C8BA".to_string(),
shadow: "#000000".to_string(),
scrim: "#000000".to_string(),
inverse_surface: "#2E312A".to_string(),
inverse_on_surface: "#F0F2E7".to_string(),
inverse_primary: "#ADD28E".to_string(),
primary_fixed: "#C8EEA8".to_string(),
on_primary_fixed: "#0B2000".to_string(),
primary_fixed_dim: "#ADD28E".to_string(),
on_primary_fixed_variant: "#314F19".to_string(),
secondary_fixed: "#DAE7C9".to_string(),
on_secondary_fixed: "#141E0C".to_string(),
secondary_fixed_dim: "#BECBAE".to_string(),
on_secondary_fixed_variant: "#3F4A34".to_string(),
tertiary_fixed: "#BBECEA".to_string(),
on_tertiary_fixed: "#00201F".to_string(),
tertiary_fixed_dim: "#A0CFCE".to_string(),
on_tertiary_fixed_variant: "#1E4E4D".to_string(),
surface_dim: "#D9DBD1".to_string(),
surface_bright: "#F9FAEF".to_string(),
surface_container_lowest: "#FFFFFF".to_string(),
surface_container_low: "#F3F5EA".to_string(),
surface_container: "#EDEFE4".to_string(),
surface_container_high: "#E7E9DE".to_string(),
surface_container_highest: "#E2E3D9".to_string(),
};
let dark_scheme = MaterialScheme {
primary: "#ADD28E".to_string(),
surface_tint: "#ADD28E".to_string(),
on_primary: "#1B3704".to_string(),
primary_container: "#314F19".to_string(),
on_primary_container: "#C8EEA8".to_string(),
secondary: "#BECBAE".to_string(),
on_secondary: "#29341F".to_string(),
secondary_container: "#3F4A34".to_string(),
on_secondary_container: "#DAE7C9".to_string(),
tertiary: "#A0CFCE".to_string(),
on_tertiary: "#003736".to_string(),
tertiary_container: "#1E4E4D".to_string(),
on_tertiary_container: "#BBECEA".to_string(),
error: "#FFB4AB".to_string(),
on_error: "#690005".to_string(),
error_container: "#93000A".to_string(),
on_error_container: "#FFDAD6".to_string(),
background: "#11140E".to_string(),
on_background: "#E2E3D9".to_string(),
surface: "#11140E".to_string(),
on_surface: "#E2E3D9".to_string(),
surface_variant: "#44483E".to_string(),
on_surface_variant: "#C4C8BA".to_string(),
outline: "#8E9286".to_string(),
outline_variant: "#44483E".to_string(),
shadow: "#000000".to_string(),
scrim: "#000000".to_string(),
inverse_surface: "#E2E3D9".to_string(),
inverse_on_surface: "#2E312A".to_string(),
inverse_primary: "#48672F".to_string(),
primary_fixed: "#C8EEA8".to_string(),
on_primary_fixed: "#0B2000".to_string(),
primary_fixed_dim: "#ADD28E".to_string(),
on_primary_fixed_variant: "#314F19".to_string(),
secondary_fixed: "#DAE7C9".to_string(),
on_secondary_fixed: "#141E0C".to_string(),
secondary_fixed_dim: "#BECBAE".to_string(),
on_secondary_fixed_variant: "#3F4A34".to_string(),
tertiary_fixed: "#BBECEA".to_string(),
on_tertiary_fixed: "#00201F".to_string(),
tertiary_fixed_dim: "#A0CFCE".to_string(),
on_tertiary_fixed_variant: "#1E4E4D".to_string(),
surface_dim: "#11140E".to_string(),
surface_bright: "#373A33".to_string(),
surface_container_lowest: "#0C0F09".to_string(),
surface_container_low: "#191D16".to_string(),
surface_container: "#1E211A".to_string(),
surface_container_high: "#282B24".to_string(),
surface_container_highest: "#33362F".to_string(),
};
let light_medium_contrast_scheme = MaterialScheme {
primary: "#253D05".to_string(),
surface_tint: "#4C662B".to_string(),
on_primary: "#FFFFFF".to_string(),
primary_container: "#5A7539".to_string(),
on_primary_container: "#FFFFFF".to_string(),
secondary: "#303924".to_string(),
on_secondary: "#FFFFFF".to_string(),
secondary_container: "#667157".to_string(),
on_secondary_container: "#FFFFFF".to_string(),
tertiary: "#083D3A".to_string(),
on_tertiary: "#FFFFFF".to_string(),
tertiary_container: "#477572".to_string(),
on_tertiary_container: "#FFFFFF".to_string(),
error: "#740006".to_string(),
on_error: "#FFFFFF".to_string(),
error_container: "#CF2C27".to_string(),
on_error_container: "#FFFFFF".to_string(),
background: "#F9FAEF".to_string(),
on_background: "#1A1C16".to_string(),
surface: "#F9FAEF".to_string(),
on_surface: "#0F120C".to_string(),
surface_variant: "#E1E4D5".to_string(),
on_surface_variant: "#34382D".to_string(),
outline: "#505449".to_string(),
outline_variant: "#6B6F62".to_string(),
shadow: "#000000".to_string(),
scrim: "#000000".to_string(),
inverse_surface: "#2F312A".to_string(),
inverse_on_surface: "#F1F2E6".to_string(),
inverse_primary: "#B1D18A".to_string(),
primary_fixed: "#5A7539".to_string(),
on_primary_fixed: "#FFFFFF".to_string(),
primary_fixed_dim: "#425C23".to_string(),
on_primary_fixed_variant: "#FFFFFF".to_string(),
secondary_fixed: "#667157".to_string(),
on_secondary_fixed: "#FFFFFF".to_string(),
secondary_fixed_dim: "#4E5840".to_string(),
on_secondary_fixed_variant: "#FFFFFF".to_string(),
tertiary_fixed: "#477572".to_string(),
on_tertiary_fixed: "#FFFFFF".to_string(),
tertiary_fixed_dim: "#2E5C59".to_string(),
on_tertiary_fixed_variant: "#FFFFFF".to_string(),
surface_dim: "#C6C7BD".to_string(),
surface_bright: "#F9FAEF".to_string(),
surface_container_lowest: "#FFFFFF".to_string(),
surface_container_low: "#F3F4E9".to_string(),
surface_container: "#E8E9DE".to_string(),
surface_container_high: "#DCDED3".to_string(),
surface_container_highest: "#D1D3C8".to_string(),
};
let light_high_contrast_scheme = MaterialScheme {
primary: "#1C3200".to_string(),
surface_tint: "#4C662B".to_string(),
on_primary: "#FFFFFF".to_string(),
primary_container: "#375018".to_string(),
on_primary_container: "#FFFFFF".to_string(),
secondary: "#262F1A".to_string(),
on_secondary: "#FFFFFF".to_string(),
secondary_container: "#434C35".to_string(),
on_secondary_container: "#FFFFFF".to_string(),
tertiary: "#003230".to_string(),
on_tertiary: "#FFFFFF".to_string(),
tertiary_container: "#21504E".to_string(),
on_tertiary_container: "#FFFFFF".to_string(),
error: "#600004".to_string(),
on_error: "#FFFFFF".to_string(),
error_container: "#98000A".to_string(),
on_error_container: "#FFFFFF".to_string(),
background: "#F9FAEF".to_string(),
on_background: "#1A1C16".to_string(),
surface: "#F9FAEF".to_string(),
on_surface: "#000000".to_string(),
surface_variant: "#E1E4D5".to_string(),
on_surface_variant: "#000000".to_string(),
outline: "#2A2D24".to_string(),
outline_variant: "#474B40".to_string(),
shadow: "#000000".to_string(),
scrim: "#000000".to_string(),
inverse_surface: "#2F312A".to_string(),
inverse_on_surface: "#FFFFFF".to_string(),
inverse_primary: "#B1D18A".to_string(),
primary_fixed: "#375018".to_string(),
on_primary_fixed: "#FFFFFF".to_string(),
primary_fixed_dim: "#213903".to_string(),
on_primary_fixed_variant: "#FFFFFF".to_string(),
secondary_fixed: "#434C35".to_string(),
on_secondary_fixed: "#FFFFFF".to_string(),
secondary_fixed_dim: "#2C3620".to_string(),
on_secondary_fixed_variant: "#FFFFFF".to_string(),
tertiary_fixed: "#21504E".to_string(),
on_tertiary_fixed: "#FFFFFF".to_string(),
tertiary_fixed_dim: "#033937".to_string(),
on_tertiary_fixed_variant: "#FFFFFF".to_string(),
surface_dim: "#B8BAAF".to_string(),
surface_bright: "#F9FAEF".to_string(),
surface_container_lowest: "#FFFFFF".to_string(),
surface_container_low: "#F1F2E6".to_string(),
surface_container: "#E2E3D8".to_string(),
surface_container_high: "#D4D5CA".to_string(),
surface_container_highest: "#C6C7BD".to_string(),
};
let dark_medium_contrast_scheme = MaterialScheme {
primary: "#C7E79E".to_string(),
surface_tint: "#B1D18A".to_string(),
on_primary: "#172B00".to_string(),
primary_container: "#7D9A59".to_string(),
on_primary_container: "#000000".to_string(),
secondary: "#D5E1C2".to_string(),
on_secondary: "#1F2814".to_string(),
secondary_container: "#8A9579".to_string(),
on_secondary_container: "#000000".to_string(),
tertiary: "#B5E6E1".to_string(),
on_tertiary: "#002B29".to_string(),
tertiary_container: "#6B9995".to_string(),
on_tertiary_container: "#000000".to_string(),
error: "#FFD2CC".to_string(),
on_error: "#540003".to_string(),
error_container: "#FF5449".to_string(),
on_error_container: "#000000".to_string(),
background: "#12140E".to_string(),
on_background: "#E2E3D8".to_string(),
surface: "#12140E".to_string(),
on_surface: "#FFFFFF".to_string(),
surface_variant: "#44483D".to_string(),
on_surface_variant: "#DBDECF".to_string(),
outline: "#B0B3A6".to_string(),
outline_variant: "#8E9285".to_string(),
shadow: "#000000".to_string(),
scrim: "#000000".to_string(),
inverse_surface: "#E2E3D8".to_string(),
inverse_on_surface: "#282B24".to_string(),
inverse_primary: "#364F17".to_string(),
primary_fixed: "#CDEDA3".to_string(),
on_primary_fixed: "#081400".to_string(),
primary_fixed_dim: "#B1D18A".to_string(),
on_primary_fixed_variant: "#253D05".to_string(),
secondary_fixed: "#DCE7C8".to_string(),
on_secondary_fixed: "#0B1403".to_string(),
secondary_fixed_dim: "#BFCBAD".to_string(),
on_secondary_fixed_variant: "#303924".to_string(),
tertiary_fixed: "#BCECE7".to_string(),
on_tertiary_fixed: "#001413".to_string(),
tertiary_fixed_dim: "#A0D0CB".to_string(),
on_tertiary_fixed_variant: "#083D3A".to_string(),
surface_dim: "#12140E".to_string(),
surface_bright: "#43453D".to_string(),
surface_container_lowest: "#060804".to_string(),
surface_container_low: "#1C1E18".to_string(),
surface_container: "#262922".to_string(),
surface_container_high: "#31342C".to_string(),
surface_container_highest: "#3C3F37".to_string(),
};
let dark_high_contrast_scheme = MaterialScheme {
primary: "#DAFBB0".to_string(),
surface_tint: "#B1D18A".to_string(),
on_primary: "#000000".to_string(),
primary_container: "#ADCD86".to_string(),
on_primary_container: "#050E00".to_string(),
secondary: "#E9F4D5".to_string(),
on_secondary: "#000000".to_string(),
secondary_container: "#BCC7A9".to_string(),
on_secondary_container: "#060D01".to_string(),
tertiary: "#C9F9F5".to_string(),
on_tertiary: "#000000".to_string(),
tertiary_container: "#9CCCC7".to_string(),
on_tertiary_container: "#000E0D".to_string(),
error: "#FFECE9".to_string(),
on_error: "#000000".to_string(),
error_container: "#FFAEA4".to_string(),
on_error_container: "#220001".to_string(),
background: "#12140E".to_string(),
on_background: "#E2E3D8".to_string(),
surface: "#12140E".to_string(),
on_surface: "#FFFFFF".to_string(),
surface_variant: "#44483D".to_string(),
on_surface_variant: "#FFFFFF".to_string(),
outline: "#EEF2E2".to_string(),
outline_variant: "#C1C4B6".to_string(),
shadow: "#000000".to_string(),
scrim: "#000000".to_string(),
inverse_surface: "#E2E3D8".to_string(),
inverse_on_surface: "#000000".to_string(),
inverse_primary: "#364F17".to_string(),
primary_fixed: "#CDEDA3".to_string(),
on_primary_fixed: "#000000".to_string(),
primary_fixed_dim: "#B1D18A".to_string(),
on_primary_fixed_variant: "#081400".to_string(),
secondary_fixed: "#DCE7C8".to_string(),
on_secondary_fixed: "#000000".to_string(),
secondary_fixed_dim: "#BFCBAD".to_string(),
on_secondary_fixed_variant: "#0B1403".to_string(),
tertiary_fixed: "#BCECE7".to_string(),
on_tertiary_fixed: "#000000".to_string(),
tertiary_fixed_dim: "#A0D0CB".to_string(),
on_tertiary_fixed_variant: "#001413".to_string(),
surface_dim: "#12140E".to_string(),
surface_bright: "#4F5149".to_string(),
surface_container_lowest: "#000000".to_string(),
surface_container_low: "#1E201A".to_string(),
surface_container: "#2F312A".to_string(),
surface_container_high: "#3A3C35".to_string(),
surface_container_highest: "#454840".to_string(),
};
let mut schemes = HashMap::new();
schemes.insert("light".to_string(), light_scheme);
schemes.insert(
"light-medium-contrast".to_string(),
light_medium_contrast_scheme,
);
schemes.insert(
"light-high-contrast".to_string(),
light_high_contrast_scheme,
);
schemes.insert("dark".to_string(), dark_scheme);
schemes.insert(
"dark-medium-contrast".to_string(),
dark_medium_contrast_scheme,
);
schemes.insert("dark-high-contrast".to_string(), dark_high_contrast_scheme);
let mut core_colors = HashMap::new();
core_colors.insert("primary".to_string(), "#5C883A".to_string());
MaterialThemeFile {
description: "TYPE: CUSTOM Material Theme Builder export 2025-08-21 11:51:45".to_string(),
seed: "#5C883A".to_string(),
core_colors,
extended_colors: Vec::new(),
schemes,
palettes: HashMap::new(),
}
}
impl MaterialThemeContext {
pub fn setup_fonts(font_name: Option<&str>) {
let font_name = font_name.unwrap_or("Google Sans Code");
let font_file_path = format!(
"resources/{}.ttf",
font_name.replace(" ", "-").to_lowercase()
);
let font_data = if std::path::Path::new(&font_file_path).exists() {
Self::load_local_font(&font_file_path)
} else {
#[cfg(feature = "ondemand")]
{
Self::download_google_font(font_name)
}
#[cfg(not(feature = "ondemand"))]
{
eprintln!(
"Font '{}' not found locally and ondemand feature is not enabled",
font_name
);
None
}
};
if let Some(data) = font_data {
let font_family_name = font_name.replace(" ", "");
let prepared_font = PreparedFont {
name: font_family_name.clone(),
data: Arc::new(FontData::from_owned(data)),
families: vec![FontFamily::Proportional, FontFamily::Monospace],
};
if let Ok(mut fonts) = PREPARED_FONTS.lock() {
fonts.retain(|f| f.name != font_family_name);
fonts.push(prepared_font);
}
}
}
fn load_local_font(font_path: &str) -> Option<Vec<u8>> {
std::fs::read(font_path).ok()
}
#[cfg(feature = "ondemand")]
fn download_google_font(font_name: &str) -> Option<Vec<u8>> {
let font_url_name = font_name.replace(" ", "+");
let css_url = format!(
"https://fonts.googleapis.com/css2?family={}:wght@400&display=swap",
font_url_name
);
match ureq::get(&css_url)
.set(
"User-Agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
)
.call()
{
Ok(response) => {
let css_content = response.into_string().ok()?;
let font_url = Self::extract_font_url_from_css(&css_content)?;
match ureq::get(&font_url).call() {
Ok(font_response) => {
let mut font_data = Vec::new();
if font_response
.into_reader()
.read_to_end(&mut font_data)
.is_ok()
{
let target_path = format!(
"resources/{}.ttf",
font_name.replace(" ", "-").to_lowercase()
);
if let Ok(()) = std::fs::write(&target_path, &font_data) {
eprintln!(
"Font '{}' downloaded and saved to {}",
font_name, target_path
);
}
Some(font_data)
} else {
eprintln!("Failed to read font data for '{}'", font_name);
None
}
}
Err(e) => {
eprintln!("Failed to download font '{}': {}", font_name, e);
None
}
}
}
Err(e) => {
eprintln!("Failed to fetch CSS for font '{}': {}", font_name, e);
None
}
}
}
#[cfg(feature = "ondemand")]
fn extract_font_url_from_css(css_content: &str) -> Option<String> {
for line in css_content.lines() {
if line.contains("src:") && line.contains("url(") && line.contains("format('truetype')")
{
if let Some(start) = line.find("url(") {
let start = start + 4; if let Some(end) = line[start..].find(")") {
let url = &line[start..start + end];
return Some(url.to_string());
}
}
}
}
None
}
pub fn setup_local_fonts(font_path: Option<&str>) {
if let Some(path) = font_path {
if std::path::Path::new(path).exists() {
if let Ok(data) = std::fs::read(path) {
let font_name = std::path::Path::new(path)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("CustomFont")
.split(['-', '_'])
.map(|part| {
let mut chars = part.chars();
match chars.next() {
Some(first) => {
let upper: String = first.to_uppercase().collect();
format!("{}{}", upper, chars.as_str())
}
None => String::new(),
}
})
.collect::<String>();
let prepared_font = PreparedFont {
name: font_name.clone(),
data: Arc::new(FontData::from_owned(data)),
families: vec![FontFamily::Proportional, FontFamily::Monospace],
};
if let Ok(mut fonts) = PREPARED_FONTS.lock() {
fonts.retain(|f| f.name != font_name);
fonts.push(prepared_font);
}
}
}
}
}
pub fn setup_local_fonts_from_bytes(font_name: &str, font_data: &[u8]) {
let prepared_font = PreparedFont {
name: font_name.to_owned(),
data: Arc::new(FontData::from_owned(font_data.to_vec())),
families: vec![FontFamily::Proportional, FontFamily::Monospace],
};
if let Ok(mut fonts) = PREPARED_FONTS.lock() {
fonts.retain(|f| f.name != font_name);
fonts.push(prepared_font);
}
}
pub fn setup_local_theme(theme_path: Option<&str>) {
let theme_data = if let Some(path) = theme_path {
if std::path::Path::new(path).exists() {
std::fs::read_to_string(path).ok()
} else {
Self::get_embedded_theme_data(path).or_else(|| {
Some(serde_json::to_string(&get_default_material_theme()).unwrap_or_default())
})
}
} else {
Self::get_embedded_theme_data("resources/material-theme1.json").or_else(|| {
Some(serde_json::to_string(&get_default_material_theme()).unwrap_or_default())
})
};
if let Some(data) = theme_data {
if let Ok(theme_file) = serde_json::from_str::<MaterialThemeFile>(&data) {
let theme_name = theme_path
.and_then(|p| {
std::path::Path::new(p)
.file_stem()
.map(|s| s.to_string_lossy().to_string())
})
.unwrap_or_else(|| "default".to_string());
let prepared_theme = PreparedTheme {
name: theme_name.clone(),
theme_data: theme_file,
};
if let Ok(mut themes) = PREPARED_THEMES.lock() {
themes.retain(|t| t.name != theme_name);
themes.push(prepared_theme);
}
}
}
}
fn get_embedded_theme_data(theme_path: &str) -> Option<String> {
std::fs::read_to_string(theme_path).ok()
}
pub fn load_themes() {
if let Ok(prepared_themes) = PREPARED_THEMES.lock() {
if let Some(theme) = prepared_themes.first() {
let theme_context = MaterialThemeContext {
material_theme: Some(theme.theme_data.clone()),
..Default::default()
};
update_global_theme(theme_context);
}
}
}
pub fn load_fonts(ctx: &egui::Context) {
let mut fonts = FontDefinitions::default();
if let Ok(prepared_fonts) = PREPARED_FONTS.lock() {
for prepared_font in prepared_fonts.iter() {
fonts
.font_data
.insert(prepared_font.name.clone(), prepared_font.data.clone());
for family in &prepared_font.families {
match family {
FontFamily::Proportional => {
if prepared_font.name.contains("MaterialSymbols") {
fonts
.families
.entry(FontFamily::Proportional)
.or_default()
.push(prepared_font.name.clone());
} else {
fonts
.families
.entry(FontFamily::Proportional)
.or_default()
.insert(0, prepared_font.name.clone());
}
}
FontFamily::Monospace => {
fonts
.families
.entry(FontFamily::Monospace)
.or_default()
.push(prepared_font.name.clone());
}
_ => {}
}
}
}
}
ctx.set_fonts(fonts);
}
pub fn get_current_scheme(&self) -> Option<&MaterialScheme> {
if let Some(ref theme) = self.material_theme {
let scheme_key = match (self.theme_mode, self.contrast_level) {
(ThemeMode::Light, ContrastLevel::Normal) => "light",
(ThemeMode::Light, ContrastLevel::Medium) => "light-medium-contrast",
(ThemeMode::Light, ContrastLevel::High) => "light-high-contrast",
(ThemeMode::Dark, ContrastLevel::Normal) => "dark",
(ThemeMode::Dark, ContrastLevel::Medium) => "dark-medium-contrast",
(ThemeMode::Dark, ContrastLevel::High) => "dark-high-contrast",
(ThemeMode::Auto, contrast) => {
match contrast {
ContrastLevel::Normal => "light",
ContrastLevel::Medium => "light-medium-contrast",
ContrastLevel::High => "light-high-contrast",
}
}
};
theme.schemes.get(scheme_key)
} else {
None
}
}
pub fn hex_to_color32(hex: &str) -> Option<Color32> {
if hex.starts_with('#') && hex.len() == 7 {
if let Ok(r) = u8::from_str_radix(&hex[1..3], 16) {
if let Ok(g) = u8::from_str_radix(&hex[3..5], 16) {
if let Ok(b) = u8::from_str_radix(&hex[5..7], 16) {
return Some(Color32::from_rgb(r, g, b));
}
}
}
}
None
}
pub fn color32_to_hex(color: Color32) -> String {
format!("#{:02X}{:02X}{:02X}", color.r(), color.g(), color.b())
}
pub fn get_color_by_name(&self, name: &str) -> Color32 {
if let Some(color) = self.selected_colors.get(name) {
return *color;
}
if let Some(scheme) = self.get_current_scheme() {
let hex = match name {
"primary" => &scheme.primary,
"surfaceTint" => &scheme.surface_tint,
"onPrimary" => &scheme.on_primary,
"primaryContainer" => &scheme.primary_container,
"onPrimaryContainer" => &scheme.on_primary_container,
"secondary" => &scheme.secondary,
"onSecondary" => &scheme.on_secondary,
"secondaryContainer" => &scheme.secondary_container,
"onSecondaryContainer" => &scheme.on_secondary_container,
"tertiary" => &scheme.tertiary,
"onTertiary" => &scheme.on_tertiary,
"tertiaryContainer" => &scheme.tertiary_container,
"onTertiaryContainer" => &scheme.on_tertiary_container,
"error" => &scheme.error,
"onError" => &scheme.on_error,
"errorContainer" => &scheme.error_container,
"onErrorContainer" => &scheme.on_error_container,
"background" => &scheme.background,
"onBackground" => &scheme.on_background,
"surface" => &scheme.surface,
"onSurface" => &scheme.on_surface,
"surfaceVariant" => &scheme.surface_variant,
"onSurfaceVariant" => &scheme.on_surface_variant,
"outline" => &scheme.outline,
"outlineVariant" => &scheme.outline_variant,
"shadow" => &scheme.shadow,
"scrim" => &scheme.scrim,
"inverseSurface" => &scheme.inverse_surface,
"inverseOnSurface" => &scheme.inverse_on_surface,
"inversePrimary" => &scheme.inverse_primary,
"primaryFixed" => &scheme.primary_fixed,
"onPrimaryFixed" => &scheme.on_primary_fixed,
"primaryFixedDim" => &scheme.primary_fixed_dim,
"onPrimaryFixedVariant" => &scheme.on_primary_fixed_variant,
"secondaryFixed" => &scheme.secondary_fixed,
"onSecondaryFixed" => &scheme.on_secondary_fixed,
"secondaryFixedDim" => &scheme.secondary_fixed_dim,
"onSecondaryFixedVariant" => &scheme.on_secondary_fixed_variant,
"tertiaryFixed" => &scheme.tertiary_fixed,
"onTertiaryFixed" => &scheme.on_tertiary_fixed,
"tertiaryFixedDim" => &scheme.tertiary_fixed_dim,
"onTertiaryFixedVariant" => &scheme.on_tertiary_fixed_variant,
"surfaceDim" => &scheme.surface_dim,
"surfaceBright" => &scheme.surface_bright,
"surfaceContainerLowest" => &scheme.surface_container_lowest,
"surfaceContainerLow" => &scheme.surface_container_low,
"surfaceContainer" => &scheme.surface_container,
"surfaceContainerHigh" => &scheme.surface_container_high,
"surfaceContainerHighest" => &scheme.surface_container_highest,
_ => return Color32::GRAY, };
Self::hex_to_color32(hex).unwrap_or(Color32::GRAY)
} else {
match name {
"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,
}
}
}
pub fn get_primary_color(&self) -> Color32 {
self.get_color_by_name("primary")
}
pub fn get_secondary_color(&self) -> Color32 {
self.get_color_by_name("secondary")
}
pub fn get_tertiary_color(&self) -> Color32 {
self.get_color_by_name("tertiary")
}
pub fn get_surface_color(&self, _dark_mode: bool) -> Color32 {
self.get_color_by_name("surface")
}
pub fn get_on_primary_color(&self) -> Color32 {
self.get_color_by_name("onPrimary")
}
}
static GLOBAL_THEME: std::sync::LazyLock<Arc<Mutex<MaterialThemeContext>>> =
std::sync::LazyLock::new(|| Arc::new(Mutex::new(MaterialThemeContext::default())));
pub fn get_global_theme() -> Arc<Mutex<MaterialThemeContext>> {
GLOBAL_THEME.clone()
}
pub fn update_global_theme(theme: MaterialThemeContext) {
if let Ok(mut global_theme) = GLOBAL_THEME.lock() {
*global_theme = theme;
}
}
pub fn setup_google_fonts(font_name: Option<&str>) {
MaterialThemeContext::setup_fonts(font_name);
}
pub fn setup_local_fonts(font_path: Option<&str>) {
MaterialThemeContext::setup_local_fonts(font_path);
}
pub fn setup_local_fonts_from_bytes(font_name: &str, font_data: &[u8]) {
MaterialThemeContext::setup_local_fonts_from_bytes(font_name, font_data);
}
pub fn setup_local_theme(theme_path: Option<&str>) {
MaterialThemeContext::setup_local_theme(theme_path);
}
pub fn load_themes() {
MaterialThemeContext::load_themes();
}
pub trait ContextRef {
fn context_ref(&self) -> &egui::Context;
}
impl ContextRef for egui::Context {
fn context_ref(&self) -> &egui::Context {
self
}
}
impl ContextRef for &egui::Context {
fn context_ref(&self) -> &egui::Context {
self
}
}
pub fn load_fonts<C: ContextRef>(ctx: C) {
MaterialThemeContext::load_fonts(ctx.context_ref());
}
pub fn update_window_background<C: ContextRef>(ctx: C) {
let ctx = ctx.context_ref();
if let Ok(theme) = GLOBAL_THEME.lock() {
let mut resolved_theme = theme.clone();
if resolved_theme.theme_mode == ThemeMode::Auto {
if ctx.style().visuals.dark_mode {
resolved_theme.theme_mode = ThemeMode::Dark;
} else {
resolved_theme.theme_mode = ThemeMode::Light;
}
}
let background_color = match (resolved_theme.theme_mode, resolved_theme.contrast_level) {
(ThemeMode::Dark, ContrastLevel::High) => {
resolved_theme.get_color_by_name("surfaceContainerHighest")
}
(ThemeMode::Dark, ContrastLevel::Medium) => {
resolved_theme.get_color_by_name("surfaceContainerHigh")
}
(ThemeMode::Dark, _) => resolved_theme.get_color_by_name("surface"),
(ThemeMode::Light, ContrastLevel::High) => {
resolved_theme.get_color_by_name("surfaceContainerLowest")
}
(ThemeMode::Light, ContrastLevel::Medium) => {
resolved_theme.get_color_by_name("surfaceContainerLow")
}
(ThemeMode::Light, _) => resolved_theme.get_color_by_name("surface"),
(ThemeMode::Auto, _) => resolved_theme.get_color_by_name("surface"), };
let mut visuals = ctx.style().visuals.clone();
visuals.window_fill = background_color;
visuals.panel_fill = background_color;
visuals.extreme_bg_color = background_color;
let mut style = (*ctx.style()).clone();
style.visuals = visuals;
ctx.set_style(style);
}
}
pub fn get_global_color(name: &str) -> Color32 {
if let Ok(theme) = GLOBAL_THEME.lock() {
theme.get_color_by_name(name)
} else {
match name {
"primary" => Color32::from_rgb(103, 80, 164),
"onPrimary" => Color32::WHITE,
"surface" => Color32::from_rgb(254, 247, 255),
"onSurface" => Color32::from_rgb(28, 27, 31),
"surfaceContainer" => Color32::from_rgb(247, 243, 249),
"surfaceContainerHigh" => Color32::from_rgb(237, 231, 246),
"surfaceContainerHighest" => Color32::from_rgb(230, 224, 233),
"surfaceContainerLow" => Color32::from_rgb(247, 243, 249),
"surfaceContainerLowest" => Color32::from_rgb(255, 255, 255),
"outline" => Color32::from_rgb(121, 116, 126),
"outlineVariant" => Color32::from_rgb(196, 199, 197),
"surfaceVariant" => Color32::from_rgb(232, 222, 248),
"secondary" => Color32::from_rgb(125, 82, 96),
"tertiary" => Color32::from_rgb(125, 82, 96),
"error" => Color32::from_rgb(186, 26, 26),
"background" => Color32::from_rgb(255, 251, 254),
"onBackground" => Color32::from_rgb(28, 27, 31),
_ => Color32::GRAY,
}
}
}