feat(theme): themes as files (theme crate: toml subset + ints, contrast check, wire); whimsy dark + light; display reads themes/, serves THEME/THEME_NAME, init SET_THEME -> THEME_CHANGED to all windows, bar themed; k-ui follows; settings appearance; init keeps choice in sealed users/<id>/settings/, reapplies at sign-in + display restart
This commit is contained in:
126
crates/theme/src/parse.rs
Normal file
126
crates/theme/src/parse.rs
Normal file
@@ -0,0 +1,126 @@
|
||||
//! 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<Rgba> {
|
||||
manifest::Accent::parse(s).map(|a| Rgba::rgb(a.0))
|
||||
}
|
||||
|
||||
fn once<T>(slot: &mut Option<T>, value: Option<T>, 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<Theme, Error> {
|
||||
let mut name: Option<([u8; NAME_BYTES], usize)> = None;
|
||||
let mut colours: [Option<Rgba>; 10] = [None; 10];
|
||||
let mut orbs: Option<[Rgba; 3]> = None;
|
||||
let mut numbers: [Option<u32>; 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,
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user