ced/src/app/theme.rs
2025-07-05 14:42:45 -04:00

197 lines
5.7 KiB
Rust

use eframe::egui;
#[derive(Debug, Clone, Copy, PartialEq, serde::Serialize, serde::Deserialize, Default)]
pub enum Theme {
#[default]
System,
Light,
Dark,
}
pub fn apply(theme: Theme, ctx: &egui::Context) {
match theme {
Theme::System => {
if let Some(system_visuals) = get_system_colors() {
ctx.set_visuals(system_visuals);
} else {
let is_dark = detect_system_dark_mode();
if is_dark {
ctx.set_visuals(egui::Visuals::dark());
} else {
ctx.set_visuals(egui::Visuals::light());
}
}
}
Theme::Light => {
ctx.set_visuals(egui::Visuals::light());
}
Theme::Dark => {
ctx.set_visuals(egui::Visuals::dark());
}
}
}
fn get_system_colors() -> Option<egui::Visuals> {
if let Some(visuals) = get_pywal_colors() {
return Some(visuals);
}
#[cfg(target_os = "linux")]
{
if let Some(visuals) = get_gtk_colors() {
return Some(visuals);
}
}
None
}
fn get_pywal_colors() -> Option<egui::Visuals> {
use std::fs;
use std::path::Path;
let home = std::env::var("HOME").ok()?;
let colors_path = Path::new(&home).join(".cache/wal/colors");
if !colors_path.exists() {
return None;
}
let colors_content = fs::read_to_string(&colors_path).ok()?;
let colors: Vec<&str> = colors_content.lines().collect();
if colors.len() < 8 {
return None;
}
let parse_color = |hex: &str| -> Option<egui::Color32> {
if hex.len() != 7 || !hex.starts_with('#') {
return None;
}
let r = u8::from_str_radix(&hex[1..3], 16).ok()?;
let g = u8::from_str_radix(&hex[3..5], 16).ok()?;
let b = u8::from_str_radix(&hex[5..7], 16).ok()?;
Some(egui::Color32::from_rgb(r, g, b))
};
let bg = parse_color(colors[0])?;
let fg = parse_color(colors.get(7).unwrap_or(&colors[0]))?;
let bg_alt = parse_color(colors.get(8).unwrap_or(&colors[0]))?;
let accent = parse_color(colors.get(1).unwrap_or(&colors[0]))?;
let secondary = parse_color(colors.get(2).unwrap_or(&colors[0]))?;
let mut visuals = if is_dark_color(bg) {
egui::Visuals::dark()
} else {
egui::Visuals::light()
};
visuals.window_fill = bg;
visuals.extreme_bg_color = bg;
visuals.code_bg_color = bg;
visuals.panel_fill = bg;
visuals.faint_bg_color = blend_colors(bg, bg_alt, 0.15);
visuals.error_fg_color = parse_color(colors.get(1).unwrap_or(&colors[0]))?;
visuals.override_text_color = Some(fg);
visuals.hyperlink_color = accent;
visuals.selection.bg_fill = blend_colors(accent, bg, 0.3);
visuals.selection.stroke.color = accent;
let separator_color = blend_colors(fg, bg, 0.3);
visuals.widgets.noninteractive.bg_stroke = egui::Stroke::new(1.0, separator_color);
visuals.widgets.noninteractive.bg_fill = bg;
visuals.widgets.noninteractive.fg_stroke.color = fg;
visuals.widgets.inactive.bg_fill = blend_colors(bg, accent, 0.2);
visuals.widgets.inactive.fg_stroke.color = fg;
visuals.widgets.inactive.bg_stroke = egui::Stroke::new(1.0, blend_colors(accent, bg, 0.4));
visuals.widgets.inactive.weak_bg_fill = blend_colors(bg, accent, 0.1);
visuals.widgets.hovered.bg_fill = blend_colors(bg, accent, 0.3);
visuals.widgets.hovered.fg_stroke.color = fg;
visuals.widgets.hovered.bg_stroke = egui::Stroke::new(1.0, accent);
visuals.widgets.hovered.weak_bg_fill = blend_colors(bg, accent, 0.15);
visuals.widgets.active.bg_fill = blend_colors(bg, accent, 0.4);
visuals.widgets.active.fg_stroke.color = fg;
visuals.widgets.active.bg_stroke = egui::Stroke::new(1.0, accent);
visuals.widgets.active.weak_bg_fill = blend_colors(bg, accent, 0.2);
visuals.window_stroke = egui::Stroke::new(1.0, separator_color);
visuals.widgets.open.bg_fill = blend_colors(bg, accent, 0.25);
visuals.widgets.open.fg_stroke.color = fg;
visuals.widgets.open.bg_stroke = egui::Stroke::new(1.0, accent);
visuals.widgets.open.weak_bg_fill = blend_colors(bg, accent, 0.15);
visuals.striped = true;
visuals.button_frame = true;
visuals.collapsing_header_frame = false;
Some(visuals)
}
fn get_gtk_colors() -> Option<egui::Visuals> {
None
}
fn is_dark_color(color: egui::Color32) -> bool {
let r = color.r() as f32 / 255.0;
let g = color.g() as f32 / 255.0;
let b = color.b() as f32 / 255.0;
let r_lin = if r <= 0.04045 {
r / 12.92
} else {
((r + 0.055) / 1.055).powf(2.4)
};
let g_lin = if g <= 0.04045 {
g / 12.92
} else {
((g + 0.055) / 1.055).powf(2.4)
};
let b_lin = if b <= 0.04045 {
b / 12.92
} else {
((b + 0.055) / 1.055).powf(2.4)
};
let luminance = 0.2126 * r_lin + 0.7152 * g_lin + 0.0722 * b_lin;
luminance < 0.5
}
fn blend_colors(base: egui::Color32, blend: egui::Color32, factor: f32) -> egui::Color32 {
let factor = factor.clamp(0.0, 1.0);
let inv_factor = 1.0 - factor;
egui::Color32::from_rgb(
(base.r() as f32 * inv_factor + blend.r() as f32 * factor) as u8,
(base.g() as f32 * inv_factor + blend.g() as f32 * factor) as u8,
(base.b() as f32 * inv_factor + blend.b() as f32 * factor) as u8,
)
}
fn detect_system_dark_mode() -> bool {
#[cfg(target_os = "windows")]
{
true
}
#[cfg(target_os = "macos")]
{
true
}
#[cfg(target_os = "linux")]
{
true
}
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
{
true
}
}