205 lines
5.6 KiB
Rust
205 lines
5.6 KiB
Rust
//! 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>, 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),
|
|
}
|
|
}
|
|
}
|