use retroglyph_core::color::{Color, Style};
use crate::Response;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Theme {
pub bg: Color,
pub panel_bg: Color,
pub border: Color,
pub title_bg: Color,
pub fg: Color,
pub accent: Color,
pub hover_bg: Color,
pub press_bg: Color,
pub dim: Color,
}
impl Theme {
pub const DARK: Self = Self {
bg: Color::Rgb {
r: 16,
g: 16,
b: 24,
},
panel_bg: Color::Rgb {
r: 22,
g: 22,
b: 32,
},
border: Color::Rgb {
r: 70,
g: 74,
b: 96,
},
title_bg: Color::Rgb {
r: 30,
g: 32,
b: 48,
},
fg: Color::Rgb {
r: 190,
g: 192,
b: 208,
},
accent: Color::Rgb {
r: 90,
g: 170,
b: 250,
},
hover_bg: Color::Rgb {
r: 40,
g: 44,
b: 64,
},
press_bg: Color::Rgb {
r: 60,
g: 110,
b: 170,
},
dim: Color::Rgb {
r: 110,
g: 112,
b: 130,
},
};
pub const LIGHT: Self = Self {
bg: Color::Rgb {
r: 240,
g: 240,
b: 246,
},
panel_bg: Color::Rgb {
r: 255,
g: 255,
b: 255,
},
border: Color::Rgb {
r: 160,
g: 164,
b: 180,
},
title_bg: Color::Rgb {
r: 224,
g: 226,
b: 240,
},
fg: Color::Rgb {
r: 20,
g: 22,
b: 32,
},
accent: Color::Rgb {
r: 20,
g: 100,
b: 210,
},
hover_bg: Color::Rgb {
r: 230,
g: 236,
b: 248,
},
press_bg: Color::Rgb {
r: 160,
g: 194,
b: 240,
},
dim: Color::Rgb {
r: 130,
g: 132,
b: 150,
},
};
#[must_use]
pub const fn bg_for<Id>(&self, response: &Response<Id>, base: Color) -> Color {
if response.pressed() {
self.press_bg
} else if response.hovered() {
self.hover_bg
} else {
base
}
}
#[must_use]
pub const fn fg_for<Id>(&self, response: &Response<Id>) -> Color {
if response.pressed() || response.focused() {
self.accent
} else if response.hovered() {
self.fg
} else {
self.dim
}
}
#[must_use]
pub fn style_for<Id>(&self, response: &Response<Id>, base: Color) -> Style {
if response.disabled() {
return Style::new().fg(self.dim).bg(base);
}
if response.pressed() {
return Style::new().fg(self.accent).bg(self.press_bg);
}
if response.focused() {
return Style::new().fg(self.accent).bg(base);
}
if response.hovered() {
return Style::new().fg(self.fg).bg(self.hover_bg);
}
Style::new().fg(self.dim).bg(base)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dark_and_light_are_distinct() {
assert_ne!(Theme::DARK, Theme::LIGHT);
}
#[cfg(feature = "serde")]
#[test]
fn serializes_and_deserializes() {
let json = serde_json::to_string(&Theme::DARK).expect("serialize");
let round_tripped: Theme = serde_json::from_str(&json).expect("deserialize");
assert_eq!(round_tripped, Theme::DARK);
}
#[test]
fn bg_for_is_base_when_idle() {
let theme = Theme::DARK;
let response: Response<()> = Response::default();
assert_eq!(theme.bg_for(&response, theme.panel_bg), theme.panel_bg);
}
#[test]
fn bg_for_is_hover_bg_when_hovered() {
let theme = Theme::DARK;
let response: Response<()> = Response {
hovered: true,
..Response::default()
};
assert_eq!(theme.bg_for(&response, theme.panel_bg), theme.hover_bg);
}
#[test]
fn bg_for_prefers_press_bg_over_hover_bg() {
let theme = Theme::DARK;
let response: Response<()> = Response {
hovered: true,
pressed: true,
..Response::default()
};
assert_eq!(theme.bg_for(&response, theme.panel_bg), theme.press_bg);
}
#[test]
fn fg_for_is_dim_when_idle() {
let theme = Theme::DARK;
let response: Response<()> = Response::default();
assert_eq!(theme.fg_for(&response), theme.dim);
}
#[test]
fn fg_for_is_fg_when_hovered() {
let theme = Theme::DARK;
let response: Response<()> = Response {
hovered: true,
..Response::default()
};
assert_eq!(theme.fg_for(&response), theme.fg);
}
#[test]
fn fg_for_prefers_accent_over_hover_when_focused() {
let theme = Theme::DARK;
let response: Response<()> = Response {
hovered: true,
focused: true,
..Response::default()
};
assert_eq!(theme.fg_for(&response), theme.accent);
}
#[test]
fn fg_for_prefers_accent_over_hover_when_pressed() {
let theme = Theme::DARK;
let response: Response<()> = Response {
hovered: true,
pressed: true,
..Response::default()
};
assert_eq!(theme.fg_for(&response), theme.accent);
}
#[test]
fn style_for_is_dim_on_base_when_idle() {
let theme = Theme::DARK;
let response: Response<()> = Response::default();
let style = theme.style_for(&response, theme.panel_bg);
assert_eq!(style.foreground(), theme.dim);
assert_eq!(style.background(), theme.panel_bg);
}
#[test]
fn style_for_is_fg_on_hover_bg_when_hovered() {
let theme = Theme::DARK;
let response: Response<()> = Response {
hovered: true,
..Response::default()
};
let style = theme.style_for(&response, theme.panel_bg);
assert_eq!(style.foreground(), theme.fg);
assert_eq!(style.background(), theme.hover_bg);
}
#[test]
fn style_for_is_accent_on_base_when_focused_and_not_hovered() {
let theme = Theme::DARK;
let response: Response<()> = Response {
focused: true,
..Response::default()
};
let style = theme.style_for(&response, theme.panel_bg);
assert_eq!(style.foreground(), theme.accent);
assert_eq!(style.background(), theme.panel_bg);
}
#[test]
fn style_for_prefers_focused_over_hovered() {
let theme = Theme::DARK;
let response: Response<()> = Response {
hovered: true,
focused: true,
..Response::default()
};
let style = theme.style_for(&response, theme.panel_bg);
assert_eq!(style.foreground(), theme.accent);
assert_eq!(style.background(), theme.panel_bg);
}
#[test]
fn style_for_is_accent_on_press_bg_when_pressed() {
let theme = Theme::DARK;
let response: Response<()> = Response {
pressed: true,
focused: true,
hovered: true,
..Response::default()
};
let style = theme.style_for(&response, theme.panel_bg);
assert_eq!(style.foreground(), theme.accent);
assert_eq!(style.background(), theme.press_bg);
}
#[test]
fn style_for_prefers_disabled_over_everything_else() {
let theme = Theme::DARK;
let response: Response<()> = Response {
disabled: true,
pressed: true,
focused: true,
hovered: true,
..Response::default()
};
let style = theme.style_for(&response, theme.panel_bg);
assert_eq!(style.foreground(), theme.dim);
assert_eq!(style.background(), theme.panel_bg);
}
#[test]
fn dark_background_is_darker_than_light_background() {
let Color::Rgb {
r: dr,
g: dg,
b: db,
} = Theme::DARK.bg
else {
unreachable!()
};
let Color::Rgb {
r: lr,
g: lg,
b: lb,
} = Theme::LIGHT.bg
else {
unreachable!()
};
let dark_luma = u32::from(dr) + u32::from(dg) + u32::from(db);
let light_luma = u32::from(lr) + u32::from(lg) + u32::from(lb);
assert!(dark_luma < light_luma);
}
}