| 1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
use egui::Color32; |
| 12 |
use parking_lot::RwLock; |
| 13 |
use std::collections::HashMap; |
| 14 |
use std::path::{Path, PathBuf}; |
| 15 |
use std::sync::LazyLock; |
| 16 |
use tracing::{error, warn}; |
| 17 |
|
| 18 |
|
| 19 |
|
| 20 |
|
| 21 |
static LOGO_FONT: &[u8] = include_bytes!("../../fonts/RecursiveMonoLnrSt-Bold.ttf"); |
| 22 |
|
| 23 |
|
| 24 |
pub const LOGO_FONT_FAMILY: &str = "RecursiveMono"; |
| 25 |
|
| 26 |
|
| 27 |
|
| 28 |
|
| 29 |
|
| 30 |
|
| 31 |
|
| 32 |
|
| 33 |
|
| 34 |
|
| 35 |
|
| 36 |
pub mod space { |
| 37 |
|
| 38 |
pub const XS: f32 = 2.0; |
| 39 |
|
| 40 |
pub const SM: f32 = 4.0; |
| 41 |
|
| 42 |
pub const MD: f32 = 8.0; |
| 43 |
|
| 44 |
pub const LG: f32 = 12.0; |
| 45 |
|
| 46 |
pub const SECTION: f32 = 16.0; |
| 47 |
|
| 48 |
pub const XL: f32 = 20.0; |
| 49 |
} |
| 50 |
|
| 51 |
|
| 52 |
pub mod stroke { |
| 53 |
pub const THIN: f32 = 0.5; |
| 54 |
pub const DEFAULT: f32 = 1.0; |
| 55 |
pub const FOCUS: f32 = 1.5; |
| 56 |
} |
| 57 |
|
| 58 |
|
| 59 |
|
| 60 |
static BUNDLED_THEMES: &[(&str, &str)] = &[ |
| 61 |
("audiofiles", include_str!("../../themes/audiofiles.toml")), |
| 62 |
("tokyonight", include_str!("../../themes/tokyonight.toml")), |
| 63 |
("catppuccin-mocha", include_str!("../../themes/catppuccin-mocha.toml")), |
| 64 |
("catppuccin-macchiato", include_str!("../../themes/catppuccin-macchiato.toml")), |
| 65 |
("catppuccin-frappe", include_str!("../../themes/catppuccin-frappe.toml")), |
| 66 |
("catppuccin-latte", include_str!("../../themes/catppuccin-latte.toml")), |
| 67 |
("one-dark", include_str!("../../themes/one-dark.toml")), |
| 68 |
("palenight", include_str!("../../themes/palenight.toml")), |
| 69 |
("dracula", include_str!("../../themes/dracula.toml")), |
| 70 |
("nightfox", include_str!("../../themes/nightfox.toml")), |
| 71 |
("carbonfox", include_str!("../../themes/carbonfox.toml")), |
| 72 |
("oxocarbon-dark", include_str!("../../themes/oxocarbon-dark.toml")), |
| 73 |
("poimandres", include_str!("../../themes/poimandres.toml")), |
| 74 |
("ayu-mirage", include_str!("../../themes/ayu-mirage.toml")), |
| 75 |
("nord", include_str!("../../themes/nord.toml")), |
| 76 |
("ayu-light", include_str!("../../themes/ayu-light.toml")), |
| 77 |
("flatwhite", include_str!("../../themes/flatwhite.toml")), |
| 78 |
("dawnfox", include_str!("../../themes/dawnfox.toml")), |
| 79 |
("oxocarbon-light", include_str!("../../themes/oxocarbon-light.toml")), |
| 80 |
("neobrute", include_str!("../../themes/neobrute.toml")), |
| 81 |
("high-contrast", include_str!("../../themes/high-contrast.toml")), |
| 82 |
("gruvbox-dark", include_str!("../../themes/gruvbox-dark.toml")), |
| 83 |
("gruvbox-light", include_str!("../../themes/gruvbox-light.toml")), |
| 84 |
("rosepine", include_str!("../../themes/rosepine.toml")), |
| 85 |
("rosepine-dawn", include_str!("../../themes/rosepine-dawn.toml")), |
| 86 |
("everforest", include_str!("../../themes/everforest.toml")), |
| 87 |
("solarized-dark", include_str!("../../themes/solarized-dark.toml")), |
| 88 |
("kanagawa", include_str!("../../themes/kanagawa.toml")), |
| 89 |
]; |
| 90 |
|
| 91 |
|
| 92 |
#[derive(Debug, Clone)] |
| 93 |
pub struct ThemeColors { |
| 94 |
|
| 95 |
pub bg_primary: Color32, |
| 96 |
pub bg_secondary: Color32, |
| 97 |
pub bg_tertiary: Color32, |
| 98 |
pub bg_surface: Color32, |
| 99 |
|
| 100 |
pub fg_primary: Color32, |
| 101 |
pub fg_secondary: Color32, |
| 102 |
pub fg_muted: Color32, |
| 103 |
|
| 104 |
pub accent_red: Color32, |
| 105 |
pub accent_green: Color32, |
| 106 |
pub accent_blue: Color32, |
| 107 |
pub accent_yellow: Color32, |
| 108 |
pub accent_purple: Color32, |
| 109 |
pub accent_cyan: Color32, |
| 110 |
|
| 111 |
pub border_default: Color32, |
| 112 |
|
| 113 |
pub rounding: f32, |
| 114 |
pub item_spacing_x: f32, |
| 115 |
pub item_spacing_y: f32, |
| 116 |
|
| 117 |
pub section_spacing: f32, |
| 118 |
pub grid_row_spacing: f32, |
| 119 |
pub button_padding_x: f32, |
| 120 |
pub button_padding_y: f32, |
| 121 |
pub window_margin: f32, |
| 122 |
pub indent: f32, |
| 123 |
} |
| 124 |
|
| 125 |
impl Default for ThemeColors { |
| 126 |
fn default() -> Self { |
| 127 |
|
| 128 |
Self { |
| 129 |
bg_primary: Color32::from_rgb(0x00, 0x00, 0x00), |
| 130 |
bg_secondary: Color32::from_rgb(0x0a, 0x0a, 0x0a), |
| 131 |
bg_tertiary: Color32::from_rgb(0x1a, 0x1a, 0x1a), |
| 132 |
bg_surface: Color32::from_rgb(0x05, 0x05, 0x05), |
| 133 |
fg_primary: Color32::from_rgb(0xff, 0xff, 0xff), |
| 134 |
fg_secondary: Color32::from_rgb(0xd0, 0xd0, 0xd0), |
| 135 |
fg_muted: Color32::from_rgb(0x85, 0x85, 0x85), |
| 136 |
accent_red: Color32::from_rgb(0xff, 0x3b, 0x30), |
| 137 |
accent_green: Color32::from_rgb(0x30, 0xd1, 0x58), |
| 138 |
accent_blue: Color32::from_rgb(0x0a, 0x84, 0xff), |
| 139 |
accent_yellow: Color32::from_rgb(0xff, 0xd6, 0x0a), |
| 140 |
accent_purple: Color32::from_rgb(0xbf, 0x5a, 0xf2), |
| 141 |
accent_cyan: Color32::from_rgb(0x64, 0xd2, 0xff), |
| 142 |
border_default: Color32::from_rgb(0x33, 0x33, 0x33), |
| 143 |
rounding: 4.0, |
| 144 |
item_spacing_x: 8.0, |
| 145 |
item_spacing_y: 5.0, |
| 146 |
section_spacing: 16.0, |
| 147 |
grid_row_spacing: 6.0, |
| 148 |
button_padding_x: 8.0, |
| 149 |
button_padding_y: 4.0, |
| 150 |
window_margin: 10.0, |
| 151 |
indent: 18.0, |
| 152 |
} |
| 153 |
} |
| 154 |
} |
| 155 |
|
| 156 |
pub use theme_common::ThemeMeta; |
| 157 |
|
| 158 |
static THEME: LazyLock<RwLock<ThemeColors>> = LazyLock::new(|| RwLock::new(ThemeColors::default())); |
| 159 |
|
| 160 |
|
| 161 |
|
| 162 |
|
| 163 |
|
| 164 |
fn contrast_color(bg: Color32) -> Color32 { |
| 165 |
let luminance = (0.299 * bg.r() as f32 + 0.587 * bg.g() as f32 + 0.114 * bg.b() as f32) / 255.0; |
| 166 |
if luminance > 0.5 { Color32::BLACK } else { Color32::WHITE } |
| 167 |
} |
| 168 |
|
| 169 |
|
| 170 |
|
| 171 |
|
| 172 |
fn lerp_color(a: Color32, b: Color32, t: f32) -> Color32 { |
| 173 |
let mix = |a: u8, b: u8| -> u8 { |
| 174 |
(a as f32 + (b as f32 - a as f32) * t).round() as u8 |
| 175 |
}; |
| 176 |
Color32::from_rgb(mix(a.r(), b.r()), mix(a.g(), b.g()), mix(a.b(), b.b())) |
| 177 |
} |
| 178 |
|
| 179 |
|
| 180 |
|
| 181 |
|
| 182 |
pub fn bg_primary() -> Color32 { THEME.read().bg_primary } |
| 183 |
|
| 184 |
pub fn bg_secondary() -> Color32 { THEME.read().bg_secondary } |
| 185 |
|
| 186 |
pub fn bg_tertiary() -> Color32 { THEME.read().bg_tertiary } |
| 187 |
|
| 188 |
|
| 189 |
pub fn bg_row_even() -> Color32 { |
| 190 |
let t = THEME.read(); |
| 191 |
lerp_color(t.bg_primary, t.bg_secondary, 0.3) |
| 192 |
} |
| 193 |
|
| 194 |
pub fn bg_row_odd() -> Color32 { THEME.read().bg_primary } |
| 195 |
|
| 196 |
pub fn bg_hover() -> Color32 { THEME.read().bg_tertiary } |
| 197 |
|
| 198 |
pub fn bg_selected() -> Color32 { |
| 199 |
let t = THEME.read(); |
| 200 |
lerp_color(t.bg_primary, t.accent_blue, 0.3) |
| 201 |
} |
| 202 |
|
| 203 |
|
| 204 |
pub fn text_primary() -> Color32 { THEME.read().fg_primary } |
| 205 |
|
| 206 |
pub fn text_secondary() -> Color32 { THEME.read().fg_secondary } |
| 207 |
|
| 208 |
pub fn text_muted() -> Color32 { THEME.read().fg_muted } |
| 209 |
|
| 210 |
|
| 211 |
pub fn bg_surface() -> Color32 { THEME.read().bg_surface } |
| 212 |
|
| 213 |
|
| 214 |
pub fn accent_red() -> Color32 { THEME.read().accent_red } |
| 215 |
|
| 216 |
pub fn accent_green() -> Color32 { THEME.read().accent_green } |
| 217 |
|
| 218 |
pub fn accent_blue() -> Color32 { THEME.read().accent_blue } |
| 219 |
|
| 220 |
pub fn accent_yellow() -> Color32 { THEME.read().accent_yellow } |
| 221 |
|
| 222 |
pub fn accent_purple() -> Color32 { THEME.read().accent_purple } |
| 223 |
|
| 224 |
pub fn accent_cyan() -> Color32 { THEME.read().accent_cyan } |
| 225 |
|
| 226 |
|
| 227 |
pub fn border_default() -> Color32 { THEME.read().border_default } |
| 228 |
|
| 229 |
|
| 230 |
pub fn section_spacing() -> f32 { THEME.read().section_spacing } |
| 231 |
|
| 232 |
pub fn grid_row_spacing() -> f32 { THEME.read().grid_row_spacing } |
| 233 |
|
| 234 |
|
| 235 |
pub fn piano_white_key() -> Color32 { |
| 236 |
lerp_color(THEME.read().bg_surface, Color32::WHITE, 0.7) |
| 237 |
} |
| 238 |
|
| 239 |
pub fn piano_black_key() -> Color32 { |
| 240 |
lerp_color(THEME.read().bg_surface, Color32::BLACK, 0.7) |
| 241 |
} |
| 242 |
|
| 243 |
|
| 244 |
|
| 245 |
|
| 246 |
pub fn trim_mute_overlay() -> Color32 { |
| 247 |
|
| 248 |
|
| 249 |
|
| 250 |
let bg = THEME.read().bg_primary; |
| 251 |
Color32::from_rgba_unmultiplied(bg.r(), bg.g(), bg.b(), 160) |
| 252 |
} |
| 253 |
|
| 254 |
|
| 255 |
|
| 256 |
|
| 257 |
pub fn custom_themes_dir() -> Option<PathBuf> { |
| 258 |
dirs::config_dir().map(|c| c.join("audiofiles").join("themes")) |
| 259 |
} |
| 260 |
|
| 261 |
|
| 262 |
|
| 263 |
|
| 264 |
|
| 265 |
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] |
| 266 |
pub enum ContrastTier { |
| 267 |
|
| 268 |
Low, |
| 269 |
|
| 270 |
Standard, |
| 271 |
|
| 272 |
High, |
| 273 |
} |
| 274 |
|
| 275 |
impl ContrastTier { |
| 276 |
|
| 277 |
pub fn badge(self) -> &'static str { |
| 278 |
match self { |
| 279 |
ContrastTier::High => "AA", |
| 280 |
ContrastTier::Standard => "OK", |
| 281 |
ContrastTier::Low => "low", |
| 282 |
} |
| 283 |
} |
| 284 |
} |
| 285 |
|
| 286 |
fn relative_luminance(c: Color32) -> f64 { |
| 287 |
fn lin(ch: u8) -> f64 { |
| 288 |
let c = ch as f64 / 255.0; |
| 289 |
if c <= 0.03928 { |
| 290 |
c / 12.92 |
| 291 |
} else { |
| 292 |
((c + 0.055) / 1.055).powf(2.4) |
| 293 |
} |
| 294 |
} |
| 295 |
0.2126 * lin(c.r()) + 0.7152 * lin(c.g()) + 0.0722 * lin(c.b()) |
| 296 |
} |
| 297 |
|
| 298 |
fn contrast_ratio(a: Color32, b: Color32) -> f64 { |
| 299 |
let (la, lb) = (relative_luminance(a), relative_luminance(b)); |
| 300 |
let (hi, lo) = if la >= lb { (la, lb) } else { (lb, la) }; |
| 301 |
(hi + 0.05) / (lo + 0.05) |
| 302 |
} |
| 303 |
|
| 304 |
|
| 305 |
fn theme_colors_for(id: &str) -> Option<ThemeColors> { |
| 306 |
for (bundled_id, content) in BUNDLED_THEMES { |
| 307 |
if *bundled_id == id { |
| 308 |
return parse_theme(content).ok(); |
| 309 |
} |
| 310 |
} |
| 311 |
let dir = custom_themes_dir()?; |
| 312 |
let content = std::fs::read_to_string(dir.join(format!("{id}.toml"))).ok()?; |
| 313 |
parse_theme(&content).ok() |
| 314 |
} |
| 315 |
|
| 316 |
|
| 317 |
|
| 318 |
pub fn theme_contrast_tier(id: &str) -> ContrastTier { |
| 319 |
let Some(c) = theme_colors_for(id) else { |
| 320 |
return ContrastTier::Standard; |
| 321 |
}; |
| 322 |
let worst = contrast_ratio(c.fg_muted, c.bg_primary) |
| 323 |
.min(contrast_ratio(c.fg_muted, c.bg_tertiary)); |
| 324 |
if worst >= 4.5 { |
| 325 |
ContrastTier::High |
| 326 |
} else if worst >= 3.0 { |
| 327 |
ContrastTier::Standard |
| 328 |
} else { |
| 329 |
ContrastTier::Low |
| 330 |
} |
| 331 |
} |
| 332 |
|
| 333 |
|
| 334 |
pub fn list_themes() -> Vec<ThemeMeta> { |
| 335 |
let mut themes: Vec<ThemeMeta> = Vec::new(); |
| 336 |
let mut seen = std::collections::HashSet::new(); |
| 337 |
|
| 338 |
|
| 339 |
for (id, content) in BUNDLED_THEMES { |
| 340 |
let table: toml::Table = match content.parse() { |
| 341 |
Ok(t) => t, |
| 342 |
Err(_) => continue, |
| 343 |
}; |
| 344 |
seen.insert(id.to_string()); |
| 345 |
themes.push(theme_common::parse_meta(id, &table, false)); |
| 346 |
} |
| 347 |
|
| 348 |
|
| 349 |
if let Some(dir) = custom_themes_dir() |
| 350 |
&& let Ok(entries) = std::fs::read_dir(&dir) { |
| 351 |
for entry in entries.flatten() { |
| 352 |
let path = entry.path(); |
| 353 |
if path.extension().is_some_and(|e| e == "toml") { |
| 354 |
let id = path.file_stem() |
| 355 |
.unwrap_or_default() |
| 356 |
.to_string_lossy() |
| 357 |
.to_string(); |
| 358 |
if let Ok(content) = std::fs::read_to_string(&path) { |
| 359 |
let table: toml::Table = match content.parse() { |
| 360 |
Ok(t) => t, |
| 361 |
Err(_) => continue, |
| 362 |
}; |
| 363 |
let meta = theme_common::parse_meta(&id, &table, true); |
| 364 |
if seen.contains(&id) { |
| 365 |
if let Some(existing) = themes.iter_mut().find(|t| t.id == id) { |
| 366 |
existing.name = meta.name; |
| 367 |
existing.variant = meta.variant; |
| 368 |
existing.is_custom = true; |
| 369 |
} |
| 370 |
} else { |
| 371 |
seen.insert(id.clone()); |
| 372 |
themes.push(meta); |
| 373 |
} |
| 374 |
} |
| 375 |
} |
| 376 |
} |
| 377 |
} |
| 378 |
|
| 379 |
themes |
| 380 |
} |
| 381 |
|
| 382 |
|
| 383 |
|
| 384 |
|
| 385 |
|
| 386 |
fn parse_hex(s: &str) -> Option<Color32> { |
| 387 |
let h = s.strip_prefix('#')?; |
| 388 |
if h.len() != 6 { |
| 389 |
return None; |
| 390 |
} |
| 391 |
let r = u8::from_str_radix(&h[0..2], 16).ok()?; |
| 392 |
let g = u8::from_str_radix(&h[2..4], 16).ok()?; |
| 393 |
let b = u8::from_str_radix(&h[4..6], 16).ok()?; |
| 394 |
Some(Color32::from_rgb(r, g, b)) |
| 395 |
} |
| 396 |
|
| 397 |
|
| 398 |
|
| 399 |
fn get_color(colors: &HashMap<String, String>, key: &str) -> Option<Color32> { |
| 400 |
colors.get(key).and_then(|v| parse_hex(v)) |
| 401 |
} |
| 402 |
|
| 403 |
|
| 404 |
fn parse_theme(content: &str) -> Result<ThemeColors, toml::de::Error> { |
| 405 |
let table: toml::Table = content.parse()?; |
| 406 |
|
| 407 |
let colors = theme_common::extract_colors(&table); |
| 408 |
|
| 409 |
|
| 410 |
let spacing = table.get("spacing").and_then(|s| s.as_table()); |
| 411 |
let get_f32 = |key: &str, default: f32| -> f32 { |
| 412 |
spacing |
| 413 |
.and_then(|s| s.get(key)) |
| 414 |
.and_then(|v| v.as_float().map(|f| f as f32).or_else(|| v.as_integer().map(|i| i as f32))) |
| 415 |
.unwrap_or(default) |
| 416 |
}; |
| 417 |
|
| 418 |
Ok(ThemeColors { |
| 419 |
bg_primary: get_color(&colors, "background.primary").unwrap_or(Color32::BLACK), |
| 420 |
bg_secondary: get_color(&colors, "background.secondary").unwrap_or(Color32::BLACK), |
| 421 |
bg_tertiary: get_color(&colors, "background.tertiary").unwrap_or(Color32::BLACK), |
| 422 |
bg_surface: get_color(&colors, "background.surface").unwrap_or(Color32::BLACK), |
| 423 |
fg_primary: get_color(&colors, "foreground.primary").unwrap_or(Color32::WHITE), |
| 424 |
fg_secondary: get_color(&colors, "foreground.secondary").unwrap_or(Color32::WHITE), |
| 425 |
fg_muted: get_color(&colors, "foreground.muted").unwrap_or(Color32::GRAY), |
| 426 |
accent_red: get_color(&colors, "accent.red").unwrap_or(Color32::RED), |
| 427 |
accent_green: get_color(&colors, "accent.green").unwrap_or(Color32::GREEN), |
| 428 |
accent_blue: get_color(&colors, "accent.blue").unwrap_or(Color32::BLUE), |
| 429 |
accent_yellow: get_color(&colors, "accent.yellow").unwrap_or(Color32::YELLOW), |
| 430 |
accent_purple: get_color(&colors, "accent.purple").unwrap_or(Color32::from_rgb(0xBD, 0x93, 0xF9)), |
| 431 |
accent_cyan: get_color(&colors, "accent.cyan").unwrap_or(Color32::from_rgb(0x88, 0xC0, 0xD0)), |
| 432 |
border_default: get_color(&colors, "border.default").unwrap_or(Color32::DARK_GRAY), |
| 433 |
rounding: get_f32("rounding", 4.0), |
| 434 |
item_spacing_x: get_f32("item_spacing_x", 8.0), |
| 435 |
item_spacing_y: get_f32("item_spacing_y", 5.0), |
| 436 |
section_spacing: get_f32("section_spacing", 16.0), |
| 437 |
grid_row_spacing: get_f32("grid_row_spacing", 6.0), |
| 438 |
button_padding_x: get_f32("button_padding_x", 8.0), |
| 439 |
button_padding_y: get_f32("button_padding_y", 4.0), |
| 440 |
window_margin: get_f32("window_margin", 10.0), |
| 441 |
indent: get_f32("indent", 18.0), |
| 442 |
}) |
| 443 |
} |
| 444 |
|
| 445 |
|
| 446 |
pub fn load_theme(path: &Path) -> Result<ThemeColors, crate::error::ThemeError> { |
| 447 |
let content = std::fs::read_to_string(path).map_err(|e| crate::error::ThemeError::Read { |
| 448 |
path: path.to_path_buf(), |
| 449 |
source: e, |
| 450 |
})?; |
| 451 |
parse_theme(&content).map_err(|e| crate::error::ThemeError::Parse { |
| 452 |
path: path.to_path_buf(), |
| 453 |
source: e, |
| 454 |
}) |
| 455 |
} |
| 456 |
|
| 457 |
|
| 458 |
|
| 459 |
pub fn init(id: Option<&str>) { |
| 460 |
set_theme(id.unwrap_or("audiofiles")); |
| 461 |
} |
| 462 |
|
| 463 |
|
| 464 |
pub fn set_theme(id: &str) { |
| 465 |
|
| 466 |
for (bundled_id, content) in BUNDLED_THEMES { |
| 467 |
if *bundled_id == id { |
| 468 |
match parse_theme(content) { |
| 469 |
Ok(colors) => { |
| 470 |
*THEME.write() = colors; |
| 471 |
return; |
| 472 |
} |
| 473 |
Err(e) => { |
| 474 |
error!("Failed to parse bundled theme '{id}': {e}"); |
| 475 |
return; |
| 476 |
} |
| 477 |
} |
| 478 |
} |
| 479 |
} |
| 480 |
|
| 481 |
|
| 482 |
if let Some(dir) = custom_themes_dir() { |
| 483 |
let path = dir.join(format!("{id}.toml")); |
| 484 |
if path.exists() { |
| 485 |
match load_theme(&path) { |
| 486 |
Ok(colors) => { |
| 487 |
*THEME.write() = colors; |
| 488 |
return; |
| 489 |
} |
| 490 |
Err(e) => { |
| 491 |
error!("Failed to load custom theme '{id}': {e}"); |
| 492 |
return; |
| 493 |
} |
| 494 |
} |
| 495 |
} |
| 496 |
} |
| 497 |
|
| 498 |
warn!("Theme '{id}' not found; keeping current theme"); |
| 499 |
} |
| 500 |
|
| 501 |
|
| 502 |
|
| 503 |
pub fn theme_preview_colors(id: &str) -> Option<(Color32, Color32, Color32)> { |
| 504 |
let content = { |
| 505 |
|
| 506 |
let mut found = None; |
| 507 |
for (bundled_id, c) in BUNDLED_THEMES { |
| 508 |
if *bundled_id == id { |
| 509 |
found = Some(c.to_string()); |
| 510 |
break; |
| 511 |
} |
| 512 |
} |
| 513 |
if found.is_none() |
| 514 |
&& let Some(dir) = custom_themes_dir() { |
| 515 |
let path = dir.join(format!("{id}.toml")); |
| 516 |
found = std::fs::read_to_string(&path).ok(); |
| 517 |
} |
| 518 |
found? |
| 519 |
}; |
| 520 |
|
| 521 |
let table: toml::Table = content.parse().ok()?; |
| 522 |
let colors = theme_common::extract_colors(&table); |
| 523 |
let bg = get_color(&colors, "background.primary").unwrap_or(Color32::from_rgb(30, 30, 30)); |
| 524 |
let accent = get_color(&colors, "accent.blue").unwrap_or(Color32::from_rgb(100, 100, 255)); |
| 525 |
let fg = get_color(&colors, "foreground.primary").unwrap_or(Color32::from_rgb(220, 220, 220)); |
| 526 |
Some((bg, accent, fg)) |
| 527 |
} |
| 528 |
|
| 529 |
|
| 530 |
|
| 531 |
pub fn export_theme_content(id: &str) -> Option<String> { |
| 532 |
|
| 533 |
if let Some(dir) = custom_themes_dir() { |
| 534 |
let path = dir.join(format!("{id}.toml")); |
| 535 |
if let Ok(content) = std::fs::read_to_string(&path) { |
| 536 |
return Some(content); |
| 537 |
} |
| 538 |
} |
| 539 |
|
| 540 |
|
| 541 |
for (bundled_id, content) in BUNDLED_THEMES { |
| 542 |
if *bundled_id == id { |
| 543 |
return Some(content.to_string()); |
| 544 |
} |
| 545 |
} |
| 546 |
|
| 547 |
None |
| 548 |
} |
| 549 |
|
| 550 |
|
| 551 |
|
| 552 |
|
| 553 |
|
| 554 |
|
| 555 |
|
| 556 |
|
| 557 |
pub fn classification_color(class: &str) -> Color32 { |
| 558 |
match class { |
| 559 |
"kick" => Color32::from_rgb(0xE0, 0x50, 0x50), |
| 560 |
"snare" => Color32::from_rgb(0xE0, 0x90, 0x40), |
| 561 |
"hihat" => Color32::from_rgb(0xE0, 0xD0, 0x40), |
| 562 |
"cymbal" => Color32::from_rgb(0xC8, 0xC8, 0x50), |
| 563 |
"percussion" => Color32::from_rgb(0xD0, 0x70, 0xB0), |
| 564 |
"bass" => Color32::from_rgb(0x50, 0x80, 0xE0), |
| 565 |
"vocal" => Color32::from_rgb(0x50, 0xC0, 0xE0), |
| 566 |
"synth" => Color32::from_rgb(0xA0, 0x60, 0xE0), |
| 567 |
"pad" => Color32::from_rgb(0x60, 0xB0, 0x60), |
| 568 |
"misc" => Color32::from_rgb(0xE0, 0x60, 0x80), |
| 569 |
"noise" => Color32::from_rgb(0x90, 0x90, 0x90), |
| 570 |
"music" => Color32::from_rgb(0x70, 0xC0, 0xA0), |
| 571 |
_ => text_secondary(), |
| 572 |
} |
| 573 |
} |
| 574 |
|
| 575 |
|
| 576 |
|
| 577 |
|
| 578 |
pub fn setup_fonts(ctx: &egui::Context) { |
| 579 |
let mut fonts = egui::FontDefinitions::default(); |
| 580 |
fonts.font_data.insert( |
| 581 |
LOGO_FONT_FAMILY.to_owned(), |
| 582 |
egui::FontData::from_static(LOGO_FONT).into(), |
| 583 |
); |
| 584 |
fonts.families.insert( |
| 585 |
egui::FontFamily::Name(LOGO_FONT_FAMILY.into()), |
| 586 |
vec![LOGO_FONT_FAMILY.to_owned(), "Hack".to_owned()], |
| 587 |
); |
| 588 |
ctx.set_fonts(fonts); |
| 589 |
} |
| 590 |
|
| 591 |
|
| 592 |
pub fn apply_theme(ctx: &egui::Context) { |
| 593 |
let t = THEME.read(); |
| 594 |
let mut visuals = egui::Visuals::dark(); |
| 595 |
|
| 596 |
visuals.panel_fill = t.bg_secondary; |
| 597 |
visuals.window_fill = t.bg_secondary; |
| 598 |
visuals.extreme_bg_color = t.bg_primary; |
| 599 |
visuals.faint_bg_color = lerp_color(t.bg_primary, t.bg_secondary, 0.3); |
| 600 |
|
| 601 |
visuals.selection.bg_fill = lerp_color(t.bg_primary, t.accent_blue, 0.3); |
| 602 |
|
| 603 |
|
| 604 |
|
| 605 |
visuals.selection.stroke = egui::Stroke::new(stroke::FOCUS, t.accent_blue); |
| 606 |
|
| 607 |
visuals.widgets.noninteractive.bg_fill = t.bg_secondary; |
| 608 |
visuals.widgets.inactive.bg_fill = lerp_color(t.bg_secondary, t.bg_tertiary, 0.3); |
| 609 |
visuals.widgets.hovered.bg_fill = t.bg_tertiary; |
| 610 |
visuals.widgets.active.bg_fill = t.accent_blue; |
| 611 |
|
| 612 |
visuals.widgets.noninteractive.fg_stroke = egui::Stroke::new(1.0, t.fg_secondary); |
| 613 |
visuals.widgets.inactive.fg_stroke = egui::Stroke::new(1.0, t.fg_primary); |
| 614 |
visuals.widgets.hovered.fg_stroke = egui::Stroke::new(1.0, t.fg_primary); |
| 615 |
visuals.widgets.active.fg_stroke = egui::Stroke::new(1.0, contrast_color(t.accent_blue)); |
| 616 |
visuals.widgets.open.fg_stroke = egui::Stroke::new(1.0, t.fg_primary); |
| 617 |
|
| 618 |
visuals.window_stroke = egui::Stroke::new(1.0, t.border_default); |
| 619 |
visuals.widgets.noninteractive.bg_stroke = egui::Stroke::new(1.0, t.border_default); |
| 620 |
|
| 621 |
|
| 622 |
let rounding = egui::CornerRadius::same(t.rounding as u8); |
| 623 |
visuals.widgets.noninteractive.corner_radius = rounding; |
| 624 |
visuals.widgets.inactive.corner_radius = rounding; |
| 625 |
visuals.widgets.hovered.corner_radius = rounding; |
| 626 |
visuals.widgets.active.corner_radius = rounding; |
| 627 |
visuals.widgets.open.corner_radius = rounding; |
| 628 |
|
| 629 |
|
| 630 |
visuals.widgets.inactive.bg_stroke = egui::Stroke::new(0.5, lerp_color(t.border_default, t.bg_secondary, 0.3)); |
| 631 |
visuals.widgets.hovered.bg_stroke = egui::Stroke::new(1.0, t.border_default); |
| 632 |
visuals.widgets.active.bg_stroke = egui::Stroke::new(1.0, t.accent_blue); |
| 633 |
|
| 634 |
|
| 635 |
visuals.widgets.noninteractive.bg_stroke = egui::Stroke::new(0.5, lerp_color(t.border_default, t.bg_secondary, 0.4)); |
| 636 |
|
| 637 |
|
| 638 |
visuals.widgets.hovered.expansion = 1.0; |
| 639 |
visuals.widgets.active.expansion = 0.0; |
| 640 |
|
| 641 |
let spacing_x = t.item_spacing_x; |
| 642 |
let spacing_y = t.item_spacing_y; |
| 643 |
let btn_pad_x = t.button_padding_x; |
| 644 |
let btn_pad_y = t.button_padding_y; |
| 645 |
let win_margin = t.window_margin; |
| 646 |
let indent = t.indent; |
| 647 |
drop(t); |
| 648 |
ctx.set_visuals(visuals); |
| 649 |
|
| 650 |
|
| 651 |
let mut style = (*ctx.global_style()).clone(); |
| 652 |
style.spacing.item_spacing = egui::vec2(spacing_x, spacing_y); |
| 653 |
style.spacing.button_padding = egui::vec2(btn_pad_x, btn_pad_y); |
| 654 |
style.spacing.window_margin = egui::vec2(win_margin, win_margin).into(); |
| 655 |
style.spacing.indent = indent; |
| 656 |
ctx.set_global_style(style); |
| 657 |
} |
| 658 |
|
| 659 |
#[cfg(test)] |
| 660 |
mod tests { |
| 661 |
use super::*; |
| 662 |
use std::collections::HashMap; |
| 663 |
|
| 664 |
|
| 665 |
|
| 666 |
|
| 667 |
|
| 668 |
#[test] |
| 669 |
fn contrast_color_black_bg_returns_white() { |
| 670 |
assert_eq!(contrast_color(Color32::BLACK), Color32::WHITE); |
| 671 |
} |
| 672 |
|
| 673 |
#[test] |
| 674 |
fn contrast_color_white_bg_returns_black() { |
| 675 |
assert_eq!(contrast_color(Color32::WHITE), Color32::BLACK); |
| 676 |
} |
| 677 |
|
| 678 |
#[test] |
| 679 |
fn contrast_color_dark_blue_returns_white() { |
| 680 |
|
| 681 |
|
| 682 |
assert_eq!(contrast_color(Color32::from_rgb(0x0a, 0x84, 0xff)), Color32::WHITE); |
| 683 |
} |
| 684 |
|
| 685 |
#[test] |
| 686 |
fn contrast_color_bright_yellow_returns_black() { |
| 687 |
assert_eq!(contrast_color(Color32::from_rgb(0xff, 0xd6, 0x0a)), Color32::BLACK); |
| 688 |
} |
| 689 |
|
| 690 |
|
| 691 |
|
| 692 |
|
| 693 |
|
| 694 |
#[test] |
| 695 |
fn lerp_color_t0_returns_a() { |
| 696 |
let a = Color32::from_rgb(100, 150, 200); |
| 697 |
let b = Color32::from_rgb(200, 50, 0); |
| 698 |
assert_eq!(lerp_color(a, b, 0.0), a); |
| 699 |
} |
| 700 |
|
| 701 |
#[test] |
| 702 |
fn lerp_color_t1_returns_b() { |
| 703 |
let a = Color32::from_rgb(100, 150, 200); |
| 704 |
let b = Color32::from_rgb(200, 50, 0); |
| 705 |
assert_eq!(lerp_color(a, b, 1.0), b); |
| 706 |
} |
| 707 |
|
| 708 |
#[test] |
| 709 |
fn lerp_color_midpoint() { |
| 710 |
let a = Color32::from_rgb(0, 0, 0); |
| 711 |
let b = Color32::from_rgb(100, 200, 50); |
| 712 |
let mid = lerp_color(a, b, 0.5); |
| 713 |
assert_eq!(mid, Color32::from_rgb(50, 100, 25)); |
| 714 |
} |
| 715 |
|
| 716 |
#[test] |
| 717 |
fn lerp_color_quarter() { |
| 718 |
let a = Color32::from_rgb(0, 0, 0); |
| 719 |
let b = Color32::from_rgb(100, 200, 40); |
| 720 |
let result = lerp_color(a, b, 0.25); |
| 721 |
assert_eq!(result, Color32::from_rgb(25, 50, 10)); |
| 722 |
} |
| 723 |
|
| 724 |
#[test] |
| 725 |
fn lerp_color_identical_returns_same() { |
| 726 |
let c = Color32::from_rgb(42, 42, 42); |
| 727 |
assert_eq!(lerp_color(c, c, 0.5), c); |
| 728 |
} |
| 729 |
|
| 730 |
#[test] |
| 731 |
fn lerp_color_black_to_white() { |
| 732 |
let black = Color32::from_rgb(0, 0, 0); |
| 733 |
let white = Color32::from_rgb(255, 255, 255); |
| 734 |
let mid = lerp_color(black, white, 0.5); |
| 735 |
assert_eq!(mid, Color32::from_rgb(128, 128, 128)); |
| 736 |
} |
| 737 |
|
| 738 |
#[test] |
| 739 |
fn lerp_color_white_to_black() { |
| 740 |
let black = Color32::from_rgb(0, 0, 0); |
| 741 |
let white = Color32::from_rgb(255, 255, 255); |
| 742 |
let mid = lerp_color(white, black, 0.5); |
| 743 |
assert_eq!(mid, Color32::from_rgb(128, 128, 128)); |
| 744 |
} |
| 745 |
|
| 746 |
|
| 747 |
|
| 748 |
|
| 749 |
|
| 750 |
#[test] |
| 751 |
fn parse_hex_valid() { |
| 752 |
assert_eq!(parse_hex("#ff0000"), Some(Color32::from_rgb(255, 0, 0))); |
| 753 |
assert_eq!(parse_hex("#00ff00"), Some(Color32::from_rgb(0, 255, 0))); |
| 754 |
assert_eq!(parse_hex("#0000ff"), Some(Color32::from_rgb(0, 0, 255))); |
| 755 |
} |
| 756 |
|
| 757 |
#[test] |
| 758 |
fn parse_hex_black_and_white() { |
| 759 |
assert_eq!(parse_hex("#000000"), Some(Color32::from_rgb(0, 0, 0))); |
| 760 |
assert_eq!(parse_hex("#ffffff"), Some(Color32::from_rgb(255, 255, 255))); |
| 761 |
} |
| 762 |
|
| 763 |
#[test] |
| 764 |
fn parse_hex_uppercase() { |
| 765 |
assert_eq!(parse_hex("#FF8800"), Some(Color32::from_rgb(255, 136, 0))); |
| 766 |
} |
| 767 |
|
| 768 |
#[test] |
| 769 |
fn parse_hex_mixed_case() { |
| 770 |
assert_eq!(parse_hex("#aAbBcC"), Some(Color32::from_rgb(0xAA, 0xBB, 0xCC))); |
| 771 |
} |
| 772 |
|
| 773 |
#[test] |
| 774 |
fn parse_hex_missing_hash() { |
| 775 |
assert_eq!(parse_hex("ff0000"), None); |
| 776 |
} |
| 777 |
|
| 778 |
#[test] |
| 779 |
fn parse_hex_too_short() { |
| 780 |
assert_eq!(parse_hex("#fff"), None); |
| 781 |
} |
| 782 |
|
| 783 |
#[test] |
| 784 |
fn parse_hex_too_long() { |
| 785 |
assert_eq!(parse_hex("#ff00ff00"), None); |
| 786 |
} |
| 787 |
|
| 788 |
#[test] |
| 789 |
fn parse_hex_invalid_digits() { |
| 790 |
assert_eq!(parse_hex("#gggggg"), None); |
| 791 |
} |
| 792 |
|
| 793 |
#[test] |
| 794 |
fn parse_hex_empty_string() { |
| 795 |
assert_eq!(parse_hex(""), None); |
| 796 |
} |
| 797 |
|
| 798 |
#[test] |
| 799 |
fn parse_hex_just_hash() { |
| 800 |
assert_eq!(parse_hex("#"), None); |
| 801 |
} |
| 802 |
|
| 803 |
#[test] |
| 804 |
fn parse_hex_specific_theme_color() { |
| 805 |
|
| 806 |
assert_eq!(parse_hex("#1a1b26"), Some(Color32::from_rgb(0x1a, 0x1b, 0x26))); |
| 807 |
} |
| 808 |
|
| 809 |
|
| 810 |
|
| 811 |
|
| 812 |
|
| 813 |
#[test] |
| 814 |
fn get_color_found() { |
| 815 |
let mut map = HashMap::new(); |
| 816 |
map.insert("accent.blue".to_string(), "#7aa2f7".to_string()); |
| 817 |
assert_eq!( |
| 818 |
get_color(&map, "accent.blue"), |
| 819 |
Some(Color32::from_rgb(0x7a, 0xa2, 0xf7)) |
| 820 |
); |
| 821 |
} |
| 822 |
|
| 823 |
#[test] |
| 824 |
fn get_color_missing_key() { |
| 825 |
let map = HashMap::new(); |
| 826 |
assert_eq!(get_color(&map, "accent.blue"), None); |
| 827 |
} |
| 828 |
|
| 829 |
#[test] |
| 830 |
fn get_color_invalid_value() { |
| 831 |
let mut map = HashMap::new(); |
| 832 |
map.insert("accent.blue".to_string(), "not-a-color".to_string()); |
| 833 |
assert_eq!(get_color(&map, "accent.blue"), None); |
| 834 |
} |
| 835 |
|
| 836 |
#[test] |
| 837 |
fn get_color_empty_value() { |
| 838 |
let mut map = HashMap::new(); |
| 839 |
map.insert("bg.primary".to_string(), String::new()); |
| 840 |
assert_eq!(get_color(&map, "bg.primary"), None); |
| 841 |
} |
| 842 |
|
| 843 |
|
| 844 |
|
| 845 |
|
| 846 |
|
| 847 |
fn full_theme_toml() -> &'static str { |
| 848 |
r##" |
| 849 |
[meta] |
| 850 |
name = "Test Theme" |
| 851 |
variant = "dark" |
| 852 |
|
| 853 |
[background] |
| 854 |
primary = "#1a1b26" |
| 855 |
secondary = "#16161e" |
| 856 |
tertiary = "#283457" |
| 857 |
surface = "#1a1b26" |
| 858 |
|
| 859 |
[foreground] |
| 860 |
primary = "#c0caf5" |
| 861 |
secondary = "#a9b1d6" |
| 862 |
muted = "#565f89" |
| 863 |
|
| 864 |
[accent] |
| 865 |
red = "#f7768e" |
| 866 |
green = "#9ece6a" |
| 867 |
blue = "#7aa2f7" |
| 868 |
yellow = "#e0af68" |
| 869 |
purple = "#9d7cd8" |
| 870 |
cyan = "#7dcfff" |
| 871 |
|
| 872 |
[border] |
| 873 |
default = "#3b4261" |
| 874 |
"## |
| 875 |
} |
| 876 |
|
| 877 |
#[test] |
| 878 |
fn parse_theme_full() { |
| 879 |
let theme = parse_theme(full_theme_toml()).unwrap(); |
| 880 |
assert_eq!(theme.bg_primary, Color32::from_rgb(0x1a, 0x1b, 0x26)); |
| 881 |
assert_eq!(theme.bg_secondary, Color32::from_rgb(0x16, 0x16, 0x1e)); |
| 882 |
assert_eq!(theme.bg_tertiary, Color32::from_rgb(0x28, 0x34, 0x57)); |
| 883 |
assert_eq!(theme.bg_surface, Color32::from_rgb(0x1a, 0x1b, 0x26)); |
| 884 |
assert_eq!(theme.fg_primary, Color32::from_rgb(0xc0, 0xca, 0xf5)); |
| 885 |
assert_eq!(theme.fg_secondary, Color32::from_rgb(0xa9, 0xb1, 0xd6)); |
| 886 |
assert_eq!(theme.fg_muted, Color32::from_rgb(0x56, 0x5f, 0x89)); |
| 887 |
assert_eq!(theme.accent_red, Color32::from_rgb(0xf7, 0x76, 0x8e)); |
| 888 |
assert_eq!(theme.accent_green, Color32::from_rgb(0x9e, 0xce, 0x6a)); |
| 889 |
assert_eq!(theme.accent_blue, Color32::from_rgb(0x7a, 0xa2, 0xf7)); |
| 890 |
assert_eq!(theme.accent_yellow, Color32::from_rgb(0xe0, 0xaf, 0x68)); |
| 891 |
assert_eq!(theme.accent_purple, Color32::from_rgb(0x9d, 0x7c, 0xd8)); |
| 892 |
assert_eq!(theme.accent_cyan, Color32::from_rgb(0x7d, 0xcf, 0xff)); |
| 893 |
assert_eq!(theme.border_default, Color32::from_rgb(0x3b, 0x42, 0x61)); |
| 894 |
} |
| 895 |
|
| 896 |
#[test] |
| 897 |
fn parse_theme_missing_sections_uses_defaults() { |
| 898 |
|
| 899 |
let toml = r##" |
| 900 |
[background] |
| 901 |
primary = "#112233" |
| 902 |
"##; |
| 903 |
let theme = parse_theme(toml).unwrap(); |
| 904 |
assert_eq!(theme.bg_primary, Color32::from_rgb(0x11, 0x22, 0x33)); |
| 905 |
|
| 906 |
assert_eq!(theme.bg_secondary, Color32::BLACK); |
| 907 |
|
| 908 |
assert_eq!(theme.fg_primary, Color32::WHITE); |
| 909 |
assert_eq!(theme.fg_secondary, Color32::WHITE); |
| 910 |
|
| 911 |
assert_eq!(theme.fg_muted, Color32::GRAY); |
| 912 |
|
| 913 |
assert_eq!(theme.accent_red, Color32::RED); |
| 914 |
assert_eq!(theme.accent_green, Color32::GREEN); |
| 915 |
assert_eq!(theme.accent_blue, Color32::BLUE); |
| 916 |
assert_eq!(theme.accent_yellow, Color32::YELLOW); |
| 917 |
assert_eq!(theme.border_default, Color32::DARK_GRAY); |
| 918 |
} |
| 919 |
|
| 920 |
#[test] |
| 921 |
fn parse_theme_empty_toml() { |
| 922 |
|
| 923 |
let theme = parse_theme("").unwrap(); |
| 924 |
assert_eq!(theme.bg_primary, Color32::BLACK); |
| 925 |
assert_eq!(theme.fg_primary, Color32::WHITE); |
| 926 |
assert_eq!(theme.accent_red, Color32::RED); |
| 927 |
} |
| 928 |
|
| 929 |
#[test] |
| 930 |
fn parse_theme_invalid_toml() { |
| 931 |
assert!(parse_theme("this is not [valid toml [[[").is_err()); |
| 932 |
} |
| 933 |
|
| 934 |
#[test] |
| 935 |
fn parse_theme_partial_accent() { |
| 936 |
let toml = r##" |
| 937 |
[accent] |
| 938 |
red = "#ff0000" |
| 939 |
blue = "#0000ff" |
| 940 |
"##; |
| 941 |
let theme = parse_theme(toml).unwrap(); |
| 942 |
assert_eq!(theme.accent_red, Color32::from_rgb(255, 0, 0)); |
| 943 |
assert_eq!(theme.accent_blue, Color32::from_rgb(0, 0, 255)); |
| 944 |
|
| 945 |
assert_eq!(theme.accent_green, Color32::GREEN); |
| 946 |
} |
| 947 |
|
| 948 |
#[test] |
| 949 |
fn parse_theme_ignores_extra_sections() { |
| 950 |
let toml = r##" |
| 951 |
[meta] |
| 952 |
name = "Extra" |
| 953 |
variant = "dark" |
| 954 |
|
| 955 |
[background] |
| 956 |
primary = "#aabbcc" |
| 957 |
|
| 958 |
[custom_section] |
| 959 |
foo = "bar" |
| 960 |
"##; |
| 961 |
let theme = parse_theme(toml).unwrap(); |
| 962 |
assert_eq!(theme.bg_primary, Color32::from_rgb(0xaa, 0xbb, 0xcc)); |
| 963 |
} |
| 964 |
|
| 965 |
#[test] |
| 966 |
fn parse_theme_invalid_hex_in_field_uses_fallback() { |
| 967 |
let toml = r##" |
| 968 |
[background] |
| 969 |
primary = "not-a-color" |
| 970 |
secondary = "#16161e" |
| 971 |
"##; |
| 972 |
let theme = parse_theme(toml).unwrap(); |
| 973 |
|
| 974 |
assert_eq!(theme.bg_primary, Color32::BLACK); |
| 975 |
|
| 976 |
assert_eq!(theme.bg_secondary, Color32::from_rgb(0x16, 0x16, 0x1e)); |
| 977 |
} |
| 978 |
|
| 979 |
|
| 980 |
|
| 981 |
|
| 982 |
|
| 983 |
#[test] |
| 984 |
fn classification_color_known_classes() { |
| 985 |
assert_eq!(classification_color("kick"), Color32::from_rgb(0xE0, 0x50, 0x50)); |
| 986 |
assert_eq!(classification_color("snare"), Color32::from_rgb(0xE0, 0x90, 0x40)); |
| 987 |
assert_eq!(classification_color("hihat"), Color32::from_rgb(0xE0, 0xD0, 0x40)); |
| 988 |
assert_eq!(classification_color("cymbal"), Color32::from_rgb(0xC8, 0xC8, 0x50)); |
| 989 |
assert_eq!(classification_color("percussion"), Color32::from_rgb(0xD0, 0x70, 0xB0)); |
| 990 |
assert_eq!(classification_color("bass"), Color32::from_rgb(0x50, 0x80, 0xE0)); |
| 991 |
assert_eq!(classification_color("vocal"), Color32::from_rgb(0x50, 0xC0, 0xE0)); |
| 992 |
assert_eq!(classification_color("synth"), Color32::from_rgb(0xA0, 0x60, 0xE0)); |
| 993 |
assert_eq!(classification_color("pad"), Color32::from_rgb(0x60, 0xB0, 0x60)); |
| 994 |
assert_eq!(classification_color("misc"), Color32::from_rgb(0xE0, 0x60, 0x80)); |
| 995 |
assert_eq!(classification_color("noise"), Color32::from_rgb(0x90, 0x90, 0x90)); |
| 996 |
assert_eq!(classification_color("music"), Color32::from_rgb(0x70, 0xC0, 0xA0)); |
| 997 |
} |
| 998 |
|
| 999 |
#[test] |
| 1000 |
fn classification_color_unknown_falls_back_to_text_secondary() { |
| 1001 |
|
| 1002 |
let fallback = classification_color("unknown_class"); |
| 1003 |
assert_eq!(fallback, text_secondary()); |
| 1004 |
} |
| 1005 |
|
| 1006 |
#[test] |
| 1007 |
fn classification_color_empty_string_falls_back() { |
| 1008 |
let fallback = classification_color(""); |
| 1009 |
assert_eq!(fallback, text_secondary()); |
| 1010 |
} |
| 1011 |
|
| 1012 |
#[test] |
| 1013 |
fn classification_color_case_sensitive() { |
| 1014 |
|
| 1015 |
let result = classification_color("Kick"); |
| 1016 |
assert_eq!(result, text_secondary()); |
| 1017 |
} |
| 1018 |
|
| 1019 |
|
| 1020 |
|
| 1021 |
|
| 1022 |
|
| 1023 |
#[test] |
| 1024 |
fn theme_colors_default_is_audiofiles() { |
| 1025 |
let d = ThemeColors::default(); |
| 1026 |
assert_eq!(d.bg_primary, Color32::from_rgb(0x00, 0x00, 0x00)); |
| 1027 |
assert_eq!(d.fg_primary, Color32::from_rgb(0xff, 0xff, 0xff)); |
| 1028 |
assert_eq!(d.accent_blue, Color32::from_rgb(0x0a, 0x84, 0xff)); |
| 1029 |
assert_eq!(d.border_default, Color32::from_rgb(0x33, 0x33, 0x33)); |
| 1030 |
} |
| 1031 |
|
| 1032 |
|
| 1033 |
|
| 1034 |
|
| 1035 |
|
| 1036 |
#[test] |
| 1037 |
fn all_bundled_themes_parse_successfully() { |
| 1038 |
for (id, content) in BUNDLED_THEMES { |
| 1039 |
let result = parse_theme(content); |
| 1040 |
assert!(result.is_ok(), "Bundled theme '{id}' failed to parse: {:?}", result.err()); |
| 1041 |
} |
| 1042 |
} |
| 1043 |
|
| 1044 |
#[test] |
| 1045 |
fn all_bundled_themes_have_valid_meta() { |
| 1046 |
for (id, content) in BUNDLED_THEMES { |
| 1047 |
let table: toml::Table = content.parse() |
| 1048 |
.unwrap_or_else(|e| panic!("Bundled theme '{id}' is invalid TOML: {e}")); |
| 1049 |
let meta = theme_common::parse_meta(id, &table, false); |
| 1050 |
assert!(!meta.name.is_empty(), "Bundled theme '{id}' has empty name"); |
| 1051 |
assert!(!meta.variant.is_empty(), "Bundled theme '{id}' has empty variant"); |
| 1052 |
} |
| 1053 |
} |
| 1054 |
|
| 1055 |
#[test] |
| 1056 |
fn bundled_theme_colors_are_not_all_black() { |
| 1057 |
|
| 1058 |
for (id, content) in BUNDLED_THEMES { |
| 1059 |
let theme = parse_theme(content).unwrap(); |
| 1060 |
let all_black = theme.bg_primary == Color32::BLACK |
| 1061 |
&& theme.fg_primary == Color32::BLACK |
| 1062 |
&& theme.accent_blue == Color32::BLACK; |
| 1063 |
assert!(!all_black, "Bundled theme '{id}' parsed to all-black colors"); |
| 1064 |
} |
| 1065 |
} |
| 1066 |
} |
| 1067 |
|