use image::Rgba;
use serde::{Deserialize, Deserializer};
use std::fs::File;
use std::io::Read;
use std::path::Path;
#[derive(Clone, Debug, Deserialize)]
pub struct Theme {
#[serde(deserialize_with = "deserialize_color")]
pub background: Rgba<u8>,
#[serde(deserialize_with = "deserialize_color")]
pub grid_major: Rgba<u8>,
#[serde(deserialize_with = "deserialize_color")]
pub grid_minor: Rgba<u8>,
#[serde(deserialize_with = "deserialize_color")]
pub axis_lines: Rgba<u8>,
#[serde(deserialize_with = "deserialize_color")]
pub text_primary: Rgba<u8>,
#[serde(deserialize_with = "deserialize_color")]
pub text_secondary: Rgba<u8>,
#[serde(deserialize_with = "deserialize_color")]
pub text_dim: Rgba<u8>,
#[serde(deserialize_with = "deserialize_color")]
pub glucose_high: Rgba<u8>,
#[serde(deserialize_with = "deserialize_color")]
pub glucose_low: Rgba<u8>,
#[serde(deserialize_with = "deserialize_color")]
pub glucose_in_range: Rgba<u8>,
#[serde(deserialize_with = "deserialize_color")]
pub insulin: Rgba<u8>,
#[serde(deserialize_with = "deserialize_color")]
pub carbs: Rgba<u8>,
#[serde(deserialize_with = "deserialize_color")]
pub glucose_reading_fill: Rgba<u8>,
#[serde(deserialize_with = "deserialize_color")]
pub glucose_reading_outline: Rgba<u8>,
}
macro_rules! builtin_theme {
($(#[$doc:meta])* $fn_name:ident, $file:literal) => {
$(#[$doc])*
pub fn $fn_name() -> Self {
let json = include_str!(concat!("./themes/", $file, ".json"));
serde_json::from_str(json)
.unwrap_or_else(|e| panic!("Built-in theme `{}` is invalid JSON: {e}", $file))
}
};
}
impl Theme {
pub fn dark() -> Self {
let json = include_str!("./themes/beetroot_dark.json");
serde_json::from_str(json).expect("Built-in dark theme is invalid JSON")
}
pub fn light() -> Self {
let json = include_str!("./themes/beetroot_light.json");
serde_json::from_str(json).expect("Built-in light theme is invalid JSON")
}
builtin_theme!(licorice_dark, "licorice_dark");
builtin_theme!(watermelon_dark, "watermelon_dark");
builtin_theme!(paper_light, "paper_light");
builtin_theme!(ube_light, "ube_light");
pub fn builtins() -> Vec<(&'static str, Theme)> {
vec![
("beetroot_dark", Self::dark()),
("licorice_dark", Self::licorice_dark()),
("watermelon_dark", Self::watermelon_dark()),
("beetroot_light", Self::light()),
("paper_light", Self::paper_light()),
("ube_light", Self::ube_light()),
]
}
pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, Box<dyn std::error::Error>> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
let theme: Theme = serde_json::from_str(&contents)?;
Ok(theme)
}
}
fn deserialize_color<'de, D>(deserializer: D) -> Result<Rgba<u8>, D::Error>
where
D: Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
parse_hex_color(&s).map_err(serde::de::Error::custom)
}
fn parse_hex_color(hex: &str) -> Result<Rgba<u8>, String> {
let hex = hex.trim_start_matches('#');
match hex.len() {
6 => {
let r = u8::from_str_radix(&hex[0..2], 16).map_err(|_| "Invalid Red")?;
let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_| "Invalid Green")?;
let b = u8::from_str_radix(&hex[4..6], 16).map_err(|_| "Invalid Blue")?;
Ok(Rgba([r, g, b, 255]))
}
8 => {
let r = u8::from_str_radix(&hex[0..2], 16).map_err(|_| "Invalid Red")?;
let g = u8::from_str_radix(&hex[2..4], 16).map_err(|_| "Invalid Green")?;
let b = u8::from_str_radix(&hex[4..6], 16).map_err(|_| "Invalid Blue")?;
let a = u8::from_str_radix(&hex[6..8], 16).map_err(|_| "Invalid Alpha")?;
Ok(Rgba([r, g, b, a]))
}
_ => Err(format!(
"Invalid hex length: {}. Expected 6 (RGB) or 8 (RGBA)",
hex.len()
)),
}
}