| 258 |
258 |
|
dirs::config_dir().map(|c| c.join("audiofiles").join("themes"))
|
| 259 |
259 |
|
}
|
| 260 |
260 |
|
|
|
261 |
+ |
/// Accessibility tier for a theme's muted (tertiary) text, by WCAG contrast of
|
|
262 |
+ |
/// `fg_muted` against the theme's darkest and lightest panel backgrounds. We
|
|
263 |
+ |
/// surface this in the picker rather than overriding curated palettes' colors,
|
|
264 |
+ |
/// so users can choose informed while the themes keep their identity.
|
|
265 |
+ |
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
|
266 |
+ |
pub enum ContrastTier {
|
|
267 |
+ |
/// Muted text below the 3:1 WCAG floor for large text / UI components.
|
|
268 |
+ |
Low,
|
|
269 |
+ |
/// Muted text meets 3:1 but not the 4.5:1 normal-text bar.
|
|
270 |
+ |
Standard,
|
|
271 |
+ |
/// Muted text meets WCAG AA (>= 4.5:1) on every panel surface.
|
|
272 |
+ |
High,
|
|
273 |
+ |
}
|
|
274 |
+ |
|
|
275 |
+ |
impl ContrastTier {
|
|
276 |
+ |
/// Short badge shown next to the theme name.
|
|
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 |
+ |
/// Load a theme's full color set by id (bundled or custom on disk).
|
|
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 |
+ |
/// Compute the muted-text contrast tier for a theme id. Defaults to `Standard`
|
|
317 |
+ |
/// if the theme can't be loaded.
|
|
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 |
+ |
|
| 261 |
333 |
|
/// List all available themes (bundled + custom). Custom themes override bundled by ID.
|
| 262 |
334 |
|
pub fn list_themes() -> Vec<ThemeMeta> {
|
| 263 |
335 |
|
let mut themes: Vec<ThemeMeta> = Vec::new();
|
| 529 |
601 |
|
visuals.faint_bg_color = lerp_color(t.bg_primary, t.bg_secondary, 0.3);
|
| 530 |
602 |
|
|
| 531 |
603 |
|
visuals.selection.bg_fill = lerp_color(t.bg_primary, t.accent_blue, 0.3);
|
| 532 |
|
- |
visuals.selection.stroke = egui::Stroke::new(1.0, contrast_color(lerp_color(t.bg_primary, t.accent_blue, 0.3)));
|
|
604 |
+ |
// Keyboard-focus / selection outline: use the dedicated FOCUS stroke width
|
|
605 |
+ |
// (1.5) and the saturated accent so focused widgets are clearly ringed,
|
|
606 |
+ |
// rather than egui's near-invisible default 1px contrast hairline.
|
|
607 |
+ |
visuals.selection.stroke = egui::Stroke::new(stroke::FOCUS, t.accent_blue);
|
| 533 |
608 |
|
|
| 534 |
609 |
|
visuals.widgets.noninteractive.bg_fill = t.bg_secondary;
|
| 535 |
610 |
|
visuals.widgets.inactive.bg_fill = lerp_color(t.bg_secondary, t.bg_tertiary, 0.3);
|