//! A theme's tokens and the words apps describe their UI in (see the //! vision's K-UI note): apps say what a thing is (a primary button, a //! heading), never its colour or size. Themes are files (`themes/*.toml` //! on the system image), read strictly (`parse`) and refused unless every //! pairing text is drawn in has enough contrast (`check`). The display //! holds the one in use and hands it to every window (`wire`). Pure. #![no_std] #![forbid(unsafe_code)] mod accent; mod contrast; mod parse; pub mod wire; #[cfg(test)] mod tests; pub use accent::with_accent; pub use contrast::{check, ratio}; pub use parse::{Error, parse}; /// A colour as the theme names it: straight (not premultiplied) RGBA. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Rgba(pub u8, pub u8, pub u8, pub u8); impl Rgba { pub const fn opaque(r: u8, g: u8, b: u8) -> Self { Self(r, g, b, 255) } /// From `0xrrggbb`. pub const fn rgb(v: u32) -> Self { Self::opaque((v >> 16) as u8, (v >> 8) as u8, v as u8) } pub const fn to_rgb(self) -> u32 { (self.0 as u32) << 16 | (self.1 as u32) << 8 | self.2 as u32 } } /// Steps of the spacing scale. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Spacing { None, Xs, Sm, Md, Lg, Xl, } /// Steps of the type scale. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum TextStyle { Heading, Subheading, Body, Caption, Label, } /// What a text is, for its colour. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum TextColour { Primary, Secondary, Accent, OnAccent, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ButtonVariant { Primary, Secondary, Ghost, Destructive, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Weight { Regular, SemiBold, } /// A theme's name: what Settings lists and a user's choice records. pub const NAME_BYTES: usize = types::name::BYTES; #[derive(Clone, Copy, Debug, PartialEq)] pub struct Theme { pub name: [u8; NAME_BYTES], pub name_len: usize, pub background: Rgba, pub surface: Rgba, pub surface_raised: Rgba, pub accent: Rgba, pub on_accent: Rgba, pub text_primary: Rgba, pub text_secondary: Rgba, pub destructive: Rgba, pub on_destructive: Rgba, pub focus: Rgba, /// Soft colours the wallpaper glows with, behind the glass. pub orbs: [Rgba; 3], pub corner_radius: f32, /// The spacing scale's unit, in pixels. pub spacing_base: f32, /// Milliseconds; zero turns motion off. pub duration_fast: u16, pub duration_normal: u16, pub spring_stiffness: u16, pub spring_damping: u16, /// Pixels. pub blur_radius: u8, /// Percent. pub panel_opacity: u8, pub highlight: u8, } pub(crate) const fn name_bytes(s: &[u8]) -> ([u8; NAME_BYTES], usize) { let mut out = [0u8; NAME_BYTES]; let mut i = 0; while i < s.len() && i < NAME_BYTES { out[i] = s[i]; i += 1; } (out, i) } impl Theme { /// What a window shows before the display has said, and what the /// display uses when no theme file loads; `disk/themes/whimsy-dark.toml` /// says the same (a test keeps them so). pub const WHIMSY_DARK: Theme = Theme { name: name_bytes(b"Whimsy Dark").0, name_len: name_bytes(b"Whimsy Dark").1, background: Rgba::rgb(0x0f0e17), surface: Rgba::rgb(0x1a1a2e), surface_raised: Rgba::rgb(0x2a2a44), accent: Rgba::rgb(0x4fc3f7), on_accent: Rgba::rgb(0x0f0e17), text_primary: Rgba::rgb(0xe0e0e0), text_secondary: Rgba::rgb(0x9e9eb8), destructive: Rgba::rgb(0xef5350), on_destructive: Rgba::rgb(0x1a0a0a), focus: Rgba::rgb(0xffffff), orbs: [Rgba::rgb(0x3a1c71), Rgba::rgb(0x0f4c5c), Rgba::rgb(0x5b1a4a)], corner_radius: 16.0, spacing_base: 4.0, duration_fast: 150, duration_normal: 280, spring_stiffness: 300, spring_damping: 30, blur_radius: 12, panel_opacity: 72, highlight: 18, }; pub fn name(&self) -> &[u8] { &self.name[..self.name_len] } pub fn space(&self, s: Spacing) -> f32 { self.spacing_base * match s { Spacing::None => 0.0, Spacing::Xs => 1.0, Spacing::Sm => 2.0, Spacing::Md => 4.0, Spacing::Lg => 6.0, Spacing::Xl => 8.0, } } /// A text style's size in pixels and its weight. pub fn text(&self, s: TextStyle) -> (f32, Weight) { match s { TextStyle::Heading => (32.0, Weight::SemiBold), TextStyle::Subheading => (22.0, Weight::SemiBold), TextStyle::Body => (17.0, Weight::Regular), TextStyle::Caption => (13.0, Weight::Regular), TextStyle::Label => (17.0, Weight::SemiBold), } } pub fn text_colour(&self, c: TextColour) -> Rgba { match c { TextColour::Primary => self.text_primary, TextColour::Secondary => self.text_secondary, TextColour::Accent => self.accent, TextColour::OnAccent => self.on_accent, } } /// A button's fill (none for a ghost) and its label's colour. pub fn button(&self, v: ButtonVariant) -> (Option, Rgba) { match v { ButtonVariant::Primary => (Some(self.accent), self.on_accent), ButtonVariant::Secondary => (Some(self.surface_raised), self.text_primary), ButtonVariant::Ghost => (None, self.accent), ButtonVariant::Destructive => (Some(self.destructive), self.on_destructive), } } }