use std::path::{Path, PathBuf}; use std::sync::OnceLock; use iced_wgpu::core::Color; use serde::Deserialize; const DEFAULT_TOML: &str = include_str!("../../resources/colors.toml"); static LOADED: OnceLock = OnceLock::new(); #[derive(Debug, Clone, Copy)] pub struct Rgba { pub r: f32, pub g: f32, pub b: f32, pub a: f32, } impl Rgba { pub fn to_color(self) -> Color { Color::from_rgba(self.r, self.g, self.b, self.a) } pub fn with_alpha(self, a: f32) -> Color { Color::from_rgba(self.r, self.g, self.b, a.clamp(0.0, 1.0)) } pub fn mul_alpha(self, k: f32) -> Color { Color::from_rgba(self.r, self.g, self.b, (self.a * k).clamp(0.0, 1.0)) } } #[derive(Debug, Clone, Copy)] pub struct Window { pub background: Rgba, pub alpha_focused_hovered: f32, pub alpha_partial: f32, pub alpha_idle: f32, } #[derive(Debug, Clone, Copy)] pub struct Panel { pub surface: Rgba, pub surface_alt: Rgba, pub border: Rgba, pub text_primary: Rgba, pub text_muted: Rgba, } #[derive(Debug, Clone, Copy)] pub struct Accent { pub base: Rgba, pub hover: Rgba, pub pressed: Rgba, } #[derive(Debug, Clone, Copy)] pub struct Status { pub connected: Rgba, pub disconnected: Rgba, pub warning: Rgba, } #[derive(Debug, Clone, Copy)] pub struct GhostButton { pub border_active: f32, pub border_hovered: f32, pub border_pressed: f32, pub border_disabled: f32, pub bg_hovered: f32, pub bg_pressed: f32, pub text_active: f32, pub text_hovered: f32, pub text_pressed: f32, pub text_disabled: f32, } #[derive(Debug, Clone, Copy)] pub struct AccentButton { pub bg_active: f32, pub bg_hovered: f32, pub bg_pressed: f32, pub bg_disabled: f32, pub border_active: f32, pub border_hovered: f32, pub border_pressed: f32, pub border_disabled: f32, } #[derive(Debug, Clone, Copy)] pub struct Colors { pub window: Window, pub panel: Panel, pub accent: Accent, pub status: Status, pub ghost: GhostButton, pub accent_button: AccentButton, } /// Initialise the global palette. Call once during `layers_startup`. Later calls no-op. pub fn init(plugin_root: Option<&Path>) { let _ = LOADED.set(load(plugin_root)); } pub fn get() -> &'static Colors { LOADED.get_or_init(|| load(None)) } fn load(plugin_root: Option<&Path>) -> Colors { let override_path = plugin_root .map(|r| r.join("resources").join("colors.toml")) .or_else(|| { std::env::var_os("LAYERS_COLORS_TOML").map(PathBuf::from) }); let source = match override_path.as_deref().and_then(read_if_present) { Some(s) => s, None => DEFAULT_TOML.to_string(), }; parse(&source).unwrap_or_else(|e| { tracing::warn!("colors.toml parse failed: {e}; using compiled defaults"); parse(DEFAULT_TOML).expect("compiled-in default colors must parse") }) } fn read_if_present(p: &Path) -> Option { std::fs::read_to_string(p).ok() } fn parse(src: &str) -> Result { let raw: Raw = toml::from_str(src).map_err(|e| e.to_string())?; Ok(Colors { window: Window { background: raw.window.background.parse()?, alpha_focused_hovered: raw.window.alpha_focused_hovered, alpha_partial: raw.window.alpha_partial, alpha_idle: raw.window.alpha_idle, }, panel: Panel { surface: raw.panel.surface.parse()?, surface_alt: raw.panel.surface_alt.parse()?, border: raw.panel.border.parse()?, text_primary: raw.panel.text_primary.parse()?, text_muted: raw.panel.text_muted.parse()?, }, accent: Accent { base: raw.accent.base.parse()?, hover: raw.accent.hover.parse()?, pressed: raw.accent.pressed.parse()?, }, status: Status { connected: raw.status.connected.parse()?, disconnected: raw.status.disconnected.parse()?, warning: raw.status.warning.parse()?, }, ghost: raw.button.ghost, accent_button: raw.button.accent, }) } #[derive(Deserialize)] struct Raw { window: RawWindow, panel: RawPanel, accent: RawAccent, status: RawStatus, button: RawButton, } #[derive(Deserialize)] struct RawWindow { background: HexColor, alpha_focused_hovered: f32, alpha_partial: f32, alpha_idle: f32, } #[derive(Deserialize)] struct RawPanel { surface: HexColor, surface_alt: HexColor, border: HexColor, text_primary: HexColor, text_muted: HexColor, } #[derive(Deserialize)] struct RawAccent { base: HexColor, hover: HexColor, pressed: HexColor, } #[derive(Deserialize)] struct RawStatus { connected: HexColor, disconnected: HexColor, warning: HexColor, } #[derive(Deserialize)] struct RawButton { ghost: GhostButton, accent: AccentButton, } impl<'de> Deserialize<'de> for GhostButton { fn deserialize>(d: D) -> Result { #[derive(Deserialize)] struct R { border_active: f32, border_hovered: f32, border_pressed: f32, border_disabled: f32, bg_hovered: f32, bg_pressed: f32, text_active: f32, text_hovered: f32, text_pressed: f32, text_disabled: f32, } let r = R::deserialize(d)?; Ok(Self { border_active: r.border_active, border_hovered: r.border_hovered, border_pressed: r.border_pressed, border_disabled: r.border_disabled, bg_hovered: r.bg_hovered, bg_pressed: r.bg_pressed, text_active: r.text_active, text_hovered: r.text_hovered, text_pressed: r.text_pressed, text_disabled: r.text_disabled, }) } } impl<'de> Deserialize<'de> for AccentButton { fn deserialize>(d: D) -> Result { #[derive(Deserialize)] struct R { bg_active: f32, bg_hovered: f32, bg_pressed: f32, bg_disabled: f32, border_active: f32, border_hovered: f32, border_pressed: f32, border_disabled: f32, } let r = R::deserialize(d)?; Ok(Self { bg_active: r.bg_active, bg_hovered: r.bg_hovered, bg_pressed: r.bg_pressed, bg_disabled: r.bg_disabled, border_active: r.border_active, border_hovered: r.border_hovered, border_pressed: r.border_pressed, border_disabled: r.border_disabled, }) } } #[derive(Clone)] struct HexColor(String); impl HexColor { fn parse(&self) -> Result { parse_hex(&self.0) } } impl<'de> Deserialize<'de> for HexColor { fn deserialize>(d: D) -> Result { let s = String::deserialize(d)?; Ok(HexColor(s)) } } fn parse_hex(s: &str) -> Result { let raw = s.trim_start_matches('#'); let (r, g, b, a) = match raw.len() { 3 => ( dup(u8::from_str_radix(&raw[0..1], 16).map_err(err)?), dup(u8::from_str_radix(&raw[1..2], 16).map_err(err)?), dup(u8::from_str_radix(&raw[2..3], 16).map_err(err)?), 255, ), 4 => ( dup(u8::from_str_radix(&raw[0..1], 16).map_err(err)?), dup(u8::from_str_radix(&raw[1..2], 16).map_err(err)?), dup(u8::from_str_radix(&raw[2..3], 16).map_err(err)?), dup(u8::from_str_radix(&raw[3..4], 16).map_err(err)?), ), 6 => ( u8::from_str_radix(&raw[0..2], 16).map_err(err)?, u8::from_str_radix(&raw[2..4], 16).map_err(err)?, u8::from_str_radix(&raw[4..6], 16).map_err(err)?, 255, ), 8 => ( u8::from_str_radix(&raw[0..2], 16).map_err(err)?, u8::from_str_radix(&raw[2..4], 16).map_err(err)?, u8::from_str_radix(&raw[4..6], 16).map_err(err)?, u8::from_str_radix(&raw[6..8], 16).map_err(err)?, ), _ => return Err(format!("invalid hex colour: {s:?}")), }; Ok(Rgba { r: r as f32 / 255.0, g: g as f32 / 255.0, b: b as f32 / 255.0, a: a as f32 / 255.0, }) } fn dup(n: u8) -> u8 { (n << 4) | n } fn err(e: E) -> String { e.to_string() }