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:
202
crates/theme/src/lib.rs
Normal file
202
crates/theme/src/lib.rs
Normal file
@@ -0,0 +1,202 @@
|
||||
//! 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 contrast;
|
||||
mod parse;
|
||||
pub mod wire;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user