use eframe::egui::{self, Color32, Stroke};
use crate::theme::ThemeSpec;
#[derive(Clone, Copy)]
pub struct GuiTheme {
pub bg: Color32,
pub panel: Color32,
pub text: Color32,
pub dim: Color32,
pub accent: Color32,
pub ok: Color32,
pub err: Color32,
pub subst: Color32,
pub pending: Color32,
pub computed: Color32,
pub select_bg: Color32,
pub select_fg: Color32,
pub line: Color32,
field: Color32,
raised: Color32,
sunken: Color32,
}
fn c([r, g, b]: [u8; 3]) -> Color32 {
Color32::from_rgb(r, g, b)
}
pub fn from_ratatui(color: ratatui::style::Color, fallback: Color32) -> Color32 {
match color {
ratatui::style::Color::Rgb(r, g, b) => Color32::from_rgb(r, g, b),
_ => fallback,
}
}
fn is_light(c: Color32) -> bool {
let f = |v: u8| {
let v = v as f32 / 255.0;
if v <= 0.04045 {
v / 12.92
} else {
((v + 0.055) / 1.055).powf(2.4)
}
};
0.2126 * f(c.r()) + 0.7152 * f(c.g()) + 0.0722 * f(c.b()) > 0.5
}
fn mix(a: Color32, b: Color32, t: f32) -> Color32 {
let l = |x: u8, y: u8| (x as f32 * (1.0 - t) + y as f32 * t).round() as u8;
Color32::from_rgb(l(a.r(), b.r()), l(a.g(), b.g()), l(a.b(), b.b()))
}
impl GuiTheme {
pub fn from_spec(s: &ThemeSpec) -> Self {
Self {
bg: c(s.bg),
panel: c(s.panel),
text: c(s.text),
dim: c(s.dim),
accent: c(s.accent),
ok: c(s.ok),
err: c(s.err),
subst: c(s.subst),
pending: c(s.pending),
computed: c(s.computed),
select_bg: c(s.select_bg),
select_fg: c(s.select_fg),
line: c(s.line),
field: c(s.field),
raised: c(s.raised),
sunken: c(s.sunken),
}
}
pub fn raised(&self) -> Color32 {
self.raised
}
pub fn sunken(&self) -> Color32 {
self.sunken
}
pub fn field(&self) -> Color32 {
self.field
}
pub fn apply(&self, ctx: &egui::Context) {
let mut visuals = if is_light(self.bg) {
egui::Visuals::light()
} else {
egui::Visuals::dark()
};
visuals.override_text_color = Some(self.text);
visuals.window_fill = self.bg;
visuals.panel_fill = self.panel;
visuals.faint_bg_color = self.raised();
visuals.extreme_bg_color = self.sunken();
visuals.text_edit_bg_color = Some(self.field());
visuals.hyperlink_color = self.accent;
visuals.selection.bg_fill = self.select_bg;
visuals.selection.stroke = Stroke::new(1.0, self.select_fg);
visuals.window_stroke = Stroke::new(1.0, self.line);
let w = &mut visuals.widgets;
w.noninteractive.bg_fill = self.panel;
w.noninteractive.weak_bg_fill = self.panel;
w.noninteractive.fg_stroke = Stroke::new(1.0, self.dim);
w.noninteractive.bg_stroke = Stroke::new(1.0, self.line);
w.inactive.bg_fill = self.raised();
w.inactive.weak_bg_fill = self.raised();
w.inactive.fg_stroke = Stroke::new(1.0, self.text);
w.inactive.bg_stroke = Stroke::new(1.0, self.line);
w.hovered.bg_fill = mix(self.panel, self.accent, 0.30);
w.hovered.weak_bg_fill = mix(self.panel, self.accent, 0.22);
w.hovered.fg_stroke = Stroke::new(1.0, self.text);
w.hovered.bg_stroke = Stroke::new(1.0, self.accent);
w.active.bg_fill = mix(self.panel, self.accent, 0.45);
w.active.weak_bg_fill = mix(self.panel, self.accent, 0.35);
w.active.fg_stroke = Stroke::new(1.0, self.text);
w.active.bg_stroke = Stroke::new(1.0, self.accent);
w.open.bg_fill = self.raised();
w.open.fg_stroke = Stroke::new(1.0, self.text);
ctx.all_styles_mut(|style| {
style.visuals = visuals.clone();
style.spacing.item_spacing = egui::vec2(5.0, 5.0);
style.spacing.button_padding = egui::vec2(7.0, 3.0);
});
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_surfaces_come_from_the_theme_rather_than_from_panel() {
for spec in crate::theme::builtin_presets() {
let th = GuiTheme::from_spec(&spec);
assert_eq!(th.field(), c(spec.field), "{}: field", spec.name);
assert_eq!(th.raised(), c(spec.raised), "{}: raised", spec.name);
assert_eq!(th.sunken(), c(spec.sunken), "{}: sunken", spec.name);
assert_ne!(
th.field(),
th.raised(),
"{}: a field and a button are not the same colour",
spec.name
);
}
}
#[test]
fn the_base_visuals_follow_the_themes_own_ground() {
let light = crate::theme::builtin_presets()
.into_iter()
.find(|p| p.name == crate::theme::PRESET_DAYLIGHT)
.expect("the light preset is offered");
for (spec, expect_dark) in [(crate::theme::default_preset(), true), (light, false)] {
let ctx = egui::Context::default();
GuiTheme::from_spec(&spec).apply(&ctx);
let mut dark = None;
let _ = ctx.run_ui(egui::RawInput::default(), |ui| {
dark = Some(ui.visuals().dark_mode);
});
assert_eq!(dark, Some(expect_dark), "{}", spec.name);
}
}
}
pub fn method_color(method: &str, fallback: Color32) -> Color32 {
match crate::hurl::method_rgb(method) {
Some((r, g, b)) => Color32::from_rgb(r, g, b),
None => fallback,
}
}