Layers/src/ui/colors.rs

319 lines
8.5 KiB
Rust

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<Colors> = 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<String> {
std::fs::read_to_string(p).ok()
}
fn parse(src: &str) -> Result<Colors, String> {
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: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
#[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: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
#[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<Rgba, String> {
parse_hex(&self.0)
}
}
impl<'de> Deserialize<'de> for HexColor {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
Ok(HexColor(s))
}
}
fn parse_hex(s: &str) -> Result<Rgba, String> {
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: std::fmt::Display>(e: E) -> String { e.to_string() }