mod catppuccin;
mod cyberpunk;
mod dracula;
mod gruvbox;
mod normal;
mod tokyo_night;
use pixtuoid_core::sprite::Rgb;
pub use catppuccin::CATPPUCCIN;
pub use cyberpunk::CYBERPUNK;
pub use dracula::DRACULA;
pub use gruvbox::GRUVBOX;
pub use normal::NORMAL;
pub use tokyo_night::TOKYO_NIGHT;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ThemeKind {
Light,
Dark,
}
#[derive(Debug, Clone)]
pub struct Theme {
pub name: &'static str,
pub kind: ThemeKind,
pub surface: SurfaceColors,
pub office: OfficeColors,
pub lighting: LightingColors,
pub furniture: FurnitureColors,
pub effects: EffectColors,
pub ui: UiColors,
pub tool_glow: ToolGlowColors,
pub appliance: ApplianceColors,
}
#[derive(Debug, Clone)]
pub struct SurfaceColors {
pub wall: Rgb,
pub wall_trim: Rgb,
pub baseboard: Rgb,
pub carpet_base: Rgb,
pub carpet_light: Rgb,
pub carpet_dark: Rgb,
pub window_frame: Rgb,
pub bg_fallback: Rgb,
}
#[derive(Debug, Clone)]
pub struct OfficeColors {
pub room_wall_body: Rgb,
pub room_wall_trim_light: Rgb,
pub room_wall_trim_dark: Rgb,
pub cubicle_divider: Rgb,
pub runner_base: Rgb,
pub runner_stripe: Rgb,
pub runner_edge: Rgb,
pub neon_panel_bg: Rgb,
pub neon_frame_base: Rgb,
pub building_dark: Rgb,
pub building_light: Rgb,
pub city_lit_windows: [Rgb; 3],
pub city_dark_window: Rgb,
pub clock_rim: Rgb,
pub clock_face: Rgb,
pub clock_hand: Rgb,
pub shadow: Rgb,
}
#[derive(Debug, Clone)]
pub struct LightingColors {
pub day_sky_a: Rgb,
pub day_sky_b: Rgb,
pub night_sky_a: Rgb,
pub night_sky_b: Rgb,
pub twilight_a: Rgb,
pub twilight_b: Rgb,
pub sun_spill: Rgb,
pub ceiling_pool: Rgb,
pub floor_lamp_halo: Rgb,
pub night_tint: Rgb,
}
#[derive(Debug, Clone)]
pub struct FurnitureColors {
pub wood_top: Rgb,
pub wood_trim: Rgb,
pub rug_field: Rgb,
pub rug_trim: Rgb,
pub rug_accent: Rgb,
pub magazine: Rgb,
pub magazine_trim: Rgb,
pub chair_seat: Rgb,
pub chair_trim: Rgb,
pub coffee_cup: Rgb,
pub coffee_cup_shadow: Rgb,
}
#[derive(Debug, Clone)]
pub struct EffectColors {
pub monitor_frame_lit: Rgb,
pub sleep_z: Rgb,
pub coffee_steam: Rgb,
pub walking_dust: Rgb,
pub waiting_bubble: Rgb,
}
#[derive(Debug, Clone)]
pub struct ToolGlowColors {
pub edit: Rgb,
pub read: Rgb,
pub bash: Rgb,
pub agent: Rgb,
pub grep: Rgb,
pub default: Rgb,
}
#[derive(Debug, Clone)]
pub struct UiColors {
pub label_active: Rgb,
pub label_waiting: Rgb,
pub label_idle: Rgb,
pub label_exiting: Rgb,
pub tooltip_bg: Rgb,
pub tooltip_title: Rgb,
pub tooltip_text: Rgb,
pub tooltip_dim: Rgb,
pub neon_brand: Rgb,
pub neon_star: Rgb,
pub neon_ticker: Rgb,
}
#[derive(Debug, Clone)]
pub struct ApplianceColors {
pub vending_body: Rgb,
pub vending_panel: Rgb,
pub vending_drinks: [Rgb; 4],
pub vending_trim: Rgb,
pub vending_dark: Rgb,
pub printer_body: Rgb,
pub printer_top: Rgb,
pub printer_glass: Rgb,
pub printer_paper: Rgb,
pub printer_tray: Rgb,
pub coats: [Rgb; 3],
}
pub static ALL_THEMES: &[&Theme] = &[
&NORMAL,
&CYBERPUNK,
&DRACULA,
&TOKYO_NIGHT,
&CATPPUCCIN,
&GRUVBOX,
];
pub fn theme_by_name(name: &str) -> Option<&'static Theme> {
ALL_THEMES.iter().find(|t| t.name == name).copied()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_themes_resolve_by_name() {
for t in ALL_THEMES {
assert!(
theme_by_name(t.name).is_some(),
"theme '{}' not found",
t.name
);
}
}
#[test]
fn unknown_theme_returns_none() {
assert!(theme_by_name("doesnotexist").is_none());
}
#[test]
fn theme_gallery_manifest_matches_all_themes() {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/../../site/src/themes.json");
let json = match std::fs::read_to_string(path) {
Ok(s) => s,
Err(_) => {
eprintln!("skipping: {path} not present (packaged build)");
return;
}
};
let manifest: Vec<serde_json::Value> =
serde_json::from_str(&json).expect("themes.json parses");
let mut ids: Vec<&str> = manifest
.iter()
.map(|t| t["id"].as_str().expect("themes.json entry has a string id"))
.collect();
let mut names: Vec<&str> = ALL_THEMES.iter().map(|t| t.name).collect();
ids.sort_unstable();
names.sort_unstable();
assert_eq!(
ids, names,
"site/src/themes.json ids must match ALL_THEMES names — update the \
manifest + run scripts/gen-demos.sh when the registry changes"
);
}
#[test]
fn dark_themes_marked_dark() {
assert_eq!(CYBERPUNK.kind, ThemeKind::Dark);
assert_eq!(DRACULA.kind, ThemeKind::Dark);
assert_eq!(TOKYO_NIGHT.kind, ThemeKind::Dark);
assert_eq!(GRUVBOX.kind, ThemeKind::Dark);
assert_eq!(CATPPUCCIN.kind, ThemeKind::Dark);
}
#[test]
fn light_themes_marked_light() {
assert_eq!(NORMAL.kind, ThemeKind::Light);
}
#[test]
fn appliance_palette_is_legible_for_every_theme() {
fn lum(c: Rgb) -> u32 {
c.r as u32 + c.g as u32 + c.b as u32
}
for t in ALL_THEMES {
let a = &t.appliance;
assert!(
lum(a.printer_paper) > lum(a.printer_body)
&& lum(a.printer_body) > lum(a.printer_top),
"{}: printer must layer paper > body > top by luminance",
t.name
);
assert_ne!(
a.vending_panel, a.vending_body,
"{}: vending panel invisible",
t.name
);
for (i, d) in a.vending_drinks.iter().enumerate() {
assert_ne!(
*d, a.vending_body,
"{}: drink {i} invisible on body",
t.name
);
}
let brightest_drink = a.vending_drinks.iter().map(|c| lum(*c)).max().unwrap();
assert!(
lum(a.vending_body) < brightest_drink,
"{}: vending body should be darker than its drinks",
t.name
);
}
}
}