#![forbid(unsafe_code)]
pub use llimphi_raster::peniko::Color;
use std::time::Duration;
pub const ENTITY_PALETTE: [(u8, u8, u8); 8] = [
(94, 129, 172), (163, 109, 156), (122, 162, 110), (191, 138, 92), (108, 153, 168), (170, 120, 120), (130, 140, 175), (150, 150, 110), ];
pub fn stable_color(seed: &str) -> Color {
let mut h: u32 = 2_166_136_261;
for b in seed.bytes() {
h ^= b as u32;
h = h.wrapping_mul(16_777_619);
}
let (r, g, b) = ENTITY_PALETTE[(h as usize) % ENTITY_PALETTE.len()];
Color::from_rgba8(r, g, b, 255)
}
pub mod motion {
use super::Duration;
pub const MICRO: Duration = Duration::from_millis(50);
pub const FAST: Duration = Duration::from_millis(80);
pub const NORMAL: Duration = Duration::from_millis(160);
pub const SLOW: Duration = Duration::from_millis(320);
pub const DRAMATIC: Duration = Duration::from_millis(480);
#[inline]
pub fn ease_out_cubic(t: f32) -> f32 {
let inv = 1.0 - t.clamp(0.0, 1.0);
1.0 - inv * inv * inv
}
#[inline]
pub fn ease_in_out_cubic(t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
if t < 0.5 {
4.0 * t * t * t
} else {
let f = -2.0 * t + 2.0;
1.0 - f * f * f / 2.0
}
}
#[inline]
pub fn ease_out_quint(t: f32) -> f32 {
let inv = 1.0 - t.clamp(0.0, 1.0);
1.0 - inv * inv * inv * inv * inv
}
#[inline]
pub fn ease_out_back(t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
const C1: f32 = 1.701_58;
const C3: f32 = C1 + 1.0;
let u = t - 1.0;
1.0 + C3 * u * u * u + C1 * u * u
}
#[inline]
pub fn linear(t: f32) -> f32 {
t.clamp(0.0, 1.0)
}
}
pub mod elevation {
pub type Elev = (u8, f64, f64);
pub const E1: Elev = (44, 4.0, 1.5);
pub const E2: Elev = (60, 10.0, 4.0);
pub const E3: Elev = (84, 18.0, 8.0);
pub const E4: Elev = (110, 32.0, 14.0);
pub const E5: Elev = (140, 48.0, 22.0);
}
pub mod alpha {
pub const SCRIM: u8 = 64;
pub const GLASS_PANEL: u8 = 232;
pub const DISABLED: u8 = 140;
pub const HINT: u8 = 96;
}
pub mod radius {
pub const XS: f64 = 2.0;
pub const SM: f64 = 4.0;
pub const MD: f64 = 8.0;
pub const LG: f64 = 12.0;
pub const XL: f64 = 20.0;
}
#[derive(Debug, Clone, Copy)]
pub struct Theme {
pub name: &'static str,
pub bg_app: Color,
pub bg_panel: Color,
pub bg_panel_alt: Color,
pub bg_input: Color,
pub bg_input_focus: Color,
pub bg_button: Color,
pub bg_button_hover: Color,
pub bg_selected: Color,
pub bg_row_hover: Color,
pub fg_text: Color,
pub fg_muted: Color,
pub fg_placeholder: Color,
pub fg_destructive: Color,
pub border: Color,
pub border_focus: Color,
pub accent: Color,
}
impl Default for Theme {
fn default() -> Self {
Self::dark()
}
}
impl Theme {
pub const fn dark() -> Self {
Self {
name: "Dark",
bg_app: Color::from_rgba8(14, 16, 22, 255),
bg_panel: Color::from_rgba8(22, 26, 36, 255),
bg_panel_alt: Color::from_rgba8(18, 22, 30, 255),
bg_input: Color::from_rgba8(16, 20, 28, 255),
bg_input_focus: Color::from_rgba8(20, 26, 38, 255),
bg_button: Color::from_rgba8(36, 42, 56, 255),
bg_button_hover: Color::from_rgba8(54, 64, 86, 255),
bg_selected: Color::from_rgba8(58, 78, 128, 255),
bg_row_hover: Color::from_rgba8(36, 44, 60, 255),
fg_text: Color::from_rgba8(214, 222, 232, 255),
fg_muted: Color::from_rgba8(140, 152, 170, 255),
fg_placeholder: Color::from_rgba8(95, 105, 122, 255),
fg_destructive: Color::from_rgba8(220, 110, 110, 255),
border: Color::from_rgba8(46, 54, 70, 255),
border_focus: Color::from_rgba8(110, 140, 220, 255),
accent: Color::from_rgba8(110, 140, 220, 255),
}
}
pub const fn tawa() -> Self {
Self {
name: "Tawa",
bg_app: Color::from_rgba8(20, 19, 17, 255), bg_panel: Color::from_rgba8(30, 28, 26, 255),
bg_panel_alt: Color::from_rgba8(25, 24, 22, 255),
bg_input: Color::from_rgba8(24, 23, 21, 255),
bg_input_focus: Color::from_rgba8(32, 30, 28, 255),
bg_button: Color::from_rgba8(42, 40, 37, 255),
bg_button_hover: Color::from_rgba8(56, 53, 49, 255),
bg_selected: Color::from_rgba8(26, 74, 64, 255), bg_row_hover: Color::from_rgba8(40, 38, 35, 255),
fg_text: Color::from_rgba8(232, 230, 224, 255), fg_muted: Color::from_rgba8(160, 154, 144, 255),
fg_placeholder: Color::from_rgba8(112, 107, 99, 255),
fg_destructive: Color::from_rgba8(232, 116, 97, 255), border: Color::from_rgba8(54, 51, 47, 255),
border_focus: Color::from_rgba8(43, 217, 166, 255), accent: Color::from_rgba8(43, 217, 166, 255), }
}
pub const fn light() -> Self {
Self {
name: "Light",
bg_app: Color::from_rgba8(244, 246, 250, 255),
bg_panel: Color::from_rgba8(232, 236, 242, 255),
bg_panel_alt: Color::from_rgba8(224, 230, 240, 255),
bg_input: Color::from_rgba8(255, 255, 255, 255),
bg_input_focus: Color::from_rgba8(250, 252, 255, 255),
bg_button: Color::from_rgba8(220, 226, 236, 255),
bg_button_hover: Color::from_rgba8(200, 210, 226, 255),
bg_selected: Color::from_rgba8(160, 180, 220, 255),
bg_row_hover: Color::from_rgba8(214, 222, 236, 255),
fg_text: Color::from_rgba8(24, 32, 45, 255),
fg_muted: Color::from_rgba8(86, 98, 116, 255),
fg_placeholder: Color::from_rgba8(140, 150, 168, 255),
fg_destructive: Color::from_rgba8(168, 48, 48, 255),
border: Color::from_rgba8(190, 199, 214, 255),
border_focus: Color::from_rgba8(48, 92, 196, 255),
accent: Color::from_rgba8(48, 92, 196, 255),
}
}
pub const fn aurora() -> Self {
Self {
name: "Aurora",
bg_app: Color::from_rgba8(8, 18, 22, 255),
bg_panel: Color::from_rgba8(14, 28, 34, 255),
bg_panel_alt: Color::from_rgba8(12, 24, 30, 255),
bg_input: Color::from_rgba8(10, 22, 28, 255),
bg_input_focus: Color::from_rgba8(14, 30, 38, 255),
bg_button: Color::from_rgba8(20, 44, 52, 255),
bg_button_hover: Color::from_rgba8(30, 66, 78, 255),
bg_selected: Color::from_rgba8(30, 90, 100, 255),
bg_row_hover: Color::from_rgba8(20, 46, 56, 255),
fg_text: Color::from_rgba8(214, 232, 232, 255),
fg_muted: Color::from_rgba8(130, 168, 168, 255),
fg_placeholder: Color::from_rgba8(90, 120, 120, 255),
fg_destructive: Color::from_rgba8(220, 110, 110, 255),
border: Color::from_rgba8(38, 70, 78, 255),
border_focus: Color::from_rgba8(80, 200, 200, 255),
accent: Color::from_rgba8(80, 200, 200, 255),
}
}
pub const fn sunset() -> Self {
Self {
name: "Sunset",
bg_app: Color::from_rgba8(22, 14, 14, 255),
bg_panel: Color::from_rgba8(34, 22, 22, 255),
bg_panel_alt: Color::from_rgba8(28, 18, 18, 255),
bg_input: Color::from_rgba8(28, 18, 18, 255),
bg_input_focus: Color::from_rgba8(36, 24, 22, 255),
bg_button: Color::from_rgba8(54, 34, 28, 255),
bg_button_hover: Color::from_rgba8(78, 50, 38, 255),
bg_selected: Color::from_rgba8(120, 64, 38, 255),
bg_row_hover: Color::from_rgba8(56, 36, 28, 255),
fg_text: Color::from_rgba8(238, 220, 200, 255),
fg_muted: Color::from_rgba8(174, 142, 120, 255),
fg_placeholder: Color::from_rgba8(120, 96, 80, 255),
fg_destructive: Color::from_rgba8(220, 100, 100, 255),
border: Color::from_rgba8(70, 46, 36, 255),
border_focus: Color::from_rgba8(232, 140, 70, 255),
accent: Color::from_rgba8(232, 140, 70, 255),
}
}
pub const fn print() -> Self {
Self {
name: "Print",
bg_app: Color::from_rgba8(255, 255, 255, 255),
bg_panel: Color::from_rgba8(255, 255, 255, 255),
bg_panel_alt: Color::from_rgba8(246, 246, 246, 255),
bg_input: Color::from_rgba8(255, 255, 255, 255),
bg_input_focus: Color::from_rgba8(248, 248, 248, 255),
bg_button: Color::from_rgba8(238, 238, 238, 255),
bg_button_hover: Color::from_rgba8(224, 224, 224, 255),
bg_selected: Color::from_rgba8(220, 220, 220, 255),
bg_row_hover: Color::from_rgba8(240, 240, 240, 255),
fg_text: Color::from_rgba8(0, 0, 0, 255),
fg_muted: Color::from_rgba8(90, 90, 90, 255),
fg_placeholder: Color::from_rgba8(140, 140, 140, 255),
fg_destructive: Color::from_rgba8(0, 0, 0, 255),
border: Color::from_rgba8(0, 0, 0, 255),
border_focus: Color::from_rgba8(0, 0, 0, 255),
accent: Color::from_rgba8(0, 0, 0, 255),
}
}
pub const fn xp_blue() -> Self {
Self {
name: "WinXP",
bg_app: Color::from_rgba8(236, 240, 249, 255),
bg_panel: Color::from_rgba8(214, 223, 247, 255),
bg_panel_alt: Color::from_rgba8(60, 100, 190, 255), bg_input: Color::from_rgba8(255, 255, 255, 255),
bg_input_focus: Color::from_rgba8(248, 250, 255, 255),
bg_button: Color::from_rgba8(222, 230, 246, 255),
bg_button_hover: Color::from_rgba8(198, 214, 244, 255),
bg_selected: Color::from_rgba8(49, 106, 197, 255), bg_row_hover: Color::from_rgba8(214, 226, 248, 255),
fg_text: Color::from_rgba8(20, 30, 50, 255),
fg_muted: Color::from_rgba8(78, 92, 120, 255),
fg_placeholder: Color::from_rgba8(130, 142, 168, 255),
fg_destructive: Color::from_rgba8(176, 32, 32, 255),
border: Color::from_rgba8(122, 152, 206, 255),
border_focus: Color::from_rgba8(49, 106, 197, 255),
accent: Color::from_rgba8(36, 94, 220, 255), }
}
pub const fn mac_light() -> Self {
Self {
name: "macOS",
bg_app: Color::from_rgba8(246, 246, 248, 255),
bg_panel: Color::from_rgba8(236, 236, 240, 255),
bg_panel_alt: Color::from_rgba8(242, 242, 245, 235), bg_input: Color::from_rgba8(255, 255, 255, 255),
bg_input_focus: Color::from_rgba8(252, 252, 255, 255),
bg_button: Color::from_rgba8(228, 228, 233, 255),
bg_button_hover: Color::from_rgba8(214, 214, 221, 255),
bg_selected: Color::from_rgba8(10, 132, 255, 255),
bg_row_hover: Color::from_rgba8(232, 234, 240, 255),
fg_text: Color::from_rgba8(28, 28, 32, 255),
fg_muted: Color::from_rgba8(110, 110, 120, 255),
fg_placeholder: Color::from_rgba8(160, 160, 170, 255),
fg_destructive: Color::from_rgba8(215, 58, 50, 255),
border: Color::from_rgba8(208, 208, 215, 255),
border_focus: Color::from_rgba8(10, 132, 255, 255),
accent: Color::from_rgba8(10, 132, 255, 255),
}
}
pub const fn kde_breeze() -> Self {
Self {
name: "Breeze",
bg_app: Color::from_rgba8(239, 240, 241, 255),
bg_panel: Color::from_rgba8(252, 252, 252, 255),
bg_panel_alt: Color::from_rgba8(49, 54, 59, 255), bg_input: Color::from_rgba8(255, 255, 255, 255),
bg_input_focus: Color::from_rgba8(248, 252, 254, 255),
bg_button: Color::from_rgba8(224, 226, 228, 255),
bg_button_hover: Color::from_rgba8(208, 211, 214, 255),
bg_selected: Color::from_rgba8(61, 174, 233, 255),
bg_row_hover: Color::from_rgba8(227, 229, 231, 255),
fg_text: Color::from_rgba8(35, 38, 41, 255),
fg_muted: Color::from_rgba8(99, 104, 109, 255),
fg_placeholder: Color::from_rgba8(150, 155, 160, 255),
fg_destructive: Color::from_rgba8(218, 68, 83, 255),
border: Color::from_rgba8(188, 192, 196, 255),
border_focus: Color::from_rgba8(61, 174, 233, 255),
accent: Color::from_rgba8(61, 174, 233, 255),
}
}
pub const fn win31() -> Self {
Self {
name: "Win3.1",
bg_app: Color::from_rgba8(0, 128, 128, 255), bg_panel: Color::from_rgba8(192, 192, 192, 255), bg_panel_alt: Color::from_rgba8(0, 0, 128, 255), bg_input: Color::from_rgba8(255, 255, 255, 255),
bg_input_focus: Color::from_rgba8(255, 255, 255, 255),
bg_button: Color::from_rgba8(192, 192, 192, 255),
bg_button_hover: Color::from_rgba8(208, 208, 208, 255),
bg_selected: Color::from_rgba8(0, 0, 128, 255),
bg_row_hover: Color::from_rgba8(200, 200, 200, 255),
fg_text: Color::from_rgba8(0, 0, 0, 255),
fg_muted: Color::from_rgba8(64, 64, 64, 255),
fg_placeholder: Color::from_rgba8(112, 112, 112, 255),
fg_destructive: Color::from_rgba8(128, 0, 0, 255),
border: Color::from_rgba8(128, 128, 128, 255),
border_focus: Color::from_rgba8(0, 0, 128, 255),
accent: Color::from_rgba8(0, 0, 128, 255), }
}
pub const fn cde() -> Self {
Self {
name: "CDE",
bg_app: Color::from_rgba8(45, 70, 90, 255), bg_panel: Color::from_rgba8(174, 178, 195, 255), bg_panel_alt: Color::from_rgba8(120, 130, 150, 255),
bg_input: Color::from_rgba8(220, 222, 230, 255),
bg_input_focus: Color::from_rgba8(235, 237, 244, 255),
bg_button: Color::from_rgba8(160, 166, 185, 255),
bg_button_hover: Color::from_rgba8(176, 182, 200, 255),
bg_selected: Color::from_rgba8(90, 130, 130, 255),
bg_row_hover: Color::from_rgba8(168, 174, 192, 255),
fg_text: Color::from_rgba8(20, 24, 32, 255),
fg_muted: Color::from_rgba8(64, 72, 84, 255),
fg_placeholder: Color::from_rgba8(100, 108, 120, 255),
fg_destructive: Color::from_rgba8(140, 40, 40, 255),
border: Color::from_rgba8(108, 116, 134, 255),
border_focus: Color::from_rgba8(64, 132, 132, 255),
accent: Color::from_rgba8(64, 132, 132, 255), }
}
pub fn sunken(&self) -> Color {
let c = self.bg_app.components;
let lum = 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
let factor = if lum < 0.5 { 0.5 } else { 0.93 };
Color::from_rgba8(
(c[0] * factor * 255.0).round().clamp(0.0, 255.0) as u8,
(c[1] * factor * 255.0).round().clamp(0.0, 255.0) as u8,
(c[2] * factor * 255.0).round().clamp(0.0, 255.0) as u8,
255,
)
}
pub fn all() -> Vec<Self> {
vec![
Self::tawa(),
Self::dark(),
Self::light(),
Self::aurora(),
Self::sunset(),
]
}
pub fn by_name(name: &str) -> Option<Self> {
Self::all()
.into_iter()
.chain([
Self::print(),
Self::xp_blue(),
Self::mac_light(),
Self::kde_breeze(),
Self::win31(),
Self::cde(),
])
.find(|t| t.name == name)
}
pub fn next_after(current: &str) -> Self {
let all = Self::all();
let idx = all
.iter()
.position(|t| t.name == current)
.map(|i| (i + 1) % all.len())
.unwrap_or(0);
all[idx]
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn presets_have_unique_names() {
let all = Theme::all();
let mut names: Vec<&str> = all.iter().map(|t| t.name).collect();
let n_before = names.len();
names.sort();
names.dedup();
assert_eq!(names.len(), n_before, "nombres duplicados en Theme::all()");
}
#[test]
fn by_name_finds_each_preset() {
for t in Theme::all() {
let by = Theme::by_name(t.name).expect("preset registrado");
assert_eq!(by.name, t.name);
}
}
#[test]
fn by_name_returns_none_for_unknown() {
assert!(Theme::by_name("ThisDoesNotExist").is_none());
}
#[test]
fn next_after_cycles_through_all_presets() {
let all = Theme::all();
let mut current = all[0].name;
let mut visited = vec![current];
for _ in 0..all.len() - 1 {
current = Theme::next_after(current).name;
visited.push(current);
}
let names: Vec<&str> = all.iter().map(|t| t.name).collect();
assert_eq!(visited, names);
let wrapped = Theme::next_after(current).name;
assert_eq!(wrapped, all[0].name);
}
#[test]
fn next_after_unknown_falls_back_to_first() {
let n = Theme::next_after("Nope").name;
assert_eq!(n, Theme::all()[0].name);
}
#[test]
fn dark_is_the_default() {
assert_eq!(Theme::default().name, "Dark");
}
#[test]
fn tawa_es_el_primero_y_se_resuelve() {
let all = Theme::all();
assert_eq!(all[0].name, "Tawa", "Tawa debe ir al frente de la rotación");
assert_eq!(Theme::by_name("Tawa").expect("registrado").name, "Tawa");
}
#[test]
fn sunken_is_deeper_than_bg_app() {
let lum = |c: Color| {
let k = c.components;
0.2126 * k[0] + 0.7152 * k[1] + 0.0722 * k[2]
};
for t in Theme::all() {
assert!(
lum(t.sunken()) < lum(t.bg_app),
"{}: sunken debe ser más oscura que bg_app",
t.name
);
}
}
}