//! A theme file, read strictly: every token present once, nothing unknown, //! every value in range. Contrast is `check`'s. use manifest::Error as Toml; use manifest::toml::{self, Value}; use crate::{NAME_BYTES, Rgba, Theme}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Error { /// Not the TOML subset, a token given twice or unknown, or out of range. Toml(Toml), /// Two colours drawn on one another are too close: the pairing, and /// the ratio they have, times 100. Contrast { pair: &'static str, ratio_x100: u32 }, } const COLOURS: [&str; 10] = [ "background", "surface", "surface_raised", "accent", "on_accent", "text_primary", "text_secondary", "destructive", "on_destructive", "focus", ]; /// The whole numbers: table, key, and the range allowed. const NUMBERS: [(&str, &str, u32, u32); 9] = [ ("shape", "corner_radius", 0, 32), ("spacing", "base", 2, 8), ("motion", "duration_fast", 0, 2000), ("motion", "duration_normal", 0, 2000), ("motion", "spring_stiffness", 1, 2000), ("motion", "spring_damping", 1, 200), ("glass", "blur_radius", 0, 32), ("glass", "panel_opacity", 0, 100), ("glass", "highlight", 0, 100), ]; fn colour(s: &str) -> Option { manifest::Accent::parse(s).map(|a| Rgba::rgb(a.0)) } fn once(slot: &mut Option, value: Option, line: usize) -> Result<(), Toml> { if slot.is_some() { return Err(Toml::Duplicate { line }); } *slot = Some(value.ok_or(Toml::Invalid { line })?); Ok(()) } /// Reads a theme file. It is not checked for contrast here. pub fn parse(text: &str) -> Result { let mut name: Option<([u8; NAME_BYTES], usize)> = None; let mut colours: [Option; 10] = [None; 10]; let mut orbs: Option<[Rgba; 3]> = None; let mut numbers: [Option; 9] = [None; 9]; toml::parse(text, |e| { let line = e.line; match (e.table, e.key, e.value) { ("meta", "name", Value::Str(s)) => { let fits = (1..=NAME_BYTES).contains(&s.len()); once(&mut name, fits.then(|| crate::name_bytes(s.as_bytes())), line) } ("colour", key, Value::Str(s)) => { let i = COLOURS.iter().position(|k| *k == key).ok_or(Toml::Unknown { line })?; once(&mut colours[i], colour(s), line) } ("wallpaper", "orbs", Value::List(list)) => { let mut out = [Rgba::rgb(0); 3]; let mut n = 0; for s in list.iter() { *out.get_mut(n).ok_or(Toml::Invalid { line })? = colour(s).ok_or(Toml::Invalid { line })?; n += 1; } once(&mut orbs, (n == 3).then_some(out), line) } (table, key, Value::Int(v)) => { let i = NUMBERS.iter().position(|(t, k, ..)| *t == table && *k == key).ok_or(Toml::Unknown { line })?; let (_, _, lo, hi) = NUMBERS[i]; once(&mut numbers[i], (lo..=hi).contains(&v).then_some(v), line) } ("meta" | "colour" | "wallpaper" | "shape" | "spacing" | "motion" | "glass", _, _) => Err(Toml::Invalid { line }), _ => Err(Toml::Unknown { line }), } }) .map_err(Error::Toml)?; let (name, name_len) = name.ok_or(Error::Toml(Toml::Missing("name")))?; let mut c = [Rgba::rgb(0); 10]; for (i, slot) in colours.iter().enumerate() { c[i] = slot.ok_or(Error::Toml(Toml::Missing(COLOURS[i])))?; } let mut n = [0u32; 9]; for (i, slot) in numbers.iter().enumerate() { n[i] = slot.ok_or(Error::Toml(Toml::Missing(NUMBERS[i].1)))?; } Ok(Theme { name, name_len, background: c[0], surface: c[1], surface_raised: c[2], accent: c[3], on_accent: c[4], text_primary: c[5], text_secondary: c[6], destructive: c[7], on_destructive: c[8], focus: c[9], orbs: orbs.ok_or(Error::Toml(Toml::Missing("orbs")))?, corner_radius: n[0] as f32, spacing_base: n[1] as f32, // In range, so these fit. duration_fast: n[2] as u16, duration_normal: n[3] as u16, spring_stiffness: n[4] as u16, spring_damping: n[5] as u16, blur_radius: n[6] as u8, panel_opacity: n[7] as u8, highlight: n[8] as u8, }) }