use egui::{Stroke, Visuals, style::WidgetVisuals};
use serde::{Deserialize, Serialize};
use super::oklch::{Oklch, contrast_ratio};
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
pub struct Palette {
pub dark: bool,
pub surface: Oklch,
pub on_surface: Oklch,
pub on_surface_dim: Oklch,
pub surface_container: Oklch,
pub surface_extreme: Oklch,
pub primary: Oklch,
pub on_primary: Oklch,
pub accent: Oklch,
pub outline: Oklch,
pub selection: Oklch,
pub success: Oklch,
pub on_success: Oklch,
pub warn: Oklch,
pub on_warn: Oklch,
pub error: Oklch,
pub on_error: Oklch,
pub glow: Oklch,
}
impl Palette {
fn step(&self, base: Oklch, magnitude: f32) -> Oklch {
let dir = if self.dark { 1.0 } else { -1.0 };
base.lighten(dir * magnitude)
}
fn widget(&self, lighten: f32, stroke_role: Oklch, fg_role: Oklch, radius: u8, expansion: f32) -> WidgetVisuals {
let bg = self.step(self.surface_container, lighten);
WidgetVisuals {
bg_fill: bg.to_color32(),
weak_bg_fill: self.step(self.surface_container, lighten * 0.5).to_color32(),
bg_stroke: Stroke::new(1.0, stroke_role.to_color32()),
fg_stroke: Stroke::new(1.0, fg_role.to_color32()),
corner_radius: egui::CornerRadius::same(radius),
expansion,
}
}
pub fn to_visuals(&self, radius: u8) -> Visuals {
let mut v = if self.dark { Visuals::dark() } else { Visuals::light() };
v.dark_mode = self.dark;
v.hyperlink_color = self.accent.to_color32();
v.panel_fill = self.surface.to_color32();
v.window_fill = self.surface_container.to_color32();
v.extreme_bg_color = self.surface_extreme.to_color32();
v.faint_bg_color = self.step(self.surface_container, 0.03).to_color32();
v.code_bg_color = self.surface_extreme.to_color32();
v.warn_fg_color = self.warn.to_color32();
v.error_fg_color = self.error.to_color32();
v.window_stroke = Stroke::new(1.0, self.outline.to_color32());
v.selection.bg_fill = self.selection.to_color32_alpha(90);
v.selection.stroke = Stroke::new(1.0, self.selection.to_color32());
v.widgets.noninteractive = self.widget(0.0, self.outline.with_chroma_scale(0.5), self.on_surface_dim, radius, 0.0);
v.widgets.inactive = self.widget(0.04, self.outline, self.on_surface, radius, 0.0);
v.widgets.hovered = self.widget(0.10, self.accent, self.on_surface, radius, 1.0);
v.widgets.active = self.widget(0.16, self.accent, self.on_surface, radius, 1.0);
v.widgets.open = self.widget(0.08, self.outline, self.on_surface, radius, 0.0);
v.window_corner_radius = egui::CornerRadius::same(radius);
v.menu_corner_radius = egui::CornerRadius::same(radius);
v
}
pub fn body_text_contrast(&self) -> f32 {
let on = self.on_surface.to_color32();
[self.surface, self.surface_container, self.surface_extreme]
.iter()
.map(|s| contrast_ratio(on, s.to_color32()))
.fold(f32::INFINITY, f32::min)
}
pub fn dim_text_contrast(&self) -> f32 {
let on = self.on_surface_dim.to_color32();
[self.surface, self.surface_container]
.iter()
.map(|s| contrast_ratio(on, s.to_color32()))
.fold(f32::INFINITY, f32::min)
}
pub fn ui_boundary_contrast(&self) -> f32 {
let s = self.surface.to_color32();
contrast_ratio(self.outline.to_color32(), s).min(contrast_ratio(self.accent.to_color32(), s))
}
pub fn status_contrast(&self) -> f32 {
let pairs = [
(self.on_primary, self.primary),
(self.on_success, self.success),
(self.on_warn, self.warn),
(self.on_error, self.error),
];
pairs
.iter()
.map(|(fg, bg)| contrast_ratio(fg.to_color32(), bg.to_color32()))
.fold(f32::INFINITY, f32::min)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::look::Theme;
#[test]
fn widget_states_get_progressively_lighter_on_dark() {
let v = Theme::windows_dark().palette.to_visuals(6);
let l = |c: egui::Color32| super::super::oklch::relative_luminance(c);
assert!(l(v.widgets.inactive.bg_fill) <= l(v.widgets.hovered.bg_fill));
assert!(l(v.widgets.hovered.bg_fill) <= l(v.widgets.active.bg_fill));
}
#[test]
fn no_global_override_text_color() {
let v = Theme::windows_light().palette.to_visuals(6);
assert!(v.override_text_color.is_none(), "§27: never set override_text_color globally");
}
}